C3I FINAL INDIVIDUAL DECISIONS: - modul-12: CONTROLLED ROOT<->CANONICAL RECONCILIATION + PAIRED MIGRATION. Canonical auf aktuellen Root-Stand (inkl. Phase 10e Intrabar/Gap Execution) gebracht, Root unveraendert, atomare Unit, derived_from=Root-ID. - modul-18/19: ROOT-ONLY MIGRATION (kein Canonical-Partner). - vps.md: NICHT migriert/löschen/archivieren, DEFERRED_STUB_CLEANUP dokumentiert. Body byte-genau. IDs stabil. Knowledge schema v1. (Red Queen)
10 KiB
| knowledge_schema | id | type | role | representation | state |
|---|---|---|---|---|---|
| 1 | object/3f834840-867d-9e9f-c7d2-8f1caf1550a4 | arch | module | source | current |
Modul-19: Broker-Reconciliation
Status: FREIGEGEBEN (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 ausexecution_order(read-only)broker/registry.py— Provider-Registrycore/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 deaktiviertstorage/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.
Interne Positionswahrheit (Kernregel, 21.08.2026)
⚠️ Kernregel (User, 21.08.2026): Eine externe Broker-Position darf nur
MATCHsein, wenn eine echte interne OPEN-Position existiert. Interne Positionswahrheit =position_state(M18).trade_journal/execution_orderallein dürfen KEINE interne offene Position vortäuschen.
Vorher (Bug): get_internal_open() las trade_journal (M10) als Basis. Ein trade_journal-Eintrag mit
status='OPEN' galt als intern offen, auch ohne position_state (M18). Folge: IG-Position existierte,
intern nur execution_order/trade_journal → M19 klassifizierte fälschlich MATCH mit qty 0 (Fake-MATCH).
Nachher (Fix): get_internal_open() liest primär position_state (M18) als Treiber. Ein
trade_journal-Eintrag ohne position_state zählt nicht als intern offen → Broker-Position wird
MISSING_INTERNAL statt Fake-MATCH. CLOSED position_state-Einträge werden mitgeliefert, damit die Engine
STATUS_MISMATCH erkennen kann.
Erwartete Klassifikation (Kernregel):
| Situation | Klassifikation |
|---|---|
IG OPEN + kein position_state |
MISSING_INTERNAL |
IG OPEN + position_state OPEN + gleiche qty/direction |
MATCH |
IG OPEN + position_state CLOSED |
STATUS_MISMATCH |
| IG OPEN + andere qty | QUANTITY_MISMATCH |
| IG OPEN + andere direction | DIRECTION_MISMATCH |
| Intern OPEN + IG flat | MISSING_BROKER |
Wichtig: qty=0 darf niemals zu MATCH führen (Engine: internal_open erfordert qty>0).
Safety (hart)
allow_trade_actionsist hartFalse(Konfiguration + API erzwingen;/reconcilelehnt 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
55019nur intern (expose, keine Host-Port-Bindings — wie M18). - Netz
trading-modules_trading-modules,restart: unless-stopped. - RMQ-Host
Modul-02-RabbitMQ, vhosttrading. PG-HostModul-01-PostgreSQL. - Reconciliation-Loop
loop_interval_seconds=60,loop_enabled=True.
Tests
- Unit:
tests/test_engine.py— 16/16 grün (Klassifikation, Provider, Safetytest_never_allows_trade, Kernregelqty=0nie MATCH, journal-ohne-position_state → MISSING_INTERNAL). - E2E:
tests/e2e_m19_full.py— 14/14 grün (MATCH, QUANTITY, DIRECTION, MISSING_BROKER/INTERNAL, PARTIAL_FILL, STATUS_MISMATCH, Duplikat/Idempotenz, parallel/race-safe, Health/Readiness, Event-Dedup, Audit-Trail, S13 Fake-MATCH → MISSING_INTERNAL). - 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
- RMQ-vhost
/vstrading: M18 nutzt vhosttrading; M19 musste auf vhosttradingkorrigiert werden (anfänglich/→ Fehler). Nach Fix sauber. - Envs-Präfix
M19_: pydantic-settingsenv_prefix="M19_"— Envs wieM19_PG_HOST,M19_RABBITMQ_HOSTverwenden, NICHT barePG_HOST(Falle beim Isolationstest). Produktiv-Container trägt noch barePG_*/RABBITMQ_*-Envs (M18-Muster) = tote Envs; M19 greift auf korrekte pydantic-Defaults (Modul-01-PostgreSQL,Modul-02-RabbitMQ, vhosttrading) zurück. Für saubere Konsistenz künftig Envs aufM19_-Präfix umstellen. - psycopg2 nicht thread-safe: Storage nutzt
threading.local()pro Thread (Fix aus M18 übernommen). MISSING_INTERNALvsSTATUS_MISMATCH: sauber getrennt (s. o.), sonst fälschliche Klassifikation.- Broker-Realität rekonstruiert: V1 akzeptiert
execution_orderals Broker-Realität; echter Adapter folgt nach M19 (Demo-Broker-Adapter), siehe oben. - Keine Host-Port-Bindings (nur
expose) — wie M18, keine öffentliche API. - Fake-MATCH (Root Cause, 21.08.2026):
get_internal_open()lastrade_journal(M10) als Basis. Eintrade_journal-Eintrag mitstatus='OPEN'galt als intern offen, auch ohneposition_state(M18). Folge: IG-Position existierte, intern nurexecution_order/trade_journal→ M19 klassifizierte fälschlichMATCHmit qty 0. Fix: Interne Positionswahrheit primär ausposition_state(M18);trade_journal/execution_orderallein vortäuschen keine interne offene Position →MISSING_INTERNAL.qty=0nie MATCH.