Files
AH-Oil-Trader/backtest_srclose_reentry.py
Axel HocksandClaude Opus 5 3ee2b4114a Wiedereinstieg nach S/R-Close: 2-Halbjahres-Backtest faellt durch
Nachfolger des Live-Laufs (n=14, von einem Crash-Tag getragen).
80k M5-Bars, live-treu: Level ueber core.sr_levels.linien (ph/pl
getrennt geclustert), P(break) mit den echten _PB_*-Gewichten aus
core.engine, Bruch als Barrieren-Rennen, Wiedereinstieg als ruhende
Stop-Order mit Fill bei Beruehrung, kanonischer Exit.

Bruchquote 32-45 % - nahe der Basisrate ~38 %, die
Plausibilitaetspruefung besteht.

Ergebnis: die vorab fixierte Regel ist in keiner Zeile erfuellt.
Bei 0,3xATR ist der OeR beidhaelftig positiv (+0,119/+0,196), aber
H1s Intervall enthaelt die Null und der Nachbar 0,5 dreht H1 ins
Negative. Keine Parameter-Robustheit.

Fairerweise: zwei gleichgerichtete Haelften sind mehr als eine - "KI
enthaelt 0" heisst nicht widerlegt, sondern nicht belegt. H1 ist duenn
(285 Closes gegen 1.168), der ATR-Floor filtert die Niedrig-Vola-
Periode staerker.

Kohaerenter Nebenbefund: die ENGERE Bestaetigung ist besser als die
weiteren - dieselbe Mechanik wie bei breakout_k (spaeterer Einstieg
verschenkt den Anfang des Moves).

Urteil: nicht bauen. Wiedervorlage, wenn 0,3 auch in H1 ein Intervall
ueber null liefert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 09:46:08 +02:00

174 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Wiedereinstieg nach einem „falschen" S/R-Close — ueber 2 Halbjahre.
ANLASS (2026-08-12, User). Der Live-Lauf (`analyze_srclose_reentry.py`, 43 echte
Closes) sah gut aus — 71 % Treffer, ØR +0,859, ~+501 € — war aber **nicht
entscheidbar**: das 95-%-KI enthielt die Null, und **drei Trades eines einzigen
Crash-Tages (04.08.) trugen +530 €, die uebrigen elf 29 €**. Hier dieselbe Frage
auf 80k M5-Bars in zwei Halbjahren.
DIE FRAGE: Das P(break)-Gate schliesst, wenn das Level wahrscheinlich HAELT.
In ~38 % der Faelle bricht es doch. Lohnt es, dann wieder einzusteigen?
METHODE — bewusst LIVE-TREU, nicht nachgebaut:
· Level via `build_levels_live` + `_cluster` + `pick_level` aus
`backtest_pbreak_retrain` (dieselbe Hysterese wie `_draw_levels`).
· P(break) mit den **echten Live-Gewichten** `_PB_MU/_SD/_W` aus `core.engine`
— kein Nachbau, damit das Gate hier genau so entscheidet wie im Betrieb.
· Schliess-Moment = „am Level" (≤0,15×ATR, wie live `at_level`) UND
`p_break < sr_close_pbreak` (live 0,35).
· Bruch = **Barrieren-Rennen** `outcome()` (Bestaetigung gegen Ablehnung, wer
zuerst; Timeout = Abprall) — dieselbe Definition wie im Modell-TRAINING.
⚠ Genau hier lag der Fehler im ersten Live-Lauf: nur EINE Barriere gefragt
→ 93 % „Brueche" bei einer Basisrate von 38 %.
· Wiedereinstieg = ruhende Stop-Order an der Bestaetigungsmarke, Fuellung bei
BERUEHRUNG, **kanonischer Exit** (`core.exit_model.LIVE`), Echtkosten.
⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FIXIERT:
(1) ØR in BEIDEN Haelften > 0, (2) 95-%-Bootstrap-KI schliesst in beiden die
Null aus, (3) die Nachbar-Schwellen (0,3 / 0,7×ATR Bestaetigung) kippen nicht.
Alle drei muessen erfuellt sein.
Aufruf: python backtest_srclose_reentry.py [n_bars] (Default 80000)
"""
from __future__ import annotations
import random
import sys
import MetaTrader5 as mt5
from backtest_pbreak_retrain import (_ATRMIN, _BRK_W, _M30, _TWIN, _ZONE,
_atr_series, _ema_series,
build_levels_live)
from core.engine import _p_break
from core.exit_model import LIVE, simulate
# ⚠ Clusterung/Auswahl/Hysterese kommen aus dem LIVE-Modul, nicht aus dem
# Backtest-Nachbau: `_cluster` dort laeuft ueber den GEMISCHTEN ph+pl-Pool und
# erzeugt Ketten-Mega-Cluster — im Live-Code ist genau das als behobener Fehler
# dokumentiert. `linien()` clustert ph und pl GETRENNT.
from core.sr_levels import halten, linien, naechste
_SCHWELLE = 0.35 # live `sr_close_pbreak`
_BRK_STUFEN = (0.3, 0.5, 0.7)
def ci(v, n=4000, seed=42):
if len(v) < 20:
return None, None
r = random.Random(seed); m = len(v)
b = sorted(sum(v[r.randrange(m)] for _ in range(m)) / m for _ in range(n))
return b[int(.025 * n)], b[int(.975 * n)]
def lauf(lo, hi, H, L, C, SP, A, EF, ES, LV, brk):
"""Alle Schliess-Momente durchgehen; bei bestaetigtem Bruch wieder einsteigen."""
zu, gebrochen, Rs = 0, 0, []
prev_r = prev_s = None
letzt = {} # Dedup: (gerundetes Level, Seite) -> Bar
for i in range(lo, min(hi, len(C) - LIVE.max_hold - _BRK_W - 2)):
atr = A[i]
if not atr or atr < _ATRMIN:
continue
# ⚠ `build_levels_live` ist nach M30-Bars indiziert, nicht nach M5.
m = i // _M30
if m >= len(LV):
continue
lvd = LV[m]
px = C[i]
lv = linien(lvd["ph"], lvd["pl"], atr) # ph/pl GETRENNT geclustert
r_, s_ = naechste(lv, px)
r_ = halten(r_, prev_r, px, atr, True)
s_ = halten(s_, prev_s, px, atr, False)
prev_r, prev_s = r_, s_
mom6 = (C[i] - C[i - 6]) / atr
mom3 = (C[i] - C[i - 3]) / atr
ediff = EF[i] - ES[i]
for d, lvl in ((1, r_), (-1, s_)):
if lvl is None:
continue
gap = abs(lvl - px)
if gap > _ZONE * atr:
continue # nicht am Level
k = (round(lvl, 2), d)
if i - letzt.get(k, -999) < _TWIN:
continue # dieselbe Beruehrung
letzt[k] = i
wt = 1.0 if ediff * d > 0 else 0.0
p = _p_break(mom6 * d, mom3 * d, wt, gap / atr)
if p >= _SCHWELLE:
continue # Gate haette NICHT geschlossen
zu += 1
# Barrieren-Rennen mit der jeweils geprueften Bestaetigungsbreite
up = lvl + d * brk * atr
dn = lvl - d * brk * atr
fill = None
for j in range(i + 1, min(i + 1 + _BRK_W, len(C))):
if (L[j] <= dn) if d > 0 else (H[j] >= dn):
break # Level hielt
if (H[j] >= up) if d > 0 else (L[j] <= up):
fill = j; break
if fill is None:
continue
gebrochen += 1
kost = (SP[fill] if SP[fill] > 0 else 0.0225) / atr
Rs.append(simulate(up, d, atr, H, L, C, fill + 1, LIVE) - kost)
return zu, gebrochen, Rs
def zeig(lbl, zu, gebr, Rs):
if not Rs:
return f" {lbl:<22}{zu:>7}{gebr:>9} keine Wiedereinstiege"
n = len(Rs); w = sum(1 for x in Rs if x > 0)
g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
lo, hi = ci(Rs)
cis = f"[{lo:+.3f}{hi:+.3f}]" if lo is not None else "—"
flag = "✅" if (lo is not None and lo > 0) else ("❌" if (hi is not None and hi < 0) else "⚠")
return (f" {lbl:<22}{zu:>7}{gebr:>9}{100*gebr/max(1,zu):>7.0f} %"
f"{n:>7}{100*w/n:>7.0f} %{sum(Rs)/n:>+9.3f}"
f"{(g/v if v else 9.99):>7.2f}{sum(Rs):>+9.1f} {cis:>20} {flag}")
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]
A = _atr_series(H, L, C)
EF, ES = _ema_series(C, 12), _ema_series(C, 50)
LV = build_levels_live(H, L)
mid = len(C) // 2
print("=" * 118)
print(f" WIEDEREINSTIEG NACH S/R-CLOSE — {sym}, {len(C)} M5-Bars, 2 Halbjahre")
print(f" Schliess-Moment: am Level (≤{_ZONE}×ATR) UND p_break < {_SCHWELLE}")
print(" Bruch = Barrieren-Rennen (Bestaetigung gegen Ablehnung, Timeout = Abprall)")
print(" Wiedereinstieg: ruhende Stop-Order, Fill bei Beruehrung, kanonischer Exit")
print("=" * 118)
print(f"\n {'Bestaetigung':<22}{'Closes':>7}{'Brueche':>9}{'Quote':>8}"
f"{'n':>7}{'Treffer':>8}{'ØR':>9}{'PF':>7}{'ΣR':>9} {'95-%-KI':>20}")
for brk in _BRK_STUFEN:
for lbl, (a, b) in (("H1", (60, mid)), ("H2", (mid, len(C)))):
zu, gebr, Rs = lauf(a, b, H, L, C, SP, A, EF, ES, LV, brk)
print(zeig(f"{brk:.1f}×ATR {lbl}", zu, gebr, Rs))
print()
print(" " + "-" * 114)
print(" ⚠ REGEL (vorab): ØR beidhaelftig > 0 UND beide KI ohne Null UND die")
print(" Nachbar-Schwellen kippen nicht. Alle drei muessen erfuellt sein.")
print(" ⚠ Nicht modelliert: der Positions-Slot ist waehrend des Wiedereinstiegs")
print(" belegt — andere Setups koennen in der Zeit nicht feuern.")
print(" ⚠ Die Bruchquote MUSS nahe der Basisrate (~38 %) liegen. Tut sie das")
print(" nicht, ist die Bruch-Definition kaputt (so geschehen im Live-Lauf: 93 %).")
return 0
if __name__ == "__main__":
raise SystemExit(main())