From dd0d7675c92e369bbd2d9ccb8c704c56cb96e2a4 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Fri, 21 Aug 2026 03:26:10 +0000 Subject: [PATCH] Modul-18-Position-Manager: formal abgeschlossen + FREIGEGEBEN (E2E 47/47, Regression M03->M09 gruen, Thread-Local-DB-Fix, Doku) --- README.md | 14 ++-- modul-18-position-manager.md | 128 +++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 modul-18-position-manager.md diff --git a/README.md b/README.md index adc6cd6..9b4a743 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + ## Architektur-Überblick -- **17 Module** (`Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`), orchestriert über `docker-compose.yml` unter `/opt/trading-modules/`. +- **18 Module** (`Modul-01-PostgreSQL` … `Modul-18-Position-Manager`), orchestriert über `docker-compose.yml` unter `/opt/trading-modules/`. - **Netzwerk:** `trading-modules` (bridge). Kommunikation ausschließlich über **Docker-interne Service-Hostnamen** (keine festen IPs). - **Feste Host-Ports:** Modul-01=`55432`, Modul-02=`55672`+`15672`, Modul-03–17=`55003`–`55017`. - **Zugangsdaten:** nur über Environment Variables / Docker-Secrets (Defaults in Compose mit `${VAR:-default}`). @@ -43,10 +43,12 @@ Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + | 13 | Optimization | ✅ FREIGEGEBEN (optimization:0.1.0) | 55013 (intern) | | 14 | Notification | ✅ FREIGEGEBEN (notification:0.1.0) | 55014 (intern) | | 15 | Monitoring-Control | ✅ FREIGEGEBEN (monitoring-control:0.1.0) | 55015 (intern) | -| 16–17 | … | ⬜ Platzhalter (alpine) | 55016–55017 | +| 16 | Paperclip | ✅ FREIGEGEBEN (paperclip:0.1.0) | 55016 (intern) | +| 17 | Hermes-Agent | ✅ FREIGEGEBEN (hermes-agent:0.1.0) | 55017 (intern) | +| 18 | Position-Manager | ✅ FREIGEGEBEN (position-manager:0.1.0) | 55018 (intern) | -Details: siehe `modul-03-market-data.md` … `modul-15-monitoring-control.md` (Module 03–15 -vollständig implementiert und freigegeben). Modul-16 ff. folgen. +Details: siehe `modul-03-market-data.md` … `modul-18-position-manager.md` (Module 03–18 +vollständig implementiert und freigegeben). --- @@ -95,3 +97,7 @@ Grund: README-Modultabelle aktualisiert — Modul-14 Notification als FREIGEGEBE Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: README-Modultabelle aktualisiert — Modul-15 Monitoring-Control als FREIGEGEBEN eingetragen; Details-Link auf modul-15-monitoring-control.md erweitert. + +Geändert von: Rain Ocampo +Datum: 21.08.2026 +Grund: README-Modultabelle aktualisiert — Modul-16 Paperclip, Modul-17 Hermes-Agent, Modul-18 Position-Manager als FREIGEGEBEN eingetragen; Details-Link auf modul-18-position-manager.md erweitert. diff --git a/modul-18-position-manager.md b/modul-18-position-manager.md new file mode 100644 index 0000000..39e572b --- /dev/null +++ b/modul-18-position-manager.md @@ -0,0 +1,128 @@ +# Modul-18: Position-Manager + +**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-18-Position-Manager` · Port `55018` (nur intern/expose) + +## Zweck +Modul-18 verwaltet **bereits offene Positionen** deterministisch: Es beobachtet offene Trades und gibt auf Basis fester +Exit-Regeln (Stop-Loss, Take-Profit, Break-even, Trailing, Time-Exit) gezielte **Exit-Aktionen** an Modul-09. + +**Kritische Grenze:** M18 ist ein **reiner Exit-/Positions-Manager**, KEIN Signal-/Einstiegsmodul. +- ✅ M18 darf NUR `REDUCE` / `CLOSE` / `CANCEL` an M09 geben. **NIE `OPEN` / `INCREASE`.** +- ❌ Kein eigenmächtiges Nachkaufen/Pyramiding. Keine KI/ML. Keine direkte Broker-Anbindung. +- Jede Order-Aktion läuft ausschließlich über **M09 Execution** via RabbitMQ `trade.approved`. +- **FAIL-CLOSED:** stale/unbekannter Zustand → nie blind senden; Restart/Reconnect erzeugt keine Doppelaktion; keine Zombies. + +## Architektur / Datenfluss +``` +Modul-03-Market-Data (Preise) ──┐ + ▼ +Modul-10-Trade-Journal (OPEN) ──► Modul-18-Position-Manager (Port 55018, NUR intern) +Modul-15 (Trading-State, readonly) │ + ├── Engine-Loop (deterministische Exit-Regeln) + ├── Publisher → RabbitMQ market.portfolio / trade.approved → M09 + └── Consumer ← RabbitMQ market.execution / order.filled (Reconciliation) + │ + ▼ + Modul-09-Execution-Service (PaperBroker: broker=paper) → order.filled +``` + +**App-Struktur (`app/`):** +- `core/engine.py` — Exit-Regel-Engine (Regel-Priorität deterministisch) +- `core/orchestrator.py` — Hydrate-State, wendet Regeln an, idempotent +- `publisher/publisher.py` — publiziert Aktionen an M09 (`trade.approved`, je Aktion eine eindeutige `action_id`) +- `consumer/consumer.py` — konsumiert M09-Execution-Events (`order.filled` etc.) für Reconciliation +- `pricing/client.py` — liest Preise von M03, inkl. **Stale-Guard** +- `providers/journal.py` — Quelle offener Positionen (Trade-Journal, `status=OPEN`) +- `storage/storage.py` — Persistenz (`position_state`/`position_action`/`position_event`) +- `api/main.py` — REST-API (`/health`, `/readiness`), Port 55018 nur intern + +**Eigene Tabellen (`migrations/001_position.sql`):** +- `position_state` — aktueller verwalteter Zustand je Position +- `position_action` — jede an M09 gegebene Order-Aktion (REDUCE/CLOSE/CANCEL) +- `position_event` — append-only Audit-Event-Historie + +## Exit-Regeln (deterministisch) +| Regel | Trigger | Aktion | +|-------|---------|--------| +| Stop-Loss | Preis ≤ stop_loss | CLOSE | +| Take-Profit | Preis ≥ target | CLOSE | +| Break-even | Preis günstig, SL auf Entry gesetzt | CLOSE bei BE-SL | +| Trailing | Preis weiter günstig (best_price_seen) | CLOSE bei Trailing-SL | +| Time-Exit | max. Haltedauer erreicht | CLOSE | +| Partial-Fill / Duplikat | idempotente Aktion | exakt 1 Order, keine Doppelaktion | +| HALTED (M15) | Trading-State HALTED | CLOSE weiterhin erlaubt (Exit) | +| stale Preis | Kerze älter als `stale_price_max_age_seconds` (3600s) | **FAIL-CLOSED: keine Aktion** | + +Regeln laufen mit fester Priorität; jede Exit-Aktion wird als eindeutige `position_action` (action_id) persistiert. + +## M09-CLOSE-Pfad (Reduktion → Execution) +``` +M18 position_action (action_id, CLOSE) + → Publisher → RabbitMQ exchange "market.portfolio" routing "trade.approved" + → M09 Execution (control/client bypass für REDUCE/CLOSE/CANCEL) → PaperBroker (broker=paper) + → execution_order (source_portfolio_decision_id = M18 action_id, status FILLED) + → RabbitMQ "market.execution" routing "order.filled" + → M18-Consumer → Reconciliation (position_state → CLOSED, qty=0, Audit-Event) +``` +**Verifikation:** Für jede Exit-Aktion existiert **exakt 1** execution_order mit `broker=paper`, `status=FILLED`, +`source_portfolio_decision_id` = M18 `action_id`. M09 speichert die M18-`action_id` als +`source_portfolio_decision_id` (top-level + Payload). Keine Doppelaktionen je action_id (HAVING count>1 = 0). + +## Reconciliation über order.filled +M18 konsumiert M09-Execution-Events (`order.filled`, `order.partially_filled`, `order.rejected`, `order.failed`) +und gleicht den eigenen `position_state` ab: FILLED → Position als `CLOSED` markieren, `quantity=0`, Audit-Event +`ORDER_EXECUTED`/`ORDER_FILLED`. Damit sind M10 (Journal) und M18 (Manager) konsistent, auch wenn M10 CLOSE +nicht automatisch verknüpft. + +## Thread-Local-DB-Fix (Root Cause „connection pointer is NULL") +**Symptom:** Reconciliation scheiterte sporadisch mit `connection pointer is NULL`. +**Root Cause:** `PositionStorage._conn` war eine **geteilte Singleton-Verbindung** zwischen Engine-Loop-Thread und +ExecutionConsumer-Thread. psycopg2 ist **nicht thread-safe**: ein Thread schloss die Verbindung +(`resolve_position_key`/`has_pending_exit`), der andere Thread lief auf totem Cursor. +**Fix:** **Thread-Local-Verbindung** — `self._tl = threading.local()`; jeder Thread erhält eine eigene +psycopg2-Verbindung; kein Cross-Thread-`.close()`. `close()`/`rollback()` (ensure_schema) auf lokale Verbindung umgestellt. +**Verifikation:** Unit-Tests **13/13 grün**, E2E **47/47 PASS**. + +## Testdesign-Lessons (E2E) +1. **action_id statt Journal-pd_id:** M09 speichert die M18-`action_id` als `source_portfolio_decision_id` — + Verifikation muss `wait_execution_for_action(action_id)` nutzen, nicht Journal-`pd_id`. +2. **Audit-Eventname:** Consumer schreibt `ORDER_EXECUTED` (bzw. `ORDER_FILLED`); Test akzeptiert beide. +3. **Szenario 10 RA/RB:** Verifikation über `action_id`, nicht `pd_id`. +4. **Stale-Szenario:** `stale_price_max_age_seconds=3600`; eine 5-min-Kerze ist NICHT stale. M03 akzeptiert + bis 30 Tage alte Kerzen, persistiert injiziertes `ts` aber nicht (vergibt frisches ts). Fix: 2h-alte Kerze + (>1h < 30Tage) injizieren + **pro-Lauf eindeutiges Symbol** (M03-Speicher hat keinen Delete-Endpoint). + +## Safety (User-Vorgabe, kritisch) +- **NUR REDUCE/CLOSE/CANCEL** — niemals OPEN/INCREASE. Kein Nachkaufen/Pyramiding. +- Idempotent + race-safe + **FAIL-CLOSED**. Restart/Reconnect keine Doppelaktion. Keine Zombies. +- Broker/Execution unklar → nie blind erneut senden. +- Vollständiger Audit-Trail (`position_event` append-only). +- Port 55018 nur Docker-intern (`expose`), kein öffentliches Mapping. Netz `trading-modules`. +- Keine Secrets in Logs/API/DB. + +## Tests & E2E-Verifikation (VPS, 21.08.2026) +- **Unit-Tests** `tests/test_engine.py`: **13/13 grün** (Thread-Local-Fix). +- **E2E** `tests/e2e_m18_full2.py`: **47/47 PASS** (EXIT=0), deterministischer Lauf 0–15: + HOLD · SL→CLOSE · TP→CLOSE · Break-even · Trailing · Time-Exit · Partial-Fill · bereits CLOSED · Duplikat · + parallel/race-safe (RA/RB) · M09 down · M15 HALTED · **stale Preis (FAIL-CLOSED)** · Restart · Reconciliation order.filled. +- Für jede Exit-Aktion: exakt 1 Execution je action_id, `broker=paper`, `status=FILLED`, keine Doppelaktionen, + keine offenen Zombie-Testpositionen. +- **Regression M03→M09:** M18 erzeugt **NIE OPEN/INCREASE** (nur CLOSE). M09 execution_order: + nur `CLOSE`, alle `FILLED`, `broker=paper`, keine Doppel-Order je action_id. +- **Health/Security:** `/health` + `/readiness` 200; Port 55018 nur intern; keine Secrets in + Logs/API/DB; keine Tracebacks seit finalem Lauf; Consumer/RMQ-Reconnect sauber. + +## Compose / Betrieb +- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), RabbitMQ `Modul-02-RabbitMQ`. +- Port 55018 nur `expose` (Docker-intern), kein Host-Mapping. `restart: unless-stopped`. +- Keine Secrets im Compose-Env. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 21.08.2026 +Grund: Modul-18-Position-Manager formal abgeschlossen + FREIGEGEBEN. E2E 47/47 PASS, Regression M03→M09 gruen + (nur CLOSE, nie OPEN/INCREASE), Health/Security gruen (Port 55018 intern, keine Secrets/Tracebacks). + Thread-Local-DB-Fix (psycopg2 "connection pointer is NULL" geteiltes Singleton → Thread-Local). Doku angelegt. +```