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
@@ -1968,6 +1968,22 @@ konstant; (d) es gab **keine Telemetrie über die Entscheidung selbst**.
|
||||
**Behebung in 3 Stufen (User-Entscheidung 2026-07-31):**
|
||||
- **Stufe 1 — ERLEDIGT:** `_MULT_BY_TF` auf durchgehend **1,5** und
|
||||
`_MULT_BREAKOUT_ADD` auf **0,0** (`core/trailing.py`) — der einzige gemessene Wert.
|
||||
⚠ **BEGRÜNDUNG NACHTRÄGLICH KORRIGIERT (beim Stufe-3-Refactor):** die zuerst
|
||||
genannte „67 % größerer Einzelverlust" war ein **Artefakt** — `backtest_trailing.py`
|
||||
setzt den Initial-SL auf `mult`×ATR statt fix 2,0, dort war der Worst-Case also
|
||||
per Konstruktion gleich dem Multiplikator. Sauber nachgemessen mit fixem SL
|
||||
(`backtest_trailmult.py`, `core/exit_model.py`, 80k Bars, 2 Halbjahre):
|
||||
| Trail | H1 ΣR | H2 ΣR | Worst |
|
||||
|---|---|---|---|
|
||||
| 1,0 | **−73** | **+1346** | −2,00 |
|
||||
| **1,5 (live)** | −325 | +1294 | −2,00 |
|
||||
| 2,0 | −446 | +1195 | −2,00 |
|
||||
| 2,5 | −1174 | +1223 | −2,00 |
|
||||
| 3,0 | −1088 | +1397 | −2,00 |
|
||||
**Der Worst-Case ist bei JEDEM Multiplikator identisch −2,00** (der SL deckelt).
|
||||
**Die Entscheidung bleibt richtig:** 1,5 schlägt 2,0 UND 2,5 in beiden Hälften.
|
||||
⚠ **NEU und offen:** **Trail 1,0 schlägt 1,5 in BEIDEN Hälften** (−73/+1346 vs
|
||||
−325/+1294) — eigener Vorschlag, bewusst NICHT ungefragt umgesetzt.
|
||||
- **Stufe 2 — ERLEDIGT:** **Entscheidungs-Telemetrie.** (a) Jede Empfehlung loggt
|
||||
jetzt einen maschinenlesbaren **`block_reason`** (`recommendations.block_reason`,
|
||||
DB migriert mit Backup): `deadband · dead_hour · eia · htf_counter · stretch ·
|
||||
@@ -1982,10 +1998,36 @@ konstant; (d) es gab **keine Telemetrie über die Entscheidung selbst**.
|
||||
aber −0,71 im Logit (23 % statt 41 %). Deshalb ist **D0 der schärfere Test** und die
|
||||
D-Schwelle auf 0,5σ gesetzt. Erster Lauf nach dem Fix: D0 zeigt **27,0 % vorhergesagt
|
||||
vs. 28,0 % real** (vorher 23 vs 41) — die Reparatur ist damit live bestätigt.
|
||||
- **Stufe 3 — OFFEN (später, einzeln):** geteilter Kern. Den Exit-Simulator EINMAL in
|
||||
`core/exit_model.py` extrahieren und von `trailing.py` UND allen Backtests nutzen
|
||||
lassen — Fall 3 wäre damit unmöglich gewesen. Gegenprüfung: bestehende Backtests
|
||||
müssen danach dieselben Zahlen liefern.
|
||||
- **Stufe 3 — ERLEDIGT (`core/exit_model.py`):** geteilter Exit-Kern. Enthält
|
||||
**`LIVE: ExitParams`** (sl 2,0 · trail 1,5 · start 0,3 · be 1,3 · lock 3,5/0,6/1,2 ·
|
||||
timestop 24 · tp 3,5 · max_hold 200) als **einzige Quelle** — `core/trailing.py`
|
||||
importiert sie jetzt von dort (`_TRAIL_START_ATR`, `_BREAKEVEN_ATR`, `_PHASE4_*`,
|
||||
`_MULT_BY_TF` sind abgeleitet, keine eigenen Zahlen mehr). Ändert jemand `LIVE.mult`,
|
||||
ändern sich Live-Verhalten UND Messung gemeinsam → **Fall 3 ist konstruktiv
|
||||
unmöglich geworden.** Dazu die kanonische `simulate()` für 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** in den Backtests:
|
||||
**(A)** Phasen-Modell ≈ 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`). ⇒ **Mehrere dokumentierte Befunde (Dead-Hours, EIA-Blackout,
|
||||
Bounce, Chop-Gate, Stunden-Analyse, breakout_k) ruhen auf einem Exit, der dem
|
||||
Live-System NICHT entspricht.** Bewusst NICHT stillschweigend umgestellt — das
|
||||
würde historische Schlüsse rückwirkend ändern; markiert als `LEGACY_SIMPLE` /
|
||||
`LEGACY_BE10` in `exit_model.py`, Neubewertung = eigene Entscheidung.
|
||||
(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**. Live hat BEIDES → nur `timestop=True, use_tp=True` ist live-treu.
|
||||
(3) **`backtest_trailing.py` koppelt den Initial-SL an `mult`** (`sl = entry −
|
||||
d*mult*atr`) — dadurch war die Stufe-1-Begründung teilweise falsch (s. o.).
|
||||
✅ **Äquivalenz verifiziert** (500 synthetische Zufalls-Kursreihen je Fall):
|
||||
`_candle_fade` (`timestop=True, use_tp=False`) und `_atrfloor` (`timestop=False,
|
||||
use_tp=True`, ×ATR) sind **bitgenau identisch** zur neuen `simulate()`.
|
||||
**Migrations-Regel (in `exit_model.py` festgehalten):** ein Backtest darf nur auf
|
||||
`simulate()` umgestellt werden, wenn er danach DIESELBEN Zahlen liefert — sonst ist
|
||||
es keine Refaktorierung, sondern eine unbemerkte Änderung der Messgrundlage.
|
||||
|
||||
⚠ **Erwartung: die drei Fälle sind nicht vollständig.** Sie wurden bei gezielter Suche
|
||||
in ~20 min gefunden; die Trefferquote spricht für weitere. `analyze_divergence.py`
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""core/exit_model.py — EINE Quelle der Wahrheit für den Exit (Stufe 3, 2026-07-31).
|
||||
|
||||
WARUM ES DAS GIBT — Deployment-Drift, Fall 3:
|
||||
Der Trailing-Multiplikator lief live auf 1,5–3,0 (je Zeitebene), während JEDER
|
||||
Backtest 1,5 annahm. Gemessen war das 5,5× schlechteres H1 und ein 67 % größerer
|
||||
Einzelverlust. Niemand hat es bemerkt, weil `trailing.py` (live) und die Backtests
|
||||
zwei getrennte Zahlenwelten waren.
|
||||
|
||||
Dieses Modul ist die gemeinsame Basis:
|
||||
* `LIVE` — die Exit-Parameter als **einzige Quelle**. `core/trailing.py` importiert
|
||||
sie; ändert sie jemand dort, ändern sich die Backtests mit. Genau das hätte
|
||||
Fall 3 unmöglich gemacht.
|
||||
* `simulate()` — die kanonische, bar-basierte Nachbildung des Live-Exits für
|
||||
Backtests (Phasen Init→Trail→Lock, HW-Ratsche, Breakeven-Boden, Time-Stop).
|
||||
|
||||
⚠⚠ BEFUND BEIM BAU (2026-07-31): Die Backtests des Projekts nutzen **mindestens DREI
|
||||
materiell verschiedene Exit-Modelle**:
|
||||
(A) Phasen-Modell ≈ live — `backtest_trailing/_exit/_atrfloor/_candle_fade/…`
|
||||
(B) **Einfach** (`_TPTRAIL=0.5`, KEIN Breakeven, KEIN Lock, KEIN Time-Stop,
|
||||
`_MAXH=240`) — `backtest_hourly`, `_hourly_split`, `_bounce`, `_events`,
|
||||
`_deadhour`, `_chopgate`
|
||||
(C) Phasen mit `be_on=1.0` statt 1,3 — `backtest_breakout`, `_confluence_angle`
|
||||
Damit ruhen mehrere dokumentierte Befunde (Dead-Hours, EIA-Blackout, Bounce,
|
||||
Chop-Gate, Stunden-Analyse) auf einem Exit, der dem Live-System NICHT entspricht.
|
||||
Diese Skripte werden **bewusst NICHT stillschweigend umgestellt** — das würde
|
||||
historische Schlussfolgerungen rückwirkend verändern. Sie sind in `LEGACY_SIMPLE`
|
||||
bzw. `LEGACY_BE10` als Altlast markiert; eine Neubewertung ist eine eigene,
|
||||
begründete Entscheidung.
|
||||
|
||||
MIGRATIONS-REGEL: Ein Backtest darf nur dann auf `simulate()` umgestellt werden,
|
||||
wenn er danach **dieselben Zahlen** liefert wie vorher (Variante A) — sonst ist es
|
||||
keine Refaktorierung, sondern eine unbemerkte Änderung der Messgrundlage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExitParams:
|
||||
"""Alle Größen in ×ATR, Zeiten in Bars der jeweiligen Basis-TF."""
|
||||
sl_atr: float = 2.0 # Initial-SL (Live-Band 1,8–2,2 → Ziel 2,0)
|
||||
trail_start: float = 0.3 # Phase Init→Trail
|
||||
mult: float = 1.5 # Trail-Abstand HW ∓ mult×ATR
|
||||
be: float = 1.3 # Breakeven-Boden (Entry) ab diesem Profit
|
||||
lock_start: float = 3.5 # Phase Trail→Lock
|
||||
lock_scale: float = 0.6 # mult×scale in der Lock-Phase
|
||||
lock_min: float = 1.2 # Untergrenze des Lock-Multiplikators
|
||||
tp_init: float = 3.5 # Initial-TP (Phase Init)
|
||||
timestop_bars: int = 24 # Time-Stop: nach N Bars noch in Phase Init → raus
|
||||
max_hold: int = 200 # harte Obergrenze der Haltedauer
|
||||
|
||||
def with_(self, **kw) -> "ExitParams":
|
||||
return replace(self, **kw)
|
||||
|
||||
|
||||
# ── DIE Live-Parameter. `core/trailing.py` importiert sie von hier. ───────────
|
||||
# Werte = der jeweils GEMESSENE Stand:
|
||||
# sl 2,0 `backtest_exit.py` (Band 1,8–2,2, Ø-R +32 % ggü. 1,2–1,5)
|
||||
# trail 1,5 `backtest_trailing.py` (2,0/2,5 messbar schlechter, s. Stufe 1)
|
||||
# start 0,3 `backtest_trailing.py` (0,6/1,0 schlechter)
|
||||
# be 1,3 `backtest_exit.py be` (0,6 → 21 % Breakeven-Scratches)
|
||||
# timestop 24 `backtest_timestop.py` (120 min; 30/60 min kippten)
|
||||
LIVE = ExitParams()
|
||||
|
||||
# Altlasten — NUR zur Dokumentation, nicht zum Verwenden in neuem Code.
|
||||
# (B) Der einfache Exit ohne Breakeven/Lock/Time-Stop, 240 Bars Haltedauer.
|
||||
LEGACY_SIMPLE = ("backtest_hourly.py", "backtest_hourly_split.py",
|
||||
"backtest_bounce.py", "backtest_events.py",
|
||||
"backtest_deadhour.py", "backtest_chopgate.py")
|
||||
# (C) Phasen-Modell, aber Breakeven bei 1,0 statt 1,3.
|
||||
LEGACY_BE10 = ("backtest_breakout.py", "backtest_confluence_angle.py")
|
||||
|
||||
|
||||
def simulate(entry: float, d: int, atr: float,
|
||||
H: list, L: list, C: list, j0: int,
|
||||
p: ExitParams = LIVE,
|
||||
timestop: bool = True,
|
||||
use_tp: bool = True) -> float:
|
||||
"""Kanonische Exit-Simulation → Ergebnis in R (×ATR), Kosten NICHT enthalten.
|
||||
|
||||
`d` = +1 LONG / −1 SHORT · `j0` = erster Bar NACH dem Einstieg.
|
||||
Pessimistisch: wird in EINEM Bar sowohl SL als auch TP berührt, zählt der SL.
|
||||
Die Phasen-Ratsche (`rank`) verhindert das Zurückfallen in eine frühere Phase —
|
||||
identisch zur Live-Mechanik in `core/trailing.py`.
|
||||
|
||||
⚠ `timestop` / `use_tp` machen die **Teilmodelle explizit**. Beim Bau (2026-07-31)
|
||||
zeigte sich, dass selbst die „Phasen-Modell"-Backtests untereinander abweichen:
|
||||
`backtest_trailing.py` TP ja · Time-Stop NEIN
|
||||
`backtest_candle_fade.py` TP NEIN · Time-Stop ja
|
||||
`backtest_atrfloor.py` TP ja · Time-Stop NEIN · liefert PUNKTE statt R
|
||||
Live hat BEIDES (`_TP_INIT_ATR=3.5`, `_TIMESTOP_MIN=120`) — die Vollausstattung
|
||||
(`timestop=True, use_tp=True`) ist also die einzige live-treue Variante. Die
|
||||
Flags erlauben es, ein bestehendes Skript BITGENAU zu migrieren und dabei
|
||||
sichtbar zu machen, welchen Ausschnitt es misst, statt ihn zu verstecken.
|
||||
"""
|
||||
if atr <= 0:
|
||||
return 0.0
|
||||
sl = entry - d * p.sl_atr * atr
|
||||
tp = (entry + d * p.tp_init * atr) if use_tp else None
|
||||
hw = entry
|
||||
rank = 0
|
||||
end = min(j0 + p.max_hold, len(C) - 1)
|
||||
for j in range(j0, end + 1):
|
||||
hi, lo = H[j], L[j]
|
||||
if (lo <= sl) if d > 0 else (hi >= sl):
|
||||
return (sl - entry) * d / atr
|
||||
if tp is not None and ((hi >= tp) if d > 0 else (lo <= tp)):
|
||||
return (tp - entry) * d / atr
|
||||
hw = max(hw, hi) if d > 0 else min(hw, lo)
|
||||
profit = (hw - entry) * d
|
||||
ph = 0 if profit < p.trail_start * atr else (
|
||||
1 if profit < p.lock_start * atr else 2)
|
||||
if ph < rank:
|
||||
ph = rank
|
||||
rank = ph
|
||||
if timestop and rank == 0 and (j - j0) >= p.timestop_bars:
|
||||
return (C[j] - entry) * d / atr
|
||||
if ph == 0:
|
||||
continue
|
||||
m = p.mult if ph == 1 else max(p.lock_min, p.mult * p.lock_scale)
|
||||
cand = hw - d * m * atr
|
||||
if profit >= p.be * atr or ph == 2:
|
||||
cand = max(cand, entry) if d > 0 else min(cand, entry)
|
||||
sl = max(sl, cand) if d > 0 else min(sl, cand)
|
||||
return (C[end] - entry) * d / atr
|
||||
+16
-12
@@ -56,14 +56,21 @@ _ATR_MIN = 0.06 # Untergrenze ATR. 0,12→0,06 gesenkt (gemessen,
|
||||
# breit). 0,06 in BEIDEN Hälften besser (H1 −153→−27 Pts,
|
||||
# H2 +512→+554); ganz ohne Floor nur marginal besser →
|
||||
# 0,06 als Schutz gegen Absurd-ATR (Dead-Hours) behalten.
|
||||
_TRAIL_START_ATR = 0.3 # Phase 1→2: ab diesem Profit startet HW-Trailing
|
||||
_BREAKEVEN_ATR = 1.3 # Entry-Floor (Breakeven) ab diesem Profit. 0,6 war zu
|
||||
# ── Exit-Parameter: EINE Quelle für Live UND Backtests (Stufe 3, 2026-07-31) ──
|
||||
# Diese Werte kamen bis dahin hier UND in ~20 Backtests getrennt vor. Genau daraus
|
||||
# entstand Deployment-Drift-Fall 3: `_MULT_BY_TF` lief live auf bis zu 3,0, während
|
||||
# jeder Backtest 1,5 annahm. Jetzt liegen sie in `core/exit_model.py`; ändert sie
|
||||
# jemand dort, ändern sich Live-Verhalten UND Messung gemeinsam.
|
||||
from core.exit_model import LIVE as _EXIT
|
||||
|
||||
_TRAIL_START_ATR = _EXIT.trail_start # 0,3 — Phase 1→2: ab hier HW-Trailing
|
||||
_BREAKEVEN_ATR = _EXIT.be # 1,3 — Entry-Floor (Breakeven). 0,6 war zu
|
||||
# eng: 21 % der Trades wurden auf Breakeven gescratcht
|
||||
# (backtest_exit.py be). 1,3 = gemessenes Plateau-Optimum:
|
||||
# Scratch 5 %, Ø-R +15 %, ohne Tail-Risiko (SL-gedeckelt).
|
||||
_PHASE4_ATR = 3.5 # Phase 2→3: ab hier engeres Trailing zum Lock-In
|
||||
_PHASE4_MULT_SCALE = 0.6 # Multiplikator-Faktor in Phase 3
|
||||
_PHASE4_MULT_MIN = 1.2 # Untergrenze Mult in Phase 3
|
||||
_PHASE4_ATR = _EXIT.lock_start # 3,5 — Phase 2→3: engeres Trailing (Lock-In)
|
||||
_PHASE4_MULT_SCALE = _EXIT.lock_scale # 0,6 — Multiplikator-Faktor in Phase 3
|
||||
_PHASE4_MULT_MIN = _EXIT.lock_min # 1,2 — Untergrenze Mult in Phase 3
|
||||
|
||||
_PARTIAL_TP_ATR = 1.5 # Teil-Exit: ab diesem Profit 1× die Hälfte sichern
|
||||
_PARTIAL_TP_FRAC = 0.0 # Anteil beim Teil-Exit. 0 = AUS → kompletter Trade
|
||||
@@ -122,13 +129,10 @@ _TF_LABELS = {
|
||||
# häufig auf M15/M30 landet (31.07.: 19 von 33 Phasen).
|
||||
# ⚠ Falls die Staffelung je zurück soll: erst `backtest_trailing.py` auf
|
||||
# M15-/M30-BASIS-Signalen laufen lassen — bisher misst er nur M5.
|
||||
_MULT_BY_TF = {
|
||||
mt5.TIMEFRAME_M1: 1.5,
|
||||
mt5.TIMEFRAME_M5: 1.5,
|
||||
mt5.TIMEFRAME_M15: 1.5,
|
||||
mt5.TIMEFRAME_M30: 1.5,
|
||||
mt5.TIMEFRAME_H1: 1.5,
|
||||
}
|
||||
# ⚠ Werte kommen aus `core/exit_model.py` (Stufe 3) — NICHT hier hart eintragen.
|
||||
_MULT_BY_TF = {tf: _EXIT.mult for tf in (
|
||||
mt5.TIMEFRAME_M1, mt5.TIMEFRAME_M5, mt5.TIMEFRAME_M15,
|
||||
mt5.TIMEFRAME_M30, mt5.TIMEFRAME_H1)}
|
||||
# 0,0 statt 0,5: der Aufschlag hob Breakout-Trades auf mind. 2,0 — gemessen
|
||||
# schlechter als 1,5 (s. o.) und für Breakouts nie separat validiert.
|
||||
_MULT_BREAKOUT_ADD = 0.0
|
||||
|
||||
Reference in New Issue
Block a user