# PLAN_EXTERNAL_CHANNEL_CAP_ALIGN **Status:** planned (backlog, awaits operator GO) **Priority:** P1 **Erstellt:** 2026-05-17 **Quelle:** Audit während C1-Monitoring 2026-05-17 — Bot hielt 6 statt 5 Positionen in BEAR-Regime **Roadmap-ID:** EXTERNAL-CHANNEL-CAP-ALIGN **Voraussetzung:** C1-Monitoring durch; idealerweise mit SNAPSHOT-EMIT-COMPLETENESS gebündelt im selben Cutover (beides Bot-Touch). --- ## 1. Bug-Beschreibung Operator-Beobachtung: Bot hält 6 offene Positionen, obwohl der Regime-Detektor `max_positions=5` für BEAR liefert. Beleg aus `bot_stdout.log` 2026-05-17 10:51-10:52 UTC (HUMA-Open): ``` 10:51:53 Scanner Kandidat HUMA Score 6.0 → risk_mgr.check_open_positions(5, max_pos=5)=True → BLOCK ✓ 10:51:53 Log: "Max offene Positionen erreicht: 5/5 (source=regime cap=5 global=10)" 10:51:54 Telegram-Scraper liefert HUMA-Signal 10:51:57 Web-Channel: HUMA Score 10.0 → ✅ TRADE! → risk_mgr_shared.check_open_positions(5, max_override=None) → fallback auf global_max=10 → 5 < 10 → ALLOW ✗ 10:52:05 PAPER BUY HUMA → 6. Position open 10:54:27 Log: "Max offene Positionen erreicht: 6/5" (jetzt erst eskaliert) ``` ## 2. Root cause `trading/execution/risk_manager.py:105` `check_open_positions(open, max_override=None, active_config=None)`: ```python if max_override is not None: effective_max = min(max_override, global_max) else: effective_max = global_max # ← Bypass-Pfad ``` Beim Wegfall des `max_override` (regime cap) greift `global_max=10` aus Settings (`MAX_OPEN_POSITIONS=10`). Der regime-spezifische BEAR-Cap=5 wird damit unbeachtet ignoriert. In `trading/main.py` gibt es **drei** BUY-Pfade mit unterschiedlichen Aufrufkonventionen: | Zeile | Pfad | Aufruf | Effektiver Cap | |---|---|---|---| | `~898` | Scanner approved-BUY | `check_open_positions(open, max_pos, active_config)` mit `max_pos=regime.max_positions` | **regime=5** ✓ | | `~970` | Web-Scraper Channel | `check_open_positions(open, active_config=active_config)` ohne `max_override` | **global=10** ✗ | | `~1109` | Telegram-Bot Channel | `check_open_positions(open, active_config=active_config)` ohne `max_override` | **global=10** ✗ | Die External-Channel-Pfade umgehen daher unbeabsichtigt den regime-Cap. ## 3. Fix (Scope) Minimal-Diff: an beiden External-Channel-Aufrufen den `max_pos` zur regime-Auflösung mitschleifen, exakt wie im Scanner-Pfad: ```python # main.py:898 (Scanner) – unverändert, dient als Referenz max_pos = regime.get('max_positions', 5) if regime else 5 if not risk_mgr.check_open_positions(portfolio['open_positions'], max_pos, active_config=active_config): ... # main.py:~970 (Web-Channel) – ergänze max_pos max_pos_w = regime.get('max_positions', 5) if regime else 5 if not risk_mgr_shared.check_max_drawdown(portfolio) and \ not risk_mgr_shared.check_open_positions(portfolio['open_positions'], max_pos_w, active_config=active_config): ... # main.py:~1109 (Telegram-Bot Channel) – ergänze max_pos max_pos_b = regime.get('max_positions', 5) if regime else 5 if not risk_mgr_shared.check_max_drawdown(portfolio) and \ not risk_mgr_shared.check_open_positions(portfolio['open_positions'], max_pos_b, active_config=active_config): ... ``` Diff < 10 Zeilen Code. ## 4. Boundaries * **Bot-Touch**: ja (`main.py` x2 Stellen). Container-Recreate notwendig. * 0× Strategieparameter-Tuning * 0× DB-Migration * 0× DB-Mass-Mutation (bestehende 6. Position bleibt offen; SL/TP funktionieren weiter, kein Force-Close) * 0× Mainnet * 0× Worker-Recreate * 0× CommandBus-Version-Bump * 0× Push ohne separates GO ## 5. Auswirkung auf bestehende Überzahl-Positionen Die aktuelle Überzahl (6 statt 5) bleibt nach dem Fix bestehen — wird **NICHT** force-closed. Verhalten nach Cutover: * Keine neuen External-Channel-BUYs solange `open_positions >= regime_cap`. * Bestehende 6 Positionen laufen normal mit SL/TP/Trailing weiter. * Sobald eine Position via SL/TP/manual close geschlossen ist und `open_positions < 5` fällt, sind neue BUYs wieder möglich. * Operator könnte optional via `/admin/positions` Detail-Action `closeAtMarket` eine Position manuell schließen, um sofort auf 5 zu kommen. **Nicht erforderlich für den Fix.** ## 6. Cutover-Plan (SOT-1d) 1. Pre-cutover snapshot (HEAD, container PID, env flags, position-count). 2. Watchdog freeze `CUTOVER_FREEZE_EXTERNAL_CHANNEL_CAP_ALIGN`. 3. `docker compose build clawbot`. 4. Container-Test im neuen Image: ``` python3 -m unittest tests.test_external_channel_cap_align ``` 5. `docker compose up -d --force-recreate --no-deps clawbot`. 6. 3-Way MD5 Repo == Image == Container für `main.py`. 7. Bot spawn + healthcheck + 0 Tracebacks. 8. Live-Verify: Log enthält `Max offene Positionen erreicht: X/5 (source=regime cap=5 global=10)` auch für External-Channel-Pfade, nicht mehr nur Scanner. 9. Watchdog re-enable. 10. Roadmap-Update EXTERNAL-CHANNEL-CAP-ALIGN → done. ## 7. Tests (Plan) `trading/tests/test_external_channel_cap_align.py` (neu): * AST-Guard: alle drei `check_open_positions`-Aufrufe in `main.py` haben ein `max_pos`-Argument (zweites positional oder named). * AST-Guard: keiner der drei Pfade ruft `check_open_positions` mit nur `active_config=...` (zwei Argumente ohne max_override). * Optional unit-test gegen RiskManager: regime-cap wird respektiert wenn übergeben. ## 8. Bundle-Empfehlung Da beide Items (`SNAPSHOT-EMIT-COMPLETENESS` P2 + `EXTERNAL-CHANNEL-CAP-ALIGN` P1) Bot-Touch + Container-Recreate brauchen, empfiehlt sich ein **gemeinsamer Cutover**: * gemeinsamer Build → ein Image rebuild * gemeinsamer Recreate → eine Bot-Downtime von ~30s statt zweimal * zwei separate Commits (klare Forensik), gleicher Cutover-Block Priorität-Reihenfolge bleibt: P1 vor P2. Operator entscheidet beim GO. ## 9. Erwartete Lieferung * `trading/main.py` Diff < 10 Zeilen. * 1 neue Test-Datei mit ~3-5 Tests. * Tests grün (104/104 nach Cutover inkl. bestehender Phase-D-Tests). Commit-Vorschlag: `external-channel-cap-align: respect regime max_positions in web/telegram channels` ## 10. STOP Kein Code vor Operator-`GO EXTERNAL-CHANNEL-CAP-ALIGN`. Bis dahin: * Cap-Bug bleibt aktiv — External-Channels können bis `global_max=10` Positionen öffnen. * In aktueller Marktphase (BEAR + Threshold-Block + Quality-Shadow) ist External-Channel-Aktivität dünn → praktischer Schaden begrenzt. * C1-Monitoring läuft uninterruptiert weiter, sammelt Quality-Shadow-Daten.