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

114 lines
5.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
"""Verifiziert Gate-Option A ('nur 12+16 blocken' statt '11-14') auf ZWEI unabhängigen
Zeiträumen (History-Hälften). Prüft: (1) sind Std 12 & 16 in BEIDEN Hälften negativ,
Std 13 nicht? (Sign-Stabilität = kein Overfitting). (2) schlägt Gate A das aktuelle
Gate (11-14) und Kein-Gate im Gesamt-Netto-R — in BEIDEN Hälften?
"""
import sys, datetime as dt
from zoneinfo import ZoneInfo
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=240; _TRAILON=0.3; _TPTRAIL=0.5; _ATRMIN=0.12; _SL_ATR=2.0; _COST=0.10
_BROKER_OFF=3*3600; _BERLIN=ZoneInfo("Europe/Berlin")
GATE_CUR={11,12,13,14}; GATE_A={12,16}
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 simulate(entry,d,atr,sl,H,L,C,j0):
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]
if (lo<=eff) if d>0 else (hi>=eff): return (eff-entry)*d/atr
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
def bhour(raw):
return dt.datetime.fromtimestamp(int(raw)-_BROKER_OFF, tz=dt.timezone.utc).astimezone(_BERLIN).hour
def collect(H,L,C,T,EF,ES,AT,lo,hi):
"""→ Liste (berlin_hour, R) für Bars in [lo,hi)."""
TH=_REVERSAL_STRETCH; out=[]
for i in range(max(lo,_N_BARS), min(hi,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
out.append((bhour(T[i]), simulate(C[i],d,atr,C[i]-d*_SL_ATR*atr,H,L,C,i+1)))
return out
def policy(ev, blocked):
kept=[r for h,r in ev if h not in blocked]
net=sum(r-_COST for r in kept)
return len(kept), net, (net/len(kept) if kept else 0)
def main():
n=int(sys.argv[1]) if len(sys.argv)>1 else 120000
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)>1000: break
mt5.shutdown()
if bars is None: print("Keine Bars von MT5."); return
print(f" {len(bars)} M5-Bars geladen.")
H=[float(b["high"]) for b in bars]; L=[float(b["low"]) for b in bars]
C=[float(b["close"]) for b in bars]; T=[int(b["time"]) 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
def span(a,b): return f"{dt.datetime.utcfromtimestamp(T[a]-_BROKER_OFF):%d.%m.%y}{dt.datetime.utcfromtimestamp(T[b-1]-_BROKER_OFF):%d.%m.%y}"
halves=[("H1 (alt)",_N_BARS,mid),("H2 (neu)",mid,len(C))]
print("="*82)
print(f" Gate-A-Verifikation auf 2 Zeiträumen — {sym} M5 (netto = Ø-R {_COST})")
print("="*82)
EV={}
for lbl,a,b in halves:
EV[lbl]=collect(H,L,C,T,EF,ES,AT,a,b)
print(f" {lbl}: {span(a,b)} ({len(EV[lbl])} Signale)")
# (1) Sign-Stabilität der Schlüsselstunden
print("\n(1) Netto-R je Stunde — sind 12 & 16 in BEIDEN negativ, 13 nicht?")
print(f" {'Std':>3} " + "".join(f"{lbl:>16}" for lbl,_,_ in halves))
for h in (11,12,13,14,15,16,17):
cells=""
for lbl,_,_ in halves:
v=[r for hh,r in EV[lbl] if hh==h]
cells+=f"{(sum(v)/len(v)-_COST):>+10.3f}(n{len(v)})" if v else f"{'-':>16}"
flag=""
if h in GATE_A: flag=" ← blocken(A)"
if h==13: flag=" ← behalten?"
print(f" {h:>3} {cells}{flag}")
# (2) Politik-Vergleich je Hälfte
print("\n(2) Gesamt-Netto-R je Gate-Politik (höher = besser), je Hälfte:")
print(f" {'Politik':<16}" + "".join(f"{lbl:>20}" for lbl,_,_ in halves))
for name,blk in (("Kein Gate",set()),("Aktuell 11-14",GATE_CUR),("A: 12+16",GATE_A)):
cells=""
for lbl,_,_ in halves:
k,net,avg=policy(EV[lbl],blk)
cells+=f" ΣR{net:>+7.0f} (n{k}{avg:+.3f})"
print(f" {name:<16}{cells}")
print("\n Gate A validiert NUR, wenn es in BEIDEN Hälften ΣR > 'Aktuell 11-14' liefert")
print(" UND 12+16 in beiden negativ, 13 in beiden nicht-negativ sind.")
if __name__=="__main__":
main()