Files
AH-Oil-Trader/backtest_trail_be.py
T
Axel HocksandClaude Opus 5 c8c3e32926 Trail x Breakeven gemeinsam geprueft: keine Aenderung - und ein Aufloesungs-
Artefakt, das einen alten Schluss entwertet

Anlass: Frage zur Trail-Kalibrierung "jetzt bei 3 Lot". Zur Praemisse: die
Positionsgroesse aendert das Optimum NICHT (R ist ATR-normiert). Gemessen wurde
die echte offene Frage - be=1,3 stammt aus der Zeit mit Trail 1,5, mult wurde am
31.07. auf 1,0 gesenkt ohne das Breakeven neu zu pruefen.

(1) Breakeven ist praktisch wirkungslos: bei mult <= 0,6 sind alle be-Zeilen
identisch (der enge Trail erreicht den Einstand frueher als der BE-Boden). Bei
mult 1,0 ist be=0,6 um +0,019 besser - unter einem Standardfehler. Keine
Aenderung.

(2) Der Trail ist monoton besser bis hinunter zu 0,05 xATR (~3 Cent) - das waere
vom Spread allein ausgeloest. Ein Optimum, das bis an die physikalische
Unmoeglichkeit monoton bleibt, misst die Methode, nicht den Markt.

Aufloesungs-Artefakt nachgewiesen (analyze_trail_resolution.py): mit in DOLLAR
fixiertem Trail auf M5 gegen M1, 597 gemeinsame Einstiege, kippt das Vorzeichen
der Differenz bei ~0,3 xATR - die groebere Aufloesung ueberschaetzt enge Trails
(-0,027 bei 0,10) und unterschaetzt weite (+0,036 bei 1,0, +0,066 bei 1,5).
Damit ist der Befund vom 31.07. ("enger ist monoton besser") teilweise entwertet.

Erster Testentwurf war falsch konstruiert: der Trail wurde mit dem ATR der
jeweiligen Zeitebene skaliert, damit bleibt das Verhaeltnis Trail/Bar-Spanne
konstant und die Aufloesung wird gar nicht variiert.

Nebenbefund gegen die eigene Slippage-Vermutung: die Trade-Zahl ist ueber alle
Multiplikatoren fast konstant (2108 bei 0,3 gegen 2083 bei 1,0), Worst-Case
ueberall -2,44. Das Slippage-Argument traegt kaum, das Aufloesungs-Argument
traegt.

Ergebnis: Trail 1,0 / Breakeven 1,3 bleiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:58:50 +02:00

243 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Trailing × Breakeven — sind die beiden noch GEMEINSAM kalibriert? (2026-08-05)
ANLASS: Frage des Users nach der Trailing-Kalibrierung, ausgelöst vom Trade
T=49408993 (05.08.): er lief +0,50 $ ins Plus (0,85×ATR), wurde vom Trail
ausgestoppt und endete bei 15,33 €.
⚠ ZUR PRÄMISSE „mein Einsatz ist jetzt 3 Lot": **die Positionsgröße ändert das
Trailing-Optimum NICHT.** R ist ATR-normiert; Lots skalieren nur das EUR-Ergebnis,
nicht den optimalen Multiplikator. Gemessen wird deshalb etwas anderes — und
Wichtigeres:
**TRAIL UND BREAKEVEN WURDEN NIE GEMEINSAM OPTIMIERT.**
* `be = 1.3` stammt aus `backtest_exit.py` — gemessen, als der Trail noch **1,5** war.
* `mult` wurde am 31.07. auf **1,0** gesenkt (`backtest_trailmult.py`), **ohne**
das Breakeven neu zu prüfen.
Die beiden hängen aber zusammen: solange der Gewinn unter `be` liegt, steht der
Stop noch auf der Verlustseite; ist der Trail eng, wird dieses Band schneller
durchlaufen. Genau in diesem Band ist der Trade von heute gestorben — Gewinn
0,85×ATR, Breakeven-Schwelle 1,3×ATR, also **0,45×ATR zu wenig**, und der Trail
(1,0×ATR) lag zwangsläufig noch über dem Einstieg.
MESSUNG: 2D-Sweep mult × be, kanonischer Exit (`exit_model.simulate`), Echtkosten,
80k M5, 2 Halbjahre, auf ZWEI Populationen (Squeeze-Ausbrüche und gegatete
Wellensignale) über eine JE POPULATION FESTE Entry-Liste — nur die Exit-Parameter
variieren, sonst wäre nicht vergleichbar, was verglichen wird.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Umstellen nur, wenn ein Paar (mult, be) den Ist-Stand (1,0 / 1,3)
(a) in ΣR **und** ØR in BEIDEN Halbjahren schlägt,
(b) auf BEIDEN Populationen,
(c) und die direkten Nachbarwerte mithalten (kein Parameter-Zufall),
(d) ohne den Worst-Case zu verschlechtern.
Sonst bleibt es, wie es ist.
Aufruf: python backtest_trail_be.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_cost_gate import _atr_series, kandidaten
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS,
_ANGLE_LR, _ANGLE_DEAD, _HTF_DEADBAND)
_ATRMIN = 0.06
_TIMEOUT_BARS = 12
_PIV_K, _PIV_LOOK = 3, 300
_ATR_FLOOR_PB = 0.12
_K = 0.3
# ⚠ Bereich bewusst NACH UNTEN erweitert: der Vorlauf zeigte das Optimum bei 0,6,
# also am RAND — dann misst man moeglicherweise nur die Grenze des Suchraums.
# Genau diese Warnung steht seit 31.07. in CLAUDE.md (backtest_trailmult.py).
_MULT = (0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.5)
# Bei engem Trail ist das Breakeven wirkungslos (der Trail erreicht den Einstand
# ohnehin frueher) — deshalb ein grobes Raster, das reicht fuer die Interaktion.
_BE = (0.6, 1.0, 1.3, 2.0)
def _ema_series(v, p):
k = 2 / (p + 1); out = [v[0]]
for x in v[1:]:
out.append(out[-1] + k * (x - out[-1]))
return out
def _htf_sign(C, i, n, atr):
if i < n * _EMA_SLOW:
return 0
seg = C[i - n * _EMA_SLOW:i + 1:n]
if len(seg) < _EMA_SLOW + 2:
return 0
ef = _ema_series(seg, _EMA_FAST)[-1]; es = _ema_series(seg, _EMA_SLOW)[-1]
dead = _HTF_DEADBAND * atr * (n ** 0.5)
return 1 if ef - es > dead else (-1 if es - ef > dead else 0)
def wellen_entries(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL):
"""Gegatete Wellensignale — volle Live-Kette, EINE feste Liste."""
out = []
pend = None
for i in range(_PIV_LOOK + _EMA_SLOW + 5, len(C) - 2):
atr = A[i]
if not atr or atr < _ATRMIN:
continue
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K],
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal")
frisch = False
if sig in ("LONG", "SHORT"):
d = 1 if sig == "LONG" else -1
if pend is None or pend["dir"] != d:
pend = {"dir": d, "level": C[i] + d * _K * atr,
"invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i}
if not pend["confirmed"]:
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
against = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])
if broke:
pend["confirmed"] = True; frisch = True
else:
if against or (i - pend["bar"]) > _TIMEOUT_BARS:
pend = {"dir": d, "level": C[i] + d * _K * atr,
"invalid": C[i] - d * _K * atr,
"confirmed": False, "bar": i}
continue
else:
pend = None
continue
if not frisch:
continue
rec = w._room_gate(rec, C[i])
if rec.get("signal") not in ("LONG", "SHORT"):
continue
out.append({"i": i, "d": 1 if rec["signal"] == "LONG" else -1,
"lvl": pend["level"], "atr": max(atr, _ATRMIN),
"kosten": (SP[i] if SP[i] > 0 else 0.0225) / max(atr, _ATRMIN)})
pend = None
return out
def sweep(K, H, L, C, lo, hi, mult, be):
"""Sequentiell, EIN Slot — nur die Exit-Parameter variieren."""
p = LIVE.with_(mult=mult, be=be)
rs = []
frei = lo
for k in K:
if not (lo <= k["i"] < hi) or k["i"] < frei:
continue
r, xb = simulate(k["lvl"], k["d"], k["atr"], H, L, C, k["i"] + 1, p,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - k["kosten"])
frei = xb + 1
return rs
def kenn(rs):
if not rs:
return None
n = len(rs); s = sum(rs)
g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0)
return {"n": n, "oer": s / n, "sr": s,
"pf": (g / v) if v > 0 else float("inf"), "worst": min(rs)}
def main():
n_bars = 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_bars)
point = mt5.symbol_info(sym).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]; T = [int(b["time"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = _atr_series(H, L, C)
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
AD = [0.0] * len(C)
for i in range(_N_BARS, len(C)):
AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0
if abs(AD[i]) < _ANGLE_DEAD:
AD[i] = 0.0
HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
w = WaveRecommender(lambda: None)
w.set_reversal_enabled(False)
w.set_entry_room(0.6) # ⚠ ohne diese Zeile ist das Gate ein No-op
w.set_dead_hours("")
mid = len(C) // 2
print("=" * 96)
print(f" TRAILING x BREAKEVEN — gemeinsam kalibriert? · {sym} M5, {len(C)} Bars")
print(f" Ist-Stand: mult {LIVE.mult} · be {LIVE.be} · 2 Halbjahre · Echtkosten")
print(" Hinweis: die Positionsgroesse aendert das Optimum NICHT (R ist ATR-normiert).")
print("=" * 96)
pops = [("SQUEEZE", kandidaten(H, L, C, T, SP, A)),
("WELLE (gegated)",
wellen_entries(w, H, L, C, A, SP, EF, ES, AD, HTF, H1S, PH, PL))]
ist = {}
for name, K in pops:
print(f"\n {name}{len(K)} Entries")
print(f" {'be \\ mult':<11}" + "".join(f"{m:>15.1f}" for m in _MULT))
for be in _BE:
zeile = f" {be:<11.1f}"
for m in _MULT:
k1 = kenn(sweep(K, H, L, C, 0, mid, m, be))
k2 = kenn(sweep(K, H, L, C, mid, len(C), m, be))
if m == LIVE.mult and be == LIVE.be:
ist[name] = (k1, k2)
zeile += f"{(k1['oer'] if k1 else 0):>+7.3f}/{(k2['oer'] if k2 else 0):>+7.3f}"
print(zeile)
print(f" (Zellen = OeR H1 / OeR H2; Ist-Stand {LIVE.mult}/{LIVE.be} ist die Referenz)")
# ── Urteil nach der VORAB fixierten Regel ─────────────────────────────
print("\n" + "=" * 96)
print(" URTEIL (Regel VOR dem Lauf fixiert: schlaegt Ist-Stand in OeR UND SigmaR,")
print(" in BEIDEN Haelften, auf BEIDEN Populationen, Nachbarn halten mit,")
print(" Worst-Case nicht schlechter)")
print("=" * 96)
treffer = []
for m in _MULT:
for be in _BE:
if m == LIVE.mult and be == LIVE.be:
continue
ok = True
for name, K in pops:
i1, i2 = ist[name]
k1 = kenn(sweep(K, H, L, C, 0, mid, m, be))
k2 = kenn(sweep(K, H, L, C, mid, len(C), m, be))
if not (k1 and k2 and i1 and i2):
ok = False; break
if not (k1["oer"] > i1["oer"] and k2["oer"] > i2["oer"]
and k1["sr"] > i1["sr"] and k2["sr"] > i2["sr"]
and k1["worst"] >= i1["worst"] - 1e-9
and k2["worst"] >= i2["worst"] - 1e-9):
ok = False; break
if ok:
treffer.append((m, be))
if treffer:
print(f" Kandidaten (vor Nachbar-Pruefung): {treffer}")
print(" ⚠ Nachbarn von Hand gegenpruefen — eine einzelne bessere Zelle")
print(" zwischen schlechteren ist Rauschen.")
else:
print(" => KEIN Paar schlaegt den Ist-Stand auf beiden Populationen.")
print(" Trailing 1,0 / Breakeven 1,3 bleibt.")
print()
if __name__ == "__main__":
main()