Handelszeiten im Core: DebugTrading handelt nicht mehr in die geschlossene Boerse
Das Modul erzeugte ueber das Wochenende Dutzende wartender Orders - alle 15
Minuten eine, alle von IBKR angenommen und fuer Montag 09:00 vorgemerkt, alle
gleichzeitig ausloesend. Am 2026-09-06 waren es 31.
Die Auskunft gehoert in den Core, nicht ins Modul: Handelszeiten braucht jede
Strategie, und sie ist nichts, was ein Modul selbst wissen kann.
- MarketHours.cs: MarketSession, MarketSchedule (IsOpen, NextOpenUtc), der
Parser TradingHours und der Seam IMarketCalendar. Reine Rechnung, damit die
Formate ohne Broker pruefbar sind - 20 Tests ueber beide Schreibweisen der
TWS-API, Feiertage, Zeitraeume ueber Mitternacht und unbekannte Zonen.
- Die Zeiten kommen aus derselben Kontraktabfrage, die der Adapter ohnehin
macht: contractDetails liefert sie mit, eine eigene Runde waere Verschwendung.
- LiquidHours und nicht TradingHours. Letzteres enthaelt bei US-Werten die vor-
und nachboersliche Zeit; unsere Orders setzen OutsideRth nicht, wuerden dort
also gar nicht ausgefuehrt, sondern warten - genau der Zustand, den das hier
verhindern soll.
- Keine Ersatzzeitzone, wenn TWS eine unbekannte meldet. Mit der falschen Zone
gerechnete Handelszeiten saehen richtig aus und laegen um Stunden daneben.
- Unbekannt gilt als geschlossen. Wer aus fehlender Auskunft auf einen offenen
Markt schloesse, handelte bei jeder Stoerung ins Geschlossene.
DebugTrading.OnlyDuringMarketHours, Vorgabe an. Die Pruefung gilt fuer beide
Richtungen: Auch eine Schliessung erzeugte dieselbe wartende Order. Eine
Testposition ein paar Stunden laenger zu halten kostet dagegen nichts - und die
nicht geschlossene Position belegt ihren Platz weiter, sonst eroeffnete die
Planung ueber die eigene Grenze hinaus.
Auf der Testmaschine am Sonntagabend nachgewiesen:
[20:31:28] Kein Instrument handelbar, naechste Eroeffnung SAP:EUR um
2026-09-07 07:00 UTC.
07:00 UTC ist 09:00 MEZ - dieselbe Zeit, die IBKR heute frueh im Warntext 399
genannt hat. Zwei unabhaengige Quellen, gleiche Antwort. Seither keine neue
Order; der Lauf dauert zwei statt neunzig Sekunden.
524 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -186,4 +186,85 @@ public class DebugTradingLogicTests
|
||||
|
||||
parsed.Should().ContainSingle().Which.Key.Should().Be("SAP:EUR");
|
||||
}
|
||||
|
||||
// ─── Geschlossene Börse (#502) ──────────────────────────────────────
|
||||
|
||||
private static readonly Func<InstrumentRef, bool> NichtsHandelbar = _ => false;
|
||||
|
||||
/// <summary>
|
||||
/// Der Fall, um den es geht: Am Wochenende nimmt IBKR jede Order an und merkt sie für Montag
|
||||
/// vor. Ein Modul im 15-Minuten-Takt sammelt so über ein Wochenende Dutzende an, die dann alle
|
||||
/// auf einmal auslösen – am 2026-09-06 mit 31 offenen Orders beobachtet.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WithTheExchangeClosed_NothingIsOpened()
|
||||
{
|
||||
var actions = DebugTradingPlan.Plan(
|
||||
[], Now, 0m, Settings(), Price, new Random(1), NichtsHandelbar);
|
||||
|
||||
actions.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithTheExchangeClosed_ADuePositionIsNotClosedEither()
|
||||
{
|
||||
// Eine Schließung erzeugte dieselbe wartende Order wie eine Eröffnung. Die Testposition ein
|
||||
// paar Stunden länger zu halten, kostet dieses Modul dagegen nichts.
|
||||
var sap = InstrumentRef.ForStock("SAP", "EUR");
|
||||
var open = new[] { new OpenTestPosition(sap, 5, Now.AddMinutes(-1)) };
|
||||
|
||||
DebugTradingPlan.Plan(open, Now, 0m, Settings(), Price, new Random(1), NichtsHandelbar)
|
||||
.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ABlockedCloseStillOccupiesItsSlot()
|
||||
{
|
||||
// Sonst rechnete die Planung den Platz als frei, den die nicht geschlossene Position
|
||||
// weiterhin belegt - und eröffnete über die eigene Grenze hinaus, sobald ein anderes
|
||||
// Instrument handelbar ist.
|
||||
var sap = InstrumentRef.ForStock("SAP", "EUR");
|
||||
var alv = InstrumentRef.ForStock("ALV", "EUR");
|
||||
|
||||
var settings = Settings();
|
||||
settings.MaxOpenPositions = 3;
|
||||
settings.Universe = ["ALV:EUR"];
|
||||
|
||||
var open = new[]
|
||||
{
|
||||
new OpenTestPosition(sap, 5, Now.AddMinutes(-1)), // fällig, aber SAP ist zu
|
||||
new OpenTestPosition(sap, 5, Now.AddHours(5)),
|
||||
new OpenTestPosition(sap, 5, Now.AddHours(9))
|
||||
};
|
||||
|
||||
// Nur ALV ist handelbar - SAP weder schließbar noch eröffenbar.
|
||||
var actions = DebugTradingPlan.Plan(
|
||||
open, Now, 0m, settings, Price, new Random(1),
|
||||
i => i.Key == "ALV:EUR");
|
||||
|
||||
actions.Should().BeEmpty("drei Plätze sind belegt, auch der der nicht geschlossenen Position");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyTradableInstrumentsAreConsideredForOpening()
|
||||
{
|
||||
var settings = Settings();
|
||||
settings.Universe = ["SAP:EUR", "AAPL:USD"];
|
||||
|
||||
var actions = DebugTradingPlan.Plan(
|
||||
[], Now, 0m, settings, Price, new Random(1),
|
||||
i => i.Key == "AAPL");
|
||||
|
||||
var opened = actions.Should().ContainSingle().Subject
|
||||
.Should().BeOfType<DebugAction.Open>().Subject;
|
||||
opened.Instrument.Key.Should().Be("AAPL", "SAP ist gesperrt, AAPL nicht");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithoutTheCheck_NothingChanges()
|
||||
{
|
||||
// Die Vorgabe null heißt "ohne Prüfung" - damit bleibt jedes bestehende Verhalten gleich.
|
||||
DebugTradingPlan.Plan([], Now, 0m, Settings(), Price, new Random(1), null)
|
||||
.Should().ContainSingle();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user