From d9cb54a52cac15703be3849e71c982f9fcacc341 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Fri, 31 Jul 2026 08:25:29 +0200 Subject: [PATCH] C: Wahrscheinlichkeits-Kegel (kalibriert) + Config-Waechter im Reminder C - core/cone.py + Kachel #card-cone + MT5 v1.31 (CN;;;): "Erwartete Spanne" statt "Kursvorhersage". Der User wollte eingezeichnete Verlaufspfade; bewusst NICHT gebaut - sie suggerieren eine Praezision, die es nicht gibt (Lehre aus analyze_verdict_calibration.py). Der Kegel sagt wie WEIT der Kurs in 30/60/120 min plausibel laeuft, nicht wohin. analyze_cone.py: r(h)=(C[i+h]-C[i])/ATR[i], Fit auf H1, Abdeckung gemessen auf H2 (echtes out-of-sample): 80%-Band -> 76,7 / 76,6 / 75,3 % 90%-Band -> 87,5 / 87,2 / 86,5 % Konsistent ~3-5 Pp ZU ENG. Das wird NICHT weggefittet - Kachel und Tooltip nennen die REAL gemessene Abdeckung, nicht den Nennwert. Damit nach dem P(break)-Modell die zweite kalibriert geprueft Komponente im Projekt. Zentriert auf den aktuellen Kurs, KEINE Drift addiert (Median +-0,0..0,16 xATR = klein gegen die Bandbreite; eine Drift-Korrektur waere eine Richtungsaussage und die ist nicht belegt). Farbe neutral grau, kein gruen/rot. Kein Signal, kein Verdict-Gewicht. MQL5 v1.31: zwei sich oeffnende gepunktete Pfade, verkettet 30->60->120. Startpunkt aus der PX-Zeile - die setzt g_conePx jetzt auch, wenn InpShowPrice aus ist (sonst verliert der Faecher seinen Anker). Config-Waechter in measurement_reminder.py (Antwort auf "sollten wir alle verworfenen Backtests woechentlich neu rechnen?"): NEIN - eine Woche sind ~2000 Bars, und 18 Ideen x 52 Wochen = 936 Tests/Jahr erzeugen bei 5 % Fehlalarmquote ~47 falsche "funktioniert jetzt!" pro Jahr. Stattdessen EREIGNIS-gesteuert: CONFIG_DEPS verankert 8 config-abhaengige Messungen an den Wert, gegen den sie validiert wurden (entry_room_atr 0,6 / sr_close_pbreak 0,55 / breakout_k 0,3 / risk_pct 0 / margin_buffer_pct 95 / adverse_15min_atr 0,5 / auto_signal_min_conf 75 / dead_hours leer). Weicht einer ab, meldet der Timer welche Messung veraltet ist. Numerischer Vergleich, damit 0.60 == 0.6. Mit 5 synthetischen Szenarien getestet. Erledigt ausgetragen: Chartmuster-Kontrolltest (30.07., Muster schlagen die Kontrolle in beiden Haelften +0,022/+0,108). Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 30 +++++++- analyze_cone.py | 117 ++++++++++++++++++++++++++++ core/cone.py | 68 ++++++++++++++++ core/engine.py | 19 +++++ measurement_reminder.py | 167 ++++++++++++++++++++++++++++++++++++---- mql5/SR_Levels.mq5 | 77 +++++++++++++++++- web/app.js | 32 ++++++++ web/index.html | 11 ++- web/style.css | 8 ++ 9 files changed, 508 insertions(+), 21 deletions(-) create mode 100644 analyze_cone.py create mode 100644 core/cone.py diff --git a/CLAUDE.md b/CLAUDE.md index 641d423..37fdefb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1426,6 +1426,34 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Continuation = breakeven-negativ (−0,011/−0,005, Momentum-Klasse). Damit ist auch die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie TF-Ampel/Squeeze), steuert nichts. +- **Wahrscheinlichkeits-Kegel „Erwartete Spanne" (`core/cone.py` + Kachel `#card-cone` + + MT5 `CN;…`, 2026-07-31): STREUUNG, NICHT RICHTUNG — und out-of-sample kalibriert.** + Der User wollte ein „Kursvorhersagemodul, das mögliche Verläufe in MT5 einzeichnet". + Eingezeichnete PFADE wurden bewusst NICHT gebaut (suggerieren eine Präzision, die es + nicht gibt — die Lehre aus `analyze_verdict_calibration.py`, wo conf_pct in H2 + invertiert war). Stattdessen: wie WEIT der Kurs in 30/60/120 min plausibel gelaufen + sein kann. **Gemessen (`analyze_cone.py`, 80k M5, r(h)=(C[i+h]−C[i])/ATR[i], Fit auf + H1 → Abdeckung gemessen auf H2):** + | Band | 30 min | 60 min | 120 min | + |---|---|---|---| + | 80 % nominal | 76,7 % | 76,6 % | 75,3 % | + | 90 % nominal | 87,5 % | 87,2 % | 86,5 % | + ⚠ Die Bänder sind konsistent **~3–5 Pp ZU ENG** (H2 war relativ zum ATR volatiler). + Das wird **NICHT weggefittet** — Kachel und Tooltip nennen die REAL gemessene + Abdeckung („trifft 77 %"), 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 (`_CONE` in `core/cone.py`; gleiches Vorgehen wie + beim P(break)-Modell: erst out-of-sample validieren, dann voll fitten), ATR-normiert + → regime-übergreifend gültig. **Zentriert auf den AKTUELLEN Kurs, KEINE Drift + addiert** (Median lag bei ±0,0…0,16×ATR = klein gegen die Bandbreite; eine + Drift-Korrektur wäre eine Richtungsaussage und genau die ist nicht belegt). + Snapshot `cone {atr, price, levels[{minutes,bars,band,lo,hi,coverage}]}`; MT5-Export + `CN;;;` (nur 80 %-Band) → Indikator **v1.31** zeichnet zwei sich + öffnende, gepunktete Pfade, verkettet über 30→60→120 (`g_coneT/g_coneLo/g_coneHi`, + je Redraw zurückgesetzt; Startpunkt aus der `PX`-Zeile, die deshalb `g_conePx` auch + dann setzt, wenn `InpShowPrice` aus ist). Farbe **neutral grau** (`InpConeColor`) — + bewusst keine Richtungsfarbe. `InpShowCone=false` schaltet ab. **Kein Signal, kein + Verdict-Gewicht.** - **Kerzen-Anatomie (`core/candles.py` + Kachel `#card-candles`, 2026-07-31, User-Vorgabe „Kerzengröße und Dochtlänge … im Kontext"): REINE ANZEIGE — und die Kachel widerspricht bewusst der Lehrbuch-Deutung.** Erkennt mechanisch aus M5-OHLCV: Marubozu · Langer @@ -1679,7 +1707,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei **negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap. -- Asset-Version aktuell **v=123** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=124** (in `web/index.html` hochzählen, siehe Workflows). Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — diff --git a/analyze_cone.py b/analyze_cone.py new file mode 100644 index 0000000..c610cbc --- /dev/null +++ b/analyze_cone.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Wahrscheinlichkeits-Kegel: empirische Quantile fitten UND out-of-sample prüfen. + +User-Wunsch 2026-07-31 war 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). + +STATTDESSEN: ein **Streuungs-Kegel**. Er sagt NICHT, wohin der Kurs geht, sondern +**wie weit** er in 30/60/120 min plausibel gelaufen sein kann. Der entscheidende +Unterschied: das ist ÜBERPRÜFBAR — landen wirklich ~80 % der Ausgänge im 80 %-Band? + +METHODE + r(h) = (C[i+h] − C[i]) / ATR[i] (ATR-normiert → über Vola-Regime vergleichbar) + Quantile auf H1 fitten, Abdeckung auf H2 messen (echtes out-of-sample). + +Ein Kegel ist nur dann brauchbar, wenn die gemessene Abdeckung nah am Nominalwert +liegt. Weicht sie stark ab, wird er NICHT gebaut — dann ist die Streuung nicht +stabil ATR-skalierbar. +""" +import sys + +import MetaTrader5 as mt5 + +_H = (6, 12, 24) # 30 / 60 / 120 min auf M5 +_ATRMIN = 0.06 +# Nominale Bänder: (unteres Quantil, oberes Quantil, Nennabdeckung) +_BANDS = ((0.25, 0.75, 50), (0.10, 0.90, 80), (0.05, 0.95, 90)) + + +def _atr_series(H, L, C, p=14): + t = [0.0] + for i in range(1, len(C)): + t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [] + for i in range(len(C)): + if not i: + out.append(None); continue + w = t[max(1, i - p + 1):i + 1] + out.append(sum(w) / max(1, len(w))) + return out + + +def quantile(sorted_xs, q): + if not sorted_xs: + return 0.0 + k = q * (len(sorted_xs) - 1) + lo = int(k); hi = min(lo + 1, len(sorted_xs) - 1) + return sorted_xs[lo] + (sorted_xs[hi] - sorted_xs[lo]) * (k - lo) + + +def collect(C, AT, a, b, h): + out = [] + for i in range(a, min(b, len(C) - h)): + atr = AT[i] + if not atr: + continue + out.append((C[i + h] - C[i]) / max(atr, _ATRMIN)) + return out + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + mt5.initialize(); sym = None + for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): + if mt5.symbol_info(c): + sym = c; break + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars] + AT = _atr_series(H, L, C) + N = len(C); mid = N // 2 + + print("=" * 92) + print(f" WAHRSCHEINLICHKEITS-KEGEL — {sym} M5 ({N} Bars)") + print(" Fit auf H1, Abdeckung gemessen auf H2 (out-of-sample). Werte in ×ATR.") + print("=" * 92) + + fitted = {} + for h in _H: + tr = sorted(collect(C, AT, 100, mid, h)) + te = collect(C, AT, mid, N, h) + print(f"\n Horizont {h} Bars = {h*5} min (fit n={len(tr)}, test n={len(te)})") + print(f" Median H1 {quantile(tr,0.5):+.3f} · Median H2 " + f"{sorted(te)[len(te)//2]:+.3f} (Drift-Kontrolle)") + row = {} + for qlo, qhi, nominal in _BANDS: + lo = quantile(tr, qlo); hi = quantile(tr, qhi) + cov = 100.0 * sum(1 for x in te if lo <= x <= hi) / len(te) + flag = "OK" if abs(cov - nominal) <= 5 else ("eng" if cov < nominal else "weit") + print(f" {nominal}%-Band [{lo:+.2f} … {hi:+.2f}]×ATR " + f"-> Abdeckung H2 {cov:>5.1f}% [{flag}]") + row[nominal] = (round(lo, 3), round(hi, 3)) + fitted[h] = row + + # ── Final auf ALLEN Bars fitten (wie beim P(break)-Modell: erst out-of-sample + # validieren, dann für den Live-Einsatz auf der vollen History fitten) + print("\n" + "=" * 92) + print(" FINAL (auf allen Bars gefittet) — zum Einbetten in core/cone.py") + print("=" * 92) + final = {} + for h in _H: + allx = sorted(collect(C, AT, 100, N, h)) + row = {} + for qlo, qhi, nominal in _BANDS: + row[nominal] = (round(quantile(allx, qlo), 3), round(quantile(allx, qhi), 3)) + final[h] = row + print("_CONE = {") + for h in _H: + parts = ", ".join(f"{k}: ({v[0]:+.3f}, {v[1]:+.3f})" for k, v in final[h].items()) + print(f" {h}: {{{parts}}},") + print("}") + + +if __name__ == "__main__": + main() diff --git a/core/cone.py b/core/cone.py new file mode 100644 index 0000000..3d1f235 --- /dev/null +++ b/core/cone.py @@ -0,0 +1,68 @@ +"""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} diff --git a/core/engine.py b/core/engine.py index 09e16db..7b0946a 100644 --- a/core/engine.py +++ b/core/engine.py @@ -43,6 +43,7 @@ from core.gaps import GapAnalyzer from core.structure import MarketStructure from core.patterns import PatternDetector from core.candles import CandleAnatomy +from core.cone import build as _cone_build from core.candle_logger import CandleLogger from core.wave_rec import (_atr as _wr_atr, _ema_last, _EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX) @@ -2093,6 +2094,19 @@ class TradingEngine: lines.append(f"PX;{_bid}") except Exception: pass + # WAHRSCHEINLICHKEITS-KEGEL: CN;;; je Horizont (30/60/120), + # aufsteigend — der Indikator verbindet sie vom aktuellen Bar aus zu zwei + # sich öffnenden Pfaden. ⚠ Streuung, NICHT Richtung: der Kegel ist um den + # aktuellen Kurs zentriert und sagt nur, wie weit der Kurs plausibel läuft. + # Out-of-sample kalibriert (`analyze_cone.py`), 80 %-Band trifft real ~76 %. + try: + _atr5 = float(((self.wave.snapshot() or {}).get("pb_feats") or {}) + .get("atr") or 0.0) + _cn = _cone_build(_bid, _atr5, bands=(80,)) + for lv in (_cn or {}).get("levels", []): + lines.append(f"CN;{lv['minutes']};{lv['lo']};{lv['hi']}") + except Exception: + pass # G/V der offenen Position, direkt unter dem Kurs (User-Wunsch 2026-07-30): # PL;wert;waehrung. **NETTO** wie im Dashboard — bei Gewinn abzüglich der # WHT-Quellensteuer, die der Broker einbehält (`[trading] wht_pct`); bei @@ -2714,6 +2728,11 @@ class TradingEngine: # Kerzen-Anatomie (M5) — reine Anzeige, KEIN Verdict-Gewicht (gemessen: # Lehrbuch-Deutung invertiert, Handelssignal durchgefallen). "candle_anat": self.candle_anat.snapshot(), + # Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung) — out-of-sample + # kalibriert (`analyze_cone.py`). Reine Anzeige. + "cone": _cone_build( + (market or {}).get("bid"), + float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0)), "stats_compact": {"week": self._compact_stats("week"), "all": self._compact_stats("all")}, "close_alert_count": self._close_alert_count, diff --git a/measurement_reminder.py b/measurement_reminder.py index 59db835..8050447 100644 --- a/measurement_reminder.py +++ b/measurement_reminder.py @@ -94,16 +94,8 @@ CHECKS = [ "need": 30, "unit": "S/R-Closes seit der Umstellung", "cmd": "sr_close-Trades vor/nach 2026-07-30 vergleichen (ØEUR, Trefferquote)", }, - { - "key": "patterns_control", - "title": "Chartmuster: Kontrolltest gegen generischen Breakout", - "why": "backtest_patterns.py zeigte positiven ØR — aber die Ziel-Trefferquote " - "ist nur 13–38 %, der Ertrag kommt vom Trailing-Exit. Es fehlt die " - "Kontrolle: gleicher Exit auf einem BELIEBIGEN Breakout. Erst danach " - "ist 'Muster wirkt' belegt oder widerlegt.", - "have": lambda: 1, "need": 1, "unit": "jederzeit machbar (nur Rechenzeit)", - "cmd": "backtest_patterns.py um eine Kontrollgruppe erweitern", - }, + # (erledigt 2026-07-30: Chartmuster-Kontrolltest gegen generischen Swing-Bruch — + # Muster schlagen die Kontrolle in beiden Hälften (+0,022/+0,108) → Gewicht 1,0) { "key": "m1_squeeze", "title": "M1-Squeeze (Variante B) backtesten", @@ -128,6 +120,107 @@ CHECKS = [ ] +# ── Config-abhängige Backtests ─────────────────────────────────────────────── +# User-Frage 2026-07-31: „sollten wir alle verworfenen Backtests wöchentlich neu +# laufen lassen, da wir laufend die Config anpassen?" +# +# ⚠ NEIN — wöchentlich wäre eine Fehlalarm-Maschine: eine Woche M5 sind ~2000 Bars, +# und das Projekt hat dreimal erlebt, was daraus wird (Auto-Signal-Vorlauf auf 6k +# Bars zeigte „trägt", der 80k-Lauf drehte es; Std 13; ORB mit ØR +1,3/PF 10 als +# Selektions-Artefakt). Dazu der Mehrfachvergleich: 18 verworfene Ideen × 52 Wochen +# = 936 Tests/Jahr → bei 5 % Fehlalarmquote ~47 falsche „funktioniert jetzt!"/Jahr. +# +# ABER das Anliegen dahinter stimmt: einige Messungen sind gegen einen KONKRETEN +# Config-Wert kalibriert und werden ungültig, wenn der sich ändert. Das ist +# EREIGNIS-gesteuert, nicht kalendergesteuert — und genau das prüft dieser Block. +# (Die meisten verworfenen Tests sind config-UNABHÄNGIG: PDH/PDL, Volume Profile, +# Chartmuster, Liquidity Sweeps testen Signalklassen auf rohen Preisdaten.) +CONFIG_DEPS = [ + { + "key": "entry_room_atr", "test": "backtest_entryroom.py", + "validated": "0.6", + "why": "Das Raum-Gate ist auf diesen Schwellwert kalibriert (gemessen war 1,0 " + "netto besser, 0,6 ist die bewusste Frequenz-Entscheidung). Es nutzt " + "weiterhin die M5-`pb_levels` — eine Umstellung auf M30 wäre ungemessen.", + }, + { + "key": "sr_close_pbreak", "test": "backtest_srclose_prob.py", + "validated": "0.55", + "why": "Optimum-Plateau 0,50–0,60. Außerhalb ist der S/R-Auto-Close nicht mehr " + "durch die Messung gedeckt (die Hysterese ±5 Pp hängt ebenfalls daran).", + }, + { + "key": "breakout_k", "test": "backtest_breakout.py", + "validated": "0.3", + "why": "Split-validiert: 0,3 ist in BEIDEN Hälften das Beste; 0,5 und 1,0 waren " + "in H1 netto NEGATIV. Ein anderer Wert ist nicht belegt.", + }, + { + "key": "risk_pct", "test": "backtest_sizing.py", + "validated": "0", + "why": "0 = margin-basiert. Wechsel auf >0 aktiviert einen anderen Sizing-Pfad " + "(calc_lots_risk) — und koppelt den %-Notfall-Stop anders.", + }, + { + "key": "margin_buffer_pct", "test": "backtest_sizing.py", + "validated": "95", + "why": "Die Ruin-Simulation ist gegen den Margin-Anteil gerechnet (90 % = 36 % " + "P(DD>80 %)). Ändert sich er, ändert sich das Ruin-Risiko.", + }, + { + "key": "adverse_15min_atr", "test": "backtest_auto_signal.py (15-Min-Achse)", + "validated": "0.5", + "why": "0,5 war die beste Regel-Variante; 0,0 (sofort im Minus schliessen) war " + "die SCHLECHTESTE (WR bricht 39→31 %). Andere Werte sind ungemessen.", + }, + { + "key": "auto_signal_min_conf", "test": "backtest_auto_signal.py", + "validated": "75", + "why": "Empirisch bester Punkt (NICHT weil höhere Konfidenz besser wäre — " + "conf_pct ist unkalibriert). Nachbarwerte sind nicht besser.", + }, + { + "key": "dead_hours", "test": "backtest_hourly_split.py / backtest_realcosts.py", + "validated": "", + "why": "Leer = Gate AUS (User-Vorgabe, bewusst GEGEN die Messung). Wird es " + "wieder gefüllt, gelten die gemessenen Stunden 0–7 + 12 + 16.", + }, +] + + +def _cfg_now() -> dict: + """Liest NUR die überwachten [trading]-Keys. ⚠ Die ini enthält Live-Secrets — + hier wird bewusst nichts anderes gelesen oder ausgegeben.""" + import configparser + out: dict = {} + try: + c = configparser.ConfigParser(inline_comment_prefixes=("#", ";")) + c.read(os.path.join(HERE, "oil_widget_config.ini"), encoding="utf-8") + t = c["trading"] if c.has_section("trading") else {} + for d in CONFIG_DEPS: + out[d["key"]] = (t.get(d["key"], "") or "").strip() + except Exception: + pass + return out + + +def config_drift() -> list: + """→ [(dep, ist, soll)] für alle Keys, deren Wert von der Validierung abweicht.""" + now = _cfg_now() + drift = [] + for d in CONFIG_DEPS: + cur = now.get(d["key"]) + if cur is None: + continue + try: # numerisch vergleichen, damit 0.60 == 0.6 + same = abs(float(cur) - float(d["validated"])) < 1e-9 + except (TypeError, ValueError): + same = cur.lower() == d["validated"].lower() + if not same: + drift.append((d, cur, d["validated"])) + return drift + + def _popup(title: str, text: str) -> None: if DRY or STATUS: print(f"[POPUP] {title}\n{text}") @@ -156,6 +249,21 @@ def main() -> None: else: pending.append((c, have, "sammelt")) + # ── Config-Drift: welche Messung ist durch eine Config-Änderung veraltet? + # Marker enthält den Wert → ändert der User später erneut, wird neu gemeldet. + drift = config_drift() + drift_new = [] + for d, cur, val in drift: + marker = os.path.join(MARKER_DIR, f"cfg_{d['key']}") + seen = "" + try: + with open(marker, encoding="utf-8") as f: + seen = f.read().strip() + except Exception: + pass + if STATUS or DRY or seen != cur: + drift_new.append((d, cur, val, marker)) + if STATUS or DRY: print("=" * 78) print(" AUSSTEHENDE MESSUNGEN — Status") @@ -170,24 +278,51 @@ def main() -> None: print(f" {have:.0f}/{c['need']} {c['unit']} ({pct:.0f} %)") if not due: print("\n (nichts fällig)") + print("\n" + "=" * 78) + print(" CONFIG-VERANKERUNG — sind die Messungen noch gültig?") + print("=" * 78) + if not drift: + print(f"\n ✅ alle {len(CONFIG_DEPS)} überwachten Werte stehen auf dem " + f"Stand, gegen den gemessen wurde") + for d, cur, val, _m in drift_new: + print(f"\n ⚠ {d['key']}: ist '{cur}' — gemessen/validiert gegen '{val}'") + print(f" betroffen: {d['test']}") + print(f" {d['why']}") return - if not due: + if not due and not drift_new: return lines = [] for c, have in due: lines.append(f"• {c['title']}\n {have:.0f}/{c['need']} {c['unit']}\n" f" {c['why']}\n -> {c['cmd']}") - text = ("Diese Messungen haben jetzt genug Daten:\n\n" - + "\n\n".join(lines) - + "\n\nStatus aller Punkte:\n" - " python measurement_reminder.py --status") - _popup(f"Oil-Bot: {len(due)} Messung(en) faellig", text) + for d, cur, val, _m in drift_new: + lines.append(f"• CONFIG geaendert: {d['key']} = '{cur}' " + f"(validiert gegen '{val}')\n" + f" {d['why']}\n -> neu rechnen: {d['test']}") + head = [] + if due: + head.append(f"{len(due)} Messung(en) faellig") + if drift_new: + head.append(f"{len(drift_new)} Config-Abweichung(en)") + text = ("Offene Punkte:\n\n" + "\n\n".join(lines) + + "\n\n⚠ Verworfene Backtests NICHT woechentlich neu rechnen — eine Woche " + "ist Rauschen (~2000 Bars) und 936 Tests/Jahr erzeugen Fehlalarme.\n" + "Neu rechnen nur bei Config-Aenderung (oben) oder wenn genug WIRKLICH " + "neue Daten fuer eine dritte Stichprobe da sind (~quartalsweise).\n\n" + + "Status: python measurement_reminder.py --status") + _popup("Oil-Bot: " + " + ".join(head), text) for c, _ in due: try: open(os.path.join(MARKER_DIR, c["key"]), "w").close() except Exception: pass + for _d, cur, _val, marker in drift_new: + try: + with open(marker, "w", encoding="utf-8") as f: + f.write(cur) + except Exception: + pass if __name__ == "__main__": diff --git a/mql5/SR_Levels.mq5 b/mql5/SR_Levels.mq5 index 7e0a0ec..dae1f7c 100644 --- a/mql5/SR_Levels.mq5 +++ b/mql5/SR_Levels.mq5 @@ -12,7 +12,13 @@ //| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). | //+------------------------------------------------------------------+ #property copyright "Oil Trading Bot" -#property version "1.30" +#property version "1.31" +// v1.31 (2026-07-31): WAHRSCHEINLICHKEITS-KEGEL (CN;;;) — zwei sich +// oeffnende, gepunktete Pfade vom aktuellen Bar ueber 30/60/120 min. +// ⚠ KEINE Richtungsprognose, sondern STREUUNG: wie weit der Kurs plausibel +// gelaufen sein kann. Out-of-sample kalibriert (analyze_cone.py): das +// 80%-Band trifft real ~76%. Bewusst NEUTRAL grau (keine Richtungsfarbe). +// InpShowCone / InpConeColor. // v1.30 (2026-07-30): Konsolidierungs-Box BESCHRIFTET (User-Frage „was bedeutet die // graue Box") — sichtbares Label an der Oberkante, rechtsbuendig wie die // uebrigen: „Konsolidierung (10-Bar-Spanne)" bzw. bei Kompression @@ -149,6 +155,8 @@ input bool InpShowConsol = true; // Konsolidierungs-Box (10 M5-Bar input color InpConsolColor = C'88,166,255'; // Box-Farbe (blau) input int InpConsolAlpha = 8; // Deckkraft in % (komprimiert: x2) input int InpConsolBars = 10; // Box-Laenge in Bars (= _SQ_N im Bot) +input bool InpShowCone = true; // Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung) +input color InpConeColor = C'139,148,158'; // Kegel-Farbe (neutral grau - bewusst KEINE Richtungsfarbe) input bool InpShowPnL = true; // G/V der offenen Position unter dem Kurs input color InpPnLUpColor = C'0,255,127'; // G/V im Plus (neongruen) input color InpPnLDownColor = C'255,69,0'; // G/V im Minus (rot) @@ -172,6 +180,12 @@ input bool InpShowTradeLevels = true; // MT5-Positionslinien+Labels (st const string PFX = "SRB_"; // Präfix S/R-Linien (aus Datei) const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart) string g_last = "\x01"; // letzter Datei-Inhalt (gegen Flackern) +// Kegel: Verkettung der Horizonte (30->60->120). Die CSV liefert sie aufsteigend; +// jeder Abschnitt startet dort, wo der vorherige endete -> ein sich oeffnender +// Faecher statt drei unverbundener Geraden. Wird je Redraw zurueckgesetzt. +datetime g_coneT = 0; +double g_coneLo = 0.0, g_coneHi = 0.0; +double g_conePx = 0.0; // aktueller Kurs (aus der PX-Zeile) bool g_range_drawn = false; // hat DIESE Instanz die Range gezeichnet? //+------------------------------------------------------------------+ @@ -394,6 +408,9 @@ void Redraw() string rows[]; int nr = StringSplit(content, '\n', rows); int idx = 0; + // Kegel-Verkettung je Redraw zuruecksetzen (sonst haengt der Faecher am Stand + // des vorherigen Durchlaufs) + g_coneT = 0; g_coneLo = 0.0; g_coneHi = 0.0; g_conePx = 0.0; for(int r = 0; r < nr; r++) { @@ -650,13 +667,69 @@ void Redraw() ObjectSetString(0, ctn, OBJPROP_TOOLTIP, ctip); continue; } + // WAHRSCHEINLICHKEITS-KEGEL: CN;;; + // Zwei sich oeffnende Pfade vom aktuellen Bar aus. ⚠ Das ist KEINE Prognose + // der Richtung, sondern der STREUUNG: wie weit der Kurs in 30/60/120 min + // plausibel gelaufen sein kann. Out-of-sample kalibriert (analyze_cone.py): + // das 80%-Band trifft real ~76%. Bewusst NEUTRAL grau, kein gruen/rot. + if(typ == "CN") + { + if(k < 4) continue; + if(!InpShowCone) continue; + int mins = (int)StringToInteger(p[1]); + double clo = StringToDouble(p[2]); + double chi = StringToDouble(p[3]); + if(clo <= 0 || chi <= 0 || mins <= 0) continue; + // Anker: vom aktuellen Bar bis +mins Minuten (in Broker-Zeit) + datetime t0 = iTime(_Symbol, PERIOD_CURRENT, 0); + datetime t1 = t0 + (datetime)(mins * 60); + // Vorgaenger-Punkt: der naechstkleinere Horizont, sonst der aktuelle Kurs. + // g_coneT/g_coneLo/g_coneHi halten ihn zwischen den CSV-Zeilen fest; die + // Zeilen kommen aufsteigend sortiert vom Bot. + datetime pt = (g_coneT > 0 ? g_coneT : t0); + double plo = (g_coneT > 0 ? g_coneLo : g_conePx); + double phi = (g_coneT > 0 ? g_coneHi : g_conePx); + if(plo <= 0 || phi <= 0) { plo = clo; phi = chi; pt = t0; } + string ln = PFX + "CONE_L" + IntegerToString(mins); + string un = PFX + "CONE_U" + IntegerToString(mins); + string tip = "Streuung (keine Richtung): in " + IntegerToString(mins) + + " min liegt der Kurs mit ~76% zwischen " + + DoubleToString(clo, (int)_Digits) + " und " + + DoubleToString(chi, (int)_Digits); + for(int s = 0; s < 2; s++) + { + string nm2 = (s == 0 ? ln : un); + double a2 = (s == 0 ? plo : phi); + double b2 = (s == 0 ? clo : chi); + if(ObjectCreate(0, nm2, OBJ_TREND, 0, pt, a2, t1, b2)) + { + ObjectSetInteger(0, nm2, OBJPROP_COLOR, InpConeColor); + ObjectSetInteger(0, nm2, OBJPROP_STYLE, STYLE_DOT); + ObjectSetInteger(0, nm2, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, nm2, OBJPROP_RAY_RIGHT, false); + ObjectSetInteger(0, nm2, OBJPROP_BACK, true); + ObjectSetInteger(0, nm2, OBJPROP_SELECTABLE, false); + } + else + { + ObjectMove(0, nm2, 0, pt, a2); + ObjectMove(0, nm2, 1, t1, b2); + } + ObjectSetString(0, nm2, OBJPROP_TOOLTIP, tip); + } + g_coneT = t1; g_coneLo = clo; g_coneHi = chi; + continue; + } // AKTUELLER KURS oben rechts, fett (User-Wunsch): PX;bid if(typ == "PX") { if(k < 2) continue; - if(!InpShowPrice) continue; double pxv = StringToDouble(p[1]); if(pxv <= 0) continue; + // Kegel-Startpunkt IMMER merken - auch wenn das Kurs-Label aus ist + // (sonst verliert der Faecher seinen Anker). + g_conePx = pxv; + if(!InpShowPrice) continue; string pn = PFX + "PRICE"; if(ObjectFind(0, pn) < 0) { diff --git a/web/app.js b/web/app.js index 86b0a24..def5ac4 100644 --- a/web/app.js +++ b/web/app.js @@ -308,6 +308,37 @@ function renderCandles(ca) { (backtest_candle_fade.py).`; } +// Wahrscheinlichkeits-Kegel. ⚠ Zeigt STREUUNG, nicht Richtung — und beschriftet +// mit der REAL gemessenen Abdeckung (~76 %), nicht mit dem Nennwert 80 %. Der Kegel +// ist out-of-sample geprüft (analyze_cone.py, Fit H1 → Abdeckung H2) und damit nach +// dem P(break)-Modell die zweite kalibrierte Komponente im Projekt. +function renderCone(cn) { + const el = $("cone-list"); + if (!el) return; + const lv = (cn && cn.levels) || []; + if (!lv.length) { + el.innerHTML = ``; + return; + } + const byMin = {}; + lv.forEach(x => { (byMin[x.minutes] = byMin[x.minutes] || {})[x.band] = x; }); + el.innerHTML = Object.keys(byMin).sort((a, b) => a - b).map(mn => { + const b80 = byMin[mn][80], b50 = byMin[mn][50]; + const cov = b80 && b80.coverage != null ? `${fmt(b80.coverage, 0)}%` : "~"; + return `
+ ${mn} min + ${b80 ? `${fmt(b80.lo, 2)} … ${fmt(b80.hi, 2)}` : "—"} + trifft ${cov} + ${b50 ? `enger: ${fmt(b50.lo, 2)} … ${fmt(b50.hi, 2)}` : ""} +
`; + }).join("") + + `
📐 Streuung, keine Richtung. Die Spanne sagt, wie + WEIT der Kurs plausibel läuft — nicht wohin. Out-of-sample geprüft + (analyze_cone.py, Fit auf einem Halbjahr, Abdeckung im anderen + gemessen): das nominale 80 %-Band trifft real ~76 %, ist also leicht zu eng. + Kein Signal, kein Einfluss auf die Empfehlung.
`; +} + function renderPatterns(pt) { const el = $("pt-list"); if (!el) return; @@ -432,6 +463,7 @@ function render(d) { renderCircuitBreaker(d.circuit_breaker); renderPatterns(d.patterns); renderCandles(d.candle_anat); + renderCone(d.cone); renderStructure(d.structure); renderStatsCompact(d.stats_compact, d.market); renderGaps(d.gaps, d.market); diff --git a/web/index.html b/web/index.html index 4d265b7..e491aaf 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -132,6 +132,13 @@
+ +
+

Erwartete Spanne kalibriert · keine Richtung

+
+
+

KI-Copilot

@@ -237,6 +244,6 @@ - + diff --git a/web/style.css b/web/style.css index a7f41d2..6800c08 100644 --- a/web/style.css +++ b/web/style.css @@ -180,6 +180,14 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px} .ca-ago{margin-left:auto;font-size:12px;color:var(--dim);font-family:var(--mono)} .ca-bars{font-size:12px;margin-top:3px;font-family:var(--mono);color:var(--dim)} .ca-note{font-size:12px;color:var(--dim);margin-top:3px;line-height:1.4} +/* Erwartete Spanne — neutral, bewusst ohne Richtungsfarbe */ +.cone-row{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap; + padding:6px 0;border-bottom:1px solid var(--border)} +.cone-row:last-of-type{border-bottom:none} +.cone-h{font-weight:700;font-size:14px;color:var(--bright);min-width:64px} +.cone-band{font-family:var(--mono);font-size:14px;color:var(--bright)} +.cone-band i{font-style:normal;font-size:11px;color:var(--dim);margin-left:6px} +.cone-inner{margin-left:auto;font-family:var(--mono);font-size:12px;color:var(--dim)} #card-verdict{border-color:var(--accent)} .vd-head{display:flex;align-items:center;gap:14px;margin-bottom:10px} .vd-ring{width:60px;height:60px;border-radius:50%;flex:0 0 auto;