Files
AH-Oil-Trader/backtest_flipclose2.py
T
Axel HocksandClaude Opus 5 fea91fddb3 Auto-Flip-Close gebaut (User-Wunsch) - trotz erneut negativer Messung
User: "schliesse den trade automatisch beim close signal". Dreht die
Empfehlung gegen die offene Position und steht sie >=0,5xATR im Plus ->
close(reason="flip_close").

VORHER NEU GEMESSEN, weil das alte Urteil (backtest_flipclose.py,
16.07.) auf einem VIERTEN Exit-Modell fusste: Trail fest 1,5 (live jetzt
1,0), keine Lock-Phase, kein Time-Stop, kein Initial-TP, _MAXH 288 statt
200, Phasen ueber den Bar-Close statt ueber das High-Water.

backtest_flipclose2.py (80k M5, 2 Halbjahre, Echtkosten, Exit aus
core/exit_model.py) bestaetigt das alte Urteil - deutlicher:
    Basis ohne Flip      H1 SigmaR -537 · H2 -140
    Flip ab 0,0xATR      Delta H1 -126 · H2 -221
    Flip ab 0,3xATR      Delta H1  -77 · H2 -216
    Flip ab 0,5xATR      Delta H1  -92 · H2 -160   <- mildeste, Default
    Flip auch im Minus   Delta H1 -205 · H2 -364
JEDE Variante ist in BEIDEN Haelften schlechter. Trefferquote steigt
39 -> 45 %, Ertrag faellt = Gewinner-Kappen (die nachlaufende EMA dreht
oft mitten im Pullback).

Gebaut wurde die mildeste Variante (0,5xATR Mindestgewinn), abschaltbar
ueber [trading] auto_flip_close.

Fuer die Neumessung bekam exit_model.simulate() einen stop_when-Hook
(+ ret_bar), damit die Phasen-Mechanik nicht zum fuenften Mal kopiert
werden musste. Rueckwaertskompatibilitaet verifiziert: ohne Hook bitgenau
identisch (die zunaechst gemeldeten Abweichungen kamen allein aus
LIVE.mult 1,5 -> 1,0, gegengeprueft mit mult=1.5 -> identisch).

Mit 10 synthetischen Szenarien getestet: beide Richtungen, aus, unter
Schwelle, gleichgerichtet, WARTEN, im Minus, flat, Startup-Schonfrist,
Ticket-Dedup - alle korrekt.

B4/B5-Pflicht: Live-Ertrag gegen die Erwartung halten, bei Drift
auto_flip_close=false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:55:06 +02:00

154 lines
5.9 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 NEU gemessen — mit dem ECHTEN Exit (User-Wunsch 2026-07-31).
FRAGE: „Schließe den Trade automatisch beim Close-Signal" — also: dreht die
Empfehlung gegen die offene Position, sofort raus (heute nur Alarm + Ton).
⚠ DAS WURDE SCHON EINMAL VERWORFEN (`backtest_flipclose.py`, 2026-07-16): keine
Variante hielt in beiden Hälften, H2 kostete 1732 % des Ertrags („Gewinner-Kappen",
weil die nachlaufende EMA oft mitten im Pullback dreht).
WARUM TROTZDEM NEU: Beim Stufe-3-Refactor (2026-07-31) kam heraus, dass das alte
Skript ein **viertes, eigenes Exit-Modell** benutzt, das dem Live-System in mindestens
fünf Punkten widerspricht:
Trail hart 1,5 (live seit heute **1,0**)
KEINE Lock-Phase (live: ab 3,5×ATR enger)
KEIN Time-Stop (live: 120 min in Phase Init)
KEIN Initial-TP (live: 3,5×ATR)
_MAXH 288 (live/kanonisch 200)
+ Phasen-Entscheidung über den BAR-CLOSE statt über das High-Water.
Ein Urteil auf dieser Grundlage ist für den Live-Betrieb nicht bindend.
HIER: derselbe sequentielle 1-Positions-Aufbau, aber der Exit kommt aus
`core/exit_model.simulate()` — inklusive des neuen `stop_when`-Hooks für den Flip,
damit die Phasen-Mechanik NICHT noch einmal kopiert wird (genau diese Kopiererei
hat die vier divergierenden Modelle erzeugt). Echtkosten, 2 Halbjahre.
VARIANTEN: Flip erst ab minR Gewinn (0 / 0,3 / 0,5×ATR) sowie „auch im Minus".
Urteil „bauen" nur, wenn eine Variante in BEIDEN Hälften ≥ Basis liegt.
"""
import sys
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
_REVERSAL_STRETCH, _STRETCH_MAX)
_ATRMIN = 0.12
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])))
out = []
for i in range(len(C)):
if not i:
out.append(None); continue
w = t[max(1, i - p + 1):i + 1]
out.append(sum(w) / max(1, len(w)))
return out
def signal_series(H, L, C, EF, ES, AT):
"""Richtung je Bar (+1/1/0) — dieselbe Regel wie im Live-Signal."""
TH = _REVERSAL_STRETCH
sig = [0] * len(C)
for i in range(_N_BARS, len(C)):
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
if stretch <= -TH and ad >= _ANGLE_DEAD: sig[i] = 1
elif stretch >= TH and ad <= -_ANGLE_DEAD: sig[i] = -1
elif abs(stretch) < _STRETCH_MAX: sig[i] = 1 if ef > es else -1 if ef < es else 0
return sig
def run(sig, H, L, C, AT, SP, lo, hi, flip=False, min_r=0.0, flip_neg=False):
"""Sequentiell: eine Position, Entry bei Signal, Exit aus dem GEMEINSAMEN Kern."""
Rs = []
i = lo
while i < hi:
d = sig[i]
if d == 0:
i += 1; continue
atr = max(AT[i] or 0, _ATRMIN)
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
hook = None
if flip:
def hook(j, prof_atr, _d=d):
if sig[j] != -_d:
return False
return flip_neg or prof_atr > min_r
R, xb = simulate(C[i], d, atr, H, L, C, i + 1, LIVE,
stop_when=hook, ret_bar=True)
Rs.append(R - cost)
i = xb + 1
return Rs
def st(Rs):
if not Rs:
return " —"
n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0)
return (f"n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={(g/l if l > 0 else 9.99):>4.2f} "
f"ΣR={s:>+6.0f} Worst={min(Rs):+.2f}")
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 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
si = mt5.symbol_info(sym); point = si.point
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]; SP = [float(b["spread"]) * point for b in bars]
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
sig = signal_series(H, L, C, EF, ES, AT)
mid = len(C) // 2
halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C) - LIVE.max_hold - 1)]
print("=" * 96)
print(f" AUTO-FLIP-CLOSE — {sym} M5 ({len(C)} Bars), Echtkosten")
print(f" Exit aus core/exit_model.py: SL {LIVE.sl_atr} · Trail {LIVE.mult} · "
f"BE {LIVE.be} · Lock {LIVE.lock_start} · Time-Stop {LIVE.timestop_bars}")
print(" (das alte Urteil von 2026-07-16 fusste auf einem abweichenden Exit)")
print("=" * 96)
variants = [("Basis — kein Flip-Close", dict(flip=False)),
("Flip ab 0,0×ATR Gewinn", dict(flip=True, min_r=0.0)),
("Flip ab 0,3×ATR Gewinn", dict(flip=True, min_r=0.3)),
("Flip ab 0,5×ATR Gewinn", dict(flip=True, min_r=0.5)),
("Flip AUCH im Minus", dict(flip=True, flip_neg=True))]
base = {}
for name, kw in variants:
print(f"\n{name}")
for lbl, a, b in halves:
Rs = run(sig, H, L, C, AT, SP, a, b, **kw)
s = sum(Rs)
if not kw.get("flip"):
base[lbl] = s
delta = f" Δ vs Basis {s - base.get(lbl, 0):+.0f}" if kw.get("flip") else ""
print(f" {lbl} {st(Rs)}{delta}")
if __name__ == "__main__":
main()