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; } }