From b517f7a637ac89f9985359384fed08ee2b3179fa Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 4 Sep 2026 15:44:08 +0200 Subject: [PATCH] OrderRequest entsteht aus dem Instrument, nicht aus Einzelfeldern Beim ersten erfolgreichen Order-Nachweis (#293) gegen das Paper-Konto lief die Order auf die falsche Notierung: geprueft, bepreist und reserviert wurde SAP:EUR (XETRA, 188,74 EUR), ausgefuehrt wurde die SAP-ADR in New York zu 216,68 USD. Das USD-Guthaben rutschte dadurch von -3.751 auf -4.185 - also genau der Margin-Kredit, den Schicht C verhindern soll, aufgenommen an der Sicherung vorbei. Ursache: ExecutionService baute die OrderRequest aus Einzelfeldern und setzte Currency nicht; die Eigenschaft fiel still auf USD zurueck. Es gab keine Fehlermeldung, und die Buecher wiesen anschliessend eine EUR-Position aus, die es beim Broker nie gab. Behebung nicht durch Nachtragen der Zuweisung, sondern baulich: OrderRequest.For(instrument, ...) uebertraegt die Identitaet vollstaendig. Wer die Felder einzeln setzt, baut ein zweites, halb ausgefuelltes Instrument - das darf im Orderpfad nicht mehr moeglich sein. Zwei Regressionstests, die genau diesen Fall fangen. Co-Authored-By: Claude Opus 5 --- .../Trading/ExecutionService.cs | 13 +++---- src/IBKRTrader.Core/Trading/TradingModels.cs | 25 +++++++++++++ .../Trading/ExecutionServiceTests.cs | 36 +++++++++++++++++++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/IBKRTrader.Core/Trading/ExecutionService.cs b/src/IBKRTrader.Core/Trading/ExecutionService.cs index 05a9f55..8aa498a 100644 --- a/src/IBKRTrader.Core/Trading/ExecutionService.cs +++ b/src/IBKRTrader.Core/Trading/ExecutionService.cs @@ -129,15 +129,10 @@ public sealed class ExecutionService : IExecutionService var reservationId = reservation.Reservation!.Id; // 7. Order platzieren - var order = new OrderRequest - { - Symbol = signal.Symbol, - Side = signal.Side, - Quantity = decision.Quantity, - Type = orderType, - LimitPrice = signal.LimitPrice, - Option = signal.Option - }; + // Aus dem Instrument gebaut, nicht aus Einzelfeldern: sonst faellt die Waehrung still auf + // USD zurueck und die Order trifft eine andere Notierung als die, die geprueft wurde. + var order = OrderRequest.For( + signal.Instrument, signal.Side, decision.Quantity, orderType, signal.LimitPrice); var result = await _broker.PlaceOrderAsync(order, ct); if (!result.Success) diff --git a/src/IBKRTrader.Core/Trading/TradingModels.cs b/src/IBKRTrader.Core/Trading/TradingModels.cs index be97768..35f3eba 100644 --- a/src/IBKRTrader.Core/Trading/TradingModels.cs +++ b/src/IBKRTrader.Core/Trading/TradingModels.cs @@ -82,6 +82,31 @@ public sealed record OrderRequest public InstrumentRef Instrument => Option is null ? InstrumentRef.ForStock(Symbol, Currency) : InstrumentRef.ForOption(Option); + + /// + /// Baut den Auftrag aus dem Instrument – der einzige Weg, der die Identität vollständig + /// überträgt. + /// + /// Warum es diese Fabrik gibt. Wer die Felder einzeln setzt, kann die Währung + /// vergessen; sie fällt dann auf USD zurück. Am 2026-09-04 ist genau das passiert: geprüft und + /// reserviert wurde SAP:EUR, gehandelt wurde die SAP-ADR in New York – Kurs 188,74 EUR + /// gegen Ausführung zu 216,68 USD, und das USD-Guthaben rutschte um 434 ins Minus. Es gab keine + /// Fehlermeldung; die Bücher wiesen eine EUR-Position aus, die es beim Broker nie gab. Ein + /// Auftrag muss aus dem Instrument entstehen, sonst ist er ein zweites, halb ausgefülltes + /// Instrument. + /// + public static OrderRequest For( + InstrumentRef instrument, TradeSide side, int quantity, OrderType type, decimal? limitPrice = null) => + new() + { + Symbol = instrument.Symbol, + Side = side, + Quantity = quantity, + Type = type, + LimitPrice = limitPrice, + Option = instrument.Option, + Currency = instrument.Currency + }; } /// Ergebnis einer Order-Platzierung. diff --git a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs index e5fb5da..c0db0ea 100644 --- a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs +++ b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs @@ -247,6 +247,42 @@ public class ExecutionServiceTests await _capital.Received(1).SettleReservationAsync(1, 5, 500m, Arg.Any()); } + [Fact] + public async Task TheOrderCarriesTheSignalsCurrency_NotTheDefault() + { + // Am 2026-09-04 gegen das Paper-Konto beobachtet: geprueft und reserviert wurde SAP:EUR, + // gehandelt wurde die SAP-ADR in New York, weil die OrderRequest aus Einzelfeldern gebaut + // wurde und die Waehrung dabei auf USD zurueckfiel. Ohne Fehlermeldung. + ArrangeHappyPath(); + var signal = new TradeSignal + { + Symbol = "SAP", Side = TradeSide.Buy, SourceModule = "CT", Currency = "EUR" + }; + + await CreateSut().ExecuteAsync(signal); + + await _broker.Received(1).PlaceOrderAsync( + Arg.Is(o => o.Currency == "EUR" && o.Instrument.Key == "SAP:EUR"), + Arg.Any()); + } + + [Fact] + public async Task TheOrderCarriesTheOptionContract() + { + ArrangeHappyPath(); + var spec = new OptionSpec("AAPL", new DateOnly(2026, 9, 18), 150m, OptionRight.Put); + var signal = new TradeSignal + { + Symbol = "AAPL", Side = TradeSide.Sell, SourceModule = "CT", Option = spec + }; + + await CreateSut().ExecuteAsync(signal); + + await _broker.Received(1).PlaceOrderAsync( + Arg.Is(o => o.Option == spec && o.Instrument.Kind == InstrumentKind.Option), + Arg.Any()); + } + [Fact] public async Task OrderFails_ReturnsError_AndDoesNotBook() {