trading-system-docs/modul-09-execution-service.md
Rain Ocampo df3cd3fc57 M09/M19: IG-Demo-Adapter + CLOSE-Pfad-Fix + Fake-MATCH-Fix (21.08.2026)
M09 (modul-09-execution-service.md):
- IG-Demo-Adapter (IG_DEMO-Modus) dokumentiert
- Order-Aktions-Auswertung: OPEN/INCREASE->create_position, CLOSE/REDUCE->close_position
- CLOSE-Direction korrekt umkehren (_close_direction: LONG->SELL, SHORT->BUY)
- broker_order_id/dealId sauber durchreichen
- unbekannte/ungueltige Aktionen FAIL-CLOSED (IG_UNSUPPORTED_ACTION)
- 6 IG-Adapter-Tests dokumentiert
- erster erfolgreicher IG-DEMO OPEN->CLOSE-E2E (US500)

M19 (modul-19-broker-reconciliation.md):
- Fake-MATCH Root Cause dokumentiert
- korrigierte interne Positionswahrheit: position_state (M18) primaer
- trade_journal/execution_order allein vortaeuschen keine interne offene Position
- qty=0 nie MATCH
- Tests: Unit 16/16, E2E 14/14 (S13 Fake-MATCH->MISSING_INTERNAL)
2026-08-21 06:48:27 +00:00

9.2 KiB
Raw Permalink 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
  • IgDemoAdapter (app/broker/ig_demo.py): IG-Markets-Demo-Broker (IG_DEMO-Modus), echter externer Broker-Adapter. Liest/schreibt über IG REST API (demo-api.ig.com).
  • BrokerRegistry (app/broker/registry.py): wählt Adapter anhand DEFAULT_BROKER (V1: paper). Echte Broker-Adapter später austauschbar, keine Brokerlogik im Core-Code.

IG-Demo-Adapter (IG_DEMO-Modus, 21.08.2026)

Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT — erster echter externer Broker-Adapter.

  • Eigener Modus IG_DEMO (nicht LIVE+Gate); PAPER/IG_DEMO/LIVE strikt getrennt.
  • Demo-Basis-URL erzwungen (demo-api.ig.com), LIVE-Endpunkt (api.ig.com) technisch blockiert, kein IG_DEMO→LIVE-Autowechsel.
  • Credentials ausschließlich VPS-Secret/ENV (.env.ig, chmod 600/root-only); nie in Code/Forgejo/Tolaria/DB/Logs.
  • Mengen instrumentabhängig über broker_mapping (PostgreSQL); kein globales 1:1.
  • Epic-Mapping produktiv in PostgreSQL broker_mapping; Forgejo nur Schema/Doku/Beispiele.

Order-Aktions-Auswertung (CLOSE-Pfad-Fix, 21.08.2026)

BrokerOrderRequest trägt action_type (Default OPEN) + broker_order_id. Der Service reicht beides durch; der Adapter wertet action_type aus:

action_type IG-Pfad
OPEN / INCREASE POST /positions/otc (create_position)
CLOSE / REDUCE POST /positions/otc mit _method:DELETE (close_position)
unbekannt/ungültig FAIL-CLOSED (IG_UNSUPPORTED_ACTION)

CLOSE-Direction korrekt umkehren: CLOSE einer LONG/BUY-Position muss mit SELL erfolgen (Gegenseite), nicht mit der Positionsrichtung. _close_direction(): LONG→SELL, SHORT→BUY.

broker_order_id/dealId sauber durchreichen: broker_order_id wird vom Service in die Order übernommen und an den Adapter durchgereicht; IG dealId wird als broker_order_id persistiert.

Unbekannte/ungültige Aktionen FAIL-CLOSED: _derive_action_type() setzt unbekannte Aktionen NICHT mehr auf OPEN zurück, sondern reicht sie durch → Control blockt mit UNKNOWN_ACTION_TYPE, Adapter mit IG_UNSUPPORTED_ACTION.

IG-Adapter-Tests (6/6 grün)

  1. test_unsupported_action_fail_closed — unbekannte Aktion → FAIL-CLOSED
  2. test_close_direction_inverted — CLOSE einer BUY-Position → SELL
  3. test_open_uses_create_position — OPEN → create_position
  4. test_close_uses_close_position — CLOSE → close_position mit _method:DELETE
  5. test_broker_order_id_passthrough — broker_order_id/dealId durchgereicht
  6. test_fail_closed_without_credentials — ohne Credentials → nicht konfiguriert

Erster erfolgreicher IG-DEMO OPEN→CLOSE-E2E (21.08.2026)

  • OPEN IGE2E-US500-OPEN-1787292734 → dealId DIAAAAYB46KMNAE, size 1.0, BUY, openLevel 7652.58 → FILLED
  • CLOSE-Bug gefunden + gefixt: erster CLOSE sendete fälschlich als zweite OPEN (action_type ignoriert) + falsche direction (BUY statt SELL)
  • CLOSE2 IGE2E-US500-CLOSE2-1787293768 für DIAAAAYB463S4ABFILLED (filled_quantity 1, avg_fill_price 7652.19)
  • IG GET /positions: Anzahl 0 — alle Positionen geschlossen
  • Safety-Reset: M09 zurück auf PAPER (EXECUTION_MODE=PAPER, TRADING_ENABLED=false, DEFAULT_BROKER=paper)

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

  • 31/31 Unit-Tests (test_execution.py + test_ig_demo.py + test_control.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, Control-Gate (M15), IG-Adapter (6/6)
  • 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).