C: Wahrscheinlichkeits-Kegel (kalibriert) + Config-Waechter im Reminder

C - core/cone.py + Kachel #card-cone + MT5 v1.31 (CN;<min>;<lo>;<hi>):
"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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-31 08:25:29 +02:00
co-authored by Claude Opus 5
parent 03be3b8f5f
commit d9cb54a52c
9 changed files with 508 additions and 21 deletions
+29 -1
View File
@@ -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 Continuation = breakeven-negativ (0,011/0,005, Momentum-Klasse). Damit ist auch
die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie
TF-Ampel/Squeeze), steuert nichts. 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 **~35 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;<minuten>;<lo>;<hi>` (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 - **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 „Kerzengröße und Dochtlänge … im Kontext"): REINE ANZEIGE — und die Kachel widerspricht
bewusst der Lehrbuch-Deutung.** Erkennt mechanisch aus M5-OHLCV: Marubozu · Langer 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 Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine **negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap. 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 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 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` `font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge`
+117
View File
@@ -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()
+68
View File
@@ -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 ~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}
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}
+19
View File
@@ -43,6 +43,7 @@ from core.gaps import GapAnalyzer
from core.structure import MarketStructure from core.structure import MarketStructure
from core.patterns import PatternDetector from core.patterns import PatternDetector
from core.candles import CandleAnatomy from core.candles import CandleAnatomy
from core.cone import build as _cone_build
from core.candle_logger import CandleLogger from core.candle_logger import CandleLogger
from core.wave_rec import (_atr as _wr_atr, _ema_last, from core.wave_rec import (_atr as _wr_atr, _ema_last,
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX) _EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX)
@@ -2093,6 +2094,19 @@ class TradingEngine:
lines.append(f"PX;{_bid}") lines.append(f"PX;{_bid}")
except Exception: except Exception:
pass pass
# WAHRSCHEINLICHKEITS-KEGEL: CN;<minuten>;<lo>;<hi> 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): # 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 # PL;wert;waehrung. **NETTO** wie im Dashboard — bei Gewinn abzüglich der
# WHT-Quellensteuer, die der Broker einbehält (`[trading] wht_pct`); bei # 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: # Kerzen-Anatomie (M5) — reine Anzeige, KEIN Verdict-Gewicht (gemessen:
# Lehrbuch-Deutung invertiert, Handelssignal durchgefallen). # Lehrbuch-Deutung invertiert, Handelssignal durchgefallen).
"candle_anat": self.candle_anat.snapshot(), "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"), "stats_compact": {"week": self._compact_stats("week"),
"all": self._compact_stats("all")}, "all": self._compact_stats("all")},
"close_alert_count": self._close_alert_count, "close_alert_count": self._close_alert_count,
+151 -16
View File
@@ -94,16 +94,8 @@ CHECKS = [
"need": 30, "unit": "S/R-Closes seit der Umstellung", "need": 30, "unit": "S/R-Closes seit der Umstellung",
"cmd": "sr_close-Trades vor/nach 2026-07-30 vergleichen (ØEUR, Trefferquote)", "cmd": "sr_close-Trades vor/nach 2026-07-30 vergleichen (ØEUR, Trefferquote)",
}, },
{ # (erledigt 2026-07-30: Chartmuster-Kontrolltest gegen generischen Swing-Bruch —
"key": "patterns_control", # Muster schlagen die Kontrolle in beiden Hälften (+0,022/+0,108) → Gewicht 1,0)
"title": "Chartmuster: Kontrolltest gegen generischen Breakout",
"why": "backtest_patterns.py zeigte positiven ØR — aber die Ziel-Trefferquote "
"ist nur 1338 %, 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",
},
{ {
"key": "m1_squeeze", "key": "m1_squeeze",
"title": "M1-Squeeze (Variante B) backtesten", "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,500,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 07 + 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: def _popup(title: str, text: str) -> None:
if DRY or STATUS: if DRY or STATUS:
print(f"[POPUP] {title}\n{text}") print(f"[POPUP] {title}\n{text}")
@@ -156,6 +249,21 @@ def main() -> None:
else: else:
pending.append((c, have, "sammelt")) 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: if STATUS or DRY:
print("=" * 78) print("=" * 78)
print(" AUSSTEHENDE MESSUNGEN — Status") print(" AUSSTEHENDE MESSUNGEN — Status")
@@ -170,24 +278,51 @@ def main() -> None:
print(f" {have:.0f}/{c['need']} {c['unit']} ({pct:.0f} %)") print(f" {have:.0f}/{c['need']} {c['unit']} ({pct:.0f} %)")
if not due: if not due:
print("\n (nichts fällig)") 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 return
if not due: if not due and not drift_new:
return return
lines = [] lines = []
for c, have in due: for c, have in due:
lines.append(f"{c['title']}\n {have:.0f}/{c['need']} {c['unit']}\n" lines.append(f"{c['title']}\n {have:.0f}/{c['need']} {c['unit']}\n"
f" {c['why']}\n -> {c['cmd']}") f" {c['why']}\n -> {c['cmd']}")
text = ("Diese Messungen haben jetzt genug Daten:\n\n" for d, cur, val, _m in drift_new:
+ "\n\n".join(lines) lines.append(f"• CONFIG geaendert: {d['key']} = '{cur}' "
+ "\n\nStatus aller Punkte:\n" f"(validiert gegen '{val}')\n"
" python measurement_reminder.py --status") f" {d['why']}\n -> neu rechnen: {d['test']}")
_popup(f"Oil-Bot: {len(due)} Messung(en) faellig", text) 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: for c, _ in due:
try: try:
open(os.path.join(MARKER_DIR, c["key"]), "w").close() open(os.path.join(MARKER_DIR, c["key"]), "w").close()
except Exception: except Exception:
pass 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__": if __name__ == "__main__":
+75 -2
View File
@@ -12,7 +12,13 @@
//| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). | //| 4) Der Bot muss laufen (schreibt die CSV alle ~5 s). |
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
#property copyright "Oil Trading Bot" #property copyright "Oil Trading Bot"
#property version "1.30" #property version "1.31"
// v1.31 (2026-07-31): WAHRSCHEINLICHKEITS-KEGEL (CN;<minuten>;<lo>;<hi>) — 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 // v1.30 (2026-07-30): Konsolidierungs-Box BESCHRIFTET (User-Frage „was bedeutet die
// graue Box") — sichtbares Label an der Oberkante, rechtsbuendig wie die // graue Box") — sichtbares Label an der Oberkante, rechtsbuendig wie die
// uebrigen: „Konsolidierung (10-Bar-Spanne)" bzw. bei Kompression // 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 color InpConsolColor = C'88,166,255'; // Box-Farbe (blau)
input int InpConsolAlpha = 8; // Deckkraft in % (komprimiert: x2) input int InpConsolAlpha = 8; // Deckkraft in % (komprimiert: x2)
input int InpConsolBars = 10; // Box-Laenge in Bars (= _SQ_N im Bot) 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 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 InpPnLUpColor = C'0,255,127'; // G/V im Plus (neongruen)
input color InpPnLDownColor = C'255,69,0'; // G/V im Minus (rot) 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 PFX = "SRB_"; // Präfix S/R-Linien (aus Datei)
const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart) const string RPFX = "SRR_"; // Präfix dynamische Range (aus Chart)
string g_last = "\x01"; // letzter Datei-Inhalt (gegen Flackern) 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? bool g_range_drawn = false; // hat DIESE Instanz die Range gezeichnet?
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
@@ -394,6 +408,9 @@ void Redraw()
string rows[]; string rows[];
int nr = StringSplit(content, '\n', rows); int nr = StringSplit(content, '\n', rows);
int idx = 0; 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++) for(int r = 0; r < nr; r++)
{ {
@@ -650,13 +667,69 @@ void Redraw()
ObjectSetString(0, ctn, OBJPROP_TOOLTIP, ctip); ObjectSetString(0, ctn, OBJPROP_TOOLTIP, ctip);
continue; continue;
} }
// WAHRSCHEINLICHKEITS-KEGEL: CN;<minuten>;<lo>;<hi>
// 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 // AKTUELLER KURS oben rechts, fett (User-Wunsch): PX;bid
if(typ == "PX") if(typ == "PX")
{ {
if(k < 2) continue; if(k < 2) continue;
if(!InpShowPrice) continue;
double pxv = StringToDouble(p[1]); double pxv = StringToDouble(p[1]);
if(pxv <= 0) continue; 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"; string pn = PFX + "PRICE";
if(ObjectFind(0, pn) < 0) if(ObjectFind(0, pn) < 0)
{ {
+32
View File
@@ -308,6 +308,37 @@ function renderCandles(ca) {
(<code>backtest_candle_fade.py</code>).</div>`; (<code>backtest_candle_fade.py</code>).</div>`;
} }
// 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 = `<span class="dim">—</span>`;
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 `<div class="cone-row">
<span class="cone-h">${mn} min</span>
<span class="cone-band">${b80 ? `${fmt(b80.lo, 2)}${fmt(b80.hi, 2)}` : "—"}
<i>trifft ${cov}</i></span>
<span class="cone-inner">${b50 ? `enger: ${fmt(b50.lo, 2)}${fmt(b50.hi, 2)}` : ""}</span>
</div>`;
}).join("") +
`<div class="pt-disclaimer">📐 <b>Streuung, keine Richtung.</b> Die Spanne sagt, wie
WEIT der Kurs plausibel läuft — nicht wohin. Out-of-sample geprüft
(<code>analyze_cone.py</code>, 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.</div>`;
}
function renderPatterns(pt) { function renderPatterns(pt) {
const el = $("pt-list"); const el = $("pt-list");
if (!el) return; if (!el) return;
@@ -432,6 +463,7 @@ function render(d) {
renderCircuitBreaker(d.circuit_breaker); renderCircuitBreaker(d.circuit_breaker);
renderPatterns(d.patterns); renderPatterns(d.patterns);
renderCandles(d.candle_anat); renderCandles(d.candle_anat);
renderCone(d.cone);
renderStructure(d.structure); renderStructure(d.structure);
renderStatsCompact(d.stats_compact, d.market); renderStatsCompact(d.stats_compact, d.market);
renderGaps(d.gaps, d.market); renderGaps(d.gaps, d.market);
+9 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117"> <meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title> <title>Oil · MT5</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=123"> <link rel="stylesheet" href="style.css?v=124">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -132,6 +132,13 @@
<div class="ca-list" id="ca-list"><span class="dim"></span></div> <div class="ca-list" id="ca-list"><span class="dim"></span></div>
</section> </section>
<!-- Wahrscheinlichkeits-Kegel — Streuung, NICHT Richtung. Out-of-sample
kalibriert (analyze_cone.py); die UI nennt die REAL gemessene Abdeckung -->
<section class="card wide" id="card-cone">
<h2>Erwartete Spanne <span class="ca-tf">kalibriert · keine Richtung</span></h2>
<div class="cone-list" id="cone-list"><span class="dim"></span></div>
</section>
<!-- KI-Agent --> <!-- KI-Agent -->
<section class="card wide" id="card-agent"> <section class="card wide" id="card-agent">
<h2>KI-Copilot</h2> <h2>KI-Copilot</h2>
@@ -237,6 +244,6 @@
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script> <script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=123"></script> <script src="app.js?v=124"></script>
</body> </body>
</html> </html>
+8
View File
@@ -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-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-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} .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)} #card-verdict{border-color:var(--accent)}
.vd-head{display:flex;align-items:center;gap:14px;margin-bottom:10px} .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; .vd-ring{width:60px;height:60px;border-radius:50%;flex:0 0 auto;