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
@@ -0,0 +1,63 @@
using IBKRTrader.Core.Trading;
namespace IBKRTrader.Modules.DebugTrading.Logic;
/// <summary>
/// Die Sicherungen des Prüfmoduls – reine Rechnung, damit sie ohne Broker und ohne Datenbank
/// prüfbar sind.
///
/// <para><b>Die erste ist nicht verhandelbar:</b> Das Modul arbeitet ausschließlich im
/// Paper-Modus. Nicht als Einstellung, die jemand umlegen kann, sondern als Bedingung, die bei
/// jedem Lauf neu geprüft wird. Ein Testmodul, das auf einem Live-Konto anläuft, wäre der
/// teuerste denkbare Fehler dieses Projekts – und Einstellungen werden kopiert, wenn eine
/// Instanz von der Test- auf die Live-Maschine umzieht.</para>
/// </summary>
public static class DebugTradingGuard
{
/// <summary>
/// Prüft den Handelsmodus. Liefert <c>null</c>, wenn gearbeitet werden darf, sonst den Grund.
///
/// <para>Bewusst gegen den geparsten Modus und nicht gegen die Zeichenkette: Ein unbekannter
/// Wert fällt in <c>TradingSettings.ParsedMode</c> auf Paper zurück, und genau darauf will
/// sich diese Prüfung nicht verlassen. Sie sieht den Rohwert mit an.</para>
/// </summary>
public static string? CheckPaperMode(TradingMode mode, string rawMode)
{
if (mode != TradingMode.Paper)
return $"Handelsmodus ist {mode} – das Prüfmodul arbeitet ausschließlich im Paper-Modus.";
// Der Rohwert muss ebenfalls eindeutig Paper sagen. "Papier", "paper-live" oder ein
// Tippfehler ergeben sonst über den Rückfall ein stilles Ja.
if (!string.Equals(rawMode?.Trim(), nameof(TradingMode.Paper), StringComparison.OrdinalIgnoreCase))
return $"Handelsmodus \"{rawMode}\" ist nicht eindeutig Paper – das Prüfmodul bleibt stehen.";
return null;
}
/// <summary>
/// Prüft, ob eine neue Position eröffnet werden darf. <c>null</c> = ja.
/// </summary>
/// <param name="openPositions">Aktuell offene Testpositionen.</param>
/// <param name="maxOpen">Obergrenze.</param>
/// <param name="spentToday">Heute eröffneter Nominalwert.</param>
/// <param name="dailyBudget">Tagesbudget.</param>
/// <param name="plannedNotional">Nominalwert der geplanten Position.</param>
/// <param name="maxPerTrade">Obergrenze je Position.</param>
public static string? CheckOpening(
int openPositions, int maxOpen,
decimal spentToday, decimal dailyBudget,
decimal plannedNotional, decimal maxPerTrade)
{
if (openPositions >= maxOpen)
return $"Bereits {openPositions} offene Testpositionen (Grenze {maxOpen}).";
if (plannedNotional > maxPerTrade)
return $"Nominalwert {plannedNotional:F0} über der Grenze je Trade ({maxPerTrade:F0}).";
if (spentToday + plannedNotional > dailyBudget)
return $"Tagesbudget erschöpft: heute {spentToday:F0}, geplant {plannedNotional:F0}, " +
$"Budget {dailyBudget:F0}.";
return null;
}
}