trading-system-docs/notes/trading/system-docs/modul-10-trade-journal.md

4.6 KiB

type _organized
Trading-Modul-System true

Modul-10: Trade-Journal

Status: FREIGEGEBEN (20.08.2026)

Deterministisches, append-only Trade-Journal. Konsumiert die Execution-Events (ORDER_SUBMITTED/ORDER_FILLED/ORDER_REJECTED/ORDER_FAILED) aus Modul-09, validiert die Herkunftskette hart (FAIL-CLOSED) und dokumentiert jeden Trade in einer Audit-Trail-konformen, idempotenten Struktur. Ergebnis wird als TRADE_RECORDED-Event publiziert.

Sicherheit & Audit:

  • Keine KI/ML, keine Orderausführung, keine Risikoberechnung — reines Dokumentieren
  • Append-only: trade_event_history ist unveränderlich (nur INSERT, kein UPDATE/DELETE)
  • Herkunftskette wird validiert (Signal → Ranking → Risk → Portfolio → Execution)
  • Bei inkonsistenter Kette → Trade als INCONSISTENT markiert (FAIL-CLOSED), nicht stumm verworfen
  • TRADE_RECORDED optional: ACK nach persistenter Speicherung

Architektur

ORDER_* (market.execution, Modul-09)
   │
   ▼
JournalConsumer (journal.input, Consumer-Fix von Anfang an)
   │
   ▼
Herkunfts-Validierung (FAIL-CLOSED)
   │  Kette 05→06→07→08→09 nachvollziehbar?
   │  provenance_consistent in metadata
   ▼
JournalService
   │  erstes Event  → Trade anlegen + TRADE_RECORDED
   │  Folge-Event   → append-only Event + Status-Update (bestehenden Trade)
   ▼
JournalStorage (idempotent, Unique-Index)
   │  trade_journal + trade_event_history
   ▼
RabbitMQPublisher (market.journal)
      TRADE_RECORDED (trade.recorded)

Service-/Storage-Grenze (vereinheitlicht)

Storage gibt IMMER TradeJournal zurück — nie ein dict.

  • Einzige Konvertierungsstelle _row_to_trade (DB-Zeile → Modell)
  • get_trade_by_execution()Optional[TradeJournal]
  • Der Service arbeitet ausschließlich mit Modellen (kein dict/Model-Mix je Codepfad)
  • Dadurch ist trade_id auf jedem Rückgabewert garantiert verfügbar
  • (Fix gegen 'dict' object has no attribute 'trade_id' → NACK/Requeue)

Verarbeitung

  • Erstes Event (ORDER_SUBMITTED) → Trade anlegen, TRADE_RECORDED publizieren
  • Folge-Event (ORDER_FILLED u.a.) → bestehenden Trade als Status-Update verarbeiten: append-only Event + Status-Update, KEIN neuer Trade, kein zweites TRADE_RECORDED
  • Bei FILLED: entry_filled/filled_at/Status aktualisiert (Trade öffnen/aktualisieren)
  • Eingehende Events werden nicht doppelt verarbeitet (idempotent, Unique-Index)

Idempotenz

  • Unique-Index auf (execution_id, event_id) → jedes Event exakt einmal
  • provenance_consistent in metadata (jsonb), nicht als Spalte
  • TRADE_RECORDED nur beim ersten Event pro Trade
  • Kein Doppel-Journal bei Re-Publish desselben Events

DB-Tabellen

  • trade_journal: trade_id, execution_id, source_portfolio_decision_id, Herkunftsketten-IDs (signal/ranking/risk/portfolio), symbol, asset_class, strategy, direction, timeframe, entry_requested, entry_filled, stop_loss, target, quantity, risk_amount, execution_mode, broker_order_id, status, provenance_consistent (in metadata), created_at, updated_at
  • trade_event_history: execution_id, event_id, event_type, payload, created_at (append-only, unveränderlich)

RabbitMQ

  • Exchange market.journal (topic, durable)
  • Routing: trade.recorded
  • Queue journal.input (durable), gebunden an market.execution/order.*
  • Consumer-Fix von Anfang an: dauerhafte Verbindung, process_data_events(time_limit=1.0) in innerer Schleife, KEIN queue_delete bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s
  • ACK erst nach persistenter Speicherung; bei Fehler NACK/Requeue

Tests

  • 39/39 Unit-Tests (test_journal.py): Anlage, Status-Update, Idempotenz, FAIL-CLOSED-Herkunftskette, append-only, exakt 1 TRADE_RECORDED, Reconnect
  • Gezielter Fix-Test 9/9 (test_fix_update.py): SUBMITTED→Journal, FILLED→Update, Rückgabe ist TradeJournal (kein dict), trade_id vorhanden, genau 1 Trade, append-only (2 Events), idempotent, kein Doppel-Journal
  • E2E 7/7 Checks (Kette 03→10): Journal-Eintrag in DB, exakt 1 Trade, Pflichtfelder + Herkunftskette konsistent, append-only Event-Historie (SUBMITTED+FILLED), exakt 1 TRADE_RECORDED, Idempotenz nach Re-Publish
  • Reconnect-Test: RabbitMQ gestoppt → Backoff 1→2→4→8→16s → wieder verbunden, journal.input 1 Consumer, 0 Messages, kein Zombie
  • Log-Check: 0 ERROR, 0 Traceback, ACK auf ORDER_SUBMITTED + ORDER_FILLED

Deploy

  • Container Modul-10-Trade-Journal, Port 55010 nur Docker-intern (kein öffentlicher Host-Port)
  • Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit
  • Env: EXECUTION_MODE=PAPER, TRADING_ENABLED=false