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)
9.4 KiB
| knowledge_schema | id | type | role | representation | state |
|---|---|---|---|---|---|
| 1 | object/48bd264f-607b-15f1-5f73-3e922af9b19d | arch | module | source | current |
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=falseals 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_idbzw. 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 TestsIgDemoAdapter(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 anhandDEFAULT_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)
test_unsupported_action_fail_closed— unbekannte Aktion → FAIL-CLOSEDtest_close_direction_inverted— CLOSE einer BUY-Position → SELLtest_open_uses_create_position— OPEN → create_positiontest_close_uses_close_position— CLOSE → close_position mit_method:DELETEtest_broker_order_id_passthrough— broker_order_id/dealId durchgereichttest_fail_closed_without_credentials— ohne Credentials → nicht konfiguriert
Erster erfolgreicher IG-DEMO OPEN→CLOSE-E2E (21.08.2026)
- OPEN
IGE2E-US500-OPEN-1787292734→ dealIdDIAAAAYB46KMNAE, 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-1787293768fürDIAAAAYB463S4AB→ 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_srcaufsource_portfolio_decision_id→ gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder - Unique-Index
uq_execution_order_clientaufclient_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_versionexecution_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 anmarket.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).