Exit-Abstand aus M15-ATR gemessen = faellt durch, Exit bleibt am M5-ATR
Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den Exit groeber fuehren.
Schliesst zugleich den seit 31.07. in core/trailing.py offenen Punkt
("bisher misst er nur M5").
M5-ATR (heute) H1 +0,219 / PF 1,41 / Worst -2,17
H2 +0,117 / PF 1,25 / Worst -2,23
M15-ATR H1 +0,096 / PF 1,14 / Worst -4,26
H2 +0,019 / PF 1,02 / Worst -6,73
Schlechter in BEIDEN Haelften und das Tail-Risiko verdreifacht sich. Dasselbe
Muster wie bei backtest_sl_basis.py.
Zwei Methoden-Fallen, die die Messung fast entwertet haetten:
1) simulate() normiert R mit DEMSELBEN ATR, den es fuer SL/Trail/TP nutzt. Mit
dem 1,86x groesseren M15-ATR waere dieselbe Dollar-Bewegung ein kleineres R -
die M15-Variante haette mechanisch schlechter ausgesehen. Beide werden
deshalb in M5-Risikoeinheiten zurueckgerechnet.
2) Der erste Lauf sammelte die Entry-Liste ueber exit_fn - das wird von scan()
bei JEDEM Box-Ausbruch gerufen, nicht nur bei den komprimierten: 519 statt
132 Einstiege, also die Kontrollgruppe statt des Squeeze. Behoben ueber den
additiven Rueckruf on_entry(j, d, lvl, atr, komprimiert) in
core/squeeze_scan.py; Bitgleichheit weiter durch tests/test_squeeze_scan.py
belegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b5eff2a5d6
commit
010007f7a3
@@ -4942,6 +4942,33 @@ Modell schliesst keine Trades.
|
||||
„die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die
|
||||
Basisraten-/Kalibrierungslücke.
|
||||
|
||||
## ⚠⚠ EXIT-ABSTAND AUS M15-ATR = FÄLLT DURCH (2026-08-08)
|
||||
|
||||
Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den EXIT gröber führen.
|
||||
Gemessen (`backtest_exit_atr_tf.py`, 80k M5, feste geteilte Entry-Liste,
|
||||
kanonischer Exit, Echtkosten). Schliesst zugleich den seit dem 31.07. in
|
||||
`core/trailing.py` offenen Punkt („bisher misst er nur M5").
|
||||
| | H1 ØR / PF / Worst | H2 ØR / PF / Worst |
|
||||
|---|---|---|
|
||||
| **M5-ATR (heute)** | **+0,219 / 1,41 / −2,17** | **+0,117 / 1,25 / −2,23** |
|
||||
| M15-ATR (1,86× breiter) | +0,096 / 1,14 / **−4,26** | +0,019 / 1,02 / **−6,73** |
|
||||
**Schlechter in BEIDEN Hälften — und das Tail-Risiko verdreifacht sich.**
|
||||
Dasselbe Muster wie bei `backtest_sl_basis.py`: ein breiterer Exit kauft nichts
|
||||
und kostet Tail. **Der Exit bleibt am M5-ATR.**
|
||||
|
||||
⚠⚠ **METHODEN-FALLE, die die Messung fast entwertet hätte** — zweimal:
|
||||
**(1)** `simulate()` normiert R mit DEMSELBEN ATR, den es für SL/Trail/TP nutzt.
|
||||
Mit dem 1,86× grösseren M15-ATR wäre dieselbe Dollar-Bewegung ein kleineres R —
|
||||
die M15-Variante hätte mechanisch schlechter ausgesehen. Beide werden deshalb in
|
||||
**M5-Risikoeinheiten** zurückgerechnet (`R × ATR_verwendet / ATR_M5`).
|
||||
**(2)** Der erste Lauf sammelte die Entry-Liste über `exit_fn` — das wird von
|
||||
`scan()` bei **JEDEM** Box-Ausbruch gerufen, nicht nur bei den komprimierten:
|
||||
**519 statt 132 Einstiege**, also die Kontrollgruppe statt des Squeeze. Behoben
|
||||
über den additiven Rückruf **`on_entry(j, d, lvl, atr, komprimiert)`** in
|
||||
`core/squeeze_scan.py` (Bitgleichheit weiter durch `tests/test_squeeze_scan.py`
|
||||
belegt). ⚠ **Ein Haken, der mehr liefert als gedacht, ist eine stille
|
||||
Populations-Änderung.**
|
||||
|
||||
## ⚠⚠ SQUEEZE AUF M15 = FÄLLT DURCH — kein M15-Modul gebaut (2026-08-08)
|
||||
|
||||
Anlass: User „baue mir das M15-Empfehlungsmodul". Der Squeeze ist der einzige
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Squeeze-Einstieg auf M5, EXIT-Abstände aus dem M15-ATR — trägt das?
|
||||
|
||||
ANLASS (2026-08-08). Der M15-EINSTIEG ist gemessen durchgefallen
|
||||
(`backtest_squeeze_m15.py`: n=33 in H1, Nachbarn kippen). Offen bleibt die
|
||||
andere Hälfte der Idee: den validierten M5-Einstieg behalten und nur den EXIT
|
||||
gröber führen. Dafür spricht die Kostenseite (Median Spread/ATR: M15 **0,104**
|
||||
gegen M5 0,193) und der dokumentierte Befund, dass der Verlust-Hebel im Exit und
|
||||
im Sizing liegt, nicht im Signal.
|
||||
|
||||
⚠ IM CODE STEHT DIESE FRAGE SEIT DEM 31.07. OFFEN — `core/trailing.py`:
|
||||
„Falls die Staffelung je zurück soll: erst `backtest_trailing.py` auf
|
||||
M15-/M30-BASIS-Signalen laufen lassen — **bisher misst er nur M5**."
|
||||
Live nimmt das Trailing den ATR der WELLEN-Zeitebene als Abstandsmaß; die
|
||||
Heuristik landet häufig auf M15/M30 (31.07.: 19 von 33 Phasen). Welcher ATR das
|
||||
sein SOLLTE, ist nie gemessen worden.
|
||||
|
||||
⚠⚠ DIE FALLE, DIE DIESE MESSUNG SONST WERTLOS MACHT: `simulate()` normiert das
|
||||
Ergebnis mit DEMSELBEN ATR, den es für SL/Trail/TP benutzt (R = Gewinn/ATR). Mit
|
||||
dem ~1,8× größeren M15-ATR würde dieselbe Dollar-Bewegung zu einem kleineren R —
|
||||
die Varianten wären **nicht vergleichbar**, und die M15-Variante sähe
|
||||
mechanisch schlechter aus. Beide werden deshalb in **M5-Risikoeinheiten**
|
||||
zurückgerechnet: R_m5 = R_ergebnis × ATR_verwendet / ATR_M5.
|
||||
|
||||
⚠ NICHT dasselbe wie `backtest_sl_basis.py` (05.08.): dort wurde nur der
|
||||
INITIAL-SL variiert (Ergebnis: Wash beim Squeeze, Tail-Risiko ×3, daraufhin
|
||||
`_calc_sl_tp(atr_tf=M5)` für Squeeze-Trades gebaut). Hier hängt der GESAMTE Exit
|
||||
am ATR — SL, Trail-Abstand, Breakeven, Lock-Schwellen und TP.
|
||||
|
||||
AUFBAU: feste, geteilte Entry-Liste (Squeeze auf M5, dokumentierte Parameter
|
||||
Box 10 / k 0,1 / Floor 0,12), Touch-Fill, Echtkosten, kanonischer Exit-Kern,
|
||||
2 Halbjahre. M15-ATR kausal aus den M5-Bars aggregiert.
|
||||
|
||||
⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: die M15-Variante muss in BEIDEN Hälften mehr ΣR
|
||||
(in M5-Einheiten) liefern UND darf den Worst-Case nicht verschlechtern. Ein
|
||||
breiterer Exit kauft Ertrag sonst mit Tail-Risiko — genau die Abwägung, die bei
|
||||
`backtest_sl_basis.py` gegen die Umstellung sprach.
|
||||
|
||||
Aufruf: python backtest_exit_atr_tf.py [n_M5_bars] (Default 80000)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
from core.exit_model import LIVE, simulate
|
||||
from core.squeeze_scan import atr_series, scan
|
||||
|
||||
_FLOOR = 0.12 # dokumentierter M5-Floor (backtest_squeeze_touchfill.py)
|
||||
_M15 = 3 # 3 M5-Bars = 1 M15-Bar
|
||||
|
||||
|
||||
def atr_m15_auf_m5(H, L, C):
|
||||
"""ATR aus M15-Bars, auf den M5-Index abgebildet.
|
||||
|
||||
⚠ LOOK-AHEAD-frei: an M5-Index i zählt nur der letzte ABGESCHLOSSENE
|
||||
M15-Bar. Dieselbe Konstruktion wie `_htf_signs` in `backtest_auto_signal`."""
|
||||
hh, hl, hc = [], [], []
|
||||
for s in range(0, len(C) - _M15 + 1, _M15):
|
||||
hh.append(max(H[s:s + _M15])); hl.append(min(L[s:s + _M15]))
|
||||
hc.append(C[s + _M15 - 1])
|
||||
a15 = atr_series(hh, hl, hc)
|
||||
out = [None] * len(C)
|
||||
for i in range(len(C)):
|
||||
j = i // _M15 - 1
|
||||
out[i] = a15[j] if 0 <= j < len(a15) else None
|
||||
return out
|
||||
|
||||
|
||||
def kz(Rs, label):
|
||||
if not Rs:
|
||||
return f" {label:<26} n=0"
|
||||
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); v = -sum(x for x in Rs if x < 0)
|
||||
return (f" {label:<26} n={n:>4} Treffer={100*w/n:>3.0f}% ØR={s/n:+.3f} "
|
||||
f"PF={(g/v if v > 0 else 9.99):>4.2f} Σ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 = 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)
|
||||
point = mt5.symbol_info(sym).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]
|
||||
A5 = atr_series(H, L, C)
|
||||
A15 = atr_m15_auf_m5(H, L, C)
|
||||
mid = len(C) // 2
|
||||
|
||||
gueltig = [(a15 / a5) for a5, a15 in zip(A5, A15) if a5 and a15 and a5 > 0]
|
||||
gueltig.sort()
|
||||
print("=" * 98)
|
||||
print(f" EXIT-ABSTAND aus M5-ATR gegen M15-ATR — {sym}, {len(C)} M5-Bars")
|
||||
print(f" Verhältnis ATR_M15/ATR_M5: Median {gueltig[len(gueltig)//2]:.2f}×")
|
||||
print(" ⚠ Beide Varianten in M5-RISIKOEINHEITEN gerechnet, sonst unvergleichbar")
|
||||
print("=" * 98)
|
||||
|
||||
for nm, lo, hi in (("H1", 40, mid), ("H2", mid, len(C) - LIVE.max_hold - 2)):
|
||||
# ── EINE feste Entry-Liste (der Einstieg ist in beiden Varianten gleich)
|
||||
eintraege = []
|
||||
|
||||
# ⚠ NUR komprimierte Ausbrueche = die Squeeze-Population. `exit_fn`
|
||||
# allein taugt dafuer NICHT: es wird bei JEDEM Box-Ausbruch gerufen und
|
||||
# lieferte still die Kontrollgruppe (519 statt 132 Einstiege).
|
||||
def sammeln(j, d, lvl, atr, komprimiert, _l=eintraege):
|
||||
if komprimiert:
|
||||
_l.append((lvl, d, j + 1))
|
||||
|
||||
scan(H, L, C, A5, SP, 1.0, lo, hi, 2.5, box_n=10, k=0.1,
|
||||
atr_min=_FLOOR, max_hold=LIVE.max_hold, on_entry=sammeln)
|
||||
|
||||
print(f"\n ── {nm} ── {len(eintraege)} feste Squeeze-Einstiege, "
|
||||
f"beide Varianten identisch")
|
||||
for label, quelle in (("M5-ATR (heute)", A5), ("M15-ATR", A15)):
|
||||
Rs = []
|
||||
for entry, d, j0 in eintraege:
|
||||
i = j0 - 1
|
||||
a5 = A5[i]
|
||||
a = quelle[i]
|
||||
if not a or not a5 or a5 <= 0:
|
||||
continue
|
||||
a = max(a, _FLOOR)
|
||||
R = simulate(entry, d, a, H, L, C, j0, LIVE)
|
||||
# ⚠ zurueck in M5-Einheiten: R × verwendeter ATR ÷ ATR_M5
|
||||
r_m5 = R * a / max(a5, _FLOOR)
|
||||
kosten = (SP[i] if SP[i] > 0 else 0.0225) / max(a5, _FLOOR)
|
||||
Rs.append(r_m5 - kosten)
|
||||
print(kz(Rs, label))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+11
-1
@@ -76,7 +76,8 @@ def scan(H, L, C, A, SP, point, lo_i, hi_i, squeeze_mult, *,
|
||||
box_n: int = BOX_N_STD, k: float = K_STD, atr_min: float = ATRMIN_STD,
|
||||
fenster: int = FENSTER_STD, cooldown: int = COOLDOWN_STD,
|
||||
max_hold: int = MAXH_STD,
|
||||
exit_fn: Callable | None = None) -> tuple[list, list]:
|
||||
exit_fn: Callable | None = None,
|
||||
on_entry: Callable | None = None) -> tuple[list, list]:
|
||||
"""Ein Durchlauf: sammelt R (netto) je Ausbruch.
|
||||
|
||||
Rueckgabe `(Rs_sq, Rs_any)` — `Rs_any` = ALLE Box-Ausbrueche (die Kontrolle),
|
||||
@@ -86,6 +87,13 @@ def scan(H, L, C, A, SP, point, lo_i, hi_i, squeeze_mult, *,
|
||||
|
||||
⚠ `point` wird nicht mehr benutzt (SP kommt bereits in Preis), bleibt aber in
|
||||
der Signatur, damit bestehende Aufrufe unveraendert funktionieren.
|
||||
|
||||
⚠ `on_entry(j, d, lvl, atr, komprimiert)` ist ADDITIV (2026-08-08): ein
|
||||
Rueckruf je Ausbruch, der auch sagt, ob er aus einer KOMPRESSION kam. Ohne
|
||||
ihn ist `exit_fn` der einzige Haken — und der wird bei JEDEM Box-Ausbruch
|
||||
gerufen, nicht nur bei den komprimierten. Wer darueber eine Entry-Liste
|
||||
sammelt, bekommt still die Kontrollgruppe statt des Squeeze (real passiert:
|
||||
519 statt 132 Einstiege). Verhalten ohne `on_entry` unveraendert.
|
||||
"""
|
||||
ex = exit_fn or (lambda e, d, a, h, l, c, j: exit_legacy(
|
||||
e, d, a, h, l, c, j, max_hold=max_hold))
|
||||
@@ -118,6 +126,8 @@ def scan(H, L, C, A, SP, point, lo_i, hi_i, squeeze_mult, *,
|
||||
i += 1
|
||||
continue
|
||||
j, d, lvl = treffer
|
||||
if on_entry is not None:
|
||||
on_entry(j, d, lvl, atr, komprimiert)
|
||||
R = ex(lvl, d, atr, H, L, C, j + 1) - kosten(j, atr)
|
||||
Rs_any.append(R)
|
||||
if komprimiert:
|
||||
|
||||
Reference in New Issue
Block a user