using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Dtos.Assets; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticCore.Dtos.Trading; using FinlyticCore.Models.Assets; using FinlyticCore.Services; using FinlyticEngine.Services.Mqtt; using FinlyticEngine.Settings; namespace FinlyticEngine.Services.Derivatives; public class KnockOutDerivativeResolver : IKnockOutDerivativeResolver { private readonly IEngineRpcClient _rpcClient; private readonly ISettingsService _settingsService; private readonly IFinlyticLogger _logger; public KnockOutDerivativeResolver( IEngineRpcClient rpcClient, ISettingsService settingsService, IFinlyticLogger logger) { _rpcClient = rpcClient; _settingsService = settingsService; _logger = logger; } public async Task ResolveOptimalTurboAsync( string underlyingIsin, SignalDirection direction, decimal chartStopLoss, decimal currentPrice, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(underlyingIsin) || chartStopLoss <= 0 || currentPrice <= 0) { return null; } var optionType = direction == SignalDirection.Buy ? "long" : "short"; var minLeverage = await _settingsService.GetSettingAsync(EngineSettingKeys.MinDerivativeLeverage, cancellationToken); var targetDefaultLeverage = await _settingsService.GetSettingAsync(EngineSettingKeys.TargetDefaultLeverage, cancellationToken); var safetyBufferPercent = await _settingsService.GetSettingAsync(EngineSettingKeys.KnockOutSafetyBufferPercent, cancellationToken); try { var req = new GetDerivativesRequest( UnderlyingIsin: underlyingIsin, OptionType: optionType, TargetLeverage: targetDefaultLeverage, After: null, Page: 0, ForceRefresh: false ); await _logger.LogInfoAsync(EngineSettingKeys.DerivativesChannel, "[KnockOutResolver] Requesting derivatives for {Isin} ({OptionType}, target leverage {TargetLev})", underlyingIsin, optionType, targetDefaultLeverage); var derivatives = await _rpcClient.SendRpcRequestAsync, GetDerivativesRequest>( "assets_GetDerivatives", req, TimeSpan.FromSeconds(5) ); if (derivatives == null || derivatives.Count == 0) { await _logger.LogWarningAsync(EngineSettingKeys.DerivativesChannel, "[KnockOutResolver] No derivatives returned from FinlyticAssets for {Isin}", underlyingIsin); return null; } // Hard Knock-Out Safety Check var safeDerivatives = derivatives.Where(d => { if (d.Leverage < minLeverage || d.Barrier <= 0) return false; if (direction == SignalDirection.Buy) { // For Long: Knock-Out Barrier MUST be at or below (StopLoss - Buffer%) decimal maxAllowedBarrier = chartStopLoss * (1.0m - (safetyBufferPercent / 100.0m)); return d.Barrier <= maxAllowedBarrier; } else { // For Short: Knock-Out Barrier MUST be at or above (StopLoss + Buffer%) decimal minAllowedBarrier = chartStopLoss * (1.0m + (safetyBufferPercent / 100.0m)); return d.Barrier >= minAllowedBarrier; } }).ToList(); if (safeDerivatives.Count == 0) { await _logger.LogWarningAsync(EngineSettingKeys.DerivativesChannel, "[KnockOutResolver] None of the {Count} derivatives passed the hard KO safety buffer ({Buffer}%) for ISIN {Isin} (SL: {SL})", derivatives.Count, safetyBufferPercent, underlyingIsin, chartStopLoss); return null; } // Ranking: 1. Issuer Rank, 2. Closeness to target leverage var best = safeDerivatives .OrderBy(d => GetIssuerRank(d.Issuer)) .ThenBy(d => Math.Abs(d.Leverage - targetDefaultLeverage)) .First(); decimal calculatedBuffer = direction == SignalDirection.Buy ? ((chartStopLoss - best.Barrier) / chartStopLoss) * 100.0m : ((best.Barrier - chartStopLoss) / chartStopLoss) * 100.0m; // Trade Republic liefert für Derivate keine WKN (nur ISIN, siehe DerivativeDto/ // TradeRepublicDerivativeItemDto). Die ISIN darf nicht als WKN ausgegeben werden, // da beide unterschiedliche Wertpapierkennungen sind (Rules.md §4) - daher null statt Fake-Wert. var result = new DerivativeSelectionDto( DerivativeIsin: best.Isin, DerivativeWkn: null, Issuer: best.Issuer ?? "Unknown", OptionType: optionType.ToUpperInvariant(), Strike: best.Strike, Barrier: best.Barrier, Leverage: best.Leverage, SafetyBufferPercent: Math.Round(calculatedBuffer, 2), SpreadPercentage: 0m, Size: best.Size ?? 0.1m ); await _logger.LogInfoAsync(EngineSettingKeys.DerivativesChannel, "[KnockOutResolver] Selected optimal turbo {DerivIsin} for {Isin}: Lev={Lev}x, Barrier={Barrier}, Buffer={Buffer:F1}%, Issuer={Issuer}", result.DerivativeIsin, underlyingIsin, result.Leverage, result.Barrier, result.SafetyBufferPercent, result.Issuer); return result; } catch (Exception ex) { await _logger.LogErrorAsync(EngineSettingKeys.DerivativesChannel, ex, "[KnockOutResolver] Failed to resolve derivative for ISIN {Isin}", underlyingIsin); return null; } } private static int GetIssuerRank(string? issuer) { if (string.IsNullOrWhiteSpace(issuer)) return 5; var s = issuer.ToUpperInvariant(); if (s.Contains("HSBC")) return 1; if (s.Contains("SOCIETE") || s.Contains("SG")) return 2; if (s.Contains("BNP")) return 3; if (s.Contains("UBS") || s.Contains("CITI") || s.Contains("VONTOBEL")) return 4; return 5; } }