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 <noreply@anthropic.com>
This commit is contained in:
@@ -129,15 +129,10 @@ public sealed class ExecutionService : IExecutionService
|
|||||||
var reservationId = reservation.Reservation!.Id;
|
var reservationId = reservation.Reservation!.Id;
|
||||||
|
|
||||||
// 7. Order platzieren
|
// 7. Order platzieren
|
||||||
var order = new OrderRequest
|
// 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.
|
||||||
Symbol = signal.Symbol,
|
var order = OrderRequest.For(
|
||||||
Side = signal.Side,
|
signal.Instrument, signal.Side, decision.Quantity, orderType, signal.LimitPrice);
|
||||||
Quantity = decision.Quantity,
|
|
||||||
Type = orderType,
|
|
||||||
LimitPrice = signal.LimitPrice,
|
|
||||||
Option = signal.Option
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await _broker.PlaceOrderAsync(order, ct);
|
var result = await _broker.PlaceOrderAsync(order, ct);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
|
|||||||
@@ -82,6 +82,31 @@ public sealed record OrderRequest
|
|||||||
|
|
||||||
public InstrumentRef Instrument =>
|
public InstrumentRef Instrument =>
|
||||||
Option is null ? InstrumentRef.ForStock(Symbol, Currency) : InstrumentRef.ForOption(Option);
|
Option is null ? InstrumentRef.ForStock(Symbol, Currency) : InstrumentRef.ForOption(Option);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut den Auftrag aus dem <b>Instrument</b> – der einzige Weg, der die Identität vollständig
|
||||||
|
/// überträgt.
|
||||||
|
///
|
||||||
|
/// <para><b>Warum es diese Fabrik gibt.</b> 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 <c>SAP:EUR</c>, 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.</para>
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Ergebnis einer Order-Platzierung.</summary>
|
/// <summary>Ergebnis einer Order-Platzierung.</summary>
|
||||||
|
|||||||
@@ -247,6 +247,42 @@ public class ExecutionServiceTests
|
|||||||
await _capital.Received(1).SettleReservationAsync(1, 5, 500m, Arg.Any<CancellationToken>());
|
await _capital.Received(1).SettleReservationAsync(1, 5, 500m, Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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<OrderRequest>(o => o.Currency == "EUR" && o.Instrument.Key == "SAP:EUR"),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<OrderRequest>(o => o.Option == spec && o.Instrument.Kind == InstrumentKind.Option),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OrderFails_ReturnsError_AndDoesNotBook()
|
public async Task OrderFails_ReturnsError_AndDoesNotBook()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user