From a0297469575b0e756d89a787fb0f811b94c31a9e Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Fri, 21 Aug 2026 04:15:06 +0000 Subject: [PATCH] Modul-19-Broker-Reconciliation: finalisiert (E2E 13/13, Infrastruktur-Szenarien gruen, Regression M03->M09->M18 gruen, Doku; Freigabe ausstehend) --- modul-19-broker-reconciliation.md | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 modul-19-broker-reconciliation.md diff --git a/modul-19-broker-reconciliation.md b/modul-19-broker-reconciliation.md new file mode 100644 index 0000000..1bb96a0 --- /dev/null +++ b/modul-19-broker-reconciliation.md @@ -0,0 +1,112 @@ +# Modul-19: Broker-Reconciliation + +**Status: FINALISIERT — Freigabe ausstehend (21.08.2026)** · Container `Modul-19-Broker-Reconciliation` · Port `55019` (nur intern/expose) + +## Zweck +Modul-19 gleicht die **interne Trading-Realität** (M10 Trade-Journal, M18 Position-Manager) gegen die +**Broker-/Execution-Realität** ab. Es erkennt Diskrepanzen (fehlende/überschüssige Positionen, Mengen-, +Richtungs- oder Statusabweichungen) und erzeugt daraus **Audit + Alarm**, ohne selbst zu handeln. + +**Kritische Grenze:** M19 ist ein **reiner Reconciliation-/Monitoring-Dienst**, KEIN Trade-Ausführer. +- ✅ M19 erzeugt **NIE** `OPEN` / `INCREASE` (keine automatische Reparatur, kein Ordering). +- ✅ M19 schreibt ausschließlich in eigene Audit-Tabellen (`reconciliation_*`), liest fremde Tabellen **read-only**. +- ❌ Keine KI/ML. Keine direkte Broker-Anbindung in V1. +- **FAIL-CLOSED:** unklare/unerreichbare Broker-Realität → STALE/UNKNOWN/Alarm + Audit, nie blind handeln. +- Keine Zombie-Konsumenten; Restart/Reconnect erzeugt keine Doppel-Events (idempotent). + +## V1-Grenze: „Brokerrealität“ = rekonstruiert aus M09 `execution_order` +> ⚠️ **WICHTIG (V1-Akzeptanz):** In V1 gibt es **keinen echten Broker-Adapter**. Die „Brokerrealität" +> wird **rekonstruiert** aus der von M09 persistierten Tabelle `execution_order` (PaperBroker hält sein +> Orderbuch nur In-Memory; die einzig nachweisbare Quelle ist die DB-Persistenz). +> +> Das ist für V1 **akzeptabel**, aber **NICHT gleichwertig mit echter Broker-Reconciliation**. +> Echte Broker-Reconciliation (Abruf von Live-Positionen/Fills beim Broker/MT5) folgt **nach M19** über +> einen **Demo-Broker-Adapter** (Plugin am `BrokerStateProvider`-Interface). + +## Architektur / Datenfluss +``` +M09 Execution (execution_order, status SUBMITTED/PARTIALLY_FILLED/FILLED) ──┐ Broker-Realität (V1) +M10 Trade-Journal (trade_journal, status=OPEN) ─────────────────────────────┤ interne Realität +M18 Position-Manager (position_state, position_key=trade_id) ───────────────┘ + │ + ▼ +Modul-19-Broker-Reconciliation (Port 55019, NUR intern) + ├── BrokerStateProvider (Interface, V1=paper) → Broker-Positionen (netto je Symbol) + ├── InternalStateReader → interne offene Positionen (Journal + M18 state) + ├── ReconciliationEngine → Klassifikation je Symbol/Position + ├── Storage → reconciliation_run / reconciliation_result / reconciliation_event (idempotent) + └── Publisher → RabbitMQ exchange "reconciliation" routing "reconciliation.alert" (additiv, M15 kann später konsumieren) +``` + +**App-Struktur:** +- `broker/base.py` — `BrokerStateProvider`-Interface (V1=paper, später live/demo-Adapter) +- `broker/paper.py` — V1-Paper-Provider: rekonstruiert Broker-Positionen aus `execution_order` (read-only) +- `broker/registry.py` — Provider-Registry +- `core/internal_state.py` — liest interne offene Positionen (M10 + M18) +- `core/engine.py` — Reconciliation-Klassifikation (deterministisch) +- `core/service.py` — Loop-Orchestrator, publiziert kritische Alarme, **Trade-Actions hart deaktiviert** +- `storage/storage.py` — Persistenz (eigene Tabellen), Thread-Local-DB (psycopg2 nicht thread-safe) +- `publisher/publisher.py` — Alarm-Publish auf RMQ (additive Schnittstelle) +- `api/main.py` — REST-API (`/health`, `/health/ready`, `/reconcile`, `/reconcile/runs`, `/reconcile/results`), Port 55019 intern + +**Eigene Tabellen (`migrations/001_reconciliation.sql`):** +- `reconciliation_run` — Lauf (run_id PK, status RUNNING/COMPLETED/FAILED, totals) +- `reconciliation_result` — Ergebnis je Position/Symbol (idempotent: UNIQUE(run_id, position_key)) +- `reconciliation_event` — append-only Alarm-/Audit-Events + +## Klassifikationen (deterministisch, V1) +| Klassifikation | Bedingung | Konsequenz | +|----------------|-----------|------------| +| `MATCH` | intern offen == Broker offen (Symbol, Richtung, Menge) | kein Event | +| `QUANTITY_MISMATCH` | Broker-Menge ≠ interne Menge | CRITICAL-Event | +| `DIRECTION_MISMATCH` | Broker-Richtung ≠ interne Richtung | CRITICAL-Event | +| `MISSING_BROKER` | intern offen, Broker hat Position | CRITICAL-Event | +| `MISSING_INTERNAL` | Broker offen, intern **kein** Datensatz | CRITICAL-Event | +| `STATUS_MISMATCH` | intern kennt Position, aber CLOSED/abweichend vs. Broker offen | CRITICAL-Event | +| `PARTIAL_FILL` | Broker PARTIALLY_FILLED → Menge berücksichtigt | (Konsistenz) | +| `STALE` / `UNKNOWN` | Broker-Realität unklar/unerreichbar | **FAIL-CLOSED**, kein Trade, Alarm + Audit | + +**Hinweis (Design):** `MISSING_INTERNAL` = Broker offen, intern hat gar keinen Datensatz. `STATUS_MISMATCH` = +intern kennt die Position, aber sie ist CLOSED/abweichend während der Broker offen ist (vorher fälschlich +MISSING_INTERNAL). Beides sind kritische Abweichungen, die als `reconciliation.alert`-CRITICAL gemeldet werden. + +## Safety (hart) +- `allow_trade_actions` ist **hart `False`** (Konfiguration + API erzwingen; `/reconcile` lehnt ab wenn True). +- M19 erzeugt **niemals** `OPEN`/`INCREASE`, keine automatische Reparatur, überschreibt Brokerrealität nie. +- Bei `UNKNOWN`/`STALE`/`UNREACHABLE` → FAIL-CLOSED + Audit + Alarm, nie blind senden. +- M19 ist reiner **Publisher** auf RMQ (kein Consumer → keine Zombie-Konsumenten). + +## Deployment +- Port `55019` **nur intern** (`expose`, keine Host-Port-Bindings — wie M18). +- Netz `trading-modules_trading-modules`, `restart: unless-stopped`. +- RMQ-Host `Modul-02-RabbitMQ`, vhost `trading`. PG-Host `Modul-01-PostgreSQL`. +- Reconciliation-Loop `loop_interval_seconds=60`, `loop_enabled=True`. + +## Tests +- **Unit:** `tests/test_engine.py` — 11/11 grün (Klassifikation, Provider, Safety `test_never_allows_trade`). +- **E2E:** `tests/e2e_m19_full.py` — 13/13 grün (MATCH, QUANTITY, DIRECTION, MISSING_BROKER/INTERNAL, + PARTIAL_FILL, STATUS_MISMATCH, Duplikat/Idempotenz, parallel/race-safe, Health/Readiness, Event-Dedup, + Audit-Trail). +- **Infrastruktur (live, VPS):** Broker-DB unreachable → FAIL-CLOSED (broker_reachable:false, + trade_actions_allowed:false); Restart sauber; RMQ-Down → kein Crash (gehaltene pika-Warnungen); + RMQ-Reconnect ohne Zombies; M09-Ausfall → M19 arbeitet weiter (Quelle ist PG `execution_order`); Health+Readiness 200; Port nur intern. + +## Regression M03→M09→M18 +Alle Kettenglieder healthy nach M19-Deployment (M03/M05/M08/M09/M10/M15/M18 `/health` = ok). +M19 schreibt ausschließlich in `reconciliation_*`; fremde Tabellen (`execution_order`, `trade_journal`, +`position_state`) bleiben unberührt. Keine M19-Konsumenten an der Trading-Kette; M19-Exchange `reconciliation` +ist additiv (M15 kann künftig konsumieren). Keine Produktivpositionen — alle Datensätze sind M18E2E2/M19E2E2-Testreste. + +## Bugs / Lessons Learned +1. **RMQ-vhost `/` vs `trading`:** M18 nutzt vhost `trading`; M19 musste auf vhost `trading` korrigiert werden + (anfänglich `/` → Fehler). Nach Fix sauber. +2. **Envs-Präfix `M19_`:** pydantic-settings `env_prefix="M19_"` — Envs wie `M19_PG_HOST`, `M19_RABBITMQ_HOST` + verwenden, NICHT bare `PG_HOST` (Falle beim Isolationstest). Produktiv-Container trägt noch bare `PG_*`/ + `RABBITMQ_*`-Envs (M18-Muster) = tote Envs; M19 greift auf korrekte pydantic-Defaults + (`Modul-01-PostgreSQL`, `Modul-02-RabbitMQ`, vhost `trading`) zurück. Für saubere Konsistenz künftig Envs + auf `M19_`-Präfix umstellen. +3. **psycopg2 nicht thread-safe:** Storage nutzt `threading.local()` pro Thread (Fix aus M18 übernommen). +4. **`MISSING_INTERNAL` vs `STATUS_MISMATCH`:** sauber getrennt (s. o.), sonst fälschliche Klassifikation. +5. **Broker-Realität rekonstruiert:** V1 akzeptiert `execution_order` als Broker-Realität; echter Adapter folgt + nach M19 (Demo-Broker-Adapter), siehe oben. +6. **Keine Host-Port-Bindings** (nur `expose`) — wie M18, keine öffentliche API.