trading-system-docs/modul-15-m09-anbindung-design.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

339 lines
18 KiB
Markdown
Raw 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/1d6c2323-a31a-8b50-fcdd-3c9268175a69
type: design
role: design
representation: source
state: current
---
# Design-Vorschlag: Modul-15 → Modul-09 Anbindung (Safety-/Trading-Control)
**Status: NUR DESIGN-VORSCHLAG — keine Implementierung.**
**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)
**Gilt für:** Modul-09-Execution-Service (FREIGEGEBEN, Order-Pfad) ↔ Modul-15-Monitoring-Control (IMPLEMENTIERT + E2E-verifiziert, **noch nicht freigegeben**)
> **Scope-Regeln (verbindlich):**
> - M09 wird **nicht** verändert, bis dieser Vorschlag vom Nutzer freigegeben wurde.
> - M15 bleibt **nicht freigegeben** bis Nutzer-Freigabe.
> - M16 wird **nicht** begonnen.
> - Dieser Vorschlag ist **Architektur/Pseudoflow**, kein Code.
---
## 1. Ziel
**Modul-15** wird die **autoritative Safety-/Trading-Control-Instanz** für den Order-Durchstich.
**Modul-09** darf neue Orders **nur senden, wenn M15 dies eindeutig erlaubt**.
Kernauftrag: kein OPEN/INCREASE, solange M15 nicht *eindeutig* ENABLED bestätigt. Gleichzeitig darf ein HALT **niemals** Risikoreduktion / Position-Schließen verhindern (Exit-Pfade bleiben immer frei).
---
## 2. Ist-Zustand in M09 (Kontext für den Vorschlag)
Der Order-Pfad in `app/core/service.py::_on_event` ist heute:
```
TRADE_APPROVED (Modul-08)
→ 1) Idempotenz (already_processed)
→ 2) Harte Validierung validate_trade_approved (FAIL-CLOSED)
→ 3) Broker-Bereitschaft validate_broker_ready (FAIL-CLOSED)
→ 4) Order atomar in DB anlegen (Advisory Lock + Transaktion, PENDING)
→ 5) _submit_and_track → BrokerAdapter.submit_order
```
**Heutiger Kill-Switch:** rein konfigurativ `TRADING_ENABLED=false` (Default, sicher für LIVE).
Es existiert **keine** externe Echtzeit-Control-Schnittstelle im Order-Pfad.
**Einbau-Punkt (Vorschlag):** zwischen Schritt 4 (Order angelegt) und Schritt 5 (Broker-Submit)
— d. h. **unmittelbar vor `submit_order`**. Alternativ als zusätzlicher Check in Schritt 2.
Empfehlung: als eigener Schritt zwischen 4 und 5, weil dort die Order bereits idempotent in der DB
liegt und wir den Final-Check „direkt vor dem Senden" platzieren (Race-Window minimal).
---
## 3. Trading-States & Regelwerk
### 3.1 Globale Trading-States (autoritativ: M15)
| State | Bedeutung | neue OPEN/INCREASE | REDUCE | CLOSE/CANCEL |
|-------|-----------|--------------------|--------|--------------|
| `ENABLED` | Alle krit. Dienste gesund, aktuelle Market-Data, Trading erlaubt | **ERLAUBT** | erlaubt | erlaubt |
| `PAUSED` | Nicht-krit. Störung (Analytics/Notification), Risiko-umfeld unklar | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |
| `HALTED` | Krit. Ausfall (PG/RMQ/Execution/Market-Data/UNKNOWN) | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |
| `UNKNOWN` / M15 nicht erreichbar / Status veraltet | Unklarheit | **VERBOTEN (FAIL-CLOSED)** | **ERLAUBT** (konservativ) | **ERLAUBT** |
**Kernregel:** Nur `ENABLED` erlaubt neue Positionen (OPEN) oder Aufstockung (INCREASE).
**Alles andere** (PAUSED/HALTED/UNKNOWN/nicht erreichbar/veraltet) blockiert OPEN/INCREASE.
**REDUCE/CLOSE** (Risikoreduktion/Position schließen) sind **immer** erlaubt — unabhängig vom State.
### 3.2 Order-Kategorien (Einordnung)
| Aktion | Bedeutung | Regel bei PAUSED/HALTED/UNKNOWN |
|---|---|---|
| `OPEN` | Neue Position eröffnen | **BLOCKIEREN** |
| `INCREASE` | Bestehende Position aufstocken | **BLOCKIEREN** (Neue-Position-Logik, Risiko wächst) |
| `REDUCE` | Position verkleinern | **ERLAUBT** (Risiko sinkt) |
| `CLOSE` | Position schließen | **ERLAUBT** (Risiko eliminieren) |
| `CANCEL` | Offene/geplante Order stornieren | **ERLAUBT** (kein neues Risiko) |
> Begründung INCREASE blockiert: Aufstocken fügt neues Marktrisiko hinzu — bei HALT/Unklarheit
> unerlaubt. REDUCE/CLOSE/CANCEL sind per Definition risikoreduzierend → immer frei.
---
## 4. Vergleich der Anbindungs-Varianten
### Variante A — synchroner HTTP-Check vor jeder Order
M09 ruft vor jedem Submit `GET /control/trading-state` auf M15 (Docker-intern) auf.
| Kriterium | Bewertung |
|---|---|
| **Sicherheit** | Sehr hoch — liest immer den frischesten autoritativen Zustand. FAIL-CLOSED bei Timeout. |
| **Latenz** | Pro Order +1 Round-Trip (ms-Bereich, Docker-Netz). Bei vielen Orders spürbar, aber Orders sind selten. |
| **Ausfallsicherheit** | M15-down → HTTP-Time out → FAIL-CLOSED (blockieren). Sicher, aber **M15 wird Single Point of Failure für OPEN**. |
| **Race Conditions** | M15 könnte direkt nach Antwort auf HALTED wechseln → Race trotz Check (siehe §7). Final-Check nur minimal schmaler. |
| **Single Point of Failure** | **Ja** — M15 ist für OPEN/INCREASE zwingend. Bei M15-down wird der Execution-Service sonst blind blockiert. |
| **Bei RabbitMQ-Ausfall** | HTTP bleibt funktionsfähig (separater Kanal) → Check ok; aber M15 meldet RABBITMQ_UNREACHABLE→HALTED → korrekt blockiert. |
| **Bei M15-Ausfall** | Timeout → FAIL-CLOSED → OPEN/INCREASE blockiert (gewünscht). REDUCE/CLOSE müssen **ausgenommen** werden, sonst kann Position nicht geschlossen werden. |
| **Komplexität** | Gering — ein HTTP-Call, kein Cache, kein Subscription. |
| **Nachvollziehbarkeit/Audit** | M15 protokolliert jede Anfrage; M09 loggt Check + Ergebnis. Gut, aber hochfrequent (jede Order). |
### Variante B — Event-basierter Status-Cache (publish/subscribe)
M15 publiziert Statusänderungen auf `market.control`; M09 subscribt und hält ein lokales State-Modell.
| Kriterium | Bewertung |
|---|---|
| **Sicherheit** | Mittel — Status nur so frisch wie das letzte Event + Cache-TTL; **riskant ohne TTL/Herzschlag**. |
| **Latenz** | Sehr gering — kein HTTP im kritischen Pfad, Cache-Lookp. |
| **Ausfallsicherheit** | Schlecht — RabbitMQ-Ausfall ⇒ keine Events mehr; Cache wird **stale**. Ohne TTL würde M09 mit veraltetem ENABLED weiter traden. **Gefahr.** |
| **Race Conditions** | Stale-Cache ist die größte Race-Quelle. Event-Reihenfolge/Lag. |
| **Single Point of Failure** | RabbitMQ als Event-Bus ist der SPOF für den Status-Transport. |
| **Bei RabbitMQ-Ausfall** | Events stoppen → Cache veraltet → **genau das Risiko**, das wir vermeiden wollen. Muss über Cache-TTL + Fail-CLOSED gelöst werden. |
| **Bei M15-Ausfall** | Keine neuen Events → Cache veraltet. Ohne TTL gefährlich (weiter OPEN). |
| **Komplexität** | Mittel — Subscription, Cache-Update, TTL-Reinigung, RabbitMQ-Reconnect im M09. |
| **Nachvollziehbarkeit/Audit** | Gut — Event-Stream ist Append-Log; aber Cache-Echo im M09 ist ein zweites Modell (Konsistenzpflege). |
### Variante C — Kombination: Event-Cache + synchroner Final-Check (EMPFEHLUNG)
**Event-Cache als schnelle Vorfilterung**, **synchroner HTTP-Final-Check unmittelbar vor dem
Broker-Submit** als harte Bestätigung. Nur wer beide klar "ENABLED" liefert, darf OPEN/INCREASE.
| Kriterium | Bewertung |
|---|---|
| **Sicherheit** | **Sehr hoch** — zweistufig. Cache als Bauraus/Performance, HTTP-Final als Autorität. |
| **Latenz** | Fast so gering wie B im Common-Case, da Vorfilter die meiste Zeit per Cache durchläuft; **aber** der obligatorische Final-Check ist der Latenzdominante Teil (immer +1 HTTP). |In Praxis Orders selten → akzeptabel. |
| **Ausfallsicherheit** | Beste — Cache kann "PAUSED/HALTED/UNKNOWN" sofort blockieren; HTTP- Ausfall ⇒ FAIL-CLOSED. Zwei unabhängige Quellen. |
| **Race Conditions** | Der synchronier Final-Check (TTL = kurz, unmittelbar vor dem Submit) minimiert die Chance, dass M15 direkt danach HALTED setzt. Plus `control_version`-Bump (§7) zum harten Ausschluss. |
| **Single Point of Failure** | M15 bleibt SPOF für OPEN/INCREASE (unvermeidbar, da autoritativ). REDUCE/CLOSE ausgenommen. |
| **Bei RabbitMQ-Ausfall** | Cache veraltet → aber Cache-Logik blockiert bei "veraltet/UNKNOWN"; HTTP-Final ist unabhängig vom Bus → liefert korrekte M15-Sicht (die selbst RMQ-Ausfall als HALTED sieht). |
| **Bei M15-Ausfall** | HTTP-Final-Check schlägt fehl → FAIL-CLOSED, OPEN/INCREASE blockiert. REDUCE/CLOSE frei. |
| **Komplexität** | Höher (beides). Aber gut beherrschbar; Cache als reiner nicht-kritischer Vorfilter, Autorität liegt klar beim HTTP. |
| **Nachvollziehbarkeit/Audit** | Exzellent — Final-Check liefert `state_version` + `timestamp` + `audit_id` pro Order; Cache-Zustand separat auditierbar. |
### Zusammenfassung der Varianten
| | A (HTTP) | B (Event-Cache) | C (Kombi) |
|---|---|---|---|
| Sicherheit | hoch | mittel | **sehr hoch** |
| Latenz | +HTTP | **sehr gering** | gering (HTTP-Final) |
| Ausfallsicherheit | ok (SPOF M15) | **schwach** (RMQ SPOF) | **beste** |
| Race-Risiko | minimiert (kurz) | **hoch (stale)** | **minimiert** |
| RMQ-Ausfall | korrekt (HALTED) | **risiko (stale)** | korrekt |
| M15-Ausfall | FAIL-CLOSED | **risiko (stale)** | FAIL-CLOSED |
| Komplexität | gering | mittel | **höher** |
| Audit | gut | zweites Modell | **exzellent** |
**Empfehlung: Variante C** (Event-Cache als Vorfilter + synchroner Final-Check).
---
## 5. Race Condition & Lösung
**Problem:** M09 prüft `ENABLED`, M15 wechselt sofort danach auf `HALTED` — bevor M09 die Order gesendet hat.
**Lösung (Kombination):**
1. **Final-Check unmittelbar vor `submit_order`** — minimales Zeitfenster (ms) zwischen Check und Send.
2. **TTL auf der HTTP-Antwort:** `expires_at` (z. B. `now + 500 ms`). Ist die Antwort beim Senden abgelaufen
(Fenster überschritten), NICHT senden → Order nach `FAILED/REJECTED` mit `CONTROL_STATE_STALE` markieren.
3. **`state_version`** (Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check
gesehene Version. Beim Broker-Submit trägt die Order die `control_version`; die Audit-Kette verbindet
genau die Version, die gegolten hat, mit dem Versand.
4. **Bestätigtes Einverständnis:** Der Check liefert `trading_status=ENABLED` **und** `state_version` **und**
`expires_at`. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen.
5. **Audit-ID / Correlation:** M09 erzeugt `control_audit_id`, M15 protokolliert die Bestätigung. Damit ist
der „wir haben im ENABLED-Zustand gesendet" Moment exakt nachvollziehbar.
**Kein "Senden dann hoffen":** Ist das Fenster überschritten oder Status unklar → `FAILED` mit
`CONTROL_STATE_UNKNOWN/CONTROL_STATE_STALE`, **niemals** blind erneut senden (konsistent zum bestehenden
M09-Time-out-Handling: erst Status beim Broker klären, dann entscheiden).
---
## 6. API-Vertrag M15 → M09
### 6.1 `GET /control/trading-state` (von M09 im Final-Check)
**Request:** keiner (oder optional `?action=OPEN` / `?for_action=OPEN` zur Kontext-Auditierung)
**Response 200 (ENABLED):**
```json
{
"trading_status": "ENABLED", // ENABLED|PAUSED|HALTED|UNKNOWN
"system_status": "HEALTHY", // Kontext
"state_version": 42, // monoton, von M15 je Änderung
"timestamp": "2026-08-20T15:30:00Z", // Erzeugzeit (UTC, ISO 8601)
"expires_at": "2026-08-20T15:30:00.5Z", // TTL-Horizont (Server), z. B. now+500ms
"ttl_seconds": 0.5,
"audit_id": "ctl-..." // M15-interne Audit-Kennung der Bestätigung
}
```
**Nur** dieses Objekt mit `trading_status=ENABLED` und `expires_at` in der Zukunft gilt als "eindeutige Erlaubnis".
**Response (blockiert):** gleiches Schema, aber `trading_status` = `PAUSED`/`HALTED`/`UNKNOWN`.
**Fehler/Timeouts:**
- HTTP 503 → M15 selbst nicht Ready/unbekannt → **FAIL-CLOSED** (als HALTED/unbekannt behandeln).
- Timeout/5xx/netz → **FAIL-CLOSED**, als `UNKNOWN` behandeln.
- Alle nicht-`ENABLED`-Antworten → OPEN/INCREASE **verweigert**.
### 6.2 Empfehlung der aktuellen API-Form
M15 hat aktuell `GET /control/trading-state` (liefert `trading_status` + `system_status`). Für M09-Anbindung
empfehle ich die obige **Erweiterung** um `state_version`, `timestamp`, `expires_at`, `audit_id` — dies ist ein
**additiver API-Vertrag** (kein Breaking Change). Das ist im Vorschlag berücksichtigt; die konkrete
Umsetzung erfolgt erst nach Freigabe.
---
## 7. Event-Schema für Status-Änderungen (market.control)
M15 publiziert auf Exchange `market.control`, Routing `trading.state`**nur bei Status-Änderung** (keine Schleife).
```json
{
"event_id": "evt-...",
"event_type": "trading.state",
"timestamp": "2026-08-20T17:30:00Z",
"version": 1,
"payload": {
"trading_status": "HALTED", // ENABLED|PAUSED|HALTED|UNKNOWN
"system_status": "UNHEALTHY",
"state_version": 43,
"reason_codes": ["POSTGRESQL_UNREACHABLE", "PERSISTENCE_FAILURE"],
"changed_at": "2026-08-20T17:30:00Z"
}
}
```
**Im Cache (Variante C):**
- M09 hält `last_state` (trading_status + state_version + received_at).
- **TTL (z. B. 5 s):** ist `received_at` älter als TTL → Cache behandelt als `UNKNOWN/veraltet` → OPEN/INCREASE
**blockiert** (auch wenn letztes Event `ENABLED` war).
- Ein Event `state_version` älter als bereits gesehen → ignorieren (kein Regress).
---
## 8. Verhalten nach Recovery
- **Kein automatisches RESUME:** M15 nimmt nach Behebung der Ursache Status selbst neu (z. B. HALTED→HEALTHY→ENABLED)
**nur durch den regulären Monitoring-Zyklus** (Ursache wirklich erneut geprüft). Es gibt kein verstecktes Auto-Resume.
- M09 wartet darauf: erst wenn ein **neues Event mit `trading_status=ENABLED` und neuer `state_version`** oder
ein **HTTP-Final-Check mit `ENABLED`** vorliegt, sind OPEN/INCREASE wieder möglich.
- Ein `PAUSED→ENABLED`-Übergang verlangt, dass M15 nachweislich die früheren HALT-Gründe entfernt hat
(keine Reason-Codes mehr). Dies wird durch die bestehende FAIL-CLOSED-Regel + idempotentes Event sichergestellt.
- **Cache-TTL-Disziplin:** Nach einem HALTED/PAUSED-Ereignis bleibt M09 vorsichtig: Es traut dem Cache erst
wieder, wenn ein neues `ENABLED`-Event (hohe Version) eingegangen ist — niemals basierend auf Zeitablauf der
Sperre, sondern auf **bestätigter Freigabe**.
---
## 9. Audit in M09 (zusätzlich zu M15-Audit)
Für jede Order-Entscheidung (im M09-Storage bzw. `execution_event`):
- `control_status`: Ergebnis des Final-Checks (`ENABLED`/`PAUSED`/...)
- `control_state_version`: Version, die beim Send gegolten hat
- `control_expires_at` / `control_ttl`: TTL-Horizont
- `control_audit_id`: vom Final-Check zugeordnet
- `control_check_source`: `final_http` (und optional `cache`)
Damit ist jede Order exakt dem autoritativen Zustand zum Sendzeitpunkt zugeordnet (Audit-Kette vollständig).
---
## 10. Zusammenfassung Architektur-Empfehlung
**Variante C**: Event-Cache (mark-to-control, TTL, state_version) als **nicht-kritischen Vorfilter** im M09 +
**synchronier HTTP-Final-Check** (`GET /control/trading-state`, mit `expires_at`+`state_version`+`audit_id`)
unmittelbar vor `submit_order`. Beide müssen `ENABLED` und frisch sein; sonst OPEN/INCREASE blockiert
(FAIL-CLOSED). REDUCE/CLOSE/CANCEL sind von der Control-Regel ausgenommen und laufen immer.
- **Zustand, der OPEN/INCREASE erlaubt:** Cache == `ENABLED` (frisch) UND Final-Check == `ENABLED` (frisch).
- **Sonst alles:** blockiert OPEN/INCREASE; REDUCE/CLOSE/CANCEL frei.
- **FAIL-CLOSED:** M15 nicht erreichbar, Timeout, Cache veraltet, `UNKNOWN` → OPEN/INCREASE verweigert.
- **M15 bleibt SPOF nur für OPEN/INCREASE** (unvermeidbar, autoritativ). Exit/Close bleibt immer möglich.
---
## 11. Pseudoflow (Order-Durchstich M09)
```
TRADE_APPROVED (Modul-08)
1. Kategorisiere Aktion: OPEN | INCREASE | REDUCE | CLOSE | CANCEL
├─ action ∈ {REDUCE, CLOSE, CANCEL} → ERLANG DURCHSTICH (kein Control-Check)
│ └─→ weiter zum bestehenden Submit-Pfad
└─ action ∈ {OPEN, INCREASE}
2. [Cache-Vorfilter] local_state = cache.get()
├─ local_state ist stale (age > TTL) → → zum Final-Check (unten) aber NICHT aus Cache ableiten
└─ local_state.trading_status != ENABLED → BLOCKIERT (markiere FAILED/REJECTED, CONTROL_STATE_*)
3. [Final-Check] resp = M15 GET /control/trading-state (Time out: 800ms, Retry: 1×)
├─ Timeout/5xx → FAIL-CLOSED → BLOCKIERT (FAILED, CONTROL_STATE_UNKNOWN)
└─ resp.trading_status != ENABLED → BLOCKIERT (FAILED, CONTROL_STATE_*)
└─ resp.expires_at <= now → BLOCKIERT (FAILED, CONTROL_STATE_STALE)
4. Zustand grün (ENABLED, frisch, version=resp.state_version)
→ Order in DB (PENDING, CONTROL_FIELDS: version, audit_id, expires)
5. unmittelbar danach: broker.submit_order(request) ← nur zwischen 4 und 5 minimales Fenster
6. Ergebnis verarbeiten (FILLED/REJECTED/FAILED), Audit-Event mit control_* schreiben
```
### Zeit-/Retry-Parameter (Vorschlag)
- Final-Check-HTTP-Timeout: **500 ms** (Docker-intern, schnell); Retry: **max 1** sofort.
- `expires_at`-TTL (M15): **500 ms** (Server-seitig gesetzt).
- Nach Timeout/unklar: **nicht senden**; Order als `FAILED`/`REJECTED` mit `CONTROL_STATE_UNKNOWN/STALE`.
- **Kein Auto-Resume**, kein "dann schicken wir eben ohne Check".
### Warum REDUCE/CLOSE/CANCEL durchlaufen
- Ziel des Control ist **Risiko-Management**. Ein HALT darf Risiko niemals einfrieren — im Gegenteil,
muss das System bei Problemen schneller in der Lage sein, Positionen abzubauen.
- `REDUCE`/`CLOSE`/`CANCEL` senken/eliminieren Risiko → **immer erlaubt**, unabhängig von M15-Status.
---
## 12. Offene Punkte für die Nutzer-Freigabe
1. **API-Vertrag erweitern** (`state_version`, `expires_at`, `ttl_seconds`, `audit_id`) in M15 (additiv).
2. **M09-Check-Position:** zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen.
3. **TTL-Wert** (`expires_at`-Fenster) festschreiben (Vorschlag 500 ms).
4. **Kategorisierung** der Order-Aktion in M09 (OPEN/INCREASE/REDUCE/CLOSE/CANCEL) ableiten — M09 muss
aus dem TRADE_APPROVED die Aktion bestimmen (Vorschlag: über `direction`/Kanten-Art der Entscheidung).
5. **M03M06-Klassifikation** (Market-Data-Pipeline): ob Einzel-Ausfall dieser Module wirklich HALTED
auslösen soll, oder nur deren Daten-Staleness — zu finalisieren, beeinflusst WEN oft HALTED greift.
6. **SPOF-Akzeptanz:** M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei.
7. M15 weiterhin **nicht freigeben**, M16 **nicht beginnen** bis Nutzer-Entscheid.
---
*Dies ist ein Design-Vorschlag. Es wurde kein Code geändert. M15 nicht freigegeben. M16 nicht begonnen.*