Files
AH-Oil-Trader/core/cone.py
T
Axel HocksandClaude Opus 5 b8705e297f Kegel mit Adaptive Conformal Inference nachkalibriert (v=158)
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>
2026-08-08 23:45:26 +02:00

105 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 ~35 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 **35 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,0050,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}