feat(simulation): add quant simulation microservice with virtual backtest broker and replay engine
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user