159 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|