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:
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SL-ATR-Timeframe-Mismatch bei Squeeze-Trades (2026-07-23, User-Frage nach
|
||||
−89,36-€-Nacht-Trade): Der VALIDIERTE Squeeze-Backtest (`backtest_breakout_squeeze.py`)
|
||||
sizt den SL auf **M5-ATR** (2,0×) — dieselbe TF wie das Signal. LIVE sizt `trader.
|
||||
_calc_sl_tp` den Initial-SL aber IMMER auf **M15-ATR** (`SL_TF=M15`, Band 1,8–2,2×),
|
||||
unabhängig vom Signal-TF. Bei einem Squeeze (M5-Signal) kann das stark divergieren —
|
||||
real letzte Nacht: M5-ATR fiel von 0,29→0,10 (Vola-Kompression, die den Squeeze
|
||||
überhaupt erst auslöste!), während M15-ATR bei ~0,36 blieb → SL 0,787 statt ~0,20-0,22
|
||||
bei M5-Sizing = ~3,5× zu weit für GENAU dieses Setup.
|
||||
|
||||
Test: Squeeze-Entries (Box/Ausbruch wie `backtest_breakout_squeeze.py`), SL/Trailing/
|
||||
BE-Distanz aus ZWEI Quellen vergleichen:
|
||||
LIVE = SL 2,2×ATR(M15) zum Entry-Zeitpunkt (min 1,8×, wie `_calc_sl_tp`), Trailing
|
||||
bleibt M5-basiert (wie live, TF folgt dem Signal).
|
||||
MODEL = SL 2,2×ATR(M5) — dieselbe TF wie das Signal (= was der Original-Backtest
|
||||
validiert hat).
|
||||
2 Halbjahre, Echtkosten. Verdict: nur wechseln, wenn MODEL in BEIDEN Hälften ΣR/PF
|
||||
schlägt.
|
||||
"""
|
||||
import sys
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
_MAXH = 288; _ATRMIN = 0.12
|
||||
_N = 12; _W = 24; _COOL = 12; _K = 0.1
|
||||
_SL_MIN = 1.8; _SL_MAX = 2.2
|
||||
|
||||
|
||||
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])))
|
||||
out = [None]
|
||||
for i in range(1, len(C)):
|
||||
seg = t[max(1, i-p+1):i+1]
|
||||
out.append(sum(seg)/len(seg))
|
||||
return out
|
||||
|
||||
|
||||
def sim(entry, d, sl_dist, atr_trail, H, L, C, j0, trail=1.5, trail_on=0.3, be_on=1.3):
|
||||
"""sl_dist = absolute Preisdistanz (schon TF-spezifisch berechnet). Trailing/BE
|
||||
laufen wie live auf atr_trail (M5, das Signal-TF) — nur der INITIALE SL variiert."""
|
||||
eff = entry - d*sl_dist; hw = entry
|
||||
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): exit_px = eff; break
|
||||
hw = max(hw, hi) if d > 0 else min(hw, lo)
|
||||
prof = (C[j]-entry)*d
|
||||
if prof >= trail_on*atr_trail:
|
||||
cand = hw - d*trail*atr_trail
|
||||
if prof >= be_on*atr_trail: cand = max(cand, entry) if d > 0 else min(cand, entry)
|
||||
eff = max(eff, cand) if d > 0 else min(eff, cand)
|
||||
return (exit_px-entry)*d/atr_trail
|
||||
|
||||
|
||||
def rep(name, Rs):
|
||||
if not Rs: print(f" {name:<28} —"); return
|
||||
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
|
||||
up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
|
||||
pf = up/dn if dn > 0 else 9.99
|
||||
print(f" {name:<28} n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={pf:.2f} ΣR={s:+.0f}")
|
||||
|
||||
|
||||
def run(H, L, C, A5, A15_at, SP, lo_i, hi_i, squeeze_mult):
|
||||
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
|
||||
live_Rs, model_Rs = [], []
|
||||
i = max(lo_i, _N+15)
|
||||
while i < min(hi_i, len(C)-_MAXH-1):
|
||||
atr5 = A5[i]
|
||||
if not atr5 or atr5 < _ATRMIN: i += 1; continue
|
||||
boxHi = max(H[i-_N:i]); boxLo = min(L[i-_N:i]); box = boxHi-boxLo
|
||||
if box > squeeze_mult*atr5: i += 1; continue
|
||||
hit = None
|
||||
for j in range(i, min(i+_W, len(C)-_MAXH-1)):
|
||||
up = boxHi + _K*atr5; dn = boxLo - _K*atr5
|
||||
if H[j] >= up: hit = (j, 1, up); break
|
||||
if L[j] <= dn: hit = (j, -1, dn); break
|
||||
if hit is None: i += 1; continue
|
||||
j, d, lvl = hit
|
||||
atr5_j = A5[j] or atr5
|
||||
atr15_j = A15_at(j)
|
||||
c = cost(j, atr5_j)
|
||||
# LIVE: SL aus M15-ATR (wie trader._calc_sl_tp), Band [1.8,2.2], Trailing auf M5
|
||||
if atr15_j:
|
||||
sl_live = min(_SL_MAX, max(_SL_MIN, _SL_MAX)) * atr15_j # live nutzt fix 2.2 (Cap)
|
||||
sl_live = _SL_MAX * atr15_j
|
||||
live_Rs.append(sim(lvl, d, sl_live, atr5_j, H, L, C, j+1) - c)
|
||||
# MODEL: SL aus M5-ATR (Signal-TF, wie der validierte Original-Backtest)
|
||||
sl_model = _SL_MAX * atr5_j
|
||||
model_Rs.append(sim(lvl, d, sl_model, atr5_j, H, L, C, j+1) - c)
|
||||
i = j + _COOL
|
||||
return live_Rs, model_Rs
|
||||
|
||||
|
||||
def main():
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||
mt5.initialize()
|
||||
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None)
|
||||
m5 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n+_MAXH+30)
|
||||
m15 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M15, 0, (n+_MAXH+30)//3+50)
|
||||
si = mt5.symbol_info(sym); point = si.point
|
||||
mt5.shutdown()
|
||||
H = [float(b["high"]) for b in m5]; L = [float(b["low"]) for b in m5]
|
||||
C = [float(b["close"]) for b in m5]; SP = [float(b["spread"])*point for b in m5]
|
||||
T = [int(b["time"]) for b in m5]
|
||||
A5 = _atr_series(H, L, C)
|
||||
|
||||
H15 = [float(b["high"]) for b in m15]; L15 = [float(b["low"]) for b in m15]
|
||||
C15 = [float(b["close"]) for b in m15]; T15 = [int(b["time"]) for b in m15]
|
||||
A15 = _atr_series(H15, L15, C15)
|
||||
# Für jeden M5-Index den ZULETZT ABGESCHLOSSENEN M15-ATR nachschlagen (wie live
|
||||
# copy_rates_from_pos "jetzt" die letzten M15-Bars holt) — simple Vorwärts-Suche.
|
||||
import bisect
|
||||
def A15_at(i5):
|
||||
t = T[i5]
|
||||
k = bisect.bisect_right(T15, t) - 1
|
||||
return A15[k] if 0 <= k < len(A15) and A15[k] else None
|
||||
|
||||
N = len(C); mid = N//2
|
||||
print("="*90)
|
||||
print(f" SL-ATR-TF-Mismatch — {sym} M5 Squeeze-Entries ({N} Bars, 2 Halbjahre, Echtkosten)")
|
||||
print(f" LIVE = SL 2,2×ATR(M15, fix) vs MODEL = SL 2,2×ATR(M5, Signal-TF, validierte Basis)")
|
||||
print("="*90)
|
||||
for lbl, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N)):
|
||||
live_Rs, model_Rs = run(H, L, C, A5, A15_at, SP, lo, hi, 2.5)
|
||||
print(f"\n {lbl}:")
|
||||
rep("LIVE (M15-ATR-SL)", live_Rs)
|
||||
rep("MODEL (M5-ATR-SL)", model_Rs)
|
||||
if live_Rs and model_Rs:
|
||||
print(f" Δ ΣR (Model−Live): {sum(model_Rs)-sum(live_Rs):+.0f}")
|
||||
print(f"\n Verdict: MODEL nur übernehmen, wenn es in BEIDEN Hälften ΣR/PF schlägt.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user