Enger Stop fuer die ersten N Bars: keine Aenderung (backtest_sl_staged.py)
User-Idee: SL beim Eroeffnen sehr eng, damit falsch laufende Trades frueh geschlossen werden; laeuft er richtig, etappenweise nachziehen. Der zweite Teil ist laengst gebaut (Phasen-Ratsche); neu war allein der zeitlich befristete enge Stop. Ohne Exit-Nachbau gerechnet: Lauf A (sl_atr=eng) und B (sl_atr=weit) laufen auf DEMSELBEN Pfad; innerhalb der ersten N Bars ist der einzige Unterschied der SL-Abstand, jeder Nicht-SL-Ausstieg faellt identisch aus. Also exakt R = R_A wenn A vor/bei Bar N aussteigt, sonst R_B. Keine fuenfte Exit-Kopie. 80k M5, Squeeze-Entries, ruhende Order am Level, kanonischer Exit, Echtkosten, ein Slot. Basis = durchgehend 3,4xATR_M5 (der Live-Stop, aus 121 echten Trades als Median gegengerechnet): Basis H1 -0,248 / WR 37,0 % / worst -3,73 H2 -0,107 / 44,4 % / -3,74 N=3, eng 0,5 H1 -0,263 / WR 23,3 % H2 -0,043 / 30,4 % N=6, eng 0,5 H1 -0,242 / WR 22,9 % / worst -0,89 H2 -0,045 / 30,1 % / -0,96 N=12, eng 0,5 H1 -0,240 H2 -0,044 DER ENTSCHEIDENDE BEFUND IST STRUKTURELL: die Staffelung hat keinen Raum. Bei N=12 steigen 269 von 279 Trades schon in Phase 1 aus (96 %) -- die mittlere Haltedauer ist 12-18 min, "die ersten 30-60 Minuten" sind der ganze Trade. Wo die Staffelung ueberhaupt etwas anderes tut als ein dauerhaft enger Stop (N=3), ist sie in H1 bei allen vier eng-Werten schlechter. Wo sie gewinnt (N=6/12), ist sie kein gestaffelter Stop mehr, sondern ein permanent enger. Regel verfehlt: die zwei nominell besseren Kombinationen liegen in H1 bei +0,007/+0,008, also weit unter einem Standardfehler (~0,055 bei n=279), die Nachbarn kippen, und alle Werte sind negativ (Verlustgrade, kein Edge). WAS ECHT IST: der Tail. Worst-Case -3,73 -> -0,83/-0,96 = Faktor 4. Bezahlt mit der Trefferquote (37,0 -> 22,9 %). Aber die Bar-Sim ueberschaetzt enge Stops systematisch (Kipp ~0,3xATR) -- die Verzerrung geht zugunsten der Idee, das Nein ist damit umso belastbarer. Nichts am Live-System geaendert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
02bec8fbb2
commit
c251652d33
@@ -5675,6 +5675,61 @@ Trades bei anderer Größe, was bei **manuellen** Trades eine starke Annahme ist
|
|||||||
und der Breaker wird auf **realisierte** Tagesverluste geprüft, während er live
|
und der Breaker wird auf **realisierte** Tagesverluste geprüft, während er live
|
||||||
auch die offene Position kappt → die Sim **unterschätzt** ihn.
|
auch die offene Position kappt → die Sim **unterschätzt** ihn.
|
||||||
|
|
||||||
|
### ⚠⚠ ENGER STOP FÜR DIE ERSTEN N BARS = KEINE ÄNDERUNG (`backtest_sl_staged.py`, 2026-08-13)
|
||||||
|
|
||||||
|
User-Idee: „SL beim Eröffnen sehr eng, damit falsch laufende Trades früh
|
||||||
|
geschlossen werden; läuft er richtig, den SL etappenweise nachziehen."
|
||||||
|
⚠ Der **zweite** Teil ist längst gebaut (Phasen-Ratsche in `core/trailing.py`);
|
||||||
|
neu und ungemessen war allein der **zeitlich befristete** enge Stop. Nicht zu
|
||||||
|
verwechseln mit einem durchgehend engen Stop — der ist 2× gemessen
|
||||||
|
(`backtest_sl_width.py`, `backtest_sl_basis.py`).
|
||||||
|
✅ **Ohne Exit-Nachbau gerechnet:** Lauf A (`sl_atr=eng`) und Lauf B
|
||||||
|
(`sl_atr=weit`) laufen auf DEMSELBEN Pfad; innerhalb der ersten N Bars ist der
|
||||||
|
einzige Unterschied der SL-Abstand, jeder Nicht-SL-Ausstieg fällt identisch aus.
|
||||||
|
Also exakt `R = R_A wenn A vor/bei Bar N aussteigt, sonst R_B`. Keine Näherung,
|
||||||
|
keine fünfte Exit-Kopie.
|
||||||
|
|
||||||
|
**80k M5, Squeeze-Entries, ruhende Order am Level, kanonischer Exit, Echtkosten,
|
||||||
|
EIN Slot. Basis = durchgehend 3,4×ATR_M5 (= der Live-Stop 2,0×ATR_M15, aus 121
|
||||||
|
echten Trades als Median gegengerechnet):**
|
||||||
|
| Variante | H1 ØR / WR / worst | H2 ØR / WR / worst |
|
||||||
|
|---|---|---|
|
||||||
|
| **Basis (heute)** | −0,248 / 37,0 % / **−3,73** | −0,107 / 44,4 % / **−3,74** |
|
||||||
|
| N=3, eng 0,5 | −0,263 / 23,3 % / −3,66 | −0,043 / 30,4 % / −0,96 |
|
||||||
|
| **N=6, eng 0,5** | **−0,242** / 22,9 % / **−0,89** | **−0,045** / 30,1 % / **−0,96** |
|
||||||
|
| N=12, eng 0,5 | −0,240 / 22,9 % / **−0,83** | −0,044 / 30,1 % / −0,96 |
|
||||||
|
| N=6, eng 0,8 | −0,258 / 27,2 % / −3,15 | −0,066 / 35,0 % / −1,26 |
|
||||||
|
|
||||||
|
⚠⚠ **DER ENTSCHEIDENDE BEFUND IST STRUKTURELL: die Staffelung hat gar keinen
|
||||||
|
Raum.** Die Spalte „früh" zeigt, wie viele Trades schon in Phase 1 aussteigen —
|
||||||
|
bei **N=12: 269 von 279 (96 %)**. Die mittlere Haltedauer liegt bei 12–18 min
|
||||||
|
(2–4 M5-Bars); „die ersten 30–60 Minuten" sind also **der ganze Trade**. Wo die
|
||||||
|
Staffelung überhaupt etwas anderes tut als ein dauerhaft enger Stop (N=3), ist
|
||||||
|
sie in H1 bei **allen vier** eng-Werten schlechter. Wo sie „gewinnt" (N=6/12),
|
||||||
|
ist sie **kein gestaffelter Stop mehr, sondern ein permanent enger** — und der
|
||||||
|
war die Ausgangsfrage nicht.
|
||||||
|
❌ **Regel verfehlt.** Zwei Kombinationen schlagen die Basis nominell in beiden
|
||||||
|
Hälften (N=6/12 mit eng 0,5: Δ H1 **+0,007/+0,008**), aber (a) das liegt bei
|
||||||
|
n≈279 **weit** unter einem Standardfehler (~0,055), (b) die **Nachbarn kippen**
|
||||||
|
(N=3 gleiche Enge → −0,014; eng 0,8 → −0,009 in H1), (c) es ist der degenerierte
|
||||||
|
Fall. Der ganze Ertrag steckt in H2 (+0,063), H1 ist ein Wash.
|
||||||
|
⚠ Und: **alle** Werte sind negativ — verglichen werden Verlustgrade, kein Edge.
|
||||||
|
|
||||||
|
✅ **WAS ECHT IST — der Tail.** Worst-Case **−3,73 → −0,83/−0,96**, also **Faktor
|
||||||
|
4**. Genau das ist für unbeaufsichtigten Betrieb die relevante Größe. Bezahlt
|
||||||
|
wird es mit der Trefferquote: **37,0 → 22,9 %** bzw. 44,4 → 30,1 %.
|
||||||
|
⚠⚠ **Aber die Bar-Simulation ÜBERSCHÄTZT enge Stops systematisch** (Kipp-Punkt
|
||||||
|
~0,3×ATR, `analyze_trail_resolution.py`) — die Verzerrung geht **zugunsten**
|
||||||
|
dieser Idee. Ein ✅ wäre also abzuschlagen; das ❌ ist umso belastbarer.
|
||||||
|
⚠ Nebenbefund: `durchgehend 2,0×ATR` schlägt die Basis in beiden Hälften
|
||||||
|
(Δ +0,019/+0,029) und in H1 sogar die gestaffelten Varianten. Das widerspricht
|
||||||
|
`backtest_sl_width.py` NICHT — dort war die Population die gegatete Wellen-Seite
|
||||||
|
mit Bar-Close-Einstieg, hier sind es Level-Einstiege. Falls jemand die SL-Breite
|
||||||
|
je anfasst, gehört diese Zelle nachgemessen, nicht die gestaffelte.
|
||||||
|
⚠ **Gleiche Grenze wie beim S/R-Lauf:** die ABSOLUTEN Werte reproduzieren den
|
||||||
|
dokumentierten Squeeze-Kontrollwert (+0,124/+0,292) nicht. Belastbar ist allein
|
||||||
|
der **relative** Vergleich — alle Varianten laufen auf derselben Entry-Liste.
|
||||||
|
|
||||||
### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11)
|
### ✅ S/R-AUTO-CLOSE WIEDER AN (User-Entscheidung 2026-08-11)
|
||||||
|
|
||||||
Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?"
|
Frage war: „sollten wir Trades bei einem S/R-Bounce automatisch schliessen?"
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ENGER Initial-Stop fuer die ersten N Bars, danach der normale — traegt das?
|
||||||
|
|
||||||
|
USER-IDEE (2026-08-13): „beim Eroeffnen den SL sehr eng setzen, so dass Trades
|
||||||
|
in die falsche Richtung schnell geschlossen werden. Laeuft der Trade richtig,
|
||||||
|
den SL etappenweise nachziehen."
|
||||||
|
|
||||||
|
⚠ Der zweite Teil ist bereits gebaut (die Phasen-Ratsche in `core/trailing.py`).
|
||||||
|
Neu und ungemessen ist allein der erste: ein **zeitlich befristeter** enger Stop.
|
||||||
|
Nicht zu verwechseln mit einem durchgehend engen Stop — DER ist zweimal gemessen
|
||||||
|
(`backtest_sl_width.py` 04.08., `backtest_sl_basis.py` 05.08.) und schlaegt den
|
||||||
|
Live-Stop in KEINER Variante beidhaelftig.
|
||||||
|
|
||||||
|
⚠⚠ WIE ES OHNE EXIT-NACHBAU GEHT (die Kopiererei hat 4 divergierende Exit-
|
||||||
|
Modelle erzeugt — das ist die dokumentierte Falle):
|
||||||
|
Lauf A = `simulate` mit sl_atr = ENG · Lauf B = `simulate` mit sl_atr = WEIT
|
||||||
|
Beide laufen auf DEMSELBEN Kurspfad. Innerhalb der ersten N Bars ist der
|
||||||
|
EINZIGE Unterschied zwischen A und B der SL-Abstand; jeder Nicht-SL-Ausstieg
|
||||||
|
(TP, Time-Stop) faellt in beiden identisch aus. Also gilt exakt:
|
||||||
|
R_gestaffelt = R_A wenn A vor/bei Bar N aussteigt, sonst R_B
|
||||||
|
Kein Nachbau, keine Naeherung.
|
||||||
|
|
||||||
|
⚠⚠ DIE BINDENDE GRENZE — bitte vor der Interpretation lesen:
|
||||||
|
`analyze_trail_resolution.py` (05.08.) hat gemessen, dass eine BAR-Simulation
|
||||||
|
**enge Stops systematisch UEBERSCHAETZT** (Kipp-Punkt ~0,3xATR): das Modell
|
||||||
|
nimmt den Bar-Hochpunkt mit und steigt erst im naechsten Bar aus. Ein
|
||||||
|
POSITIVES Ergebnis fuer die enge Variante ist deshalb nach oben verzerrt und
|
||||||
|
muesste abgeschlagen werden. Ein NEGATIVES ist belastbar.
|
||||||
|
Dazu: die Exit-Slippage ist ABSOLUT (Median 0,017 $, Ø 0,234 $). In R gerechnet
|
||||||
|
trifft sie alle Varianten gleich stark — sie verschiebt das Niveau, nicht die
|
||||||
|
Rangfolge; die Spalte steht trotzdem dabei.
|
||||||
|
|
||||||
|
Aufruf: python backtest_sl_staged.py [bars]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
|
from core.exit_model import LIVE, simulate
|
||||||
|
from core.squeeze_scan import atr_series, scan
|
||||||
|
|
||||||
|
_SQ_N, _SQ_K, _SQ_MULT = 10, 0.1, 2.5
|
||||||
|
_ATRMIN = 0.06 # ⚠ LIVE-Wert; der Modul-Default 0,12 waere eine andere Population
|
||||||
|
_WARTE = 8
|
||||||
|
# Live-Stop: 2,0xATR_M15 ~= 3,4xATR_M5 (Median ueber 121 echte Trades, 13.08.)
|
||||||
|
_WEIT_LIVE = 3.4
|
||||||
|
|
||||||
|
|
||||||
|
def entries(H, L, C, A, SP, point, lo, hi):
|
||||||
|
got = []
|
||||||
|
scan(H, L, C, A, SP, point, lo, hi, _SQ_MULT, box_n=_SQ_N, k=_SQ_K,
|
||||||
|
atr_min=_ATRMIN, exit_fn=lambda e, d, a, h, l, c, j: 0.0,
|
||||||
|
on_entry=lambda j, d, lvl, a, komp: (
|
||||||
|
got.append({"j": j, "d": d, "lvl": lvl, "atr": a}) if komp else None))
|
||||||
|
return got
|
||||||
|
|
||||||
|
|
||||||
|
def lauf(ein, H, L, C, SP, eng, nbars, weit, slip=0.0):
|
||||||
|
"""Sequentiell EIN Slot. `eng=None` → durchgehend `weit` (die Basis)."""
|
||||||
|
pW = replace(LIVE, sl_atr=weit)
|
||||||
|
pE = None if eng is None else replace(LIVE, sl_atr=eng)
|
||||||
|
Rs, n_frueh = [], 0
|
||||||
|
frei_ab, grenze = -1, len(C) - LIVE.max_hold - 2
|
||||||
|
for e in ein:
|
||||||
|
i, d, lvl, a = e["j"], e["d"], e["lvl"], e["atr"]
|
||||||
|
if i < frei_ab or i >= grenze or not a:
|
||||||
|
continue
|
||||||
|
fill = None
|
||||||
|
for k in range(i + 1, min(i + 1 + _WARTE, grenze)):
|
||||||
|
if L[k] <= lvl <= H[k]:
|
||||||
|
fill = k; break
|
||||||
|
if fill is None:
|
||||||
|
continue
|
||||||
|
RB, xbB = simulate(lvl, d, a, H, L, C, fill + 1, pW, ret_bar=True)
|
||||||
|
R, xb = RB, xbB
|
||||||
|
if pE is not None:
|
||||||
|
RA, xbA = simulate(lvl, d, a, H, L, C, fill + 1, pE, ret_bar=True)
|
||||||
|
if xbA <= fill + nbars: # in Phase 1 ausgestiegen → A gilt
|
||||||
|
R, xb = RA, xbA
|
||||||
|
n_frueh += 1
|
||||||
|
Rs.append(R - SP[i] / a - (slip / a if slip else 0.0))
|
||||||
|
frei_ab = xb + 1
|
||||||
|
return Rs, n_frueh
|
||||||
|
|
||||||
|
|
||||||
|
def ci(Rs, n=2500, seed=42):
|
||||||
|
if len(Rs) < 20:
|
||||||
|
return None, None
|
||||||
|
r = random.Random(seed); m = len(Rs)
|
||||||
|
b = sorted(sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n))
|
||||||
|
return b[int(.025 * n)], b[int(.975 * n)]
|
||||||
|
|
||||||
|
|
||||||
|
def kenn(Rs, frueh=0):
|
||||||
|
if not Rs:
|
||||||
|
return None
|
||||||
|
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
|
||||||
|
u, o = ci(Rs)
|
||||||
|
return {"n": len(Rs), "oer": sum(Rs) / len(Rs), "wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs),
|
||||||
|
"pf": (g / v if v else float("inf")), "worst": min(Rs), "u": u, "o": o,
|
||||||
|
"fr": frueh}
|
||||||
|
|
||||||
|
|
||||||
|
def zeig(name, k, basis=None):
|
||||||
|
if not k:
|
||||||
|
print(f" {name:<32} —"); return
|
||||||
|
kib = " — " if k["u"] is None else f"[{k['u']:+.3f} … {k['o']:+.3f}]"
|
||||||
|
dl = "" if basis is None else f" Δ{k['oer']-basis['oer']:+.3f}"
|
||||||
|
print(f" {name:<32} n={k['n']:>4} WR {k['wr']:>4.1f}% ØR {k['oer']:>+.3f} "
|
||||||
|
f"PF {k['pf']:>5.2f} worst {k['worst']:>+6.2f} früh {k['fr']:>4} {kib}{dl}")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
if bars is None:
|
||||||
|
print(" keine Bars"); return 1
|
||||||
|
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]
|
||||||
|
A = atr_series(H, L, C)
|
||||||
|
mid = len(C) // 2
|
||||||
|
e1 = entries(H, L, C, A, SP, point, 60, mid)
|
||||||
|
e2 = entries(H, L, C, A, SP, point, mid, len(C))
|
||||||
|
print(f"\n {sym} · {len(C)} M5-Bars · Squeeze-Entries H1 {len(e1)} · H2 {len(e2)}")
|
||||||
|
print(f" Einstieg ruhend am Level · kanonischer Exit · Echtkosten · EIN Slot")
|
||||||
|
print(f" Basis = durchgehend {_WEIT_LIVE}xATR_M5 (= der Live-Stop 2,0xATR_M15)")
|
||||||
|
print(f" ⚠ Bar-Sim UEBERSCHAETZT enge Stops (Kipp ~0,3xATR) — ein ✅ waere "
|
||||||
|
f"nach oben verzerrt\n")
|
||||||
|
|
||||||
|
b1 = kenn(*lauf(e1, H, L, C, SP, None, 0, _WEIT_LIVE))
|
||||||
|
b2 = kenn(*lauf(e2, H, L, C, SP, None, 0, _WEIT_LIVE))
|
||||||
|
print(" ── BASIS (heute) ──────────────────────────────────────────────")
|
||||||
|
zeig("durchgehend 3,4xATR H1", b1); zeig("durchgehend 3,4xATR H2", b2)
|
||||||
|
|
||||||
|
print("\n ── GESTAFFELT: eng fuer die ersten N Bars, dann 3,4xATR ───────")
|
||||||
|
besser = []
|
||||||
|
for nb in (3, 6, 12):
|
||||||
|
print(f" ── N = {nb} Bars ({nb*5} min) ──")
|
||||||
|
for eng in (0.5, 0.8, 1.0, 1.5):
|
||||||
|
k1 = kenn(*lauf(e1, H, L, C, SP, eng, nb, _WEIT_LIVE))
|
||||||
|
k2 = kenn(*lauf(e2, H, L, C, SP, eng, nb, _WEIT_LIVE))
|
||||||
|
zeig(f"eng {eng:.1f}xATR H1", k1, b1)
|
||||||
|
zeig(f"eng {eng:.1f}xATR H2", k2, b2)
|
||||||
|
if k1 and k2 and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"]:
|
||||||
|
besser.append((nb, eng, k1["oer"] - b1["oer"], k2["oer"] - b2["oer"]))
|
||||||
|
|
||||||
|
print("\n ── KONTROLLE: durchgehend eng (= der 2x gemessene Fall) ───────")
|
||||||
|
for eng in (0.8, 1.5, 2.0):
|
||||||
|
k1 = kenn(*lauf(e1, H, L, C, SP, None, 0, eng))
|
||||||
|
k2 = kenn(*lauf(e2, H, L, C, SP, None, 0, eng))
|
||||||
|
zeig(f"durchgehend {eng:.1f}xATR H1", k1, b1)
|
||||||
|
zeig(f"durchgehend {eng:.1f}xATR H2", k2, b2)
|
||||||
|
|
||||||
|
print("\n ── URTEIL ─────────────────────────────────────────────────────")
|
||||||
|
if besser:
|
||||||
|
print(f" {len(besser)} Kombination(en) schlagen die Basis in BEIDEN Haelften:")
|
||||||
|
for nb, eng, d1, d2 in besser:
|
||||||
|
print(f" N={nb}, eng={eng}xATR Δ H1 {d1:+.3f} · H2 {d2:+.3f}")
|
||||||
|
print(" ⚠ VOR einer Umsetzung: das Auflösungs-Artefakt verzerrt genau "
|
||||||
|
"diese Richtung\n nach oben. Belastbar waere erst eine Tick-Simulation.")
|
||||||
|
else:
|
||||||
|
print(" KEINE Kombination schlaegt die Basis in beiden Haelften.")
|
||||||
|
print(" ⚠ Dieses Ergebnis ist belastbar: die Verzerrung der Bar-Sim geht "
|
||||||
|
"ZUGUNSTEN\n der engen Stops — sie faellt trotzdem durch.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user