using System; using System.Collections.Generic; 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 _logger; private readonly string _feedbackDir; private readonly object _cacheLock = new(); private List? _cachedRecords; private DateTime _lastCacheTime = DateTime.MinValue; private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3); public WinRateCalculator(ILogger logger) { _logger = logger; _feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback"); if (!Directory.Exists(_feedbackDir)) { Directory.CreateDirectory(_feedbackDir); } } /// /// 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. /// public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime) { return CalculateDynamicWinRate(sector, symbol, regime); } /// /// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime. /// public double CalculateDynamicWinRate( string sector, string symbol, VixMarketRegime regime, double? n8nEvalScore = null, double? technicalScore = null, double? sentimentScore = null, double? fundamentalScore = null, string signalType = "BUY") { 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 { sentComponent = sentimentScore.Value; } } // 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 _ => 0.0 }; composite += vixAdjustment; // 6. Historical track record calibration (if available in feedback records) var records = GetCachedOrLoadRecords(); if (records.Count > 0) { var matching = records.Where(r => string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) && r.VixRegime == regime).ToList(); if (matching.Count >= 5) { int winningTrades = matching.Count(r => r.IsWin); double historicalWinRate = (double)winningTrades / matching.Count * 100.0; composite = (composite * 0.75) + (historicalWinRate * 0.25); } } // 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); return finalWinRate; } catch (Exception ex) { _logger.LogWarning(ex, "[{Channel}] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", "AnalyzerChannel", symbol); return 65.0; } } private List GetCachedOrLoadRecords() { lock (_cacheLock) { if (_cachedRecords != null && (DateTime.UtcNow - _lastCacheTime) < CacheTtl) { return _cachedRecords; } var loadedList = new List(); if (Directory.Exists(_feedbackDir)) { var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories); foreach (var file in jsonFiles) { try { var content = File.ReadAllText(file); var records = JsonSerializer.Deserialize(content); if (records != null && records.Length > 0) { loadedList.AddRange(records); } } catch (Exception ex) { _logger.LogWarning(ex, "[{Channel}] Failed to read or parse feedback file '{File}'", "AnalyzerChannel", file); } } } _cachedRecords = loadedList; _lastCacheTime = DateTime.UtcNow; return _cachedRecords; } } }