feat(engine): add FinlyticEngine microservice with trade lifecycle, AI reasoning gate, composite scoring, and unit tests
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Dtos.Trading;
|
||||
|
||||
namespace FinlyticEngine.Services.Derivatives;
|
||||
|
||||
public interface IKnockOutDerivativeResolver
|
||||
{
|
||||
Task<DerivativeSelectionDto?> ResolveOptimalTurboAsync(
|
||||
string underlyingIsin,
|
||||
SignalDirection direction,
|
||||
decimal chartStopLoss,
|
||||
decimal currentPrice,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user