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

202 lines
9.2 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.

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