feat(analyzer): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -9,30 +9,32 @@ using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Models.Analyzer;
|
||||
using FinlyticCore.Models.Trades;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticAnalyzer.Services;
|
||||
|
||||
public class ActiveTradeMonitorWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<ActiveTradeMonitorWorker> _logger;
|
||||
private readonly IFinlyticLogger<ActiveTradeMonitorWorker> _finlyticLogger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly AnalyzerMqttClient _mqttClient;
|
||||
|
||||
public ActiveTradeMonitorWorker(ILogger<ActiveTradeMonitorWorker> logger, IServiceScopeFactory scopeFactory,
|
||||
public ActiveTradeMonitorWorker(
|
||||
IFinlyticLogger<ActiveTradeMonitorWorker> finlyticLogger,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
AnalyzerMqttClient mqttClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_scopeFactory = scopeFactory;
|
||||
_mqttClient = mqttClient;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker started.", "AnalyzerChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker started.");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -51,7 +53,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Error in ActiveTradeMonitorWorker loop.", "AnalyzerChannel");
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Error in ActiveTradeMonitorWorker loop.");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -64,24 +66,22 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker stopped.", "AnalyzerChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker stopped.");
|
||||
}
|
||||
|
||||
private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_mqttClient.IsConnected)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Skipping trade monitoring. RPC client not connected.", "AnalyzerChannel");
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Skipping trade monitoring. RPC client not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch active trades
|
||||
var activeTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
|
||||
"trades_Get",
|
||||
new GetTradesRequest(null, "Active"),
|
||||
TimeSpan.FromSeconds(10));
|
||||
|
||||
// Fetch proposed global trades
|
||||
var proposedTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
|
||||
"trades_Get",
|
||||
new GetTradesRequest(null, "Proposed"),
|
||||
@@ -93,13 +93,11 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
|
||||
if (trades.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] No active or proposed global trades found to monitor.",
|
||||
"AnalyzerChannel");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] No active or proposed global trades found to monitor.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[{Channel}] Found {Count} trades to monitor. Starting evaluation...", "AnalyzerChannel",
|
||||
trades.Count);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Found {Count} trades to monitor. Starting evaluation...", trades.Count);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nEvaluationService>();
|
||||
@@ -115,8 +113,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Failed to monitor trade {TradeId} ({Symbol}).", "AnalyzerChannel",
|
||||
trade.TradeId, trade.Symbol);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Failed to monitor trade {TradeId} ({Symbol}).", trade.TradeId, trade.Symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,22 +121,18 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
|
||||
IVixTrackerService vixService, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Get Live Price
|
||||
var livePriceReq = new IsinRequest(trade.Isin);
|
||||
var livePriceDto = await _mqttClient.SendRpcRequestAsync<LivePriceDto, IsinRequest>(
|
||||
"tr_GetLivePrice", livePriceReq, TimeSpan.FromSeconds(3));
|
||||
|
||||
decimal currentPrice = livePriceDto?.CurrentPrice > 0 ? livePriceDto.CurrentPrice : trade.EntryPrice;
|
||||
|
||||
// 2. Evaluate Hard Stops (StopLoss / TakeProfit / TimeStop)
|
||||
bool isLong = string.Equals(trade.SignalType, "BUY", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(trade.SignalType, "LONG", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Time-Stop Evaluierung
|
||||
int maxHoldingDays = EstimateMaxHoldingDays(trade.Timeframe);
|
||||
double daysOpen = (DateTime.UtcNow - trade.CreatedAt).TotalDays;
|
||||
|
||||
// 50% Grace Period. Bei z.B. 10 Tagen max. Haltedauer wird nach 15 Tagen ohne Zielerreichung glattgestellt.
|
||||
if (daysOpen > (maxHoldingDays * 1.5))
|
||||
{
|
||||
await SendUpdateAsync(trade, currentPrice, "Close",
|
||||
@@ -176,7 +169,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Run AI evaluation for soft/dynamic updates
|
||||
var taResult = await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
|
||||
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
|
||||
|
||||
@@ -225,8 +217,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken);
|
||||
if (aiResponse == null)
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] AI evaluation returned null for {TradeId}. Skipping update.",
|
||||
"AnalyzerChannel", trade.TradeId);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] AI evaluation returned null for {TradeId}. Skipping update.", trade.TradeId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,7 +226,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
decimal? newStopLoss = trade.StopLoss;
|
||||
decimal? newTakeProfit = trade.TakeProfit;
|
||||
|
||||
// Check for trend reversal
|
||||
bool aiSuggestsShort =
|
||||
string.Equals(aiResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(aiResponse.SuggestedDirection, "Sell", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -256,13 +246,11 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
else if (aiResponse.ExecutionPlan != null)
|
||||
{
|
||||
// Ratchet / Trailing Logic: StopLoss darf das Risiko nicht vergrößern!
|
||||
if (aiResponse.ExecutionPlan.StopLoss > 0)
|
||||
{
|
||||
var proposedSl = aiResponse.ExecutionPlan.StopLoss;
|
||||
if (isLong)
|
||||
{
|
||||
// Bei Long darf der StopLoss nur NACH OBEN angepasst werden
|
||||
if (trade.StopLoss <= 0 || proposedSl > trade.StopLoss)
|
||||
{
|
||||
newStopLoss = proposedSl;
|
||||
@@ -271,7 +259,6 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bei Short darf der StopLoss nur NACH UNTEN angepasst werden
|
||||
if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss)
|
||||
{
|
||||
newStopLoss = proposedSl;
|
||||
@@ -310,18 +297,16 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Direktes Objekt-Publishing nutzen (ManagedMqttClient serialisiert typgerecht)
|
||||
string topic = $"finlytic/trades/updates/{trade.Isin}";
|
||||
await _mqttClient.PublishAsync(topic, update);
|
||||
|
||||
_logger.LogInformation(
|
||||
"[{Channel}] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}",
|
||||
"AnalyzerChannel", trade.TradeId, topic, recommendation, reasoning);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}",
|
||||
trade.TradeId, topic, recommendation, reasoning);
|
||||
}
|
||||
|
||||
private static int EstimateMaxHoldingDays(string timeframe)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timeframe)) return 14; // Default
|
||||
if (string.IsNullOrWhiteSpace(timeframe)) return 14;
|
||||
|
||||
string tfLower = timeframe.ToLowerInvariant();
|
||||
int multiplier = 1;
|
||||
@@ -351,7 +336,7 @@ public class ActiveTradeMonitorWorker : BackgroundService
|
||||
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
|
||||
|
||||
if (maxNum == 0) maxNum = 14;
|
||||
if (multiplier == 1 && maxNum < 3) maxNum = 3; // Mindestens 3 Tage Kulanz
|
||||
if (multiplier == 1 && maxNum < 3) maxNum = 3;
|
||||
|
||||
return maxNum * multiplier;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user