using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticCore.Dtos.Trading; using FinlyticCore.Services; using FinlyticEngine.Database; using FinlyticEngine.Database.Entities; using FinlyticEngine.Services.Mqtt; using FinlyticEngine.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace FinlyticEngine.Services.Trading; public record GetCandlesRpcRequest( string Isin = "", string Timeframe = "15m" ); public class ActiveTradeMonitoringBackgroundService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly IEngineRpcClient _rpcClient; private readonly ISettingsService _settingsService; private readonly IFinlyticLogger _logger; public ActiveTradeMonitoringBackgroundService( IServiceScopeFactory scopeFactory, IEngineRpcClient rpcClient, ISettingsService settingsService, IFinlyticLogger logger) { _scopeFactory = scopeFactory; _rpcClient = rpcClient; _settingsService = settingsService; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _logger.LogInfoAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Starting active trade lifecycle monitoring service."); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { var intervalSec = await _settingsService.GetSettingAsync(EngineSettingKeys.MonitoringIntervalSeconds, stoppingToken); using (var scope = _scopeFactory.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var lifecycleService = scope.ServiceProvider.GetRequiredService(); var activeTrades = await db.Trades .Include(t => t.Fills) .Where(t => t.Status == TradeStatus.Active || t.Status == TradeStatus.BreakEvenTriggered || t.Status == TradeStatus.Tp1Hit || t.Status == TradeStatus.Tp2Hit) .ToListAsync(stoppingToken); if (activeTrades.Count > 0) { await _logger.LogInfoAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Monitoring {Count} active trades against live price feeds.", activeTrades.Count); foreach (var trade in activeTrades) { if (stoppingToken.IsCancellationRequested) break; try { // 1. Fetch latest candle for current price var candles = await _rpcClient.SendRpcRequestAsync, GetCandlesRpcRequest>( "ta_GetCandles", new GetCandlesRpcRequest(trade.UnderlyingIsin, "1m"), TimeSpan.FromSeconds(3) ); if (candles == null || candles.Count == 0) { continue; } var latestCandle = candles.Last(); decimal currentPrice = latestCandle.Close; trade.CurrentPrice = currentPrice; trade.LastUpdatedAtUtc = DateTime.UtcNow; // 2. Check Stop-Loss Violation bool isStoppedOut = false; if (trade.Direction == SignalDirection.Buy && currentPrice <= trade.CurrentStopLoss) { isStoppedOut = true; } else if (trade.Direction == SignalDirection.Sell && currentPrice >= trade.CurrentStopLoss) { isStoppedOut = true; } if (isStoppedOut) { trade.Status = TradeStatus.StoppedOut; trade.ClosedAtUtc = DateTime.UtcNow; if (trade.Direction == SignalDirection.Buy) { trade.RealizedPnlEur = ((currentPrice - trade.AverageBuyIn) * trade.TotalQuantity) - trade.TotalFeesEur; } else { trade.RealizedPnlEur = ((trade.AverageBuyIn - currentPrice) * trade.TotalQuantity) - trade.TotalFeesEur; } await _logger.LogWarningAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Trade {TradeId} for {Isin} STOPPED OUT at {Price:F2} € (SL: {SL:F2} €, PnL: {PnL:F2} €)", trade.Id, trade.UnderlyingIsin, currentPrice, trade.CurrentStopLoss, trade.RealizedPnlEur); await db.SaveChangesAsync(stoppingToken); await _rpcClient.PublishAsync("finlytic/engine/trades/status_changed", MapTradeEntityToDto(trade)); continue; } // 3. Check Break-Even Trigger (Free-Roll when TP1 is hit) bool isTp1Reached = false; if (trade.Direction == SignalDirection.Buy && currentPrice >= trade.TakeProfit1) { isTp1Reached = true; } else if (trade.Direction == SignalDirection.Sell && currentPrice <= trade.TakeProfit1) { isTp1Reached = true; } if (isTp1Reached && trade.Status == TradeStatus.Active) { decimal oldSl = trade.CurrentStopLoss; trade.CurrentStopLoss = trade.AverageBuyIn; // Move SL to Break-Even (Free-Roll) trade.Status = TradeStatus.BreakEvenTriggered; await _logger.LogInfoAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Trade {TradeId} for {Isin} hit TP1 ({TP1:F2} €). Moving SL from {OldSl:F2} to Break-Even ({BuyIn:F2} €)", trade.Id, trade.UnderlyingIsin, trade.TakeProfit1, oldSl, trade.AverageBuyIn); await db.SaveChangesAsync(stoppingToken); await _rpcClient.PublishAsync("finlytic/engine/trades/status_changed", MapTradeEntityToDto(trade)); } // 4. Check Trailing Stop logic if (trade.ExitPlan?.TrailingStopRule != null && trade.Status == TradeStatus.BreakEvenTriggered) { var rule = trade.ExitPlan.TrailingStopRule; if (trade.Direction == SignalDirection.Buy && currentPrice > rule.ActivationPrice) { decimal trailingSl = currentPrice * 0.97m; // 3% trail if (trailingSl > trade.CurrentStopLoss) { trade.CurrentStopLoss = Math.Round(trailingSl, 2); await _logger.LogInfoAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Trailing SL for trade {TradeId} moved up to {NewSl:F2} €", trade.Id, trade.CurrentStopLoss); await db.SaveChangesAsync(stoppingToken); await _rpcClient.PublishAsync("finlytic/engine/trades/status_changed", MapTradeEntityToDto(trade)); } } } await db.SaveChangesAsync(stoppingToken); } catch (Exception ex) { await _logger.LogWarningAsync(EngineSettingKeys.TradeLifecycleChannel, ex, "[ActiveTradeMonitor] Error evaluating active trade {TradeId}", trade.Id); } } } } await Task.Delay(TimeSpan.FromSeconds(Math.Max(5, intervalSec)), stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { await _logger.LogErrorAsync(EngineSettingKeys.TradeLifecycleChannel, ex, "[ActiveTradeMonitor] Unexpected error in monitoring loop. Waiting 15s."); await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken); } } await _logger.LogInfoAsync(EngineSettingKeys.TradeLifecycleChannel, "[ActiveTradeMonitor] Active trade lifecycle monitoring service stopped."); } private static ActiveTradeDto MapTradeEntityToDto(EngineTradeEntity e) { decimal unrealizedPnlEur = 0m; decimal unrealizedPnlPercent = 0m; if (e.AverageBuyIn > 0 && e.TotalQuantity > 0 && e.CurrentPrice > 0) { if (e.Direction == SignalDirection.Buy) { unrealizedPnlEur = (e.CurrentPrice - e.AverageBuyIn) * e.TotalQuantity; unrealizedPnlPercent = ((e.CurrentPrice - e.AverageBuyIn) / e.AverageBuyIn) * 100m; } else { unrealizedPnlEur = (e.AverageBuyIn - e.CurrentPrice) * e.TotalQuantity; unrealizedPnlPercent = ((e.AverageBuyIn - e.CurrentPrice) / e.AverageBuyIn) * 100m; } } return new ActiveTradeDto( TradeId: e.Id, ProposalId: e.ProposalId, UnderlyingIsin: e.UnderlyingIsin, Symbol: e.Symbol, DerivativeIsin: e.DerivativeIsin, DerivativeWkn: e.DerivativeWkn, ExecutionMode: e.ExecutionMode, InstrumentType: e.InstrumentType, Direction: e.Direction, Status: e.Status, AverageBuyIn: e.AverageBuyIn, TotalQuantity: e.TotalQuantity, InitialStopLoss: e.InitialStopLoss, CurrentStopLoss: e.CurrentStopLoss, CurrentPrice: e.CurrentPrice, UnrealizedPnlEur: Math.Round(unrealizedPnlEur, 2), UnrealizedPnlPercent: Math.Round(unrealizedPnlPercent, 2), RealizedPnlEur: Math.Round(e.RealizedPnlEur, 2), ExitPlan: e.ExitPlan, Fills: e.Fills.Select(f => new TradeFillDto( FillId: f.Id, ExecutedAtUtc: f.ExecutedAtUtc, Price: f.Price, Quantity: f.Quantity, Fee: f.Fee, Note: f.Note )).ToList(), OpenedAtUtc: e.OpenedAtUtc, ClosedAtUtc: e.ClosedAtUtc ); } }