Files
Finlytic/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs
T

343 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAnalyzer.Util;
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;
namespace FinlyticAnalyzer.Services;
public class ActiveTradeMonitorWorker : BackgroundService
{
private readonly IFinlyticLogger<ActiveTradeMonitorWorker> _finlyticLogger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly AnalyzerMqttClient _mqttClient;
public ActiveTradeMonitorWorker(
IFinlyticLogger<ActiveTradeMonitorWorker> finlyticLogger,
IServiceScopeFactory scopeFactory,
AnalyzerMqttClient mqttClient)
{
_finlyticLogger = finlyticLogger;
_scopeFactory = scopeFactory;
_mqttClient = mqttClient;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker started.");
try
{
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
await MonitorActiveTradesAsync(stoppingToken);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Error in ActiveTradeMonitorWorker loop.");
}
try
{
await Task.Delay(TimeSpan.FromMinutes(60), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker stopped.");
}
private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken)
{
if (!_mqttClient.IsConnected)
{
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Skipping trade monitoring. RPC client not connected.");
return;
}
var activeTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Active"),
TimeSpan.FromSeconds(10));
var proposedTrades = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get",
new GetTradesRequest(null, "Proposed"),
TimeSpan.FromSeconds(10));
var trades = new List<TradeProposalDto>();
if (activeTrades != null) trades.AddRange(activeTrades);
if (proposedTrades != null) trades.AddRange(proposedTrades.Where(t => t.IsGlobalProposal));
if (trades.Count == 0)
{
await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] No active or proposed global trades found to monitor.");
return;
}
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>();
var vixService = scope.ServiceProvider.GetRequiredService<IVixTrackerService>();
foreach (var trade in trades)
{
if (cancellationToken.IsCancellationRequested) break;
try
{
await ProcessTradeAsync(trade, n8nService, vixService, cancellationToken);
}
catch (Exception ex)
{
await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Failed to monitor trade {TradeId} ({Symbol}).", trade.TradeId, trade.Symbol);
}
}
}
private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService,
IVixTrackerService vixService, CancellationToken cancellationToken)
{
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;
bool isLong = string.Equals(trade.SignalType, "BUY", StringComparison.OrdinalIgnoreCase) ||
string.Equals(trade.SignalType, "LONG", StringComparison.OrdinalIgnoreCase);
int maxHoldingDays = EstimateMaxHoldingDays(trade.Timeframe);
double daysOpen = (DateTime.UtcNow - trade.CreatedAt).TotalDays;
if (daysOpen > (maxHoldingDays * 1.5))
{
await SendUpdateAsync(trade, currentPrice, "Close",
$"Time-Stop getriggert: Setup ist invalidiert. Der Trade bewegt sich zu lange seitwärts (Offen seit {(int)daysOpen} Tagen, anvisiert waren max. {maxHoldingDays} Tage).");
return;
}
if (isLong)
{
if (trade.StopLoss > 0 && currentPrice <= trade.StopLoss)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
return;
}
if (trade.TakeProfit > 0 && currentPrice >= trade.TakeProfit)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
return;
}
}
else
{
if (trade.StopLoss > 0 && currentPrice >= trade.StopLoss)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Stop-Loss getriggert.");
return;
}
if (trade.TakeProfit > 0 && currentPrice <= trade.TakeProfit)
{
await SendUpdateAsync(trade, currentPrice, "Close", "Hard Take-Profit erreicht.");
return;
}
}
var taResult = await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5));
var latestIndicator = taResult?.Indicators?.LastOrDefault();
var taInfo = new TechnicalContextInfo
{
Rsi = latestIndicator?.Rsi14?.ToString("F1") ?? "N/A",
SupertrendStatus = latestIndicator?.SupertrendDirection ?? "N/A",
Atr = latestIndicator?.Atr14?.ToString("F2") ?? "N/A",
Sma50 = (double?)latestIndicator?.Sma50,
Sma200 = (double?)latestIndicator?.Sma200,
DetectedPatterns = taResult?.Patterns?.Select(p => new PatternContextInfo
{
PatternName = p.Type,
BreakoutDirection = p.BreakoutSignal?.Direction,
TargetPrice = (double?)p.BreakoutSignal?.TargetPrice,
PotentialPercent = (double?)p.BreakoutSignal?.PotentialPercent
}).ToList() ?? new List<PatternContextInfo>()
};
var n8nReq = new N8nAnalysisRequestDto
{
RequestId = Guid.NewGuid().ToString("N"),
Timestamp = DateTime.UtcNow,
TriggerType = "HourlyMonitor",
TargetAsset = new TargetAssetInfo
{
Symbol = trade.Symbol,
Isin = trade.Isin,
Sector = trade.Sector
},
MarketContext = new MarketContextInfo
{
Vix = vixService.GetCurrentVix(),
MarketRegime = vixService.GetCurrentRegime().ToString()
},
UserPreferences = new UserPreferencesInfo
{
InstrumentType = trade.InstrumentType,
TimeframeFormatted = trade.Timeframe
},
TechnicalContext = taInfo
};
var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken);
if (aiResponse == null)
{
await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] AI evaluation returned null for {TradeId}. Skipping update.", trade.TradeId);
return;
}
string newRecommendation = "Hold";
string reasoning = aiResponse.AiReasoning;
decimal? newStopLoss = trade.StopLoss;
decimal? newTakeProfit = trade.TakeProfit;
bool aiSuggestsShort =
string.Equals(aiResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ||
string.Equals(aiResponse.SuggestedDirection, "Sell", StringComparison.OrdinalIgnoreCase);
bool aiSuggestsLong =
string.Equals(aiResponse.SuggestedDirection, "Long", StringComparison.OrdinalIgnoreCase) ||
string.Equals(aiResponse.SuggestedDirection, "Buy", StringComparison.OrdinalIgnoreCase);
if ((isLong && aiSuggestsShort) || (!isLong && aiSuggestsLong))
{
newRecommendation = "Close";
reasoning =
$"Trendwende detektiert: KI empfiehlt {aiResponse.SuggestedDirection}, Trade ist aber {(isLong ? "Long" : "Short")}.";
}
else if (string.Equals(aiResponse.AiDecision, "Reject", StringComparison.OrdinalIgnoreCase))
{
newRecommendation = "Close";
reasoning = $"Risiko zu hoch: KI empfiehlt Exit. ({aiResponse.AiReasoning})";
}
else if (aiResponse.ExecutionPlan != null)
{
if (aiResponse.ExecutionPlan.StopLoss > 0)
{
var proposedSl = aiResponse.ExecutionPlan.StopLoss;
if (isLong)
{
if (trade.StopLoss <= 0 || proposedSl > trade.StopLoss)
{
newStopLoss = proposedSl;
if (proposedSl > trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
}
}
else
{
if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss)
{
newStopLoss = proposedSl;
if (proposedSl < trade.StopLoss && trade.StopLoss > 0) newRecommendation = "AdjustSL";
}
}
}
if (aiResponse.ExecutionPlan.TakeProfitTargets != null &&
aiResponse.ExecutionPlan.TakeProfitTargets.Count > 0)
{
var proposedTp = aiResponse.ExecutionPlan.TakeProfitTargets[0];
if (proposedTp > 0 && proposedTp != trade.TakeProfit)
{
newTakeProfit = proposedTp;
if (newRecommendation == "Hold") newRecommendation = "AdjustTP";
}
}
}
await SendUpdateAsync(trade, currentPrice, newRecommendation, reasoning, newStopLoss, newTakeProfit);
}
private async Task SendUpdateAsync(TradeProposalDto trade, decimal currentPrice, string recommendation,
string reasoning, decimal? suggestedStopLoss = null, decimal? suggestedTakeProfit = null)
{
var update = new TradeHourlyUpdateDto
{
TradeId = trade.TradeId,
Recommendation = recommendation,
CurrentPrice = currentPrice,
SuggestedStopLoss = suggestedStopLoss,
SuggestedTakeProfit = suggestedTakeProfit,
VixValue = trade.VixValue,
Reasoning = reasoning,
Timestamp = DateTime.UtcNow
};
string topic = $"finlytic/trades/updates/{trade.Isin}";
await _mqttClient.PublishAsync(topic, update);
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;
string tfLower = timeframe.ToLowerInvariant();
int multiplier = 1;
if (tfLower.Contains("woche") || tfLower.Contains("week")) multiplier = 7;
else if (tfLower.Contains("monat") || tfLower.Contains("month")) multiplier = 30;
else if (tfLower.Contains("jahr") || tfLower.Contains("year")) multiplier = 365;
var numbers = new List<int>();
string currentNum = "";
foreach (char c in timeframe)
{
if (char.IsDigit(c))
{
currentNum += c;
}
else if (currentNum.Length > 0)
{
if (int.TryParse(currentNum, out int n)) numbers.Add(n);
currentNum = "";
}
}
if (currentNum.Length > 0 && int.TryParse(currentNum, out int lastN)) numbers.Add(lastN);
int maxNum = numbers.Count > 0 ? numbers.Max() : 14;
if (maxNum == 0) maxNum = 14;
if (multiplier == 1 && maxNum < 3) maxNum = 3;
return maxNum * multiplier;
}
}