"""core/cone.py — Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung). User-Wunsch 2026-07-31: „ein Kursvorhersagemodul, das mögliche Verläufe in MT5 einzeichnet". Eingezeichnete PFADE würden eine Präzision suggerieren, die es nicht gibt — das Projekt hat mit `analyze_verdict_calibration.py` teuer gelernt, dass eine Zahl ohne Kalibrierungsprüfung in die Irre führt (conf_pct war in H2 invertiert). Deshalb bewusst WENIGER, dafür belegt: der Kegel sagt nicht, WOHIN der Kurs geht, sondern WIE WEIT er in 30/60/120 min plausibel gelaufen sein kann. GEMESSEN & OUT-OF-SAMPLE VALIDIERT (`analyze_cone.py`, 80k M5-Bars, Fit auf H1, Abdeckung gemessen auf H2): Band 30 min 60 min 120 min 80 % nom. 76,7 % 76,6 % 75,3 % 90 % nom. 87,5 % 87,2 % 86,5 % ⚠ Die Bänder sind konsistent ~3–5 Prozentpunkte ZU ENG (H2 war relativ zum ATR etwas volatiler als H1). Das wird NICHT weggefittet — die Anzeige nennt die real gemessene Abdeckung, nicht den Nennwert. Damit ist der Kegel nach dem P(break)- Modell die zweite kalibriert geprüfte Komponente im Projekt. Quantile final auf allen 80k Bars gefittet (dasselbe Vorgehen wie beim P(break)- Modell: erst out-of-sample validieren, dann für den Live-Einsatz auf der vollen History fitten). ATR-normiert → über Vola-Regime hinweg gültig. ⚠ KEIN Signal, kein Verdict-Gewicht, kein Trade-Trigger. Der Kegel ist um den AKTUELLEN Kurs zentriert (keine Drift addiert — der Median lag bei ±0,0…0,16×ATR, also klein gegen die Bandbreite; eine Drift-Korrektur wäre eine Richtungsaussage und genau die ist hier nicht belegt). """ from __future__ import annotations # Horizont (M5-Bars) → {Nennband: (unteres, oberes Quantil in ×ATR)} _CONE = { 6: {50: (-0.915, +0.940), 80: (-2.000, +1.962), 90: (-2.800, +2.713)}, 12: {50: (-1.333, +1.346), 80: (-2.854, +2.833), 90: (-4.064, +4.000)}, 24: {50: (-1.949, +2.000), 80: (-4.183, +4.177), 90: (-5.955, +5.938)}, } # real gemessene Abdeckung (H2, out-of-sample) — das steht in der UI, nicht der Nennwert _COVERAGE = {6: {50: 44.4, 80: 76.7, 90: 87.5}, 12: {50: 45.1, 80: 76.6, 90: 87.2}, 24: {50: 45.5, 80: 75.3, 90: 86.5}} _ATRMIN = 0.06 MINUTES = {6: 30, 12: 60, 24: 120} def build(price: float, atr: float, bands=(50, 80)) -> dict | None: """→ {'atr':…, 'levels':[{minutes, bars, band, lo, hi, coverage}, …]} `price` = aktueller Kurs (Bid), `atr` = ATR der M5-Basis. """ if not price or not atr or atr <= 0: return None a = max(float(atr), _ATRMIN) out = [] for bars in sorted(_CONE): for band in bands: q = _CONE[bars].get(band) if not q: continue out.append({ "minutes": MINUTES[bars], "bars": bars, "band": band, "lo": round(price + q[0] * a, 3), "hi": round(price + q[1] * a, 3), "coverage": _COVERAGE.get(bars, {}).get(band), }) return {"atr": round(a, 4), "price": round(float(price), 3), "levels": out}