Der Kegel war seit dem 31.07. dokumentiert 3-5 Pp ZU ENG - 80 % nominal gegen
real 76,7 / 76,6 / 75,3 % - und blieb es, weil Nachfitten das Problem nur
verschiebt. ACI justiert stattdessen ONLINE nach jedem aufgeloesten Fenster und
konvergiert nachweislich gegen die Zielabdeckung, auch unter Drift.
EHRLICH ZUR BAUART: die Lehrbuch-Form justiert alpha und schlaegt das Quantil
nach. Hier liegt nur eine feste Quantil-Tabelle vor, kein Verteilungsobjekt -
umgesetzt ist die multiplikative Variante:
s <- s * exp(gamma * (1{verfehlt} - alpha_ziel)), gamma 0,02, Deckel 0,7-2,0
Dieselbe Regelungsidee, aber die Konvergenz-GARANTIE der Originalform ist damit
nicht woertlich uebertragen.
Verifiziert: bei exakt 20 % Verfehlern bleibt die Skala bei 1,0000 - der
Fixpunkt stimmt. Simulation aus der dokumentierten Lage (76,6 %): nach ~50
Checks um 80 %. Die Skala pendelt um das Ziel statt exakt zu treffen - das ist
das bekannte ACI-Verhalten (garantiert ist die Langfrist-Abdeckung).
NICHT-UEBERLAPPEND geloggt (neue Tabelle cone_checks): ein 120-min-Fenster alle
5 min waere 24-fach ueberlappend - genau die Cluster-Verzerrung, an der
analyze_hl_funding.py aufgelaufen ist und die die Faelligkeitsbedingung
pbreak_accuracy_v2 um das Zehnfache danebenliegen liess. Je Horizont startet ein
neuer Check erst, wenn der vorige ablief -> 48/24/12 Checks je Tag.
Neustart-fest ueber runtime_state.json. build() ohne skalen liefert bitgenau die
alten Werte (additiv geprueft).
ZWEI EIGENE PLATZIERUNGSFEHLER, beide gefangen: der Lade-Block landete zuerst
HINTER den except-Klauseln von _load_runtime_state, wo st nicht mehr existiert;
und beim M15-Setup landete ein Block mitten in einem mehrzeiligen Aufruf. Der
erste waere stillschweigend nie gelaufen - gefunden nur, weil ich die Funktion
danach gelesen habe statt dem gruenen py_compile zu trauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
5.1 KiB
Python
105 lines
5.1 KiB
Python
"""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}
|
||
|
||
|
||
# ── ADAPTIVE CONFORMAL INFERENCE (2026-08-08) ────────────────────────────────
|
||
# ⚠⚠ WOZU: die Baender sind oben dokumentiert **3–5 Pp ZU ENG** (80 % nominal →
|
||
# real 76,7 / 76,6 / 75,3 %). Das stand seit dem 31.07. so da und wurde NICHT
|
||
# behoben, weil ein Nachfitten das Problem nur verschiebt: die naechste Periode
|
||
# hat wieder eine andere Vola.
|
||
# ACI loest genau das — es justiert das Niveau ONLINE nach jedem aufgeloesten
|
||
# Fenster und konvergiert nachweislich gegen die Zielabdeckung, AUCH unter
|
||
# Verteilungsdrift (Gibbs & Candes; DtACI schaetzt zusaetzlich gamma selbst).
|
||
#
|
||
# ⚠ EHRLICH ZUR BAUART: die Lehrbuch-Form justiert `alpha` und schlaegt das
|
||
# zugehoerige Quantil nach. Hier liegt nur eine feste Quantil-TABELLE vor, kein
|
||
# vollstaendiges Verteilungsobjekt — deshalb die **multiplikative Variante**:
|
||
# ein Skalenfaktor je Horizont, aktualisiert als
|
||
# s <- s * exp(gamma * (1{verfehlt} - alpha_ziel))
|
||
# Verfehlt das Fenster oefter als alpha_ziel, waechst s (Band wird breiter),
|
||
# sonst schrumpft es. Dieselbe Regelungsidee, aber die Konvergenz-GARANTIE der
|
||
# Originalform ist damit NICHT woertlich uebertragen — es ist ACI im Geiste.
|
||
_ACI_GAMMA = 0.02 # Schrittweite (Literatur ~0,005–0,05)
|
||
_ACI_MIN, _ACI_MAX = 0.7, 2.0 # Skalen-Deckel gegen Ausreisser/Endlos-Drift
|
||
|
||
|
||
def aci_update(skala: float, verfehlt: bool, band: int = 80,
|
||
gamma: float = _ACI_GAMMA) -> float:
|
||
"""Ein ACI-Schritt. `verfehlt` = der Kurs lag AUSSERHALB des Bandes."""
|
||
ziel = 1.0 - band / 100.0 # z. B. 0,20 bei 80 %
|
||
import math
|
||
s = skala * math.exp(gamma * ((1.0 if verfehlt else 0.0) - ziel))
|
||
return max(_ACI_MIN, min(_ACI_MAX, s))
|
||
|
||
|
||
def build(price: float, atr: float, bands=(50, 80),
|
||
skalen: dict | None = None) -> dict | None:
|
||
"""→ {'atr':…, 'levels':[{minutes, bars, band, lo, hi, coverage, skala}, …]}
|
||
|
||
`price` = aktueller Kurs (Bid), `atr` = ATR der M5-Basis.
|
||
`skalen` = {bars: Faktor} aus ACI; ohne Angabe bleibt alles wie bisher
|
||
(⚠ ADDITIV — ein Aufruf ohne `skalen` liefert bitgenau die alten Werte).
|
||
"""
|
||
if not price or not atr or atr <= 0:
|
||
return None
|
||
a = max(float(atr), _ATRMIN)
|
||
sk = skalen or {}
|
||
out = []
|
||
for bars in sorted(_CONE):
|
||
for band in bands:
|
||
q = _CONE[bars].get(band)
|
||
if not q:
|
||
continue
|
||
s = float(sk.get(bars) or sk.get(str(bars)) or 1.0)
|
||
out.append({
|
||
"minutes": MINUTES[bars], "bars": bars, "band": band,
|
||
"lo": round(price + q[0] * a * s, 3),
|
||
"hi": round(price + q[1] * a * s, 3),
|
||
"coverage": _COVERAGE.get(bars, {}).get(band),
|
||
"skala": round(s, 3),
|
||
})
|
||
return {"atr": round(a, 4), "price": round(float(price), 3), "levels": out}
|