151 lines
6.2 KiB
Markdown
151 lines
6.2 KiB
Markdown
---
|
||
type: Modul
|
||
_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
|
||
- `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-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
|
||
|
||
- **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).
|