using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Dtos.Simulation; using FinlyticCore.Services; using FinlyticSimulation.Database; using FinlyticSimulation.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace FinlyticSimulation.Services; /// /// Keeps the backtest-reliability matrix (SimulationStrategyMatrixEntity) fresh on a schedule, instead /// of it only ever being updated as a side effect of a human manually re-running the exact same backtest (the /// previous behavior - there was no scheduled/background recompute job at all). Every stale (Isin, StrategyKey, /// Timeframe) row already present in the matrix gets a fresh 2-year backtest re-run; rows are never added /// speculatively for combinations nobody has ever backtested (Rules.md ยง4 - this refreshes existing data, it /// does not invent new coverage). /// public class ReliabilityMatrixRecomputeBackgroundService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ISettingsService _settingsService; private readonly IFinlyticLogger _logger; public ReliabilityMatrixRecomputeBackgroundService( IServiceScopeFactory scopeFactory, ISettingsService settingsService, IFinlyticLogger logger) { _scopeFactory = scopeFactory; _settingsService = settingsService; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _logger.LogInfoAsync(SimulationSettingKeys.MatrixChannel, "[MatrixRecompute] Starting reliability matrix recompute background service."); // Initial grace delay for MQTT/DB connections to stabilize. await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { var enabled = await _settingsService.GetSettingAsync(SimulationSettingKeys.EnableScheduledMatrixRecompute, stoppingToken); if (enabled) { await RecomputeStaleEntriesAsync(stoppingToken); } else { await _logger.LogInfoAsync(SimulationSettingKeys.MatrixChannel, "[MatrixRecompute] Scheduled recompute is disabled via settings. Skipping this cycle."); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { await _logger.LogErrorAsync(SimulationSettingKeys.MatrixChannel, ex, "[MatrixRecompute] Unexpected error in recompute cycle."); } var checkIntervalMinutes = await _settingsService.GetSettingAsync(SimulationSettingKeys.MatrixRecomputeCheckIntervalMinutes, stoppingToken); try { await Task.Delay(TimeSpan.FromMinutes(Math.Max(5, checkIntervalMinutes)), stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } } await _logger.LogInfoAsync(SimulationSettingKeys.MatrixChannel, "[MatrixRecompute] Reliability matrix recompute background service stopped."); } private async Task RecomputeStaleEntriesAsync(CancellationToken stoppingToken) { var intervalHours = await _settingsService.GetSettingAsync(SimulationSettingKeys.MatrixRecomputeIntervalHours, stoppingToken); var staleCutoff = DateTime.UtcNow.AddHours(-Math.Max(1, intervalHours)); List<(string Isin, string StrategyKey, string Timeframe)> staleEntries; using (var scope = _scopeFactory.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var rows = await db.StrategyMatrix .AsNoTracking() .Where(m => m.UpdatedAtUtc <= staleCutoff) .Select(m => new { m.Isin, m.StrategyKey, m.Timeframe }) .ToListAsync(stoppingToken); staleEntries = rows.Select(m => (m.Isin, m.StrategyKey, m.Timeframe)).ToList(); } if (staleEntries.Count == 0) { await _logger.LogInfoAsync(SimulationSettingKeys.MatrixChannel, "[MatrixRecompute] No stale reliability matrix entries found this cycle."); return; } await _logger.LogInfoAsync(SimulationSettingKeys.MatrixChannel, "[MatrixRecompute] Refreshing {Count} stale reliability matrix entries (older than {Hours}h)...", staleEntries.Count, intervalHours); var now = DateTime.UtcNow; foreach (var (isin, strategyKey, timeframe) in staleEntries) { if (stoppingToken.IsCancellationRequested) break; try { using var scope = _scopeFactory.CreateScope(); var simEngine = scope.ServiceProvider.GetRequiredService(); var request = new BacktestRequestDto( Isin: isin, Symbol: "", // Resolved from the ISIN by QuantSimulationEngine itself. StrategyKey: strategyKey, Timeframe: timeframe, StartDateUtc: now.AddYears(-2), EndDateUtc: now ); await simEngine.RunBacktestAsync(request, stoppingToken); } catch (Exception ex) { await _logger.LogWarningAsync(SimulationSettingKeys.MatrixChannel, ex, "[MatrixRecompute] Failed to refresh matrix entry for {Isin} ({Strategy}/{Timeframe}).", isin, strategyKey, timeframe); } // Gentle throttle so this doesn't hammer Yahoo/FinlyticTechnicals with back-to-back requests. await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } }