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