Files
AH-Oil-Trader/backtest_trailing_sr.py
T
Axel HocksandClaude Opus 5 9ad1b6d8d4 backtest_trailing_sr.py: S/R-verankertes Trailing gemessen - nicht gebaut
User-Idee "Trailing TP und SL auf die S/R-Linien anpassen". Zerfaellt in
drei Teile; Initial-SL an S/R und fester TP am Gegenlevel waren schon
verworfen, der TRAILING-Teil war offen (bisher nur ATR-Abstaende getestet).

Aufbau wie backtest_trailing.py (echte Phasen-Mechanik, Breakeven 1,3),
Level = M30-Pivots k=3 KAUSAL (Pivot erst 3 M30-Bars spaeter bekannt),
Deckel [0,5..2,0]xATR gegen den -11,4xATR-Fall aus backtest_sl_method.py.
80k Bars, ~19,7k Signale je Haelfte.

Ergebnis: schadet nicht, traegt aber zu wenig. 3 von 5 Varianten schlagen
die Basis in BEIDEN Haelften (Hybrid "engerer Stop" +0,006/+0,006, WR
37->43 %). Trotzdem nicht gebaut: (a) Effekt 0,006-0,014 R/Trade = eine
Groessenordnung unter der B3-Huerde; (b) nicht parameter-robust - Puffer
0,10 besteht, 0,25 faellt durch, 0,50 besteht wieder.

Positiv: Worst-Case bleibt in ALLEN Varianten -2,00xATR, der Deckel
funktioniert - S/R-Verankerung fuegt kein Tail-Risiko hinzu.

Die Kontrollvariante "TP am Gegenlevel" reproduziert den alten Befund
exakt (WR 40->54 %, SigmaR H2 +1261->+740 = Gewinner-Kappen) -> Simulation
gegen backtest_srclose.py validiert, TP-Teil endgueltig erledigt.

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

252 lines
9.9 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
"""S/R-verankertes TRAILING statt fixem ATR-Abstand (User-Idee 2026-07-31).
FRAGE: Der Live-Trail zieht den SL stur `mult×ATR` hinter das Hoch. Wäre es besser,
ihn hinter das **nächste S/R-Level** zu legen (LONG: knapp unter die Unterstützung)?
Und/oder einen TP ans Gegenlevel zu setzen?
⚠ ABGRENZUNG — zwei der drei Teil-Ideen sind SCHON gemessen und durchgefallen:
* **Initial-SL an S/R** → `backtest_sl_method.py`: gedeckelt identisch zu fix
2,0×ATR (Ø-R 0,1960,201), der Band-Cap dominiert; ungedeckelt Worst 11,4×ATR.
* **Fester TP am Gegenlevel** → `backtest_srclose.py` (2×): verdoppelt die WR,
verschlechtert aber PF/ΣR in BEIDEN Hälften = Gewinner-Kappen.
* **TRAILING-SL an S/R** = hier NEU gemessen (bisher nur ATR-Abstände getestet,
`backtest_trailing.py`).
Deshalb wird der TP-Teil hier NUR als Kontrollvariante mitgeführt (Erwartung: fällt
durch — dient als Sanity-Check, dass die Sim mit dem alten Befund übereinstimmt).
AUFBAU (identisch zu `backtest_trailing.py`, damit die Zahlen vergleichbar sind):
echte Phasen-Mechanik (Init→Trail→Lock, HW-Ratsche, Breakeven-Boden 1,3), Signal-Set
Trend+Reversal, 2 Halbjahre. Level-Quelle = **M30-Pivots** (k=3) — dieselbe Quelle,
auf die der Live-Bot am 2026-07-30 umgestellt hat; kausal (ein Pivot ist erst 3 M30-
Bars später bekannt).
Urteil nur „trägt", wenn eine Variante in BEIDEN Hälften ≥ Live-Basis liegt UND der
Worst-Case nicht davonläuft (die Lehre aus `backtest_sl_method.py`).
"""
import sys
from bisect import bisect_left
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
_REVERSAL_STRETCH, _STRETCH_MAX)
_MAXH = 200; _ATRMIN = 0.12; _TP_INIT = 3.5
_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2
_BE = 1.3 # Breakeven-Boden (live)
_M30 = 6 # M5-Bars je M30-Bar
_PIV_K = 3 # Pivot-Fenster (wie live `pb_levels30`)
_PIV_LOOKBACK = 50 # letzte N M30-Bars (wie live)
# Sicherheits-Deckel für den S/R-Trail: ohne den droht der 11,4×ATR-Fall aus
# `backtest_sl_method.py` (Level zu weit weg = Kontokiller unter Margin-Sizing).
_SL_MIN_ATR = 0.5
_SL_MAX_ATR = 2.0
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 build_levels(H, L):
"""M30-Pivots (k=3) je M5-Bar-Index — KAUSAL.
Liefert `lv[m]` = sortierte Level-Liste, die zum Beginn von M30-Bar `m` bereits
bekannt ist (ein Pivot bei M30-Index p ist erst ab p+k bestätigt).
"""
nm = len(H) // _M30
mh = [max(H[i * _M30:(i + 1) * _M30]) for i in range(nm)]
ml = [min(L[i * _M30:(i + 1) * _M30]) for i in range(nm)]
# Pivot bei p → bekannt ab p + _PIV_K
born: list[list[float]] = [[] for _ in range(nm)]
for p in range(_PIV_K, nm - _PIV_K):
seg_h = mh[p - _PIV_K:p + _PIV_K + 1]
seg_l = ml[p - _PIV_K:p + _PIV_K + 1]
if mh[p] == max(seg_h):
if p + _PIV_K < nm:
born[p + _PIV_K].append(mh[p])
if ml[p] == min(seg_l):
if p + _PIV_K < nm:
born[p + _PIV_K].append(ml[p])
# rollierendes Fenster der letzten _PIV_LOOKBACK M30-Bars
lv: list[list[float]] = []
from collections import deque
win: deque = deque()
cur: list[float] = []
for m in range(nm):
win.append(born[m])
cur.extend(born[m])
while len(win) > _PIV_LOOKBACK:
old = win.popleft()
for x in old:
try:
cur.remove(x)
except ValueError:
pass
lv.append(sorted(cur))
return lv
def _sr_below(levels, px):
i = bisect_left(levels, px)
return levels[i - 1] if i > 0 else None
def _sr_above(levels, px):
i = bisect_left(levels, px)
return levels[i] if i < len(levels) else None
def sim(entry, d, atr, H, L, C, j0, LV, mode="atr", mult=1.5,
buf=0.25, trail_start=0.3, use_sr_tp=False):
"""Phasen-Trailing. `mode`:
'atr' — Live: SL = HW mult×ATR
'sr' — SL = nächstes Level in Gegenrichtung ∓ buf×ATR (auf [0,5..2,0]×ATR gedeckelt)
'hybrid' — der ENGERE von beiden (schützt Gewinne früher)
'sr_wide'— der WEITERE von beiden (gibt dem Trade mehr Luft)
"""
sl = entry - d * 2.0 * atr # Initial-SL wie live (2,0×ATR)
tp = entry + d * _TP_INIT * atr
if use_sr_tp:
lv = LV[min(j0 // _M30, len(LV) - 1)]
t = _sr_above(lv, entry) if d > 0 else _sr_below(lv, entry)
if t is not None:
tp = t
hw = entry; rank = 0
end = min(j0 + _MAXH, len(C) - 1)
for j in range(j0, end + 1):
hi, lo = H[j], L[j]
if (lo <= sl) if d > 0 else (hi >= sl):
return (sl - entry) * d / atr # pessimistisch: SL vor TP im selben Bar
if (hi >= tp) if d > 0 else (lo <= tp):
return (tp - entry) * d / atr
hw = max(hw, hi) if d > 0 else min(hw, lo)
profit = (hw - entry) * d
ph = 0 if profit < trail_start * atr else (1 if profit < _LOCK_START * atr else 2)
if ph < rank:
ph = rank
rank = ph
if ph == 0:
continue
m = mult if ph == 1 else max(_LOCK_MIN, mult * _LOCK_SCALE)
cand_atr = hw - d * m * atr
cand = cand_atr
if mode != "atr":
lv = LV[min(j // _M30, len(LV) - 1)]
lev = _sr_below(lv, C[j]) if d > 0 else _sr_above(lv, C[j])
if lev is not None:
cand_sr = lev - d * buf * atr
# Deckel: nie enger als 0,5×ATR (Rauschen) / weiter als 2,0×ATR (Tail)
dist = (C[j] - cand_sr) * d
if dist < _SL_MIN_ATR * atr:
cand_sr = C[j] - d * _SL_MIN_ATR * atr
elif dist > _SL_MAX_ATR * atr:
cand_sr = C[j] - d * _SL_MAX_ATR * atr
if mode == "sr":
cand = cand_sr
elif mode == "hybrid": # der engere = höher (LONG)
cand = max(cand_atr, cand_sr) if d > 0 else min(cand_atr, cand_sr)
elif mode == "sr_wide":
cand = min(cand_atr, cand_sr) if d > 0 else max(cand_atr, cand_sr)
if profit >= _BE * atr or ph == 2:
cand = max(cand, entry) if d > 0 else min(cand, entry)
sl = max(sl, cand) if d > 0 else min(sl, cand)
return (C[end] - entry) * d / atr
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) - _MAXH - 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 = None
for req in (n, 100000, 80000, 60000, 40000):
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req)
if bars is not None and len(bars) > 2000:
break
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)
LV = build_levels(H, L)
mid = len(C) // 2
halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))]
print("=" * 96)
print(f" S/R-VERANKERTES TRAILING — {sym} M5 ({len(C)} Bars, step {step})")
print(f" Level = M30-Pivots k={_PIV_K}, Fenster {_PIV_LOOKBACK} M30-Bars (kausal), "
f"Deckel [{_SL_MIN_ATR}..{_SL_MAX_ATR}]×ATR")
print("=" * 96)
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")
variants = [
("LIVE ATR-Trail 1,5 (Basis)", dict(mode="atr", mult=1.5)),
("S/R-Trail Puffer 0,10", dict(mode="sr", buf=0.10)),
("S/R-Trail Puffer 0,25", dict(mode="sr", buf=0.25)),
("S/R-Trail Puffer 0,50", dict(mode="sr", buf=0.50)),
("Hybrid (engerer von beiden)", dict(mode="hybrid", buf=0.25)),
("Hybrid weit (weiterer)", dict(mode="sr_wide", buf=0.25)),
("KONTROLLE: TP am Gegenlevel", dict(mode="atr", mult=1.5, use_sr_tp=True)),
]
for name, kw in variants:
print(f"\n{name}")
for lbl, _, _ in halves:
Rs = [sim(C[i], d, atr, H, L, C, i + 1, LV, **kw) for (i, d, atr) in SIG[lbl]]
print(f" {lbl} {st(Rs)}")
if __name__ == "__main__":
main()