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
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Flatter-Analyse der Durchbruchs-Empfehlung (letzte 2 Tage, M1-Auflösung):
Rekonstruiert P(break) minütlich WIE DIE ENGINE (M5-Serie inkl. LAUFENDER Kerze,
deren Close = aktueller Kurs — genau das lässt mom3/mom6 im Bar springen) und misst
in Level-Nähe (≤0,6×ATR):
- P-Spanne innerhalb einzelner M5-Kerzen (wie stark springt P intra-Bar)
- Schwellen-Kreuzungen (close↔laufen bei 60 %) je Annäherungs-Episode
- Zerlegung: Sprünge durch mom3 (laufende Kerze) vs. Level-Wechsel
"""
import sys, math
import MetaTrader5 as mt5
from core.engine import _p_break
from core.wave_rec import _EMA_FAST, _EMA_SLOW
_PIV_K=3; _LOOKBACK=300; _THR=0.60
def ema_last(vals,p):
k=2.0/(p+1); e=vals[0]
for x in vals[1:]: e=x*k+e*(1-k)
return e
def atr14(H,L,C):
tr=[max(H[i]-L[i],abs(H[i]-C[i-1]),abs(L[i]-C[i-1])) for i in range(1,len(C))]
return sum(tr[-14:])/14
def main():
days=int(sys.argv[1]) if len(sys.argv)>1 else 2
mt5.initialize(); sym='SpotCrude'
m1=mt5.copy_rates_from_pos(sym,mt5.TIMEFRAME_M1,0,days*1440+50)
m5=mt5.copy_rates_from_pos(sym,mt5.TIMEFRAME_M5,0,days*288+_LOOKBACK+60)
mt5.shutdown()
T5=[int(b["time"]) for b in m5]
O5=[float(b["open"]) for b in m5]; H5=[float(b["high"]) for b in m5]
L5=[float(b["low"]) for b in m5]; C5=[float(b["close"]) for b in m5]
T1=[int(b["time"]) for b in m1]; C1=[float(b["close"]) for b in m1]
import bisect
ep=[] # aktive Annäherungs-Episode: Liste von (P, level, mom3)
episodes=[] # abgeschlossene Episoden
per_bar={} # (m5-index) -> [P...] für Intra-Bar-Spanne
n_eval=0
for i1 in range(30, len(m1)):
t=T1[i1]; px=C1[i1]
j=bisect.bisect_right(T5,t)-1 # laufende/letzte M5-Kerze
if j < _LOOKBACK+10: continue
# M5-Serie wie die Engine sie sieht: abgeschlossene Bars + laufende Kerze
# mit Close = aktuellem M1-Kurs
closes=C5[j-_LOOKBACK:j]+[px]
highs =H5[j-_LOOKBACK:j]+[max(px,O5[j])]
lows =L5[j-_LOOKBACK:j]+[min(px,O5[j])]
atr=max(atr14(highs,lows,closes),0.12)
mom6=(closes[-1]-closes[-7])/atr
mom3=(closes[-1]-closes[-4])/atr
ediff=(ema_last(closes,_EMA_FAST)-ema_last(closes,_EMA_SLOW))/atr
# Pivot-Level aus abgeschlossenen Bars
ph=[H5[k] for k in range(j-_LOOKBACK+_PIV_K, j-_PIV_K)
if H5[k]==max(H5[k-_PIV_K:k+_PIV_K+1])]
# LONG-Sicht: nächster Widerstand über Kurs (analog gilt Short symmetrisch)
cands=[p for p in ph if p>px]
level=min(cands) if cands else None
if level is None or (level-px) > 0.6*atr:
if ep: episodes.append(ep); ep=[]
continue
wt=1.0 if ediff>0 else 0.0
dist=abs(level-px)/atr
P=_p_break(mom6,mom3,wt,dist)
ep.append((P,level,mom3)); n_eval+=1
per_bar.setdefault(j,[]).append(P)
if ep: episodes.append(ep)
print("="*84)
print(f" P(break)-Flattern — letzte {days} Tage, {n_eval} Minuten in Level-Nähe (≤0,6×ATR)")
print("="*84)
# Intra-Bar-Spanne
spans=[max(v)-min(v) for v in per_bar.values() if len(v)>=3]
if spans:
spans.sort()
print(f"\nP-Spanne INNERHALB einer M5-Kerze (n={len(spans)} Kerzen):")
print(f" Median {100*spans[len(spans)//2]:.0f} Pp · P90 {100*spans[int(len(spans)*0.9)]:.0f} Pp · Max {100*max(spans):.0f} Pp")
# Episoden: Schwellen-Flips
flips=[]; levsw=[]
for e in episodes:
if len(e)<3: continue
f=sum(1 for k in range(1,len(e)) if (e[k][0]<_THR)!=(e[k-1][0]<_THR))
sw=sum(1 for k in range(1,len(e)) if abs(e[k][1]-e[k-1][1])>1e-9)
flips.append(f); levsw.append(sw)
if flips:
flips.sort()
print(f"\nAnnäherungs-Episoden: {len(flips)} · Ø-Dauer "
f"{sum(len(e) for e in episodes if len(e)>=3)/len(flips):.0f} min")
print(f" close↔laufen-Flips (60%-Schwelle) je Episode: "
f"Median {flips[len(flips)//2]} · P90 {flips[int(len(flips)*0.9)]} · Max {max(flips)}")
print(f" Episoden mit ≥2 Flips: {100*sum(1 for f in flips if f>=2)/len(flips):.0f}% "
f"· Level-Wechsel je Episode Ø {sum(levsw)/len(levsw):.1f}")
# mom3-Beitrag: P-Änderung je Minute vs mom3-Änderung
dP=[]; dM=[]
for e in episodes:
for k in range(1,len(e)):
if abs(e[k][1]-e[k-1][1])<1e-9: # gleiches Level → reiner Feature-Effekt
dP.append(abs(e[k][0]-e[k-1][0])); dM.append(abs(e[k][2]-e[k-1][2]))
if dP:
print(f"\nMinuten-Sprünge bei GLEICHEM Level (n={len(dP)}):")
print(f" Ø |ΔP|/min = {100*sum(dP)/len(dP):.1f} Pp · Ø |Δmom3|/min = {sum(dM)/len(dM):.2f}×ATR")
big=[p for p,m in zip(dP,dM) if m>0.3]
print(f" bei |Δmom3|>0,3: Ø |ΔP| = {100*sum(big)/max(1,len(big)):.1f} Pp "
f"({100*len(big)/len(dP):.0f}% der Minuten) → mom3 (laufende Kerze) ist der Treiber")
if __name__=="__main__":
main()