4.2 KiB
4.2 KiB
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=falseals 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_idbzw. 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 TestsBrokerRegistry(app/broker/registry.py): wählt Adapter anhandDEFAULT_BROKER(V1: paper). Echte Broker-Adapter später austauschbar, keine Brokerlogik im Core-Code.
Idempotenz
- Unique-Index
uq_execution_order_srcaufsource_portfolio_decision_id→ gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder - Unique-Index
uq_execution_order_clientaufclient_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_versionexecution_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 anmarket.portfolio/trade.approved - Consumer-Fix aus Modul-04–08 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