trading-system-docs/modul-09-execution-service.md

102 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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