Files
AH-Oil-Trader/core/sr_levels.py
T
Axel HocksandClaude Opus 5 aefac274d3 Level-Auswahl: Nachbau behoben (core/sr_levels.py) - Luecke bleibt offen
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>
2026-08-07 11:30:07 +02:00

92 lines
4.3 KiB
Python
Raw 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.
"""S/R-Linien: Clusterung, Auswahl und Hysterese — die geteilte Mechanik.
⚠⚠ WARUM ES DIESES MODUL GIBT (2026-08-07). `engine._draw_levels` entscheidet
live, WELCHES Level das P(break)-Modell bewertet. Die Backtests bauten dieselbe
Entscheidung nach (`build_levels`/`_cluster`/`pick_level` in
`backtest_pbreak_retrain.py`) — und der Nachbau wich in drei Punkten ab:
1. ⚠⚠ **Er clusterte den GEMISCHTEN Rohpool.** `build_levels` warf Hochs und
Tiefs in EINE sortierte Liste, danach lief die Nachbar-Kette darüber. Genau
das ist im Live-Code ausdrücklich als behobener Fehler dokumentiert:
„ph/pl bleiben GETRENNT geclustert (nicht vorher zusammenlegen!) — ein
erster Versuch clusterte den kombinierten Rohpool direkt, was in dichten
Zonen zu Ketten-Mega-Clustern führte." Der Nachbau machte also das, was live
als falsch erkannt und repariert worden war.
**Gemessen: 4,5 % aller Zeitpunkte hatten dadurch einen anderen Widerstand**
(80k M5-Bars, 13.283 Vergleichspunkte; Ø-Linienzahl 8,1 statt 8,4, grösste
Abweichung 12,98×ATR).
2. Live rundet jede Cluster-Linie auf **3 Nachkommastellen**, der Nachbau nicht.
3. Live führt Widerstand UND Unterstützung mit **getrennter** Hysterese; der
Nachbau hielt nur EINE Linie je Trade-Richtung.
Dieselbe Fehlerklasse wie bei `core/squeeze_scan.py` (07.08.) und beim
P(break)-Drift (31.07.): **ein Nachbau ist kein Nutzer desselben Codes.**
Hier stehen die Parameter deshalb in der Signatur, und Live wie Backtest rufen
dieselben Funktionen auf.
⚠ Die Zahlen des Live-Pfads ändern sich dadurch NICHT — `engine._draw_levels`
liefert nachweislich dieselben Linien wie vorher (Gleichheitstest in
`tests/test_sr_levels.py`).
"""
from __future__ import annotations
TOLERANZ_STD = 0.5 # Cluster-Toleranz in ×ATR ("nahe Pivot-Zwillinge")
HYSTERESE_STD = 0.10 # echter Durchbruch erst ab ±X×ATR
WEIT_STD = 1.5 # alte Linie gilt ab X×ATR als "irrelevant weit"
NAEHER_STD = 0.5 # ... und wird nur ersetzt, wenn die neue X×ATR näher ist
def cluster(vals, atr: float, toleranz: float = TOLERANZ_STD) -> list:
"""Nahe Werte zu je EINER Linie verschmelzen (Nachbar-Kette).
⚠ Nur auf EINE Seite anwenden (Hochs ODER Tiefs). Über den gemischten Pool
laufen zu lassen, verdoppelt die Punktdichte und lässt die Kette durch dichte
Zonen "durchlaufen" — s. Modul-Docstring, Punkt 1.
"""
out: list[list[float]] = []
for v in sorted(vals):
if out and v - out[-1][-1] <= toleranz * atr:
out[-1].append(v)
else:
out.append([v])
return [round(sum(g) / len(g), 3) for g in out]
def linien(ph, pl, atr: float, toleranz: float = TOLERANZ_STD) -> list:
"""Fertige Linienliste: ph und pl GETRENNT clustern, dann zusammenführen."""
return cluster(ph or [], atr, toleranz) + cluster(pl or [], atr, toleranz)
def naechste(alle, px: float):
"""(Widerstand, Unterstützung) = nächste Linie über/unter dem Kurs.
⚠ Ausgewählt wird nach der POSITION zum Kurs, NICHT nach dem Entstehungstyp
(Swing-Hoch/-Tief). Sonst bleibt ein gebrochenes Level für immer im falschen
Topf: ein unterschrittenes Swing-Tief, von unten erneut angesteuert, tauchte
nie als Widerstand auf (Fix 2026-07-23).
"""
r = min((p for p in alle if p > px), default=None)
s = max((p for p in alle if p < px), default=None)
return r, s
def halten(neu, alt, px: float, atr: float, is_res: bool,
hysterese: float = HYSTERESE_STD, weit: float = WEIT_STD,
naeher: float = NAEHER_STD):
"""Hysterese: die Linie bleibt stehen, bis der Kurs sie WIRKLICH durchbricht.
Ein neuer, näherer Pivot löst KEINEN Wechsel aus — genau das war die
Zitter-Quelle (auf M5 bilden sich laufend neue Pivots). Ausnahme: die alte
Linie ist weit weg (>`weit`×ATR) UND eine deutlich nähere existiert.
"""
if alt is None:
return neu
m = hysterese * atr
gebrochen = (px > alt + m) if is_res else (px < alt - m)
if gebrochen:
return neu # echter Durchbruch → nächste Linie (darf None sein)
if (neu is not None and abs(alt - px) > weit * atr
and abs(neu - px) < abs(alt - px) - naeher * atr):
return neu # alte Linie veraltet/weit weg → näheres Level
return alt # sonst: HALTEN (kein Springen)