Live-Code von Backtest-Skript entkoppelt: core/squeeze_scan.py

core/engine.py (squeeze_monitor) und weekly_review.py importierten
backtest_breakout_squeeze - die Abhaengigkeitsrichtung war verkehrt:
ein Fehler im Backtest konnte den laufenden Bot treffen.

Schwerer wog, WIE die Parameter hineinkamen:

    BQ._N = _SQ_N; BQ._K = _SQ_K

Das mutiert Modul-Globale eines Backtests, wirkt prozessweit und damit auf
jeden anderen Nutzer desselben Moduls im selben Prozess. Die Werte weichen
real ab (Backtest _N=12, live _SQ_N=10).

Geloest ueber core/squeeze_scan.py - Parameter stehen in der Signatur.
backtest_breakout_squeeze.py leitet nur noch weiter, damit es EINE
Implementierung gibt statt zweier (der sim_run/_ema_series-Fehler).

Der Exit ist bewusst NICHT mitrepariert: exit_legacy ist eine bitgenaue
Kopie inkl. seiner Abweichungen vom kanonischen Exit (Trail 1,5 statt 1,0,
kein Lock/Time-Stop/TP, max_hold 288 statt 200). Damit misst der B4-Monitor
gegen einen veralteten Exit - separater, offener Befund. Ihn hier still
mitzuaendern haette die Vergleichsgrundlage verschoben.

Bitgenauigkeit bewiesen (tests/test_squeeze_scan.py): die alte Fassung liegt
eingefroren im Test und laeuft gegen die neue ueber 500 synthetische Reihen
x 3 Multiplikatoren. Ein Vorher/Nachher-Lauf waere ungueltig gewesen - der
Backtest zieht Bars live aus MT5, das Fenster verschiebt sich staendig.

Test selbst per Mutationsprobe geprueft: 5 von 6 eingebauten Fehlern schlagen
an, der sechste (d>0 -> d>=0) ist beweisbar aequivalent, da d nur +-1 ist.

Live-Gegenprobe auf 9000 M5-Bars: n=112, Treffer 44%, OeR +0,178, PF 1,26.

Aktivierung beim naechsten Neustart - bei offener Position kein Neustart fuer
eine verhaltensneutrale Aenderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-07 07:21:45 +02:00
co-authored by Claude Opus 5
parent a35a0e7006
commit fd3c7f57a5
6 changed files with 398 additions and 51 deletions
+22 -43
View File
@@ -19,30 +19,27 @@ _COOL = 12 # Cooldown nach einem Trade (Bars) gegen Überlappung
_K = 0.1 # Ausbruch k×ATR über/unter die Box-Grenze
# ⚠⚠ DIE MECHANIK LIEGT SEIT 2026-08-07 IN `core/squeeze_scan.py`.
# Grund: `core/engine.py` und `weekly_review.py` haben sie von HIER importiert —
# LIVE-Code, der an einem Backtest-Skript haengt, und die Live-Parameter wurden
# ueber MUTIERTE Modul-Globale (`BQ._N = ...`) eingeschleust. Beides ist behoben.
# Hier bleiben nur duenne Weiterleitungen, damit es EINE Implementierung gibt und
# nicht zwei, die auseinanderdriften — der Fehler, den dieses Projekt bei
# `sim_run`/`_ema_series` (Kopien in >=6 Skripten) schon einmal gemacht hat.
# ⚠ Die Zahlen dieses Backtests bleiben unveraendert: bitgenau geprueft in
# `tests/test_squeeze_scan.py` (500 synthetische Kursreihen).
from core.squeeze_scan import atr_series as _core_atr
from core.squeeze_scan import exit_legacy as _core_exit
from core.squeeze_scan import scan as _core_scan
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 = [None]
for i in range(1, len(C)):
seg = t[max(1, i-p+1):i+1]
out.append(sum(seg)/len(seg))
return out
return _core_atr(H, L, C, p)
def sim(entry, d, atr, H, L, C, j0, sl_atr=2.0, trail=1.5, trail_on=0.3, be_on=1.3):
eff = entry - d*sl_atr*atr; hw = entry
end = min(j0+_MAXH, len(C)-1); exit_px = C[end]
for j in range(j0, end+1):
hi, lo = H[j], L[j]
if (lo <= eff) if d > 0 else (hi >= eff): exit_px = eff; break
hw = max(hw, hi) if d > 0 else min(hw, lo)
prof = (C[j]-entry)*d
if prof >= trail_on*atr:
cand = hw - d*trail*atr
if prof >= be_on*atr: cand = max(cand, entry) if d > 0 else min(cand, entry)
eff = max(eff, cand) if d > 0 else min(eff, cand)
return (exit_px-entry)*d/atr
return _core_exit(entry, d, atr, H, L, C, j0, sl_atr=sl_atr, trail=trail,
trail_on=trail_on, be_on=be_on, max_hold=_MAXH)
def rep(name, Rs):
@@ -55,29 +52,11 @@ def rep(name, Rs):
def scan(H, L, C, A, SP, point, lo_i, hi_i, squeeze_mult):
"""Ein Durchlauf: sammelt R (netto) je Breakout. Rs_any = ALLE Box-Ausbrüche,
Rs_sq = nur die mit Squeeze (Box<=squeeze_mult×ATR). i springt nach jedem
Breakout um _COOL vor (Überlappungsschutz) — für beide Listen identisch."""
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
Rs_sq = []; Rs_any = []
i = max(lo_i, _N+15)
while i < min(hi_i, len(C)-_MAXH-1):
atr = A[i]
if not atr or atr < _ATRMIN: i += 1; continue
boxHi = max(H[i-_N:i]); boxLo = min(L[i-_N:i]); box = boxHi-boxLo
squeezed = box <= squeeze_mult*atr
hit = None
for j in range(i, min(i+_W, len(C)-_MAXH-1)):
up = boxHi + _K*atr; dn = boxLo - _K*atr
if H[j] >= up: hit = (j, 1, up); break
if L[j] <= dn: hit = (j, -1, dn); break
if hit is None: i += 1; continue
j, d, lvl = hit
R = sim(lvl, d, atr, H, L, C, j+1) - cost(j, atr)
Rs_any.append(R)
if squeezed: Rs_sq.append(R)
i = j + _COOL
return Rs_sq, Rs_any
"""Weiterleitung auf `core.squeeze_scan.scan` mit den Modul-Globalen dieses
Skripts als Parameter (`main()` variiert `_N`/`_K` beim Sweep)."""
return _core_scan(H, L, C, A, SP, point, lo_i, hi_i, squeeze_mult,
box_n=_N, k=_K, atr_min=_ATRMIN, fenster=_W,
cooldown=_COOL, max_hold=_MAXH)
def main():