Stufe 3: geteilter Exit-Kern (core/exit_model.py) + Korrektur der Stufe-1-Begruendung
core/exit_model.py (NEU): LIVE:ExitParams als EINZIGE Quelle der Exit-
Parameter; core/trailing.py leitet _TRAIL_START_ATR, _BREAKEVEN_ATR,
_PHASE4_* und _MULT_BY_TF jetzt davon ab statt eigene Zahlen zu halten.
Aendert jemand LIVE.mult, aendern sich Live-Verhalten UND Messung gemeinsam
-> Deployment-Drift-Fall 3 ist konstruktiv unmoeglich geworden. Dazu die
kanonische simulate() fuer Backtests mit Flags timestop/use_tp, um
Teilmodelle EXPLIZIT zu machen statt zu verstecken.
BEFUNDE BEIM REFACTOR - schlimmer als angenommen:
(1) Es gibt mindestens DREI materiell verschiedene Exit-Modelle:
A) Phasen ~ live (_trailing/_exit/_atrfloor/_candle_fade)
B) EINFACH - _TPTRAIL=0.5, kein Breakeven, kein Lock, kein Time-Stop,
_MAXH=240 (_hourly, _hourly_split, _bounce, _events, _deadhour,
_chopgate)
C) Phasen mit be_on=1.0 statt 1,3 (_breakout, _confluence_angle)
=> Dead-Hours, EIA-Blackout, Bounce, Chop-Gate, Stunden-Analyse und
breakout_k ruhen auf einem Exit, der dem Live-System nicht entspricht.
Bewusst NICHT stillschweigend umgestellt (wuerde historische Schluesse
rueckwirkend aendern); als LEGACY_SIMPLE / LEGACY_BE10 markiert.
(2) Selbst die "Phasen"-Skripte weichen voneinander ab: _trailing hat TP
aber keinen Time-Stop, _candle_fade Time-Stop aber kein TP, _atrfloor
liefert Punkte statt R.
(3) backtest_trailing.py koppelt den Initial-SL an mult (sl = entry -
d*mult*atr) statt fix 2,0.
KORREKTUR DER STUFE-1-BEGRUENDUNG: die "67 % groesserer Einzelverlust" war
ein Artefakt von (3) - dort war der Worst-Case per Konstruktion gleich dem
Multiplikator. Sauber nachgemessen mit fixem SL (backtest_trailmult.py, NEU,
80k Bars, 2 Halbjahre):
Trail 1,0 H1 -73 · H2 +1346 · Worst -2,00
Trail 1,5 H1 -325 · H2 +1294 · Worst -2,00 (live)
Trail 2,0 H1 -446 · H2 +1195 · Worst -2,00
Trail 2,5 H1 -1174 · H2 +1223 · Worst -2,00
Trail 3,0 H1 -1088 · H2 +1397 · Worst -2,00
Der Worst-Case ist bei JEDEM Multiplikator identisch -2,00. Die Entscheidung
bleibt richtig (1,5 schlaegt 2,0 und 2,5 in beiden Haelften), nur die
Tail-Begruendung war falsch.
NEU UND OFFEN: Trail 1,0 schlaegt 1,5 in BEIDEN Haelften - eigener
Vorschlag, bewusst nicht ungefragt umgesetzt.
Aequivalenz verifiziert (500 synthetische Kursreihen je Fall): _candle_fade
und _atrfloor sind bitgenau identisch zur neuen simulate().
Live verifiziert: "AKTIVIERT ATR=0.5796 (M30) mult=1.5x".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
97617cd81d
commit
4c1a68a7e1
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Trailing-Multiplikator SAUBER gemessen — Initial-SL fest bei 2,0×ATR wie live.
|
||||
|
||||
⚠ WARUM DIESES SKRIPT EXISTIERT (2026-07-31, Fund beim Stufe-3-Refactor):
|
||||
`backtest_trailing.py` setzt den Initial-SL auf **`mult`×ATR** statt auf die feste
|
||||
Live-Größe 2,0×ATR:
|
||||
|
||||
sl = entry - d*mult*atr # <-- Initial-SL = mult
|
||||
|
||||
Der dortige Vergleich „mult 1,5 vs 2,5" misst also in Wahrheit
|
||||
*(SL 1,5 · Trail 1,5)* gegen *(SL 2,5 · Trail 2,5)* — zwei Änderungen auf einmal.
|
||||
Deshalb war der Worst-Case dort immer exakt gleich dem Multiplikator (−1,50 / −2,50):
|
||||
das war der **SL**, nicht der Trail. Live ist der SL dagegen fest (Band 1,8–2,2 →
|
||||
2,0, `backtest_exit.py`), nur der Trail hing an der Zeitebene.
|
||||
|
||||
Die auf dieser Grundlage getroffene Stufe-1-Entscheidung (`_MULT_BY_TF` überall auf
|
||||
1,5) muss deshalb neu belegt werden — HIER, mit fixem SL und variablem Trail, also
|
||||
genau der Änderung, um die es live geht.
|
||||
|
||||
Aufbau ansonsten identisch zu `backtest_trailing.py` (gleiches Signal-Set
|
||||
Trend+Reversal, 2 Halbjahre), Exit aus `core/exit_model.py` — der gemeinsamen Quelle,
|
||||
damit dieser Fehler nicht noch einmal passieren kann.
|
||||
"""
|
||||
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 st(Rs):
|
||||
if not Rs:
|
||||
return " -"
|
||||
n = len(Rs); w = sum(1 for r in Rs if r > 0)
|
||||
g = sum(r for r in Rs if r > 0); ls = -sum(r for r in Rs if r < 0)
|
||||
return (f"WR={100*w/n:>3.0f}% Ø-R={sum(Rs)/n:+.3f} PF={(g/ls if ls > 0 else 99):>4.2f} "
|
||||
f"Worst={min(Rs):+.2f} ΣR={sum(Rs):+.0f}")
|
||||
|
||||
|
||||
def signals(H, L, C, ES, EF, AT, lo, hi, step):
|
||||
TH = _REVERSAL_STRETCH; out = []
|
||||
for i in range(max(lo, _N_BARS), min(hi, len(C) - LIVE.max_hold - 1), step):
|
||||
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
|
||||
d = 0
|
||||
if stretch <= -TH and ad >= _ANGLE_DEAD: d = 1
|
||||
elif stretch >= TH and ad <= -_ANGLE_DEAD: d = -1
|
||||
elif abs(stretch) < _STRETCH_MAX: d = 1 if ef > es else -1 if ef < es else 0
|
||||
if d:
|
||||
out.append((i, d, atr))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
|
||||
step = int(sys.argv[2]) if len(sys.argv) > 2 else 2
|
||||
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)
|
||||
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]
|
||||
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
|
||||
mid = len(C) // 2
|
||||
halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))]
|
||||
print("=" * 92)
|
||||
print(f" TRAIL-MULTIPLIKATOR — {sym} M5 ({len(C)} Bars)")
|
||||
print(f" Initial-SL FEST {LIVE.sl_atr}×ATR (wie live) · nur der Trail variiert")
|
||||
print(f" Exit aus core/exit_model.py · Time-Stop {LIVE.timestop_bars} Bars · BE {LIVE.be}")
|
||||
print("=" * 92)
|
||||
SIG = {lbl: signals(H, L, C, ES, EF, AT, a, b, step) for lbl, a, b in halves}
|
||||
for lbl, _, _ in halves:
|
||||
print(f" {lbl}: {len(SIG[lbl])} Signale")
|
||||
for mult in (1.0, 1.5, 2.0, 2.5, 3.0):
|
||||
tag = " ← live (nach Stufe 1)" if abs(mult - 1.5) < 1e-9 else ""
|
||||
print(f"\nTrail {mult:.1f}×ATR{tag}")
|
||||
for lbl, _, _ in halves:
|
||||
p = LIVE.with_(mult=mult)
|
||||
Rs = [simulate(C[i], d, atr, H, L, C, i + 1, p) for (i, d, atr) in SIG[lbl]]
|
||||
print(f" {lbl} {st(Rs)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user