--- type: Trading-Modul-System _organized: 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 - `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 `DIAAAAYB463S4AB` → **FILLED** (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-`) - 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 - **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).