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

108 lines
4.9 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
"""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()