Auftrag zum benannten naechsten Schritt: die Backtests bauten die
Live-Level-Auswahl nach, statt sie zu benutzen. Drei echte Abweichungen:
1. Der Nachbau clusterte den GEMISCHTEN Rohpool (sorted(ch+cl)); live werden
ph und pl GETRENNT geclustert. Das ist exakt der Fehler, den der Live-Code
als behoben dokumentiert ("nicht vorher zusammenlegen!" - Ketten-Mega-
Cluster in dichten Zonen). Gemessen: 4,5 % aller Zeitpunkte hatten einen
anderen Widerstand.
2. Live rundet jede Linie auf 3 Stellen, der Nachbau nicht.
3. Live fuehrt Widerstand und Unterstuetzung mit getrennter Hysterese.
Geloest ueber core/sr_levels.py (cluster/linien/naechste/halten).
engine._draw_levels ruft es auf, Verhalten bitgenau unveraendert
(tests/test_sr_levels.py, 6 Tests, je 300-400 Zufallsfaelle). Der Test enthaelt
die Gegenprobe, dass der Nachbau wirklich abwich - sonst waere nicht belegt,
dass es etwas zu beheben gab.
backtest_pbreak_retrain.build_levels bleibt unveraendert (Migrations-Regel: die
daraus gefitteten Live-Gewichte duerfen sich nicht rueckwirkend verschieben).
Daneben neu: build_levels_live fuer kuenftige Fits.
ABER die Korrektur schliesst die Basisraten-Luecke nicht: 38,0 -> 38,3 %,
live 52,7 %.
Damit fuenf Hypothesen geprueft und alle widerlegt:
Niveau/Achsenabschnitt Roll-Achse faellt live durch
Vola-Regime ueber 5 Quintile flach (37,0-39,2 %)
Sammelart -1,7 Pp, falsche Richtung
ATR-Floor-Artefakt +2,8 Pp; bei live-gleichem ATR erst 40,1 %
Level-Auswahl +0,4 Pp
Nebenbefund: 53,9 % aller Backtest-Bars sitzen auf dem ATR-Floor 0,12, live nur
5,4 % - der Backtest-Zeitraum ist deutlich ruhiger. Der ATR selbst stimmt aber
ueberein (Median live 0,2167 gegen letzte 7 Tage 0,2174).
Kein Notfall: auto_sr_close ist false, das Modell schliesst keine Trades.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""Beweist, dass `core/sr_levels.py` bitgenau das rechnet, was bis 2026-08-07
|
|
inline in `engine._draw_levels` stand — und dass der Backtest-Nachbau ABWICH.
|
|
|
|
⚠ `_draw_levels` ist LIVE-Code: er bestimmt die gezeichneten S/R-Linien, den
|
|
Dashboard-Wert UND das Level, das der P(break)-Auto-Close bewertet. Eine
|
|
Refaktorierung dort darf das Verhalten nicht anfassen.
|
|
"""
|
|
import random
|
|
|
|
from core.sr_levels import cluster, halten, linien, naechste
|
|
|
|
|
|
# ── eingefrorene Fassung, wörtlich aus `engine._draw_levels` (vor dem Umbau) ──
|
|
def _alt_cluster(vals, atr):
|
|
out = []
|
|
for v in sorted(vals):
|
|
if out and v - out[-1][-1] <= 0.5 * atr:
|
|
out[-1].append(v)
|
|
else:
|
|
out.append([v])
|
|
return [round(sum(g) / len(g), 3) for g in out]
|
|
|
|
|
|
def _alt_hold(new, old, bid, atr, is_res):
|
|
if old is None:
|
|
return new
|
|
m = 0.10 * atr
|
|
broken = (bid > old + m) if is_res else (bid < old - m)
|
|
if broken:
|
|
return new
|
|
if (new is not None and abs(old - bid) > 1.5 * atr
|
|
and abs(new - bid) < abs(old - bid) - 0.5 * atr):
|
|
return new
|
|
return old
|
|
|
|
|
|
# ── der ABWEICHENDE Backtest-Nachbau (gemischter Pool, ohne Rundung) ─────────
|
|
def _nachbau_cluster(levels, atr):
|
|
if not levels:
|
|
return []
|
|
out = []; grp = [levels[0]]
|
|
for x in levels[1:]:
|
|
if x - grp[-1] <= 0.5 * atr:
|
|
grp.append(x)
|
|
else:
|
|
out.append(sum(grp) / len(grp)); grp = [x]
|
|
out.append(sum(grp) / len(grp))
|
|
return out
|
|
|
|
|
|
def _pivots(seed, n=40):
|
|
rnd = random.Random(seed)
|
|
basis = 75.0 + rnd.uniform(-3, 3)
|
|
ph = sorted(round(basis + rnd.uniform(0.0, 3.0), 3) for _ in range(n))
|
|
pl = sorted(round(basis - rnd.uniform(0.0, 3.0), 3) for _ in range(n))
|
|
return ph, pl
|
|
|
|
|
|
def test_cluster_bitgenau():
|
|
for seed in range(300):
|
|
ph, pl = _pivots(seed)
|
|
atr = 0.12 + (seed % 30) * 0.01
|
|
assert cluster(ph, atr) == _alt_cluster(ph, atr), f"seed {seed}"
|
|
assert cluster(pl, atr) == _alt_cluster(pl, atr), f"seed {seed}"
|
|
|
|
|
|
def test_linien_und_auswahl_bitgenau():
|
|
"""Der ganze Live-Pfad: getrennt clustern → zusammenführen → nächste wählen."""
|
|
for seed in range(300):
|
|
ph, pl = _pivots(seed)
|
|
atr = 0.12 + (seed % 30) * 0.01
|
|
px = 75.0 + (seed % 11) * 0.2
|
|
alt = _alt_cluster(ph, atr) + _alt_cluster(pl, atr)
|
|
r_alt = min((p for p in alt if p > px), default=None)
|
|
s_alt = max((p for p in alt if p < px), default=None)
|
|
r_neu, s_neu = naechste(linien(ph, pl, atr), px)
|
|
assert (r_neu, s_neu) == (r_alt, s_alt), f"seed {seed}"
|
|
|
|
|
|
def test_hysterese_bitgenau():
|
|
rnd = random.Random(7)
|
|
faelle = 0
|
|
for seed in range(400):
|
|
atr = 0.1 + (seed % 25) * 0.02
|
|
px = 75.0 + rnd.uniform(-2, 2)
|
|
for alt_l in (None, px + rnd.uniform(-3, 3)):
|
|
neu = px + rnd.uniform(-3, 3)
|
|
for is_res in (True, False):
|
|
a = halten(neu, alt_l, px, atr, is_res)
|
|
b = _alt_hold(neu, alt_l, px, atr, is_res)
|
|
assert a == b, f"seed {seed}"
|
|
faelle += 1
|
|
assert faelle > 1000
|
|
|
|
|
|
def test_nachbau_wich_wirklich_ab():
|
|
"""⚠ Gegenprobe: der Backtest-Nachbau (gemischter Pool) liefert NICHT
|
|
dasselbe. Ohne diesen Test wäre nicht belegt, dass es überhaupt etwas zu
|
|
beheben gab — und ein Test, der nicht scheitern kann, ist wertlos."""
|
|
abweichungen = 0
|
|
for seed in range(300):
|
|
ph, pl = _pivots(seed)
|
|
atr = 0.12 + (seed % 30) * 0.01
|
|
live = sorted(linien(ph, pl, atr))
|
|
nachbau = sorted(_nachbau_cluster(sorted(ph + pl), atr))
|
|
if [round(x, 3) for x in nachbau] != live:
|
|
abweichungen += 1
|
|
assert abweichungen > 0, "Nachbau und Live sind gleich — dann gab es nichts zu beheben"
|
|
|
|
|
|
def test_cluster_leer():
|
|
assert cluster([], 0.2) == []
|
|
assert linien([], [], 0.2) == []
|
|
assert naechste([], 75.0) == (None, None)
|