feat(technicals): add technical analysis microservice with indicator engines, pattern detectors, and strategies

This commit is contained in:
2026-08-24 21:36:05 +02:00
parent 12e7b57b16
commit f43ce2b7e9
36 changed files with 6792 additions and 0 deletions
@@ -0,0 +1,118 @@
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<TechnicalScannerBackgroundService> _logger;
public TechnicalScannerBackgroundService(
ITechnicalUniverseManager universeManager,
ITechnicalScoringEngine scoringEngine,
ISettingsService settingsService,
IFinlyticLogger<TechnicalScannerBackgroundService> 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.");
}
}