PFEILE (User: "anstatt des Trichters einen Pfeil, der die wahrscheinlichste
Kursentwicklung anzeigt"). Randbedingung: die FREIE Richtungsfrage ist
gemessen ein Muenzwurf (analyze_reversal.py, AUC 0,499-0,509 oos, in-sample
nur 0,52). Ein frei schwebender Prognose-Pfeil waere unbelegt - und
gefaehrlicher als der Kegel, weil er ueberzeugender aussieht. Deshalb DREI
Pfeile mit jeweils eigener, benannter Beleglage:
L blau naechstes Level, aus dem kalibrierten P(break)
(AUC 0,65 oos, live 27 % vorhergesagt vs 28 % real)
S neongruen Squeeze-Ausbruch - das EINZIGE validierte Richtungssignal
K grau Konsens der Module - NICHT kalibriert, heisst deshalb
"Konsens" und nicht "Prognose"
L hat ein TOTBAND 45-55 %: dort waagerechter Pfeil "unentschieden", statt
aus einem Muenzwurf eine Richtung zu zeichnen.
MQL5 v1.32: OBJ_ARROWED_LINE + Label an der Spitze. Label heisst ALBL_* und
NICHT T* - sonst zieht RepositionLabels() es an den rechten Rand und loest
es von der Pfeilspitze.
Kegel bleibt im Code und in der Dashboard-Kachel; der Bot exportiert ihn nur
nicht mehr ins Chart ([trading] export_cone=false).
CHARTMUSTER: alle Handbuch-Muster ergaenzt - Dreifach-Top/-Boden, Flagge,
Wimpel, Rechteck, Steigender/Fallender Keil. ALLE mit measured=False, sie
tragen die Verdict-Stimme NICHT: das Gewicht 1,0 stammt aus einem
Kontrolltest, der nur die alten Typen abdeckte. engine._verdict waehlt jetzt
nur Muster mit measured=True.
Bug beim Bau gefunden: der fallende Keil feuerte in 20.000 Bars KEIN
EINZIGES MAL - die Bedingung stand auf dh > dl, bei einem fallenden Keil
faellt aber die OBERE Linie schneller (sonst konvergiert nichts). Beim
Spiegelbild (steigender Keil) stimmte es.
backtest_patterns_v2.py (NEU) misst die neuen Typen - mit dem ECHTEN
Detektor (core.patterns._detect statt einer Nachbildung) und dem
kanonischen Exit, plus Kontrollgruppe "generischer Swing-Bruch".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 lines
7.3 KiB
Python
183 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Die NEUEN Handbuch-Muster messen (User-Vorgabe 2026-07-31 „messe die neuen Muster").
|
||
|
||
Ergänzt wurden am selben Tag: **Dreifach-Top/-Boden · Flagge · Wimpel · Rechteck ·
|
||
Steigender/Fallender Keil**. Sie stehen in `core/patterns.py` auf `measured=False`
|
||
und tragen die Verdict-Stimme NICHT — dieses Skript prüft, ob sich das ändern darf.
|
||
|
||
⚠ ZWEI UNTERSCHIEDE ZU `backtest_patterns.py` (bewusst):
|
||
1. **Der ECHTE Detektor wird benutzt** (`core.patterns._detect`), nicht eine
|
||
Nachbildung. Das alte Skript hat die Bedingungen in `scan()` NACHGEBAUT — misst
|
||
also möglicherweise etwas anderes als das, was live läuft. Nach den fünf
|
||
Deployment-Drift-Fällen dieses Tages ist das keine akzeptable Grundlage mehr.
|
||
2. **Exit aus `core/exit_model.py`** statt einer eigenen Kopie (das alte Skript war
|
||
ein FÜNFTES Exit-Modell: Trail fest 1,5 · `_MAXH` 288 · kein Time-Stop).
|
||
|
||
KONTROLLGRUPPE (entscheidend): derselbe Einstieg/Exit auf einem **generischen
|
||
Swing-Bruch** — Bruch des letzten Swing-Hochs/-Tiefs ohne jedes Muster. Nur wenn ein
|
||
Muster die Kontrolle in BEIDEN Hälften schlägt, trägt das MUSTER etwas bei und nicht
|
||
bloss „Breakout + Trailing" (die Lehre aus dem 30.07.-Kontrolltest).
|
||
|
||
Zusätzlich: **Ziel-Trefferquote** — wie oft wird der Measured Move vor dem SL
|
||
erreicht? Das ist der ehrliche Muster-Check (alt: nur 13–38 %).
|
||
"""
|
||
import sys
|
||
from collections import defaultdict
|
||
|
||
import MetaTrader5 as mt5
|
||
|
||
from core.exit_model import LIVE, simulate
|
||
from core.patterns import _atr, _pivots, _detect, _PIVOT_K, _MEASURED
|
||
|
||
_BREAK_WIN = 48 # so viele Bars auf den Trigger-Bruch warten
|
||
_ATRMIN = 0.06
|
||
_NEW = ("triple_top", "triple_bottom", "flag", "pennant", "wedge_rise", "wedge_fall")
|
||
|
||
|
||
def _atr_series(H, L, C, p=14):
|
||
out = [None]
|
||
trs = []
|
||
for i in range(1, len(C)):
|
||
trs.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
|
||
out.append(sum(trs[-p:]) / min(len(trs), p))
|
||
return out
|
||
|
||
|
||
def scan(H, L, C, A, SP, lo, hi, step=3):
|
||
"""Läuft die History kausal ab und sammelt (typ, entry_bar, dir, trigger, target).
|
||
|
||
Nutzt den ECHTEN Detektor auf dem jeweils bis `i` bekannten Fenster.
|
||
"""
|
||
found = []
|
||
seen = set()
|
||
for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step):
|
||
atr = A[i]
|
||
if not atr or atr < _ATRMIN:
|
||
continue
|
||
piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K)
|
||
if len(piv) < 3:
|
||
continue
|
||
try:
|
||
pats = _detect(piv, C[i], atr, i)
|
||
except Exception:
|
||
continue
|
||
for m in pats:
|
||
if m.get("status") != "forming" or m.get("dir") not in ("bull", "bear"):
|
||
continue
|
||
key = (m["type"], round(m["trigger"], 2), m["dir"])
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
d = 1 if m["dir"] == "bull" else -1
|
||
trig = m["trigger"]
|
||
# auf den Trigger-Bruch warten
|
||
for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)):
|
||
broke = (H[j] >= trig) if d > 0 else (L[j] <= trig)
|
||
if broke:
|
||
found.append((m["type"], j, d, trig, m.get("target"), max(A[j] or 0, _ATRMIN)))
|
||
break
|
||
return found
|
||
|
||
|
||
def control(H, L, C, A, lo, hi, step=3):
|
||
"""KONTROLLE: Bruch des letzten Swing-Hochs/-Tiefs, ohne jedes Muster."""
|
||
out = []
|
||
seen = set()
|
||
for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step):
|
||
atr = A[i]
|
||
if not atr or atr < _ATRMIN:
|
||
continue
|
||
piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K)
|
||
if len(piv) < 2:
|
||
continue
|
||
for d, kind in ((1, "H"), (-1, "L")):
|
||
last = next((p for p in reversed(piv) if p[2] == kind), None)
|
||
if not last:
|
||
continue
|
||
trig = last[1]
|
||
key = (kind, round(trig, 2))
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)):
|
||
broke = (H[j] >= trig) if d > 0 else (L[j] <= trig)
|
||
if broke:
|
||
out.append(("control", j, d, trig, None, max(A[j] or 0, _ATRMIN)))
|
||
break
|
||
return out
|
||
|
||
|
||
def evaluate(rows, H, L, C, SP):
|
||
"""→ {typ: [(R_netto, ziel_getroffen)]}"""
|
||
res = defaultdict(list)
|
||
for typ, j, d, trig, target, atr in rows:
|
||
cost = (SP[j] if SP[j] > 0 else 0.0225) / atr
|
||
R = simulate(trig, d, atr, H, L, C, j + 1, LIVE) - cost
|
||
hit = None
|
||
if target is not None:
|
||
hit = False
|
||
sl = trig - d * LIVE.sl_atr * atr
|
||
for k in range(j + 1, min(j + 1 + LIVE.max_hold, len(C))):
|
||
if (L[k] <= sl) if d > 0 else (H[k] >= sl):
|
||
break
|
||
if (H[k] >= target) if d > 0 else (L[k] <= target):
|
||
hit = True; break
|
||
res[typ].append((R, hit))
|
||
return res
|
||
|
||
|
||
def rep(name, rows):
|
||
if not rows or len(rows) < 8:
|
||
print(f" {name:<24} n={len(rows):<4} — zu wenige")
|
||
return
|
||
Rs = [r for r, _ in rows]
|
||
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
|
||
g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0)
|
||
hits = [h for _, h in rows if h is not None]
|
||
zt = f"{100*sum(hits)/len(hits):>3.0f}%" if hits else " —"
|
||
print(f" {name:<24} n={n:<4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} "
|
||
f"PF={(g/l if l > 0 else 9.99):>4.2f} ΣR={s:>+6.0f} Ziel={zt}")
|
||
|
||
|
||
def main():
|
||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 120000
|
||
mt5.initialize()
|
||
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None)
|
||
bars = None
|
||
for req in (n, 100000, 80000, 60000, 40000):
|
||
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, req)
|
||
if bars is not None and len(bars) > 3000:
|
||
break
|
||
si = mt5.symbol_info(sym); point = si.point; 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]; SP = [float(b["spread"]) * point for b in bars]
|
||
A = _atr_series(H, L, C); N = len(C); mid = N // 2
|
||
|
||
print("=" * 96)
|
||
print(f" NEUE HANDBUCH-MUSTER — {sym} M30 ({N} Bars), Echtkosten")
|
||
print(f" Detektor: core/patterns._detect (der ECHTE) · Exit: core/exit_model.py "
|
||
f"(Trail {LIVE.mult})")
|
||
print(" Urteil: nur wenn ein Typ die KONTROLLE (generischer Swing-Bruch) in BEIDEN")
|
||
print(" Hälften schlägt, trägt das MUSTER etwas bei — sonst ist es Breakout+Trailing.")
|
||
print("=" * 96)
|
||
|
||
label = {"triple_top": "Dreifach-Top", "triple_bottom": "Dreifach-Boden",
|
||
"flag": "Flagge", "pennant": "Wimpel",
|
||
"wedge_rise": "Steigender Keil", "wedge_fall": "Fallender Keil"}
|
||
for lbl, a, b in (("H1 (alt)", _PIVOT_K, mid), ("H2 (neu)", mid, N - LIVE.max_hold - 1)):
|
||
print(f"\n{lbl}:")
|
||
res = evaluate(scan(H, L, C, A, SP, a, b), H, L, C, SP)
|
||
ctl = evaluate(control(H, L, C, A, a, b), H, L, C, SP)
|
||
rep("KONTROLLE Swing-Bruch", ctl["control"])
|
||
print(" " + "-" * 74)
|
||
for t in _NEW:
|
||
rep(label[t], res.get(t, []))
|
||
print(" " + "-" * 74)
|
||
for t in sorted(_MEASURED):
|
||
if res.get(t):
|
||
rep(f"(alt) {t}", res[t])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|