feat(analyzer): dynamic settings, IFinlyticLogger, live log streaming, and EF migration

This commit is contained in:
2026-08-15 21:30:16 +02:00
parent 62e030e2cf
commit 0d370d09e7
13 changed files with 687 additions and 215 deletions
@@ -9,30 +9,32 @@ using FinlyticCore.Dtos;
using FinlyticCore.Dtos.TechnicalAnalysis;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
using FinlyticCore.Services;
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 IFinlyticLogger<ActiveTradeMonitorWorker> _finlyticLogger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly AnalyzerMqttClient _mqttClient;
public ActiveTradeMonitorWorker(ILogger<ActiveTradeMonitorWorker> logger, IServiceScopeFactory scopeFactory,
public ActiveTradeMonitorWorker(
IFinlyticLogger<ActiveTradeMonitorWorker> finlyticLogger,
IServiceScopeFactory scopeFactory,
AnalyzerMqttClient mqttClient)
{
_logger = logger;
_finlyticLogger = finlyticLogger;
_scopeFactory = scopeFactory;
_mqttClient = mqttClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker started.", "AnalyzerChannel");
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker started.");
try
{
@@ -51,7 +53,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "[{Channel}] Error in ActiveTradeMonitorWorker loop.", "AnalyzerChannel");
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Error in ActiveTradeMonitorWorker loop.");
}
try
@@ -64,24 +66,22 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
}
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker stopped.", "AnalyzerChannel");
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker stopped.");
}
private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken)
{
if (!_mqttClient.IsConnected)
{
_logger.LogWarning("[{Channel}] Skipping trade monitoring. RPC client not connected.", "AnalyzerChannel");
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Skipping trade monitoring. RPC client not connected.");
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"),
@@ -93,13 +93,11 @@ public class ActiveTradeMonitorWorker : BackgroundService
if (trades.Count == 0)
{
_logger.LogInformation("[{Channel}] No active or proposed global trades found to monitor.",
"AnalyzerChannel");
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] No active or proposed global trades found to monitor.");
return;
}
_logger.LogInformation("[{Channel}] Found {Count} trades to monitor. Starting evaluation...", "AnalyzerChannel",
trades.Count);
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Found {Count} trades to monitor. Starting evaluation...", trades.Count);
using var scope = _scopeFactory.CreateScope();
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
@@ -115,8 +113,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to monitor trade {TradeId} ({Symbol}).", "AnalyzerChannel",
trade.TradeId, trade.Symbol);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Failed to monitor trade {TradeId} ({Symbol}).", trade.TradeId, trade.Symbol);
}
}
}
@@ -124,22 +121,18 @@ public class ActiveTradeMonitorWorker : BackgroundService
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
IVixTrackerService vixService, 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",
@@ -176,7 +169,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
}
// 3. Run AI evaluation for soft/dynamic updates
var taResult = await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
@@ -225,8 +217,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken);
if (aiResponse == null)
{
_logger.LogWarning("[{Channel}] AI evaluation returned null for {TradeId}. Skipping update.",
"AnalyzerChannel", trade.TradeId);
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] AI evaluation returned null for {TradeId}. Skipping update.", trade.TradeId);
return;
}
@@ -235,7 +226,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
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);
@@ -256,13 +246,11 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
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;
@@ -271,7 +259,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
else
{
// Bei Short darf der StopLoss nur NACH UNTEN angepasst werden
if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss)
{
newStopLoss = proposedSl;
@@ -310,18 +297,16 @@ public class ActiveTradeMonitorWorker : BackgroundService
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);
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}",
trade.TradeId, topic, recommendation, reasoning);
}
private static int EstimateMaxHoldingDays(string timeframe)
{
if (string.IsNullOrWhiteSpace(timeframe)) return 14; // Default
if (string.IsNullOrWhiteSpace(timeframe)) return 14;
string tfLower = timeframe.ToLowerInvariant();
int multiplier = 1;
@@ -351,7 +336,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
if (maxNum == 0) maxNum = 14;
if (multiplier == 1 && maxNum < 3) maxNum = 3; // Mindestens 3 Tage Kulanz
if (multiplier == 1 && maxNum < 3) maxNum = 3;
return maxNum * multiplier;
}
@@ -1,26 +1,33 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAnalyzer.Util;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Services;
using FinlyticCore.Util;
using Microsoft.Extensions.Configuration;
namespace FinlyticAnalyzer.Services;
public class N8nEvaluationService : IN8nEvaluationService
{
private readonly HttpClient _httpClient;
private readonly ILogger<N8nEvaluationService> _logger;
private readonly IFinlyticLogger<N8nEvaluationService> _finlyticLogger;
private readonly string _webhookUrl;
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, ILogger<N8nEvaluationService> logger)
public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, IFinlyticLogger<N8nEvaluationService> finlyticLogger)
{
_httpClient = httpClient;
_logger = logger;
_finlyticLogger = finlyticLogger;
_webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? string.Empty;
if (string.IsNullOrWhiteSpace(_webhookUrl))
{
_logger.LogWarning("[{Channel}] N8N:WebhookUrl configuration is missing or empty.", "AnalyzerChannel");
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] N8N:WebhookUrl configuration is missing or empty.");
}
// Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n
_httpClient.Timeout = TimeSpan.FromSeconds(45);
}
@@ -31,16 +38,15 @@ public class N8nEvaluationService : IN8nEvaluationService
{
if (string.IsNullOrWhiteSpace(_webhookUrl))
{
_logger.LogError("[{Channel}] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", "AnalyzerChannel", request.TargetAsset.Symbol);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", request.TargetAsset.Symbol);
return null;
}
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);
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...",
request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, _webhookUrl);
// Typsichere AOT-Serialisierung verwenden
using var content = JsonContent.Create(
request,
FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto);
@@ -53,11 +59,10 @@ public class N8nEvaluationService : IN8nEvaluationService
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);
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", 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(']'))
{
@@ -74,28 +79,28 @@ public class N8nEvaluationService : IN8nEvaluationService
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);
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}",
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);
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}",
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);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", request.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Error calling n8n AI Evaluation Webhook for Request {RequestId}", "AnalyzerChannel", request.RequestId);
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Error calling n8n AI Evaluation Webhook for Request {RequestId}", request.RequestId);
}
return null; // Signals RPC/Service failure to caller
return null;
}
private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning)
@@ -1,21 +1,22 @@
using System;
using System.Collections.Concurrent;
using FinlyticAnalyzer.Util;
using FinlyticCore.Dtos.News;
using FinlyticCore.Models.Analyzer;
using Microsoft.Extensions.Logging;
using FinlyticCore.Services;
namespace FinlyticAnalyzer.Services;
public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
{
private readonly ILogger<ThreeLayerFilterEngine> _logger;
private readonly IFinlyticLogger<ThreeLayerFilterEngine> _finlyticLogger;
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
private readonly object _cleanupLock = new();
private DateTime _lastCleanupTime = DateTime.UtcNow;
public ThreeLayerFilterEngine(ILogger<ThreeLayerFilterEngine> logger)
public ThreeLayerFilterEngine(IFinlyticLogger<ThreeLayerFilterEngine> finlyticLogger)
{
_logger = logger;
_finlyticLogger = finlyticLogger;
}
/// <summary>
@@ -25,9 +26,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
{
var result = new FilterResult();
// -------------------------------------------------------------
// Layer 1: Relevance, ISIN & Deduplication
// -------------------------------------------------------------
if (newsEvent == null || newsEvent.Id == Guid.Empty)
{
result.Passed = false;
@@ -38,7 +36,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
string eventId = newsEvent.Id.ToString();
var now = DateTime.UtcNow;
// Safely clean up dictionary every 30 minutes (thread-safe lock)
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
{
lock (_cleanupLock)
@@ -50,7 +47,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
}
}
// Deduplication check (keep history for 12 hours)
if (_seenEvents.TryGetValue(eventId, out var prevTime) && (now - prevTime).TotalHours < 12.0)
{
result.Passed = false;
@@ -63,7 +59,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
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];
@@ -71,7 +66,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
assetName = !string.IsNullOrWhiteSpace(firstAsset.Name) ? firstAsset.Name.Trim() : string.Empty;
}
// Mandatory check: Must have a valid ISIN
if (string.IsNullOrWhiteSpace(isin))
{
result.Passed = false;
@@ -80,13 +74,9 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
}
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
result.Sector = "General";
// -------------------------------------------------------------
// Layer 2: Impact & Dynamic VIX Threshold
// -------------------------------------------------------------
double impactScore = newsEvent.Confidence ?? 0.75;
if (impactScore <= 0) impactScore = 0.75;
@@ -106,14 +96,11 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
{
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);
_ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} (ISIN: {Isin}) rejected by Layer 2 filter. Impact: {Impact:F2}, Threshold: {Threshold:F2}, Regime: {Regime}",
eventId, isin, impactScore, requiredThreshold, regime);
return result;
}
// -------------------------------------------------------------
// Layer 3: Dynamic Parameter & Risk Engine
// -------------------------------------------------------------
result.RiskTolerance = regime switch
{
VixMarketRegime.Panic => "Conservative",
@@ -125,8 +112,8 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
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);
_ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} passed 3-Layer Filter for ISIN {Isin}. Impact: {Impact:F2}, Regime: {Regime}",
eventId, result.Isin, impactScore, regime);
return result;
}
+11 -12
View File
@@ -1,25 +1,26 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAnalyzer.Util;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Services;
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 readonly IFinlyticLogger<VixTrackerService> _finlyticLogger;
private decimal _currentVix = 18.5m; // Default: Normal Regime
private decimal _currentVix = 18.5m;
private VixMarketRegime _currentRegime = VixMarketRegime.Normal;
private readonly object _lock = new();
public VixTrackerService(YahooFinanceClient yahooClient, ILogger<VixTrackerService> logger)
public VixTrackerService(YahooFinanceClient yahooClient, IFinlyticLogger<VixTrackerService> finlyticLogger)
{
_yahooClient = yahooClient;
_logger = logger;
_finlyticLogger = finlyticLogger;
}
public decimal GetCurrentVix()
@@ -52,13 +53,13 @@ public class VixTrackerService : IVixTrackerService
if (oldRegime != _currentRegime)
{
_logger.LogWarning("[{Channel}] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})",
"AnalyzerChannel", oldRegime, _currentRegime, _currentVix);
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})",
oldRegime, _currentRegime, _currentVix);
}
else if (Math.Abs(oldVix - vixValue) >= 0.5m)
{
_logger.LogInformation("[{Channel}] VIX aktualisiert: {Vix:F2} (Regime: {Regime})",
"AnalyzerChannel", _currentVix, _currentRegime);
_ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] VIX aktualisiert: {Vix:F2} (Regime: {Regime})",
_currentVix, _currentRegime);
}
}
}
@@ -77,12 +78,10 @@ public class VixTrackerService : IVixTrackerService
}
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());
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[VixTrackerService] Fehler beim Abfragen von ^VIX über YahooFinanceClient. Nutze gecachten Wert {Vix}.", GetCurrentVix());
}
return GetCurrentVix();
+13 -22
View File
@@ -3,15 +3,16 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using FinlyticAnalyzer.Util;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
using Microsoft.Extensions.Logging;
using FinlyticCore.Services;
namespace FinlyticAnalyzer.Services;
public class WinRateCalculator : IWinRateCalculator
{
private readonly ILogger<WinRateCalculator> _logger;
private readonly IFinlyticLogger<WinRateCalculator> _finlyticLogger;
private readonly string _feedbackDir;
private readonly object _cacheLock = new();
@@ -19,9 +20,9 @@ public class WinRateCalculator : IWinRateCalculator
private DateTime _lastCacheTime = DateTime.MinValue;
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3);
public WinRateCalculator(ILogger<WinRateCalculator> logger)
public WinRateCalculator(IFinlyticLogger<WinRateCalculator> finlyticLogger)
{
_logger = logger;
_finlyticLogger = finlyticLogger;
_feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback");
if (!Directory.Exists(_feedbackDir))
{
@@ -31,7 +32,6 @@ public class WinRateCalculator : IWinRateCalculator
/// <summary>
/// Calculates the win rate for a given sector and symbol under the specified market regime.
/// Uses cached feedback records (3-minute TTL) to prevent disk I/O bottlenecks.
/// </summary>
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
{
@@ -53,27 +53,23 @@ public class WinRateCalculator : IWinRateCalculator
{
try
{
// 1. N8n AI Confidence Score (Weight: 40%)
double n8nComponent = 62.0;
if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0)
{
n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value;
}
// 2. Technical Score (Weight: 30%)
double taComponent = 60.0;
if (technicalScore.HasValue && technicalScore.Value > 0)
{
taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value;
}
// 3. Sentiment Score (Weight: 15%)
double sentComponent = 58.0;
if (sentimentScore.HasValue)
{
if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0)
{
// Map sentiment from -1.0..+1.0 into 35.0..85.0
sentComponent = 50.0 + (sentimentScore.Value * 25.0);
}
else
@@ -82,29 +78,25 @@ public class WinRateCalculator : IWinRateCalculator
}
}
// 4. Fundamental Score (Weight: 15%)
double fundComponent = 60.0;
if (fundamentalScore.HasValue && fundamentalScore.Value > 0)
{
fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value;
}
// Multi-factor weighted composite
double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15);
// 5. Market Regime & Volatility Adjustment
double vixAdjustment = regime switch
{
VixMarketRegime.LowVol => +4.0, // Calm trending market
VixMarketRegime.Normal => +1.5, // Normal conditions
VixMarketRegime.HighVol => -3.5, // Increased whipsaws
VixMarketRegime.Panic => -8.0, // High panic / uncertainty
VixMarketRegime.LowVol => +4.0,
VixMarketRegime.Normal => +1.5,
VixMarketRegime.HighVol => -3.5,
VixMarketRegime.Panic => -8.0,
_ => 0.0
};
composite += vixAdjustment;
// 6. Historical track record calibration (if available in feedback records)
var records = GetCachedOrLoadRecords();
if (records.Count > 0)
{
@@ -120,17 +112,16 @@ public class WinRateCalculator : IWinRateCalculator
}
}
// Clamp between realistic financial statistical bounds (45.0% to 92.0%)
double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0);
_logger.LogInformation("[{Channel}] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]",
"AnalyzerChannel", symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime);
_ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[WinRateCalculator] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]",
symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime);
return finalWinRate;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[{Channel}] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", "AnalyzerChannel", symbol);
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", symbol);
return 65.0;
}
}
@@ -162,7 +153,7 @@ public class WinRateCalculator : IWinRateCalculator
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[{Channel}] Failed to read or parse feedback file '{File}'", "AnalyzerChannel", file);
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Failed to read or parse feedback file '{File}'", file);
}
}
}