# 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`