Kanal-Pullback ueber alle Zeitebenen gemessen — 24/24 negativ

Anlass: User-Beobachtung im M5-Chart "der Kurs folgt dem Marktstrukturkanal und
bricht die untere Linie nicht — koennen wir das nutzen?". Die vorhandene Messung
(backtest_structure.py, 20.07.) lief NUR auf M30; M5 war ungetestet.

Neues Skript backtest_structure_tf.py — bewusst mit dem KANONISCHEN Live-Exit
(exit_model.LIVE: Trail 1,0 + Time-Stop + TP) statt der Exit-Kopie der alten
Datei (Trail 1,5, kein Time-Stop). Die alte Datei bleibt unveraendert, weil die
Migrations-Regel eine Umstellung nur bei identischen Zahlen erlaubt.

Getestet: M5/M15/M30 x zwei Lesarten des M5-Kanals (60 Bars = 5 h "was man im
Chart sieht" und 360 Bars = 30 h zeitgleich zu M30) x drei Regel-Varianten,
80k Bars je Zeitebene, 2 Haelften, Echtkosten.

Ergebnis: 24 Zellen, ALLE negativ, PF 0,66-0,99.
- Trefferquote 37-43 % ueber 1200-2400 Beruehrungen je Haelfte: der Kanalrand
  haelt in WENIGER als der Haelfte der Faelle. Die im Chart sichtbaren "Halter"
  sind Selektion.
- M5 ist die SCHLECHTESTE Zeitebene (-0,201 vs -0,103 auf M30).
- Keine Parameterfrage: 60-Bar- und 360-Bar-Kanal liefern identische Zahlen.

Legacy-Recheck-Bonus: die M30-Zeile reproduziert den Befund vom 20.07.
(-0,103/-0,019 gegen damals -0,05...-0,11) — der alte Schluss war NICHT durch
das abweichende Exit-Modell verzerrt.

Mechanismus: hinter einer Regressionslinie liegt nichts — keine Order, kein
Pivot. Darin liegt der Unterschied zu P(break) auf einem echten S/R-Level.
19. verworfener Signal-Eingriff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-03 16:37:52 +02:00
co-authored by Claude Opus 5
parent 00c78201fa
commit 5f94917014
2 changed files with 192 additions and 0 deletions
+30
View File
@@ -1747,6 +1747,36 @@ 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.
- **NACHTEST über ALLE Zeitebenen = ERNEUT VERWORFEN (`backtest_structure_tf.py`,
2026-08-03, User-Beobachtung im M5-Chart: „der Kurs folgt dem Kanal und bricht die
untere Linie nicht — können wir das nutzen?"):** Die alte Messung lief NUR auf M30,
M5 war streng genommen ungetestet. Jetzt mit dem **kanonischen Live-Exit**
(`exit_model.LIVE`: Trail 1,0 + Time-Stop + TP — die alte Datei nutzt eine Exit-KOPIE
mit Trail 1,5 OHNE Time-Stop) über M5/M15/M30, zwei Lesarten des M5-Kanals und drei
Regel-Varianten, 80k Bars je Zeitebene, 2 Hälften, Echtkosten:
| Zeitebene / Kanal | X=0,20 | X=0,30 | +Swing |
|---|---|---|---|
| **M5, 60 Bars** (=5 h, „was man im Chart sieht") | **0,201/0,017** | 0,170/0,054 | 0,161/0,082 |
| M5, 360 Bars (=30 h, zeitgleich zu M30) | 0,206/0,017 | 0,179/0,054 | 0,159/0,082 |
| M15, 120 Bars | 0,032/0,003 | 0,068/0,034 | 0,043/0,033 |
| M30, 60 Bars (die ALTE Messung) | 0,103/0,019 | 0,094/0,013 | 0,090/0,024 |
**24 Zellen, ALLE negativ**, PF 0,660,99. Drei Befunde:
(a) **Trefferquote 3743 %** über 1.2002.400 Berührungen je Hälfte — der Kanalrand
hält in WENIGER als der Hälfte der Fälle. Die im Chart sichtbaren „Halter" sind
Selektion: die Durchbrüche sehen hinterher nicht mehr nach Kanal aus.
(b) **M5 ist die SCHLECHTESTE Zeitebene** (0,201 vs. 0,103 auf M30) — feiner
auflösen erzeugt mehr Berührungen, und jede kostet Spread.
(c) **Keine Parameterfrage:** 60-Bar- und 360-Bar-Kanal liefern praktisch IDENTISCHE
Zahlen. Zwei so verschiedene Fenster mit gleichem Ergebnis → es liegt am Mechanismus.
**Legacy-Recheck-Bonus:** die M30-Zeile **reproduziert** den Befund vom 20.07.
(0,103/0,019 gegen damals 0,05…−0,11) — der alte Schluss war NICHT durch das
abweichende Exit-Modell verzerrt.
**Mechanismus:** Der Kanalrand ist eine Regressionslinie durch die letzten N Closes —
dahinter liegt **nichts**: keine Order, kein Pivot, keine Struktur. Er hält nur,
solange der Trend ohnehin läuft. Genau darin liegt der Unterschied zu **P(break) auf
einem echten S/R-Level** (Pivot = Stelle, an der real gehandelt wurde), das
out-of-sample kalibriert ist (AUC 0,65 bzw. 0,680,72). „Linie im Chart" ist nicht
„Struktur im Markt". **19. verworfener Signal-Eingriff.**
- **⚠⚠ P(break) LIVE-AUSWERTUNG (`analyze_pbreak_live.py`, 2026-07-31, 2275 ausgewertete - **⚠⚠ P(break) LIVE-AUSWERTUNG (`analyze_pbreak_live.py`, 2026-07-31, 2275 ausgewertete
Vorhersagen seit 23.07.): DAS MODELL HÄLT LIVE NICHT, WAS DER BACKTEST VERSPRACH — Vorhersagen seit 23.07.): DAS MODELL HÄLT LIVE NICHT, WAS DER BACKTEST VERSPRACH —
B5-Fall.** Fällige Messung aus dem Reminder, jetzt erledigt. B5-Fall.** Fällige Messung aus dem Reminder, jetzt erledigt.
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Marktstruktur-Kanal als SIGNAL — über MEHRERE Zeitebenen (2026-08-03).
Anlass: User-Beobachtung im M5-Chart „der Kurs folgt dem Marktstrukturkanal und
bricht die untere Linie nicht — können wir das nutzen?". Die vorhandene Messung
(`backtest_structure.py`, 2026-07-20) lief ausschliesslich auf **M30** und fiel
in beiden Hälften durch. M5 ist damit streng genommen eine UNGETESTETE Variante.
Unterschiede zu `backtest_structure.py` — bewusst, und beide relevant:
1. **Kanonischer Exit** (`core/exit_model.simulate`, LIVE) statt der dortigen
Exit-KOPIE. Jene nutzt Trail **1,5** und kennt weder Time-Stop noch TP; live
läuft seit 2026-07-31 Trail **1,0** MIT Time-Stop. Für eine NEUE Messung ist
der kanonische Kern richtig (die alte Datei bleibt unverändert — sie umzu-
stellen wäre laut Migrations-Regel nur erlaubt, wenn die Zahlen gleich blieben).
2. **Zeitebene als Parameter**, plus zwei Lesarten des „M5-Kanals":
· `bars` — gleiche BAR-Anzahl wie auf M30 (=schnellerer, kürzerer Kanal;
das, was man im M5-Chart tatsächlich sieht)
· `time` — gleiche ZEIT-Spanne (Fenster ×6 auf M5) = derselbe Kanal, nur
feiner aufgelöst
Beide zu testen ist Pflicht: kippt das Vorzeichen zwischen ihnen, ist es
Rauschen (die ORB-/SMC-Lehre — Nachbar-Parameter müssen mithalten).
Maßstab wie immer: beidhälftig positiv UND parameter-robust. Vergleich:
Squeeze ØR +0,14…+0,23 bei PF>1.
Aufruf: python backtest_structure_tf.py [bars]
"""
import sys
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
from core.structure import _pivots, _classify, _channel
_ATRMIN = 0.06
_COOL = 3
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])))
return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
if i else None for i in range(len(C))]
def st(Rs):
if not Rs:
return None
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99), sum=s)
def line(lbl, s):
if not s:
return f" {lbl:<26} — (keine Trades)"
return (f" {lbl:<26} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}")
def run(H, L, C, A, SP, lo, hi, rule, X, look, reg):
"""Sequentielle 1-Positions-Sim mit dem KANONISCHEN Live-Exit."""
def cost(i, atr):
return (SP[i] if SP[i] > 0 else 0.0225) / atr
Rs = []
i = max(lo, look)
prev_pos = None
while i < hi:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; prev_pos = None; continue
ch = _channel(C[max(0, i - reg + 1):i + 1], atr)
if not ch:
i += 1; prev_pos = None; continue
pos, cdir = ch["pos"], ch["dir"]
d = 0
if rule in ("A", "A2"):
# frischer Eintritt ins untere (Aufwärtskanal) bzw. obere (Abwärts) Drittel
if cdir == "up" and pos <= X and (prev_pos is None or prev_pos > X):
d = 1
elif cdir == "down" and pos >= 1 - X and (prev_pos is None or prev_pos < 1 - X):
d = -1
if d != 0 and rule == "A2":
pl = _pivots(H[max(0, i - look):i + 1], L[max(0, i - look):i + 1], 3)
lab = _classify(pl)
recent = [x["type"] for x in lab[-4:]]
ups = sum(1 for t in recent if t in ("HH", "HL"))
dns = sum(1 for t in recent if t in ("LH", "LL"))
strend = ("up" if ups >= 3 and ups > dns else
"down" if dns >= 3 and dns > ups else "range")
if (d > 0 and strend != "up") or (d < 0 and strend != "down"):
d = 0
prev_pos = pos
if d == 0:
i += 1; continue
entry = C[i]
a = max(atr, _ATRMIN)
r, xj = simulate(entry, d, a, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
Rs.append(r - cost(i, a))
i = xj + _COOL
prev_pos = None
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
point = mt5.symbol_info(sym).point
TFS = [("M5", mt5.TIMEFRAME_M5), ("M15", mt5.TIMEFRAME_M15),
("M30", mt5.TIMEFRAME_M30)]
data = {}
for lbl, tf in TFS:
b = mt5.copy_rates_from_pos(sym, tf, 0, n)
if b is not None and len(b) > 5000:
data[lbl] = ([float(x["high"]) for x in b], [float(x["low"]) for x in b],
[float(x["close"]) for x in b],
[float(x["spread"]) * point for x in b])
mt5.shutdown()
print("=" * 92)
print(f" Kanal-Pullback über Zeitebenen — {sym}, kanonischer Live-Exit "
f"(SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}× · Time-Stop), Echtkosten")
print(" Regel: im Aufwärtskanal am UNTEREN Drittel long / im Abwärtskanal am OBEREN short")
print(" Maßstab: beidhälftig ØR>0 UND PF>1 UND Nachbar-Varianten halten mit")
print("=" * 92)
# (reg, look) je Lesart. M30-Referenz = 60/220 wie in backtest_structure.py.
plans = [
("M5 Kanal 60 Bars (=5 h, 'was man im M5-Chart sieht')", "M5", 60, 220),
("M5 Kanal 360 Bars (=30 h, zeitgleich zum M30-Kanal)", "M5", 360, 1320),
("M15 Kanal 120 Bars (=30 h, Nachbar-Zeitebene)", "M15", 120, 440),
("M30 Kanal 60 Bars (=30 h, die ALTE Messung)", "M30", 60, 220),
]
for title, tfl, reg, look in plans:
if tfl not in data:
print(f"\n {title}: keine Daten"); continue
H, L, C, SP = data[tfl]
A = _atr_series(H, L, C)
N = len(C); mid = N // 2
print(f"\n {title} [{N} Bars]")
for rl, X, sub in (("A", 0.20, "Kanal-Pullback X=0.20"),
("A", 0.30, "Kanal-Pullback X=0.30"),
("A2", 0.25, "+Swing-Filter X=0.25")):
s1 = st(run(H, L, C, A, SP, 0, mid, rl, X, look, reg))
s2 = st(run(H, L, C, A, SP, mid, N - LIVE.max_hold - 1, rl, X, look, reg))
print(f" · {sub}")
print(line("H1 (alt)", s1))
print(line("H2 (neu)", s2))
ok = (s1 and s2 and s1["oR"] > 0 and s2["oR"] > 0
and s1["pf"] > 1 and s2["pf"] > 1)
print(f"{'ROBUST' if ok else 'fällt durch'}")
print()
if __name__ == "__main__":
main()