Modul-09-Execution-Service: PAPER, FAIL-CLOSED, BrokerAdapter-Interface, PaperBroker V1, idempotente Execution

This commit is contained in:
Rain Ocampo 2026-08-20 08:40:26 +00:00
parent 312ffda48d
commit c01b315a64

View file

@ -0,0 +1,102 @@
# Modul-09: Execution-Service
**Status: FREIGEGEBEN** (20.08.2026)
Deterministischer Execution-Service. Konsumiert `TRADE_APPROVED` aus Modul-08,
validiert die Order hart (FAIL-CLOSED), übergibt sie an einen Broker-Adapter
(V1: PaperBrokerAdapter) und überwacht den Status. Ergebnis wird idempotent
in eigenen DB-Tabellen gespeichert und als RabbitMQ-Events publiziert.
**Sicherheit: KEIN unkontrolliertes Live-Trading.**
- Default `EXECUTION_MODE=PAPER` (bzw. DRY_RUN)
- `TRADING_ENABLED=false` als sicherer Default für LIVE
- LIVE nur über explizite Konfiguration aktivierbar, FAIL-CLOSED
- Bei Timeout nach Order-Senden NIE blind erneut senden — erst über
`broker_order_id`/`client_order_id` bzw. Brokerstatus klären
## Architektur
```
TRADE_APPROVED (market.portfolio/trade.approved, Modul-08)
ExecutionConsumer (execution.input, Consumer-Fix von Anfang an)
Order-Validierung (hart, FAIL-CLOSED)
│ source_portfolio_decision_id gültig, decision=APPROVED,
│ symbol/direction/quantity vorhanden, quantity>0,
│ entry/stop/target plausibel, Kette nachvollziehbar,
│ kein bereits ausgeführter identischer Auftrag, Kill-Switch
BrokerAdapter-Interface (modular, austauschbar)
│ V1: PaperBrokerAdapter (realistischer Lifecycle)
ExecutionStorage (idempotent, Advisory Lock + Transaktion)
│ execution_order + execution_event
RabbitMQPublisher (market.execution)
ORDER_SUBMITTED / ORDER_FILLED / ORDER_REJECTED / ORDER_FAILED
```
## BrokerAdapter-Interface
- `BrokerAdapter` (app/broker/base.py): `name()`, `is_configured()`,
`submit_order()`, `get_order_status()`
- `PaperBrokerAdapter` (app/broker/paper.py): V1, simuliert realistischen
Lifecycle (SUBMITTED → FILLED, Partial Fill, Reject, Timeout) für Tests
- `BrokerRegistry` (app/broker/registry.py): wählt Adapter anhand
`DEFAULT_BROKER` (V1: paper). Echte Broker-Adapter später austauschbar,
keine Brokerlogik im Core-Code.
## Idempotenz
- Unique-Index `uq_execution_order_src` auf `source_portfolio_decision_id`
→ gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder
- Unique-Index `uq_execution_order_client` auf `client_order_id`
- Client Order ID deterministisch aus Execution-ID abgeleitet
(`EXEC-<execution_id[:32]>`)
- Advisory Lock + Transaktion: parallele identische Events → exakt eine
Execution (Race-Condition-Schutz, wie Modul-08)
- ACK eines TRADE_APPROVED erst, wenn die Order dauerhaft in der DB gespeichert ist
## Order-Status
`PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED`
sowie `REJECTED`, `CANCELLED`, `FAILED`.
## DB-Tabellen
- `execution_order`: execution_id, source_portfolio_decision_id, broker, mode,
symbol, asset_class, direction, quantity, requested_price, stop_loss, target,
broker_order_id, client_order_id, status, filled_quantity, avg_fill_price,
timestamps, error/reason_codes, execution_version
- `execution_event`: append-only Log der Status-Übergänge
## RabbitMQ
- Exchange `market.execution` (topic, durable)
- Routing: `order.submitted`, `order.filled`, `order.rejected`, `order.failed`
- Queue `execution.input` (durable), gebunden an `market.portfolio`/`trade.approved`
- Consumer-Fix aus Modul-0408 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
## Tests
- **21/21 Unit-Tests** (test_execution.py): gültige Order → PAPER FILLED,
Idempotenz, ungültige Quantity → REJECTED, Kill-Switch, fehlende
Broker-Credentials → FAIL-CLOSED, Broker-Reject, Timeout → keine blinde
Doppelorder, Partial Fill, Reconnect-Backoff, parallele identische Events
- **E2E 6/6 Checks** (Kette 03→04→05→06→07→08→09): Execution-Order in DB,
exakt 1 Paper-Order, Pflichtfelder, Idempotenz, ORDER_SUBMITTED+ORDER_FILLED,
gültiger Trade → PAPER FILLED
- **Reconnect-Test**: RabbitMQ gestoppt → Backoff → neu verbunden, 1 Consumer,
kein queue_delete bei normalen Reconnects
## Deploy
- Container `Modul-09-Execution-Service`, Port 55009 nur Docker-intern
(kein öffentlicher Host-Port)
- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit
- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`