using System; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Services; using FinlyticTechnicals.Util; using Microsoft.Extensions.Hosting; namespace FinlyticTechnicals.Services; public class TechnicalScannerBackgroundService : BackgroundService { private readonly ITechnicalUniverseManager _universeManager; private readonly ITechnicalScoringEngine _scoringEngine; private readonly ISettingsService _settingsService; private readonly IFinlyticLogger _logger; public TechnicalScannerBackgroundService( ITechnicalUniverseManager universeManager, ITechnicalScoringEngine scoringEngine, ISettingsService settingsService, IFinlyticLogger logger) { _universeManager = universeManager; _scoringEngine = scoringEngine; _settingsService = settingsService; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _logger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScanner] Starting Technical Universe Scanner Background Service..."); // Initial delay for MQTT connections to stabilize await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // Initial synchronization of Universe await _universeManager.RefreshFavoritesAsync(stoppingToken); await _universeManager.RefreshDiscoveryAsync(stoppingToken); DateTime lastFavoritesSyncUtc = DateTime.UtcNow; DateTime lastDiscoverySyncUtc = DateTime.UtcNow; while (!stoppingToken.IsCancellationRequested) { try { DateTime now = DateTime.UtcNow; // 1. Check Periodic Sync Timers if (now - lastFavoritesSyncUtc >= TimeSpan.FromMinutes(15)) { await _universeManager.RefreshFavoritesAsync(stoppingToken); lastFavoritesSyncUtc = DateTime.UtcNow; } if (now - lastDiscoverySyncUtc >= TimeSpan.FromMinutes(30)) { await _universeManager.RefreshDiscoveryAsync(stoppingToken); lastDiscoverySyncUtc = DateTime.UtcNow; } // 2. Retrieve Active Prioritized Scan Universe var universe = await _universeManager.GetActiveUniverseAsync(stoppingToken); if (universe.Count > 0) { await _logger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScanner] Scanning {Count} assets in active universe across all strategies...", universe.Count); foreach (var entry in universe) { if (stoppingToken.IsCancellationRequested) break; try { var setups = await _scoringEngine.AnalyzeIsinAsync(entry.Isin, entry.Symbol, entry.Source, entry.AddedAtUtc, stoppingToken); if (setups.Count > 0) { await _logger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScanner] Found {Count} active setup(s) for ISIN {Isin} (Top Score: {Score:F1})", setups.Count, entry.Isin, setups[0].QualityScore); } } catch (Exception ex) { await _logger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalScanner] Error analyzing ISIN {Isin}", entry.Isin); } // Gentle throttle between asset analysis runs await Task.Delay(250, stoppingToken); } } else { await _logger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScanner] Scan universe is currently empty. Waiting for next cycle."); } // Wait 60 seconds before next full universe evaluation pass await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { await _logger.LogErrorAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalScanner] Unexpected error in scanner loop. Retrying in 30 seconds."); await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } await _logger.LogInfoAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScanner] Technical Universe Scanner Background Service stopped."); } }