diff --git a/CLAUDE.md b/CLAUDE.md index 9b65235..62d915d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6106,6 +6106,44 @@ was arithmetisch unmöglich ist und den Fehler verriet. Real sind **~11 %** der M5-Kerzen unverändert. **Merksatz: wenn A und Nicht-A sich nicht zu 100 % addieren, ist die Messung kaputt, nicht der Markt.** +### ⚠⚠ KERZENMUSTER FÜR DIE RICHTUNG DER NÄCHSTEN KERZE = NEIN (`backtest_kerzenmuster_richtung.py`, 2026-08-17) + +User-Anschlussfrage. Gemessen mit den **echten Muster-Schwellen** aus +`core/candles.py` (1:1 importiert, kein Nachbau), 80k M5-Bars, 2 Halbjahre, +unveränderte Schlusskurse ausgeschlossen, 95-%-Intervall je Zelle. +Bewertet wird die **Lehrbuch-Lesart** (±1), damit sichtbar wird, ob sie stimmt. + +| Muster | H1 Treffer | H2 Treffer | Ø Bewegung | +|---|---|---|---| +| Marubozu | **46,5 % ❌** | **45,2 % ❌** | −0,0019 / +0,0077 $ | +| Engulfing | **47,4 % ❌** | 49,5 % | −0,0018 / −0,0009 | +| Outside Bar | **47,3 % ❌** | 48,9 % | −0,0019 / +0,0055 | +| langer Körper | 48,5 % | **47,6 % ❌** | +0,0009 / +0,0027 | +| langer Docht OBEN | 49,4 % | **47,8 % ❌** | +0,0011 / +0,0020 | +| langer Docht UNTEN | 50,6 % | 50,6 % | +0,0004 / **−0,0037** | + +✅ **KEIN Muster erfüllt die vorab fixierte Regel.** Die Trefferquoten liegen +zwischen **45,2 und 50,6 %** — am oder **unter** dem Münzwurf; **fünf Zellen** +haben ein Intervall komplett unter 50, sind in der Lehrbuch-Lesart also +**verlässlich falsch**. Die einzige beidhälftig über 50 (langer Docht UNTEN, +50,6 %) hat in beiden Hälften ein Intervall MIT der 50 und eine Ø-Bewegung, die +das **Vorzeichen kippt**. +✅ **Das reproduziert den 60-Minuten-Befund vom 31.07.** („die Lehrbuch-Lesart +ist in BEIDEN Hälften invertiert") jetzt auch auf dem **1-Bar-Horizont** — mit +den echten Klassen statt der groben Proxys. Damit ist die Kerzen-Klasse zum +**vierten** Mal gemessen (Forward-Return 60 min · Fade als Handelsregel · +Fade mit Ausführungs-Achse, 40 Zellen · jetzt 1-Bar-Richtung). +⚠ **Und selbst wenn man sie umdreht, zahlt es nicht:** die grösste Ø-Bewegung +über alle Zellen ist **0,0077 $** — und die kippt in der anderen Hälfte auf +−0,0019. Der halbe Spread beträgt **0,0100 $**, eine M5-Kerze bewegt sich im +Median **0,0400 $**. + +✅ **URTEIL: der Pfeil bleibt bei der kalibrierten Level-Aussage.** Es gibt +keine Kerzen-Grundlage für eine Richtungsschätzung der nächsten Kerze — weder +in der Lehrbuch- noch in der umgedrehten Lesart. +⚠ Konsistent mit der Kachel-Entscheidung vom 31.07.: die Kerzen-Anatomie ist +bewusst **neutral** gefärbt und nennt die gemessene statt der Lehrbuch-Lesart. + ### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11) Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?" diff --git a/backtest_kerzenmuster_richtung.py b/backtest_kerzenmuster_richtung.py new file mode 100644 index 0000000..19d8548 --- /dev/null +++ b/backtest_kerzenmuster_richtung.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Sagt das KERZENMUSTER die Richtung der naechsten Kerze? (User 2026-08-17) + +Vorlauf: `backtest_naechste_kerze.py` hat gezeigt, dass die naechste M5-Kerze +grundsaetzlich nur ueber 1-Bar-Mean-Reversion schwach vorhersagbar ist (51,3 %), +und dass der Effekt 50-100x unter dem Spread liegt. Dort wurden aber nur GROBE +Proxys geprueft (letzte Kerze fort/umkehr, langer Docht). + +Hier die richtige Frage: die echten MUSTER-Klassen — mit denselben Schwellen wie +der Live-Detektor `core/candles.py` (Marubozu, langer Koerper, langer Docht +oben/unten, Spinning Top, Doji, Engulfing) plus die Zwei-Bar-Muster +Inside/Outside Bar, die dort nicht klassifiziert werden. + +⚠ WAS BEREITS GEMESSEN IST (und was hier NEU ist): + `backtest_candles.py` (31.07.) hat die Muster gegen den Forward-Return ueber + **60 Minuten** gemessen — Ergebnis: die Lehrbuch-Lesart ist in BEIDEN Haelften + INVERTIERT („langer Koerper = Fortsetzung" -0,059/-0,076; „2x Volumen = echt" + -0,117/-0,204, monoton falsch herum). `backtest_candle_fade.py` und `_fade2` + haben den Fade als HANDELSREGEL zweimal verworfen (zuletzt alle 40 Zellen + negativ). NEU ist allein der **1-Bar-Horizont** — die Frage des Users. + +VORAB FIXIERTE REGEL: ein Muster taugt, wenn es + (1) in BEIDEN Haelften > 50 % trifft, (2) beide 95-%-Intervalle die 50 + ausschliessen, (3) n >= 300 je Haelfte, und (4) die Oe-Bewegung in + Vorhersagerichtung den HALBEN Spread (0,0100 $) schlaegt. +⚠ (4) ist die harte Huerde: eine M5-Kerze bewegt sich im Median 0,0400 $, der +Spread ist 0,0200 $ = 50 % davon. + +⚠ UNVERAENDERTE Schlusskurse werden AUSGESCHLOSSEN (~11 % der Kerzen). Sie als +Fehltreffer zu zaehlen liess im Vorlauf ein Muster UND sein Gegenteil beide +unter 50 % liegen — arithmetisch unmoeglich, und der Verraeter des Fehlers. + +Aufruf: python backtest_kerzenmuster_richtung.py [bars] +""" +from __future__ import annotations + +import random +import sys + +import MetaTrader5 as mt5 + +# Schwellen 1:1 aus dem Live-Detektor +from core.candles import (_DOJI_BR, _LONG_BR, _LONG_X, _MARU_BR, _REF_N, + _SPIN_BR, _SPIN_W, _VOL_HOT, _WICK) + + +def muster(O, H, L, C, V, i): + """Klassifiziert Bar i — gibt (Name, Richtungs-Erwartung) zurueck. + + Die Richtungs-Erwartung ist die LEHRBUCH-Lesart (+1 aufwaerts / -1 abwaerts), + damit sichtbar wird, ob sie stimmt oder — wie ueber 60 min gemessen — + invertiert ist. `0` = Muster ohne Richtungsaussage. + """ + sp = H[i] - L[i] + if sp <= 0: + return None + body = C[i] - O[i] + ab = abs(body) + br = ab / sp + ob = H[i] - max(O[i], C[i]) # oberer Docht + un = min(O[i], C[i]) - L[i] # unterer Docht + d = 1 if body > 0 else -1 + lo = max(0, i - _REF_N) + med_body = sorted(abs(C[j] - O[j]) for j in range(lo, i)) or [0] + med = med_body[len(med_body) // 2] if med_body else 0 + lang = med > 0 and ab >= _LONG_X * med + + # Zwei-Bar-Muster zuerst (spezifischer) + if H[i] <= H[i - 1] and L[i] >= L[i - 1]: + return ("Inside Bar", 0) + if H[i] > H[i - 1] and L[i] < L[i - 1]: + return ("Outside Bar", d) + vor = abs(C[i - 1] - O[i - 1]) + if ab > vor > 0 and (C[i] - O[i]) * (C[i - 1] - O[i - 1]) < 0 \ + and max(O[i], C[i]) >= max(O[i - 1], C[i - 1]) \ + and min(O[i], C[i]) <= min(O[i - 1], C[i - 1]): + return ("Engulfing", d) # Lehrbuch: in Richtung der Huellkerze + + if br >= _MARU_BR: + return ("Marubozu", d) + if br >= _LONG_BR and lang: + return ("langer Koerper", d) # Lehrbuch: Fortsetzung + if br <= _DOJI_BR: + return ("Doji", 0) + if br <= _SPIN_BR and ob > _SPIN_W * sp and un > _SPIN_W * sp: + return ("Spinning Top", 0) + if ob / sp >= _WICK: + return ("langer Docht OBEN", -1) # Lehrbuch: abwaerts + if un / sp >= _WICK: + return ("langer Docht UNTEN", +1) # Lehrbuch: aufwaerts + return None + + +def ci(tr, n, seed=5, w=2000): + if n < 50: + return None, None + r = random.Random(seed); p = tr / n + b = sorted(sum(1 if r.random() < p else 0 for _ in range(n)) / n for _ in range(w)) + return 100 * b[int(.025 * w)], 100 * b[int(.975 * w)] + + +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) + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + point = mt5.symbol_info(sym).point + mt5.shutdown() + O = [float(b["open"]) for b in bars]; H = [float(b["high"]) for b in bars] + L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars] + V = [float(b["tick_volume"]) for b in bars] + SP = sorted(float(b["spread"]) * point for b in bars) + med_sp = SP[len(SP) // 2] + mid = len(C) // 2 + + print(f"\n {sym} · {len(C)} M5-Bars · Muster-Schwellen 1:1 aus core/candles.py") + print(f" Huerde: halber Spread {med_sp/2:.4f} $ — so viel muss die Ø-Bewegung") + print(f" in Vorhersagerichtung mindestens bringen.\n") + print(f" {'Muster':<22}{'Lehrb.':>7}{'Haelfte':>8}{'n':>7}{'Treffer':>9}" + f"{'95-%-KI':>16}{'Ø Bewegung':>13}") + + # sammeln + daten: dict = {} + for i in range(_REF_N + 2, len(C) - 1): + m = muster(O, H, L, C, V, i) + if not m: + continue + name, erw = m + if erw == 0: + continue # ohne Richtungsaussage nicht bewertbar + bew = (C[i + 1] - C[i]) * erw + if bew == 0: + continue # unveraenderter Schluss — ausgeschlossen + daten.setdefault(name, {"H1": [], "H2": []}) + daten[name]["H1" if i < mid else "H2"].append(bew) + + besteht = [] + for name in sorted(daten): + zeilen = [] + for lbl in ("H1", "H2"): + w = daten[name][lbl] + if len(w) < 50: + zeilen.append(None); continue + tr = sum(1 for x in w if x > 0) + q = 100 * tr / len(w) + u, o = ci(tr, len(w)) + mb = sum(w) / len(w) + flag = "" if u is None or (u <= 50 <= o) else (" ✅" if u > 50 else " ❌") + print(f" {name:<22}{'±1':>7}{lbl:>8}{len(w):>7}{q:>8.1f}%" + f" [{u:>4.1f} … {o:>4.1f}]{mb:>+11.4f} $" + flag) + zeilen.append((len(w), q, u, o, mb)) + if all(z and z[0] >= 300 and z[2] > 50 for z in zeilen): + besteht.append((name, zeilen)) + print() + + print(" ── URTEIL ─────────────────────────────────────────────────────") + if not besteht: + print(" KEIN Muster erfuellt (1)-(3) in beiden Haelften.") + for name, z in besteht: + gross = all(abs(x[4]) > med_sp / 2 for x in z) + print(f" {name}: Trefferquote besteht — Bewegung " + f"{'SCHLAEGT' if gross else 'VERFEHLT'} den halben Spread " + f"({z[0][4]:+.4f} / {z[1][4]:+.4f} $ gegen {med_sp/2:.4f})") + print("\n ⚠ Eine Trefferquote ueber 50 % ist NICHT genug — sie muss die") + print(" Kosten tragen. Genau daran ist der 1-Bar-Effekt im Vorlauf") + print(" gescheitert (51,3 % Treffer, aber ±0,0002 $ Bewegung).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())