DebugTrading: ein Modul, das absichtlich Last erzeugt

Alles, was den Handel absichert - Limits, Deckung, Kapitalpool, Reservierungen,
Abgleich, Sperren - war bisher ausschliesslich durch Unit-Tests mit gemockten
Gegenstellen belegt. Im Betrieb gab es fast keine Trades. Ein Fehler in der
Buchungskette faellt so erst auf, wenn eine echte Strategie scharf geht.

Das Modul eroeffnet kleine Positionen, haelt sie eine gewuerfelte Zeit von
Minuten bis Tagen und schliesst sie selbst. Es soll keine Rendite erzeugen,
sondern Last; die Auswahl ist zufaellig und ausdruecklich nicht renditegetrieben.

Planung und Sicherungen sind reine Rechnung mit einer von aussen gereichten
Zufallsquelle - genau deshalb ist ein Modul, das absichtlich wuerfelt, ueberhaupt
pruefbar. 14 Tests.

Die Sicherung, die nicht verhandelbar ist: Der Worker prueft bei JEDEM Lauf, ob
der Handelsmodus eindeutig Paper ist, und wirft sonst. Nicht nur beim Start -
Einstellungen werden kopiert, wenn eine Instanz umzieht. Geprueft wird gegen den
Rohwert UND den geparsten Modus: TradingSettings.ParsedMode faellt bei einem
unbekannten Wert auf Paper zurueck, und darauf darf sich diese Pruefung nicht
verlassen. Dazu Tagesbudget (in Betriebszeitzone, nicht UTC), Obergrenze offener
Positionen, Grenze je Trade und ein Not-Aus, der das Eroeffnen stoppt und offene
Positionen weiter betreut.

Schliessen hat Vorrang vor Eroeffnen: Sonst koennte eine erreichte Obergrenze
verhindern, dass faellige Positionen je geschlossen werden - die Sperre bliebe
fuer immer.

Beim Bauen hat der Architekturtest aus #474 seinen ersten echten Fang gemacht:
Das Modul griff nach IBrokerClient, um Kurse zu holen. Das verstoesst gegen
Regel 4. Statt den Test aufzuweichen ist eine lesende Kursquelle entstanden
(IQuoteSource) - wer einen Kurs braucht, soll dafuer nicht den Zugang bekommen,
mit dem sich Orders platzieren lassen.

Auf dem Testserver nachgewiesen: Das Modul laeuft im Takt, plant, wird bei
fehlendem Kurs sauber abgelehnt und meldet das - und mit Trading.Mode=Live
verweigert es den Dienst, waehrend unabhaengig davon auch der Broker-Adapter
den Paper-Port gegen den Live-Modus sperrt. Zwei Schichten, die nichts
voneinander wissen.

438 Tests gruen.

Arbeit an #468.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-05 14:27:58 +02:00
co-authored by Claude Opus 5
parent 27b594ca8f
commit 1dac482cc7
20 changed files with 1144 additions and 4 deletions
@@ -309,6 +309,71 @@ public class CapitalSettings
$"Hausreserve {HouseReservePercent:0.#} %, max. {MaxSymbolPercent:0.#} % je Einzelwert";
}
// ─── DebugTrading (Prüfmodul, niemals Live) ──────────────────────────────────
/// <summary>
/// Einstellungen des DebugTrading-Moduls (#468). Es erzeugt absichtlich Last auf der
/// Buchungskette – kleine Trades, gestreut über Handelsplätze, mit gewürfelter Haltedauer.
///
/// <para><b>Die Grenzen hier sind die zweite Verteidigungslinie.</b> Die erste ist die
/// Startverweigerung im Modul selbst, wenn der Handelsmodus nicht Paper ist; sie lässt sich nicht
/// wegkonfigurieren.</para>
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public class DebugTradingSettings
{
[Category("DebugTrading")]
[DisplayName("Aktiviert")]
[Description("Modulschalter. Aus = der Worker läuft nicht.")]
public bool Enabled { get; set; } = false;
[Category("DebugTrading")]
[DisplayName("Keine Neueröffnungen")]
[Description("Not-Aus: offene Testpositionen werden weiter geschlossen, aber keine neuen " +
"eröffnet. Der schonende Halt - Abschalten ließe offene Positionen unbetreut.")]
public bool StopOpening { get; set; } = false;
[Category("DebugTrading")]
[DisplayName("Takt (Minuten)")]
[Description("Abstand zwischen zwei Läufen. Je Lauf wird höchstens eine Position eröffnet.")]
public int IntervalMinutes { get; set; } = 15;
[Category("DebugTrading")]
[DisplayName("Max. offene Testpositionen")]
[Description("Obergrenze gleichzeitig offener Positionen dieses Moduls.")]
public int MaxOpenPositions { get; set; } = 5;
[Category("DebugTrading")]
[DisplayName("Max. Nominalwert je Trade")]
[Description("Obergrenze des Nominalwerts einer einzelnen Testposition, in Kontowährung.")]
public double MaxNotionalPerTrade { get; set; } = 1_000;
[Category("DebugTrading")]
[DisplayName("Tagesbudget")]
[Description("Summe der an einem Tag eröffneten Nominalwerte. Danach wird bis Mitternacht " +
"(Betriebszeitzone) nichts Neues mehr eröffnet.")]
public double DailyBudget { get; set; } = 10_000;
[Category("DebugTrading")]
[DisplayName("Haltedauer min. (Minuten)")]
[Description("Untergrenze der gewürfelten Haltedauer.")]
public int MinHoldMinutes { get; set; } = 10;
[Category("DebugTrading")]
[DisplayName("Haltedauer max. (Minuten)")]
[Description("Obergrenze der gewürfelten Haltedauer. Vorgabe 3 Tage - die lange Spanne ist " +
"der Zweck: sie prüft, ob offene Positionen Neustarts und Tageswechsel überstehen.")]
public int MaxHoldMinutes { get; set; } = 4_320;
[Category("DebugTrading")]
[DisplayName("Prüfuniversum")]
[Description("Instrumente, aus denen zufällig gewählt wird - Schreibweise \"SYMBOL:WÄHRUNG\". " +
"Bewusst über Währungen gestreut: das prüft Kontraktauflösung, Umrechnung und " +
"die Zeitzonenbehandlung der Ausführungszeiten mit.")]
public List<string> Universe { get; set; } =
["SAP:EUR", "ALV:EUR", "BAS:EUR", "AAPL:USD", "MSFT:USD"];
}
// ─── Accounting ──────────────────────────────────────────────────────────────
[TypeConverter(typeof(ExpandableObjectConverter))]
@@ -465,6 +530,12 @@ public class AppSettings
[Description("KI-Analyse/Forensik (OpenRouter, read-only)")]
public SupervisorSettings Supervisor { get; set; } = new();
/// <summary>
/// Prüfmodul, das absichtlich Last erzeugt (#468). Vorgabe: aus. Es verweigert den Dienst
/// ohnehin, sobald der Handelsmodus nicht Paper ist.
/// </summary>
public DebugTradingSettings DebugTrading { get; set; } = new();
[Category("Deploymentcenter")]
[DisplayName("Deploymentcenter")]
[Description("Lizenz, Watchdog, UpdateService, Fehler-Stream (https://dc.mhdf.de)")]
@@ -21,3 +21,18 @@ public interface IBrokerPortfolioReader
/// </summary>
Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default);
}
/// <summary>
/// Nur Kurse, nichts sonst.
///
/// <para>Getrennt von <see cref="IBrokerClient"/>, weil dort das Auslösen von Handel wohnt. Ein
/// Modul, das einen Kurs braucht, um eine Stückzahl zu rechnen, soll dafür nicht den Zugang
/// bekommen, mit dem sich Orders platzieren lassen (Kapitalmodell §2 Regel 4). Die Trennung
/// wurde am 2026-09-05 nötig, als das Prüfmodul Kurse brauchte und der Architekturtest den
/// Griff nach IBrokerClient zu Recht abgelehnt hat.</para>
/// </summary>
public interface IQuoteSource
{
/// <summary>Aktueller Kurs; <c>null</c>, wenn keiner vorliegt.</summary>
Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default);
}
@@ -15,7 +15,7 @@ namespace IBKRTrader.Core.Trading.Ibkr;
/// fehlgeschlagene Order). Ein Konto mit Wert 0 lässt die Risikoprüfung jedes Signal ablehnen –
/// die sichere Richtung, wenn der Broker nicht erreichbar ist.
/// </summary>
public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, IOptionMarketData, IDisposable
public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, IOptionMarketData, IQuoteSource, IDisposable
{
private const string LogModule = "IBKR";
@@ -7,7 +7,7 @@ namespace IBKRTrader.Core.Trading;
/// Wird registriert, bis der echte IBKR-Adapter angebunden und gegen den
/// Paper-Gateway verifiziert ist. So kann keine Order versehentlich rausgehen.
/// </summary>
public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader, IOptionMarketData
public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader, IOptionMarketData, IQuoteSource
{
private readonly LoggingService _logger;