Modul-18-Position-Manager: formal abgeschlossen + FREIGEGEBEN (E2E 47/47, Regression M03->M09 gruen, Thread-Local-DB-Fix, Doku)

This commit is contained in:
Rain Ocampo 2026-08-21 03:26:10 +00:00
parent 8f77230d6c
commit dd0d7675c9
2 changed files with 138 additions and 4 deletions

View file

@ -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-0317=`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) |
| 1617 | … | ⬜ Platzhalter (alpine) | 5501655017 |
| 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 0315
vollständig implementiert und freigegeben). Modul-16 ff. folgen.
Details: siehe `modul-03-market-data.md``modul-18-position-manager.md` (Module 0318
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.

View file

@ -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 015:
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.
```