Files
Finlytic/FinlyticEngine/Services/Derivatives/KnockOutDerivativeResolver.cs
T

159 lines
6.5 KiB
C#

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<KnockOutDerivativeResolver> _logger;
public KnockOutDerivativeResolver(
IEngineRpcClient rpcClient,
ISettingsService settingsService,
IFinlyticLogger<KnockOutDerivativeResolver> logger)
{
_rpcClient = rpcClient;
_settingsService = settingsService;
_logger = logger;
}
public async Task<DerivativeSelectionDto?> 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<List<DerivativeDto>, 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;
}
}