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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
578711a290
commit
9ad1b6d8d4
@@ -0,0 +1,251 @@
|
||||
#!/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,196–0,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()
|
||||
Reference in New Issue
Block a user