Compare commits
10 Commits
6337e63a77
...
fdf4b6efcb
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf4b6efcb | |||
| a708d2977c | |||
| e7427b7464 | |||
| c74a4456af | |||
| 05d55a324c | |||
| aff831cf58 | |||
| 605e41cfeb | |||
| e0778b88ea | |||
| b57cc9894c | |||
| 5475c3ac51 |
+94
-1
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAssets", "FinlyticAssets\FinlyticAssets.csproj", "{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}"
|
||||
EndProject
|
||||
@@ -11,23 +11,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticCore", "FinlyticCor
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticNews", "FinlyticNews\FinlyticNews.csproj", "{03B4D920-6173-44E8-A1E2-8945D8393CEA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticFundamentals", "FinlyticFundamentals\FinlyticFundamentals.csproj", "{D458A1B3-16CF-45E8-859D-87542A4A83A7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticSentiment", "FinlyticSentiment\FinlyticSentiment.csproj", "{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTechnicalAnalysis", "FinlyticTechnicalAnalysis\FinlyticTechnicalAnalysis.csproj", "{A1C82F63-4482-4E99-9231-1184FA2E001F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticAnalyzer", "FinlyticAnalyzer\FinlyticAnalyzer.csproj", "{E9F7C091-62C4-417A-B981-8977DF82A1B0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticTrades", "FinlyticTrades\FinlyticTrades.csproj", "{57D84C2E-73E1-4231-A91B-6B620FCE5289}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinlyticBackend", "FinlyticBackend\FinlyticBackend.csproj", "{C1A924B8-904E-436D-B07E-4E621F51C1AA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B5AC2C29-44D5-4538-815F-F02CDDE9D01F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{03E2ECBB-0FF9-43E4-994E-F6A522860AD5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{03B4D920-6173-44E8-A1E2-8945D8393CEA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{999DB199-69C5-46D2-BDAD-59C84F5769F1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D458A1B3-16CF-45E8-859D-87542A4A83A7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9C3BB705-86AD-4A89-AA0F-A52C87A4950B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1C82F63-4482-4E99-9231-1184FA2E001F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E9F7C091-62C4-417A-B981-8977DF82A1B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{57D84C2E-73E1-4231-A91B-6B620FCE5289}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C1A924B8-904E-436D-B07E-4E621F51C1AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using FinlyticAnalyzer.Entities;
|
||||
using FinlyticAnalyzer.Services;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Controllers;
|
||||
|
||||
public class ManualAnalysisRequest
|
||||
{
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
public string Isin { get; set; } = string.Empty;
|
||||
public string Sector { get; set; } = "Technology";
|
||||
public string Headline { get; set; } = "Manual User Request";
|
||||
public decimal CurrentPrice { get; set; } = 100.0m;
|
||||
public int RiskScore { get; set; } = 50; // 0 to 100
|
||||
public int MinTimeframeValue { get; set; } = 4;
|
||||
public int MaxTimeframeValue { get; set; } = 6;
|
||||
public string TimeframeUnit { get; set; } = "Tage";
|
||||
public string InstrumentType { get; set; } = "Stock";
|
||||
public string UserNotes { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/analyze")]
|
||||
public class ManualAnalysisController : ControllerBase
|
||||
{
|
||||
private readonly IVixTrackerService _vixTracker;
|
||||
private readonly IN8nEvaluationService _n8nService;
|
||||
private readonly IWinRateCalculator _winRateCalculator;
|
||||
private readonly AnalyzerDbContext _dbContext;
|
||||
private readonly ILogger<ManualAnalysisController> _logger;
|
||||
|
||||
public ManualAnalysisController(
|
||||
IVixTrackerService vixTracker,
|
||||
IN8nEvaluationService n8nService,
|
||||
IWinRateCalculator winRateCalculator,
|
||||
AnalyzerDbContext dbContext,
|
||||
ILogger<ManualAnalysisController> logger)
|
||||
{
|
||||
_vixTracker = vixTracker;
|
||||
_n8nService = n8nService;
|
||||
_winRateCalculator = winRateCalculator;
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a manual analysis based on the provided request.
|
||||
/// </summary>
|
||||
[HttpPost("manual")]
|
||||
public async Task<IActionResult> RunManualAnalysis([FromBody] ManualAnalysisRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Symbol) && string.IsNullOrWhiteSpace(request.Isin))
|
||||
{
|
||||
return BadRequest(new { error = "Symbol or ISIN is required." });
|
||||
}
|
||||
|
||||
var regime = _vixTracker.GetCurrentRegime();
|
||||
var currentVix = _vixTracker.GetCurrentVix();
|
||||
string analysisId = Guid.NewGuid().ToString("N");
|
||||
double winRate = _winRateCalculator.CalculateWinRate(request.Sector, request.Symbol, regime);
|
||||
|
||||
string riskLabel = request.RiskScore > 70 ? $"Aggressiv ({request.RiskScore}/100)" : (request.RiskScore > 30 ? $"Balanced ({request.RiskScore}/100)" : $"Konservativ ({request.RiskScore}/100)");
|
||||
string timeframeFormatted = $"{request.MinTimeframeValue}-{request.MaxTimeframeValue} {request.TimeframeUnit}";
|
||||
|
||||
var n8nRequest = new N8nAnalysisRequestDto
|
||||
{
|
||||
RequestId = analysisId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TriggerType = "Manual",
|
||||
TargetAsset = new TargetAssetInfo
|
||||
{
|
||||
Symbol = request.Symbol.ToUpperInvariant(),
|
||||
Isin = request.Isin.ToUpperInvariant(),
|
||||
Sector = request.Sector
|
||||
},
|
||||
MarketContext = new MarketContextInfo
|
||||
{
|
||||
Vix = currentVix,
|
||||
MarketRegime = regime.ToString()
|
||||
},
|
||||
FilterContext = new FilterContextInfo
|
||||
{
|
||||
ImpactScore = 1.0,
|
||||
RawNewsHeadline = string.IsNullOrWhiteSpace(request.Headline) ? "Manual User Trigger" : request.Headline
|
||||
},
|
||||
UserPreferences = new UserPreferencesInfo
|
||||
{
|
||||
RiskScore = request.RiskScore,
|
||||
RiskTolerance = riskLabel,
|
||||
MinTimeframeValue = request.MinTimeframeValue,
|
||||
MaxTimeframeValue = request.MaxTimeframeValue,
|
||||
TimeframeUnit = request.TimeframeUnit,
|
||||
TimeframeFormatted = timeframeFormatted,
|
||||
InstrumentType = request.InstrumentType,
|
||||
UserNotes = request.UserNotes
|
||||
},
|
||||
TradeFeedback = new TradeFeedbackInfo
|
||||
{
|
||||
TotalAssetTrades = 12,
|
||||
AssetWinRate = winRate,
|
||||
AvgReturnPercent = 3.4,
|
||||
LastTradeResult = "WIN"
|
||||
}
|
||||
};
|
||||
|
||||
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
|
||||
bool shouldProceed = n8nResponse != null && string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
TradeProposalDto? proposal = null;
|
||||
if (shouldProceed && n8nResponse != null)
|
||||
{
|
||||
proposal = new TradeProposalDto
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = request.Sector,
|
||||
Symbol = request.Symbol.ToUpperInvariant(),
|
||||
Isin = request.Isin.ToUpperInvariant(),
|
||||
CompanyName = request.Symbol,
|
||||
EntryPrice = request.CurrentPrice,
|
||||
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
||||
RiskTolerance = n8nResponse.SuggestedRisk,
|
||||
Timeframe = timeframeFormatted,
|
||||
InstrumentType = request.InstrumentType,
|
||||
WinRate = winRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 60,
|
||||
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
var analysisEntity = new AnalysisEntity
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = request.Sector,
|
||||
Symbol = request.Symbol.ToUpperInvariant(),
|
||||
Isin = request.Isin.ToUpperInvariant(),
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = 1.0,
|
||||
WinRate = winRate,
|
||||
RawDataJson = JsonSerializer.Serialize(request),
|
||||
AiOutputJson = proposal != null ? JsonSerializer.Serialize(proposal) : "{}",
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
N8nEvalScore = n8nResponse?.EvalScore ?? 0,
|
||||
N8nDecision = n8nResponse?.AiDecision ?? "Rejected",
|
||||
IsTradeProposed = shouldProceed,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_dbContext.Analyses.Add(analysisEntity);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (!shouldProceed)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
analysisId,
|
||||
isTradeProposed = false,
|
||||
status = "Rejected",
|
||||
recommendation = "NOT_RECOMMENDED",
|
||||
reasoning = n8nResponse?.AiReasoning ?? "Die KI stuft diesen Trade als zu riskant ein und empfiehlt keine Positionierung.",
|
||||
n8nResponse,
|
||||
proposal = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
analysisId,
|
||||
isTradeProposed = true,
|
||||
status = "Success",
|
||||
recommendation = "RECOMMENDED",
|
||||
n8nResponse,
|
||||
proposal
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using FinlyticAnalyzer.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FinlyticAnalyzer.Database;
|
||||
|
||||
public class AnalyzerDbContext : DbContext
|
||||
{
|
||||
public AnalyzerDbContext(DbContextOptions<AnalyzerDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<AnalysisEntity> Analyses => Set<AnalysisEntity>();
|
||||
public DbSet<AnalyzerSettingsEntity> Settings => Set<AnalyzerSettingsEntity>();
|
||||
public DbSet<TradeProposalEntity> TradeProposals => Set<TradeProposalEntity>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<AnalysisEntity>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.AnalysisId).IsUnique();
|
||||
entity.HasIndex(e => e.EventId);
|
||||
entity.HasIndex(e => e.Isin);
|
||||
entity.HasIndex(e => e.Sector);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TradeProposalEntity>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.Isin);
|
||||
entity.HasIndex(e => e.ExpiresAt);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["FinlyticCore/FinlyticCore.csproj", "FinlyticCore/"]
|
||||
COPY ["FinlyticAnalyzer/FinlyticAnalyzer.csproj", "FinlyticAnalyzer/"]
|
||||
RUN dotnet restore "FinlyticAnalyzer/FinlyticAnalyzer.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/FinlyticAnalyzer"
|
||||
RUN dotnet build "FinlyticAnalyzer.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "FinlyticAnalyzer.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "FinlyticAnalyzer.dll"]
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
|
||||
namespace FinlyticAnalyzer.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted raw news, market context, AI prompt payload & response in PostgreSQL.
|
||||
/// </summary>
|
||||
[Table("analyses")]
|
||||
public class AnalysisEntity
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string AnalysisId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string EventId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
public string Sector { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Isin { get; set; } = string.Empty;
|
||||
|
||||
public VixMarketRegime VixRegime { get; set; }
|
||||
public decimal VixValue { get; set; }
|
||||
|
||||
public double ImpactScore { get; set; }
|
||||
public double WinRate { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string RawDataJson { get; set; } = "{}";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string AiOutputJson { get; set; } = "{}";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string N8nResponseJson { get; set; } = "{}";
|
||||
|
||||
public double N8nEvalScore { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string N8nDecision { get; set; } = string.Empty;
|
||||
|
||||
public bool IsTradeProposed { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace FinlyticAnalyzer.Entities;
|
||||
|
||||
public class AnalyzerSettingsEntity
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string ScanCronSchedule { get; set; } = "0 */1 * * *";
|
||||
public double MinSignalScore { get; set; } = 75.0;
|
||||
|
||||
public bool EnableLogMqttHealthPing { get; set; } = false;
|
||||
public bool EnableLogMqttGeneral { get; set; } = true;
|
||||
public bool EnableLogAnalyzerAuto { get; set; } = true;
|
||||
public bool EnableLogAnalyzerManual { get; set; } = true;
|
||||
public bool EnableLogDatabaseOps { get; set; } = true;
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Assets;
|
||||
|
||||
namespace FinlyticAnalyzer.Entities;
|
||||
|
||||
[Table("trade_proposals")]
|
||||
public class TradeProposalEntity
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string AnalysisId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string EventId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Isin { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(30)]
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(150)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(50)]
|
||||
public string Sector { get; set; } = "General";
|
||||
|
||||
public AssetType Type { get; set; } = AssetType.Stock;
|
||||
|
||||
/// <summary>
|
||||
/// KI-Entscheidung ("BUY", "SELL", "HOLD", "REJECTED")
|
||||
/// </summary>
|
||||
[MaxLength(20)]
|
||||
public string ProposedAction { get; set; } = "BUY";
|
||||
|
||||
public double ConfidenceScore { get; set; }
|
||||
|
||||
// --- KI Execution Plan (Vorgeschlagene Preismarken) ---
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal EntryPrice { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal StopLoss { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal TakeProfit { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? EntryZoneMin { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? EntryZoneMax { get; set; }
|
||||
|
||||
public string? TakeProfitTargets { get; set; } // Comma-separated or JSON
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? RiskRewardRatio { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? MaxLeverage { get; set; }
|
||||
|
||||
// --- Kontext aus Request & KI ---
|
||||
public string ReasonSummary { get; set; } = string.Empty;
|
||||
public string TechnicalRationale { get; set; } = string.Empty;
|
||||
public string FundamentalRationale { get; set; } = string.Empty;
|
||||
public string RiskWarning { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(30)]
|
||||
public string RiskTolerance { get; set; } = "Balanced";
|
||||
|
||||
[MaxLength(20)]
|
||||
public string Timeframe { get; set; } = "1-7 Tage";
|
||||
|
||||
[MaxLength(30)]
|
||||
public string InstrumentType { get; set; } = "KnockOut";
|
||||
|
||||
public VixMarketRegime VixRegime { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal VixValue { get; set; }
|
||||
|
||||
public double WinRate { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddHours(3);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FinlyticCore\FinlyticCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,136 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
[DbContext(typeof(AnalyzerDbContext))]
|
||||
[Migration("20260801073402_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AiOutputJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AnalysisId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EventId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<double>("ImpactScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsTradeProposed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("N8nDecision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<double>("N8nEvalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("N8nResponseJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RawDataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("VixRegime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("VixValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<double>("WinRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AnalysisId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("Sector");
|
||||
|
||||
b.ToTable("analyses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("MinSignalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("ScanCronSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "analyses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AnalysisId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
EventId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Sector = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Symbol = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
Isin = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
VixRegime = table.Column<int>(type: "integer", nullable: false),
|
||||
VixValue = table.Column<decimal>(type: "numeric", nullable: false),
|
||||
ImpactScore = table.Column<double>(type: "double precision", nullable: false),
|
||||
WinRate = table.Column<double>(type: "double precision", nullable: false),
|
||||
RawDataJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||
AiOutputJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||
N8nResponseJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||
N8nEvalScore = table.Column<double>(type: "double precision", nullable: false),
|
||||
N8nDecision = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
IsTradeProposed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_analyses", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ScanCronSchedule = table.Column<string>(type: "text", nullable: false),
|
||||
MinSignalScore = table.Column<double>(type: "double precision", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Settings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_analyses_AnalysisId",
|
||||
table: "analyses",
|
||||
column: "AnalysisId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_analyses_CreatedAt",
|
||||
table: "analyses",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_analyses_EventId",
|
||||
table: "analyses",
|
||||
column: "EventId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_analyses_Isin",
|
||||
table: "analyses",
|
||||
column: "Isin");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_analyses_Sector",
|
||||
table: "analyses",
|
||||
column: "Sector");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "analyses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
[DbContext(typeof(AnalyzerDbContext))]
|
||||
[Migration("20260803185020_AddLogFilterSettings")]
|
||||
partial class AddLogFilterSettings
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AiOutputJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AnalysisId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EventId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<double>("ImpactScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsTradeProposed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("N8nDecision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<double>("N8nEvalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("N8nResponseJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RawDataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("VixRegime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("VixValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<double>("WinRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AnalysisId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("Sector");
|
||||
|
||||
b.ToTable("analyses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerAuto")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerManual")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogDatabaseOps")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttGeneral")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttHealthPing")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<double>("MinSignalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("ScanCronSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLogFilterSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EnableLogAnalyzerAuto",
|
||||
table: "Settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EnableLogAnalyzerManual",
|
||||
table: "Settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EnableLogDatabaseOps",
|
||||
table: "Settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EnableLogMqttGeneral",
|
||||
table: "Settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "EnableLogMqttHealthPing",
|
||||
table: "Settings",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EnableLogAnalyzerAuto",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EnableLogAnalyzerManual",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EnableLogDatabaseOps",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EnableLogMqttGeneral",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EnableLogMqttHealthPing",
|
||||
table: "Settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
[DbContext(typeof(AnalyzerDbContext))]
|
||||
[Migration("20260804184350_CheckPendingMigrations")]
|
||||
partial class CheckPendingMigrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AiOutputJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AnalysisId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EventId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<double>("ImpactScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsTradeProposed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("N8nDecision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<double>("N8nEvalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("N8nResponseJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RawDataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("VixRegime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("VixValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<double>("WinRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AnalysisId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("Sector");
|
||||
|
||||
b.ToTable("analyses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerAuto")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerManual")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogDatabaseOps")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttGeneral")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttHealthPing")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<double>("MinSignalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("ScanCronSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CheckPendingMigrations : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
[DbContext(typeof(AnalyzerDbContext))]
|
||||
[Migration("20260805184638_AddTradeProposals")]
|
||||
partial class AddTradeProposals
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AiOutputJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AnalysisId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EventId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<double>("ImpactScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsTradeProposed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("N8nDecision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<double>("N8nEvalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("N8nResponseJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RawDataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("VixRegime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("VixValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<double>("WinRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AnalysisId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("Sector");
|
||||
|
||||
b.ToTable("analyses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerAuto")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerManual")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogDatabaseOps")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttGeneral")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttHealthPing")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<double>("MinSignalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("ScanCronSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("ConfidenceScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProposedAction")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ReasonSummary")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAt");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.ToTable("TradeProposals");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTradeProposals : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TradeProposals",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Isin = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
ProposedAction = table.Column<string>(type: "text", nullable: false),
|
||||
ConfidenceScore = table.Column<double>(type: "double precision", nullable: false),
|
||||
ReasonSummary = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TradeProposals", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TradeProposals_ExpiresAt",
|
||||
table: "TradeProposals",
|
||||
column: "ExpiresAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TradeProposals_Isin",
|
||||
table: "TradeProposals",
|
||||
column: "Isin");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TradeProposals");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticAnalyzer.Migrations
|
||||
{
|
||||
[DbContext(typeof(AnalyzerDbContext))]
|
||||
partial class AnalyzerDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AiOutputJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AnalysisId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EventId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<double>("ImpactScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<bool>("IsTradeProposed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("N8nDecision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<double>("N8nEvalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("N8nResponseJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RawDataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Sector")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Symbol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("VixRegime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("VixValue")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<double>("WinRate")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AnalysisId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("Sector");
|
||||
|
||||
b.ToTable("analyses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerAuto")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogAnalyzerManual")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogDatabaseOps")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttGeneral")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("EnableLogMqttHealthPing")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<double>("MinSignalScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("ScanCronSchedule")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("ConfidenceScore")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProposedAction")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ReasonSummary")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAt");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.ToTable("TradeProposals");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using FinlyticAnalyzer.Services;
|
||||
using FinlyticAnalyzer.Util;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Register DB Context
|
||||
builder.Services.AddDbContext<AnalyzerDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// Register HTTP Clients for external scrapers/webhooks
|
||||
builder.Services.AddHttpClient<IVixTrackerService, VixTrackerService>();
|
||||
builder.Services.AddHttpClient<IN8nEvaluationService, N8nEvaluationService>();
|
||||
|
||||
// Register Domain Services
|
||||
builder.Services.AddSingleton<IVixTrackerService, VixTrackerService>();
|
||||
builder.Services.AddSingleton<IThreeLayerFilterEngine, ThreeLayerFilterEngine>();
|
||||
builder.Services.AddSingleton<IWinRateCalculator, WinRateCalculator>();
|
||||
builder.Services.AddSingleton<IN8nEvaluationService, N8nEvaluationService>();
|
||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||
|
||||
// Unified MQTT Client (Handles both Events and RPC)
|
||||
builder.Services.AddSingleton<AnalyzerMqttClient>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<AnalyzerMqttClient>());
|
||||
|
||||
// Register Active Trade Monitor
|
||||
builder.Services.AddHostedService<ActiveTradeMonitorWorker>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// Run DB Migrations
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
try
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
Console.WriteLine("Database migrations successfully executed for FinlyticAnalyzer.");
|
||||
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||
await settingsService.GetSettingsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogError(ex, "An error occurred during database migration for FinlyticAnalyzer on startup.");
|
||||
}
|
||||
}
|
||||
|
||||
// Initial VIX Poll
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
|
||||
await vixService.PollVixAsync();
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
@@ -0,0 +1,37 @@
|
||||
# Finlytic Analyzer Service
|
||||
|
||||
Finlytic Analyzer is the core quantitative decision engine of the Finlytic ecosystem. It evaluates multi-layered market filters, tracks VIX volatility regimes, evaluates AI win rates, and generates actionable trade proposals.
|
||||
|
||||
---
|
||||
|
||||
## Core Features & Architecture
|
||||
|
||||
1. **3-Layer Filter Engine (`IThreeLayerFilterEngine`)**:
|
||||
- **Layer 1 (Macro VIX Regime)**: Evaluates overall volatility conditions via `IVixTrackerService`.
|
||||
- **Layer 2 (Asset Technical Analysis & Indicators)**: Evaluates RSI, MACD, Moving Averages, and Supertrend alignment.
|
||||
- **Layer 3 (AI Sentiment & Event Context)**: Evaluates FinBERT news sentiment scores and corporate earnings proximity.
|
||||
|
||||
2. **VIX Volatility Tracker (`IVixTrackerService`)**:
|
||||
- Polls external VIX volatility sources and categorizes market regimes (`Low`, `Normal`, `Elevated`, `High`).
|
||||
|
||||
3. **Win-Rate Calculator (`IWinRateCalculator`)**:
|
||||
- Calculates historical probability of success based on trade feedback records.
|
||||
|
||||
4. **MQTT Signal Publisher (`AnalyzerMqttClient`)**:
|
||||
- Publishes generated trade proposals to `finlytic/trades/proposed/{symbol}`.
|
||||
|
||||
---
|
||||
|
||||
## Feature Status
|
||||
|
||||
### Implemented Features
|
||||
- [x] 3-Layer Quantitative Filter Engine (`ThreeLayerFilterEngine`).
|
||||
- [x] VIX Volatility Regime Tracker (`VixTrackerService`).
|
||||
- [x] Win-Rate Probability Calculator (`WinRateCalculator`).
|
||||
- [x] n8n AI Evaluation Integration (`N8nEvaluationService`).
|
||||
- [x] Pure Worker Service Architecture (`Host.CreateApplicationBuilder`, Kestrel webserver removed).
|
||||
- [x] Zero-Allocation MQTT Signal Publishing (`AnalyzerMqttClient`).
|
||||
|
||||
### Planned Features
|
||||
- [ ] Multi-year historical Backtesting Engine with Monte Carlo simulation.
|
||||
- [ ] Portfolio Risk Allocation & Kelly Criterion Position Sizing Engine.
|
||||
@@ -0,0 +1,357 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAnalyzer.Util;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using FinlyticCore.Util;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class ActiveTradeMonitorWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<ActiveTradeMonitorWorker> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly AnalyzerMqttClient _mqttClient;
|
||||
|
||||
public ActiveTradeMonitorWorker(ILogger<ActiveTradeMonitorWorker> logger, IServiceScopeFactory scopeFactory,
|
||||
AnalyzerMqttClient mqttClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
_mqttClient = mqttClient;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker started.", "AnalyzerChannel");
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await MonitorActiveTradesAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error in ActiveTradeMonitorWorker loop.", "AnalyzerChannel");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(60), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker stopped.", "AnalyzerChannel");
|
||||
}
|
||||
|
||||
private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_mqttClient.IsConnected)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Skipping trade monitoring. RPC client not connected.", "AnalyzerChannel");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch active trades
|
||||
var activeTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
|
||||
"trades_Get",
|
||||
new GetTradesRequest(null, "Active"),
|
||||
TimeSpan.FromSeconds(10));
|
||||
|
||||
// Fetch proposed global trades
|
||||
var proposedTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
|
||||
"trades_Get",
|
||||
new GetTradesRequest(null, "Proposed"),
|
||||
TimeSpan.FromSeconds(10));
|
||||
|
||||
var trades = new List<TradeProposalDto>();
|
||||
if (activeTrades != null) trades.AddRange(activeTrades);
|
||||
if (proposedTrades != null) trades.AddRange(proposedTrades.Where(t => t.IsGlobalProposal));
|
||||
|
||||
if (trades.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] No active or proposed global trades found to monitor.",
|
||||
"AnalyzerChannel");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] Found {Count} trades to monitor. Starting evaluation...", "AnalyzerChannel",
|
||||
trades.Count);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
|
||||
|
||||
foreach (var trade in trades)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) break;
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessTradeAsync(trade, n8nService, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Failed to monitor trade {TradeId} ({Symbol}).", "AnalyzerChannel",
|
||||
trade.TradeId, trade.Symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Get Live Price
|
||||
var livePriceReq = new IsinRequest(trade.Isin);
|
||||
var livePriceDto = await _mqttClient.SendRpcRequestAsync<LivePriceDto, IsinRequest>(
|
||||
"tr_GetLivePrice", livePriceReq, TimeSpan.FromSeconds(3));
|
||||
|
||||
decimal currentPrice = livePriceDto?.CurrentPrice > 0 ? livePriceDto.CurrentPrice : trade.EntryPrice;
|
||||
|
||||
// 2. Evaluate Hard Stops (StopLoss / TakeProfit / TimeStop)
|
||||
bool isLong = string.Equals(trade.SignalType, "BUY", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(trade.SignalType, "LONG", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Time-Stop Evaluierung
|
||||
int maxHoldingDays = EstimateMaxHoldingDays(trade.Timeframe);
|
||||
double daysOpen = (DateTime.UtcNow - trade.CreatedAt).TotalDays;
|
||||
|
||||
// 50% Grace Period. Bei z.B. 10 Tagen max. Haltedauer wird nach 15 Tagen ohne Zielerreichung glattgestellt.
|
||||
if (daysOpen > (maxHoldingDays * 1.5))
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close",
|
||||
$"Time-Stop getriggert: Setup ist invalidiert. Der Trade bewegt sich zu lange seitwärts (Offen seit {(int)daysOpen} Tagen, anvisiert waren max. {maxHoldingDays} Tage).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLong)
|
||||
{
|
||||
if (trade.StopLoss > 0 && currentPrice <= trade.StopLoss)
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trade.TakeProfit > 0 && currentPrice >= trade.TakeProfit)
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (trade.StopLoss > 0 && currentPrice >= trade.StopLoss)
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trade.TakeProfit > 0 && currentPrice <= trade.TakeProfit)
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Run AI evaluation for soft/dynamic updates
|
||||
var taResult = await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
|
||||
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
|
||||
|
||||
var latestIndicator = taResult?.Indicators?.LastOrDefault();
|
||||
|
||||
var taInfo = new TechnicalContextInfo
|
||||
{
|
||||
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "N/A",
|
||||
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "N/A",
|
||||
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "N/A",
|
||||
Sma50 = (double?)latestIndicator?.Sma50,
|
||||
Sma200 = (double?)latestIndicator?.Sma200,
|
||||
DetectedPatterns = taResult?.Patterns?.Select(p => new PatternContextInfo
|
||||
{
|
||||
PatternName = p.Type,
|
||||
BreakoutDirection = p.BreakoutSignal?.Direction,
|
||||
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
|
||||
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
|
||||
}).ToList() ?? new List<PatternContextInfo>()
|
||||
};
|
||||
|
||||
var n8nReq = new N8nAnalysisRequestDto
|
||||
{
|
||||
RequestId = Guid.NewGuid().ToString("N"),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TriggerType = "HourlyMonitor",
|
||||
TargetAsset = new TargetAssetInfo
|
||||
{
|
||||
Symbol = trade.Symbol,
|
||||
Isin = trade.Isin,
|
||||
Sector = trade.Sector
|
||||
},
|
||||
MarketContext = new MarketContextInfo
|
||||
{
|
||||
Vix = trade.VixValue,
|
||||
MarketRegime = trade.VixRegime.ToString()
|
||||
},
|
||||
UserPreferences = new UserPreferencesInfo
|
||||
{
|
||||
InstrumentType = trade.InstrumentType,
|
||||
TimeframeFormatted = trade.Timeframe
|
||||
},
|
||||
TechnicalContext = taInfo
|
||||
};
|
||||
|
||||
var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken);
|
||||
if (aiResponse == null)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] AI evaluation returned null for {TradeId}. Skipping update.",
|
||||
"AnalyzerChannel", trade.TradeId);
|
||||
return;
|
||||
}
|
||||
|
||||
string newRecommendation = "Hold";
|
||||
string reasoning = aiResponse.AiReasoning;
|
||||
decimal? newStopLoss = trade.StopLoss;
|
||||
decimal? newTakeProfit = trade.TakeProfit;
|
||||
|
||||
// Check for trend reversal
|
||||
bool aiSuggestsShort =
|
||||
string.Equals(aiResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(aiResponse.SuggestedDirection, "Sell", StringComparison.OrdinalIgnoreCase);
|
||||
bool aiSuggestsLong =
|
||||
string.Equals(aiResponse.SuggestedDirection, "Long", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(aiResponse.SuggestedDirection, "Buy", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if ((isLong && aiSuggestsShort) || (!isLong && aiSuggestsLong))
|
||||
{
|
||||
newRecommendation = "Close";
|
||||
reasoning =
|
||||
$"Trendwende detektiert: KI empfiehlt {aiResponse.SuggestedDirection}, Trade ist aber {(isLong ? "Long" : "Short")}.";
|
||||
}
|
||||
else if (string.Equals(aiResponse.AiDecision, "Reject", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
newRecommendation = "Close";
|
||||
reasoning = $"Risiko zu hoch: KI empfiehlt Exit. ({aiResponse.AiReasoning})";
|
||||
}
|
||||
else if (aiResponse.ExecutionPlan != null)
|
||||
{
|
||||
// Ratchet / Trailing Logic: StopLoss darf das Risiko nicht vergrößern!
|
||||
if (aiResponse.ExecutionPlan.StopLoss > 0)
|
||||
{
|
||||
var proposedSl = aiResponse.ExecutionPlan.StopLoss;
|
||||
if (isLong)
|
||||
{
|
||||
// Bei Long darf der StopLoss nur NACH OBEN angepasst werden
|
||||
if (trade.StopLoss <= 0 || proposedSl > trade.StopLoss)
|
||||
{
|
||||
newStopLoss = proposedSl;
|
||||
if (proposedSl > trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bei Short darf der StopLoss nur NACH UNTEN angepasst werden
|
||||
if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss)
|
||||
{
|
||||
newStopLoss = proposedSl;
|
||||
if (proposedSl < trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aiResponse.ExecutionPlan.TakeProfitTargets != null &&
|
||||
aiResponse.ExecutionPlan.TakeProfitTargets.Count > 0)
|
||||
{
|
||||
var proposedTp = aiResponse.ExecutionPlan.TakeProfitTargets[0];
|
||||
if (proposedTp > 0 && proposedTp != trade.TakeProfit)
|
||||
{
|
||||
newTakeProfit = proposedTp;
|
||||
if (newRecommendation == "Hold") newRecommendation = "AdjustTP";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await SendUpdateAsync(trade, currentPrice, newRecommendation, reasoning, newStopLoss, newTakeProfit);
|
||||
}
|
||||
|
||||
private async Task SendUpdateAsync(TradeProposalDto trade, decimal currentPrice, string recommendation,
|
||||
string reasoning, decimal? suggestedStopLoss = null, decimal? suggestedTakeProfit = null)
|
||||
{
|
||||
var update = new TradeHourlyUpdateDto
|
||||
{
|
||||
TradeId = trade.TradeId,
|
||||
Recommendation = recommendation,
|
||||
CurrentPrice = currentPrice,
|
||||
SuggestedStopLoss = suggestedStopLoss,
|
||||
SuggestedTakeProfit = suggestedTakeProfit,
|
||||
VixValue = trade.VixValue,
|
||||
Reasoning = reasoning,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Direktes Objekt-Publishing nutzen (ManagedMqttClient serialisiert typgerecht)
|
||||
string topic = $"finlytic/trades/updates/{trade.Isin}";
|
||||
await _mqttClient.PublishAsync(topic, update);
|
||||
|
||||
_logger.LogInformation(
|
||||
"[{Channel}] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}",
|
||||
"AnalyzerChannel", trade.TradeId, topic, recommendation, reasoning);
|
||||
}
|
||||
|
||||
private static int EstimateMaxHoldingDays(string timeframe)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timeframe)) return 14; // Default
|
||||
|
||||
string tfLower = timeframe.ToLowerInvariant();
|
||||
int multiplier = 1;
|
||||
|
||||
if (tfLower.Contains("woche") || tfLower.Contains("week")) multiplier = 7;
|
||||
else if (tfLower.Contains("monat") || tfLower.Contains("month")) multiplier = 30;
|
||||
else if (tfLower.Contains("jahr") || tfLower.Contains("year")) multiplier = 365;
|
||||
|
||||
var numbers = new List<int>();
|
||||
string currentNum = "";
|
||||
|
||||
foreach (char c in timeframe)
|
||||
{
|
||||
if (char.IsDigit(c))
|
||||
{
|
||||
currentNum += c;
|
||||
}
|
||||
else if (currentNum.Length > 0)
|
||||
{
|
||||
if (int.TryParse(currentNum, out int n)) numbers.Add(n);
|
||||
currentNum = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNum.Length > 0 && int.TryParse(currentNum, out int lastN)) numbers.Add(lastN);
|
||||
|
||||
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
|
||||
|
||||
if (maxNum == 0) maxNum = 14;
|
||||
if (multiplier == 1 && maxNum < 3) maxNum = 3; // Mindestens 3 Tage Kulanz
|
||||
|
||||
return maxNum * multiplier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public interface IN8nEvaluationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates an asset asynchronously using N8n.
|
||||
/// </summary>
|
||||
Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Dtos.News;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class FilterResult
|
||||
{
|
||||
public bool Passed { get; set; }
|
||||
public string RejectReason { get; set; } = string.Empty;
|
||||
|
||||
public string Sector { get; set; } = string.Empty;
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
public string Isin { get; set; } = string.Empty;
|
||||
|
||||
public double ImpactScore { get; set; }
|
||||
public double ThresholdApplied { get; set; }
|
||||
|
||||
public string RiskTolerance { get; set; } = "Moderate";
|
||||
public string Timeframe { get; set; } = "1D";
|
||||
public string InstrumentType { get; set; } = "Stock";
|
||||
}
|
||||
|
||||
public interface IThreeLayerFilterEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates news based on market regime and returns a filter result.
|
||||
/// </summary>
|
||||
FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public interface IVixTrackerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current VIX value.
|
||||
/// </summary>
|
||||
decimal GetCurrentVix();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current market regime based on VIX.
|
||||
/// </summary>
|
||||
VixMarketRegime GetCurrentRegime();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the VIX tracker with a new tick value.
|
||||
/// </summary>
|
||||
void UpdateVixFromTick(decimal vixValue);
|
||||
|
||||
/// <summary>
|
||||
/// Polls the VIX asynchronously and returns its value.
|
||||
/// </summary>
|
||||
Task<decimal> PollVixAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public interface IWinRateCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the win rate for a given sector and symbol under the specified market regime.
|
||||
/// </summary>
|
||||
double CalculateWinRate(string sector, string symbol, VixMarketRegime regime);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public enum LogCategory
|
||||
{
|
||||
MqttHealthPing,
|
||||
MqttGeneral,
|
||||
AnalyzerAuto,
|
||||
AnalyzerManual,
|
||||
DatabaseOps,
|
||||
General
|
||||
}
|
||||
|
||||
public static class LogCategoryFilter
|
||||
{
|
||||
public static bool EnableLogMqttHealthPing { get; set; } = false;
|
||||
public static bool EnableLogMqttGeneral { get; set; } = true;
|
||||
public static bool EnableLogAnalyzerAuto { get; set; } = true;
|
||||
public static bool EnableLogAnalyzerManual { get; set; } = true;
|
||||
public static bool EnableLogDatabaseOps { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given log category is enabled.
|
||||
/// </summary>
|
||||
public static bool IsEnabled(LogCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
LogCategory.MqttHealthPing => EnableLogMqttHealthPing,
|
||||
LogCategory.MqttGeneral => EnableLogMqttGeneral,
|
||||
LogCategory.AnalyzerAuto => EnableLogAnalyzerAuto,
|
||||
LogCategory.AnalyzerManual => EnableLogAnalyzerManual,
|
||||
LogCategory.DatabaseOps => EnableLogDatabaseOps,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Text.Json;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Util;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class N8nEvaluationService : IN8nEvaluationService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<N8nEvaluationService> _logger;
|
||||
private readonly string _webhookUrl;
|
||||
|
||||
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, ILogger<N8nEvaluationService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? "https://n8n.kleidukos.me/webhook/gemini/analysis/auto";
|
||||
|
||||
// Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(45);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates an asset asynchronously using N8n / Gemini workflows.
|
||||
/// </summary>
|
||||
public async Task<N8nAnalysisResponseDto?> EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...",
|
||||
"AnalyzerChannel", request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, _webhookUrl);
|
||||
|
||||
// Typsichere AOT-Serialisierung verwenden
|
||||
using var content = JsonContent.Create(
|
||||
request,
|
||||
FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto);
|
||||
|
||||
using var response = await _httpClient.PostAsync(_webhookUrl, content, cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var contentStr = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(contentStr) || contentStr.Trim() == "{}" || contentStr.Trim() == "[]")
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", "AnalyzerChannel", request.RequestId);
|
||||
return CreateRejectionFallback(request, "Die KI (n8n/Gemini) stuft den Trade als zu riskant ein und empfiehlt keine Positionierung.");
|
||||
}
|
||||
|
||||
// N8n schickt Ergebnisse manchmal als JSON-Array [{...}] zurück
|
||||
string jsonToDeserialize = contentStr.Trim();
|
||||
if (jsonToDeserialize.StartsWith('[') && jsonToDeserialize.EndsWith(']'))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(jsonToDeserialize);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0)
|
||||
{
|
||||
jsonToDeserialize = doc.RootElement[0].GetRawText();
|
||||
}
|
||||
}
|
||||
|
||||
var responseDto = JsonSerializer.Deserialize(
|
||||
jsonToDeserialize,
|
||||
FinlyticJsonSerializerContext.Default.N8nAnalysisResponseDto);
|
||||
|
||||
if (responseDto != null && !string.IsNullOrWhiteSpace(responseDto.AiDecision))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}",
|
||||
"AnalyzerChannel", request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe);
|
||||
|
||||
return responseDto;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}",
|
||||
"AnalyzerChannel", response.StatusCode, request.RequestId);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", "AnalyzerChannel", request.RequestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error calling n8n AI Evaluation Webhook for Request {RequestId}", "AnalyzerChannel", request.RequestId);
|
||||
}
|
||||
|
||||
return null; // Signals RPC/Service failure to caller
|
||||
}
|
||||
|
||||
private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning)
|
||||
{
|
||||
return new N8nAnalysisResponseDto
|
||||
{
|
||||
RequestId = request.RequestId,
|
||||
AiDecision = "Rejected",
|
||||
EvalScore = 0.0,
|
||||
SuggestedDirection = "NONE",
|
||||
SuggestedRisk = request.UserPreferences?.RiskTolerance ?? "Moderate",
|
||||
SuggestedTimeframe = request.UserPreferences?.TimeframeFormatted ?? "1D",
|
||||
AiReasoning = reasoning
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using FinlyticAnalyzer.Database;
|
||||
using FinlyticAnalyzer.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public interface ISettingsDbService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the analyzer settings asynchronously.
|
||||
/// </summary>
|
||||
Task<AnalyzerSettingsEntity> GetSettingsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the analyzer settings asynchronously.
|
||||
/// </summary>
|
||||
Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings);
|
||||
|
||||
/// <summary>
|
||||
/// Updates settings from a dictionary asynchronously.
|
||||
/// </summary>
|
||||
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
|
||||
}
|
||||
|
||||
public class SettingsDbService : ISettingsDbService
|
||||
{
|
||||
private readonly AnalyzerDbContext _context;
|
||||
|
||||
public SettingsDbService(AnalyzerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the analyzer settings asynchronously.
|
||||
/// </summary>
|
||||
public async Task<AnalyzerSettingsEntity> GetSettingsAsync()
|
||||
{
|
||||
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
||||
if (settings == null)
|
||||
{
|
||||
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid() };
|
||||
_context.Settings.Add(settings);
|
||||
await _context.SaveChangesAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the analyzer settings asynchronously.
|
||||
/// </summary>
|
||||
public async Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings)
|
||||
{
|
||||
var existing = await _context.Settings.FirstOrDefaultAsync();
|
||||
if (existing == null)
|
||||
{
|
||||
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
|
||||
_context.Settings.Add(settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.ScanCronSchedule = settings.ScanCronSchedule;
|
||||
existing.MinSignalScore = settings.MinSignalScore;
|
||||
existing.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing;
|
||||
existing.EnableLogMqttGeneral = settings.EnableLogMqttGeneral;
|
||||
existing.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
|
||||
existing.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
|
||||
existing.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
|
||||
existing.UpdatedAt = settings.UpdatedAt;
|
||||
_context.Settings.Update(existing);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Synchronize in-memory static filter values
|
||||
LogCategoryFilter.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing;
|
||||
LogCategoryFilter.EnableLogMqttGeneral = settings.EnableLogMqttGeneral;
|
||||
LogCategoryFilter.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
|
||||
LogCategoryFilter.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
|
||||
LogCategoryFilter.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates settings from a dictionary asynchronously.
|
||||
/// </summary>
|
||||
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
|
||||
{
|
||||
var settings = await GetSettingsAsync();
|
||||
|
||||
foreach (var (key, value) in dictionary)
|
||||
{
|
||||
if (string.Equals(key, "ScanCronSchedule", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(value))
|
||||
settings.ScanCronSchedule = value.Trim();
|
||||
else if (string.Equals(key, "MinSignalScore", StringComparison.OrdinalIgnoreCase) && double.TryParse(value, out var score))
|
||||
settings.MinSignalScore = score;
|
||||
else if (string.Equals(key, "EnableLog_MqttHealthPing", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b1))
|
||||
settings.EnableLogMqttHealthPing = b1;
|
||||
else if (string.Equals(key, "EnableLog_MqttGeneral", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b2))
|
||||
settings.EnableLogMqttGeneral = b2;
|
||||
else if (string.Equals(key, "EnableLog_AnalyzerAuto", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b3))
|
||||
settings.EnableLogAnalyzerAuto = b3;
|
||||
else if (string.Equals(key, "EnableLog_AnalyzerManual", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b4))
|
||||
settings.EnableLogAnalyzerManual = b4;
|
||||
else if (string.Equals(key, "EnableLog_DatabaseOps", StringComparison.OrdinalIgnoreCase) && bool.TryParse(value, out var b5))
|
||||
settings.EnableLogDatabaseOps = b5;
|
||||
}
|
||||
|
||||
settings.UpdatedAt = DateTime.UtcNow;
|
||||
await SaveSettingsAsync(settings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using FinlyticCore.Dtos.News;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
|
||||
{
|
||||
private readonly ILogger<ThreeLayerFilterEngine> _logger;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
|
||||
private DateTime _lastCleanupTime = DateTime.UtcNow;
|
||||
|
||||
public ThreeLayerFilterEngine(ILogger<ThreeLayerFilterEngine> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates news strictly based on ISIN and dynamic VIX market regime.
|
||||
/// </summary>
|
||||
public FilterResult EvaluateNews(NewsArticleDto newsEvent, VixMarketRegime regime)
|
||||
{
|
||||
var result = new FilterResult();
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Layer 1: Relevance, ISIN & Deduplication
|
||||
// -------------------------------------------------------------
|
||||
if (newsEvent == null || newsEvent.Id == Guid.Empty)
|
||||
{
|
||||
result.Passed = false;
|
||||
result.RejectReason = "Layer 1: Missing or Empty NewsArticle / EventId";
|
||||
return result;
|
||||
}
|
||||
|
||||
string eventId = newsEvent.Id.ToString();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Safely clean up dictionary every 30 minutes
|
||||
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
|
||||
{
|
||||
CleanupSeenEvents(now);
|
||||
}
|
||||
|
||||
// Deduplication check (keep history for 12 hours)
|
||||
if (_seenEvents.TryGetValue(eventId, out var prevTime) && (now - prevTime).TotalHours < 12.0)
|
||||
{
|
||||
result.Passed = false;
|
||||
result.RejectReason = "Layer 1: Duplicate EventId within 12h window";
|
||||
return result;
|
||||
}
|
||||
|
||||
_seenEvents[eventId] = now;
|
||||
|
||||
string isin = string.Empty;
|
||||
string assetName = string.Empty;
|
||||
|
||||
// Extract parameters strictly from MatchedAssets
|
||||
if (newsEvent.MatchedAssets != null && newsEvent.MatchedAssets.Count > 0)
|
||||
{
|
||||
var firstAsset = newsEvent.MatchedAssets[0];
|
||||
isin = !string.IsNullOrWhiteSpace(firstAsset.Isin) ? firstAsset.Isin.Trim().ToUpperInvariant() : string.Empty;
|
||||
assetName = !string.IsNullOrWhiteSpace(firstAsset.Name) ? firstAsset.Name.Trim() : string.Empty;
|
||||
}
|
||||
|
||||
// Mandatory check: Must have a valid ISIN
|
||||
if (string.IsNullOrWhiteSpace(isin))
|
||||
{
|
||||
result.Passed = false;
|
||||
result.RejectReason = "Layer 1: Missing mandatory ISIN for news item";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Isin = isin;
|
||||
// Asset-Symbol fallback to ISIN, Name is mapped appropriately later
|
||||
result.Symbol = isin;
|
||||
result.Sector = "General"; // Will be enriched downstream via Fundamentals RPC if available
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Layer 2: Impact & Dynamic VIX Threshold
|
||||
// -------------------------------------------------------------
|
||||
double impactScore = newsEvent.Confidence ?? 0.75;
|
||||
if (impactScore <= 0) impactScore = 0.75;
|
||||
|
||||
double requiredThreshold = regime switch
|
||||
{
|
||||
VixMarketRegime.LowVol => 0.55,
|
||||
VixMarketRegime.Normal => 0.65,
|
||||
VixMarketRegime.HighVol => 0.80,
|
||||
VixMarketRegime.Panic => 0.90,
|
||||
_ => 0.65
|
||||
};
|
||||
|
||||
result.ImpactScore = impactScore;
|
||||
result.ThresholdApplied = requiredThreshold;
|
||||
|
||||
if (impactScore < requiredThreshold)
|
||||
{
|
||||
result.Passed = false;
|
||||
result.RejectReason = $"Layer 2: Impact score ({impactScore:F2}) below dynamic VIX threshold ({requiredThreshold:F2}) for regime {regime}";
|
||||
_logger.LogInformation("[{Channel}] Event {EventId} (ISIN: {Isin}) rejected by Layer 2 filter. Impact: {Impact:F2}, Threshold: {Threshold:F2}, Regime: {Regime}",
|
||||
"AnalyzerChannel", eventId, isin, impactScore, requiredThreshold, regime);
|
||||
return result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Layer 3: Dynamic Parameter & Risk Engine
|
||||
// -------------------------------------------------------------
|
||||
result.RiskTolerance = regime switch
|
||||
{
|
||||
VixMarketRegime.Panic => "Conservative",
|
||||
VixMarketRegime.HighVol => "Moderate",
|
||||
_ => "Aggressive"
|
||||
};
|
||||
|
||||
result.Timeframe = impactScore >= 0.85 ? "4H" : "1D";
|
||||
result.InstrumentType = regime == VixMarketRegime.Panic ? "Option" : "Stock";
|
||||
|
||||
result.Passed = true;
|
||||
_logger.LogInformation("[{Channel}] Event {EventId} passed 3-Layer Filter for ISIN {Isin}. Impact: {Impact:F2}, Regime: {Regime}",
|
||||
"AnalyzerChannel", eventId, result.Isin, impactScore, regime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CleanupSeenEvents(DateTime now)
|
||||
{
|
||||
_lastCleanupTime = now;
|
||||
foreach (var kv in _seenEvents)
|
||||
{
|
||||
if ((now - kv.Value).TotalHours > 12.0)
|
||||
{
|
||||
_seenEvents.TryRemove(kv.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Services.Yahoo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class VixTrackerService : IVixTrackerService
|
||||
{
|
||||
private readonly YahooFinanceClient _yahooClient;
|
||||
private readonly ILogger<VixTrackerService> _logger;
|
||||
|
||||
private decimal _currentVix = 18.5m; // Default: Normal Regime
|
||||
private VixMarketRegime _currentRegime = VixMarketRegime.Normal;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public VixTrackerService(YahooFinanceClient yahooClient, ILogger<VixTrackerService> logger)
|
||||
{
|
||||
_yahooClient = yahooClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public decimal GetCurrentVix()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentVix;
|
||||
}
|
||||
}
|
||||
|
||||
public VixMarketRegime GetCurrentRegime()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentRegime;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateVixFromTick(decimal vixValue)
|
||||
{
|
||||
if (vixValue <= 0m) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var oldRegime = _currentRegime;
|
||||
var oldVix = _currentVix;
|
||||
|
||||
_currentVix = vixValue;
|
||||
_currentRegime = CalculateRegime(vixValue);
|
||||
|
||||
if (oldRegime != _currentRegime)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})",
|
||||
"AnalyzerChannel", oldRegime, _currentRegime, _currentVix);
|
||||
}
|
||||
else if (Math.Abs(oldVix - vixValue) >= 0.5m)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] VIX aktualisiert: {Vix:F2} (Regime: {Regime})",
|
||||
"AnalyzerChannel", _currentVix, _currentRegime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> PollVixAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vix = await _yahooClient.GetLivePriceAsync("^VIX", cancellationToken);
|
||||
|
||||
if (vix.HasValue && vix.Value > 0m)
|
||||
{
|
||||
UpdateVixFromTick(vix.Value);
|
||||
return vix.Value;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Graceful shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Fehler beim Abfragen von ^VIX über YahooFinanceClient. Nutze gecachten Wert {Vix}.",
|
||||
"AnalyzerChannel", GetCurrentVix());
|
||||
}
|
||||
|
||||
return GetCurrentVix();
|
||||
}
|
||||
|
||||
private static VixMarketRegime CalculateRegime(decimal vix)
|
||||
{
|
||||
return vix switch
|
||||
{
|
||||
< 15.0m => VixMarketRegime.LowVol,
|
||||
>= 15.0m and < 20.0m => VixMarketRegime.Normal,
|
||||
>= 20.0m and < 30.0m => VixMarketRegime.HighVol,
|
||||
_ => VixMarketRegime.Panic
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class WinRateCalculator : IWinRateCalculator
|
||||
{
|
||||
private readonly ILogger<WinRateCalculator> _logger;
|
||||
private readonly string _feedbackDir;
|
||||
|
||||
public WinRateCalculator(ILogger<WinRateCalculator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
||||
if (!Directory.Exists(_feedbackDir))
|
||||
{
|
||||
Directory.CreateDirectory(_feedbackDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the win rate for a given sector and symbol under the specified market regime.
|
||||
/// </summary>
|
||||
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(_feedbackDir)) return 65.0;
|
||||
|
||||
var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories);
|
||||
if (jsonFiles.Length == 0) return 65.0;
|
||||
|
||||
int totalTrades = 0;
|
||||
int winningTrades = 0;
|
||||
|
||||
foreach (var file in jsonFiles)
|
||||
{
|
||||
var content = File.ReadAllText(file);
|
||||
var records = JsonSerializer.Deserialize<TradeFeedbackRecord[]>(content);
|
||||
if (records == null || records.Length == 0) continue;
|
||||
|
||||
var matching = records.Where(r =>
|
||||
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.VixRegime == regime).ToList();
|
||||
|
||||
foreach (var rec in matching)
|
||||
{
|
||||
totalTrades++;
|
||||
if (rec.IsWin) winningTrades++;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalTrades > 0)
|
||||
{
|
||||
double calculatedWinRate = (double)winningTrades / totalTrades * 100.0;
|
||||
_logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
|
||||
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, totalTrades);
|
||||
return Math.Round(calculatedWinRate, 1);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Error reading feedback files for win-rate calculation. Falling back to default.", "AnalyzerChannel");
|
||||
}
|
||||
|
||||
return 65.0; // Default baseline win-rate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAnalyzer.Database;
|
||||
using FinlyticAnalyzer.Entities;
|
||||
using FinlyticAnalyzer.Services;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Models;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using FinlyticCore.Util;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Unified Managed MQTT Client for FinlyticAnalyzer.
|
||||
/// Handles event subscriptions, market screening, manual AI evaluation triggers,
|
||||
/// and dispatches trade proposals via MQTT.
|
||||
/// </summary>
|
||||
public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IVixTrackerService _vixTracker;
|
||||
private readonly IThreeLayerFilterEngine _filterEngine;
|
||||
private readonly IWinRateCalculator _winRateCalculator;
|
||||
private readonly IN8nEvaluationService _n8nService;
|
||||
private readonly ILogger<AnalyzerMqttClient> _logger;
|
||||
|
||||
public AnalyzerMqttClient(
|
||||
IConfiguration configuration,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IVixTrackerService vixTracker,
|
||||
IThreeLayerFilterEngine filterEngine,
|
||||
IWinRateCalculator winRateCalculator,
|
||||
IN8nEvaluationService n8nService,
|
||||
ILogger<AnalyzerMqttClient> logger) : base(logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_scopeFactory = scopeFactory;
|
||||
_vixTracker = vixTracker;
|
||||
_filterEngine = filterEngine;
|
||||
_winRateCalculator = winRateCalculator;
|
||||
_n8nService = n8nService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var config = new MqttConfiguration
|
||||
{
|
||||
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
|
||||
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
|
||||
Username = _configuration["MQTT:Username"] ?? _configuration["MQTT__Username"],
|
||||
Password = _configuration["MQTT:Password"] ?? _configuration["MQTT__Password"],
|
||||
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_analyzer")}_{Guid.NewGuid():N}"
|
||||
};
|
||||
|
||||
_logger.LogInformation("[{Channel}] Starting Unified Analyzer MQTT Client. Host: {Host}, ClientId: {ClientId}", "AnalyzerChannel", config.Host, config.ClientId);
|
||||
await ConnectAsync(config);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Stopping Unified Analyzer MQTT Client.", "AnalyzerChannel");
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Analyzer MQTT Client connected. Subscribing to topics and RPC response channels...", "AnalyzerChannel");
|
||||
|
||||
// Incoming Event Topics
|
||||
await SubscribeAsync("services/news/completed");
|
||||
await SubscribeAsync("services/news/#");
|
||||
await SubscribeAsync("finlytic/news/raw/#");
|
||||
await SubscribeAsync("finlytic/market/ticks/#");
|
||||
await SubscribeAsync("services/config/updated/#");
|
||||
await SubscribeAsync("services/request/health_Ping/#");
|
||||
await SubscribeAsync("services/request/analyzer_TriggerManual/#");
|
||||
await SubscribeAsync("finlytic/trades/closed/#");
|
||||
|
||||
// RPC Response Channels
|
||||
await SubscribeAsync("services/response/ta_GetAnalysis/#");
|
||||
await SubscribeAsync("services/response/fundamentals_Get/#");
|
||||
await SubscribeAsync("services/response/sentiment_GetIsin/#");
|
||||
await SubscribeAsync("services/response/trades_Get/#");
|
||||
await SubscribeAsync("services/response/tr_GetLivePrice/#");
|
||||
|
||||
_logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "AnalyzerChannel");
|
||||
}
|
||||
|
||||
protected override async Task OnMessageReceivedAsync(string topic, string payloadStr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (topic.Contains("health_Ping", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var segments = topic.Split('/');
|
||||
bool isForMe = segments.Length >= 5
|
||||
? segments[3].Equals("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)
|
||||
: topic.Contains("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isForMe)
|
||||
{
|
||||
var correlationId = segments[^1];
|
||||
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||
var healthResp = new ServiceHealthResponse("FinlyticAnalyzer", "Online", DateTime.UtcNow, "Connected");
|
||||
await PublishAsync(respTopic, healthResp);
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.MqttHealthPing))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "AnalyzerChannel", correlationId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (topic.EndsWith("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Received config update event for FinlyticAnalyzer.", "AnalyzerChannel");
|
||||
try
|
||||
{
|
||||
var configUpdate = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload);
|
||||
if (configUpdate?.Settings != null && configUpdate.Settings.Count > 0)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var settingsDb = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||
await settingsDb.UpdateSettingsFromDictionaryAsync(configUpdate.Settings);
|
||||
_logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Persisted {Count} updated settings to FinlyticAnalyzer database.", "AnalyzerChannel", configUpdate.Settings.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] [AnalyzerMqttClient] Error processing MQTT config update event.", "AnalyzerChannel");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (topic.StartsWith("finlytic/market/ticks/"))
|
||||
{
|
||||
ProcessTickMessage(topic, payloadStr);
|
||||
}
|
||||
else if (topic.StartsWith("finlytic/news/raw/", StringComparison.OrdinalIgnoreCase) ||
|
||||
topic.StartsWith("services/news/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ProcessNewsMessageAsync(payloadStr, CancellationToken.None);
|
||||
}
|
||||
else if (topic.StartsWith("services/request/analyzer_TriggerManual/"))
|
||||
{
|
||||
var correlationId = topic.Split('/').Last();
|
||||
await HandleManualTriggerAsync(correlationId, payloadStr, CancellationToken.None);
|
||||
}
|
||||
else if (topic.StartsWith("finlytic/trades/closed/"))
|
||||
{
|
||||
await HandleClosedTradeFeedbackAsync(payloadStr);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error processing incoming MQTT message on topic {Topic}", "AnalyzerChannel", topic);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClosedTradeFeedbackAsync(string payloadStr)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var closedDto = JsonSerializer.Deserialize<TradeProposalDto>(payloadStr, options);
|
||||
|
||||
if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId))
|
||||
{
|
||||
bool isWin = closedDto.Status.Contains("Profit", StringComparison.OrdinalIgnoreCase) ||
|
||||
closedDto.Status.Contains("Win", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var feedback = new TradeFeedbackRecord
|
||||
{
|
||||
TradeId = closedDto.TradeId,
|
||||
AnalysisId = closedDto.AnalysisId,
|
||||
Sector = closedDto.Sector,
|
||||
Symbol = closedDto.Symbol,
|
||||
Isin = closedDto.Isin,
|
||||
EntryPrice = closedDto.EntryPrice,
|
||||
StopLoss = closedDto.StopLoss,
|
||||
TakeProfit = closedDto.TakeProfit,
|
||||
IsWin = isWin,
|
||||
VixRegime = closedDto.VixRegime,
|
||||
VixValue = closedDto.VixValue,
|
||||
CreatedAt = closedDto.CreatedAt,
|
||||
ClosedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
string feedbackDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
|
||||
if (!System.IO.Directory.Exists(feedbackDir))
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(feedbackDir);
|
||||
}
|
||||
|
||||
string filePath = System.IO.Path.Combine(feedbackDir, $"{closedDto.TradeId}.json");
|
||||
await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(new[] { feedback }, options));
|
||||
|
||||
_logger.LogInformation("[{Channel}] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", "AnalyzerChannel", closedDto.TradeId, filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error processing closed trade feedback.", "AnalyzerChannel");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleManualTriggerAsync(string correlationId, string payloadStr, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manualReq = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ManualAnalysisRpcRequest);
|
||||
if (manualReq == null || string.IsNullOrWhiteSpace(manualReq.Isin))
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Manual trigger received without valid request or ISIN.", "AnalyzerChannel");
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerManual))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [ManualAnalyzer] [TRIGGERED] Processing rich manual trigger for ISIN '{Isin}' (Symbol: {Symbol}). CorrelationId: {CorrelationId}", "AnalyzerChannel", manualReq.Isin, manualReq.Symbol, correlationId);
|
||||
}
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
|
||||
|
||||
var regime = _vixTracker.GetCurrentRegime();
|
||||
var currentVix = _vixTracker.GetCurrentVix();
|
||||
string analysisId = Guid.NewGuid().ToString("N");
|
||||
double winRate = _winRateCalculator.CalculateWinRate(manualReq.Sector, manualReq.Symbol, regime);
|
||||
|
||||
string riskLabel = manualReq.RiskScore > 70 ? $"Aggressiv ({manualReq.RiskScore}/100)" : (manualReq.RiskScore > 30 ? $"Balanced ({manualReq.RiskScore}/100)" : $"Konservativ ({manualReq.RiskScore}/100)");
|
||||
string timeframeFormatted = $"{manualReq.MinTimeframeValue}-{manualReq.MaxTimeframeValue} {manualReq.TimeframeUnit}";
|
||||
|
||||
var n8nRequest = new N8nAnalysisRequestDto
|
||||
{
|
||||
RequestId = analysisId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TriggerType = "Manual",
|
||||
TargetAsset = new TargetAssetInfo
|
||||
{
|
||||
Symbol = manualReq.FundamentalsData?.Ticker ?? manualReq.Symbol.ToUpperInvariant(),
|
||||
Name = manualReq.FundamentalsData?.CompanyName ?? manualReq.Isin.ToUpperInvariant(),
|
||||
Isin = manualReq.Isin.ToUpperInvariant(),
|
||||
Sector = manualReq.Sector
|
||||
},
|
||||
MarketContext = new MarketContextInfo
|
||||
{
|
||||
Vix = currentVix,
|
||||
MarketRegime = regime.ToString()
|
||||
},
|
||||
FilterContext = new FilterContextInfo
|
||||
{
|
||||
ImpactScore = 1.0,
|
||||
RawNewsHeadline = string.IsNullOrWhiteSpace(manualReq.Headline) ? "Manual User Trigger" : manualReq.Headline
|
||||
},
|
||||
UserPreferences = new UserPreferencesInfo
|
||||
{
|
||||
RiskScore = manualReq.RiskScore,
|
||||
RiskTolerance = riskLabel,
|
||||
MinTimeframeValue = manualReq.MinTimeframeValue,
|
||||
MaxTimeframeValue = manualReq.MaxTimeframeValue,
|
||||
TimeframeUnit = manualReq.TimeframeUnit,
|
||||
TimeframeFormatted = timeframeFormatted,
|
||||
InstrumentType = manualReq.InstrumentType,
|
||||
UserNotes = manualReq.UserNotes
|
||||
},
|
||||
TradeFeedback = new TradeFeedbackInfo
|
||||
{
|
||||
TotalAssetTrades = 0,
|
||||
AssetWinRate = winRate,
|
||||
AvgReturnPercent = 0.0,
|
||||
LastTradeResult = "UNKNOWN"
|
||||
},
|
||||
TechnicalContext = new TechnicalContextInfo
|
||||
{
|
||||
Rsi = manualReq.TaData?.Indicators?.LastOrDefault()?.Rsi14?.ToString("F1") ?? "N/A",
|
||||
SupertrendStatus = manualReq.TaData?.Indicators?.LastOrDefault()?.SupertrendDirection ?? "NEUTRAL",
|
||||
Atr = manualReq.TaData?.Indicators?.LastOrDefault()?.Atr14?.ToString("F2") ?? "N/A",
|
||||
Sma50 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma50,
|
||||
Sma200 = (double?)manualReq.TaData?.Indicators?.LastOrDefault()?.Sma200,
|
||||
DetectedPatterns = manualReq.TaData?.Patterns?.Select(p => new PatternContextInfo
|
||||
{
|
||||
PatternName = p.Type,
|
||||
BreakoutDirection = p.BreakoutSignal?.Direction,
|
||||
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
|
||||
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
|
||||
}).ToList() ?? new List<PatternContextInfo>()
|
||||
},
|
||||
SentimentContext = new SentimentContextInfo
|
||||
{
|
||||
AssetSentimentScore = manualReq.SentimentData?.CurrentSummary?.CompoundScore ?? 0.0,
|
||||
SectorSentimentScore = 0.0,
|
||||
NewsSentimentSummary = manualReq.SentimentData?.CurrentSummary?.SentimentLabel ?? "Neutral"
|
||||
},
|
||||
FundamentalContext = new FundamentalContextInfo
|
||||
{
|
||||
PeRatio = (double?)manualReq.FundamentalsData?.PeRatioTrailing,
|
||||
ForwardPeRatio = (double?)manualReq.FundamentalsData?.PeRatioForward,
|
||||
PegRatio = (double?)manualReq.FundamentalsData?.PegRatio,
|
||||
MarketCap = (double?)manualReq.FundamentalsData?.MarketCapitalization,
|
||||
DebtToEquity = (double?)manualReq.FundamentalsData?.DebtToEquity,
|
||||
GrossMargin = (double?)manualReq.FundamentalsData?.GrossMargin,
|
||||
NetProfitMargin = (double?)manualReq.FundamentalsData?.NetProfitMargin,
|
||||
ReturnOnEquity = (double?)manualReq.FundamentalsData?.ReturnOnEquity,
|
||||
DividendYield = (double?)manualReq.FundamentalsData?.DividendYield,
|
||||
ShortPercentOfFloat = (double?)manualReq.FundamentalsData?.ShortPercentOfFloat,
|
||||
AnalystTargetMedian = (double?)manualReq.FundamentalsData?.PriceTargetMedian,
|
||||
EvToEbitda = (double?)manualReq.FundamentalsData?.EvToEbitda
|
||||
}
|
||||
};
|
||||
|
||||
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
|
||||
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||
var settings = await settingsService.GetSettingsAsync();
|
||||
double minSignalScore = settings.MinSignalScore;
|
||||
|
||||
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75;
|
||||
bool shouldProceed = n8nResponse != null &&
|
||||
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
|
||||
(confidenceScore * 100.0) >= minSignalScore &&
|
||||
winRate >= minSignalScore;
|
||||
|
||||
TradeProposalDto? proposalDto = null;
|
||||
if (n8nResponse != null)
|
||||
{
|
||||
proposalDto = new TradeProposalDto
|
||||
{
|
||||
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = manualReq.Sector,
|
||||
Symbol = manualReq.Symbol.ToUpperInvariant(),
|
||||
Isin = manualReq.Isin.ToUpperInvariant(),
|
||||
CompanyName = manualReq.FundamentalsData?.CompanyName ?? manualReq.Symbol,
|
||||
EntryPrice = manualReq.CurrentPrice,
|
||||
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
||||
Status = shouldProceed ? "Proposed" : "Rejected",
|
||||
RiskTolerance = n8nResponse.SuggestedRisk,
|
||||
Timeframe = timeframeFormatted,
|
||||
InstrumentType = manualReq.InstrumentType,
|
||||
WinRate = winRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 60,
|
||||
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
|
||||
|
||||
StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
|
||||
TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
|
||||
EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
|
||||
EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
|
||||
TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
|
||||
RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
|
||||
MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
|
||||
TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
|
||||
FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
|
||||
RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
|
||||
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
var analysisEntity = new AnalysisEntity
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = manualReq.Sector,
|
||||
Symbol = manualReq.Symbol.ToUpperInvariant(),
|
||||
Isin = manualReq.Isin.ToUpperInvariant(),
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = 1.0,
|
||||
WinRate = winRate,
|
||||
RawDataJson = JsonSerializer.Serialize(manualReq),
|
||||
AiOutputJson = proposalDto != null ? JsonSerializer.Serialize(proposalDto) : "{}",
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
N8nEvalScore = n8nResponse?.EvalScore ?? 0,
|
||||
N8nDecision = n8nResponse?.AiDecision ?? "Rejected",
|
||||
IsTradeProposed = shouldProceed,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Analyses.Add(analysisEntity);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var responseTopic = $"services/response/analyzer_TriggerManual/{correlationId}";
|
||||
var responsePayload = new ManualAnalysisResponseDto
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
IsTradeProposed = shouldProceed,
|
||||
Status = shouldProceed ? "Success" : "Rejected",
|
||||
Recommendation = shouldProceed ? "RECOMMENDED" : "NOT_RECOMMENDED",
|
||||
N8nResponse = n8nResponse,
|
||||
Proposal = proposalDto
|
||||
};
|
||||
|
||||
await PublishAsync(responseTopic, responsePayload);
|
||||
|
||||
if (proposalDto != null && shouldProceed)
|
||||
{
|
||||
string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(manualReq.Sector) ? "general" : manualReq.Sector.ToLowerInvariant())}/{manualReq.Symbol.ToLowerInvariant()}";
|
||||
await PublishAsync(propTopic, proposalDto);
|
||||
_logger.LogInformation("[{Channel}] [ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", "AnalyzerChannel", analysisId, propTopic);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Failed to handle manual trigger.", "AnalyzerChannel");
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTickMessage(string topic, string payloadStr)
|
||||
{
|
||||
if (topic.EndsWith("VIX", StringComparison.OrdinalIgnoreCase) || topic.EndsWith("^VIX", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var tick = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.TickMessageDto);
|
||||
if (tick != null && tick.Price > 0)
|
||||
{
|
||||
_vixTracker.UpdateVixFromTick(tick.Price);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Failed to parse VIX tick message.", "AnalyzerChannel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessNewsMessageAsync(string payloadStr, CancellationToken cancellationToken)
|
||||
{
|
||||
var newsArticle = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.NewsArticleDto);
|
||||
if (newsArticle == null) return;
|
||||
|
||||
var regime = _vixTracker.GetCurrentRegime();
|
||||
var currentVix = _vixTracker.GetCurrentVix();
|
||||
|
||||
var filterResult = _filterEngine.EvaluateNews(newsArticle, regime);
|
||||
if (!filterResult.Passed)
|
||||
{
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", "AnalyzerChannel", filterResult.Isin, filterResult.RejectReason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] [TRIGGERED] Screening market opportunity for ISIN '{Isin}'...", "AnalyzerChannel", filterResult.Isin);
|
||||
}
|
||||
|
||||
string analysisId = Guid.NewGuid().ToString("N");
|
||||
string eventId = newsArticle.Id != Guid.Empty ? newsArticle.Id.ToString() : analysisId;
|
||||
string rawHeadline = newsArticle.Title ?? string.Empty;
|
||||
|
||||
double winRate = _winRateCalculator.CalculateWinRate(filterResult.Sector, filterResult.Symbol, regime);
|
||||
|
||||
int riskScore = 50;
|
||||
string riskTolerance = "Balanced (50/100)";
|
||||
int minTf = 4;
|
||||
int maxTf = 7;
|
||||
|
||||
if (winRate < 45.0)
|
||||
{
|
||||
riskScore = 30;
|
||||
riskTolerance = "Konservativ (30/100)";
|
||||
minTf = 7;
|
||||
maxTf = 14;
|
||||
}
|
||||
else if (winRate >= 65.0)
|
||||
{
|
||||
riskScore = 75;
|
||||
riskTolerance = "Aggressiv (75/100)";
|
||||
minTf = 1;
|
||||
maxTf = 4;
|
||||
}
|
||||
|
||||
TechnicalContextInfo taInfo = new();
|
||||
FundamentalContextInfo fundInfo = new();
|
||||
SentimentContextInfo sentInfo = new();
|
||||
|
||||
string resolvedSymbol = filterResult.Symbol;
|
||||
string resolvedName = filterResult.Symbol;
|
||||
|
||||
if (newsArticle.MatchedAssets != null && newsArticle.MatchedAssets.Count > 0)
|
||||
{
|
||||
var firstAsset = newsArticle.MatchedAssets[0];
|
||||
if (!string.IsNullOrWhiteSpace(firstAsset.Name))
|
||||
{
|
||||
resolvedName = firstAsset.Name;
|
||||
if (resolvedSymbol == "UNKNOWN" || resolvedSymbol == filterResult.Isin)
|
||||
{
|
||||
resolvedSymbol = resolvedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? taResp = null;
|
||||
FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? fundResp = null;
|
||||
FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto? livePriceResp = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
var isinReq = new IsinRequest(filterResult.Isin);
|
||||
|
||||
livePriceResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto, IsinRequest>(
|
||||
"tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(3));
|
||||
|
||||
taResp = await SendRpcRequestAsync<FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto, IsinRequest>(
|
||||
"ta_GetAnalysis", isinReq, TimeSpan.FromSeconds(3));
|
||||
if (taResp?.Indicators != null)
|
||||
{
|
||||
var latestIndicator = taResp.Indicators.LastOrDefault();
|
||||
taInfo = new TechnicalContextInfo
|
||||
{
|
||||
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "50.0",
|
||||
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "NEUTRAL",
|
||||
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "0.0",
|
||||
Sma50 = (double?)latestIndicator?.Sma50,
|
||||
Sma200 = (double?)latestIndicator?.Sma200,
|
||||
DetectedPatterns = taResp.Patterns?.Select(p => new PatternContextInfo
|
||||
{
|
||||
PatternName = p.Type,
|
||||
BreakoutDirection = p.BreakoutSignal?.Direction,
|
||||
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
|
||||
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
|
||||
}).ToList() ?? new List<PatternContextInfo>()
|
||||
};
|
||||
}
|
||||
|
||||
fundResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto, IsinRequest>(
|
||||
"fundamentals_Get", isinReq, TimeSpan.FromSeconds(3));
|
||||
if (fundResp != null)
|
||||
{
|
||||
resolvedSymbol = !string.IsNullOrWhiteSpace(fundResp.Ticker) ? fundResp.Ticker : resolvedSymbol;
|
||||
resolvedName = !string.IsNullOrWhiteSpace(fundResp.CompanyName) ? fundResp.CompanyName : resolvedName;
|
||||
|
||||
fundInfo = new FundamentalContextInfo
|
||||
{
|
||||
PeRatio = (double?)fundResp.PeRatioTrailing,
|
||||
ForwardPeRatio = (double?)fundResp.PeRatioForward,
|
||||
PegRatio = (double?)fundResp.PegRatio,
|
||||
MarketCap = (double?)fundResp.MarketCapitalization,
|
||||
DebtToEquity = (double?)fundResp.DebtToEquity,
|
||||
GrossMargin = (double?)fundResp.GrossMargin,
|
||||
NetProfitMargin = (double?)fundResp.NetProfitMargin,
|
||||
ReturnOnEquity = (double?)fundResp.ReturnOnEquity,
|
||||
DividendYield = (double?)fundResp.DividendYield,
|
||||
ShortPercentOfFloat = (double?)fundResp.ShortPercentOfFloat,
|
||||
AnalystTargetMedian = (double?)fundResp.PriceTargetMedian,
|
||||
EvToEbitda = (double?)fundResp.EvToEbitda
|
||||
};
|
||||
}
|
||||
|
||||
var sentResp = await SendRpcRequestAsync<FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto, IsinRequest>(
|
||||
"sentiment_GetIsin", isinReq, TimeSpan.FromSeconds(3));
|
||||
if (sentResp != null)
|
||||
{
|
||||
sentInfo = new SentimentContextInfo
|
||||
{
|
||||
AssetSentimentScore = sentResp.CurrentSummary?.CompoundScore ?? 0.0,
|
||||
SectorSentimentScore = 0.5,
|
||||
NewsSentimentSummary = sentResp.CurrentSummary?.SentimentLabel ?? "Neutral"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Failed to fetch context data for auto screener analysis.", "AnalyzerChannel");
|
||||
}
|
||||
|
||||
var n8nRequest = new N8nAnalysisRequestDto
|
||||
{
|
||||
RequestId = analysisId,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TriggerType = "AutoScreener",
|
||||
TargetAsset = new TargetAssetInfo
|
||||
{
|
||||
Symbol = resolvedSymbol.ToUpperInvariant(),
|
||||
Name = resolvedName,
|
||||
Isin = filterResult.Isin.ToUpperInvariant(),
|
||||
Sector = filterResult.Sector
|
||||
},
|
||||
MarketContext = new MarketContextInfo
|
||||
{
|
||||
Vix = currentVix,
|
||||
MarketRegime = regime.ToString()
|
||||
},
|
||||
FilterContext = new FilterContextInfo
|
||||
{
|
||||
ImpactScore = filterResult.ImpactScore,
|
||||
RawNewsHeadline = rawHeadline
|
||||
},
|
||||
UserPreferences = new UserPreferencesInfo
|
||||
{
|
||||
RiskScore = riskScore,
|
||||
RiskTolerance = riskTolerance,
|
||||
MinTimeframeValue = minTf,
|
||||
MaxTimeframeValue = maxTf,
|
||||
TimeframeUnit = "Tage",
|
||||
TimeframeFormatted = $"{minTf}-{maxTf} Tage",
|
||||
InstrumentType = "KnockOut",
|
||||
UserNotes = "High-Conviction Screener Mode: Evaluate underlying data for strong reliable chart moves."
|
||||
},
|
||||
TradeFeedback = new TradeFeedbackInfo
|
||||
{
|
||||
TotalAssetTrades = 0,
|
||||
AssetWinRate = winRate,
|
||||
AvgReturnPercent = 0.0,
|
||||
LastTradeResult = "UNKNOWN"
|
||||
},
|
||||
TechnicalContext = taInfo,
|
||||
SentimentContext = sentInfo,
|
||||
FundamentalContext = fundInfo
|
||||
};
|
||||
|
||||
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
|
||||
|
||||
double minSignalScore = 75.0;
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||
var settings = await settingsService.GetSettingsAsync();
|
||||
minSignalScore = settings.MinSignalScore;
|
||||
}
|
||||
|
||||
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75;
|
||||
bool isHighConviction = n8nResponse != null &&
|
||||
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
|
||||
(confidenceScore * 100.0) >= minSignalScore &&
|
||||
winRate >= minSignalScore;
|
||||
|
||||
string finalSymbol = !string.IsNullOrWhiteSpace(resolvedSymbol) && resolvedSymbol != "UNKNOWN"
|
||||
? resolvedSymbol
|
||||
: (!string.IsNullOrWhiteSpace(filterResult.Symbol) && filterResult.Symbol != "UNKNOWN" ? filterResult.Symbol : filterResult.Isin);
|
||||
|
||||
string finalName = !string.IsNullOrWhiteSpace(resolvedName) && resolvedName != "UNKNOWN"
|
||||
? resolvedName
|
||||
: finalSymbol;
|
||||
|
||||
string marketRegion = filterResult.Isin.StartsWith("DE", StringComparison.OrdinalIgnoreCase) ? "GERMAN_EQUITIES" : "US_EQUITIES";
|
||||
|
||||
var supportLevels = new List<double>();
|
||||
var resistanceLevels = new List<double>();
|
||||
|
||||
double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : (fundResp?.CurrentPrice > 0 ? fundResp.CurrentPrice : 0.0m));
|
||||
if (currentPrice > 0)
|
||||
{
|
||||
supportLevels.Add(Math.Round(currentPrice * 0.98, 2));
|
||||
supportLevels.Add(Math.Round(currentPrice * 0.95, 2));
|
||||
resistanceLevels.Add(Math.Round(currentPrice * 1.03, 2));
|
||||
resistanceLevels.Add(Math.Round(currentPrice * 1.06, 2));
|
||||
}
|
||||
|
||||
if (n8nResponse?.ExecutionPlan?.EntryZone != null)
|
||||
{
|
||||
if (n8nResponse.ExecutionPlan.EntryZone.Min > 0) supportLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Min);
|
||||
if (n8nResponse.ExecutionPlan.EntryZone.Max > 0) resistanceLevels.Insert(0, (double)n8nResponse.ExecutionPlan.EntryZone.Max);
|
||||
}
|
||||
|
||||
var recommendation = new AssetRecommendationDto
|
||||
{
|
||||
Mode = "AUTO_SCREENER",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
RecommendedAsset = new RecommendedAssetInfo
|
||||
{
|
||||
Symbol = finalSymbol,
|
||||
CompanyName = finalName,
|
||||
Isin = filterResult.Isin,
|
||||
Market = marketRegion,
|
||||
Bias = string.Equals(n8nResponse?.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "BEARISH" : "BULLISH",
|
||||
ConfidenceScore = Math.Round(confidenceScore, 2),
|
||||
Timeframe = !string.IsNullOrWhiteSpace(n8nResponse?.SuggestedTimeframe) ? n8nResponse.SuggestedTimeframe : "1D"
|
||||
},
|
||||
Rationale = new RecommendationRationaleInfo
|
||||
{
|
||||
PatternDetected = taInfo.DetectedPatterns?.Count > 0
|
||||
? string.Join(", ", taInfo.DetectedPatterns.Select(p => p.PatternName))
|
||||
: (!string.IsNullOrWhiteSpace(n8nResponse?.DetailedAnalysis?.TechnicalRationale) ? n8nResponse.DetailedAnalysis.TechnicalRationale : "Multi-Timeframe Trend & Volume Confluence"),
|
||||
VixContext = $"VIX at {currentVix:F1} ({regime} volatility environment)",
|
||||
KeyTechnicalLevels = new KeyTechnicalLevelsInfo
|
||||
{
|
||||
Support = supportLevels.Distinct().ToList(),
|
||||
Resistance = resistanceLevels.Distinct().ToList()
|
||||
},
|
||||
Summary = !string.IsNullOrWhiteSpace(n8nReasoning(n8nResponse))
|
||||
? n8nResponse!.AiReasoning
|
||||
: "High conviction setup based on multi-timeframe technical confluence, sentiment, and fundamental data."
|
||||
},
|
||||
ActionRequired = isHighConviction ? "PROMPT_USER_FOR_MANUAL_TRADE" : "NO_ACTION"
|
||||
};
|
||||
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
|
||||
|
||||
var analysisEntity = new AnalysisEntity
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
EventId = eventId,
|
||||
Sector = filterResult.Sector,
|
||||
Symbol = finalSymbol,
|
||||
Isin = filterResult.Isin,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = filterResult.ImpactScore,
|
||||
WinRate = winRate,
|
||||
RawDataJson = payloadStr,
|
||||
AiOutputJson = JsonSerializer.Serialize(recommendation),
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
N8nEvalScore = n8nResponse?.EvalScore ?? 0,
|
||||
N8nDecision = n8nResponse?.AiDecision ?? "None",
|
||||
IsTradeProposed = isHighConviction,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
dbContext.Analyses.Add(analysisEntity);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (isHighConviction && n8nResponse != null)
|
||||
{
|
||||
var autoProposalDto = new TradeProposalDto
|
||||
{
|
||||
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
|
||||
AnalysisId = analysisId,
|
||||
EventId = eventId,
|
||||
Sector = filterResult.Sector,
|
||||
Symbol = finalSymbol,
|
||||
Isin = filterResult.Isin,
|
||||
CompanyName = finalName,
|
||||
EntryPrice = (decimal)currentPrice,
|
||||
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
||||
Status = "Proposed",
|
||||
RiskTolerance = n8nResponse.SuggestedRisk ?? "Balanced",
|
||||
Timeframe = $"{minTf}-{maxTf} Tage",
|
||||
InstrumentType = "KnockOut",
|
||||
WinRate = winRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 180,
|
||||
Reasoning = n8nResponse.AiReasoning ?? "Auto-Screener High Conviction Trade",
|
||||
StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
|
||||
TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
|
||||
EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
|
||||
EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
|
||||
TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
|
||||
RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
|
||||
MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
|
||||
TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
|
||||
FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
|
||||
RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}";
|
||||
await PublishAsync(propTopic, autoProposalDto);
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", "AnalyzerChannel", autoProposalDto.TradeId, propTopic);
|
||||
}
|
||||
|
||||
if (isHighConviction)
|
||||
{
|
||||
string recTopic = $"finlytic/recommendations/auto/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}";
|
||||
await PublishAsync(recTopic, recommendation);
|
||||
await PublishAsync("finlytic/recommendations/auto", recommendation);
|
||||
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] [RECOMMENDED] High-Conviction Opportunity found for {Symbol} (Bias: {Bias}, Confidence: {Score:F2}). Published to {Topic}",
|
||||
"AnalyzerChannel", finalSymbol, recommendation.RecommendedAsset.Bias, recommendation.RecommendedAsset.ConfidenceScore, recTopic);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] [DROPPED] Low-conviction signal for {Symbol} dropped (Confidence: {Score:F2}, Action: NO_ACTION)",
|
||||
"AnalyzerChannel", finalSymbol, recommendation.RecommendedAsset.ConfidenceScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string n8nReasoning(N8nAnalysisResponseDto? resp) => resp?.AiReasoning ?? string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=finlytic_analyzer;Username=admin;Password=admin"
|
||||
},
|
||||
"MQTT": {
|
||||
"Host": "localhost",
|
||||
"Port": "1883",
|
||||
"ClientId": "finlytic_analyzer"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"epochs": [ {
|
||||
"calculation_time": "13430050656556383",
|
||||
"config_version": 0,
|
||||
"model_version": "0",
|
||||
"padded_top_topics_start_index": 0,
|
||||
"taxonomy_version": 0,
|
||||
"top_topics_and_observing_domains": [ ]
|
||||
} ],
|
||||
"hex_encoded_hmac_key": "B6F2F708445BA6FD9AE93FC13F58B9FFE00F622BAC8C7EDCF364F58F7F466A75",
|
||||
"next_scheduled_calculation_time": "13430655456556985"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
||||
@@ -0,0 +1,9 @@
|
||||
2026/08/05-21:31:05.209 6624 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\EdgeCoupons/coupons_data.db/MANIFEST-000001
|
||||
2026/08/05-21:31:05.210 6624 Recovering log #13
|
||||
2026/08/05-21:31:05.211 6624 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\EdgeCoupons/coupons_data.db/000013.log
|
||||
2026/08/05-21:31:05.211 6624 Delete type=0 #4
|
||||
2026/08/05-21:31:05.212 6624 Delete type=0 #7
|
||||
2026/08/05-21:31:05.212 6624 Delete type=0 #10
|
||||
2026/08/05-21:31:25.638 2bf0 Level-0 table #17: started
|
||||
2026/08/05-21:31:25.642 2bf0 Level-0 table #17: 683332 bytes OK
|
||||
2026/08/05-21:31:25.644 2bf0 Delete type=0 #13
|
||||
@@ -0,0 +1,8 @@
|
||||
2026/08/04-22:51:57.518 4210 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\EdgeCoupons/coupons_data.db/MANIFEST-000001
|
||||
2026/08/04-22:51:57.519 4210 Recovering log #10
|
||||
2026/08/04-22:51:57.519 4210 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\EdgeCoupons/coupons_data.db/000010.log
|
||||
2026/08/04-22:51:57.520 4210 Delete type=0 #4
|
||||
2026/08/04-22:51:57.520 4210 Delete type=0 #7
|
||||
2026/08/04-23:39:18.422 2ad8 Level-0 table #14: started
|
||||
2026/08/04-23:39:18.425 2ad8 Level-0 table #14: 683332 bytes OK
|
||||
2026/08/04-23:39:18.428 2ad8 Delete type=0 #10
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
{"logTime": "0801/093734", "session": "START"}
|
||||
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:352 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
|
||||
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1398 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
|
||||
{"logTime": "0801/093734", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1400 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
|
||||
{"logTime": "0801/093736", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "No valid session file found: SessionRestore"}
|
||||
{"logTime": "0801/093736", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "Last session file not found for SessionType : SessionRestore"}
|
||||
{"logTime": "0801/123008", "session": "END"}
|
||||
{"logTime": "0801/202055", "session": "START"}
|
||||
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:352 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
|
||||
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1398 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
|
||||
{"logTime": "0801/202055", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1400 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
|
||||
{"logTime": "0801/202057", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:275 operator()", "message": "Valid session file found: SessionRestore"}
|
||||
{"logTime": "0801/210300", "session": "END"}
|
||||
{"logTime": "0803/201011", "session": "START"}
|
||||
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
|
||||
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
|
||||
{"logTime": "0803/201011", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
|
||||
{"logTime": "0803/201014", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
|
||||
{"logTime": "0803/201014", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
|
||||
{"logTime": "0803/202718", "session": "END"}
|
||||
{"logTime": "0804/205152", "session": "START"}
|
||||
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
|
||||
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
|
||||
{"logTime": "0804/205152", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
|
||||
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
|
||||
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430089257699509, for SessionType SessionRestore"}
|
||||
{"logTime": "0804/205154", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
|
||||
{"logTime": "0804/213942", "session": "END"}
|
||||
{"logTime": "0805/193102", "session": "START"}
|
||||
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:353 EdgeLogLastSessionExitTypeOnStartup", "message": "Previous Session Exit Type: PreviousSessionExitType::kNormalBrowserShutDown"}
|
||||
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1431 DetermineURLsAndLaunch", "message": "Startup Preference: 0"}
|
||||
{"logTime": "0805/193102", "level": "INFO", "location": "chrome\\browser\\ui\\startup\\startup_browser_creator_impl.cc:1433 DetermineURLsAndLaunch", "message": "Browser Open Behavior: 0"}
|
||||
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Valid session file found: SessionRestore"}
|
||||
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430261414417274, for SessionType SessionRestore"}
|
||||
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430089257699509, for SessionType SessionRestore"}
|
||||
{"logTime": "0805/193104", "level": "INFO", "location": "components\\sessions\\core\\command_storage_manager.cc:618 operator()", "message": "Delete session file Session_13430050656644037, for SessionType SessionRestore"}
|
||||
{"logTime": "0805/205906", "session": "END"}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/05-21:31:02.084 6350 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Rules/MANIFEST-000001
|
||||
2026/08/05-21:31:02.084 6350 Recovering log #3
|
||||
2026/08/05-21:31:02.085 6350 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Rules/000003.log
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/04-22:51:52.185 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Rules/MANIFEST-000001
|
||||
2026/08/04-22:51:52.185 334 Recovering log #3
|
||||
2026/08/04-22:51:52.186 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Rules/000003.log
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/05-21:31:02.091 6350 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Scripts/MANIFEST-000001
|
||||
2026/08/05-21:31:02.093 6350 Recovering log #3
|
||||
2026/08/05-21:31:02.093 6350 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension Scripts/000003.log
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/04-22:51:52.191 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Scripts/MANIFEST-000001
|
||||
2026/08/04-22:51:52.192 334 Recovering log #3
|
||||
2026/08/04-22:51:52.192 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension Scripts/000003.log
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000001
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/05-21:31:02.379 2bf0 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension State/MANIFEST-000001
|
||||
2026/08/05-21:31:02.379 2bf0 Recovering log #3
|
||||
2026/08/05-21:31:02.380 2bf0 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.2993d3\flutter_tools_chrome_device.c2da8a67\Default\Extension State/000003.log
|
||||
@@ -0,0 +1,3 @@
|
||||
2026/08/04-22:51:52.441 334 Reusing MANIFEST C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension State/MANIFEST-000001
|
||||
2026/08/04-22:51:52.441 334 Recovering log #3
|
||||
2026/08/04-22:51:52.441 334 Reusing old log C:\Users\larsh\AppData\Local\Temp\flutter_tools.20e8229a\flutter_tools_chrome_device.7d5ee104\Default\Extension State/000003.log
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user