18 KiB
| type | _organized |
|---|---|
| Trading-Modul-System | true |
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). |
| 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):
- Final-Check unmittelbar vor
submit_order— minimales Zeitfenster (ms) zwischen Check und Send. - TTL auf der HTTP-Antwort:
expires_at(z. B.now + 500 ms). Ist die Antwort beim Senden abgelaufen (Fenster überschritten), NICHT senden → Order nachFAILED/REJECTEDmitCONTROL_STATE_STALEmarkieren. state_version(Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check gesehene Version. Beim Broker-Submit trägt die Order diecontrol_version; die Audit-Kette verbindet genau die Version, die gegolten hat, mit dem Versand.- Bestätigtes Einverständnis: Der Check liefert
trading_status=ENABLEDundstate_versionundexpires_at. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen. - 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):
{
"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
UNKNOWNbehandeln. - 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).
{
"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 alsUNKNOWN/veraltet→ OPEN/INCREASE blockiert (auch wenn letztes EventENABLEDwar). - 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=ENABLEDund neuerstate_versionoder ein HTTP-Final-Check mitENABLEDvorliegt, 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 hatcontrol_expires_at/control_ttl: TTL-Horizontcontrol_audit_id: vom Final-Check zugeordnetcontrol_check_source:final_http(und optionalcache)
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/REJECTEDmitCONTROL_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/CANCELsenken/eliminieren Risiko → immer erlaubt, unabhängig von M15-Status.
12. Offene Punkte für die Nutzer-Freigabe
- API-Vertrag erweitern (
state_version,expires_at,ttl_seconds,audit_id) in M15 (additiv). - M09-Check-Position: zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen.
- TTL-Wert (
expires_at-Fenster) festschreiben (Vorschlag 500 ms). - 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). - M03–M06-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.
- SPOF-Akzeptanz: M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei.
- 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.