Files
AH-Oil-Trader/backtest_trailmult.py
T
Axel HocksandClaude Opus 5 4c1a68a7e1 Stufe 3: geteilter Exit-Kern (core/exit_model.py) + Korrektur der Stufe-1-Begruendung
core/exit_model.py (NEU): LIVE:ExitParams als EINZIGE Quelle der Exit-
Parameter; core/trailing.py leitet _TRAIL_START_ATR, _BREAKEVEN_ATR,
_PHASE4_* und _MULT_BY_TF jetzt davon ab statt eigene Zahlen zu halten.
Aendert jemand LIVE.mult, aendern sich Live-Verhalten UND Messung gemeinsam
-> Deployment-Drift-Fall 3 ist konstruktiv unmoeglich geworden. Dazu die
kanonische simulate() fuer Backtests mit Flags timestop/use_tp, um
Teilmodelle EXPLIZIT zu machen statt zu verstecken.

BEFUNDE BEIM REFACTOR - schlimmer als angenommen:
(1) Es gibt mindestens DREI materiell verschiedene Exit-Modelle:
    A) Phasen ~ live (_trailing/_exit/_atrfloor/_candle_fade)
    B) EINFACH - _TPTRAIL=0.5, kein Breakeven, kein Lock, kein Time-Stop,
       _MAXH=240 (_hourly, _hourly_split, _bounce, _events, _deadhour,
       _chopgate)
    C) Phasen mit be_on=1.0 statt 1,3 (_breakout, _confluence_angle)
    => Dead-Hours, EIA-Blackout, Bounce, Chop-Gate, Stunden-Analyse und
    breakout_k ruhen auf einem Exit, der dem Live-System nicht entspricht.
    Bewusst NICHT stillschweigend umgestellt (wuerde historische Schluesse
    rueckwirkend aendern); als LEGACY_SIMPLE / LEGACY_BE10 markiert.
(2) Selbst die "Phasen"-Skripte weichen voneinander ab: _trailing hat TP
    aber keinen Time-Stop, _candle_fade Time-Stop aber kein TP, _atrfloor
    liefert Punkte statt R.
(3) backtest_trailing.py koppelt den Initial-SL an mult (sl = entry -
    d*mult*atr) statt fix 2,0.

KORREKTUR DER STUFE-1-BEGRUENDUNG: die "67 % groesserer Einzelverlust" war
ein Artefakt von (3) - dort war der Worst-Case per Konstruktion gleich dem
Multiplikator. Sauber nachgemessen mit fixem SL (backtest_trailmult.py, NEU,
80k Bars, 2 Halbjahre):
    Trail 1,0  H1  -73 · H2 +1346 · Worst -2,00
    Trail 1,5  H1 -325 · H2 +1294 · Worst -2,00   (live)
    Trail 2,0  H1 -446 · H2 +1195 · Worst -2,00
    Trail 2,5  H1 -1174 · H2 +1223 · Worst -2,00
    Trail 3,0  H1 -1088 · H2 +1397 · Worst -2,00
Der Worst-Case ist bei JEDEM Multiplikator identisch -2,00. Die Entscheidung
bleibt richtig (1,5 schlaegt 2,0 und 2,5 in beiden Haelften), nur die
Tail-Begruendung war falsch.

NEU UND OFFEN: Trail 1,0 schlaegt 1,5 in BEIDEN Haelften - eigener
Vorschlag, bewusst nicht ungefragt umgesetzt.

Aequivalenz verifiziert (500 synthetische Kursreihen je Fall): _candle_fade
und _atrfloor sind bitgenau identisch zur neuen simulate().
Live verifiziert: "AKTIVIERT ATR=0.5796 (M30) mult=1.5x".

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

117 lines
4.4 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-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 main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
step = int(sys.argv[2]) if len(sys.argv) > 2 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)
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]
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}")
print("=" * 92)
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")
for mult in (1.0, 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) for (i, d, atr) in SIG[lbl]]
print(f" {lbl} {st(Rs)}")
if __name__ == "__main__":
main()