136 lines
4.8 KiB
C#
136 lines
4.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class FavoritesPriceBackgroundService : BackgroundService
|
|
{
|
|
private readonly IHubContext<FavoritesPriceHub> _hubContext;
|
|
private readonly WebMqttClient _mqttClient;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ILogger<FavoritesPriceBackgroundService> _logger;
|
|
private readonly Random _random = new();
|
|
|
|
public FavoritesPriceBackgroundService(
|
|
IHubContext<FavoritesPriceHub> hubContext,
|
|
WebMqttClient mqttClient,
|
|
IServiceScopeFactory scopeFactory,
|
|
ILogger<FavoritesPriceBackgroundService> 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<Dictionary<string, object>> FetchFavoritePricesAsync(CancellationToken cancellationToken)
|
|
{
|
|
var priceMap = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
List<UserFavoriteAssetEntity> favorites = new();
|
|
|
|
try
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<BackendDbContext>();
|
|
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<LivePriceDto, IsinRequest>(
|
|
"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;
|
|
}
|
|
}
|