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

135 lines
6.6 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
"""S/R-Wellen-Analyse (Track B):
TEIL 1 Containment: Welcher %-Anteil der Wellen (EMA-Richtungsmoves) bleibt
INNERHALB der S/R-Linien — d. h. erreicht das gegenüberliegende Level
und PRALLT AB, statt durchzubrechen? (Basisrate P(break) am Touch.)
TEIL 2 P(Durchbruch)-Modell: logistische Regression auf beobachtbaren Merkmalen
am Touch (Anlauf-Momentum 6/3 Bars, EMA-mit-Trend, Level-Distanz), auf H1
trainiert und auf H2 KALIBRIERT geprüft (predicted vs. echte Bruchrate je
Bin) — ein P ist nur brauchbar, wenn es out-of-sample kalibriert ist.
Break = nach Touch +0,5×ATR JENSEITS des Levels binnen 12 Bars, bevor 0,5×ATR zurück.
"""
import sys
import numpy as np
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=200; _ATRMIN=0.12; _PIV_K=3; _LOOKBACK=300; _BRK_W=12; _BRK_ATR=0.5
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 collect(a,b,H,L,C,EF,ES,AT):
"""Sammelt Wellen-Touches → Liste (break?, feat-vector, reached?)."""
TH=_REVERSAL_STRETCH; out=[]; n_wave=0; n_reach=0
for i in range(max(a,_N_BARS,_LOOKBACK), min(b,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
n_wave+=1
entry=C[i]
# nächstes gegenüberliegendes Pivot-Level
phis,plos=[],[]
for j in range(i-_LOOKBACK+_PIV_K, i-_PIV_K):
if H[j]==max(H[j-_PIV_K:j+_PIV_K+1]): phis.append(H[j])
if L[j]==min(L[j-_PIV_K:j+_PIV_K+1]): plos.append(L[j])
if d>0:
cands=[p for p in phis if p>entry+0.3*atr]; level=min(cands) if cands else None
else:
cands=[p for p in plos if p<entry-0.3*atr]; level=max(cands) if cands else None
if level is None: continue
# Touch suchen (bis MAXH), solange die Welle "lebt" (kein 2×ATR-Gegenlauf)
jt=None
for j in range(i+1, min(i+_MAXH, len(C)-_BRK_W)):
if ((H[j]>=level) if d>0 else (L[j]<=level)): jt=j; break
if ((entry-L[j]) if d>0 else (H[j]-entry))*1 >= 2.0*atr: break # Welle tot
if jt is None: continue
n_reach+=1
up=level+d*_BRK_ATR*atr; dn=level-d*_BRK_ATR*atr; brk=False
for j in range(jt, min(jt+_BRK_W, len(H))):
if (H[j]>=up) if d>0 else (L[j]<=up): brk=True; break
if (L[j]<=dn) if d>0 else (H[j]>=dn): brk=False; break
mom6=(C[jt]-C[max(0,jt-6)])*d/atr
mom3=(C[jt]-C[max(0,jt-3)])*d/atr
wt=1.0 if (EF[jt]-ES[jt])*d>0 else 0.0
distlvl=abs(level-entry)/atr
out.append((1.0 if brk else 0.0, [mom6,mom3,wt,distlvl]))
return out, n_wave, n_reach
def fit_logreg(X,y,iters=3000,lr=0.3):
n,m=X.shape; Xb=np.hstack([np.ones((n,1)),X]); w=np.zeros(m+1)
for _ in range(iters):
p=1/(1+np.exp(-(Xb@w))); w-=lr*(Xb.T@(p-y))/n
return w
def predict(w,X):
return 1/(1+np.exp(-(np.hstack([np.ones((X.shape[0],1)),X])@w)))
def main():
n=int(sys.argv[1]) if len(sys.argv)>1 else 80000
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,80000,60000,40000):
bars=mt5.copy_rates_from_pos(sym,mt5.TIMEFRAME_M5,0,req)
if bars is not None and len(bars)>2000: break
mt5.shutdown()
H=[float(b["high"]) for b in bars]; L=[float(b["low"]) for b in bars]; C=[float(b["close"]) 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
ev1,w1,r1=collect(_N_BARS,mid,H,L,C,EF,ES,AT)
ev2,w2,r2=collect(mid,len(C),H,L,C,EF,ES,AT)
print("="*90)
print(f" S/R-WELLEN — {sym} M5 ({len(C)} Bars) Level=Pivot(k{_PIV_K}), Break=+{_BRK_ATR}×ATR in {_BRK_W} Bars")
print("="*90)
print("\nTEIL 1 — Containment (bleibt die Welle innerhalb der S/R-Linien?):")
for lbl,ev,nw,nr in (("H1 (alt)",ev1,w1,r1),("H2 (neu)",ev2,w2,r2)):
if not ev: continue
pbreak=100*sum(e[0] for e in ev)/len(ev)
print(f" {lbl}: {nw} Wellen · {100*nr/max(1,nw):.0f}% erreichen das Level · "
f"davon {pbreak:.0f}% DURCHBRUCH → {100-pbreak:.0f}% bleiben drin (Abpraller)")
print("\nTEIL 2 — P(Durchbruch)-Modell (Merkmale: mom6, mom3, mit-Trend, Level-Dist):")
X1=np.array([e[1] for e in ev1]); y1=np.array([e[0] for e in ev1])
X2=np.array([e[1] for e in ev2]); y2=np.array([e[0] for e in ev2])
mu=X1.mean(0); sd=X1.std(0)+1e-9
w=fit_logreg((X1-mu)/sd,y1)
p2=predict(w,(X2-mu)/sd)
# AUC (schnell, via Rangvergleich)
order=np.argsort(p2); ranks=np.empty_like(order,dtype=float); ranks[order]=np.arange(1,len(p2)+1)
npos=y2.sum(); nneg=len(y2)-npos
auc=(ranks[y2==1].sum()-npos*(npos+1)/2)/(npos*nneg) if npos and nneg else float('nan')
print(f" Trainiert auf H1, geprüft auf H2 · AUC={auc:.3f} (0,5=Zufall; >0,6=brauchbar trennscharf)")
print(f" Koeffizienten (standardisiert): mom6={w[1]:+.2f} mom3={w[2]:+.2f} "
f"mitTrend={w[3]:+.2f} dist={w[4]:+.2f} · Basis(intercept)={w[0]:+.2f}")
print("\n Kalibrierung auf H2 (vorhergesagtes P vs. echte Bruchrate):")
print(f" {'P-Bin':<12}{'n':>7}{'Ø-Vorhersage':>14}{'echt-Bruch':>12}")
edges=[0,0.25,0.30,0.35,0.40,0.45,0.55,1.01]
for k in range(len(edges)-1):
m=(p2>=edges[k])&(p2<edges[k+1])
if m.sum()<30: continue
print(f" {edges[k]:.2f}{edges[k+1]:.2f} {int(m.sum()):>7}{p2[m].mean():>13.0%}{y2[m].mean():>12.0%}")
# Nutz-Regel: Trenn-Schwelle
for thr in (0.40,0.45,0.50):
run=p2>=thr
print(f"\n Regel „laufen lassen wenn P(break)≥{thr:.0%}\": {100*run.mean():.0f}% der Touches laufen, "
f"davon {100*y2[run].mean():.0f}% brechen wirklich durch · "
f"Rest schließen: {100*y2[~run].mean():.0f}% brechen (Fehl-Close)")
if __name__=="__main__":
main()