150 lines
6.2 KiB
C#
150 lines
6.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Keeps the backtest-reliability matrix (<c>SimulationStrategyMatrixEntity</c>) 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).
|
|
/// </summary>
|
|
public class ReliabilityMatrixRecomputeBackgroundService : BackgroundService
|
|
{
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ISettingsService _settingsService;
|
|
private readonly IFinlyticLogger<ReliabilityMatrixRecomputeBackgroundService> _logger;
|
|
|
|
public ReliabilityMatrixRecomputeBackgroundService(
|
|
IServiceScopeFactory scopeFactory,
|
|
ISettingsService settingsService,
|
|
IFinlyticLogger<ReliabilityMatrixRecomputeBackgroundService> 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<SimulationDbContext>();
|
|
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<IQuantSimulationEngine>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|