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

169 lines
7.5 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
"""Sizing-Simulation: was bringt 96%-All-in vs. Bruchteil-Margin vs. risiko-basiert
für das LANGFRIST-Wachstum (Endkapital) und den Max-Drawdown?
Modell (skalenfrei, fixe Bruchteil-Sizing → geometrisches Wachstum):
- Signale: echte _build-Logik (≥55% Konf, M30-Filter, Winkel) — wie live.
- Exit: SL fix 2,0×ATR + Trailing-TP (= aktuelles Live-Modell), pessimistisch.
- Pro Trade R (=Profit/ATR) → EUR-Rendite je nach Lots/Margin (echte MT5-Params).
- Endkapital = Π(1+ret_i) (bei fixer Bruchteil-Sizing ordnungs-UNABHÄNGIG).
- Max-Drawdown: historische Reihenfolge + Monte-Carlo (Shuffle) für die Verteilung.
Kernfrage: 96%-All-in liegt vermutlich WEIT über dem Kelly-Optimum → höhere
Bruchteil-Sizing gibt mehr Endkapital UND weniger Drawdown (Vola-Drag).
"""
import sys, random, math
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=240; _TRAILON=0.3; _TPTRAIL=0.5; _ATRMIN=0.12
_SL_ATR=2.0 # fixer Initial-SL (Live-Mitte des Bands)
_MARGIN_CAP=0.96 # nie mehr als 96% Margin (auch risiko-basiert gedeckelt)
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 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 curve_stats(rets):
"""Endkapital-Faktor, Wachstum/Trade (mean ln), Max-DD (hist. Reihenfolge)."""
eq=1.0; peak=1.0; mdd=0.0; gsum=0.0; ruin=False
for r in rets:
r=max(r,-0.99)
eq*=(1+r); gsum+=math.log(max(1+r,1e-9))
peak=max(peak,eq); dd=1-eq/peak
if dd>mdd: mdd=dd
if eq<0.10*1.0: ruin=True
return eq, gsum/len(rets), mdd, ruin
def mc_maxdd(rets, runs=400):
"""Monte-Carlo: Verteilung des Max-DD über zufällige Trade-Reihenfolgen."""
dds=[]
base=list(rets)
for _ in range(runs):
random.shuffle(base)
eq=1.0; peak=1.0; mdd=0.0
for r in base:
eq*=(1+max(r,-0.99)); peak=max(peak,eq); mdd=max(mdd,1-eq/peak)
dds.append(mdd)
dds.sort()
p=lambda q: dds[min(len(dds)-1,int(q*len(dds)))]
over80=sum(1 for x in dds if x>0.80)/len(dds)
return p(0.50), p(0.95), over80
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
si=mt5.symbol_info(sym); acc=mt5.account_info()
tick=mt5.symbol_info_tick(sym)
price_now=(tick.bid+tick.ask)/2
margin1=mt5.order_calc_margin(mt5.ORDER_TYPE_BUY, sym, 1.0, price_now)
vpp=(si.trade_tick_value or 1.0)/(si.trade_tick_size or si.point) # € je 1.0 Preis je Lot
lev=vpp*price_now/margin1 if margin1 else 0
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)
trades=[] # (price_i, atr_i, R_i)
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)
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); e=C[i]
R=simulate(e,d,atr,e-d*_SL_ATR*atr,H,L,C,i+1)
trades.append((e,atr,R))
def rets_for(rule, val):
out=[]
for (e,atr,R) in trades:
margin_i=margin1*e/price_now
if rule=="margin":
lots=val/margin_i # je 1 € Equity
else: # risk: val=Risiko-Anteil bei _SL_ATR-Stop
lots=val/(_SL_ATR*atr*vpp)
if lots*margin_i>_MARGIN_CAP: lots=_MARGIN_CAP/margin_i
out.append(lots*(R*atr)*vpp) # frakt. Rendite/Trade
return out
print("="*88)
print(f" Sizing-Simulation — {sym} Trades={len(trades)} SL fix {_SL_ATR}×ATR + Trailing-TP")
print(f" Hebel≈{lev:.0f}× · Margin/Lot≈{margin1:.2f} · Wert/1.0Preis/Lot≈{vpp:.2f}{acc.currency} · Kurs {price_now:.2f}")
print("="*88)
hdr=f" {'Sizing':<22}{'Endkapital':>12}{'Wachstum/Tr':>13}{'MaxDD-hist':>11}{'MaxDD-MC95':>11}{'P(DD>80%)':>11}"
print(hdr); print(" "+"-"*84)
rules=[("Margin 96% (AKTUELL)","margin",0.96),("Margin 50%","margin",0.50),
("Margin 30%","margin",0.30),("Margin 20%","margin",0.20),
("Margin 10%","margin",0.10),
("Risiko 1%/Trade","risk",0.01),("Risiko 2%/Trade","risk",0.02),
("Risiko 3%/Trade","risk",0.03)]
for name,rule,val in rules:
rets=rets_for(rule,val)
eq,g,mdd,ruin=curve_stats(rets)
mdd50,mdd95,over80=mc_maxdd(rets)
eqs=(f"{eq:.2e}×" if (eq>=1e4 or eq<1e-2) else f"{eq:7.2f}×")
print(f" {name:<22}{eqs:>12}{g:>+13.4f}{100*mdd:>10.0f}%{100*mdd95:>10.0f}%{100*over80:>10.0f}%"
+ (" ⚠RUIN" if ruin else ""))
# Kelly-Scan: welcher Margin-Bruchteil maximiert das Wachstum/Trade?
print("\n Kelly-Scan (Margin-Bruchteil → Wachstum pro Trade, ln):")
best=(-9,0)
for f in [x/100 for x in range(2,99,2)]:
g=curve_stats(rets_for("margin",f))[1]
if g>best[0]: best=(g,f)
print(f" Wachstum-Optimum bei Margin ≈ {best[1]*100:.0f}% (g={best[0]:+.4f}/Trade)")
g96=curve_stats(rets_for("margin",0.96))[1]
print(f" Zum Vergleich 96%: g={g96:+.4f}/Trade → "
+ ("96% liegt ÜBER dem Optimum (Vola-Drag)" if g96<best[0] else "96% ist ~optimal"))
print("\n Endkapital = Faktor auf Startkapital über alle Trades (≈"
f"{len(trades)} Trades ~{len(trades)//50} Handelstage). MaxDD-MC95 = 95%-Quantil"
" des Drawdowns über zufällige Reihenfolgen. P(DD>80%) = Quasi-Ruin-Risiko.")
if __name__=="__main__":
main()