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

4.2 KiB
Raw Blame History

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