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
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 **~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
„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:<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.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;<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):
# 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,
+151 -16
View File
@@ -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 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",
},
# (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,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:
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__":
+75 -2
View File
@@ -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;<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
// 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;<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
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)
{
+32
View File
@@ -308,6 +308,37 @@ function renderCandles(ca) {
(<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) {
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);
+9 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=123">
<link rel="stylesheet" href="style.css?v=124">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -132,6 +132,13 @@
<div class="ca-list" id="ca-list"><span class="dim"></span></div>
</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 -->
<section class="card wide" id="card-agent">
<h2>KI-Copilot</h2>
@@ -237,6 +244,6 @@
<div id="toast" class="toast hidden"></div>
<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>
</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-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;