diff --git a/notes/trading/second-brain/Stop_Loss_Fix_2026-08-18.md b/notes/trading/second-brain/Stop_Loss_Fix_2026-08-18.md new file mode 100644 index 0000000..9bf86ba --- /dev/null +++ b/notes/trading/second-brain/Stop_Loss_Fix_2026-08-18.md @@ -0,0 +1,42 @@ +# Echter Stop-Loss + Risiko-Sizing — Fix 18.08.2026 + +## Problem +Der Stop-Loss war bisher **nur ein lokaler Wert** in `trades.json`. Der Monitor prüfte +**stündlich** und verkaufte dann per Market-Order. Bei volatilen Pennystocks (MXL, ACRS, +ULCC, XRX) rutschte der Kurs in der Zwischenzeit deutlich unter den SL → Verluste von +−7 bis −10 % statt der konfigurierten −8 %. Realisierte Rendite der Periode: **−24,33 €**. + +Zusätzlich war das Sizing fix auf 50 €/Position → bei weitem SL (8 %) ein Verlust von +~6–7 € (≈12 % des Positionsbudgets), statt der 2 %-Regel. + +## Fix A — Echter Stop-Loss bei T212 +- Neuer Endpunkt: `POST /api/v0/equity/orders/stop` (negative Menge = Sell-Stop) +- `place_stop()` + `cancel_stop()` in `t212_resilience.py` +- `ensure_stop()` in `t212_monitor.py`: platziert/aktualisiert den echten Stop bei + Fill, Breakeven (+3 %), Trailing (+5 %) und bei jedem Monitor-Lauf +- **Idempotent:** speichert `stop_order_id` + `stop_order_price` in `trades.json` → + kein Cancel+Neu-Churn bei jedem Lauf +- **Rate-Limit-Pacing:** 1 Stop-Request / 2s (T212-Limit) +- **Konservativ bei 429:** leere Live-Stop-Liste → nichts unternehmen (kein Churn) + +## Fix B — Risiko-basiertes Sizing +- `calc_risk()` nutzt jetzt `risk_per_trade_pct (2 %)` × Gesamtkapital statt fix 50 € +- Verlust pro Trade in € gedeckelt, egal wie weit der SL steht +- "Trades atmen lassen" (weiter SL) → Position wird automatisch kleiner + +## Verifiziert (live gegen T212-API, 18.08.2026) +4 echte Stop-Orders aktiv, keine Duplikate, zweiter Monitor-Lauf = 0 neue Orders: + +| Position | Stop | Order-ID | +|----------|------|----------| +| LFST | 11,95 € | 55909740706 | +| TRGP | 271,78 € | 55909740714 | +| WEAV | 6,72 € | 55909740725 | +| UMAC | 28,55 € | 55909740772 | + +## Backups +- `t212_monitor.py.bak_*` (mehrere Stände) +- `t212_resilience.py.bak_*` + +## Status +✅ Aktiv — Daemon liest Dateien bei jedem Lauf frisch, kein Neustart nötig. diff --git a/notes/trading/second-brain/journal/2026-07-20_T212_Recap.md b/notes/trading/second-brain/journal/2026-07-20_T212_Recap.md new file mode 100644 index 0000000..727132a --- /dev/null +++ b/notes/trading/second-brain/journal/2026-07-20_T212_Recap.md @@ -0,0 +1,216 @@ +--- +date: 2026-07-20 +summary: T212 Tages-Recap +--- + +# 📊 Tages-Recap — 20.07.2026 + +**📊 T212 Tages-Recap — 20.07.2026** + Monday | 22:30 MEZ + Letzter Scan: 21:00 | 0 Kandidaten + +**⚙️ Risiko-Modell** + 2%-Risiko/Trade | 90%-Kapazität | Max 2/Sektor + +**📊 Offene Positionen: 6** + Gefüllt: 6 | Pending: 0 + Sektoren: Commercial Services(1), Non-Energy Minerals(1), Industrials(1), Electronic Technology(1), Energy Minerals(1), Consumer Durables(1) + CCO | Entry 2.41€ | ~+0.0% (+0.00€) + GGB | Entry 4.68€ | ~+0.0% (+0.00€) + AAL | Entry 15.10€ | ~+0.0% (+0.00€) + ASX | Entry 38.75€ | ~+0.0% (+0.00€) + XOM | Entry 148.00€ | ~+0.0% (+0.00€) + F | Entry 14.20€ | ~+0.0% (+0.00€) + +**✅ Heute geschlossen: 103** + CVE | +0.0% | Grund: Portfolio-Ghost + OLPX | -0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + CVX | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + ABEV | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + TEVA | -0.5% | Grund: Auto-Management + OXY | -1.7% | Grund: Auto-Management + TSEM | +17.1% | Grund: Auto-Management + OVV | -0.6% | Grund: Auto-Management + FCX | +7.3% | Grund: Auto-Management + GOOG | -2.0% | Grund: Auto-Management + VG1 | -7.5% | Grund: Auto-Management + VKp | -3.3% | Grund: Auto-Management + GOOG | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + TWEKAa | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + BLDP | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + VACQ | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + GLW | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + VKp | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + VKp | -8.7% | Grund: SL-Hit (bereits von T212 geschlossen) + APAM | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + NVDd | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + FFARMa | -0.4% | Grund: Auto-Management + S92d | +0.0% | Grund: Auto-Management + TWEKAa | +0.0% | Grund: Auto-Management (bereits ausgebucht) + SGLd | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + VRT | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + OLPX | +0.0% | Grund: Duplicate cleanup + APLD | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + WMB | +0.0% | Grund: Manuell auf T212 geschlossen (nicht mehr im Portfolio) + CLSK | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + BE | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte) + RWEd | +0.7% | Grund: Auto-Management + IOVA | +7.9% | Grund: Auto-Management + REPe | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + ALLO | -100.0% | Grund: Auto-Management + FFARMa | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + GLW | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + MXL | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + FNB | -0.5% | Grund: Auto-Management + KEY | +4.3% | Grund: Portfolio-Sync (nicht mehr in T212) + GLW | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + CSCO | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + MO | +4.9% | Grund: Auto-Management + VPG | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + LRCX | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + IOVA | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + KLAC | -4.7% | Grund: Auto-Management + KO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + GOOG | -6.4% | Grund: Auto-Management + LRCX | -100.0% | Grund: Auto-Management + AMZN | -100.0% | Grund: Auto-Management + ANET | +2.6% | Grund: Auto-Management + GOOG | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + AFL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + RY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + SHW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + MCD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + MAIN | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + GOOD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CNQ | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + SLB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + PG | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + IBM | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + AGNC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + NUE | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + RGLD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ECL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + MA | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CSCO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + BNS | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + BLK | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + MSFT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + BMY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + KMB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + SYY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + TROW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + LOW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + PNR | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + GD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + O | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + DUK | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + EMR | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ADP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + GWW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + PPG | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + LTC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ROP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ITW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + JNJ | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + JPM | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CVX | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + C | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + BMO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CF | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CAH | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + PEP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + TD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + CB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + AAPL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ADC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + WMT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + ABT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed) + MO | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212) + +**🗑️ Heute gecancelt: 16** + TWEKAa | 24h nicht gefüllt — Cancel + VRT | 24h nicht gefüllt — Cancel + CCO | End-of-Day Cleanup + OLPX | 24h nicht gefüllt — Cancel + ABEV | End-of-Day Cleanup + IOVA | 24h nicht gefüllt — Cancel + ALLO | 24h nicht gefüllt — Cancel + TSM | 69h nicht gefüllt — Cancel + BMY | 69h nicht gefüllt — Cancel + RTPY | Order existiert nicht mehr bei T212 (404) + KEY | 61h nicht gefüllt — Cancel + RTPY | End-of-Day Cleanup + LION | 26h nicht gefüllt — Cancel + BCRX | 24h nicht gefüllt — Cancel + CX | 24h nicht gefüllt — Cancel + BMY | 24h nicht gefüllt — Cancel + +💰 **Investiert: +115.98€** + Unreal. P&L: +0.00€ + +**🧠 Analyse:** + ✅ 7 Gewinn-Trades (⌀ 6.4%) + 🔴 15 Verlust-Trades (⌀ -22.4%) + - OLPX_US_EQ: -0.0% (Portfolio-Sync (nicht mehr in T212)) + - TEVA_US_EQ: -0.5% (Auto-Management) + - OXY_US_EQ: -1.7% (Auto-Management) + - OVV_US_EQ: -0.6% (Auto-Management) + - GOOG_US_EQ: -2.0% (Auto-Management) + - VG1_US_EQ: -7.5% (Auto-Management) + - VKp_EQ: -3.3% (Auto-Management) + - VKp_EQ: -8.7% (SL-Hit (bereits von T212 geschlossen)) + - FFARMa_EQ: -0.4% (Auto-Management) + - ALLO_US_EQ: -100.0% (Auto-Management) + - FNB_US_EQ: -0.5% (Auto-Management) + - KLAC_US_EQ: -4.7% (Auto-Management) + - GOOG_US_EQ: -6.4% (Auto-Management) + - LRCX_US_EQ: -100.0% (Auto-Management) + - AMZN_US_EQ: -100.0% (Auto-Management) + ⏰ 16 Orders gecancelt (unfilled/warning) + +**💡 Learning für morgen:** + ❌ OLPX: -0.0% wegen Portfolio-Sync (nicht mehr in T212) + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ TEVA: -0.5% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ OXY: -1.7% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ✅ TSEM: +17.1% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + ❌ OVV: -0.6% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ✅ FCX: +7.3% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + ❌ GOOG: -2.0% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ VG1: -7.5% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ VKp: -3.3% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ VKp: -8.7% wegen SL-Hit (bereits von T212 geschlossen) + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ FFARMa: -0.4% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ✅ RWEd: +0.7% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + ✅ IOVA: +7.9% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + ❌ ALLO: -100.0% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ FNB: -0.5% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ✅ MO: +4.9% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + ❌ KLAC: -4.7% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ GOOG: -6.4% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ LRCX: -100.0% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ❌ AMZN: -100.0% wegen Auto-Management + → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger? + ✅ ANET: +2.6% (gut erkannt) + → SMA50-Kipp-Erkennung war richtig + +--- +_Automatisch erstellt von Alice_ diff --git a/notes/trading/second-brain/t212-logs/2026-07-21_log.md b/notes/trading/second-brain/t212-logs/2026-07-21_log.md new file mode 100644 index 0000000..64bf8ef --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-21_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 21.07.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ +- AAL_US_EQ | +0.0% | SL: 13.89€ +- XOM_US_EQ | +0.0% | SL: 136.16€ +- F_US_EQ | +0.0% | SL: 13.06€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-22_log.md b/notes/trading/second-brain/t212-logs/2026-07-22_log.md new file mode 100644 index 0000000..a50ccd6 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-22_log.md @@ -0,0 +1,8 @@ +# T212 Logs — 22.07.2026 23:45 UTC +## Offen (3) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ +- F_US_EQ | +0.0% | SL: 13.06€ +- UNHd_EQ | +0.0% | SL: 352.73€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-23_log.md b/notes/trading/second-brain/t212-logs/2026-07-23_log.md new file mode 100644 index 0000000..41dda32 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-23_log.md @@ -0,0 +1,6 @@ +# T212 Logs — 23.07.2026 23:45 UTC +## Offen (1) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-24_log.md b/notes/trading/second-brain/t212-logs/2026-07-24_log.md new file mode 100644 index 0000000..1c775b8 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-24_log.md @@ -0,0 +1,6 @@ +# T212 Logs — 24.07.2026 23:45 UTC +## Offen (1) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-27_log.md b/notes/trading/second-brain/t212-logs/2026-07-27_log.md new file mode 100644 index 0000000..ee7b9cc --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-27_log.md @@ -0,0 +1,8 @@ +# T212 Logs — 27.07.2026 23:45 UTC +## Offen (3) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ +- ACRS_US_EQ | +0.0% | SL: 5.04€ +- BRLI_US_EQ | +0.0% | SL: 9.92€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-28_log.md b/notes/trading/second-brain/t212-logs/2026-07-28_log.md new file mode 100644 index 0000000..ee449c1 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-28_log.md @@ -0,0 +1,7 @@ +# T212 Logs — 28.07.2026 23:45 UTC +## Offen (2) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ +- ACRS_US_EQ | +0.0% | SL: 5.04€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-29_log.md b/notes/trading/second-brain/t212-logs/2026-07-29_log.md new file mode 100644 index 0000000..c6ef217 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-29_log.md @@ -0,0 +1,7 @@ +# T212 Logs — 29.07.2026 23:45 UTC +## Offen (2) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.31€ +- ACRS_US_EQ | +0.0% | SL: 5.04€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-30_log.md b/notes/trading/second-brain/t212-logs/2026-07-30_log.md new file mode 100644 index 0000000..82f6f7f --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-30_log.md @@ -0,0 +1,8 @@ +# T212 Logs — 30.07.2026 23:45 UTC +## Offen (3) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- XRX_US_EQ | +0.0% | SL: 3.10€ +- FORM_US_EQ | +0.0% | SL: 95.31€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-07-31_log.md b/notes/trading/second-brain/t212-logs/2026-07-31_log.md new file mode 100644 index 0000000..5c814c0 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-07-31_log.md @@ -0,0 +1,7 @@ +# T212 Logs — 31.07.2026 23:45 UTC +## Offen (2) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- AMCX_US_EQ | +0.0% | SL: 10.34€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-03_log.md b/notes/trading/second-brain/t212-logs/2026-08-03_log.md new file mode 100644 index 0000000..2b0f2e8 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-03_log.md @@ -0,0 +1,10 @@ +# T212 Logs — 03.08.2026 23:45 UTC +## Offen (5) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- AXTI_US_EQ | +0.0% | SL: 55.60€ +- ULCC_US_EQ | +0.0% | SL: 6.96€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-04_log.md b/notes/trading/second-brain/t212-logs/2026-08-04_log.md new file mode 100644 index 0000000..4a68c6a --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-04_log.md @@ -0,0 +1,10 @@ +# T212 Logs — 04.08.2026 23:45 UTC +## Offen (5) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- AXTI_US_EQ | +0.0% | SL: 55.60€ +- ULCC_US_EQ | +0.0% | SL: 6.96€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-05_log.md b/notes/trading/second-brain/t212-logs/2026-08-05_log.md new file mode 100644 index 0000000..c8a7825 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-05_log.md @@ -0,0 +1,10 @@ +# T212 Logs — 05.08.2026 23:45 UTC +## Offen (5) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- ULCC_US_EQ | +0.0% | SL: 6.96€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- AXTI_US_EQ | +0.0% | SL: 53.82€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-06_log.md b/notes/trading/second-brain/t212-logs/2026-08-06_log.md new file mode 100644 index 0000000..ae85a3b --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-06_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 06.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- ULCC_US_EQ | +0.0% | SL: 6.96€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-07_log.md b/notes/trading/second-brain/t212-logs/2026-08-07_log.md new file mode 100644 index 0000000..85678f5 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-07_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 07.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- ULCC_US_EQ | +0.0% | SL: 6.96€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-10_log.md b/notes/trading/second-brain/t212-logs/2026-08-10_log.md new file mode 100644 index 0000000..0b8d4ba --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-10_log.md @@ -0,0 +1,10 @@ +# T212 Logs — 10.08.2026 23:45 UTC +## Offen (5) | Unrealisiert: +0.0% +- GGB_US_EQ | +0.0% | SL: 4.86€ +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- NESR_US_EQ | +0.0% | SL: 32.84€ +- LFST_US_EQ | +0.0% | SL: 10.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-11_log.md b/notes/trading/second-brain/t212-logs/2026-08-11_log.md new file mode 100644 index 0000000..61e180a --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-11_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 11.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- NESR_US_EQ | +0.0% | SL: 32.84€ +- LFST_US_EQ | +0.0% | SL: 10.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-12_log.md b/notes/trading/second-brain/t212-logs/2026-08-12_log.md new file mode 100644 index 0000000..9a04238 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-12_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 12.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- NESR_US_EQ | +0.0% | SL: 32.84€ +- LFST_US_EQ | +0.0% | SL: 10.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-13_log.md b/notes/trading/second-brain/t212-logs/2026-08-13_log.md new file mode 100644 index 0000000..e222636 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-13_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 13.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- NESR_US_EQ | +0.0% | SL: 32.84€ +- LFST_US_EQ | +0.0% | SL: 10.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-14_log.md b/notes/trading/second-brain/t212-logs/2026-08-14_log.md new file mode 100644 index 0000000..11a9cf2 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-14_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 14.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- XRX_US_EQ | +0.0% | SL: 2.98€ +- ATKR_US_EQ | +0.0% | SL: 85.99€ +- NESR_US_EQ | +0.0% | SL: 32.84€ +- LFST_US_EQ | +0.0% | SL: 10.99€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-17_log.md b/notes/trading/second-brain/t212-logs/2026-08-17_log.md new file mode 100644 index 0000000..15b631c --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-17_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 17.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- NESR_US_EQ | +0.0% | SL: 35.69€ +- LFST_US_EQ | +0.0% | SL: 11.95€ +- AXTI_US_EQ | +0.0% | SL: 90.35€ +- SMTC_US_EQ | +0.0% | SL: 142.33€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-18_log.md b/notes/trading/second-brain/t212-logs/2026-08-18_log.md new file mode 100644 index 0000000..a845bd9 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-18_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 18.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- LFST_US_EQ | +0.0% | SL: 11.95€ +- TRGP_US_EQ | +0.0% | SL: 271.78€ +- WEAV_US_EQ | +0.0% | SL: 6.72€ +- UMAC_US_EQ | +0.0% | SL: 28.55€ + +--- +*Auto-Log* diff --git a/notes/trading/second-brain/t212-logs/2026-08-19_log.md b/notes/trading/second-brain/t212-logs/2026-08-19_log.md new file mode 100644 index 0000000..7079fb2 --- /dev/null +++ b/notes/trading/second-brain/t212-logs/2026-08-19_log.md @@ -0,0 +1,9 @@ +# T212 Logs — 19.08.2026 23:45 UTC +## Offen (4) | Unrealisiert: +0.0% +- LFST_US_EQ | +0.0% | SL: 11.95€ +- TRGP_US_EQ | +0.0% | SL: 271.78€ +- WEAV_US_EQ | +0.0% | SL: 6.72€ +- UMAC_US_EQ | +0.0% | SL: 28.55€ + +--- +*Auto-Log* diff --git a/notes/trading/system-docs/README.md b/notes/trading/system-docs/README.md new file mode 100644 index 0000000..adc6cd6 --- /dev/null +++ b/notes/trading/system-docs/README.md @@ -0,0 +1,97 @@ +# Trading-System — Modulare Architektur + +Zentrales, modulares automatisiertes Trading-System, betrieben auf einem Hostinger-VPS (`187.124.31.123`). + +**Repository-Inhalt:** Live-Dokumentation der Infrastruktur, der Module und des aktuellen Betriebszustands. +Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + Zeitstempel (`DD.MM.YYYY HH:MM`) + Grund. + +--- + +## Architektur-Überblick + +- **17 Module** (`Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`), 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}`). +- **Kernprinzip:** bestehende Container/Volumes/Daten nie löschen, keine unnötigen öffentlichen Ports öffnen, Ist-Zustand vor Änderung prüfen. + +### Multi-Agent-Setup (Resident-Evil-Theme) +| Agent | Rolle | Port | +|-------|-------|------| +| **Alice** (OpenClaw) | Primary | 55163 | +| **Matt Addison** (OpenClaw) | Backup | 54524 | +| **Rain Ocampo** (Hermes) | Technischer Support | 32776 (UI) | + +--- + +## Module + +| Modul | Name | Status | Port | +|-------|------|--------|------| +| 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 | +| 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 | +| 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) | +| 04 | Market-Regime | ✅ healthy (market-regime:1.0.0) | 55004 (intern) | +| 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) | +| 06 | Signal-Ranking | ✅ FREIGEGEBEN (signal-ranking:0.1.0) | 55006 (intern) | +| 07 | Risk-Manager | ✅ FREIGEGEBEN (risk-manager:0.1.0) | 55007 (intern) | +| 08 | Portfolio-Manager | ✅ FREIGEGEBEN (portfolio-manager:0.1.0) | 55008 (intern) | +| 09 | Execution-Service | ✅ FREIGEGEBEN (execution-service:0.1.0) | 55009 (intern) | +| 10 | Trade-Journal | ✅ FREIGEGEBEN (trade-journal:0.1.0) | 55010 (intern) | +| 11 | Analytics | ✅ FREIGEGEBEN (analytics-service:0.1.0) | 55011 (intern) | +| 12 | Backtesting | ✅ FREIGEGEBEN (backtesting:0.1.2) | 55012 (intern) | +| 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 | + +Details: siehe `modul-03-market-data.md` … `modul-15-monitoring-control.md` (Module 03–15 +vollständig implementiert und freigegeben). Modul-16 ff. folgen. + +--- + +## Notation / Audit-Trail + +Jede Änderung wird mit folgender Zeile dokumentiert: + +``` +Geändert von: [Alice | Rain Ocampo] +Datum: DD.MM.YYYY HH:MM +Grund: +``` + +- **Alice** = Änderungen durch OpenClaw +- **Rain Ocampo** = Änderungen durch Hermes + +--- + +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Initiale Repository-Struktur und Modul-03-Dokumentation angelegt. +``` + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README aktualisiert — Modul-04 und Modul-05 als healthy/freigegeben eingetragen (Ports intern), Details-Link ergänzt. + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README-Modultabelle aktualisiert — Module 06–12 als freigegeben eingetragen; Modul-12-Backtesting als FREIGEGEBEN markiert. + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README-Modultabelle aktualisiert — Modul-12 auf backtesting:0.1.2 (V1.1), Modul-13 Optimization als E2E-VERIFIZIERT eingetragen. + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README-Modultabelle aktualisiert — Modul-13 Optimization als FREIGEGEBEN markiert (Freigabe durch Nutzer 20.08.2026). + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README-Modultabelle aktualisiert — Modul-14 Notification als FREIGEGEBEN eingetragen; Details-Link auf modul-14-notification.md erweitert. + +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. diff --git a/notes/trading/system-docs/infrastructure-handbook.md b/notes/trading/system-docs/infrastructure-handbook.md new file mode 100644 index 0000000..1c8a6ab --- /dev/null +++ b/notes/trading/system-docs/infrastructure-handbook.md @@ -0,0 +1,51 @@ +# Infrastruktur & Betriebs-Handbuch — Trading-System VPS + +> Stand: 20.08.2026 · VPS: `187.124.31.123` + +## Multi-Agent-Architektur (Resident-Evil-Theme) +| Agent | Plattform | Rolle | Port | +|-------|-----------|-------|------| +| **Alice** | OpenClaw (primary) | Hauptagent, Protagonistin | 55163 | +| **Matt Addison** | OpenClaw (backup) | Backup/Support | 54524 | +| **Rain Ocampo** | Hermes (support) | Technischer Support | 32776 (UI) | + +Notation in Notion/Forgejo bei JEDER Änderung: +``` +Geändert von: [Alice | Rain Ocampo] +Datum: DD.MM.YYYY HH:MM +Grund: … +``` + +## Container-Verwaltung (via SSH) +```bash +ssh root@187.124.31.123 +docker compose -f /opt/trading-modules/docker-compose.yml ps +docker compose -f /opt/trading-modules/docker-compose.yml up -d +docker compose -f /opt/trading-modules/docker-compose.yml build +docker compose -f /opt/trading-modules/docker-compose.yml down # nur bei explizitem Wunsch +``` + +## Trading-Module (17 Container) +- Compose: `/opt/trading-modules/docker-compose.yml` +- Container-Namen exakt: `Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent` +- Netzwerk: `trading-modules` +- Modul-01 = PostgreSQL 16-alpine (Host 55432) +- Modul-02 = RabbitMQ 3-management (Host 55672/15672, vhost `trading`) +- Modul-03 = Market-Data (FastAPI, Port 55003 intern, **nicht öffentlich**) +- Modul-04–17 = alpine-Platzhalter (bis ausgebaut) + +## Wichtige Hinweise +- **Coolify überschreibt Container-Configs bei Neustart.** Config-Änderungen an OpenClaw ausschließlich über Config-Dateien (nicht UI). +- **Hot-Reload** OpenClaw: `kill -HUP 1` im Container. +- **Ollama** braucht `OLLAMA_API_KEY`; interner Hostname `ollama-nb6d-ollama-1:11434`. +- VPS-Authentifizierung: **nur Public-Key-Auth**. + +## Deleted Resources (VPS-Aufräumung 20.08.2026) +Folgendes wurde entfernt (~25 GB freigegeben): Chronos/Kronos (kronos-api, chronos-service, chronos-tradelog), trading-bridge, llm-trade-manager, deepseek-bot, MetaTrader-5, t212-pilot, mehrere redundante Hermes-Container/Volumes/Netzwerke. **Bleibt:** n8n, forgejo, paperclip, Tolaria, PostgreSQL, RabbitMQ, Ollama, Traefik, Coolify-Suite. + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Infrastruktur-Handbuch für das Trading-System dokumentiert. +``` diff --git a/notes/trading/system-docs/modul-03-market-data.md b/notes/trading/system-docs/modul-03-market-data.md new file mode 100644 index 0000000..0449ecb --- /dev/null +++ b/notes/trading/system-docs/modul-03-market-data.md @@ -0,0 +1,116 @@ +# Modul-03-Market-Data — Betriebsdokumentation + +> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy) + +## Zweck +Zentrale Marktdatenquelle des Trading-Systems. Pipeline: +`Marktdaten → Validierung → Normalisierung → PostgreSQL → RabbitMQ-Event` +Keine Strategie, keine Orders, keine KI — nur zuverlässige Marktdaten. + +## Container +| Attribut | Wert | +|----------|------| +| Name | `Modul-03-Market-Data` | +| Image | `market-data:0.1.0` (lokal gebaut) | +| Port | **55003** — **NUR intern** (`expose`, nicht öffentlich) | +| Netzwerk | `trading-modules` (bridge) | +| Build-Context | `/opt/trading-modules/modul03-market-data/` | +| Restart | `unless-stopped` | +| Healthcheck | ✅ `healthy` | + +## Deployment +```bash +cd /opt/trading-modules +docker compose build modul-03-market-data +docker compose up -d modul-03-market-data +``` + +## Environment Variables (Compose) +| Variable | Wert (Default) | Zweck | +|----------|---------------|-------| +| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename | +| `PG_PORT` | `5432` | intern (Host: 55432) | +| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults | +| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename | +| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) | +| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** | +| `DATA_PROVIDER` | `noop` | `noop` / `demo` / später Broker | +| `LOG_LEVEL` | `INFO` | Strukturiertes Logging | + +Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + `${VAR:-default}`. + +## Datenbank (Modul-01-PostgreSQL) +Tabelle `public.ohlcv`: +```sql +symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT, +ts TIMESTAMPTZ, open/high/low/close DOUBLE PRECISION, volume DOUBLE PRECISION, +created_at TIMESTAMPTZ DEFAULT now(), id BIGSERIAL PRIMARY KEY +``` + +### Finale Constraints & Indizes (Stand 20.08.2026) +| Index | Typ | +|-------|-----| +| `uq_ohlcv_provider_symbol_tf_ts` | **UNIQUE** `(provider, symbol, timeframe, ts)` | +| `ohlcv_pkey` | UNIQUE `(id)` | +| `idx_ohlcv_symbol_tf` | `(symbol, timeframe)` | +| `idx_ohlcv_symbol_tf_ts` | `(symbol, timeframe, ts DESC)` | +| `idx_ohlcv_provider_symbol_tf_ts` | `(provider, symbol, timeframe, ts DESC)` | + +Der Unique-Index inkludiert den **Provider** — langfristig werden mehrere Provider/Broker unterstützt +(gleiche Symbol+Timeframe+Timestamp können von verschiedenen Quellen kommen). + +**Migrationen:** `/app/migrations/001_ohlcv.sql` + `002_unique_provider.sql` +(idempotent, löschen nichts; automatisch via `ensure_schema()`/glob angewendet). + +## RabbitMQ (Modul-02) +- **Exchange:** `market.data` (topic, durable) +- **vhost:** `trading` (wichtig!) +- **Routing-Keys:** + +| Routing-Key | Event-Typ | Wann | +|-------------|-----------|------| +| `market.data.ready` | `MARKET_DATA_READY` | **Batch/Import** — EIN Event pro Batch, `candle_count` + `batch:true` | +| `market.data.candle.closed` | `MARKET_CANDLE_CLOSED` | **Live** — pro abgeschlossener Kerze EIN Event, OHLCV im payload | + +- Eventschema v1.0: `event_id, event_type, event_version, timestamp, symbol, asset_class, timeframe, provider` + payload. + +## Interne API +| Endpoint | Zweck | +|----------|-------| +| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body | +| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ down) | +| `POST /ingest` | Kerzen einspeisen (JSON: `{"candles":[...]}`) | +| `GET /prices/{symbol}` | Letzte Kurse | +| `GET /history/{symbol}?timeframe=` | Historische OHLCV | + +## Provider-Adapter +`app/providers/providers.py`: +- `DataProvider` (ABC) — abstrakte Schnittstelle `fetch_ohlcv()`, `health()` +- `NoopProvider` — keine Datenquelle konfiguriert (Default) +- `DemoProvider` — synthetische OHLCV-Daten für Tests +- Neue Broker = neue Klasse, umschalten via `DATA_PROVIDER` env → keine harte Anbieter-Kopplung + +## Validierung (`app/validation/validator.py`) +- Timestamp gültig (UTC, nicht Zukunft, nicht zu alt/stale) +- OHLC-Werte > 0 +- High ≥ Low, High ≥ Open/Close, Low ≤ Open/Close +- Duplikat-Erkennung (Storage + DB-Unique-Index) + +## End-to-End-Test (20.08.2026, nach Provider-Constraint-Upgrade) ✅ +- **Batch-Pfad:** 4 GOOG-Kerzen → saved:4, **GENAU EIN** `MARKET_DATA_READY` (routing `market.data.ready`, candle_count:4). 3 NVDA → saved:3, EIN Event. +- **Live-Pfad:** 1 AMZN-Candle → `MARKET_CANDLE_CLOSED` (routing `market.data.candle.closed`, OHLCV im payload). +- **Provider-Duplikat:** identische NVDA-Kerze erneut → `duplicate`, kein Insert, kein Event. +- **Port-Sicherheit:** 55003 von außen (`http://187.124.31.123:55003/health`) → **nicht erreichbar** ✅ +- Datenbestand final: AAPL 5, GOOG 4, NVDA 3, EURUSD 3, TSLA 4, MSFT 1, AMZN 1. + +## Offene Punkte +- Echter Broker-/Datenprovider-Adapter (Interface bereit, noop/demo Defaults) +- Zusätzliche Daten (Bid/Ask/Spread, Ticks, Fundamentaldaten) — vorbereitet +- Consumer für `market.data.ready` / `market.data.candle.closed` (Modul-04 ff.) + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-03-Dokumentation angelegt (Provider-Constraint, Event-Trennung, Port-Nicht-Exposition). +``` diff --git a/notes/trading/system-docs/modul-04-market-regime.md b/notes/trading/system-docs/modul-04-market-regime.md new file mode 100644 index 0000000..3e3ef21 --- /dev/null +++ b/notes/trading/system-docs/modul-04-market-regime.md @@ -0,0 +1,152 @@ +# Modul-04-Market-Regime — Betriebsdokumentation + +> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden) + +## Zweck +Erster **Consumer** der Market-Data-Events von Modul-03. Pipeline: +`MARKET_DATA_READY / MARKET_CANDLE_CLOSED (Modul-03) → Regime-Berechnung → PostgreSQL (market_regime) → MARKET_REGIME_READY` +**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML.** + +## Container +| Attribut | Wert | +|----------|------| +| Name | `Modul-04-Market-Regime` | +| Image | `market-regime:1.0.0` (lokal gebaut) | +| Port | **55004** — **NUR intern** (`expose`, nicht öffentlich) | +| Netzwerk | `trading-modules` (bridge) | +| Build-Context | `/opt/trading-modules/modul04-market-regime/` | +| Restart | `unless-stopped` | +| Healthcheck | ✅ `healthy` | + +## Deployment +```bash +cd /opt/trading-modules +docker compose build modul-04-market-regime +docker compose up -d --no-deps --force-recreate modul-04-market-regime +``` + +## Environment Variables (Compose) +| Variable | Wert (Default) | Zweck | +|----------|---------------|-------| +| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename | +| `PG_PORT` | `5432` | intern (Host: 55432) | +| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults | +| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename | +| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) | +| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** | +| `LOG_LEVEL` | `INFO` | Strukturiertes Logging | + +Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults. + +## Architektur +``` +Modul-03 ──market.data.ready / market.data.candle.closed──▶ RegimeConsumer + │ (bindet beide Routing-Keys) + ▼ + RegimeEngine (deterministisch) + EMA / ADX / ATR / Slope / Preisstruktur + │ + ┌───────────┴───────────┐ + ▼ ▼ + market_regime (PG) MARKET_REGIME_READY + (16 Spalten) → market.regime.ready +``` + +- **Consumer** (`app/consumer/consumer.py`): bindet `market.data.ready` + `market.data.candle.closed`; durable Queue `market-regime.input`; manuelles Ack; Reconnect mit Backoff; **schließt alte Verbindung beim Reconnect** (verhindert Consumer-Leak/Nachrichtenverlust). +- **Engine** (`app/regime/engine.py`): deterministisch, ohne KI. +- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index. +- **Publisher** (`app/publisher/publisher.py`): publiziert `MARKET_REGIME_READY` auf `market.regime` (Routing `market.regime.ready`). +- **History-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`). + +## Regime-Engine (`app/regime/engine.py`) +Deterministische Regel-Engine (Version `1.0.0`), 7 Regime: +`TREND_UP, TREND_DOWN, RANGE, HIGH_VOLATILITY, LOW_VOLATILITY, TRANSITION, UNKNOWN` + +**Indikatoren & Metriken:** +| Indikator | Fenster/Param | Zweck | +|-----------|---------------|-------| +| EMA fast/slow | 10 / 30 | Trendrichtung (EMA-Flanken-Differenz) | +| ADX | 14 | Trendstärke (≥20 = echter Trend) | +| ATR | 14 | Volatilität (absolut + Ratio + Perzentil) | +| Slope | 20 | normierte Steigung der Close-Linie | +| Preisstruktur | — | higher_highs / lower_lows / range | + +**Zentrale Schwellenwerte** (`app/config.py`, env-overridable): +| Parameter | Default | Bedeutung | +|-----------|---------|-----------| +| `min_candles_required` | 30 | UNKNOWN, wenn weniger Daten | +| `regime_lookback` | 60 | max. Kerzen für Berechnung | +| `trend_min_ema_gap` | 0.02 | |EMA_fast-EMA_slow|/close ≥ → Trend | +| `adx_trend_threshold` | 20.0 | ADX ≥ → echter Trend | +| `slope_up/down_threshold` | 0.05 / -0.05 | normierte Steigung | +| `range_atr_ratio` | 0.02 | ATR/close darunter = Range | +| `atr_high_vol_multiplier` | 1.5 | ATR jetzt > hist_mean × → HIGH_VOL | +| `atr_low_vol_multiplier` | 0.6 | ATR jetzt < hist_mean × → LOW_VOL | +| `high_vol_atr_ratio` | 0.03 | ATR/close ≥ → starke Vol | +| `low_vol_atr_ratio` | 0.008 | ATR/close ≤ → geringe Vol | +| `slope_threshold` | 0.01 | |Slope| darunter = seitwärts | +| `transition_min_events` | 3 | Events für TRANSITION | + +## Datenbank (Modul-01-PostgreSQL) +Tabelle `public.market_regime` (16 Spalten, eigene Tabelle — bestehende unangetastet): +```sql +symbol TEXT, asset_class TEXT, timeframe TEXT, provider TEXT, +regime TEXT, confidence INTEGER (0-100), +trend_strength DOUBLE PRECISION, volatility_state TEXT, +timestamp TIMESTAMPTZ, indicators_json JSONB, +candles_used INTEGER, version TEXT, +source_event_id TEXT, correlation_id TEXT, +data_ts TIMESTAMPTZ, data_ts_end TIMESTAMPTZ +``` +- **Unique (partiell):** `uq_market_regime_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz. +- Migration: `migrations/001_market_regime.sql` (idempotent, löscht nichts). + +## RabbitMQ (Modul-02) +| Exchange | Typ | Routing | Event | +|----------|-----|---------|-------| +| `market.data` | topic | `market.data.ready` (eingang) | MARKET_DATA_READY | +| `market.data` | topic | `market.data.candle.closed` (eingang) | MARKET_CANDLE_CLOSED | +| `market.regime` | topic | `market.regime.ready` (**ausgang**) | MARKET_REGIME_READY | + +## Interne API +| Endpoint | Zweck | +|----------|-------| +| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body | +| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) | +| `GET /regime/{symbol}` | Regime-Einträge abfragen | +| `GET /regime/latest/{symbol}` | Letztes Regime eines Symbols | + +## End-to-End-Test (20.08.2026, final, nach Rebuild) ✅ +Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 Consumer → RegimeEngine → `market_regime` → `MARKET_REGIME_READY` auf `market.regime.ready`. + +| Fall | Regime | Conf | candles | version | Event | DB | +|------|--------|------|---------|---------|-------|----| +| M4TREND_UP (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ | +| M4TREND_DN (40) | TREND_DOWN | 100 | 40 | 1.0.0 | genau 1 | ✅ | +| M4RANGE (40) | LOW_VOLATILITY (Range) | 60 | 40 | 1.0.0 | genau 1 | ✅ | +| M4HIGHVOL (40) | HIGH_VOLATILITY | 75 | 40 | 1.0.0 | genau 1 | ✅ | +| M4UNKNOWN (5) | UNKNOWN | 20 | 5 | 1.0.0 | genau 1 | ✅ | +| M4IDEMPOT (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ | + +**Idempotenz:** dasselbe Quell-Event (`source_event_id`) erneut → **kein zweiter Datensatz**, kein Doppel-Event. ✅ +**Logs:** keine Errors/Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅ + +## Bugs behoben während E2E (20.08.2026) +| Bug | Fix | +|-----|-----| +| `can't adapt type 'dict'` (JSONB) | `json.dumps(ind.model_dump(mode="json"))` | +| `tuple index out of range` (16/15) | `version` in INSERT-VALUES ergänzt | +| `ON CONFLICT` + partieller Index Fehler | `WHERE source_event_id IS NOT NULL` in Klausel | +| `model_dump(default=...)` TypeError | `default`-Kwarg entfernt (`model_dump(mode="json")`) | +| Consumer-Verbindungs-Leak | `conn.close()` bei Reconnect → kein Message-Leak | + +## Offene Punkte +- Consumer-Downstream für `market.regime.ready` (Modul-05+) +- Bestätigte TRANSITION-Detektion mit echten Folgedaten + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-04-Dokumentation angelegt (Regime-Engine, market_regime-Schema, Events, E2E freigegeben). +``` diff --git a/notes/trading/system-docs/modul-05-strategy-engine.md b/notes/trading/system-docs/modul-05-strategy-engine.md new file mode 100644 index 0000000..55a9eb2 --- /dev/null +++ b/notes/trading/system-docs/modul-05-strategy-engine.md @@ -0,0 +1,154 @@ +# Modul-05-Strategy-Engine — Betriebsdokumentation + +> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden) + +## Zweck +**Strategie-Engine** — konsumiert `MARKET_REGIME_READY` (Modul-04) + OHLCV (Modul-03), berechnet deterministische Handelssignale und persistiert sie. +Pipeline: `MARKET_REGIME_READY (Modul-04) → Strategie-Engine (trend_pullback_v1) → PostgreSQL (strategy_signal) → SIGNAL_DETECTED` +**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML, ohne Ranking, ohne Risk Management, ohne Order-Ausführung.** + +## Container +| Attribut | Wert | +|----------|------| +| Name | `Modul-05-Strategy-Engine` | +| Image | `strategy-engine:0.1.0` (lokal gebaut) | +| Port | **55005** — **NUR intern** (`expose`, nicht öffentlich) | +| Netzwerk | `trading-modules` (bridge) | +| Build-Context | `/opt/trading-modules/modul05-strategy-engine/` | +| Restart | `unless-stopped` | +| Healthcheck | ✅ `healthy` | + +## Deployment +```bash +cd /opt/trading-modules +docker compose build modul-05-strategy-engine +docker compose up -d --no-deps --force-recreate modul-05-strategy-engine +``` + +## Environment Variables (Compose) +| Variable | Wert (Default) | Zweck | +|----------|---------------|-------| +| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename | +| `PG_PORT` | `5432` | intern | +| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults | +| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename | +| `RABBITMQ_PORT` | `5672` | intern | +| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** | +| `LOG_LEVEL` | `INFO` | Strukturiertes Logging | + +Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults. + +## Architektur +``` +Modul-04 ──market.regime.ready──▶ StrategyConsumer (strategy.input) + │ + ▼ + OHLCV (Modul-03, intern 55003/history/{symbol}) + │ + ▼ + StrategyEngine (deterministisch) + trend_pullback_v1 (LONG/SHORT) + │ + ┌───────────┴───────────┐ + ▼ ▼ + strategy_signal (PG) SIGNAL_DETECTED + (eigene Tabelle) → market.signals / strategy.signal.detected +``` + +- **Consumer** (`app/consumer/consumer.py`): bindet Exchange `market.regime`, Routing `market.regime.ready`; durable Queue `strategy.input`; manuelles Ack erst nach erfolgreicher Verarbeitung; Reconnect mit Backoff + `conn.close()`; **`queue_delete` beim Start** (entfernt verwaiste/Zombie-Consumer). +- **MarketData-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`); normalisiert Feld `ts` → `timestamp`. +- **Engine/Register** (`app/engine.py`): modular — Strategien via `.name/.version/.evaluate()` registriert. +- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index. +- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen). +- **Service** (`app/core/service.py`): Pipeline Event → OHLCV → Strategie → speichern + publizieren; robuste Payload-Extraktion. + +## Strategie V1 — `trend_pullback_v1` (`app/strategies/trend_pullback_v1.py`) +Deterministische Pullback-Strategie. **Nur abgeschlossene Candles, kein Lookahead-Bias.** + +**LONG-Bedingungen (Regime TREND_UP):** +1. Close > steigender SMA200 (Trendfilter) +2. Pullback: Close < EMA20 (Zug zurück in den Trend) +3. Bestätigung: Close > prev Close ODER Break prev High +4. Entry = Close; Stop unter Swing-Low; Target = 2R (R:R = 2.0) + +**SHORT-Bedingungen (Regime TREND_DOWN):** spiegelbildlich +1. Close < fallender SMA200 +2. Pullback: Close > EMA20 +3. Bestätigung: Close < prev Close ODER Break prev Low +4. Stop über Swing-High; Target = 2R + +**Mathematik (vom E2E verifiziert):** +- **LONG:** `Target = Entry + 2 × (Entry - Stop)`; `R:R = 2.0`; Stop < Entry +- **SHORT:** `Target = Entry - 2 × (Stop - Entry)`; `R:R = 2.0`; Stop > Entry + +**Zentrale Schwellenwerte** (`app/config.py`, env-overridable): +| Parameter | Default | Bedeutung | +|-----------|---------|-----------| +| `lookback` | 300 | max. Kerzen zur Berechnung | +| `min_candles_required` | 220 | UNKNOWN, wenn < 220 (SMA200 braucht 200) | +| `sma_period` | 200 | Trendfilter SMA | +| `ema_period` | 20 | Pullback-EMA | +| `risk_reward` | 2.0 | Target-Multiplikator (2R) | +| `swing_lookback` | 10 | Swing-Low/High-Fenster für Stop | + +Kein gültiges Setup → **kein Event** publiziert. + +## Datenbank (Modul-01-PostgreSQL) +Eigene Tabelle `public.strategy_signal` (bestehende unangetastet): +```sql +signal_id UUID, source_event_id TEXT, correlation_id TEXT, +timestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, +timeframe TEXT, strategy_name TEXT, strategy_version TEXT, +direction TEXT (LONG/SHORT), regime TEXT, +entry DOUBLE PRECISION, stop_loss DOUBLE PRECISION, target DOUBLE PRECISION, +risk_reward DOUBLE PRECISION, setup_metrics JSONB, trigger_reason TEXT +``` +- **Unique (partiell):** `uq_strategy_signal_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz. +- Migration: `migrations/001_strategy_signal.sql` (idempotent, löscht nichts). + +## RabbitMQ (Modul-02) +| Exchange | Typ | Routing | Event | +|----------|-----|---------|-------| +| `market.regime` | topic | `market.regime.ready` (eingang) | MARKET_REGIME_READY | +| `market.signals` | topic | `strategy.signal.detected` (ausgang) | SIGNAL_DETECTED | + +## Interne API +| Endpoint | Zweck | +|----------|-------| +| `GET /health` | Liveness (200) + Komponentenstatus im Body | +| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) | + +## End-to-End-Test (20.08.2026, final, nach Publisher-Fixes) ✅ +Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 Consumer → `trend_pullback_v1` → `strategy_signal` → `SIGNAL_DETECTED`. + +| Fall | Signal | Entry | Stop | Target | R:R | DB | Event | +|------|--------|-------|------|--------|-----|----|-------| +| M5LONG (TREND_UP) | **LONG** | 164.20 | 163.4764 | 165.6473 | **2.00** | ✅ 1 | ✅ 1 | +| M5SHORT (TREND_DOWN) | **SHORT** | 135.80 | 136.4964 | 134.4073 | **2.00** | ✅ 1 | ✅ 1 | +| M5NOPULL (kein Pullback) | keins | — | — | — | — | ✅ 0 | ✅ 0 | +| M5RANGE (Range) | keins | — | — | — | — | ✅ 0 | ✅ 0 | + +**Mathematik verifiziert:** LONG `Target=Entry+2×(Entry-Stop)` = 164.20 + 2×0.72364 = **165.6473** ✓; SHORT `Target=Entry−2×(Stop−Entry)` = 135.80 − 2×0.69636 = **134.4073** ✓. +**Idempotenz:** identisches `source_event_id` erneut → **kein zweiter DB-Eintrag, kein Doppel-Event** (count=1). ✅ +**RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04+05 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅ +**Logs:** keine `ConnectionResetError`, keine unbehandelten Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅ + +## Bugs behoben während E2E (20.08.2026) +| Bug | Fix | +|-----|-----| +| `ConnectionResetError` beim Publish | Publisher: **frische Verbindung je Publish** + `conn.close()` (RabbitMQ schließt ungenutzte Verbindung) — **gleicher Bug in Modul-03, -04, -05** | +| Modul-03 OHLCV-Feld `ts` | Normalisierung `ts`→`timestamp` im MarketData-Client | +| Zombie-Consumer (`strategy.input` 2–3) | `queue_delete` beim Consumer-Start; manuelles Cleanup via `rabbitmqctl delete_queue` | +| Keine Events nach Recreate | OHLCV/Regime-Daten waren noch in DB (Duplikat) → E2E bereinigt `ohlcv`+`market_regime`+`strategy_signal` | +| M5SHORT ging verloren | Zombie-Consumer verschluckte Event (Round-Robin) → beseitigt | + +## Offene Punkte +- Downstream-Consumer für `strategy.signal.detected` (Modul-06+) +- Weitere Strategien über Engine-Register hinzufügbar + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-05-Dokumentation angelegt (Strategy-Engine trend_pullback_v1, strategy_signal-Schema, Events, E2E freigegeben). +``` diff --git a/notes/trading/system-docs/modul-06-signal-ranking.md b/notes/trading/system-docs/modul-06-signal-ranking.md new file mode 100644 index 0000000..676f51d --- /dev/null +++ b/notes/trading/system-docs/modul-06-signal-ranking.md @@ -0,0 +1,159 @@ +# Modul-06-Signal-Ranking — Betriebsdokumentation + +> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ **Freigegeben** (E2E+Idempotenz+Reconnect+Doku grün) + +## Zweck +**Signal-Ranking** — konsumiert `SIGNAL_DETECTED` (Modul-05) + OHLCV (Modul-03), bewertet die Signalqualität deterministisch (Score 0–100, vollständig nachvollziehbar und konfigurierbar), persistiert das Ergebnis und publiziert `SIGNAL_RANKED`. +Pipeline: `SIGNAL_DETECTED (Modul-05) → Signal-Ranking (signal_quality_v1) → PostgreSQL (signal_ranking) → SIGNAL_RANKED` +**Deterministische, regelbasierte Ranking-Engine — bewusst OHNE KI/ML. Keine Positionsgröße, kein Risk Management, keine Broker-/Order-Ausführung (Trade-Freigabe macht später der Risk Manager).** Modular aufgebaut, damit später eine ML/KI-Engine als ZUSÄTZLICHE Ranking-Engine registriert werden kann. + +## Container +| Attribut | Wert | +|----------|------| +| Name | `Modul-06-Signal-Ranking` | +| Image | `signal-ranking:0.1.0` (lokal gebaut) | +| Port | **55006** — **NUR intern** (`expose`, nicht öffentlich) | +| Netzwerk | `trading-modules` (bridge) | +| Build-Context | `/opt/trading-modules/modul06-signal-ranking/` | +| Restart | `unless-stopped` | +| Healthcheck | ✅ `healthy` | + +## Deployment +```bash +cd /opt/trading-modules +docker compose build modul-06-signal-ranking +docker compose up -d --no-deps --force-recreate modul-06-signal-ranking +``` + +## Environment Variables (Compose) +| Variable | Wert (Default) | Zweck | +|----------|---------------|-------| +| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename | +| `PG_PORT` | `5432` | intern | +| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults | +| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename | +| `RABBITMQ_PORT` | `5672` | intern | +| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** | +| `LOG_LEVEL` | `INFO` | Strukturiertes Logging | + +Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults. + +## Architektur +``` +Modul-05 ──signal.detected──▶ RankingConsumer (ranking.input) + │ + ▼ + OHLCV (Modul-03, intern 55003/history/{symbol}) + │ + ▼ + RankingEngine (deterministisch, Registry) + signal_quality_v1 (9 Komponenten, Score 0–100) + │ + ┌───────────┴───────────┐ + ▼ ▼ + signal_ranking (PG) SIGNAL_RANKED + (eigene Tabelle) → market.rankings / signal.ranked +``` + +- **Consumer** (`app/consumer/consumer.py`): bindet Exchange `market.signals`, Routing `strategy.signal.detected`; durable Queue `ranking.input`; manuelles Ack erst nach erfolgreicher Verarbeitung; Reconnect mit Backoff + `conn.close()`; **`queue_delete` beim Start** (entfernt verwaiste/Zombie-Consumer). +- **MarketData-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`); hoher Lookback, damit die letzten geschlossenen Kerzen enthalten sind. +- **Engine/Register** (`app/ranking/engine.py`): modular — Ranking-Engines via `.name/.version/rank()` registriert; KI/ML später ergänzbar. +- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index auf `source_signal_id`. +- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen). +- **Service** (`app/core/service.py`): Pipeline Event → OHLCV → Ranking → speichern + publizieren; robuste Payload-Extraktion; setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inneren payload ziehen konnte. + +## Ranking-Engine V1 — `signal_quality_v1` (`app/ranking/signal_quality_v1.py`) +Deterministische Qualitätsbewertung. **9 Komponenten, Score 0–100, vollständig nachvollziehbar und konfigurierbar.** Jede Komponente liefert 0..max; Summe = `total_score`. Komponenten werden einzeln in der DB gespeichert (`comp_*`). + +| Komponente | Max | Bewertet | +|-----------|-----|----------| +| `regime` | 20 | Market-Regime-Qualität / Confidence | +| `trend` | 15 | Trendstärke | +| `pullback` | 15 | Pullback-Qualität | +| `trigger` | 10 | Entry-Bestätigung | +| `rr` | 10 | Chance/Risiko-Verhältnis | +| `volatility` | 10 | Volatilitätszustand | +| `distance` | 10 | Distanz Entry→Stop | +| `data_quality` | 5 | Datenqualität / Candle-Anzahl | +| `consistency` | 5 | Setup-Konsistenz | + +**Klassifizierung:** `total_score >= eligible_threshold` → `eligible`; darunter `weak` (wird trotzdem gespeichert). + +**Zentrale Schwellenwerte** (`app/config.py`, env-overridable): +| Parameter | Default | Bedeutung | +|-----------|---------|-----------| +| `max_score` | 100 | Score-Obergrenze | +| `eligible_threshold` | 70 | Score ≥ 70 → `eligible` | +| `ranking_lookback` | 500 | max. Kerzen für Trend/Volatilität (Modul-03 liefert die N ältesten, daher hoch) | +| `min_candles_required` | 60 | unter dieser Zahl → Datenqualität-Punktabzug | +| `full_candles_target` | 250 | ab dieser Zahl → volle Datenqualität | +| `pullback_depth_min/max` | 0.5 / 4.0 | Abstand Entry zu Trend-Referenz in ATR | +| `vol_ideal_lo/hi` | 0.005 / 0.030 | ATR %-Fenster für "ideale" Volatilität | + +Keine Trade-Freigabe — nur Qualitätsbewertung. + +## Datenbank (Modul-01-PostgreSQL) +Eigene Tabelle `public.signal_ranking` (bestehende unangetastet): +```sql +ranking_id UUID, source_signal_id TEXT, source_event_id TEXT, correlation_id TEXT, +timestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT, +ranking_name TEXT, ranking_version TEXT, ranking_class TEXT (eligible/weak), +total_score DOUBLE PRECISION, comp_regime/comp_trend/comp_pullback/comp_trigger/comp_rr/ +comp_volatility/comp_distance/comp_data_quality/comp_consistency DOUBLE PRECISION, +details JSONB, reasons JSONB +``` +- **Unique (partiell):** `uq_signal_ranking_src` auf `(symbol, timeframe, source_signal_id)` **WHERE source_signal_id IS NOT NULL** → Idempotenz (`source_signal_id` nie doppelt). +- Migration: `migrations/001_signal_ranking.sql` (idempotent, löscht nichts). + +## RabbitMQ (Modul-02) +| Exchange | Typ | Routing | Event | +|----------|-----|---------|-------| +| `market.signals` | topic | `strategy.signal.detected` (eingang) | SIGNAL_DETECTED | +| `market.rankings` | topic | `signal.ranked` (ausgang) | SIGNAL_RANKED | + +## Interne API +| Endpoint | Zweck | +|----------|-------| +| `GET /health` | Liveness (200) + Komponentenstatus im Body | +| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) | +| `GET /rankings/{symbol}` | Rankings eines Symbols | +| `GET /rankings/latest/{symbol}` | Neuestes Ranking eines Symbols | +| `GET /ranking-engines` | Registrierte Ranking-Engines | + +## End-to-End-Test (20.08.2026, final) ✅ +Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 → `SIGNAL_DETECTED` → Modul-06 Consumer → `signal_quality_v1` → `signal_ranking` → `SIGNAL_RANKED`. + +| Fall | Ranking | Score | Class | DB | SIGNAL_RANKED | +|------|---------|-------|-------|----|---------------| +| M5LONG (TREND_UP) | **eligible** | **72.2** | eligible | ✅ 1 | ✅ 1 | +| M5SHORT (TREND_DOWN) | **eligible** | **73.6** | eligible | ✅ 1 | ✅ 1 | +| M5NOPULL (kein Pullback) | — | — | — | ✅ 0 | ✅ 0 | +| M5RANGE (Range) | — | — | — | ✅ 0 | ✅ 0 | + +**Verifizierte Eigenschaften:** +- Gültiger LONG/SHORT → **genau 1** `SIGNAL_RANKED`; NOPULL/RANGE → **0** Ranking + 0 Event. ✅ +- DB-Eintrag je gültigem Signal vorhanden; `source_signal_id` korrekt übernommen (z.B. `effd782c...`, `42dd2042...`). ✅ +- `total_score` immer 0–100; **Summe aller Komponenten == total_score**. ✅ +- `eligible`/`weak` korrekt gemäß Threshold (72.2/73.6 ≥ 70 → eligible). ✅ +- **Idempotenz:** identisches `SIGNAL_DETECTED` erneut → **kein zweiter DB-Eintrag, kein zweites SIGNAL_RANKED** (count=1). ✅ +- **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04/05/06 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅ +- **Logs:** keine unbehandelten Tracebacks nach Reconnect. Health/Readiness `{postgresql:true, rabbitmq:true, market_data:true, consumer_ready:true}`. ✅ + +## Bugs behoben während E2E (20.08.2026) +| Bug | Fix | +|-----|-----| +| `source_signal_id` leer | Service setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inner-Payload ziehen konnte | +| OHLCV-Lookback zu klein (120) | `ranking_lookback` auf 500 erhöht — Modul-03 `/history?limit=N` liefert die N **ältesten** Kerzen; zu kleiner Lookback → Engine bewertete auf veralteten Kerzen (Konsistenz/Trend/Pullback schief) | +| `direction`-Spalte existierte nicht | E2E-Query auf `details`/Modell angepasst | + +## Offene Punkte +- Downstream-Consumer für `signal.ranked` (Modul-07+ — noch NICHT begonnen) +- KI/ML-Ranking-Engine über Registry ergänzbar +- Trade-Freigabe später durch Risk Manager (noch nicht gebaut) + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-06-Dokumentation angelegt (Signal-Ranking signal_quality_v1, signal_ranking-Schema, Events, E2E final grün). +``` diff --git a/notes/trading/system-docs/modul-07-risk-manager.md b/notes/trading/system-docs/modul-07-risk-manager.md new file mode 100644 index 0000000..185c155 --- /dev/null +++ b/notes/trading/system-docs/modul-07-risk-manager.md @@ -0,0 +1,169 @@ +# Modul-07: Risk-Manager + +**Status:** ✅ **Freigegeben** (20.08.2026) +**Version:** 0.1.0 +**Port (intern):** 55007 +**Netzwerk:** `trading-modules-trading-modules` + +--- + +## Zweck + +Deterministischer Risk-Manager in der Trading-Kette. Konsumiert `SIGNAL_RANKED` +(aus Modul-06), führt eine **harte, deterministische Risikoprüfung** durch, berechnet +die Positionsgröße und publiziert `RISK_APPROVED` oder `RISK_REJECTED`. + +**Prinzipien:** +- **KEINE KI/ML** — rein deterministische Regeln. +- **KEINE Broker-Order** — nur Risiko-Entscheidung + Positionsgröße. +- **FAIL-CLOSED** — Fehler oder fehlende Daten führen **niemals** zu APPROVED, + sondern immer zu REJECTED. + +--- + +## Architektur + +``` +SIGNAL_RANKED (market.rankings / signal.ranked) + │ + ▼ +┌─────────────────────────────────────────────┐ +│ Modul-07 Risk-Manager │ +│ Consumer (risk.input) │ +│ → RiskEngine (Registry) │ +│ → risk_v1 (deterministische Prüfung) │ +│ → Storage (risk_decision, idempotent) │ +│ → Publisher (market.risk) │ +└─────────────────────────────────────────────┘ + │ + ├── RISK_APPROVED (risk.approved) + └── RISK_REJECTED (risk.rejected) +``` + +### Komponenten +- `app/consumer/consumer.py` — RabbitMQ-Consumer `SIGNAL_RANKED`, manuelles ACK, + Zombie-Schutz (`queue_delete` beim Start), Reconnect-Backoff 1s→2s→4s→8s. +- `app/risk/engine.py` — Registry für Risiko-Verfahren (V1, zukünftig V2). +- `app/risk/risk_v1.py` — deterministische Kernlogik + Positionsgrößen-Mathematik. +- `app/instruments/registry.py` — instrument-agnostische Werte (Contract Size, + Lot Size, Tick Size/Value, Min/Max Quantity, Quantity Step, Währung/FX). +- `app/storage/storage.py` — idempotente Persistenz (unique auf `source_ranking_id`). +- `app/publisher/publisher.py` — frische Verbindung je Publish + `conn.close()` + im `finally` (RabbitMQ-Bug-Fix aus Modul-03/04/05/06). +- `app/api/main.py` — FastAPI, Port 55007 intern, `/health`, `/health/ready`. + +--- + +## V1-Risikoregeln (zentral konfigurierbar in `config.py`, env-overridable) + +| Regel | Default | Beschreibung | +|---|---|---| +| `account_equity` | 10000 | Account-Equity | +| `risk_percent` | 0.005 | max. Risiko pro Trade (0,5 %) | +| `min_score` | 70 | Mindest-Ranking-Score | +| `min_ranking_class` | eligible | Mindest-Ranking-Klasse | +| `max_positions` | 5 | max. Anzahl Positionen (nur bei vorhandenen Daten) | +| `max_open_risk` | 0.02 | max. gesamtes offenes Risiko (nur bei vorhandenen Daten) | +| `daily_loss_limit` | 0.03 | tägliches Verlustlimit (nur bei vorhandenen Daten) | +| `max_drawdown` | 0.20 | maximales Drawdown-Limit (nur bei vorhandenen Daten) | + +> **Hinweis:** Globale Portfolio-Limits (offene Positionen, Tagesverlust, Drawdown) +> werden **nur geprüft, soweit Daten vorhanden sind**. Die eigentliche +> Portfolio-Logik gehört zu **Modul-08**. Fehlende Portfolio-Daten sind **kein** +> REJECT-Grund. + +### Positionsgröße +``` +risk_amount = equity × risk_percent +risk_per_unit = abs(entry − stop) +raw_size = risk_amount / risk_per_unit +quantity = floor(raw_size / quantity_step) × quantity_step # ABRUNDEN +``` +Abrunden auf `quantity_step` (nie aufrunden), damit das erlaubte Risiko nie +überschritten wird (FAIL-CLOSED-Sicherheit). + +--- + +## Output (RiskDecision) + +Nachvollziehbar gespeichert in `risk_decision`: +`risk_decision_id`, `source_ranking_id`, `symbol`, `asset_class`, `direction`, +`entry`, `stop_loss`, `target`, `equity`, `risk_percent`, `max_risk_amount`, +`calculated_position_size`, `actual_risk_amount`, `decision` (APPROVED/REJECTED), +`reason_codes`, `rule_version`, `timestamp`. + +--- + +## RabbitMQ + +| Rolle | Exchange | Routing Key | Queue | +|---|---|---|---| +| Consumer | `market.rankings` | `signal.ranked` | `risk.input` | +| Publisher | `market.risk` | `risk.approved` | — | +| Publisher | `market.risk` | `risk.rejected` | — | + +- Manuelles ACK erst nach erfolgreicher Verarbeitung. +- Idempotenz: gleiches `source_ranking_id` → keine Doppelentscheidung/Event. +- Reconnect ohne Zombies (Backoff 1s→2s→4s→8s, `queue_delete` beim Start). + +--- + +## Tests + +### Unit-Tests (`test_risk.py`) — 10/10 grün +1. gültiger LONG → APPROVED + korrekte Größe +2. gültiger SHORT → APPROVED +3. Score unter Threshold → REJECTED +4. falscher Stop → REJECTED +5. fehlende/ungültige Daten → REJECTED +6. Positionsgröße/Risiko mathematisch korrekt +7. Quantity-Step/Min/Max korrekt +8. Daily-Loss/Drawdown-Limit → REJECTED +9. Idempotenz +10. RabbitMQ-Reconnect + +### E2E (03→04→05→06→07) — ALLE CHECKS BESTANDEN +- **A:** Kette 03→07, Decision in DB (LONG & SHORT) ✓ +- **B:** exakt 1 Risiko-Event je Symbol (APPROVED/REJECTED) ✓ +- **C:** Pflichtfelder in Decision vorhanden ✓ +- **D:** identisches Ranking → kein Doppel-Decision ✓ +- **E:** Score<70 / falscher Stop / fehlende Daten → REJECTED + source_ranking_id ✓ + +### Mathematik-Verifikation (aus DB) +| Symbol | Direction | Entry | Stop | equity | risk% | max_risk | qty | actual_risk | +|---|---|---|---|---|---|---|---|---| +| M5LONG | LONG | 164.2 | 163.47636 | 10000 | 0.005 | 50 | 69 | 49.93 | +| M5SHORT | SHORT | 135.8 | 136.49636 | 10000 | 0.005 | 50 | 71 | 49.44 | + +Beide `actual_risk_amount ≤ max_risk_amount` ✓ + +### Reconnect-Test +RabbitMQ kontrolliert neu gestartet → alle 4 Queues (`market-regime.input`, +`strategy.input`, `ranking.input`, `risk.input`) mit **exakt 1 Consumer**, +keine Zombies, keine verlorenen Events, `risk_decision` stabil. + +--- + +## Health/Readiness + +- `/health` → `{"status":"ok", "postgresql":true, "rabbitmq":true, "consumer_ready":true}` +- `/health/ready` → 200 +- Container: `Modul-07-Risk-Manager` (healthy) + +--- + +## Technischer offener Punkt: Modul-03 `/history?limit=N` + +**Modul-03 `/history?limit=N` liefert aktuell die N ältesten Candles (aufsteigend).** +Das ist langfristig zu korrigieren (sollte die N **neuesten** Candles liefern). +Der Workaround in Modul-06 (Lookback 500) ist **nicht als API-Vertrag** zu +behandeln — er ist ein temporärer Workaround, kein garantiertes Verhalten. + +--- + +## Deployment + +- Compose: `/opt/trading-modules/docker-compose.yml` (Block `modul-07-risk-manager`) +- Nur `expose: 55007` (kein öffentlicher Port) +- `restart: unless-stopped` +- Image: `risk-manager:0.1.0` diff --git a/notes/trading/system-docs/modul-08-portfolio-manager.md b/notes/trading/system-docs/modul-08-portfolio-manager.md new file mode 100644 index 0000000..5f520b5 --- /dev/null +++ b/notes/trading/system-docs/modul-08-portfolio-manager.md @@ -0,0 +1,252 @@ +# Modul-08: Portfolio-Manager + +**Status: FREIGEGEBEN** (20.08.2026) + +Deterministischer Portfolio-Manager. Konsumiert `RISK_APPROVED` aus Modul-07, +prüft das Gesamtportfolio gegen zentrale Limits und publiziert +`TRADE_APPROVED` oder `PORTFOLIO_REJECTED`. + +**Keine KI/ML. Keine Broker-Order. FAIL-CLOSED.** + +--- + +## 1. Rolle in der Kette + +``` +Modul-03 Ingest → 04 Regime → 05 Signal → 06 Ranking + → SIGNAL_RANKED (market.rankings/signal.ranked) + → Modul-07 Risk-Check → RISK_APPROVED (market.risk/risk.approved) + → Modul-08 Portfolio-Check → portfolio_decision-Tabelle + → TRADE_APPROVED / PORTFOLIO_REJECTED (market.portfolio) +``` + +Modul-08 prüft **NICHT erneut** die Positionsgrößenlogik aus Modul-07, +sondern ausschließlich **Portfolio-Risiken**. + +--- + +## 2. Events + +| Richtung | Exchange | Routing-Key | Event | +|----------|----------|-------------|-------| +| Input (Consumer) | `market.risk` | `risk.approved` | `RISK_APPROVED` | +| Output (Publisher) | `market.portfolio` | `trade.approved` | `TRADE_APPROVED` | +| Output (Publisher) | `market.portfolio` | `portfolio.rejected` | `PORTFOLIO_REJECTED` | + +Consumer-Queue: `portfolio.input` (durable, manuelles Ack, prefetch=1). + +--- + +## 3. V1 Portfolio-Regeln (zentral konfigurierbar, env-overridable) + +| Regel | Config-Feld | Default | +|-------|-------------|---------| +| max. Anzahl offener Positionen | `max_positions` | 5 | +| max. gesamtes Open Risk % | `max_open_risk_percent` | 0.05 (5%) | +| max. Exposure pro Position % | `max_exposure_per_position_percent` | 0.20 | +| max. Exposure pro Symbol % | `max_exposure_per_symbol_percent` | 0.30 | +| max. Exposure pro Assetklasse % | `max_exposure_per_asset_class_percent` | 0.50 | +| max. Long-Exposure % | `max_long_exposure_percent` | 0.60 | +| max. Short-Exposure % | `max_short_exposure_percent` | 0.60 | +| Duplikat Symbol/Strategie erlauben | `allow_duplicate_symbol_strategy` | false | +| Korrelation anwenden | `apply_correlation` | false (vorbereitet) | +| Account Equity | `account_equity` | 10000.0 | + +Alle Limits sind Prozentwerte des `account_equity`. Bei `account_equity <= 0` +→ FAIL-CLOSED `PORTFOLIO_REJECTED`. + +**Später geplant** (V2): Sector/Country/Currency-Exposure, Korrelation +(erst wenn verlässliche Daten vorhanden). + +--- + +## 4. FAIL-CLOSED + +Jede Exception, fehlende oder ungültige kritische Eingabe führt zu +`PORTFOLIO_REJECTED`, **niemals** zu `TRADE_APPROVED`. Fehlende kritische +Felder (quantity, risk, entry) → `PORTFOLIO_REJECTED` mit Reason-Codes. + +--- + +## 5. Portfoliozustand (autoritative Quelle) + +Der Portfoliozustand wird **NICHT nur aus Events angenommen**. Die +Architektur trennt die Quelle des Zustands vom Entscheidungs-Code: + +- `app/portfolio/state.py` definiert `PortfolioStateProvider` (Interface) + und `PortfolioState` / `OpenPosition`. +- V1-Provider liest offene Positionen aus der eigenen `portfolio_decision`- + Tabelle (alle `TRADE_APPROVED`-Entscheidungen). +- **Später** kann ein Broker-/Execution-Provider als autoritative Quelle + ergänzt werden, ohne den Entscheidungs-Code zu ändern. + +--- + +## 6. Race-Condition-Schutz (atomar) + +Zwei nahezu gleichzeitige `RISK_APPROVED` dürfen **nicht** beide auf demselben +alten Portfoliozustand genehmigt werden und dadurch Limits überschreiten. + +Lösung (`app/storage/storage.py` → `evaluate_and_save_atomic`): +1. **Advisory Lock** (`pg_advisory_xact_lock`) serialisiert parallele + Portfolio-Entscheidungen. +2. Innerhalb **einer Transaktion**: Zustand lesen → Engine bewerten → + speichern → Commit. +3. Der zweite Prozess wartet auf den Lock und bewertet auf dem **aktualisierten** + Zustand (inkl. der ersten Entscheidung). + +--- + +## 7. Idempotenz + +- Unique-Index `uq_portfolio_decision_src` auf `source_risk_decision_id` + (partial, nur wo nicht NULL). +- Gleiches `source_risk_decision_id` → keine Doppelentscheidung/Event. +- Zusätzlich prüft der Service vor der Verarbeitung, ob die Quelle bereits + entschieden wurde (Skip + Ack). + +--- + +## 8. DB-Tabelle `portfolio_decision` + +Jede Entscheidung wird gespeichert — **auch `PORTFOLIO_REJECTED`**. + +| Spalte | Typ | Beschreibung | +|--------|-----|--------------| +| `id` | BIGSERIAL PK | | +| `portfolio_decision_id` | TEXT | eigene Entscheidungs-ID | +| `source_risk_decision_id` | TEXT | id des RISK_APPROVED (Modul-07) | +| `source_ranking_id` | TEXT | durchgereichtes Ranking (Modul-06) | +| `source_signal_id` | TEXT | durchgereichtes Signal (Modul-05) | +| `source_event_id` | TEXT | | +| `correlation_id` | TEXT | | +| `timestamp` | TIMESTAMPTZ | Entscheidungszeitpunkt (UTC) | +| `symbol` | TEXT | | +| `asset_class` | TEXT | | +| `strategy` | TEXT | | +| `direction` | TEXT | LONG / SHORT | +| `timeframe` | TEXT | | +| `proposed_quantity` | DOUBLE | aus RISK_APPROVED | +| `proposed_risk` | DOUBLE | actual_risk_amount | +| `proposed_exposure` | DOUBLE | qty × entry | +| `current_portfolio_risk` | DOUBLE | offenes Risiko VOR | +| `new_portfolio_risk` | DOUBLE | offenes Risiko NACH | +| `exposure_before` | DOUBLE | Gesamt-Exposure VOR | +| `exposure_after` | DOUBLE | Gesamt-Exposure NACH | +| `decision` | TEXT | TRADE_APPROVED / PORTFOLIO_REJECTED | +| `reason_codes` | TEXT[] | z. B. `{OK}` oder `{EXPOSURE_PER_POSITION_EXCEEDED,...}` | +| `rule_version` | TEXT | `portfolio_v1@1.0.0` | +| `details` | JSONB | Begründung + Zwischenwerte | +| `portfolio_snapshot` | JSONB | Portfoliozustand zum Zeitpunkt | +| `created_at` | TIMESTAMPTZ | | + +--- + +## 9. Reason-Codes + +| Code | Bedeutung | +|------|-----------| +| `OK` | alle Limits eingehalten → TRADE_APPROVED | +| `MAX_POSITIONS_EXCEEDED` | max. offene Positionen erreicht | +| `OPEN_RISK_EXCEEDED` | gesamtes Open Risk überschritten | +| `EXPOSURE_PER_POSITION_EXCEEDED` | Exposure pro Position überschritten | +| `EXPOSURE_PER_SYMBOL_EXCEEDED` | Exposure pro Symbol überschritten | +| `EXPOSURE_PER_ASSET_CLASS_EXCEEDED` | Exposure pro Assetklasse überschritten | +| `LONG_EXPOSURE_EXCEEDED` | Long-Exposure überschritten | +| `SHORT_EXPOSURE_EXCEEDED` | Short-Exposure überschritten | +| `DUPLICATE_SYMBOL_STRATEGY` | doppelte Position Symbol/Strategie | +| `QUANTITY_MISSING` / `RISK_AMOUNT_MISSING` / `ENTRY_MISSING` | fehlende kritische Daten (FAIL-CLOSED) | +| `EQUITY_MISSING` | account_equity fehlt/ungültig (FAIL-CLOSED) | + +--- + +## 10. Architektur + +``` +app/ + config.py # zentrale Konfiguration (env-overridable) + core/ + models.py # PortfolioDecision + Event-Modelle + service.py # Orchestrator (Consumer→Engine→Storage→Publisher) + portfolio/ + engine.py # Registry der Portfolio-Verfahren + portfolio_v1.py # deterministische V1-Prüfung (FAIL-CLOSED) + state.py # PortfolioState-Provider (autoritative Quelle) + storage/ + storage.py # idempotent + atomare Race-Condition-Lösung + publisher/ + publisher.py # frische Verbindung je Publish + close + consumer/ + consumer.py # manuelles Ack, Reconnect-Backoff, Zombie-Schutz + api/ + main.py # FastAPI (Health/Readiness/Decisions/State) +migrations/ + 001_portfolio_decision.sql +Dockerfile +requirements.txt +test_portfolio.py # 11 Unit-Tests (Engine-Logik) +``` + +--- + +## 11. Tests + +### Unit-Tests (`test_portfolio.py`, 11/11 grün) +1. erstes gültiges Portfolio-Trade → TRADE_APPROVED +2. max Positions überschritten → PORTFOLIO_REJECTED +3. max Open Risk überschritten → PORTFOLIO_REJECTED +4. Symbol-Exposure überschritten → PORTFOLIO_REJECTED +5. Long/Short-Exposure-Limit → PORTFOLIO_REJECTED +6. Duplicate Symbol/Strategie → PORTFOLIO_REJECTED +7. fehlender Portfoliozustand → FAIL-CLOSED +8. Idempotenz (deterministisch) +9. Assetklassen-Exposure → PORTFOLIO_REJECTED +10. Exposure pro Position → PORTFOLIO_REJECTED +11. Short-Exposure → PORTFOLIO_REJECTED + +### E2E (Kette 03→04→05→06→07→08, alle Checks grün) +- **A**: Kette 03→08, Portfolio-Decision in DB (LONG & SHORT) +- **B**: exakt 1 Portfolio-Event je Symbol (TRADE_APPROVED/PORTFOLIO_REJECTED) +- **C**: Pflichtfelder in Portfolio-Decision vorhanden +- **D**: Idempotenz (identisches RISK_APPROVED → kein Doppel-Decision) +- **E**: parallele RISK_APPROVED → beide verarbeitet, keine Race-Verlust +- **F**: erstes gültiges Portfolio-Trade → TRADE_APPROVED + +### Reconnect-Test +RabbitMQ gestoppt → Consumer erkennt Ausfall, Reconnect-Backoff (1s→2s→4s→8s). +RabbitMQ gestartet → Consumer verbindet sich neu, alle 5 Queues haben exakt +1 Consumer, Health/Readiness 200. + +--- + +## 12. Health / Readiness + +- `GET /health` → Liveness (immer 200, Komponentenstatus im Body) +- `GET /health/ready` → Readiness (200 nur wenn PG + RabbitMQ erreichbar) +- `GET /decisions/{symbol}` → gespeicherte Entscheidungen (inkl. REJECTED) +- `GET /portfolio-rules` → verfügbare Portfolio-Regeln +- `GET /portfolio/state` → aktueller Portfoliozustand + +Port: **55008** (nur Docker-intern, kein öffentlicher Host-Port). + +--- + +## 13. Deploy + +- Compose-Service: `modul-08-portfolio-manager` +- Image: `portfolio-manager:0.1.0` +- Container: `Modul-08-Portfolio-Manager` +- Netz: `trading-modules` +- `restart: unless-stopped` +- Env: PG + RabbitMQ-Zugangsdaten (aus Compose) + +--- + +## 14. Wichtige Fixes (20.08.2026) + +**Consumer-Bug (alle Module 04–08):** `process_data_events(time_limit=None)` +verarbeitete nur EIN Event und baute danach die Verbindung neu auf. Der +erneute `_connect()` führte `queue_delete` aus und löschte wartende Events +(z. B. ein zweites, nahezu gleichzeitiges RISK_APPROVED) unwiederbringlich. +Fix: innere Schleife `while not stop: process_data_events(time_limit=1.0)` +hält die Verbindung am Leben. Betroffen und gefixt: Modul-04, 05, 06, 07, 08. diff --git a/notes/trading/system-docs/modul-09-execution-service.md b/notes/trading/system-docs/modul-09-execution-service.md new file mode 100644 index 0000000..c27a0fc --- /dev/null +++ b/notes/trading/system-docs/modul-09-execution-service.md @@ -0,0 +1,146 @@ +# Modul-09: Execution-Service + +**Status: FREIGEGEBEN** (20.08.2026) + +Deterministischer Execution-Service. Konsumiert `TRADE_APPROVED` aus Modul-08, +validiert die Order hart (FAIL-CLOSED), übergibt sie an einen Broker-Adapter +(V1: PaperBrokerAdapter) und überwacht den Status. Ergebnis wird idempotent +in eigenen DB-Tabellen gespeichert und als RabbitMQ-Events publiziert. + +**Sicherheit: KEIN unkontrolliertes Live-Trading.** +- Default `EXECUTION_MODE=PAPER` (bzw. DRY_RUN) +- `TRADING_ENABLED=false` als sicherer Default für LIVE +- LIVE nur über explizite Konfiguration aktivierbar, FAIL-CLOSED +- Bei Timeout nach Order-Senden NIE blind erneut senden — erst über + `broker_order_id`/`client_order_id` bzw. Brokerstatus klären + +## Architektur + +``` +TRADE_APPROVED (market.portfolio/trade.approved, Modul-08) + │ + ▼ +ExecutionConsumer (execution.input, Consumer-Fix von Anfang an) + │ + ▼ +Order-Validierung (hart, FAIL-CLOSED) + │ source_portfolio_decision_id gültig, decision=APPROVED, + │ symbol/direction/quantity vorhanden, quantity>0, + │ entry/stop/target plausibel, Kette nachvollziehbar, + │ kein bereits ausgeführter identischer Auftrag, Kill-Switch + ▼ +BrokerAdapter-Interface (modular, austauschbar) + │ V1: PaperBrokerAdapter (realistischer Lifecycle) + ▼ +ExecutionStorage (idempotent, Advisory Lock + Transaktion) + │ execution_order + execution_event + ▼ +RabbitMQPublisher (market.execution) + ORDER_SUBMITTED / ORDER_FILLED / ORDER_REJECTED / ORDER_FAILED +``` + +## BrokerAdapter-Interface + +- `BrokerAdapter` (app/broker/base.py): `name()`, `is_configured()`, + `submit_order()`, `get_order_status()` +- `PaperBrokerAdapter` (app/broker/paper.py): V1, simuliert realistischen + Lifecycle (SUBMITTED → FILLED, Partial Fill, Reject, Timeout) für Tests +- `BrokerRegistry` (app/broker/registry.py): wählt Adapter anhand + `DEFAULT_BROKER` (V1: paper). Echte Broker-Adapter später austauschbar, + keine Brokerlogik im Core-Code. + +## Idempotenz + +- Unique-Index `uq_execution_order_src` auf `source_portfolio_decision_id` + → gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder +- Unique-Index `uq_execution_order_client` auf `client_order_id` +- Client Order ID deterministisch aus Execution-ID abgeleitet + (`EXEC-`) +- Advisory Lock + Transaktion: parallele identische Events → exakt eine + Execution (Race-Condition-Schutz, wie Modul-08) +- ACK eines TRADE_APPROVED erst, wenn die Order dauerhaft in der DB gespeichert ist + +## Order-Status + +`PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED` +sowie `REJECTED`, `CANCELLED`, `FAILED`. + +## DB-Tabellen + +- `execution_order`: execution_id, source_portfolio_decision_id, broker, mode, + symbol, asset_class, direction, quantity, requested_price, stop_loss, target, + broker_order_id, client_order_id, status, filled_quantity, avg_fill_price, + timestamps, error/reason_codes, execution_version +- `execution_event`: append-only Log der Status-Übergänge + +## RabbitMQ + +- Exchange `market.execution` (topic, durable) +- Routing: `order.submitted`, `order.filled`, `order.rejected`, `order.failed` +- Queue `execution.input` (durable), gebunden an `market.portfolio`/`trade.approved` +- Consumer-Fix aus Modul-04–08 von Anfang an: dauerhafte Verbindung, + `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete + bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s + +## Tests + +- **21/21 Unit-Tests** (test_execution.py): gültige Order → PAPER FILLED, + Idempotenz, ungültige Quantity → REJECTED, Kill-Switch, fehlende + Broker-Credentials → FAIL-CLOSED, Broker-Reject, Timeout → keine blinde + Doppelorder, Partial Fill, Reconnect-Backoff, parallele identische Events +- **E2E 6/6 Checks** (Kette 03→04→05→06→07→08→09): Execution-Order in DB, + exakt 1 Paper-Order, Pflichtfelder, Idempotenz, ORDER_SUBMITTED+ORDER_FILLED, + gültiger Trade → PAPER FILLED +- **Reconnect-Test**: RabbitMQ gestoppt → Backoff → neu verbunden, 1 Consumer, + kein queue_delete bei normalen Reconnects + +## Deploy + +- Container `Modul-09-Execution-Service`, Port 55009 nur Docker-intern + (kein öffentlicher Host-Port) +- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit +- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper` + +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-09-Doku um final freigegebenen Control-Gate-Status (M15→M09, Variante C) ergänzt. + Kernregel, Einbau-Punkt, Audit-Felder, Parameter, Live-E2E-Ergebnisse dokumentiert. +``` + +## Control-Gate (M15→M09, FREIGEGEBEN 20.08.2026) + +**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — Variante C (Event-Cache-Vorfilter + synchroner HTTP-Final-Check). + +M15 (`Modul-15-Monitoring-Control`) ist die **autoritative Safety-/Trading-Control-Instanz**. +M09 sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt. + +### Kernregel +| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL | +|-------|---------------|---------------------| +| `ENABLED` | **ERLAUBT** | erlaubt | +| `PAUSED` | **VERBOTEN** | erlaubt | +| `HALTED` | **VERBOTEN** | erlaubt | +| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt | + +### Einbau-Punkt +Control-Check zwischen Schritt 4 (Order atomar in DB anlegen) und Schritt 5 (`_submit_and_track`). +Bei OPEN/INCREASE ohne Erlaubnis → Order als `FAILED`/`REJECTED` speichern, **nicht** senden. +`TRADING_ENABLED`-Flag bleibt unverändert (blockiert nur LIVE, nicht PAPER). + +### Audit-Felder je Order +`action_type`, `control_status`, `control_state_version`, `control_expires_at`, +`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`). + +### Parameter +- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort. +- `expires_at`-TTL (M15): **500 ms**; Cache-TTL (M09): **5 s**. +- Kein Auto-Resume; kein "Senden ohne Check". + +### Live-E2E (VPS, 20.08.2026) — alle Szenarien grün +S1 ENABLED 9/9 · S2 HALTED 9/9 · S3 PAUSED 4/4 · S4 Risikoabbau 4/4 · S5 M15-down 7/7 · +S6 Cache-vs-Final 2/2 · S7 Race/TTL 2/2 · S8 Event-Cache verifiziert · S9 Recovery 3/3 · +S10 Health/Security verifiziert · S11 Regression bestanden. + +Details: siehe `modul-15-m09-anbindung-implementierung.md` (FREIGEGEBEN). diff --git a/notes/trading/system-docs/modul-10-trade-journal.md b/notes/trading/system-docs/modul-10-trade-journal.md new file mode 100644 index 0000000..f782111 --- /dev/null +++ b/notes/trading/system-docs/modul-10-trade-journal.md @@ -0,0 +1,106 @@ +# Modul-10: Trade-Journal + +**Status: FREIGEGEBEN** (20.08.2026) + +Deterministisches, append-only Trade-Journal. Konsumiert die +Execution-Events (`ORDER_SUBMITTED`/`ORDER_FILLED`/`ORDER_REJECTED`/`ORDER_FAILED`) +aus Modul-09, validiert die Herkunftskette hart (FAIL-CLOSED) und dokumentiert +jeden Trade in einer Audit-Trail-konformen, idempotenten Struktur. +Ergebnis wird als `TRADE_RECORDED`-Event publiziert. + +**Sicherheit & Audit:** +- **Keine KI/ML, keine Orderausführung, keine Risikoberechnung** — reines Dokumentieren +- **Append-only**: `trade_event_history` ist unveränderlich (nur INSERT, kein UPDATE/DELETE) +- **Herkunftskette wird validiert** (Signal → Ranking → Risk → Portfolio → Execution) +- Bei inkonsistenter Kette → Trade als `INCONSISTENT` markiert (FAIL-CLOSED), nicht stumm verworfen +- `TRADE_RECORDED` optional: ACK nach persistenter Speicherung + +## Architektur + +``` +ORDER_* (market.execution, Modul-09) + │ + ▼ +JournalConsumer (journal.input, Consumer-Fix von Anfang an) + │ + ▼ +Herkunfts-Validierung (FAIL-CLOSED) + │ Kette 05→06→07→08→09 nachvollziehbar? + │ provenance_consistent in metadata + ▼ +JournalService + │ erstes Event → Trade anlegen + TRADE_RECORDED + │ Folge-Event → append-only Event + Status-Update (bestehenden Trade) + ▼ +JournalStorage (idempotent, Unique-Index) + │ trade_journal + trade_event_history + ▼ +RabbitMQPublisher (market.journal) + TRADE_RECORDED (trade.recorded) +``` + +## Service-/Storage-Grenze (vereinheitlicht) + +**Storage gibt IMMER `TradeJournal` zurück** — nie ein `dict`. +- Einzige Konvertierungsstelle `_row_to_trade` (DB-Zeile → Modell) +- `get_trade_by_execution()` → `Optional[TradeJournal]` +- Der Service arbeitet ausschließlich mit Modellen (kein dict/Model-Mix je Codepfad) +- Dadurch ist `trade_id` auf jedem Rückgabewert garantiert verfügbar +- (Fix gegen `'dict' object has no attribute 'trade_id'` → NACK/Requeue) + +## Verarbeitung + +- **Erstes Event (ORDER_SUBMITTED)** → Trade anlegen, `TRADE_RECORDED` publizieren +- **Folge-Event (ORDER_FILLED u.a.)** → bestehenden Trade als Status-Update + verarbeiten: append-only Event + Status-Update, KEIN neuer Trade, + **kein zweites TRADE_RECORDED** +- Bei FILLED: `entry_filled`/`filled_at`/Status aktualisiert (Trade öffnen/aktualisieren) +- Eingehende Events werden nicht doppelt verarbeitet (idempotent, Unique-Index) + +## Idempotenz + +- Unique-Index auf `(execution_id, event_id)` → jedes Event exakt einmal +- `provenance_consistent` in `metadata` (jsonb), nicht als Spalte +- `TRADE_RECORDED` nur beim ersten Event pro Trade +- Kein Doppel-Journal bei Re-Publish desselben Events + +## DB-Tabellen + +- `trade_journal`: trade_id, execution_id, source_portfolio_decision_id, + Herkunftsketten-IDs (signal/ranking/risk/portfolio), symbol, asset_class, + strategy, direction, timeframe, entry_requested, entry_filled, stop_loss, + target, quantity, risk_amount, execution_mode, broker_order_id, status, + provenance_consistent (in metadata), created_at, updated_at +- `trade_event_history`: execution_id, event_id, event_type, payload, created_at + (append-only, unveränderlich) + +## RabbitMQ + +- Exchange `market.journal` (topic, durable) +- Routing: `trade.recorded` +- Queue `journal.input` (durable), gebunden an `market.execution`/`order.*` +- Consumer-Fix von Anfang an: dauerhafte Verbindung, + `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete + bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s +- ACK erst nach persistenter Speicherung; bei Fehler NACK/Requeue + +## Tests + +- **39/39 Unit-Tests** (test_journal.py): Anlage, Status-Update, Idempotenz, + FAIL-CLOSED-Herkunftskette, append-only, exakt 1 TRADE_RECORDED, Reconnect +- **Gezielter Fix-Test 9/9** (test_fix_update.py): SUBMITTED→Journal, + FILLED→Update, Rückgabe ist TradeJournal (kein dict), `trade_id` vorhanden, + genau 1 Trade, append-only (2 Events), idempotent, kein Doppel-Journal +- **E2E 7/7 Checks** (Kette 03→10): Journal-Eintrag in DB, exakt 1 Trade, + Pflichtfelder + Herkunftskette konsistent, append-only Event-Historie + (SUBMITTED+FILLED), exakt 1 TRADE_RECORDED, Idempotenz nach Re-Publish +- **Reconnect-Test**: RabbitMQ gestoppt → Backoff 1→2→4→8→16s → wieder + verbunden, `journal.input` 1 Consumer, 0 Messages, kein Zombie +- **Log-Check**: 0 ERROR, 0 Traceback, ACK auf ORDER_SUBMITTED + ORDER_FILLED + +## Deploy + +- Container `Modul-10-Trade-Journal`, Port 55010 nur Docker-intern + (kein öffentlicher Host-Port) +- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit +- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false` diff --git a/notes/trading/system-docs/modul-11-analytics.md b/notes/trading/system-docs/modul-11-analytics.md new file mode 100644 index 0000000..e6e347f --- /dev/null +++ b/notes/trading/system-docs/modul-11-analytics.md @@ -0,0 +1,82 @@ +# Modul-11: Analytics + +**Status: FREIGEGEBEN** · Container `Modul-11-Analytics` · Image `analytics-service:0.1.0` + +## Zweck +Auswertung der Trade-Journal-Daten aus **Modul-10** (`trade_journal`) zu reproduzierbaren Performance-Kennzahlen. Deterministische Berechnung — gleiche Datenbasis → gleiche Ergebnisse. **V1 ohne KI/ML, ohne Orders, ohne automatische Strategieänderungen.** + +## Architektur / Datenfluss + +``` +TRADE_RECORDED (market.journal / trade.recorded) + │ + ▼ +Modul-11-Analytics ── konsumiert über Queue `analytics.input` + │ + ├── liest read-only → trade_journal (Modul-10, PostgreSQL) + ├── schreibt → analytics_snapshot, strategy_performance, daily_performance + └── publiziert (opt.)→ ANALYTICS_UPDATED (market.analytics / analytics.updated) + +API (Docker-intern, Port 55011): + /health, /health/ready + /analytics/portfolio Gesamt-/Portfolio-Performance + /analytics/strategy je Strategie+Version + /analytics/regime je Market Regime + /analytics/period je Monat (ab optionalem from_date) + /analytics/rebuild (POST) deterministischer Rebuild aus DB +``` + +Primärquelle ist `TRADE_RECORDED`; zusätzlich liest Analytics direkt aus PostgreSQL (`trade_journal`), da für reproduzierbare Auswertungen ein vollständiger, deterministischer Rebuild aus der DB sinnvoller ist. `TRADE_RECORDED`-Events triggern eine Neuberechnung (Idempotenz: identisches Event → keine Doppelzählung). + +## Datenquelle & Realisationslogik +- **Nur `status = 'CLOSED'` UND `realized_pnl IS NOT NULL`** fließen in realisierte Performance ein. +- **Offene Trades** (`OPEN`) werden separat ausgewiesen (`open_trade_count`) und zählen NICHT als realisierte Trades. +- Gruppierungsfelder aus `trade_journal`: `strategy`, `strategy_version`, `symbol`, `asset_class`, `direction` (LONG/SHORT), `regime`, `signal_score`. +- Zeitraum-Gruppierung nach `exit_time` (realisierter Trades), optional ab `from_date`. +- **Keine Lookahead-/Survivorship-Tricks**: nur tatsächlich geschlossene Trades, deterministische Equity-Kurve. + +## Kennzahlen (V1) +- Anzahl Trades (realisiert + offen getrennt) +- Winrate / Lossrate +- durchschnittlicher Gewinn / Verlust +- durchschnittliches R (realized_r_multiple) +- Expectancy (Währung + R) +- Profit Factor (gross_profit / gross_loss) +- Netto-PnL +- Brutto-Gewinn / Brutto-Verlust +- Max Drawdown (aus Equity-Kurve der realisierten PnL) +- Durchschnittliche Haltedauer (entry_time→exit_time, Stunden) +- Bester / schlechtester Trade + +## Eigene Tabellen (Migration `migrations/001_analytics.sql`) +- `analytics_snapshot` — stabiler Snapshot der Gesamt-Performance (jeder Rebuild = neue Zeile, chronologisch append). +- `strategy_performance` — Kennzahlen je Strategie+Version. +- `daily_performance` — Tageskennzahlen (Backtesting/Reporting). + +Diese fassen KEINE bestehenden Tabellen an (read-only auf `trade_journal`). + +## RabbitMQ +- **Queue** `analytics.input` (durable, topic `market.journal`, Routing `trade.recorded`), manuelles ACK, Reconnect mit Backoff (1→2→4→8→16s), kein Zombie. +- **Publiziert** optional `ANALYTICS_UPDATED` (topic `market.analytics`, Routing `analytics.updated`) — frische Verbindung je Publish (Bug-Fix-Pattern aus Modul-10), `conn.close()` nach jedem Publish. + +## Verifikation (20.08.2026) +| Test | Ergebnis | +|---|---| +| 1–7 Pure Metriken (Gewinn/Verlust, Winrate, PF, Expectancy, Max DD, Gruppierung, offene≠realisierte) | **OK** | +| 8 Idempotenz (identisches Event → keine Doppelzählung) | **OK** | +| 9 Rebuild aus DB → identische Kennzahlen | **OK** | +| 10 RabbitMQ-Reconnect (Backoff 4→8→16s, erneuter Connect, 1 Consumer) | **OK** | +| E2E 03→11 (Seed realisierter Trades, Portfolio/Strategie/Regime, DB + Idempotenz) | **OK** | + +- Health/Ready: `200` Port 55011 (PostgreSQL ✓, RabbitMQ ✓, Consumer ready ✓). +- Endzustand nach Freigabe: 2 echte OPEN-Trades im Journal, 0 realisierte → Snapshot zeigt 0/2. + +## Betriebshinweise +- Kein öffentlicher Port (nur `expose: 55011`). +- `restart: unless-stopped`. +- Compose-Pfad VPS: `/opt/trading-modules/docker-compose.yml`. +- Forgejo-Doku: `nexo312/trading-system-docs`, Commit-ID siehe unten. + +--- +*Geändert von: Rain Ocampo (Hermes)* +*Datum: 20.08.2026* — Modul-11-Analytics, Status FREIGABEGEBEN. diff --git a/notes/trading/system-docs/modul-12-backtesting.md b/notes/trading/system-docs/modul-12-backtesting.md new file mode 100644 index 0000000..a136478 --- /dev/null +++ b/notes/trading/system-docs/modul-12-backtesting.md @@ -0,0 +1,128 @@ +# Modul-12: Backtesting + +**Status: FREIGEGEBEN** · Container `Modul-12-Backtesting` · Image `backtesting:0.1.2` (V1 + V1.1 parallel) + +## Zweck +Deterministische, reproduzierbare **historische Strategietests** auf echten +OHLCV-Daten aus **Modul-03** (PostgreSQL). Gleiche Strategie-Logik wie +**Modul-05** (`trend_pullback_v1`) und identische Regime-Engine wie **Modul-04** — +der Backtest entscheidet exakt so wie Live/Paper. **V1 ohne KI/ML, ohne Live-Orders, +ohne Broker-Ausführung.** Modular erweiterbar für weitere Strategien (Breakout, ORB, +Mean-Reversion) über gemeinsame `Strategy`-Schnittstelle + Registry. + +## Architektur / Datenfluss + +``` +OHLCV (Modul-01 PostgreSQL, Tabelle ohlcv) + │ synchron, direkt nach Timestamp/Zeitraum (KEIN /history?limit) + ▼ +Modul-12-Backtesting ── POST /backtest + │ + ├── core/engine.py deterministische Backtest-Loop (kein Lookahead) + ├── strategies/ trend_pullback_v1 (1:1 zu Modul-05) + ├── regime/engine.py Regime-Engine (1:1 zu Modul-04) + ├── storage/storage.py Persistenz backtest_run/trade/equity + └── api/main.py REST-API (intern, Port 55012) + +API (Docker-intern, Port 55012): + /health, /health/ready + POST /backtest Backtest starten (synchron, returns Ergebnis) + GET /backtest/{run_id} Run-Metadaten + GET /backtest/{run_id}/status Status + run_hash/data_hash + GET /backtest/{run_id}/trades gespeicherte Trades + GET /backtest/{run_id}/equity Equity-Kurve + GET /backtest alle Runs + POST /backtest/{run_id}/rebuild (idempotenter Rebuild aus DB) + GET /strategies registrierte Strategien + Versionen +``` + +Kein RabbitMQ: Backtest ist ein synchroner, request/response-Aufruf. Port `55012` +ist **nur intern** (`expose:`), kein öffentliches Port-Mapping. + +## Datenquelle +- OHLCV direkt aus PostgreSQL (`ohlcv`), Spalten `provider, symbol, timeframe, ts, + open, high, low, close, volume`. +- Abfrage **nach Timestamp/Zeitraum** (`start_date`..`end_date`), nicht via + `/history?limit=N` — vollständige, deterministische Datenbasis. +- Mindestanzahl Candles: `min_candles_required` (default 220), sonst Fehler. + +## Bias-Schutz / Realismus (kein Lookahead) +- **Entry am OPEN der Folgewandle** nach dem Signal (nie zum bekannten Signal-Close). +- **Regime & Strategie** werden nur auf **abgeschlossenen** Candles (bis Candle i) + ausgewertet — keine zukünftige Information. +- **Intrabar Stop/Target konservativ**: Stop wird VOR Target geprüft (kein Lookahead + durch Intrabar-Reihenfolge). Falls beide in einer Candle getroffen, gewinnt Stop + (Worst-Case). +- **Gap-Handling**: Überspringt der Open den Stop/Target, Fill zum Gap-Open + (realistisch schlechter). +- **Gebühren/Spread/Slippage** werden auf Entry UND Exit angewendet (verschlechtern + den Fill). +- **R-Multiple** bezieht sich auf das **Geldrisiko** `qty * |entry − stop|` (nicht + Preisdifferenz) — `expectancy_r` daher korrekt (vorheriger Bug: 31431 → korrekt 2.0). + +## Eingaben (POST /backtest) +- Symbol, Asset-Klasse, Provider, Timeframe, Start-/Enddatum +- Strategy + Version (`trend_pullback_v1`, default `1.0.0`) +- Initiales Kapital, Risk % (Risk-basierte Positionsgröße) +- Gebühren (fix + %), Spread (Preispunkte), Slippage (Preispunkte) +- Optionale Limits: `max_positions`, `max_qty` + +## Eigene Tabellen (Migration `migrations/001_backtest.sql`) +- `backtest_run` — Run-Metadaten: run_id, strategy+version, symbol, timeframe, + status (COMPLETED/…), data_hash, run_hash, Parameter (JSON), Datenzeitraum. +- `backtest_trade` — einzelne Trades: direction (LONG/SHORT), qty, entry/exit-Preis, + entry/exit_ts, signal_ts, reason (target/stop/force_close), regime, gross_pnl, + fee, pnl, r (R-Multiple), strategy_version. +- `backtest_equity` — Equity-Kurve (Zeitstempel, Equity, offene Positionen). + +Jede Tabelle FK auf `backtest_run(run_id) ON DELETE CASCADE`. + +## Determinismus / Reproduzierbarkeit +- Eindeutige `run_id` + deterministischer `run_hash` + `data_hash`. +- `run_hash`: Hash über Strategy+Version+Parameter+Datenzeitraum+data_hash. +- `data_hash`: Hash über die geladenen OHLCV-Daten. +- **Gleiche Daten + gleiche Version + gleiche Parameter → identische Trades, Equity, + Kennzahlen und identischer run_hash** (E2E-verifiziert). + +## Tests +- `tests/test_backtest.py`: 12/12 grün (deterministisch, LONG/SHORT, fees, slippage, + lookahead, stop/target, gaps, unzureichende Daten, Strategy-Version/Parameter, + Rebuild-Identität). +- `tests/fixtures.py`: deterministische Fixture-Factory. +- `tests/m12_fixtures.py`: erzeugt E2E-Fixtures (BT_PULL LONG, BT_PULL_S SHORT) für + die VPS-ohlcv-Tabelle. +- `tests/determinism_check.py` / `determinism_diff.py`: E2E-Determinismus-Nachweis + (identische Trades/Equity/Kennzahlen/Hashes). + +## E2E-Verifikation (VPS, 20.08.2026) +- **LONG** (BT_PULL, TREND_UP): 1 Trade, winrate 1.0, expectancy_r 1.94, net_pnl + 196.59, max_drawdown 0.0001, r=1.94, reason=target. +- **SHORT** (BT_PULL_S, TREND_DOWN): 1 Trade, short_count 1, net_pnl 194.96. +- `signal_ts 15:00 → entry_ts 16:00` (Einstieg am Open der Folgewandle — Lookahead + praktisch verifiziert). +- run_hash + data_hash gesetzt (Beispiel: `bc6e2553…` / `d76a7549…`). +- **Determinismus**: zwei identische Backtests → identischer run_hash, data_hash, + Metrics, Trades (normalisiert), Equity-Curve. +- **Stop/Target/Gap**: Engine-`_evaluate_exit` geprüft — Gap-Down unter Stop → Fill + zum Gap-Open; Target bei high≥target; Stop vor Target (konservativ); SHORT Gap-Up + → Fill zum Gap-Open. +- Health `/health` 200, `/health/ready` 200; keine öffentlichen Ports. + +## Compose / Betrieb +- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55012 + nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping. + +## Nächste Module +- **Modul-13: Optimization** — Image `optimization:0.1.0`, E2E-verifiziert (12/12 Punkte), **FREIGEGEBEN** (20.08.2026). Details: `modul-13-optimization.md`. +- **V1.1-Strategie**: `trend_pullback_v1.1` (Image `backtesting:0.1.2`) — Spezifikation und Parameter: `trend-pullback-v1.1-spec.md`. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-12-Backtesting als FREIGEGEBEN markiert (deployt + E2E verifiziert: deterministisch, kein Lookahead, Stop/Target/Gap, LONG/SHORT, run_hash+data_hash). +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-12-Doku aktualisiert — Image auf backtesting:0.1.2 (V1.1), V1-Kompatmodus bitgenau, Referenz auf V1.1-Spec und Modul-13. diff --git a/notes/trading/system-docs/modul-13-optimization.md b/notes/trading/system-docs/modul-13-optimization.md new file mode 100644 index 0000000..09659fc --- /dev/null +++ b/notes/trading/system-docs/modul-13-optimization.md @@ -0,0 +1,129 @@ +# Modul-13: Optimization + +**Status: FREIGEGEBEN ✅** · Container `Modul-13-Optimization` · Image `optimization:0.1.0` + +## Zweck +Deterministische, reproduzierbare **Parameteroptimierung** für die V1.1-Strategie +(`trend_pullback_v1.1`, Modul-12). Findet robuste Parameter im erlaubten Suchraum +**ohne KI/LLM und ohne Live-Orders** — Ergebnis ist nur **CANDIDATE/RECOMMENDATION**, +es gibt keine automatische Parameterübernahme in Modul-05. + +## Kernprinzipien +- **Deterministisch**: gleiche Eingaben → gleiche Kandidaten, Reihenfolge, run_hash (kein Zufall außer explizit via Seed). +- **Kein Lookahead**: IS/OOS strikt zeitlich getrennt; **OOS wird nie zur Selektion verwendet**. +- **IS/OOS strikt**: `is_ratio` (z.B. 0.7) teilt die Daten; OOS ausschließlich zur Validierung. +- **Robuste Bereiche > Einzelmax**: best = Kandidat mit bester kombinierter Score (nicht max net_pnl). +- **Reject-/Overfit-Logik**: zu wenige Trades, DD-Limit, IS/OOS-Degradation → Kandidat wird `rejected`. +- **Parametergrenzen zwingend**: Suchraum begrenzt auf `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`. +- **Score-Gewichte**: Expectancy/R 0.30, PF 0.25, Max-Drawdown 0.20, Trades 0.10, Stabilität 0.15. +- **Synchron**: Request/Response über API (kein RabbitMQ). + +## Architektur / Datenfluss +``` +OHLCV (Modul-01 PostgreSQL, Tabelle ohlcv) + │ synchron, direkt nach Timestamp/Zeitraum + ▼ +Modul-13-Optimization ── POST /optimization + │ + ├── core/split.py IS/OOS + Walk-Forward-Folds (kein Leakage) + ├── core/optimizer.py Grid/Random-Suche, ScoreEngine, WF, Reject + ├── core/scoring.py Multi-Metrik-Score (Gewichte siehe oben) + ├── storage/storage.py Persistenz optimization_run/candidate/walk_forward + ├── backtest_client.py synchroner Aufruf Modul-12 (POST /backtest) + └── api/main.py REST-API (intern, Port 55013) +``` + +API (Docker-intern, Port 55013, kein öffentliches Port-Mapping): +``` +GET /health, /health/ready +POST /optimization Start (Grid/Random), synchron +GET /optimization Liste aller Runs +GET /optimization/{id} Run-Metadaten + Status +GET /optimization/{id}/candidates Kandidaten + Multi-Metrik-Ranking +GET /optimization/{id}/best bester ROBUSTER Kandidat (RECOMMENDATION/NO_RECOMMENDATION) +GET /optimization/{id}/walk-forward Walk-Forward-Folds +``` + +## Suchraum (V1.1 — NUR 3 optimierbare Parameter) +| Parameter | Suchraum (E2E) | Zweck | +|-----------|----------------|-------| +| `pullback_band_atr` | {0.2, 0.5, 0.8, 1.0} | ATR-Band um EMA20 (Pullback-Timing) | +| `trend_regime_window` | {60, 200, 300} | Fenster für strategie-internes Trend-Regime | +| `rr_multiplier` | {1.0, 2.0, 3.0} | Target = Entry + rr×Risiko | + +Fix (nicht optimiert): `atr_period=14`, `regime_vol_override=false`, `min_risk_reward=1.0`, +ADX-/EMA-Perioden (14/20/30), `adx_trend_threshold=20`. + +## Profile +| Profil | min_trades_is | min_trades_oos | max_drawdown_limit | max_oos_degradation | Zweck | +|--------|---------------|----------------|--------------------|--------------------|-------| +| **produktion** (Default) | 3 | 1 | 0.30 | 0.5 | Anti-Overfit, Produktion | +| **test** (`profile=test`) | 1 | 1 | — | — | nur Mindest-Trade-Schwellen gesenkt für technischen E2E; sonst identisch | + +Das TEST-PROFIL senkt **ausschließlich** die Mindest-Trade-Schwellen (damit ein +Sweep über die kalibrierte synthetische Fixture genügend Kandidaten akzeptiert). +Produktive Anti-Overfit-Defaults bleiben unverändert. `profile` wird per API-Feld +übergeben; explizite Request-Werte (`min_trades_is/oos`) haben Vorrang. + +## Eigene Tabellen (Migration) +- `optimization_run` — Run-Metadaten: optimization_id, strategy+version, symbol, timeframe, + optimizer_method, optimizer_version, param_space, seed, status, result (JSON), created/completed_at. +- `optimization_candidate` — Kandidaten: rank, params, backtest_run_id_is/oos, is_metrics, + oos_metrics, score, score_components, rejected, reject_reasons, flags, stability, robustness. +- `walk_forward_result` — WF-Folds: fold, params, is_start/end, oos_start/end, is/oos_metrics, + score, backtest_run_id_is/oos. + +## Reject-/Overfit- und Recommendation-Verhalten +- Kandidat wird `rejected` wenn: zu wenige Trades (min_trades_is/oos), Drawdown über Limit, + OOS-Degradation zu hoch (Overfit), fachlich unbrauchbare Metriken (z.B. PF<=0/kein Trades). +- `/best` liefert: + - `recommendation=RECOMMENDATION` + `best_candidate` wenn ein fachlich brauchbarer robuster Bester existiert. + - `recommendation=NO_RECOMMENDATION` (statt leerem `best_candidate`), wenn das Kandidatenfeld + fachlich unbrauchbar ist — eine produktive Empfehlung wird nie aus unbrauchbaren Daten erzeugt. + +## Tests +- `tests/test_optimization.py`: 12/12 grün (Grid deterministisch, Random-Seed, IS/OOS-Leak, + Walk-Forward korrekt + JSON-serialisierbar, Multi-Metrik-Ranking, Reject/Overfit, Robustheit, + Suchraum=3 Params, Reproduzierbarkeit, NO_RECOMMENDATION, TEST-PROFIL). + +## E2E-Verifikation (VPS, 20.08.2026) +- **Suchraum-Differenzierung** (kalibrierte Fixture `M13_E2E`, 1314 Bars, alle 3 Params unterscheidbar): + - `pullback_band_atr`: 0.2→8 Trades, 0.5→9–10, 0.8/1.0→1 Trade. + - `trend_regime_window`: bei pba=0.5: 60→9 Trades vs 200/300→10 Trades. + - `rr_multiplier`: 1.0→8–10 Trades (net +828…+1046), 2.0/3.0→1 Trade. +- **12/12 E2E-Punkte über API bestätigt** (e2e_full.py): + Grid deterministisch, Random-Seed identisch, IS/OOS-Leak ausgeschlossen, WF-Folds zeitlich + korrekt + DB-persistiert (3 Folds), Multi-Metrik-Ranking, Reject-Logik (36 Kandidaten, + 30 rejected / 6 accepted), best=robust + RECOMMENDATION, DB-Run/Candidates vollständig, + API list_runs, Reproduzierbarkeit (status COMPLETED), NO_RECOMMENDATION bei nur-1-Trade + Konfiguration (produktion-Profil). +- **Bugfix C**: Walk-Forward-Fold-Ergebnisse enthalten `datetime`-Timestamps → beim + `json.dumps` in `complete_run` nicht serialisierbar (422). Fix: ISO-String-Kopie im + Run-Result, datetime bleibt für `storage.insert_wf`. Verifiziert (3 WF-Folds in DB). +- Health `/health/ready` → `{"status":"ready","db":"ok"}` (M13 und M12). Keine Tracebacks. +- Port 55013 nur intern (`expose`), kein Host-Port-Binding (verifiziert). + +## Freigabe +- **20.08.2026: Modul-13-Optimization-Service vom Nutzer FREIGEGEBEN ✅** + (nach vollständiger E2E-Verifikation: 12/12 API-Punkte, Suchraum-Differenzierung für alle + 3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfixes A/B/C deployt, Tests 12/12 grün). +- Keine weiteren technischen Änderungen an Modul-13. + +## Compose / Betrieb +- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Backtest via + `Modul-12-Backtesting` (intern). Port 55013 nur intern (`expose`), `restart: unless-stopped`. + +## Nächste Module +- **Modul-14 ff.** — noch Platzhalter (alpine), NICHT gestartet. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-13-Optimization dokumentiert — E2E-verifiziert (12/12 Punkte), Suchraum-Diffusion für alle 3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfix C (WF-JSON-datetime). + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-13 vom Nutzer FREIGEGEBEN — Status auf FREIGEGEBEN gesetzt, Freigabe-Sektion ergänzt. Keine technischen Änderungen. +``` diff --git a/notes/trading/system-docs/modul-14-notification.md b/notes/trading/system-docs/modul-14-notification.md new file mode 100644 index 0000000..cb6d8a2 --- /dev/null +++ b/notes/trading/system-docs/modul-14-notification.md @@ -0,0 +1,142 @@ +# Modul-14: Notification-Service + +**Status: FREIGEGEBEN ✅** · Container `Modul-14-Notification` · Image `notification:0.1.0` + +## Zweck +Zentrale **Benachrichtigungen für Trading- und Systemereignisse**. Konsumiert relevante +Events aus den RabbitMQ-Exchanges der Module 05–10 und liefert kompakte, strukturierte +Meldungen — **V1 primär über Telegram** (`TelegramProvider`), provider-neutral angelegt. + +**Restriktionen (Nutzer-Vorgabe):** +- **keine KI/ML**, **keine Trading-Entscheidungen**, **keine Orders**. +- **FAIL-SAFE:** Ein Notification-Ausfall darf die Trading-Pipeline **NIEMALS blockieren**. + +## Architektur / Datenfluss +``` +RabbitMQ (market.signals/.rankings/.risk/.portfolio/.execution/.journal) + │ NotificationConsumer (notification.input) + ▼ +Modul-14-Notification ── NotificationService ──> RulesEngine + DedupGuard + │ │ Spam-Schutz + ├── providers/ NotificationProvider-ABC → Telegram / Fake + ├── rules/ RulesEngine (Regeln) + DedupGuard (Dedup/Cooldown/Rate-Limit) + ├── storage/ Persistenz notification_log (PostgreSQL, Modul-01) + ├── core/ formatter (kompakte Nachricht) + service (Retry/Backoff) + └── api/main.py REST-API (intern, Port 55014) + +Event → RabbitMQ → Modul-14 → notification_log → Provider (Telegram) → Chat +``` + +## API (Docker-intern, Port 55014, kein öffentliches Port-Mapping) +``` +GET /health liveness +GET /health/ready readiness (postgresql, consumer_ready, provider_configured) +GET /notifications/recent letzte Notifications +GET /notifications/{id} einzelne Notification +POST /notifications/test Test-Sendung (nur konfigurierte Destinationen) +``` + +## Konsumierte Events & Routing-Keys +| Event-Typ | Exchange | Routing-Key | Default-Severity | Default aktiv | +|-----------|----------|-------------|------------------|---------------| +| `SIGNAL_DETECTED` | `market.signals` | `strategy.signal.detected` | INFO | ❌ (deaktiviert) | +| `SIGNAL_RANKED` | `market.rankings` | `signal.ranked` | INFO | ❌ (deaktiviert) | +| `RISK_APPROVED` | `market.risk` | `risk.approved` | INFO | ❌ (deaktiviert) | +| `RISK_REJECTED` | `market.risk` | `risk.rejected` | WARNING | ❌ (deaktiviert) | +| `TRADE_APPROVED` | `market.portfolio` | `trade.approved` | INFO | ✅ | +| `PORTFOLIO_REJECTED` | `market.portfolio` | `portfolio.rejected` | WARNING | ✅ | +| `ORDER_SUBMITTED` | `market.execution` | `order.submitted` | INFO | ❌ (deaktiviert) | +| `ORDER_FILLED` | `market.execution` | `order.filled` | INFO | ✅ | +| `ORDER_REJECTED` | `market.execution` | `order.rejected` | CRITICAL | ✅ | +| `ORDER_FAILED` | `market.execution` | `order.failed` | CRITICAL | ✅ | +| `TRADE_RECORDED` | `market.journal` | `trade.recorded` | INFO | ✅ | + +Die Regeln sind **pro Event-Typ konfigurierbar** (aktiv/inaktiv, Severity, Provider, +Destination, Cooldown, Rate-Limit). **Unbekannte Event-Typen werden sicher ignoriert** +(kein Crash). V1 sind die rauschigen Events (SIGNAL_*, RISK_*, ORDER_SUBMITTED) +standardmäßig deaktiviert. + +## Severity-Hierarchie +``` +INFO < WARNING < CRITICAL +``` +Regel-Severity überschreibt den Event-Standard. V1: alle über Telegram. + +## Spam-Schutz (DedupGuard) +Mehrstufig, thread-sicher (Lock): +1. **Idempotenz** — gleiche `event_id`/`source_event_id` erzeugt nie eine zweite Notification (DB-unique über `source_event_id` + `event_processed`-Check). +2. **Dedup** — identische Meldungen (gleicher Text/Event-Typ) in kurzem Fenster werden zusammengefasst. +3. **Cooldown** — pro Event-Typ ein Mindestabstand (`cooldown_seconds`, Default 60s). +4. **Rate-Limit** — maximale Notifications pro Minute (`rate_limit_per_minute`, Default 30). +5. **Burst-Schutz** — Ansturm vieler Events wird aufs Limit begrenzt. + +## Retry / Dead (FAIL-SAFE) +- Provider nicht erreichbar → `RETRY_PENDING` mit **begrenzten Retries** (exponentielles Backoff). +- `max_attempts` (Default 5) erreicht → **DEAD** (keine Endlosschleife). +- **Provider nicht konfiguriert** (fehlender Token/Chat-ID) → **sofort DEAD** mit + `error_code=NOT_CONFIGURED` (keine sinnlosen Retries). +- Nach Provider-Recovery werden **neue Events wieder normal gesendet** (Retry betrifft nur die jeweilige Notification). + +## Eigene Tabelle (Migration) +`notification_log`: +`notification_id` (PK), `source_event_id`, `event_type`, `severity`, `provider`, +`destination`, `status` (`SENT`/`FAILED`/`RETRY_PENDING`/`DEAD`), `attempts`, +`created_at`, `sent_at`, `error_code`, `error_message`. + +## Sicherheit +- **KEINE Secrets in DB/Logs/Doku/Commits.** Telegram-Bot-Token/Chat-ID ausschließlich + über Environment (Compose `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`), nie hart kodiert. +- `/notifications/test` sendet **nur an konfigurierte Destinationen**. + +## Tests +- `tests/test_notification.py`: **14/14 grün** (Event→Notification, Idempotenz, Dedup, + Cooldown, Rate-Limit, Telegram-Ausfall blockiert nicht, Retry+Backoff, Max→DEAD, + keine Secrets, unbekannter Typ ignoriert, Severity/Regeln, FakeProvider, Reconnect). + +## E2E-Verifikation (VPS, 20.08.2026) — alle 7 Punkte grün +1. **Fake-Telegram-E2E** (Modul-14 auf `http://fake-telegram:55000`, Test-Token/Chat-ID + nur als ENV): echtes `TRADE_RECORDED` publiziert → **Event → RabbitMQ → Modul-14 → + notification_log → Fake-Telegram** komplett durchlaufen. DB: `status=SENT`, + `sent_at` gesetzt, `attempts=0`, **genau 1 Zustellung, kein Duplikat**. Fake-Telegram + RAW: `{"chat_id": "E2E_TEST_CHAT_114", "text": "TRADE RECORDED | NVDA | LONG | OPEN"}`. +2. **NO_CONFIG-Fix**: Container ohne Token → Event → `status=DEAD, attempts=1, + error_code=NOT_CONFIGURED` (sofortiges DEAD, keine 5 sinnlosen Retries). +3. **Fehlerfall-E2E**: Fake-Telegram gestoppt → Notification `RETRY_PENDING` mit + Backoff (attempts 2→4, `NETWORK_ERROR`), **Trading-Pipeline unbeeinflusst**, + Max-Retries (`attempts=5`) → `DEAD`. Nach Fake-Telegram-Recovery → neues Event + wieder `SENT`. +4. **RabbitMQ-Reconnect**: kontrollierter Restart → Modul-14 reconnectet automatisch + (Backoff 4s→8s→16s, „Consumer verbunden"), **genau 1 Consumer** an `notification.input`, + keine Zombies, keine verlorenen/duplizierten Notifications (nach Reconnect: 1 Zustellung, 0 Duplikate). +5. **Spam-/Sicherheitschecks** (praktisch): unbekannter Typ `UNKNOWN_EVENT_XYZ` → ignoriert; + gleiche `event_id` 2× → nur 1 Notification (Idempotenz); gleicher Event-Typ 2× + → Cooldown greift; deaktivierter `SIGNAL_DETECTED` → keine Notification. **Keine + Tokens/Chat-IDs in Logs/API/Doku/DB** (0 Token-Leaks). +6. **Health/Readiness**: `/health` + `/health/ready` → 200 + (`postgresql=true, consumer_ready=true, provider_configured=…`). Keine + Applikations-Tracebacks im Normalbetrieb (nur erwartete Reconnect-Logs beim Test). + Port 55014 nur intern (`expose`, kein Host-Port). +7. **Forgejo-Doku + Commit-ID** (siehe Freigabe unten), temporäre Test-Credentials + entfernt, Remote token-/key-frei. + +## Freigabe +- **20.08.2026: Modul-14-Notification-Service vom Nutzer FREIGEGEBEN ✅** + (nach vollständiger E2E-Verifikation der Punkte 1–7, alle grün). +- Keine weiteren technischen Änderungen an Modul-14. + +## Compose / Betrieb +- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`. +- Port 55014 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001). +- Token/Chat-ID via ENV (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_API_BASE`). + +## Nächste Module +- **Modul-15 ff.** — noch Platzhalter (alpine), NICHT gestartet. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-14-Notification-Service dokumentiert — E2E-verifiziert (Punkte 1–7 grün), + API/Events/Severity/Retry-Dead/Spam-Schutz/Fake-E2E dokumentiert, Status FREIGEGEBEN. +``` diff --git a/notes/trading/system-docs/modul-15-m09-anbindung-design.md b/notes/trading/system-docs/modul-15-m09-anbindung-design.md new file mode 100644 index 0000000..e7fb2f2 --- /dev/null +++ b/notes/trading/system-docs/modul-15-m09-anbindung-design.md @@ -0,0 +1,331 @@ +# 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). |In Praxis Orders selten → akzeptabel. | +| **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):** +1. **Final-Check unmittelbar vor `submit_order`** — minimales Zeitfenster (ms) zwischen Check und Send. +2. **TTL auf der HTTP-Antwort:** `expires_at` (z. B. `now + 500 ms`). Ist die Antwort beim Senden abgelaufen + (Fenster überschritten), NICHT senden → Order nach `FAILED/REJECTED` mit `CONTROL_STATE_STALE` markieren. +3. **`state_version`** (Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check + gesehene Version. Beim Broker-Submit trägt die Order die `control_version`; die Audit-Kette verbindet + genau die Version, die gegolten hat, mit dem Versand. +4. **Bestätigtes Einverständnis:** Der Check liefert `trading_status=ENABLED` **und** `state_version` **und** + `expires_at`. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen. +5. **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):** +```json +{ + "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 `UNKNOWN` behandeln. +- 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). + +```json +{ + "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 als `UNKNOWN/veraltet` → OPEN/INCREASE + **blockiert** (auch wenn letztes Event `ENABLED` war). +- 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=ENABLED` und neuer `state_version`** oder + ein **HTTP-Final-Check mit `ENABLED`** vorliegt, 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 hat +- `control_expires_at` / `control_ttl`: TTL-Horizont +- `control_audit_id`: vom Final-Check zugeordnet +- `control_check_source`: `final_http` (und optional `cache`) + +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`/`REJECTED` mit `CONTROL_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`/`CANCEL` senken/eliminieren Risiko → **immer erlaubt**, unabhängig von M15-Status. + +--- + +## 12. Offene Punkte für die Nutzer-Freigabe + +1. **API-Vertrag erweitern** (`state_version`, `expires_at`, `ttl_seconds`, `audit_id`) in M15 (additiv). +2. **M09-Check-Position:** zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen. +3. **TTL-Wert** (`expires_at`-Fenster) festschreiben (Vorschlag 500 ms). +4. **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). +5. **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. +6. **SPOF-Akzeptanz:** M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei. +7. 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.* diff --git a/notes/trading/system-docs/modul-15-m09-anbindung-implementierung.md b/notes/trading/system-docs/modul-15-m09-anbindung-implementierung.md new file mode 100644 index 0000000..4da5c51 --- /dev/null +++ b/notes/trading/system-docs/modul-15-m09-anbindung-implementierung.md @@ -0,0 +1,151 @@ +# Modul-15 → Modul-09 Control-Anbindung — Implementierung & Live-E2E + +**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung). +**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes) +**Gilt für:** Modul-09-Execution-Service ↔ Modul-15-Monitoring-Control + +> **Freigabe-Hinweis:** Diese Doku dokumentiert den Implementierungs- und E2E-Stand. +> Die **finale Freigabe** (M15/M09 als FREIGEGEBEN markieren) erfolgte am **20.08.2026** +> durch Nutzer-Bestätigung. M16 wird **nicht** begonnen. + +--- + +## 1. Umgesetzte Architektur (Variante C) + +**Event-Cache als nicht-kritischer Vorfilter + synchroner HTTP-Final-Check als Autorität.** + +- **M15** = autoritative Safety-/Trading-Control-Instanz. +- **M09** sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt. +- **REDUCE/CLOSE/CANCEL** (Risikoabbau) sind **immer** erlaubt — unabhängig vom M15-Status. + +### Kernregel +| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL | +|-------|---------------|---------------------| +| `ENABLED` | **ERLAUBT** | erlaubt | +| `PAUSED` | **VERBOTEN** | erlaubt | +| `HALTED` | **VERBOTEN** | erlaubt | +| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt | + +--- + +## 2. API-Vertrag M15 → M09 (additiv, kein Breaking Change) + +### `GET /control/trading-state` +```json +{ + "trading_status": "ENABLED", // ENABLED|PAUSED|HALTED|UNKNOWN + "system_status": "HEALTHY", + "state_version": 3, // monoton, je Status-Änderung inkrementiert + "timestamp": "2026-08-20T15:30:00Z", + "expires_at": "2026-08-20T15:30:00.5Z", // TTL-Horizont (now + 500ms) + "ttl_seconds": 0.5, + "audit_id": "ctl-..." // M15-interne Audit-Kennung +} +``` +Nur `trading_status=ENABLED` **und** `expires_at` in der Zukunft = eindeutige Erlaubnis. +Timeout/5xx/503 → **FAIL-CLOSED** (als UNKNOWN behandeln). + +### Event-Kanal (Event-Cache) +- Exchange `market.control`, Routing-Key `trading.state` (nur bei Status-Änderung). +- M09-Consumer `ControlCacheConsumer` speist den Event-Cache-Vorfilter. +- Cache-TTL: 5 s. Stale-Cache → als UNKNOWN behandeln → OPEN/INCREASE blockiert. + +--- + +## 3. M09-Implementierung + +### Neue Dateien +- `app/control/client.py` — **ControlClient** (Event-Cache-Vorfilter + HTTP-Final-Check, FAIL-CLOSED) +- `app/control/__init__.py` +- `app/consumer/control_consumer.py` — **ControlCacheConsumer** (`market.control`/`trading.state`) + +### Geänderte Dateien +- `app/core/service.py` — Control-Check zwischen Schritt 4 (Order anlegen) und Schritt 5 (`_submit_and_track`); `_apply_control_audit`; `action_type`-Ableitung; Consumer-Start/Stop +- `app/core/models.py` — `ExecutionOrder` + Audit-Felder +- `app/config.py` — M15-URL/Timeout/TTL/Cache-TTL + Control-Exchange/Routing-Key +- `app/storage/storage.py` — INSERT/UPDATE um control_*-Felder; `update_control_audit` +- `migrations/001_execution.sql` — Audit-Spalten (idempotent via `ADD COLUMN IF NOT EXISTS`) + +### Audit-Felder je Order +`action_type`, `control_status`, `control_state_version`, `control_expires_at`, +`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`). + +### Parameter +- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort. +- `expires_at`-TTL (M15): **500 ms**. +- Cache-TTL (M09): **5 s**. +- Kein Auto-Resume; kein "Senden ohne Check". + +--- + +## 4. Live-E2E-Ergebnisse (VPS, 20.08.2026) + +Alle Szenarien auf dem VPS (187.124.31.123) live ausgeführt. **Alle grün.** + +| # | Szenario | Ergebnis | +|---|----------|----------| +| S1 | **ENABLED-Pfad**: OPEN-Order durch, Auditfelder in DB | **9/9 PASS** | +| S2 | **HALTED-Pfad**: M03 gestoppt → OPEN blockiert, kein Broker-Send, reason `CONTROL_HALTED` | **9/9 PASS** | +| S3 | **PAUSED-Pfad**: nicht-krit. Service down → OPEN blockiert, reason `CONTROL_PAUSED` | **4/4 PASS** | +| S4 | **Risikoabbau bei HALTED**: REDUCE/CLOSE/CANCEL erlaubt (bypass) | **4/4 PASS** | +| S5 | **M15-down**: OPEN FAIL-CLOSED (`CONTROL_UNREACHABLE`), REDUCE/CLOSE/CANCEL erlaubt | **7/7 PASS** | +| S6 | **Cache-vs-Final-Check**: HTTP HALTED gewinnt über Cache | **2/2 PASS** | +| S7 | **Race/TTL**: Doppel-Publish → keine Doppelorder (Idempotenz) | **2/2 PASS** | +| S8 | **Event-Cache**: 1 Consumer, 0 Backlog, Reconnect nach RabbitMQ-Restart | **verifiziert** | +| S9 | **Recovery**: nach Behebung wieder ENABLED, OPEN erlaubt | **3/3 PASS** | +| S10 | **Health/Security**: health/ready 200, keine öffentl. Ports, keine Secrets, keine echten Tracebacks | **verifiziert** | +| S11 | **Regression**: bestehender M09-Paper-E2E (Kette 03→09) | **ALLE CHECKS BESTANDEN** | + +### S1-Detail (ENABLED) +``` +control_status=ENABLED +control_state_version=1 +control_expires_at=1787241066.98 +control_audit_id=ctl-2073194e651040db +control_check_source=http +action_type=OPEN +broker_order_id=PAPER-EXEC-... (Broker-Send erfolgt) +status=FILLED +``` + +### S2-Detail (HALTED) +``` +control_status=HALTED +control_state_version=2 +control_audit_id=ctl-b2cd6303f8754328 +control_check_source=http +action_type=OPEN +broker_order_id=None (KEIN Broker-Send) +reason_codes=['CONTROL_HALTED'] +status=FAILED +``` + +### S5-Detail (M15-down) +``` +reason_codes=['CONTROL_UNREACHABLE'] +broker_order_id=None (FAIL-CLOSED, kein Broker-Send) +REDUCE/CLOSE/CANCEL → control_check_source=bypass (erlaubt) +``` + +--- + +## 5. Verifikation Health/Security + +- M09 + M15 `health` und `health/ready` → **200**. +- Keine öffentlichen Ports (nur Docker-intern via `expose:`). +- Keine Secrets in Logs/API/DB. +- Tracebacks in Logs: ausschließlich pika `AMQPConnectionError` während des + kontrollierten RabbitMQ-Restarts (erwartete Reconnect-Logs, keine echten Fehler). + +--- + +## 6. Freigabe-Status + +- **FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung). +- M15/M09-Control-Anbindung ist **final freigegeben** — keine weiteren technischen Änderungen an M09/M15. +- M16: **nicht beginnen**. +- Credential-Cleanup: temporäre Forgejo-Tokens/Deploy-Keys → count=0 (siehe Git-Commit). + +--- + +*Implementierung + Live-E2E abgeschlossen. Finale Freigabe erteilt (20.08.2026).* diff --git a/notes/trading/system-docs/modul-15-monitoring-control.md b/notes/trading/system-docs/modul-15-monitoring-control.md new file mode 100644 index 0000000..1d4bd57 --- /dev/null +++ b/notes/trading/system-docs/modul-15-monitoring-control.md @@ -0,0 +1,146 @@ +# Modul-15: Monitoring-Control + +**Status: FREIGEGEBEN** (20.08.2026) · Container `Modul-15-Monitoring-Control` · Image `monitoring-control:0.1.0` + +## Zweck +Zentrale **technische Überwachung und Sicherheits-/Kontrollebene** des Trading-Systems. +Überwacht die Dienste M03–M14 (Health/Readiness/Erreichbarkeit) plus RabbitMQ-Queues/Consumer +und PostgreSQL und leitet daraus deterministisch einen **globalen Trading-Status** +(`ENABLED` / `PAUSED` / `HALTED` / `UNKNOWN`) ab. + +**Restriktionen (Nutzer-Vorgabe):** +- **Keine KI/ML**, **keine Trading-Entscheidungen**, **keine Brokerorders**, **keine Shell-/Docker-Control-Rechte**. +- **FAIL-CLOSED:** Bei unklarem/kritischem Zustand wird Trading sicher blockiert (`HALTED`). +- M15 unerreichbar → M09 behandelt den Zustand als `HALTED` (FAIL-CLOSED, siehe M15→M09-Anbindung, FREIGEGEBEN). +- **Keine Event-Schleifen:** Events nur bei Status-Änderung, dedupliziert. +- Container wird **nicht** automatisch neu gestartet/gekillt — M15 beobachtet und entscheidet Status. +- Port 55015 **nur Docker-intern** (`expose`, kein Host-Port). + +## Architektur / Datenfluss +``` +M03–M14 (health/ready) RabbitMQ (Queues) PostgreSQL + │ │ │ + └──────────┬────────────┴──────────────┘ + ▼ + Modul-15-Monitoring-Control + ├── probe/ MonitoringProbe (HTTP-health + RMQ passive declare + PG) + ├── rules/ MonitoringRules (deterministisch, FAIL-CLOSED, versioniert) + ├── core/ MonitorService (Status-Engine, Single Source of Truth) + ├── storage/ Persistenz (PostgreSQL: system_health/service_health/control_event) + ├── publisher/ ControlPublisher (market.control, nur bei Status-Änderung) + └── api/main.py REST-API (intern, Port 55015) +``` +Status → `system_health` (append), Services → `service_health` (upsert), Änderungen → `control_event` (append-only Audit). + +## API (Docker-intern, Port 55015, kein öffentliches Port-Mapping) +``` +GET /health liveness +GET /health/ready readiness (postgresql) +GET /monitoring/status aktueller System-Status + Trading-Status (internes State-Modell) +GET /monitoring/services aktueller Zustand aller überwachten Services/Queues +GET /control/status Control-Status (manual_override, control_api_enabled) +GET /control/trading-state autoritativer Trading-Status für M09 +POST /control manuelle Control PAUSE/HALT/RESUME (nur wenn CONTROL_API_ENABLED=true) +``` + +## Deterministische Regeln (Rules-Engine, Version 0.1.0) +Präzedenz strikt von oben nach unten (FAIL-CLOSED): +1. **UNKNOWN** — PostgreSQL nicht erreichbar → nicht verlässlich → `HALTED`. +2. **UNHEALTHY/HALTED** — kritische Infrastruktur (PG/RabbitMQ) oder Execution-Pfad down. +3. **DEGRADED/PAUSED** — nicht-kritische Dienste down / stale. +4. **HEALTHY/ENABLED** — alles gut. + +| Bedingung | System | Trading | Reason-Code | +|---|---|---|---| +| PostgreSQL nicht erreichbar | UNHEALTHY | HALTED | `POSTGRESQL_UNREACHABLE` (+ `PERSISTENCE_FAILURE`) | +| RabbitMQ nicht erreichbar | UNHEALTHY | HALTED | `RABBITMQ_UNREACHABLE` | +| Kritischer Service down (M03–M09) | UNHEALTHY | HALTED | `CRITICAL_SERVICE_DOWN:` | +| Market Data stale (> max_age) | UNHEALTHY | HALTED | `MARKET_DATA_STALE` | +| Kritische Queue Consumer fehlt | — | HALTED | `NO_CONSUMER_` | +| Kritischer Queue-Backlog ≥1000 | — | HALTED | `QUEUE_BACKLOG_CRIT::` | +| Nicht-kritische Unhealthy (Analytics/Notification) | DEGRADED | PAUSED | `CRITICAL_SERVICE_DOWN`/`NO_CONSUMER_…` | +| Queue-Backlog ≥100 / Warning | DEGRADED | PAUSED | `QUEUE_BACKLOG::` | +| Alles healthy | HEALTHY | ENABLED | — | + +**FAIL-CLOSED-Hinweis:** `UNKNOWN` Trading-Status wird in der API auf `HALTED` gemappt (nie ENABLED aus unklarem Zustand). + +## Events (market.control Exchange, keine Event-Schleife) +Events werden **nur bei Status-Änderung** publiziert (Idempotenz) — nie bei jedem Poll: + +| Event-Typ | Routing-Key | Anlass | +|---|---|---| +| `system.degraded` | `system.degraded` | System → DEGRADED | +| `system.halted` | `system.halted` | System → UNHEALTHY (HALT) | +| `system.recovered` | `system.recovered` | System → HEALTHY | +| `trading.state` | `trading.state` | Trading-Status-Änderung | +| `service.unhealthy` | `service.unhealthy` | Einzelner Service UNHEALTHY | + +## Persistenz / Audit (PostgreSQL Modul-01) +- `system_health` — append-only Zeile pro Beobachtung (aktueller Status + reason_codes). +- `service_health` — Upsert pro `service_id` (aktueller Zustand jedes Services/Queue). +- `control_event` — **append-only Audit-Trail** jeder Status-/Trading-Änderung (`STATE_CHANGE`) und manuellen Control-Aktion (`MANUAL_*`). Idempotent — eine Änderung wird **genau einmal** auditiert. + +## Manuelle Control (PAUSE/HALT/RESUME) +- **Nur wenn `CONTROL_API_ENABLED=true`** (Default `false` → manuelle Control über API deaktiviert, nur Regel-getrieben). +- **RESUME wird verweigert** (`RESUME_BLOCKED_CRITICAL_CAUSE`), solange eine kritische Ursache besteht. +- Jede manuelle Aktion wird append-only in `control_event` auditiert. + +## Sicherheit +- **Keine Secrets in DB/Logs/Doku/Commits** — Zugangsdaten ausschließlich via Environment (Compose). +- **Kein Docker-Socket**, **keine Shell-/Docker-Control-Rechte** (CMD=`python main.py`). +- Port 55015 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001). +- Env: `POSTGRES_*`, `RABBITMQ_*`, `CONTROL_API_ENABLED`. + +## Compose / Betrieb +- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`. +- Env per Compose, Zugangsdaten nur via ENV (`POSTGRES_*`, `RABBITMQ_*`), nie hart kodiert. + +## Unit-Tests +- `tests/test_monitoring.py`: **19/19 grün** — deterministische Rules, FAIL-CLOSED (PG/RabbitMQ/Critical), stale, Queue-Consumer, Idempotenz, Audit genau einmal, Recovery, Reconnect, keine Secrets, `state_version`-Inkrementierung. + +## E2E-Verifikation (VPS, 20.08.2026) — 17 Szenarien +Alle 17 verifiziert (System-/Trading-Status via Loop, API `/control/trading-state` + `/monitoring/status` konsistent): +1. **Normalzustand:** `HEALTHY/ENABLED` — API, State-Modell, Loop identisch. +2. **PG-Ausfall:** `HALTED`, `POSTGRESQL_UNREACHABLE`+`PERSISTENCE_FAILURE`, Audit genau einmal. +3. **RabbitMQ-Ausfall:** `HALTED`, `RABBITMQ_UNREACHABLE`. +4. **Risk-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-07-risk-manager`+`NO_CONSUMER_risk.input`. +5. **Portfolio-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-08-portfolio-manager`. +6. **Execution-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-09-execution-service`. +7. **Analytics-Ausfall:** `DEGRADED` + `PAUSED` (Trading nicht vollständig kill). +8. **Notification-Ausfall:** `DEGRADED` + `PAUSED`. +9. **Market Data stale:** `HALTED`, `MARKET_DATA_STALE`; Recovery nach frischen Daten. +10. **Kritischer Consumer fehlt:** HALT-Severity korrekt (NO_CONSUMER auf kritischer Queue). +11. **Queue-Backlog:** Warning/HALT gemäß Schwelle (100/1000, Unit-getestet). +12. **Recovery:** Ursache erneut geprüft, kein Auto-RESUME solange Ursache, Status konsistent, kein Event-Spam. +13. **Manuelle Controls:** mit `CONTROL_API_ENABLED=false` sauber abgelehnt (`control_api_disabled`); Trading unangetastet. +14. **Idempotenz/Event-Flut:** stabiler Zustand → keine wiederholten SYSTEM_HALTED/SERVICE_UNHEALTHY (nur 1 Audit je Änderung). +15. **RabbitMQ-Reconnect:** automatisch (alle Recovery-Zyklen), keine Zombies, keine Eventverluste. +16. **Audit:** `control_event`/`system_health`/`service_health` vollständig + zeitlich nachvollziehbar. +17. **Security:** kein öffentlicher Port (PortBindings `{}`), kein Docker-Socket, keine Secrets im Compose/Logs. + +## Bekannte Design-Hinweise (für separaten M15→M09-Vorschlag) +- **M03–M06 (Market-Data-Pipeline) sind in `CRITICAL_SERVICES`** klassifiziert. Ausfall eines einzelnen Daten-Moduls führt so zu `HALTED` (über `CRITICAL_SERVICE_DOWN`), nicht ausschließlich über `MARKET_DATA_STALE`. Das ist fachlich konservativ aber zu klären (nur Data-Pipeline vs. Execution-Pfad). +- **DEGRADED → `PAUSED`** (nicht `ENABLED`): bei Analytics/Notification-Ausfall wird Trading vorsichtig gestoppt, nicht vollständig killed. Design-Entscheidung, im Vorschlag benannt. +- **PG-Ausfall ⇒ `PERSISTENCE_FAILURE`**: ohne PG kann `control_event`-Audit nicht geschrieben werden (fachlich korrekt FAIL-CLOSED), aber Audit-Lücke im Fehlerfenster. + +## Freigabe +- **FREIGEGEBEN** (20.08.2026, Nutzer-Bestätigung). Implementierung + E2E (17 Szenarien) grün. +- M09-Anbindung **umgesetzt** (Variante C, Event-Cache + HTTP-Final-Check) — siehe `modul-15-m09-anbindung-implementierung.md`, FREIGEGEBEN. + +## Nächste Module +- **Modul-16 ff.** — noch Platzhalter (alpine), NICHT gestartet. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-15-Monitoring-Control dokumentiert — implementiert + E2E-verifiziert (17 Szenarien grün), + Rules/API/Events/Audit/Security dokumentiert, NICHT FREIGEGEBEN. +``` +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-15-Monitoring-Control auf FREIGEGEBEN gesetzt (Nutzer-Bestätigung 20.08.2026). + M15→M09-Anbindung (Variante C) als umgesetzt/FREIGEGEBEN vermerkt; Unit-Tests 19/19. +``` diff --git a/notes/trading/system-docs/ports-reference.md b/notes/trading/system-docs/ports-reference.md new file mode 100644 index 0000000..2052a06 --- /dev/null +++ b/notes/trading/system-docs/ports-reference.md @@ -0,0 +1,59 @@ +# Ports & Service-Referenz — Trading-System VPS + +> Stand: 20.08.2026 · VPS: `187.124.31.123` · SSH: Public-Key-Auth (nur) + +## Netzwerk & Core-Infrastruktur +- **Netzwerk:** `trading-modules` (bridge) — Kommunikation über Docker-interne Service-Hostnamen, keine festen IPs. +- **Host:** Hostinger-VPS, 300 GB Disk, ~31 GiB RAM. + +## Feste Port-Zuordnung (Schema `55NNN` nach Modulnummer) +| Modul | Container | Service | Host-Port | Öffentlich? | +|-------|-----------|---------|-----------|-------------| +| 01 | Modul-01-PostgreSQL | PostgreSQL | **keiner (intern, `expose`)** | **nein** (nur intern) | +| 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) | +| 02 | Modul-02-RabbitMQ | RabbitMQ Management | **keiner (intern, `expose`)** | **nein** (nur intern) | +| 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) | +| 04 | Modul-04-Market-Regime | FastAPI | 55004 (intern, expose) | nein (nur intern) | +| 05 | Modul-05-Strategy-Engine | FastAPI | 55005 (intern, expose) | nein (nur intern) | +| 06–12 | Modul-06…12 | FastAPI | 55006–55012 (intern, expose) | nein (nur intern) | +| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) | +| 14–17 | … | — | 55014–55017 | (Platzhalter) | + +> **SECURITY-FIX 20.08.2026:** Modul-01 (PostgreSQL) und Modul-02 (RabbitMQ) haben **keine Host-Port-Bindings** mehr. Die ehemaligen öffentlichen Ports **55432, 55672, 15672 sind geschlossen** und von außen nicht mehr erreichbar (verifiziert). Adminzugriff nur noch per SSH-Tunnel ins `trading-modules`-Netz oder `docker exec`. Alles läuft über `expose:` → nur im internen Docker-Netzwerk. + +## Weitere Dienste (Host) +| Dienst | Container | Port | +|--------|-----------|------| +| Forgejo | forgejo-c4u8yyi1eaz1gepn3pqmr5fb | 3000 (HTTP) / 22222 (SSH) | +| Tolaria (Second Brain) | tolaria | 5173 | +| OpenClaw Alice | openclaw-suqw-openclaw-1 | 55163 | +| OpenClaw Matt | openclaw-3sgu-openclaw-1 | 54524 | +| Hermes Rain | hermes-workspace-e6un… | 32776 (UI) | +| Ollama | ollama-nb6d-ollama-1 | 11434 (intern) | +| n8n | n8n | (Coolify-managed) | +| Traefik | traefik | 80/443 | + +## Docker-interne Hostnamen (wichtig für Container-Kommunikation) +- PostgreSQL: `Modul-01-PostgreSQL:5432` +- RabbitMQ: `Modul-02-RabbitMQ:5672` (vhost `trading`) +- Ollama: `ollama-nb6d-ollama-1:11434` + +## Sicherheits-Notizen +- **Kein Modul-01/02/03-Port ist öffentlich** — alle nur im Docker-Netzwerk erreichbar (via `expose`, nicht `ports`). Verifiziert: `curl` auf 55432/55672/15672/55003 schlägt von außen fehl, während Modul-03 intern weiterhin PostgreSQL & RabbitMQ erreicht. +- Zugangsdaten ausschließlich als Env-Variablen/Secrets, nie im Code. +- Öffentliche Ports nur, wo nötig (Admin/Debug/UI). + +--- +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: Modul-01/02 von öffentlichen Ports auf interne `expose`-Bindings umgestellt (Security-Fix), Doku aktualisiert. +``` + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: ports-reference aktualisiert — Modul-04 und Modul-05 von Platzhalter auf real (FastAPI, intern expose, nicht öffentlich) eingetragen. + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: ports-reference aktualisiert — Modul-06–12 als real (FastAPI, intern) und Modul-13 Optimization (55013 intern) eingetragen. diff --git a/notes/trading/system-docs/trend-pullback-v1.1-spec.md b/notes/trading/system-docs/trend-pullback-v1.1-spec.md new file mode 100644 index 0000000..269b885 --- /dev/null +++ b/notes/trading/system-docs/trend-pullback-v1.1-spec.md @@ -0,0 +1,115 @@ +# trend_pullback_v1.1 — Spezifikation + +> Status: **IMPLEMENTIERT + DEPLOYT** (`backtesting:0.1.2`, Modul-12), V1-Kompatibilitätsmodus bitgenau. +> Erstellt: 20.08.2026 (Rain Ocampo) · Aktualisiert: 20.08.2026 + +## 1. Motivation + +Die Produktions-Strategie `trend_pullback_v1` (Modul-05/12) ist unter den +**unveränderten** produktiven Regime-Schwellen fachlich widersprüchlich +(messbar): Ein TREND_UP-Regime verlangt Volatilität, der Pullback +`close < EMA20` verlangt ein flaches Plateau. Im 60-Bars-Produktionsfenster +(`regime_lookback=60`) schneiden sich `TREND_UP` und `close95% der `close ema_slow_l) UND adx_l >= adx_trend_threshold +trend_dir = DOWN wenn (ema_fast_l < ema_slow_l) UND adx_l >= adx_trend_threshold +sonst: kein Trade # KEIN atr_ratio-Check für die Richtung + +# --- Pullback-Timing (ATR-Band um EMA20) --- +atr_prev = ATR(prev_highs, prev_lows, prev_closes, atr_period) +ema20 = EMA20(prev_closes) +pullback_limit = ema20 + pullback_band_atr * atr_prev # LONG +pullback_limit = ema20 - pullback_band_atr * atr_prev # SHORT + +LONG wenn trend_dir==UP UND last_close>sma200 UND last_close < pullback_limit + UND last_close>prev_close UND swing_low=min_risk_reward +SHORT wenn trend_dir==DOWN UND last_close pullback_limit + UND last_closelast_close UND rr>=min_risk_reward +``` + +- `break_high` entfällt (Befund: `prev_high ≥ prev_close` ⇒ `break_high ⇒ close_up`; + bleibt als `close_up`/`close_down` erhalten — nicht restriktiv). +- Deterministisch: reine Listen-Berechnung, kein Zufall, kein Hidden-State. + +## 4. Parameter-Tabelle + +| Parameter | V1 produktiv (Code) | V1.1 Default | Min | Max | Zweck | V1.1 Optimierbar? | +|---|---|---|---|---|---|---| +| `pullback_band_atr` | — (strikt `< EMA20`) | **0.35** | 0.0 | 1.0 | ATR-Band um EMA20. `0.0`=strikt V1. | **Ja** | +| `atr_period` | 14 | **14** | 14 (fix) | 14 (fix) | ATR-Fenster für Pullback-Band | Nein (fix) | +| `trend_regime_window` | 60 (geteilter 60er) | **200** | 60 | 500 | Fenster für strategie-internes Trend-Regime | **Ja** | +| `regime_vol_override` | true (geteilt) | **false** | — | — | false=V1.1 strategie-eigene Richtung | Nein (fix) | +| `rr_multiplier` | 2.0 | **2.0** | 1.0 | 4.0 | Target = Entry + rr_multiplier×Risiko | **Ja** | +| `min_risk_reward` | 1.0 | **1.0** | 1.0 (min) | 3.0 | Mindest-RR; keine Kandidaten mit Chance/Risiko <1 | Nein (fix) | + +**Zusätzliche Trend-Parameter (produktiv, V1.1 fix — NICHT von Modul-13 optimieren):** + +| Parameter | V1 produktiv (Code) | V1.1 | Zweck | +|---|---|---|---| +| `adx_trend_threshold` | **20.0** (config.py:54) | fix 20.0 | ADX-Schwelle für echten Trend | +| `adx_period` | **14** (config.py:50) | fix 14 | ADX-Indikatorfenster | +| `ema_fast_period` | **20** (config.py:36) | fix 20 | Fast-EMA (Strategie-Pullback-Basis) | +| `ema_slow_period` | **30** (config.py:47) | fix 30 | Slow-EMA (Regime-Flanke) | + +## 5. V1-Kompatibilitätsmodus (bitgenau V1) + +``` +pullback_band_atr = 0.0 +regime_vol_override = true +trend_regime_window = 60 ++ alle übrigen V1-Parameter auf produktiven Defaults +``` +⇒ muss V1 **bitgenau** reproduzieren (Rückwärtskompatibilität; Backtest ohne +`strategy_params` bleibt fachlich identisch). **E2E-verifiziert**: V1-Kompatmodus +LONG+SHORT bitgenau (net 200.0, target r=2.0). + +## 6. Abgrenzung / No-Go + +- **Keine KI/LLM** in V1.1, keine Live-Orders. +- Produktions-Regime-Schwellen (`adx_trend_threshold=20`, `trend_min_ema_gap=0.02`, + `high_vol_atr_ratio=0.03`, `low_vol_atr_ratio=0.008`) werden **nicht verändert**. +- `atr_period`, ADX-/EMA-Perioden, `min_risk_reward` werden in dieser ersten + Optimierungsrunde **nicht** über Modul-13 optimiert. + +--- +## Geändert +``` +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: V1.1-Spec von ENTWURF auf DEPLOYT aktualisiert (backtesting:0.1.2); V1-Kompatmodus bitgenau E2E-verifiziert. +```