using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.Trading;
///
/// Führt Modul-Signale aus: globaler Schalter → Kurs → Konto → Risiko → Order → Buchung.
/// Kennt kein Modul – Module rufen nur mit ihrem Signal auf.
///
public sealed class ExecutionService : IExecutionService
{
private readonly IBrokerClient _broker;
private readonly IRiskService _risk;
private readonly IPortfolioService _portfolio;
private readonly SettingsService _settings;
private readonly LoggingService _logger;
public ExecutionService(
IBrokerClient broker,
IRiskService risk,
IPortfolioService portfolio,
SettingsService settings,
LoggingService logger)
{
_broker = broker;
_risk = risk;
_portfolio = portfolio;
_settings = settings;
_logger = logger;
}
public async Task ExecuteAsync(TradeSignal signal, CancellationToken ct = default)
{
var trading = _settings.Settings.Trading;
var module = signal.SourceModule;
// 1. Globaler Hauptschalter
if (!trading.TradingEnabled)
return Log(module, ExecutionResult.Skip("Trading global deaktiviert."));
// 2. Kurs
var quote = await _broker.GetQuoteAsync(signal.Symbol, ct);
if (quote is null || quote.Last <= 0)
return Log(module, ExecutionResult.Skip($"Kein Kurs für {signal.Symbol} verfügbar."));
// 3. Konto + 4. bestehende Exposure/Position
var account = await _broker.GetAccountStateAsync(ct);
var exposure = await _portfolio.GetModuleExposureAsync(module, ct);
var existingQty = await _portfolio.GetPositionQuantityAsync(module, signal.Symbol, ct);
// 5. Risikoprüfung
var context = new RiskContext
{
Price = quote.Last,
NetLiquidation = account.NetLiquidation,
ModuleExposure = exposure,
ExistingQuantity = existingQty
};
var riskParams = new RiskParameters(
(decimal)trading.MaxTradePercent,
(decimal)trading.MaxPositionPercentPerModule,
(decimal)trading.MaxSlippagePercent);
var decision = _risk.Evaluate(signal, context, riskParams);
if (!decision.Approved)
return Log(module, ExecutionResult.Skip(decision.Reason));
// 6. Order platzieren
var order = new OrderRequest
{
Symbol = signal.Symbol,
Side = signal.Side,
Quantity = decision.Quantity,
Type = signal.LimitPrice.HasValue ? OrderType.Limit : OrderType.Market,
LimitPrice = signal.LimitPrice
};
var result = await _broker.PlaceOrderAsync(order, ct);
if (!result.Success)
return Log(module, ExecutionResult.Error(result.Error ?? "Order fehlgeschlagen.", result));
// 7. Buchung
await _portfolio.RecordFillAsync(
module, signal.Symbol, signal.Side,
result.FilledQuantity, result.AvgFillPrice, result.OrderId, ct);
return Log(module, ExecutionResult.Execute(result));
}
private ExecutionResult Log(string module, ExecutionResult result)
{
var text = $"[{result.Action}] {result.Reason}";
if (result.Action == "ERROR") _logger.Error(module, text);
else _logger.Info(module, text);
return result;
}
}