Files
AH-Oil-Trader/backtest_trailmult.py
Axel HocksandClaude Opus 5 bb17ecff0a Beide offenen Vorschlaege umgesetzt: Trail 1,0 + Legacy-Recheck
VORSCHLAG 1 - Trail-Multiplikator 1,5 -> 1,0 (core/exit_model.py LIVE.mult),
live verifiziert ("AKTIVIERT ... mult=1.0x").
Voller Sweep mit FIXEM SL 2,0 (backtest_trailmult.py, 8 Werte 0,5-3,0):
enger ist MONOTON besser. Wave-Signal H1/H2 SigmaR:
    0,5 +1258/+2418 · 0,8 +852/+1668 · 1,0 +80/+1360
    1,5 -220/+1305 · 2,0 -394/+1198 · 2,5 -1072/+1327
Optimum am RAND = Warnsignal, deshalb Gegentest auf SQUEEZE-Entries mit
Echtkosten (--squeeze): das Setup lebt von Laeufern, muesste also
dagegenhalten - tut es NICHT (1,0: H1 -0,202/H2 +0,010 · 1,5:
-0,219/-0,032). Deshalb 1,0 (besser als 1,5 in beiden Haelften auf BEIDEN
Signalmengen), aber NICHT 0,5: Randwert, und die Sim modelliert keine
Exit-Slippage - ein engerer Trail loest viel haeufiger aus und ist davon
staerker betroffen (real bis 0,75xATR ueber den Stop).

VORSCHLAG 2 - backtest_legacy_recheck.py (NEU): die beiden Legacy-Befunde
nachgerechnet, die live ECHTE Gates steuern.
  EIA-Blackout BESTAETIGT: auch mit echtem Exit + Echtkosten in BEIDEN
  Haelften schlechter als der Rest (-0,097 / -0,119). Gate ist gedeckt.
  DEAD-HOURS reproduzieren NICHT als selektiver Befund: 19 von 22 Stunden
  sind in beiden Haelften negativ, keine robust positive Stunde. Die alte
  Auswahl (0-7, 12, 16) ist damit nicht mehr gestuetzt. Praktisch folgenlos
  (dead_hours ist leer), aber eine Reaktivierung auf der alten Begruendung
  waere nicht gedeckt.

Einschraenkung selbst benannt: der Recheck handelt die UNGEGATETE
EMA-Richtung (ohne min_conf, Breakout-Bestaetigung, HTF-Filter, Raum-Gate).
Fuer den Vergleich neutral, die absoluten Werte sind nicht das Live-Signal.

Konsequenz fuer den TF-Churn-Fix: "WARTEN Richtung 43 %" ist eine
Haeufigkeits-, keine Ertragsgroesse - mehr Signale sind nur dann besser,
wenn die freigegebenen Setups auch tragen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:09:32 +02:00

161 lines
6.3 KiB
Python
Raw Permalink 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-Multiplikator SAUBER gemessen — Initial-SL fest bei 2,0×ATR wie live.
⚠ WARUM DIESES SKRIPT EXISTIERT (2026-07-31, Fund beim Stufe-3-Refactor):
`backtest_trailing.py` setzt den Initial-SL auf **`mult`×ATR** statt auf die feste
Live-Größe 2,0×ATR:
sl = entry - d*mult*atr # <-- Initial-SL = mult
Der dortige Vergleich „mult 1,5 vs 2,5" misst also in Wahrheit
*(SL 1,5 · Trail 1,5)* gegen *(SL 2,5 · Trail 2,5)* — zwei Änderungen auf einmal.
Deshalb war der Worst-Case dort immer exakt gleich dem Multiplikator (1,50 / 2,50):
das war der **SL**, nicht der Trail. Live ist der SL dagegen fest (Band 1,82,2 →
2,0, `backtest_exit.py`), nur der Trail hing an der Zeitebene.
Die auf dieser Grundlage getroffene Stufe-1-Entscheidung (`_MULT_BY_TF` überall auf
1,5) muss deshalb neu belegt werden — HIER, mit fixem SL und variablem Trail, also
genau der Änderung, um die es live geht.
Aufbau ansonsten identisch zu `backtest_trailing.py` (gleiches Signal-Set
Trend+Reversal, 2 Halbjahre), Exit aus `core/exit_model.py` — der gemeinsamen Quelle,
damit dieser Fehler nicht noch einmal passieren kann.
"""
import sys
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
_REVERSAL_STRETCH, _STRETCH_MAX)
_ATRMIN = 0.12
def _ema_series(v, p):
k = 2.0 / (p + 1); o = []; e = v[0]
for i, x in enumerate(v):
e = x if i == 0 else x * k + e * (1 - k)
o.append(e)
return o
def _atr_series(H, L, C, p=14):
t = [0.0]
for i in range(1, len(C)):
t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
out = []
for i in range(len(C)):
if not i:
out.append(None); continue
w = t[max(1, i - p + 1):i + 1]
out.append(sum(w) / max(1, len(w)))
return out
def st(Rs):
if not Rs:
return " -"
n = len(Rs); w = sum(1 for r in Rs if r > 0)
g = sum(r for r in Rs if r > 0); ls = -sum(r for r in Rs if r < 0)
return (f"WR={100*w/n:>3.0f}% Ø-R={sum(Rs)/n:+.3f} PF={(g/ls if ls > 0 else 99):>4.2f} "
f"Worst={min(Rs):+.2f} ΣR={sum(Rs):+.0f}")
def signals(H, L, C, ES, EF, AT, lo, hi, step):
TH = _REVERSAL_STRETCH; out = []
for i in range(max(lo, _N_BARS), min(hi, len(C) - LIVE.max_hold - 1), step):
atr = AT[i]
if not atr or atr <= 0:
continue
atr = max(atr, _ATRMIN); es = ES[i]; ef = EF[i]
stretch = (C[i] - es) / atr
ang = calc_trend_angle(C[i - _ANGLE_LR - 2:i], _ANGLE_LR); ad = ang - 90.0
d = 0
if stretch <= -TH and ad >= _ANGLE_DEAD: d = 1
elif stretch >= TH and ad <= -_ANGLE_DEAD: d = -1
elif abs(stretch) < _STRETCH_MAX: d = 1 if ef > es else -1 if ef < es else 0
if d:
out.append((i, d, atr))
return out
def squeeze_signals(H, L, C, AT, lo, hi):
"""Nur SQUEEZE-Ausbrüche — der EINZIGE Setup mit belegtem Edge, und er lebt von
LÄUFERN (43 % Treffer, Ertrag im Tail). Gegentest zum Wave-Signal: ein enger
Trail, der dort gewinnt, muss hier verlieren, wenn er nur „früher aussteigen"
belohnt. Regel exakt wie live (`wave._squeeze_one`)."""
from core.wave_rec import _SQ_N, _SQ_MULT, _SQ_K
_W = 24; _COOL = 12
out = []
i = max(lo, _N_BARS, _SQ_N + 15)
while i < min(hi, len(C) - LIVE.max_hold - 1):
atr = AT[i]
if not atr or atr < 0.06:
i += 1; continue
boxHi = max(H[i - _SQ_N:i]); boxLo = min(L[i - _SQ_N:i])
if (boxHi - boxLo) > _SQ_MULT * atr:
i += 1; continue
hit = None
for j in range(i, min(i + _W, len(C) - LIVE.max_hold - 1)):
up = boxHi + _SQ_K * atr; dn = boxLo - _SQ_K * atr
if H[j] >= up: hit = (j, 1, up); break
if L[j] <= dn: hit = (j, -1, dn); break
if hit is None:
i += 1; continue
j, d, lvl = hit
out.append((j, d, atr, lvl))
i = j + _COOL
return out
def main():
squeeze = "--squeeze" in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith("--")]
n = int(args[0]) if args else 80000
step = int(args[1]) if len(args) > 1 else 2
mt5.initialize(); sym = None
for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
if mt5.symbol_info(c):
sym = c; break
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
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]
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
mid = len(C) // 2
halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))]
print("=" * 92)
print(f" TRAIL-MULTIPLIKATOR — {sym} M5 ({len(C)} Bars)")
print(f" Initial-SL FEST {LIVE.sl_atr}×ATR (wie live) · nur der Trail variiert")
print(f" Exit aus core/exit_model.py · Time-Stop {LIVE.timestop_bars} Bars · BE {LIVE.be}")
if squeeze:
print(" Signal-Set: NUR SQUEEZE-Ausbrueche (laeuferabhaengig) · ECHTKOSTEN")
print("=" * 92)
if squeeze:
SIG = {lbl: [(i, d, atr) for (i, d, atr, _lv)
in squeeze_signals(H, L, C, AT, a, b)] for lbl, a, b in halves}
else:
SIG = {lbl: signals(H, L, C, ES, EF, AT, a, b, step) for lbl, a, b in halves}
for lbl, _, _ in halves:
print(f" {lbl}: {len(SIG[lbl])} Signale")
# ⚠ Auch die kleinen Werte prüfen: läuft es nach unten monoton weiter besser,
# liegt das „Optimum" am Rand des getesteten Bereichs = Warnsignal (dann misst
# man womöglich nur „macht weniger" statt „macht es besser").
for mult in (0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0):
tag = " ← live (nach Stufe 1)" if abs(mult - 1.5) < 1e-9 else ""
print(f"\nTrail {mult:.1f}×ATR{tag}")
for lbl, _, _ in halves:
p = LIVE.with_(mult=mult)
Rs = [simulate(C[i], d, atr, H, L, C, i + 1, p)
- ((SP[i] if SP[i] > 0 else 0.0225) / atr if squeeze else 0.0)
for (i, d, atr) in SIG[lbl]]
print(f" {lbl} {st(Rs)}")
if __name__ == "__main__":
main()