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
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Exit-Simulation: bringt ein WEITERER Initial-SL netto mehr? (Befund B)
Replays die echten Signale (M5 + M30-Filter) und simuliert den tatsaechlichen
Exit-Ablauf bar-fuer-bar, originalgetreu zu core/trailing.py:
- Initial-SL = X×ATR (die getestete Variable)
- Teil-Exit 50 % bei +1,5×ATR (einmalig)
- Phasen: Init (<0,3×ATR halte Initial-SL) · Trail (SL = HW∓mult×ATR,
Breakeven-Floor ab +0,6×ATR, mult=1,5 fuer M5) · Lock (>=3,5×ATR enger)
- Phasen-Ratsche (nie zurueck)
Pessimistische Intrabar-Annahme: Gegenlauf VOR Mitlauf (zaehlt SL zuerst) —
ueberschaetzt den Nutzen eines weiten SL also NICHT.
PnL in R (= ATR-Vielfache, vergleichbar ueber Trades). Der weite Init-SL wirkt
nur in der Init-Phase: sobald Trailing greift, kappt HW∓1,5×ATR ihn ohnehin.
Hinweis: der feste Init-TP (+3,5×ATR) wird weggelassen — der Runner-Exit laeuft
praktisch ueber den Trailing-SL; das ist die konservative, dominante Mechanik.
"""
from __future__ import annotations
import sys
import MetaTrader5 as mt5
from core.wave_rec import (WaveRecommender, _atr, _ema_last,
_EMA_FAST, _EMA_SLOW, _N_BARS, _HTF_DEADBAND)
_MULT = 1.5 # _MULT_BY_TF[M5]
_BE_ATR = 0.6 # _BREAKEVEN_ATR
_TRAIL_ON = 0.3 # _TRAIL_START_ATR
_LOCK_ATR = 3.5 # _PHASE4_ATR
_LOCK_MULT = max(1.2, _MULT * 0.6)
_PART_ATR = 1.5 # _PARTIAL_TP_ATR
_PART_FRAC = 0.0 # Teil-Exit AUS (entspricht Live: _PARTIAL_TP_FRAC=0)
_ATR_MIN = 0.12
_MAXH = 240 # max. Haltedauer in M5-Bars (~20 h)
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 _htf_sign_at(ts, T, Ef, Es, ATR):
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)<_HTF_DEADBAND*ATR[idx] else (1 if d>0 else -1)
def simulate(entry, d, atr, X, H, L, C, j0, be=_BE_ATR):
"""Ein Trade. Gibt (R_total, stopped_in_init) zurueck. d=+1 long/-1 short.
be = Breakeven-Schwelle in xATR (ab welchem Profit der SL auf Entry rueckt)."""
mult = _MULT
sl = entry - d * X * atr
hw = entry
size = 1.0
realized = 0.0 # in Preis-Einheiten
partial = False
phase_rank = 0 # 0 Init, 1 Trail, 2 Lock
init_stop = 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]
# 1) Gegenlauf zuerst → SL-Treffer?
hit = (lo <= sl) if d > 0 else (hi >= sl)
if hit:
exit_px = sl
if phase_rank == 0:
init_stop = True
break
# 2) HW mit Mitlauf
hw = max(hw, hi) if d > 0 else min(hw, lo)
# 3) Teil-Exit 50 % bei +1,5×ATR (Mitlauf-Extrem)
fav = ((hi if d > 0 else lo) - entry) * d
if not partial and fav >= _PART_ATR * atr:
lvl = entry + d * _PART_ATR * atr
realized += _PART_FRAC * (lvl - entry) * d
size -= _PART_FRAC
partial = True
# 4) Phase aus Close-Profit + Ratsche
prof = (C[j] - entry) * d
rank = 0 if prof < _TRAIL_ON * atr else (1 if prof < _LOCK_ATR * atr else 2)
phase_rank = max(phase_rank, rank)
# 5) Trailing-SL nachziehen
if phase_rank == 1:
cand = hw - d * mult * atr
cand = (max(cand, entry - mult * atr) if d > 0
else min(cand, entry + mult * atr))
if prof >= 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 phase_rank == 2:
cand = hw - d * _LOCK_MULT * atr
cand = max(cand, entry) if d > 0 else min(cand, entry)
sl = max(sl, cand) if d > 0 else min(sl, cand)
R = (realized + size * (exit_px - entry) * d) / atr
return R, init_stop
def main():
args = sys.argv[1:]
mode = "be" if (args and args[0] == "be") else "width"
if mode == "be": args = args[1:]
n_bars = int(args[0]) if args else 8000
widths = [1.2, 1.5, 1.8, 2.0, 2.5, 3.0, 4.0]
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+_MAXH+5)
m30b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, n_bars//6+400)
mt5.shutdown()
if bars is None or m30b is None: print("Bars fehlen"); sys.exit(1)
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]
mT=[int(b["time"]) for b in m30b]; mc=[float(b["close"]) for b in m30b]
mh=[float(b["high"]) for b in m30b]; ml=[float(b["low"]) for b in m30b]
mEf=_ema_series(mc,_EMA_FAST); mEs=_ema_series(mc,_EMA_SLOW); mATR=_atr_series(mh,ml,mc)
w=WaveRecommender(_NeutralTU(), mt5.TIMEFRAME_M5)
# Signale einmal sammeln
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)
m=_htf_sign_at(T[i], mT,mEf,mEs,mATR)
rec,_=w._build(ef,es,C[i-1],atr,"M5",5,htf_trend=m)
s=rec["signal"]
if s=="WARTEN": continue
sigs.append((i, 1 if s=="LONG" else -1, max(atr,_ATR_MIN)))
def metrics(Rs):
n=len(Rs); win=sum(1 for r in Rs if r>0)
gains=sum(r for r in Rs if r>0); losses=-sum(r for r in Rs if r<0)
losers=[r for r in Rs if r<0]
pf=gains/losses if losses>0 else float('inf')
avg_loss=sum(losers)/len(losers) if losers else 0.0
return n,win,pf,avg_loss
if mode == "be":
X = 2.0
bes = [0.6, 0.8, 1.0, 1.3, 1.5, 99.0]
print("="*66)
print(f" Breakeven-Test — {sym} M5+M30 SL={X}xATR Signale={len(sigs)}")
print("="*66)
print(f" {'Breakeven':<11}{'Treffer':>8}{'Oe-R':>8}{'Summe-R':>9}"
f"{'PF':>6}{'Oe-Verl.':>9}{'Scratch%':>9}")
for be in bes:
Rs=[]; scratch=0
for (i,d,atr) in sigs:
R,_=simulate(C[i], d, atr, X, H, L, C, i+1, be=be)
Rs.append(R)
if -0.15 < R < 0.05: scratch+=1 # ~Breakeven gescratcht
n,win,pf,avg_loss=metrics(Rs)
lbl = "aus (nie)" if be>10 else f"{be:.1f}"
print(f" {lbl:<11}{100*win/n:>7.0f}%{sum(Rs)/n:>8.3f}{sum(Rs):>9.1f}"
f"{pf:>6.2f}{avg_loss:>9.2f}{100*scratch/n:>8.0f}%")
print("\n Breakeven = ab wieviel xATR Profit der SL auf Entry rückt (aus=nie)")
print(" Scratch% = Anteil ~Breakeven-Ausgänge (R zw. 0,15 und +0,05)")
return
print("="*70)
print(f" Exit-Simulation — {sym} M5+M30 Signale={len(sigs)} Halt<= {_MAXH} Bars")
print(" (Teil-Exit AUS · Breakeven@0.6 · Trail HW∓1.5ATR · pessimistisch)")
print("="*70)
print(f" {'Init-SL':<9}{'Treffer':>8}{'Oe-R':>8}{'Summe-R':>9}{'PF':>6}"
f"{'Oe-Verl.':>9}{'Worst-R':>9}{'Init-Stop':>10}")
for X in widths:
Rs=[]; init_stops=0
for (i,d,atr) in sigs:
R, istop = simulate(C[i], d, atr, X, H, L, C, i+1)
Rs.append(R)
if istop: init_stops+=1
n,win,pf,avg_loss=metrics(Rs)
print(f" {X:<9.1f}{100*win/n:>7.0f}%{sum(Rs)/n:>8.3f}{sum(Rs):>9.1f}"
f"{pf:>6.2f}{avg_loss:>9.2f}{min(Rs):>9.2f}{100*init_stops/n:>9.0f}%")
print("\n Oe-R = Ø/Trade in ATR-Vielfachen · PF = Profit-Faktor")
print(" Oe-Verl. = Ø verlierender Trade · Worst-R = größter Einzelverlust (Tail)")
if __name__ == "__main__":
main()