refactor: save current workspace state including FinlyticAnalyzer fixes, FinlyticApp trade route alignment, and DTO audit documentation

This commit is contained in:
2026-08-12 18:30:42 +02:00
parent a9553e9fbf
commit 3d8af3940b
163 changed files with 3421 additions and 1751 deletions
@@ -103,6 +103,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
using var scope = _scopeFactory.CreateScope();
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
foreach (var trade in trades)
{
@@ -110,7 +111,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
try
{
await ProcessTradeAsync(trade, n8nService, cancellationToken);
await ProcessTradeAsync(trade, n8nService, vixService, cancellationToken);
}
catch (Exception ex)
{
@@ -121,7 +122,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
}
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
CancellationToken cancellationToken)
IVixTrackerService vixService, CancellationToken cancellationToken)
{
// 1. Get Live Price
var livePriceReq = new IsinRequest(trade.Isin);
@@ -210,8 +211,8 @@ public class ActiveTradeMonitorWorker : BackgroundService
},
MarketContext = new MarketContextInfo
{
Vix = trade.VixValue,
MarketRegime = trade.VixRegime.ToString()
Vix = (double)vixService.CurrentVix,
MarketRegime = vixService.CurrentRegime.ToString()
},
UserPreferences = new UserPreferencesInfo
{
+18 -7
View File
@@ -39,11 +39,15 @@ public class SettingsDbService : ISettingsDbService
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
if (settings == null)
{
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid() };
settings = new AnalyzerSettingsEntity { Id = Guid.NewGuid(), UpdatedAt = DateTime.UtcNow };
_context.Settings.Add(settings);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
}
// Synchronize in-memory static filter values on get
SyncLogFilters(settings);
return settings;
}
@@ -52,10 +56,13 @@ public class SettingsDbService : ISettingsDbService
/// </summary>
public async Task<AnalyzerSettingsEntity> SaveSettingsAsync(AnalyzerSettingsEntity settings)
{
var existing = await _context.Settings.FirstOrDefaultAsync();
var existing = await _context.Settings.FirstOrDefaultAsync(s => s.Id == settings.Id)
?? await _context.Settings.FirstOrDefaultAsync();
if (existing == null)
{
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
settings.UpdatedAt = DateTime.UtcNow;
_context.Settings.Add(settings);
}
else
@@ -67,19 +74,23 @@ public class SettingsDbService : ISettingsDbService
existing.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
existing.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
existing.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
existing.UpdatedAt = settings.UpdatedAt;
_context.Settings.Update(existing);
existing.UpdatedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
// Synchronize in-memory static filter values
SyncLogFilters(settings);
return settings;
}
private static void SyncLogFilters(AnalyzerSettingsEntity settings)
{
LogCategoryFilter.EnableLogMqttHealthPing = settings.EnableLogMqttHealthPing;
LogCategoryFilter.EnableLogMqttGeneral = settings.EnableLogMqttGeneral;
LogCategoryFilter.EnableLogAnalyzerAuto = settings.EnableLogAnalyzerAuto;
LogCategoryFilter.EnableLogAnalyzerManual = settings.EnableLogAnalyzerManual;
LogCategoryFilter.EnableLogDatabaseOps = settings.EnableLogDatabaseOps;
return settings;
}
/// <summary>
@@ -10,6 +10,7 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
{
private readonly ILogger<ThreeLayerFilterEngine> _logger;
private readonly ConcurrentDictionary<string, DateTime> _seenEvents = new();
private readonly object _cleanupLock = new();
private DateTime _lastCleanupTime = DateTime.UtcNow;
public ThreeLayerFilterEngine(ILogger<ThreeLayerFilterEngine> logger)
@@ -37,10 +38,16 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine
string eventId = newsEvent.Id.ToString();
var now = DateTime.UtcNow;
// Safely clean up dictionary every 30 minutes
// Safely clean up dictionary every 30 minutes (thread-safe lock)
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
{
CleanupSeenEvents(now);
lock (_cleanupLock)
{
if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000)
{
CleanupSeenEvents(now);
}
}
}
// Deduplication check (keep history for 12 hours)
+54 -26
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -12,6 +13,11 @@ public class WinRateCalculator : IWinRateCalculator
{
private readonly ILogger<WinRateCalculator> _logger;
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(ILogger<WinRateCalculator> logger)
{
@@ -25,41 +31,25 @@ 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)
{
try
{
if (!Directory.Exists(_feedbackDir)) return 65.0;
var records = GetCachedOrLoadRecords();
if (records.Count == 0) return 65.0;
var jsonFiles = Directory.GetFiles(_feedbackDir, "*.json", SearchOption.AllDirectories);
if (jsonFiles.Length == 0) return 65.0;
var matching = records.Where(r =>
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
r.VixRegime == regime).ToList();
int totalTrades = 0;
int winningTrades = 0;
foreach (var file in jsonFiles)
if (matching.Count > 0)
{
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;
int winningTrades = matching.Count(r => r.IsWin);
double calculatedWinRate = (double)winningTrades / matching.Count * 100.0;
_logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, totalTrades);
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, matching.Count);
return Math.Round(calculatedWinRate, 1);
}
}
@@ -70,4 +60,42 @@ public class WinRateCalculator : IWinRateCalculator
return 65.0; // Default baseline win-rate
}
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)
{
_logger.LogWarning(ex, "[{Channel}] Failed to read or parse feedback file '{File}'", "AnalyzerChannel", file);
}
}
}
_cachedRecords = loadedList;
_lastCacheTime = DateTime.UtcNow;
return _cachedRecords;
}
}
}