trading-system-docs/notes/trading/system-docs/modul-09-execution-service.md
Red Queen 022c30e537 feat(tolaria): migrate C3H HR wave 1 — 16 safe root↔canonical pairs to schema v1
C3H HUMAN REVIEW WAVE 1: migriert 16 freigegebene HR-A PAIR_MIGRATION_SAFE Paare.
Pro Pair: Root (representation=source) + Canonical (representation=canonical,
derived_from=Root-ID) atomar. Body byte-genau unveraendert. IDs stabil aus
C3 Mapping Preview v2. modul-12/18/19/vps.md NICHT angefasst.
Knowledge schema v1. (Red Queen)
2026-08-25 19:19:02 +00:00

215 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
knowledge_schema: 1
id: object/ffa8db13-8aa6-d4a4-6756-69eabd0c8d77
type: arch
role: module
representation: canonical
state: current
derived_from: object/48bd264f-607b-15f1-5f73-3e922af9b19d
_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-<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).