Modul-10: Trade-Journal FREIGEGEBEN (append-only, FAIL-CLOSED, Bug-Fix Status-Update)
This commit is contained in:
parent
5585c46caf
commit
90cf20036e
1 changed files with 106 additions and 0 deletions
106
modul-10-trade-journal.md
Normal file
106
modul-10-trade-journal.md
Normal file
|
|
@ -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`
|
||||||
Loading…
Reference in a new issue