167 lines
6.1 KiB
C#
167 lines
6.1 KiB
C#
using System;
|
|
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 FinlyticCore.Services;
|
|
|
|
namespace FinlyticAnalyzer.Services;
|
|
|
|
public class WinRateCalculator : IWinRateCalculator
|
|
{
|
|
private readonly IFinlyticLogger<WinRateCalculator> _finlyticLogger;
|
|
private readonly string _feedbackDir;
|
|
|
|
private readonly object _cacheLock = new();
|
|
private List<TradeFeedbackRecord>? _cachedRecords;
|
|
private DateTime _lastCacheTime = DateTime.MinValue;
|
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3);
|
|
|
|
public WinRateCalculator(IFinlyticLogger<WinRateCalculator> finlyticLogger)
|
|
{
|
|
_finlyticLogger = finlyticLogger;
|
|
_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)
|
|
{
|
|
return CalculateDynamicWinRate(sector, symbol, regime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
|
|
/// </summary>
|
|
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
|
|
{
|
|
double n8nComponent = 62.0;
|
|
if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0)
|
|
{
|
|
n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value;
|
|
}
|
|
|
|
double taComponent = 60.0;
|
|
if (technicalScore.HasValue && technicalScore.Value > 0)
|
|
{
|
|
taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value;
|
|
}
|
|
|
|
double sentComponent = 58.0;
|
|
if (sentimentScore.HasValue)
|
|
{
|
|
if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0)
|
|
{
|
|
sentComponent = 50.0 + (sentimentScore.Value * 25.0);
|
|
}
|
|
else
|
|
{
|
|
sentComponent = sentimentScore.Value;
|
|
}
|
|
}
|
|
|
|
double fundComponent = 60.0;
|
|
if (fundamentalScore.HasValue && fundamentalScore.Value > 0)
|
|
{
|
|
fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value;
|
|
}
|
|
|
|
double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15);
|
|
|
|
double vixAdjustment = regime switch
|
|
{
|
|
VixMarketRegime.LowVol => +4.0,
|
|
VixMarketRegime.Normal => +1.5,
|
|
VixMarketRegime.HighVol => -3.5,
|
|
VixMarketRegime.Panic => -8.0,
|
|
_ => 0.0
|
|
};
|
|
|
|
composite += vixAdjustment;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0);
|
|
|
|
_ = _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)
|
|
{
|
|
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", symbol);
|
|
return 65.0;
|
|
}
|
|
}
|
|
|
|
private List<TradeFeedbackRecord> GetCachedOrLoadRecords()
|
|
{
|
|
lock (_cacheLock)
|
|
{
|
|
if (_cachedRecords != null && (DateTime.UtcNow - _lastCacheTime) < CacheTtl)
|
|
{
|
|
return _cachedRecords;
|
|
}
|
|
|
|
var loadedList = new List<TradeFeedbackRecord>();
|
|
|
|
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<TradeFeedbackRecord[]>(content);
|
|
if (records != null && records.Length > 0)
|
|
{
|
|
loadedList.AddRange(records);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Failed to read or parse feedback file '{File}'", file);
|
|
}
|
|
}
|
|
}
|
|
|
|
_cachedRecords = loadedList;
|
|
_lastCacheTime = DateTime.UtcNow;
|
|
return _cachedRecords;
|
|
}
|
|
}
|
|
}
|