Files
IBKRTrader/Core/Trading/ExecutionService.cs
T
Richard 2ad4b55db1 @
Phase 3: Trading-Kern (Risk, Execution, Portfolio) mit sicherem Broker-Default

- Core/Trading/TradingModels: Signal, Order(Request/Result), RiskContext/Decision,
  Account, Position, Quote, ExecutionResult, Enums (Side/OrderType/Mode)
- IBrokerClient + NullBrokerClient (sicherer Default, handelt NIE bis IBKR-Adapter verifiziert)
- RiskService (+IRiskService): Sizing nach MaxTrade%, Modul-Limit, Slippage; Buy/Sell
- PortfolioService (+IPortfolioService): core_position + core_trade_history + core_budget
- ExecutionService (+IExecutionService): Signal -> Kurs -> Konto -> Risiko -> Order -> Buchung
- TradingSettings in AppSettings (Paper/Live, TradingEnabled, Risikoparameter)
- CoreMigrations: core_position; DI-Registrierung der Trading-Services
- Tests: RiskService (11) + ExecutionService (6, NSubstitute) -> 38/38 gruen

Offen (bewusst gekapselt): echter IbkrBrokerClient gegen Client-Portal-Gateway (manuell verifizieren).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-27 11:33:59 +02:00

97 lines
3.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.Trading;
/// <summary>
/// Führt Modul-Signale aus: globaler Schalter → Kurs → Konto → Risiko → Order → Buchung.
/// Kennt kein Modul Module rufen nur <see cref="ExecuteAsync"/> mit ihrem Signal auf.
/// </summary>
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<ExecutionResult> 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;
}
}