6.1 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
Geändert
Geändert von: Rain Ocampo
Datum: 20.08.2026
Grund: Modul-09-Doku um final freigegebenen Control-Gate-Status (M15→M09, Variante C) ergänzt.
Kernregel, Einbau-Punkt, Audit-Felder, Parameter, Live-E2E-Ergebnisse dokumentiert.
Control-Gate (M15→M09, FREIGEGEBEN 20.08.2026)
Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT — Variante C (Event-Cache-Vorfilter + synchroner HTTP-Final-Check).
M15 (Modul-15-Monitoring-Control) ist die autoritative Safety-/Trading-Control-Instanz.
M09 sendet neue Orders (OPEN/INCREASE) nur, wenn M15 eindeutig ENABLED + frisch bestätigt.
Kernregel
| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |
|---|---|---|
ENABLED |
ERLAUBT | erlaubt |
PAUSED |
VERBOTEN | erlaubt |
HALTED |
VERBOTEN | erlaubt |
UNKNOWN / M15-down / stale |
VERBOTEN (FAIL-CLOSED) | erlaubt |
Einbau-Punkt
Control-Check zwischen Schritt 4 (Order atomar in DB anlegen) und Schritt 5 (_submit_and_track).
Bei OPEN/INCREASE ohne Erlaubnis → Order als FAILED/REJECTED speichern, nicht senden.
TRADING_ENABLED-Flag bleibt unverändert (blockiert nur LIVE, nicht PAPER).
Audit-Felder je Order
action_type, control_status, control_state_version, control_expires_at,
control_audit_id, control_check_source (http|cache|bypass|none).
Parameter
- Final-Check-HTTP-Timeout: 500 ms, Retry: max 1 sofort.
expires_at-TTL (M15): 500 ms; Cache-TTL (M09): 5 s.- Kein Auto-Resume; kein "Senden ohne Check".
Live-E2E (VPS, 20.08.2026) — alle Szenarien grün
S1 ENABLED 9/9 · S2 HALTED 9/9 · S3 PAUSED 4/4 · S4 Risikoabbau 4/4 · S5 M15-down 7/7 · S6 Cache-vs-Final 2/2 · S7 Race/TTL 2/2 · S8 Event-Cache verifiziert · S9 Recovery 3/3 · S10 Health/Security verifiziert · S11 Regression bestanden.
Details: siehe modul-15-m09-anbindung-implementierung.md (FREIGEGEBEN).