Files
AH-Oil-Trader/backtest_kerzenmuster_richtung.py
T
Axel HocksandClaude Opus 5 55dc69ef42 Kerzenmuster fuer die Richtung der naechsten Kerze = nein (4. Messung der Klasse)
User-Anschlussfrage. Gemessen mit den ECHTEN Muster-Schwellen aus
core/candles.py (1:1 importiert, kein Nachbau), 80k M5-Bars, 2 Halbjahre,
unveraenderte Schlusskurse ausgeschlossen, 95-%-Intervall je Zelle. Bewertet
wird die Lehrbuch-Lesart, damit sichtbar wird, ob sie stimmt.

  Marubozu             46,5 % / 45,2 %   beide KI unter 50
  Engulfing            47,4 % / 49,5 %
  Outside Bar          47,3 % / 48,9 %
  langer Koerper       48,5 % / 47,6 %
  langer Docht OBEN    49,4 % / 47,8 %
  langer Docht UNTEN   50,6 % / 50,6 %   beide KI enthalten 50

KEIN Muster erfuellt die vorab fixierte Regel. Trefferquoten 45,2-50,6 %, fuenf
Zellen mit Intervall komplett unter 50 -- in der Lehrbuch-Lesart also
verlaesslich falsch. Die einzige beidhaelftig ueber 50 kippt beim Vorzeichen der
Bewegung (+0,0004 / -0,0037).

Das reproduziert den 60-Minuten-Befund vom 31.07. ("Lehrbuch-Lesart in beiden
Haelften invertiert") jetzt auf dem 1-Bar-Horizont, mit den echten Klassen statt
grober Proxys. Vierte Messung der Kerzen-Klasse.

Und selbst umgedreht zahlt es nicht: groesste Oe-Bewegung 0,0077 $, kippt in der
anderen Haelfte auf -0,0019; halber Spread 0,0100 $, Median-Kerze 0,0400 $.

URTEIL: der Pfeil bleibt bei der kalibrierten Level-Aussage. Nichts geaendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:49:08 +02:00

173 lines
7.2 KiB
Python

#!/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())