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>
154 lines
5.9 KiB
Python
154 lines
5.9 KiB
Python
#!/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 17–32 % 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()
|