feat(Backend): update API gateway and websocket hubs

This commit is contained in:
2026-08-09 21:32:52 +02:00
parent fdf4b6efcb
commit a9553e9fbf
66 changed files with 188878 additions and 0 deletions
@@ -0,0 +1,135 @@
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;
}
}