Files
AH-Oil-Trader/backtest_sl_method.py
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:29:23 +02:00

134 lines
6.1 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
"""Vergleicht SL-PLATZIERUNGEN unter dem aktuellen Live-Exit (SL FIX + Trailing-TP,
_TRAIL_SL=False). Nur die Initial-SL-Position variiert, der Exit-Mechanismus ist
identisch → fairer Vergleich.
S/R-Proxy = jüngstes Swing-Extrem (Pivot) über _SRWIN Bars — das ist die Struktur,
an der auch der Live-Pivot hängt (echte historische Web-/Zone-Level liegen nicht
vor, deshalb dieser Proxy).
Exit-Modell (konservativ, pessimistisch: Gegenlauf VOR Mitlauf):
- SL fix bei Initial-Distanz bis Profit >= 0.3xATR
- danach Profit-Lock = HW ∓ 0.5xATR (= das Live-Trailing-TP), Ratsche
- Halt <= _MAXH Bars
PnL in R (= Profit / ATR), zusätzlich Ø-SL-Distanz, Frühstopp%, Worst-R.
"""
import sys
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.wave_rec import (WaveRecommender, _atr, _ema_last, _EMA_FAST, _EMA_SLOW,
_N_BARS, _HTF_DEADBAND, _ANGLE_LR)
from core.config import INIT_SL_MIN_ATR, INIT_SL_MAX_ATR
_MAXH = 240
_TRAILON= 0.3 # ab hier Profit-Lock aktiv
_TPTRAIL= 0.5 # _TP_TRAIL_ATR
_SRWIN = 24 # Lookback für Swing-Extrem (S/R-Proxy), ~2h M5
_BUFTCK = 0.02 # Tick-Puffer Methode "Pivot" (~live SL_BUFFER_TICKS)
_ATRMIN = 0.12
class _TU:
def snapshot(self): return {"intervals": {}}
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])))
return [(sum(t[max(1,i-p+1):i+1])/max(1,len(t[max(1,i-p+1):i+1]))) if i else None for i in range(len(C))]
def clamp_dist(dist, atr):
return max(INIT_SL_MIN_ATR*atr, min(dist, INIT_SL_MAX_ATR*atr))
def simulate(entry, d, atr, sl, H, L, C, j0):
"""Fixed-SL + Trailing-Profit-Lock. Rückgabe (R, init_stop)."""
eff = sl; hw = entry; trail = False
end = min(j0+_MAXH, len(C)-1); exit_px = C[end]
for j in range(j0, end+1):
hi, lo = H[j], L[j]
hit = (lo <= eff) if d>0 else (hi >= eff) # Gegenlauf zuerst
if hit:
exit_px = eff; return (exit_px-entry)*d/atr, (not trail)
hw = max(hw,hi) if d>0 else min(hw,lo)
if (C[j]-entry)*d >= _TRAILON*atr: trail = True
if trail:
lock = hw - d*_TPTRAIL*atr
eff = max(eff,lock) if d>0 else min(eff,lock)
return (exit_px-entry)*d/atr, False
def main():
n=int(sys.argv[1]) if len(sys.argv)>1 else 12000
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+_N_BARS+_MAXH+5)
m30=mt5.copy_rates_from_pos(sym,mt5.TIMEFRAME_M30,0,n//6+400)
mt5.shutdown()
T=[int(b["time"]) for b in bars]; H=[float(b["high"]) for b in bars]
L=[float(b["low"]) for b in bars]; C=[float(b["close"]) for b in bars]
mT=[int(b["time"]) for b in m30]; mc=[float(b["close"]) for b in m30]
mh=[float(b["high"]) for b in m30]; ml=[float(b["low"]) for b in m30]
mEf=_ema_series(mc,_EMA_FAST); mEs=_ema_series(mc,_EMA_SLOW); mA=_atr_series(mh,ml,mc)
def m30s(ts):
lo,hi,idx=0,len(mT)-1,-1
while lo<=hi:
md=(lo+hi)//2
if mT[md]<=ts: idx=md; lo=md+1
else: hi=md-1
if idx<_EMA_SLOW or mA[idx] is None or mA[idx]<=0: return 0
dd=mEf[idx]-mEs[idx]
return 0 if abs(dd)<_HTF_DEADBAND*mA[idx] else (1 if dd>0 else -1)
w=WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
sigs=[]
for i in range(max(_N_BARS,_SRWIN), len(C)-_MAXH-1):
wc=C[i-_N_BARS:i]; wh=H[i-_N_BARS:i]; wl=L[i-_N_BARS:i]
atr=_atr(wh,wl,wc)
if not atr or atr<=0: continue
ef=_ema_last(wc,_EMA_FAST); es=_ema_last(wc,_EMA_SLOW)
ang=calc_trend_angle(C[i-_ANGLE_LR-2:i],_ANGLE_LR)
rec,_=w._build(ef,es,C[i-1],atr,"M5",5,htf_trend=m30s(T[i]),angle=ang)
if rec["signal"]=="WARTEN": continue
d=1 if rec["signal"]=="LONG" else -1
atr=max(atr,_ATRMIN)
# S/R-Proxy = jüngstes Swing-Extrem über _SRWIN Bars
sr = min(L[i-_SRWIN:i]) if d>0 else max(H[i-_SRWIN:i])
sigs.append((i, d, atr, C[i], sr))
# SL-Platzierungs-Methoden: dist(entry,sr,atr,d) → SL-Distanz
def m_pivot(e,sr,atr,d): return clamp_dist(abs(e-sr)+_BUFTCK, atr) # ~aktuell
def m_sr_atr(e,sr,atr,d): return clamp_dist(abs(e-sr)+0.3*atr, atr) # S/R ∓0.3ATR, gedeckelt
def m_sr_010c(e,sr,atr,d):return clamp_dist(abs(e-sr)+0.10, atr) # S/R ∓0.10, gedeckelt
def m_sr_010u(e,sr,atr,d):return max(0.5*atr, abs(e-sr)+0.10) # S/R ∓0.10, UNgedeckelt
def m_fix20(e,sr,atr,d): return 2.0*atr # fest 2.0×ATR
methods=[("Aktuell Pivot+Band",m_pivot),("S/R ∓0.3ATR (gedeckelt)",m_sr_atr),
("S/R ∓0.10 (gedeckelt)",m_sr_010c),("S/R ∓0.10 (UNgedeckelt)",m_sr_010u),
("fest 2.0×ATR",m_fix20)]
print("="*92)
print(f" SL-Platzierung — {sym} M5+M30 Signale={len(sigs)} Exit: SL fix + Trailing-TP")
print(f" Band [{INIT_SL_MIN_ATR}{INIT_SL_MAX_ATR}]×ATR · S/R-Proxy=Swing über {_SRWIN} Bars · pessimistisch")
print("="*92)
print(f" {'Methode':<26}{'Treffer':>8}{'Ø-R':>8}{'Summe-R':>9}{'PF':>6}"
f"{'Ø-Verl':>8}{'Worst':>8}{'Frühstop':>9}{'Ø-SLdist':>10}")
for name,fn in methods:
Rs=[]; dists=[]; istop=0
for (i,d,atr,e,sr) in sigs:
dist=fn(e,sr,atr,d)
sl=e-d*dist
R,ist=simulate(e,d,atr,sl,H,L,C,i+1)
Rs.append(R); dists.append(dist/atr)
if ist: istop+=1
n2=len(Rs); win=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)
lo=[r for r in Rs if r<0]; pf=g/ls if ls>0 else 99.9
print(f" {name:<26}{100*win/n2:>7.0f}%{sum(Rs)/n2:>8.3f}{sum(Rs):>9.1f}"
f"{pf:>6.2f}{(sum(lo)/len(lo) if lo else 0):>8.2f}{min(Rs):>8.2f}"
f"{100*istop/n2:>8.0f}%{sum(dists)/len(dists):>9.2f}×")
if __name__=="__main__":
main()