using Backend.Interface; namespace Backend.ServiceHandler; public sealed class AlertEvaluationService : BackgroundService { private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(1); private static readonly TimeZoneInfo Pacific = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly Dictionary _evaluators; public AlertEvaluationService( IServiceProvider services, ILogger logger, IEnumerable evaluators) { _services = services; _logger = logger; _evaluators = evaluators.ToDictionary(e => e.AlertType); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { if (IsWithinActiveWindow()) { await EvaluateAlertsAsync(stoppingToken); } else { _logger.LogDebug("Outside active window, skipping alert evaluation."); } await Task.Delay(PollInterval, stoppingToken); } } private static bool IsWithinActiveWindow() { var pacificNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Pacific); if (pacificNow.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) return false; var hour = pacificNow.Hour; return hour >= 6 && hour < 14; } private async Task EvaluateAlertsAsync(CancellationToken cancellationToken) { try { var store = _services.GetRequiredService(); var marketData = _services.GetRequiredService(); var activeAlerts = await store.GetActiveAlertsAsync(cancellationToken); _logger.LogInformation("Evaluating {Count} active alerts.", activeAlerts.Count); foreach (var alert in activeAlerts) { try { if (!_evaluators.TryGetValue(alert.AlertType, out var evaluator)) { _logger.LogWarning("No evaluator registered for alert type '{AlertType}'.", alert.AlertType); continue; } var result = await evaluator.EvaluateAsync(alert, marketData); if (result.UpdatedLastState is not null) alert.LastState = result.UpdatedLastState; if (result.Triggered) { alert.IsTriggered = true; _logger.LogInformation( "Alert {Id} triggered: {Type} for {Symbol} (user: {User})", alert.Id, alert.AlertType, alert.Symbol, alert.Username); } await store.UpdateAlertAsync(alert, cancellationToken); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to evaluate alert {Id}.", alert.Id); } } } catch (Exception ex) { _logger.LogError(ex, "Alert evaluation cycle failed."); } } }