"""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)
