Files
AH-Oil-Trader/backtest_flipclose.py
T
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

132 lines
6.0 KiB
Python
Raw 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
"""Auto-Flip-Close (User-Taktik-Kern, 2026-07-16): Position im PLUS automatisch
schließen, sobald die Empfehlung auf die GEGENRICHTUNG dreht (heute nur Alarm).
Sequentielle Trade-Sim (EINE Position wie live): Entry bei Signal, Exit via
SL 2,0×ATR + Trailing 1,5 + BE 1,3 (Basis) — Varianten zusätzlich mit Flip-Close
(nur im Plus; optional Mindestgewinn in ×ATR ~ der '3%-Margin'-Idee; Referenz:
Flip auch im Minus). Signale aus der echten `_build`-Logik (M30-Filter +
H1-Konfluenz + Winkel, hour=None = gate-frei). Kosten = Bar-Spread/ATR.
2 Halbjahre. R = Profit/ATR.
Entscheidung: Auto-Flip-Close nur bauen, wenn ØR & ΣR in BEIDEN Hälften ≥ Basis.
"""
import sys, bisect
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.wave_rec import (WaveRecommender, _atr, _ema_last, _ema_series,
_EMA_FAST, _EMA_SLOW, _N_BARS, _HTF_DEADBAND, _ANGLE_LR)
_MAXH = 288; _ATRMIN = 0.12
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 st(Rs):
if not Rs: return None
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)
return dict(n=n, wr=100*w/n, oR=s/n, pf=(up/dn if dn > 0 else 99.9),
sum=s, worst=min(Rs))
def line(lbl, s):
if not s: return f" {lbl:<34} —"
return (f" {lbl:<34} n={s['n']:>4} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+6.0f} Worst={s['worst']:+.1f}")
def run_seq(sig, H, L, C, A, SP, lo, hi, flip=False, min_r=0.0, flip_neg=False):
"""Sequentielle Sim: eine Position, Entry bei Signal, Exit SL/Trail/(Flip)."""
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
Rs = []
i = lo
while i < hi:
d = sig[i]
if d == 0:
i += 1; continue
atr = max(A[i] or 0, _ATRMIN)
entry = C[i]
eff = entry - d*2.0*atr; hw = entry
end = min(i+_MAXH, len(C)-1); exit_px = C[end]; exit_j = end
for j in range(i+1, end+1):
hi_, lo_ = H[j], L[j]
if (lo_ <= eff) if d > 0 else (hi_ >= eff):
exit_px = eff; exit_j = j; break
hw = max(hw, hi_) if d > 0 else min(hw, lo_)
prof = (C[j]-entry)*d
# Flip-Close: Signal dreht auf Gegenrichtung → Close zum Bar-Close
if flip and sig[j] == -d and (flip_neg or prof > min_r*atr):
exit_px = C[j]; exit_j = j; break
if prof >= 0.3*atr:
cand = hw - d*1.5*atr
if prof >= 1.3*atr:
cand = max(cand, entry) if d > 0 else min(cand, entry)
eff = max(eff, cand) if d > 0 else min(eff, cand)
Rs.append((exit_px-entry)*d/atr - cost(i, atr))
i = exit_j + 1
return 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)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n+_N_BARS+_MAXH+5)
m30 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, n//6+500)
h1 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_H1, 0, n//12+500)
si = mt5.symbol_info(sym); point = si.point
mt5.shutdown()
T = [int(b["time"]) for b in bars]; H = [float(b["high"]) for b in bars]
L = [float(b["low"]) for b in bars]; C = [float(b["close"]) for b in bars]
SP = [float(b["spread"])*point for b in bars]
A = _atr_series(H, L, C)
def series(rr):
t = [int(b["time"]) for b in rr]; c = [float(b["close"]) for b in rr]
hh = [float(b["high"]) for b in rr]; ll = [float(b["low"]) for b in rr]
return t, _ema_series(c, _EMA_FAST), _ema_series(c, _EMA_SLOW), _atr_series(hh, ll, c)
mT, mEf, mEs, mA = series(m30)
hT, hEf, hEs, hA = series(h1)
def tf_sign(tt, ef, es, aa, ts):
k = bisect.bisect_right(tt, ts)-1
if k < _EMA_SLOW or aa[k] is None or aa[k] <= 0: return 0
dd = ef[k]-es[k]
return 0 if abs(dd) < _HTF_DEADBAND*aa[k] else (1 if dd > 0 else -1)
w = WaveRecommender(type("T", (), {"snapshot": lambda s: {"intervals": {}}})(), mt5.TIMEFRAME_M5)
sig = [0]*len(C)
for i in range(_N_BARS, len(C)-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)
a5 = calc_trend_angle(C[i-_ANGLE_LR-2:i], _ANGLE_LR)
rec, _ = w._build(ef, es, C[i-1], atr, "M5", 0,
htf_trend=tf_sign(mT, mEf, mEs, mA, T[i]),
h1_trend=tf_sign(hT, hEf, hEs, hA, T[i]), angle=a5)
s_ = rec["signal"]
sig[i] = 1 if s_ == "LONG" else -1 if s_ == "SHORT" else 0
mid = len(C)//2
print("="*92)
print(f" Auto-Flip-Close — {sym} M5 (sequentielle 1-Positions-Sim · Exit live · Echtkosten)")
print(f" Flip = Signal dreht auf Gegenrichtung → Close zum Bar-Close (heute nur Alarm)")
print("="*92)
for lbl, lo, hi_ in (("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C)-_MAXH-1)):
print(f"\n{lbl}:")
print(line("BASIS (nur SL/Trailing)", st(run_seq(sig, H, L, C, A, SP, lo, hi_))))
print(line("+ Flip-Close im Plus (minR=0)", st(run_seq(sig, H, L, C, A, SP, lo, hi_, flip=True))))
print(line("+ Flip-Close ab +0.3xATR", st(run_seq(sig, H, L, C, A, SP, lo, hi_, flip=True, min_r=0.3))))
print(line("+ Flip-Close ab +0.5xATR", st(run_seq(sig, H, L, C, A, SP, lo, hi_, flip=True, min_r=0.5))))
print(line("+ Flip-Close IMMER (auch Minus)", st(run_seq(sig, H, L, C, A, SP, lo, hi_, flip=True, flip_neg=True))))
print(f"\n Bauen nur, wenn eine Flip-Variante in BEIDEN Hälften ØR & ΣR ≥ BASIS hält.")
if __name__ == "__main__":
main()