Files
AH-Oil-Trader/backtest_exit2.py
T
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

117 lines
5.3 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
"""Exit-Refactoring: dreht eine andere Exit-Logik die VERLUST-ASYMMETRIE um?
Live-Problem (real gemessen): WR 59%, aber Ø-Gewinn nur ~0,47× Ø-Verlust → PF 0,68.
Ursache: enges Trailing-TP (HW0,5×ATR) kappt Gewinner, fixer 2×ATR-SL lässt
Verlierer voll laufen → kleine Gewinne, große Verluste.
Hier: gleiche Signale, verschiedene Exit-Modelle. Kennzahl = avg_win / avg_loss
(>=1 = Schiefe gedreht) + ΣR/PF. Trailing-STOP-Distanz und Initial-SL variiert.
Pessimistisch (Gegenlauf vor Mitlauf).
"""
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)
_MAXH = 288; _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])))
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 sim(entry,d,atr,H,L,C,j0, sl_atr, trail, trail_on=0.3, be_on=1.0, tp_atr=None):
"""Trailing-STOP-Modell: Initial-SL sl_atr; ab trail_on Profit Stop = HW∓trail×ATR
(Breakeven-Floor ab be_on). Optionaler harter TP. Rückgabe R."""
eff = entry - d*sl_atr*atr
hw = entry
tp = (entry + d*tp_atr*atr) if tp_atr else None
end=min(j0+_MAXH, len(C)-1); exit_px=C[end]
for j in range(j0,end+1):
hi,lo=H[j],L[j]
# Gegenlauf zuerst (Stop)
if (lo<=eff) if d>0 else (hi>=eff): exit_px=eff; break
# harter TP
if tp is not None and ((hi>=tp) if d>0 else (lo<=tp)): exit_px=tp; break
hw=max(hw,hi) if d>0 else min(hw,lo)
prof=(C[j]-entry)*d
if trail and prof>=trail_on*atr:
cand=hw-d*trail*atr
if prof>=be_on*atr:
cand=max(cand,entry) if d>0 else min(cand,entry)
eff=max(eff,cand) if d>0 else min(eff,cand)
return (exit_px-entry)*d/atr
def stats(name, Rs):
if not Rs: print(f" {name:<26} -"); return
n=len(Rs); wins=[r for r in Rs if r>0]; loss=[r for r in Rs if r<=0]
aw=sum(wins)/len(wins) if wins else 0; al=sum(loss)/len(loss) if loss else 0
ratio=(aw/abs(al)) if al<0 else 99
g=sum(wins); ls=-sum(loss); pf=g/ls if ls>0 else 99
print(f" {name:<26} n={n:>4} WR={100*len(wins)/n:>3.0f}% Øgew={aw:+.2f} Øverl={al:+.2f} "
f"Verh={ratio:>4.2f} ØR={sum(Rs)/n:+.3f} PF={pf:>4.2f} ΣR={sum(Rs):+.0f} Worst={min(Rs):+.1f}")
def main():
n=int(sys.argv[1]) if len(sys.argv)>1 else 40000
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+500)
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)
import bisect
def m30s(ts):
idx=bisect.bisect_right(mT,ts)-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)
class _TU:
def snapshot(self): return {"intervals": {}}
w=WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
sigs=[]
for i in range(_N_BARS, 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)
a5=calc_trend_angle(C[i-_ANGLE_LR-2:i],_ANGLE_LR)
rec,_=w._build(ef,es,C[i-1],atr,"M5",0,htf_trend=m30s(T[i]),angle=a5)
if rec["signal"]=="WARTEN": continue
d=1 if rec["signal"]=="LONG" else -1
sigs.append((i,d,max(atr,_ATRMIN)))
print("="*112)
print(f" Exit-Refactoring — {sym} M5 Signale={len(sigs)} (Ziel: Verh = Øgew/Øverl ≥ 1)")
print("="*112)
def run(name,**kw):
Rs=[sim(C[i],d,atr,H,L,C,i+1,**kw) for (i,d,atr) in sigs]
stats(name,Rs)
print("AKTUELL (Problem):")
run("SL2.0 + Trail 0.5 (live)", sl_atr=2.0, trail=0.5, be_on=1.0)
print("\nGewinner laufen lassen (weiteres Trailing):")
run("SL2.0 + Trail 1.0", sl_atr=2.0, trail=1.0, be_on=1.0)
run("SL2.0 + Trail 1.5", sl_atr=2.0, trail=1.5, be_on=1.0)
run("SL2.0 + Trail 2.0", sl_atr=2.0, trail=2.0, be_on=1.0)
print("\nVerlierer enger cutten (kleinerer Initial-SL) + Trailing 1.5:")
run("SL1.0 + Trail 1.5", sl_atr=1.0, trail=1.5, be_on=0.8)
run("SL1.2 + Trail 1.5", sl_atr=1.2, trail=1.5, be_on=0.8)
run("SL1.5 + Trail 1.5", sl_atr=1.5, trail=1.5, be_on=1.0)
print("\nFeste R:R-Ziele (TP erzwingt Symmetrie):")
run("SL1.5 + TP2.25 (RR1.5)", sl_atr=1.5, trail=None, tp_atr=2.25)
run("SL1.5 + TP3.0 (RR2.0)", sl_atr=1.5, trail=None, tp_atr=3.0)
run("SL2.0 + TP4.0 (RR2.0)", sl_atr=2.0, trail=None, tp_atr=4.0)
if __name__=="__main__":
main()