Files
AH-Oil-Trader/backtest_srbreak.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

144 lines
7.0 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
"""User-Regel messen: „P(Level-Durchbruch) > 60 % → laufen lassen, sonst close."
Dazu 2 Fragen (Track B, Echtkosten, 2 Hälften):
(1) Wie hoch ist die Basisrate P(Durchbruch) beim ERSTEN Touch des Ziel-Levels —
und können Merkmale am Touch (Trend, Momentum, Touch-Zahl, Tick-Volumen) sie
robust über/unter 60 % schieben? Break = nach Touch +0,5×ATR JENSEITS des
Levels binnen 12 Bars, bevor 0,5×ATR zurückfällt.
(2) Entscheidungs-relevant: Ø(R_run R_close) je Merkmal-Segment — gibt es ein
Segment, in dem CLOSE am Level das Weiterlaufen (Trailing) robust schlägt?
Wenn kein Merkmal P(break) verlässlich trennt bzw. kein Segment robust pro-Close
ist, kollabiert die Regel zu „immer laufen lassen" (= Trailing, schon gemessen).
"""
import sys
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; _SL_ATR=2.0; _TRAILON=0.3; _MULT=1.5; _BE=1.3
_LOCK_START=3.5; _LOCK_SCALE=0.6; _LOCK_MIN=1.2; _TP_INIT=3.5
_PIV_K=3; _LOOKBACK=300; _BRK_W=12; _BRK_ATR=0.5
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_run(entry,d,atr,H,L,C,j0,level):
"""Baseline-Trailing bis Ende; liefert (R_final, j_touch|None) — Touch = erstes
Erreichen des Levels, solange der Trade offen ist."""
sl=entry-d*_SL_ATR*atr; tp=entry+d*_TP_INIT*atr
hw=entry; rank=0; jt=None
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, jt
if jt is None and level is not None and ((hi>=level) if d>0 else (lo<=level)):
jt=j
if (hi>=tp) if d>0 else (lo<=tp): return (tp-entry)*d/atr, jt
hw=max(hw,hi) if d>0 else min(hw,lo)
profit=(hw-entry)*d
ph=0 if profit<_TRAILON*atr else (1 if profit<_LOCK_START*atr else 2)
if ph<rank: ph=rank
rank=ph
if ph==1:
cand=hw-d*_MULT*atr
if profit>=_BE*atr:
cand=max(cand,entry) if d>0 else min(cand,entry)
sl=max(sl,cand) if d>0 else min(sl,cand)
elif ph==2:
tm=max(_LOCK_MIN,_MULT*_LOCK_SCALE)
cand=hw-d*tm*atr
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, jt
def is_break(level,d,atr,H,L,jt):
up=level+d*_BRK_ATR*atr; dn=level-d*_BRK_ATR*atr
for j in range(jt, min(jt+_BRK_W, len(H))):
if (H[j]>=up) if d>0 else (L[j]<=up): return True
if (L[j]<=dn) if d>0 else (H[j]>=dn): return False
return False
def main():
n=int(sys.argv[1]) if len(sys.argv)>1 else 80000
mt5.initialize(); sym=None
for c in ("SpotCrude","USOIL","WTI","XTIUSD"):
if mt5.symbol_info(c): sym=c; break
si=mt5.symbol_info(sym); point=si.point
bars=None
for req in (n,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]; V=[float(b["tick_volume"]) for b in bars]
SP=[float(b["spread"])*point 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; TH=_REVERSAL_STRETCH
print("="*96)
print(f" P(Level-Durchbruch) & Close-vs-Run am Touch — {sym} M5 ({len(C)} Bars, Echtkosten)")
print("="*96)
for lbl,a,b in (("H1 (alt)",_N_BARS,mid),("H2 (neu)",mid,len(C))):
ev=[] # (break?, dR = R_run R_close, feature-dict)
for i in range(max(a,_N_BARS,_LOOKBACK), min(b,len(C)-_MAXH-1)):
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 not d: continue
entry=C[i]
# nächstes gegenüberliegendes Pivot-Level
phis,plos=[],[]
for j in range(i-_LOOKBACK+_PIV_K, i-_PIV_K):
if H[j]==max(H[j-_PIV_K:j+_PIV_K+1]): phis.append(H[j])
if L[j]==min(L[j-_PIV_K:j+_PIV_K+1]): plos.append(L[j])
if d>0:
cands=[p for p in phis if p>entry+0.3*atr]
level=min(cands) if cands else None
near=phis
else:
cands=[p for p in plos if p<entry-0.3*atr]
level=max(cands) if cands else None
near=plos
if level is None: continue
R_run,jt=sim_run(entry,d,atr,H,L,C,i+1,level)
if jt is None: continue # Level nie erreicht
R_close=(level-entry)*d/atr
brk=is_break(level,d,atr,H,L,jt)
mom=(C[jt]-C[max(0,jt-6)])*d/atr
touches=sum(1 for p in near if abs(p-level)<=0.15*atr)
volr=V[jt]/max(1.0,(sum(V[max(0,jt-20):jt])/max(1,len(V[max(0,jt-20):jt]))))
withtrend=1 if (EF[jt]-ES[jt])*d>0 else 0
ev.append((brk, R_run-R_close,
{"mom":mom,"touch":touches,"vol":volr,"wt":withtrend}))
n_ev=len(ev); pb=100*sum(1 for e in ev if e[0])/max(1,n_ev)
dall=sum(e[1] for e in ev)/max(1,n_ev)
print(f"\n{lbl}: {n_ev} Level-Touches · Basisrate P(break)={pb:.0f}% · Ø(R_runR_close)={dall:+.3f}")
segs=[("mit Trend",lambda f:f["wt"]==1),("gegen Trend",lambda f:f["wt"]==0),
("Momentum ≥0,3",lambda f:f["mom"]>=0.3),("Momentum <0,3",lambda f:f["mom"]<0.3),
("Level ≥2 Touches",lambda f:f["touch"]>=2),("Level 1 Touch",lambda f:f["touch"]<2),
("Volumen-Spike ≥1,5×",lambda f:f["vol"]>=1.5),("Volumen normal",lambda f:f["vol"]<1.5)]
print(f" {'Segment':<20}{'n':>7}{'P(break)':>10}{'Ø(runclose)':>14}")
for name,fn in segs:
sub=[e for e in ev if fn(e[2])]
if not sub: continue
p=100*sum(1 for e in sub if e[0])/len(sub)
dd=sum(e[1] for e in sub)/len(sub)
mark=" ← CLOSE besser" if dd<0 else ""
print(f" {name:<20}{len(sub):>7}{p:>9.0f}%{dd:>+14.3f}{mark}")
print("\n Die Regel trägt nur, wenn ein Segment in BEIDEN Hälften Ø(runclose)<0 zeigt")
print(" (= dort wäre Close besser) UND P(break) dort klar unter der Basisrate liegt.")
if __name__=="__main__":
main()