From af3758bb6757f1bd67ef2573c73f3ba48ca44ab7 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Thu, 20 Aug 2026 09:45:06 +0000 Subject: [PATCH] Modul-10: Trade-Journal FREIGEGEBEN (append-only, FAIL-CLOSED, Bug-Fix Status-Update) --- modul-10-trade-journal.md | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 modul-10-trade-journal.md diff --git a/modul-10-trade-journal.md b/modul-10-trade-journal.md new file mode 100644 index 0000000..f782111 --- /dev/null +++ b/modul-10-trade-journal.md @@ -0,0 +1,106 @@ +# 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`