Richtungs-Pfeile im MT5 statt des Kegels (v1.32) + Handbuch-Muster ergaenzt
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
365a0584e5
commit
81614a3128
@@ -0,0 +1,182 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user