using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FinlyticBackend.Database; using FinlyticBackend.Entities; using FinlyticBackend.Hubs; using FinlyticBackend.Util; using FinlyticCore.Dtos; using FinlyticCore.Dtos.TechnicalAnalysis; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace FinlyticBackend.Services; /// /// Background service that periodically (every 10 seconds) queries FinlyticTechnicalAnalysis over MQTT RPC /// to retrieve current close prices and daily % growth for all favorited assets, and broadcasts the updates /// via SignalR to connected clients. /// public class FavoritesPriceBackgroundService : BackgroundService { private readonly IHubContext _hubContext; private readonly WebMqttClient _mqttClient; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private readonly Random _random = new(); public FavoritesPriceBackgroundService( IHubContext hubContext, WebMqttClient mqttClient, IServiceScopeFactory scopeFactory, ILogger logger) { _hubContext = hubContext; _mqttClient = mqttClient; _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("[FavoritesPriceBackgroundService] Started 10-second periodic price & daily growth stream."); await Task.Delay(4000, stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { var priceUpdates = await FetchFavoritePricesAsync(stoppingToken); if (priceUpdates.Count > 0) { await _hubContext.Clients.All.SendAsync("ReceiveFavoritePrices", priceUpdates, cancellationToken: stoppingToken); } } catch (Exception ex) { _logger.LogWarning(ex, "[FavoritesPriceBackgroundService] Error broadcasting 10s favorite price updates."); } await Task.Delay(10000, stoppingToken); } } private async Task> FetchFavoritePricesAsync(CancellationToken cancellationToken) { var priceMap = new Dictionary(StringComparer.OrdinalIgnoreCase); List favorites = new(); try { using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); favorites = await dbContext.UserFavoriteAssets .AsNoTracking() .ToListAsync(cancellationToken); } catch { } // De-duplicate by ISIN (preferring entries that have SelectedTicker) var dedupedFavorites = favorites .GroupBy(f => f.Isin.Trim().ToUpperInvariant()) .Select(g => g.OrderByDescending(f => !string.IsNullOrEmpty(f.SelectedTicker)).First()) .ToList(); foreach (var fav in dedupedFavorites) { string cleanIsin = fav.Isin.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(cleanIsin)) continue; string querySymbol = !string.IsNullOrWhiteSpace(fav.SelectedTicker) ? fav.SelectedTicker.Trim().ToUpperInvariant() : cleanIsin; double currentPrice = 0.0; double dailyChangePercent = 0.0; bool resolvedFromTa = false; try { if (_mqttClient.IsConnected) { var livePriceDto = await _mqttClient.SendRpcRequestAsync( "tr_GetLivePrice", new IsinRequest(querySymbol), TimeSpan.FromSeconds(2) ); if (livePriceDto != null) { currentPrice = (double)livePriceDto.CurrentPrice; dailyChangePercent = (double)livePriceDto.DailyChangePercent; resolvedFromTa = true; } } } catch { } if (resolvedFromTa) { priceMap[cleanIsin] = new { isin = cleanIsin, symbol = querySymbol, currentPrice = currentPrice, dailyChangePercent = dailyChangePercent }; } } return priceMap; } }