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>
This commit is contained in:
Axel Hocks
2026-07-24 08:29:23 +02:00
co-authored by Claude Opus 4.8
commit 75d28827e8
104 changed files with 21059 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Misst vier Verbesserungs-Ideen in einem Lauf (Live-Konfig: M5 + M30-Filter):
A) Multi-TF-Konfluenz: Edge wenn nur M30 vs M30+H1 einig
B) Exit nach Qualitaet: MAE/MFE (max Gegen-/Mitlauf in ATR) je Pullback-Bucket
C) Tageszeit-Edge: Edge je Berliner Stunde
D) Vola-Gate: Edge je ATR-Band
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone, timedelta
import MetaTrader5 as mt5
from core.wave_rec import (WaveRecommender, _atr, _ema_last,
_EMA_FAST, _EMA_SLOW, _N_BARS, _HTF_DEADBAND)
_TF = {"M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5, "M15": mt5.TIMEFRAME_M15,
"M30": mt5.TIMEFRAME_M30, "H1": mt5.TIMEFRAME_H1}
_BERLIN = timezone(timedelta(hours=2)) # Juni = CEST (broker-epoch ist UTC+3 → -3+2)
class _NeutralTU:
def snapshot(self): return {"intervals": {}}
def _ema_series(vals, period):
k = 2.0/(period+1); out=[]; e=vals[0]
for i,v in enumerate(vals):
e = v if i==0 else v*k + e*(1-k); out.append(e)
return out
def _atr_series(H,L,C,period=14):
trs=[0.0]
for i in range(1,len(C)):
trs.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)):
w=trs[max(1,i-period+1):i+1]; out.append(sum(w)/len(w) if w else None)
return out
def _rep(name, rets):
if not rets: print(f" {name:<20} -"); return
n=len(rets); win=sum(1 for x in rets if x>0)
print(f" {name:<20} n={n:>4} Treffer={100*win/n:>3.0f}% Oe-Edge={sum(rets)/n:+.4f}")
def _sign_at(ts, T, Ef, Es, ATR, dead):
lo,hi,idx=0,len(T)-1,-1
while lo<=hi:
m=(lo+hi)//2
if T[m]<=ts: idx=m; lo=m+1
else: hi=m-1
if idx<_EMA_SLOW or ATR[idx] is None or ATR[idx]<=0: return 0
d=Ef[idx]-Es[idx]
return 0 if abs(d)<dead*ATR[idx] else (1 if d>0 else -1)
def main():
n_bars = int(sys.argv[1]) if len(sys.argv)>1 else 8000
K = 10
if not mt5.initialize(): print("init", mt5.last_error()); sys.exit(1)
sym=None
for c in ("SpotCrude","USOIL","WTI","XTIUSD"):
if mt5.symbol_info(c): sym=c; break
sym=sym or "SpotCrude"
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n_bars+_N_BARS+K+5)
m30b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, n_bars//6+400)
h1b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_H1, 0, n_bars//12+400)
mt5.shutdown()
if bars is None or m30b is None or h1b is None: print("Bars fehlen"); sys.exit(1)
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]
def prep(bb):
t=[int(x["time"]) for x in bb]; h=[float(x["high"]) for x in bb]
l=[float(x["low"]) for x in bb]; c=[float(x["close"]) for x in bb]
return t, _ema_series(c,_EMA_FAST), _ema_series(c,_EMA_SLOW), _atr_series(h,l,c)
m30 = prep(m30b); h1 = prep(h1b)
w=WaveRecommender(_NeutralTU(), mt5.TIMEFRAME_M5)
confl={"nur M30":[], "M30+H1 einig":[]}
mae={"near<0":[], "0-0.3":[], "0.3-1":[], "1-2":[], "2+":[]}
mfe={k:[] for k in mae}
by_hour={}; atr_vals=[]; rows=[]
for i in range(_N_BARS, len(C)-K):
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)
m=_sign_at(T[i], *m30, _HTF_DEADBAND)
rec,_=w._build(ef,es,C[i-1],atr,"M5",5,htf_trend=m)
if rec["signal"]=="WARTEN": continue
d=1 if rec["signal"]=="LONG" else -1
fwd=C[i+K]-C[i]; r=fwd*d
rows.append((r,atr,T[i]))
# A) Konfluenz
h=_sign_at(T[i], *h1, _HTF_DEADBAND)
(confl["M30+H1 einig"] if h==d else confl["nur M30"]).append(r)
# B) MAE/MFE in ATR
seg_h=H[i+1:i+K+1]; seg_l=L[i+1:i+K+1]
if d>0: adv=(min(seg_l)-C[i])/atr; fav=(max(seg_h)-C[i])/atr
else: adv=(C[i]-max(seg_h))/atr; fav=(C[i]-min(seg_l))/atr
near=(C[i-1]-es)/atr*d
b = "near<0" if near<0 else "0-0.3" if near<0.3 else "0.3-1" if near<1 else "1-2" if near<2 else "2+"
mae[b].append(adv); mfe[b].append(fav)
# C) Stunde
hr=datetime.fromtimestamp(T[i]-10800, timezone.utc).astimezone(_BERLIN).hour
by_hour.setdefault(hr,[]).append(r)
atr_vals.append((atr,r))
print("="*60); print(f" {sym} M5+M30 n_bars={len(C)} Vorlauf={K}"); print("="*60)
print("\nA) Multi-TF-Konfluenz:")
_rep("nur M30", confl["nur M30"]); _rep("M30+H1 einig", confl["M30+H1 einig"])
print("\nB) Exit: MAE/MFE (in ATR) je Pullback-Bucket [MAE=Gegenlauf, MFE=Mitlauf]:")
for k in ["near<0","0-0.3","0.3-1","1-2","2+"]:
a=mae[k]; f=mfe[k]
if a: print(f" {k:<8} n={len(a):>4} Oe-MAE={sum(a)/len(a):+.2f} Oe-MFE={sum(f)/len(f):+.2f} MFE/|MAE|={ (sum(f)/len(f))/abs(sum(a)/len(a)+1e-9):.2f}")
print("\nC) Tageszeit-Edge (Berliner Stunde):")
for hr in sorted(by_hour):
_rep(f"{hr:02d}:00", by_hour[hr])
print("\nD) Vola-Gate (ATR-Terzile):")
atr_vals.sort()
n=len(atr_vals); t1=atr_vals[:n//3]; t2=atr_vals[n//3:2*n//3]; t3=atr_vals[2*n//3:]
for nm,seg in [("niedrig ATR",t1),("mittel ATR",t2),("hoch ATR",t3)]:
rr=[r for _,r in seg]; lo=seg[0][0]; hi=seg[-1][0]
_rep(f"{nm} ({lo:.2f}-{hi:.2f})", rr)
if __name__ == "__main__":
main()