From 5585c46caf560a9b08ebe2c7642a6c67768948c2 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Thu, 20 Aug 2026 08:40:26 +0000 Subject: [PATCH] Modul-09-Execution-Service: PAPER, FAIL-CLOSED, BrokerAdapter-Interface, PaperBroker V1, idempotente Execution --- modul-09-execution-service.md | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 modul-09-execution-service.md diff --git a/modul-09-execution-service.md b/modul-09-execution-service.md new file mode 100644 index 0000000..d6abf54 --- /dev/null +++ b/modul-09-execution-service.md @@ -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-`) +- 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`