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

6.2 KiB
Raw Blame History

type _organized
Modul true

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

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).