using Marathon.Application.UseCases; using Marathon.Domain.ValueObjects; using Marathon.Infrastructure.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Marathon.Infrastructure.Workers; /// /// Forward-test engine: each cycle opens flat-stake paper bets for newly detected /// directional anomalies, then settles any open bets whose events have been graded. /// Idle (cheap re-check) while is false. /// /// /// The "since" marker is baselined to startup so pre-existing anomalies are not /// retro-traded, and advances to each cycle's upper bound only after the open pass /// succeeds. A unique index on PaperBets.AnomalyId backstops any double-open. /// Scoped use cases are resolved per cycle (EF Core DbContext lifetime). /// internal sealed class PaperTradingWorker : BackgroundService { private readonly IServiceProvider _services; private readonly IOptionsMonitor _opts; private readonly ILogger _logger; private DateTimeOffset _since; public PaperTradingWorker( IServiceProvider services, IOptionsMonitor opts, ILogger logger) { _services = services ?? throw new ArgumentNullException(nameof(services)); _opts = opts ?? throw new ArgumentNullException(nameof(opts)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Baseline: only forward-test anomalies detected after this worker started. _since = MoscowTime.Now; _logger.LogInformation("PaperTradingWorker: started"); while (!stoppingToken.IsCancellationRequested) { var opts = _opts.CurrentValue; if (!opts.Enabled) { await DelayQuietly(TimeSpan.FromSeconds(10), stoppingToken); continue; } try { var until = MoscowTime.Now; await using var scope = _services.CreateAsyncScope(); var open = scope.ServiceProvider.GetRequiredService(); await open.ExecuteAsync(_since, until, opts.MinScore, opts.FlatStake, stoppingToken); // Advance only after a successful open pass, so a failure replays the window. _since = until; var settle = scope.ServiceProvider.GetRequiredService(); await settle.ExecuteAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { _logger.LogError(ex, "PaperTradingWorker: cycle failed — will retry after interval"); } await DelayQuietly(TimeSpan.FromSeconds(Math.Max(5, opts.PollIntervalSeconds)), stoppingToken); } _logger.LogInformation("PaperTradingWorker: stopping"); } private static async Task DelayQuietly(TimeSpan delay, CancellationToken ct) { try { await Task.Delay(delay, ct); } catch (OperationCanceledException) { // Shutting down — swallow so ExecuteAsync's loop check exits cleanly. } } }