User: "messe ob wir den Autotrader bei M15-Empfehlung automatisch traden lassen
koennen." Track-B-Frage, Regel VORAB fixiert (OeR>0 & PF>1 & KI ohne Null in
beiden Haelften, n>=100, schlaegt die Kontrolle, Nachbarn kippen nicht).
Neu gemessen wurde genau EIN Zusatz: die Karte hat bewusst keine Richtung, die
entsteht erst aus der `lesart` (aus P(break)). backtest_m15_gates.py handelt die
vom HTF ERLAUBTE Richtung, nicht die Lesart -- die war nie geprueft.
80k M5-Bars, 2 Halbjahre, echte core.sr_levels/_p_break-Gewichte, Status+Lesart
inkl. Hysterese, kanonischer Exit, Echtkosten, Fill bei BERUEHRUNG, ein Slot:
A) Karte (Lesart) H1 -0,162 / PF 0,72 H2 -0,094 / PF 0,84
B) Lesart + Veto-konform H1 -0,104 / PF 0,81 H2 -0,051 / PF 0,91
C) KONTROLLE HTF-Richtung H1 -0,085 / PF 0,85 H2 -0,030 / PF 0,95
D) KONTROLLE Gegenrichtung H1 -0,070 / PF 0,87 H2 +0,007 / PF 1,01
Alle vier laufen auf DENSELBEN gruenen, gerichteten Momenten -- variiert wird nur
die Richtung. Die Lesart ist damit in BEIDEN Haelften schlechter als die
HTF-Richtung UND schlechter als ihr eigenes Gegenteil; sie kostet -0,077 bzw.
-0,064 OeR gegenueber C.
A ist nicht "unbelegt", sondern belegt NEGATIV: das 95-%-KI schliesst in beiden
Haelften die Null aus ([-0,222 … -0,099] und [-0,152 … -0,038]).
Nachbarn 0,40/0,50/0,60 alle beidhaelftig negativ und monoton schlechter.
Unabhaengige LIVE-Gegenprobe (m15_states, 3.678 Zustaende): der handelbare
Zustand trifft entkoppelt 46,8 % (30 min) und 42,6 % (60 min) -- beide unter dem
Muenzwurf. Roh saehe es mit 51,5 % besser aus; die Entkopplung (1.793 -> 124
unabhaengige Faelle) ist der ganze Unterschied.
MECHANISMUS: zum vierten Mal derselbe Fehler dieser Karte -- aus einer Aussage
ueber die STRUKTUR ("haelt dieses Level?") wird eine RICHTUNGSprognose. Das ist
Mean Reversion am Extrem, und P(break) taugt gemessen zum EXIT, nicht zum ENTRY.
URTEIL: nicht bauen. Auch kein Umschwenken auf C oder D -- die sind nur weniger
schlecht, ebenfalls negativ. Nichts am Live-System geaendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
337 lines
13 KiB
Python
337 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Kann der Autotrader die M15-KARTE automatisch handeln? (User-Auftrag 2026-08-13)
|
|
|
|
FRAGE: Der Bot soll eroeffnen, sobald die Karte GRUEN steht und eine gerichtete
|
|
Lesart zeigt. Das ist eine **Track-B-Frage** (Signal/Edge) — hohe Beweislast.
|
|
|
|
⚠⚠ WAS HIER NEU IST. Die Karte wurde ausdruecklich als BEDINGUNGS-Anzeige gebaut,
|
|
nicht als Richtungsquelle. Ihre Richtung entsteht erst aus der `lesart`, und die
|
|
ist bisher NIE als Handelsregel gemessen worden:
|
|
· `backtest_m15_gates.py` (08.08.) handelt die vom HTF ERLAUBTE Richtung —
|
|
nicht die Lesart. Ergebnis dort: H1 ueber ALLE sechs Abstands-Buckets negativ.
|
|
· `backtest_squeeze_m15.py` / `backtest_vwap_m15.py` messen andere Signale.
|
|
Neu gemessen wird also genau der Zusatz: **traegt die P(break)-Lesart als
|
|
Richtungsgeber?**
|
|
|
|
VORAB FIXIERTE ENTSCHEIDUNGSREGEL (damit die Latte hinterher nicht wandert):
|
|
(1) OeR > 0 UND PF > 1 in BEIDEN Haelften
|
|
(2) 95-%-Bootstrap-KI schliesst in BEIDEN Haelften die Null aus
|
|
(3) n >= 100 je Haelfte
|
|
(4) schlaegt die Kontrolle C (HTF-Richtung) in BEIDEN Haelften
|
|
(5) Nachbar-Schwellen (gap 0,4/0,5/0,6) kippen das Vorzeichen nicht
|
|
Verfehlt eine Bedingung → NICHT bauen.
|
|
|
|
AUFBAU — live-treu, mit den GETEILTEN Kernen statt Nachbauten:
|
|
· Bars M5 (die Ebene, auf der P(break) trainiert ist — auf M15 waere das
|
|
Modell falsch skaliert, Fix 2026-07-13).
|
|
· ATR_M15 / M30-Level / HTF-Trend durch Aggregation aus M5 (3 / 6 / 12).
|
|
· Level ueber `core.sr_levels` (linien/naechste/halten) = DERSELBE Code wie
|
|
`_draw_levels` — der Nachbau wich am 07.08. nachweislich ab.
|
|
· P(break) ueber `core.engine._p_break` mit den echten Live-Gewichten.
|
|
· Status/Lesart inkl. der **Hysterese** vom 12.08. (sonst misst man das
|
|
Flattern statt der Regel).
|
|
· Exit `core.exit_model.LIVE`, Echtkosten aus dem Bar-Spread.
|
|
· Sequentiell EIN Slot, 1x je Episode (wie der Live-Dedup).
|
|
· Fill bei BERUEHRUNG — nicht zum Level-Preis, egal wo der Bar schliesst.
|
|
Der Look-ahead-Aufschlag lag im Projekt bei 0,32-0,85 R.
|
|
|
|
Aufruf: python backtest_m15_auto.py [bars]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import sys
|
|
from bisect import bisect_left, bisect_right
|
|
|
|
import MetaTrader5 as mt5
|
|
|
|
from core.engine import _p_break
|
|
from core.exit_model import LIVE, simulate
|
|
from core.sr_levels import halten, linien, naechste
|
|
from test_m15_karte_tag import atr_series, ema
|
|
|
|
_PIV_K = 3
|
|
_LOOK = 50 # M30-Pivot-Fenster wie live
|
|
_HTF_DEAD = 0.15
|
|
_ATR5_FLOOR = 0.12 # wie im P(break)-Training
|
|
_WARTE = 8 # Bars, die die ruhende Order am Level liegt
|
|
|
|
|
|
def agg(H, L, C, step):
|
|
"""Hoehere TF aus M5 aggregieren (nur abgeschlossene Bars)."""
|
|
hh, hl, hc = [], [], []
|
|
for s in range(0, len(C) - step + 1, step):
|
|
hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step]))
|
|
hc.append(C[s + step - 1])
|
|
return hh, hl, hc
|
|
|
|
|
|
def htf_sign(H, L, C, step):
|
|
"""Trend-Vorzeichen der hoeheren TF je M5-Index — LOOK-AHEAD-frei."""
|
|
hh, hl, hc = agg(H, L, C, step)
|
|
out = [0] * len(C)
|
|
if len(hc) < 55:
|
|
return out
|
|
ef, es, ha = ema(hc, 12), ema(hc, 50), atr_series(hh, hl, hc)
|
|
sg = []
|
|
for j in range(len(hc)):
|
|
a = ha[j]
|
|
if j < 52 or not a:
|
|
sg.append(0); continue
|
|
d = ef[j] - es[j]
|
|
sg.append(0 if abs(d) < _HTF_DEAD * a else (1 if d > 0 else -1))
|
|
for i in range(len(C)):
|
|
j = i // step - 1
|
|
out[i] = sg[j] if 0 <= j < len(sg) else 0
|
|
return out
|
|
|
|
|
|
def atr_hoeher(H, L, C, step):
|
|
"""ATR der hoeheren TF je M5-Index (kausal: letzter ABGESCHLOSSENER Bar)."""
|
|
hh, hl, hc = agg(H, L, C, step)
|
|
ha = atr_series(hh, hl, hc)
|
|
out = [None] * len(C)
|
|
for i in range(len(C)):
|
|
j = i // step - 1
|
|
out[i] = ha[j] if 0 <= j < len(ha) else None
|
|
return out
|
|
|
|
|
|
def pivots(H, L, C, step):
|
|
"""ALLE M30-Pivots EINMAL berechnen (Index + Preis), statt je Bar neu.
|
|
|
|
⚠ Das ist die dokumentierte O(log n)-Form: die quadratische Fassung war der
|
|
Engpass des Projekts (9 min → 8,9 s). Das Pivot-Kriterium ist LOKAL, eine
|
|
globale Berechnung liefert daher dieselbe Menge wie das rollende Fenster.
|
|
Kausal: ein Pivot bei x ist erst bei x+K bekannt.
|
|
"""
|
|
hh, hl, _ = agg(H, L, C, step)
|
|
ph_i, ph_v, pl_i, pl_v = [], [], [], []
|
|
for x in range(_PIV_K, len(hh) - _PIV_K):
|
|
if hh[x] == max(hh[x - _PIV_K:x + _PIV_K + 1]):
|
|
ph_i.append(x); ph_v.append(round(hh[x], 3))
|
|
if hl[x] == min(hl[x - _PIV_K:x + _PIV_K + 1]):
|
|
pl_i.append(x); pl_v.append(round(hl[x], 3))
|
|
return (ph_i, ph_v), (pl_i, pl_v)
|
|
|
|
|
|
def fenster(idx, val, lo, hi):
|
|
"""Pivots mit lo <= index <= hi — zwei Binaersuchen statt Listenlauf."""
|
|
a, b = bisect_left(idx, lo), bisect_right(idx, hi)
|
|
return val[a:b]
|
|
|
|
|
|
def lauf(H, L, C, SP, lo, hi, gap_gruen, modus):
|
|
"""Sequentielle Sim EINES Slots ueber [lo, hi).
|
|
|
|
modus: 'karte' = Richtung aus der Lesart (das, was gefragt ist)
|
|
'veto' = Lesart, aber nur wenn sie der HTF-Erlaubnis entspricht
|
|
'htf' = KONTROLLE: HTF-erlaubte Richtung (= backtest_m15_gates)
|
|
'gegen' = KONTROLLE: Gegenrichtung der Lesart
|
|
"""
|
|
a5 = atr_series(H, L, C)
|
|
a15 = atr_hoeher(H, L, C, 3)
|
|
s30 = htf_sign(H, L, C, 6)
|
|
(ph_i, ph_v), (pl_i, pl_v) = pivots(H, L, C, 6)
|
|
ef, es = ema(C, 12), ema(C, 50)
|
|
|
|
Rs, n_ep = [], 0
|
|
prev_r = prev_s = None
|
|
hy_status, hy_lesart = None, None
|
|
arm = None # letzte gehandelte Episode (dir, level)
|
|
frei_ab = lo # Slot belegt bis ...
|
|
grenze = min(hi, len(C) - LIVE.max_hold - 2)
|
|
|
|
for i in range(lo, grenze):
|
|
a = a5[i]; a15i = a15[i]
|
|
if not a or not a15i:
|
|
continue
|
|
a5f = max(a, _ATR5_FLOOR)
|
|
j30 = i // 6 - 1
|
|
if j30 < _PIV_K + 5:
|
|
continue
|
|
ph = fenster(ph_i, ph_v, j30 - _LOOK, j30 - _PIV_K)
|
|
pl = fenster(pl_i, pl_v, j30 - _LOOK, j30 - _PIV_K)
|
|
if not ph and not pl:
|
|
continue
|
|
# --- Level: DERSELBE Code wie _draw_levels (Clustern mit ATR_M5) ---
|
|
lv = linien(ph, pl, a5f)
|
|
r, s = naechste(lv, C[i])
|
|
r = halten(r, prev_r, C[i], a5f, True)
|
|
s = halten(s, prev_s, C[i], a5f, False)
|
|
prev_r, prev_s = r, s
|
|
|
|
# --- Zonen + P(break), Merkmale trainingsgleich aus M5 ---
|
|
mom6 = (C[i] - C[i - 6]) / a5f
|
|
mom3 = (C[i] - C[i - 3]) / a5f
|
|
ediff = (ef[i] - es[i]) / a5f
|
|
zonen = []
|
|
for lvl, seite in ((r, "res"), (s, "sup")):
|
|
if lvl is None:
|
|
continue
|
|
d = 1 if seite == "res" else -1
|
|
g = abs(lvl - C[i])
|
|
wt = 1.0 if ediff * d > 0 else 0.0
|
|
p = _p_break(mom6 * d, mom3 * d, wt, g / a5f)
|
|
zonen.append({"seite": seite, "level": lvl,
|
|
"gap15": g / a15i, "pb": round(100 * p)})
|
|
if not zonen:
|
|
continue
|
|
nah = min(zonen, key=lambda x: x["gap15"])
|
|
|
|
# --- Status + Lesart MIT der Live-Hysterese (12.08.) ---
|
|
kost = SP[i] / a15i
|
|
if kost >= 0.32:
|
|
status = "rot"
|
|
else:
|
|
schw = (gap_gruen + 0.10) if hy_status == "gruen" else gap_gruen
|
|
status = "gruen" if nah["gap15"] <= schw else "gelb"
|
|
lesart = "unklar"
|
|
pb = nah["pb"]
|
|
war_ger = hy_lesart in ("auf", "ab")
|
|
lo_s, hi_s = (45, 55) if war_ger else (40, 60)
|
|
if pb < lo_s or pb > hi_s:
|
|
bricht = pb > 55 if war_ger else pb > hi_s
|
|
res = nah["seite"] == "res"
|
|
lesart = "auf" if (res if bricht else not res) else "ab"
|
|
elif war_ger:
|
|
lesart = hy_lesart
|
|
hy_status, hy_lesart = status, lesart
|
|
|
|
# --- Episoden-Dedup wie live: 1x je gruener, gerichteter Episode ---
|
|
if status != "gruen" or lesart == "unklar":
|
|
arm = None
|
|
continue
|
|
d_les = 1 if lesart == "auf" else -1
|
|
kennung = (d_les, nah["level"])
|
|
if arm == kennung:
|
|
continue
|
|
arm = kennung
|
|
n_ep += 1
|
|
|
|
if i < frei_ab: # Slot belegt — Episode zaehlt, kein Trade
|
|
continue
|
|
|
|
# --- Richtung je Modus ---
|
|
erlaubt = s30[i]
|
|
if modus == "karte":
|
|
d = d_les
|
|
elif modus == "gegen":
|
|
d = -d_les
|
|
elif modus == "veto":
|
|
if not erlaubt or erlaubt != d_les:
|
|
continue
|
|
d = d_les
|
|
else: # 'htf'
|
|
if not erlaubt:
|
|
continue
|
|
d = erlaubt
|
|
|
|
# --- Fill bei BERUEHRUNG, dann kanonischer Exit ---
|
|
lvl = nah["level"]
|
|
fill = None
|
|
for k in range(i + 1, min(i + 1 + _WARTE, grenze)):
|
|
if L[k] <= lvl <= H[k]:
|
|
fill = k; break
|
|
if fill is None:
|
|
continue
|
|
R, xb = simulate(lvl, d, a5f, H, L, C, fill + 1, LIVE, ret_bar=True)
|
|
Rs.append(R - SP[i] / a5f)
|
|
frei_ab = xb + 1
|
|
return Rs, n_ep
|
|
|
|
|
|
def ci(Rs, n=3000, seed=42):
|
|
if len(Rs) < 20:
|
|
return None, None
|
|
r = random.Random(seed); m = len(Rs)
|
|
b = sorted(sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n))
|
|
return b[int(.025 * n)], b[int(.975 * n)]
|
|
|
|
|
|
def kenn(Rs):
|
|
if not Rs:
|
|
return None
|
|
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
|
|
u, o = ci(Rs)
|
|
return {"n": len(Rs), "oer": sum(Rs) / len(Rs), "sr": sum(Rs),
|
|
"wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs),
|
|
"pf": (g / v if v else float("inf")), "u": u, "o": o}
|
|
|
|
|
|
def zeile(name, k):
|
|
if not k:
|
|
return f" {name:<34} —"
|
|
kib = " — " if k["u"] is None else f"[{k['u']:+.3f} … {k['o']:+.3f}]"
|
|
null = "" if k["u"] is None else ("" if (k["u"] > 0 or k["o"] < 0) else " ⚠ KI enthaelt 0")
|
|
return (f" {name:<34} n={k['n']:>4} WR {k['wr']:>4.1f} % OeR {k['oer']:>+.3f} "
|
|
f"PF {k['pf']:>5.2f} SR {k['sr']:>+8.1f} {kib}{null}")
|
|
|
|
|
|
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()
|
|
if bars is None:
|
|
print(" keine Bars"); return 1
|
|
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]
|
|
mid = len(C) // 2
|
|
print(f"\n {sym} · {len(C)} M5-Bars · Exit kanonisch · Echtkosten · "
|
|
f"Fill bei Beruehrung · EIN Slot")
|
|
print(f" Regel vorab fixiert: OeR>0 & PF>1 & KI ohne 0 in BEIDEN Haelften, "
|
|
f"n>=100, schlaegt Kontrolle C, Nachbarn kippen nicht\n")
|
|
|
|
print(" ── HAUPTFRAGE: die Karte automatisch handeln (gap<=0,50) ──────")
|
|
erg = {}
|
|
for modus, name in (("karte", "A) Karte (Lesart)"),
|
|
("veto", "B) Lesart + Veto-konform"),
|
|
("htf", "C) KONTROLLE HTF-Richtung"),
|
|
("gegen", "D) KONTROLLE Gegenrichtung")):
|
|
r1, e1 = lauf(H, L, C, SP, 60, mid, 0.50, modus)
|
|
r2, e2 = lauf(H, L, C, SP, mid, len(C), 0.50, modus)
|
|
erg[modus] = (kenn(r1), kenn(r2))
|
|
print(zeile(name + " H1", erg[modus][0]))
|
|
print(zeile(name + " H2", erg[modus][1]))
|
|
if modus == "karte":
|
|
print(f" (gruene gerichtete Episoden: H1 {e1} · H2 {e2})")
|
|
print()
|
|
|
|
print(" ── NACHBAR-SCHWELLEN (Bedingung 5) ────────────────────────────")
|
|
for g in (0.40, 0.50, 0.60):
|
|
r1, _ = lauf(H, L, C, SP, 60, mid, g, "karte")
|
|
r2, _ = lauf(H, L, C, SP, mid, len(C), g, "karte")
|
|
k1, k2 = kenn(r1), kenn(r2)
|
|
print(zeile(f"gap<={g:.2f} H1", k1))
|
|
print(zeile(f"gap<={g:.2f} H2", k2))
|
|
|
|
print("\n ── URTEIL ─────────────────────────────────────────────────────")
|
|
a1, a2 = erg["karte"]; c1, c2 = erg["htf"]
|
|
pruef = []
|
|
for nr, txt, ok in (
|
|
(1, "OeR>0 und PF>1 in beiden Haelften",
|
|
bool(a1 and a2 and a1["oer"] > 0 and a2["oer"] > 0
|
|
and a1["pf"] > 1 and a2["pf"] > 1)),
|
|
(2, "KI schliesst die Null in beiden aus",
|
|
bool(a1 and a2 and a1["u"] is not None and a2["u"] is not None
|
|
and (a1["u"] > 0 or a1["o"] < 0) and (a2["u"] > 0 or a2["o"] < 0))),
|
|
(3, "n >= 100 je Haelfte",
|
|
bool(a1 and a2 and a1["n"] >= 100 and a2["n"] >= 100)),
|
|
(4, "schlaegt Kontrolle C in beiden",
|
|
bool(a1 and a2 and c1 and c2
|
|
and a1["oer"] > c1["oer"] and a2["oer"] > c2["oer"])),
|
|
):
|
|
pruef.append(ok)
|
|
print(f" ({nr}) {txt:<42} {'✅' if ok else '❌'}")
|
|
print(f"\n ==> {'ALLE Bedingungen erfuellt' if all(pruef) else 'REGEL VERFEHLT — nicht bauen'}"
|
|
f" (Bedingung 5 aus der Nachbar-Tabelle ablesen)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|