Files
AH-Oil-Trader/backtest_m15_auto.py
Axel HocksandClaude Opus 5 354a57ef5f M15-Karte automatisch handeln = faellt durch (backtest_m15_auto.py)
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>
2026-08-13 16:19:12 +02:00

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())