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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2e89724368
commit
aefac274d3
@@ -4911,6 +4911,66 @@ Modell schliesst keine Trades.
|
||||
„die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die
|
||||
Basisraten-/Kalibrierungslücke.
|
||||
|
||||
## ⚠⚠ LEVEL-AUSWAHL: NACHBAU BEHOBEN — die Basisraten-Lücke bleibt UNERKLÄRT (2026-08-07)
|
||||
|
||||
Auftrag „behebe das" zum benannten nächsten Schritt: die Backtests bauten die
|
||||
Live-Level-Auswahl nach, statt sie zu benutzen.
|
||||
|
||||
✅ **DREI ECHTE ABWEICHUNGEN GEFUNDEN — und die erste ist ein Lehrstück:**
|
||||
| # | Nachbau (`backtest_pbreak_retrain.py`) | LIVE (`_draw_levels`) |
|
||||
|---|---|---|
|
||||
| 1 | clustert den **GEMISCHTEN** Rohpool (`sorted(ch+cl)`) | ph und pl **GETRENNT**, dann zusammenführen |
|
||||
| 2 | keine Rundung | `round(…, 3)` je Linie |
|
||||
| 3 | EINE Hysterese je Trade-Richtung | Widerstand und Unterstützung **getrennt** |
|
||||
⚠⚠ **Punkt 1 ist genau der Fehler, den der Live-Code als BEHOBEN 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 weiter das, was live
|
||||
erkannt und repariert worden war. **Gemessen: 4,5 % aller Zeitpunkte hatten einen
|
||||
anderen Widerstand** (13.283 Vergleichspunkte, Ø-Linienzahl 8,1 statt 8,4).
|
||||
|
||||
✅ **GELÖST: `core/sr_levels.py`** (`cluster` · `linien` · `naechste` · `halten`).
|
||||
`engine._draw_levels` ruft es auf; **Verhalten bitgenau unverändert**
|
||||
(`tests/test_sr_levels.py`, 6 Tests über je 300–400 Zufallsfälle inkl. Hysterese
|
||||
in beide Richtungen). ⚠ Der Test enthält bewusst auch die **Gegenprobe**, dass der
|
||||
Nachbau wirklich abwich — ohne sie wäre nicht belegt, dass es etwas zu beheben gab.
|
||||
⚠ `backtest_pbreak_retrain.build_levels` bleibt **unverändert** (Migrations-Regel:
|
||||
seine Zahlen und die daraus gefitteten Live-Gewichte `_PB_*` dürfen sich nicht
|
||||
rückwirkend verschieben). Daneben steht jetzt **`build_levels_live`** — **neue Fits
|
||||
gehören dorthin**, zusammen mit `core.sr_levels.linien` statt `_cluster`.
|
||||
|
||||
⚠⚠ **ABER: DIE KORREKTUR SCHLIESST DIE LÜCKE NICHT — +0,4 Pp.**
|
||||
| | Basisrate |
|
||||
|---|---|
|
||||
| Nachbau (gemischter Pool) | 38,0 % |
|
||||
| korrigiert (live-treu) | **38,3 %** |
|
||||
| **LIVE ab 01.08.** | **52,7 %** |
|
||||
|
||||
⚠⚠⚠ **FÜNF HYPOTHESEN, FÜNF WIDERLEGUNGEN — die 15-Pp-Lücke ist OFFEN:**
|
||||
| Hypothese | Ergebnis |
|
||||
|---|---|
|
||||
| Niveau/Achsenabschnitt driftet | Roll-Achse fällt live durch, oberste Klasse bleibt invertiert |
|
||||
| Vola-Regime | Bruchrate über 5 Quintile **flach** (37,0–39,2 %) |
|
||||
| Sammelart (je Berührung ↔ je Bar) | **−1,7 Pp**, falsche Richtung |
|
||||
| **ATR-Floor-Artefakt** | **+2,8 Pp**; bei live-gleichem ATR (≥0,20) erst **40,1 %** |
|
||||
| **Level-Auswahl (dieser Fix)** | **+0,4 Pp** |
|
||||
⚠ Der ATR-Befund ist trotzdem bemerkenswert: **53,9 % aller Backtest-Bars sitzen
|
||||
auf dem Floor 0,12**, live nur **5,4 %** — der Backtest-Zeitraum ist also viel
|
||||
ruhiger als die Live-Woche. Der ATR selbst stimmt dagegen überein (Median live
|
||||
0,2167 gegen letzte 7 Tage 0,2174, Verhältnis **1,00**).
|
||||
|
||||
**Stand: der Fix ist richtig und gebaut, erklärt den Live-Fehler aber nicht.**
|
||||
Die Live-Bruchrate von 52,7 % liegt ausserhalb der gesamten 9-Monats-Spanne
|
||||
(33,6…41,1 %) und ist nach fünf geprüften Erklärungen weiterhin offen.
|
||||
⚠ Nächste ungeprüfte Kandidaten: die **M1-gegen-M5-Auflösung** bei der
|
||||
Ausgangsbestimmung (live wertet gegen `candles_m1` aus, der Backtest gegen
|
||||
M5-Bars — innerhalb eines M5-Bars prüft der Backtest `confirm` VOR `reject`,
|
||||
was ihn *zugunsten* von „break" verzerrt, also in die falsche Richtung), und die
|
||||
Frage, ob `wave.pb_levels30` live überhaupt dieselben Pivots liefert wie
|
||||
`build_levels`. **Beides nicht gemessen.**
|
||||
⚠ Weiterhin **kein Notfall**: `auto_sr_close = false`, das Modell schliesst keine
|
||||
Trades.
|
||||
|
||||
## ⚠⚠ LIVE-CODE HING AN EINEM BACKTEST-SKRIPT — behoben 2026-08-07
|
||||
|
||||
**Befund:** `core/engine.py` (`squeeze_monitor`) und `weekly_review.py` importierten
|
||||
|
||||
@@ -105,8 +105,57 @@ def build_levels(H, L):
|
||||
return lv
|
||||
|
||||
|
||||
def build_levels_live(H, L):
|
||||
"""Wie `build_levels`, aber hält ph und pl GETRENNT — so wie live.
|
||||
|
||||
⚠⚠ WARUM ES DIESE ZWEITE FASSUNG GIBT (2026-08-07). `build_levels` wirft
|
||||
Hochs und Tiefs in EINE sortierte Liste, und `_cluster` läuft die
|
||||
Nachbar-Kette darüber. Genau das ist im Live-Code als behobener Fehler
|
||||
dokumentiert („ph/pl bleiben GETRENNT geclustert — nicht vorher
|
||||
zusammenlegen!"): über den gemischten Pool verdoppelt sich die Punktdichte
|
||||
und die Kette läuft durch dichte Zonen durch (Ketten-Mega-Cluster).
|
||||
Gemessen: **4,5 % aller Zeitpunkte** hatten dadurch einen anderen
|
||||
Widerstand; auf die Basisrate wirkt es mit **+0,4 Pp**.
|
||||
|
||||
⚠ Die ALTE `build_levels` bleibt unverändert — die dokumentierten Zahlen
|
||||
dieses Skripts (und die daraus gefitteten Live-Gewichte `_PB_*`) dürfen sich
|
||||
nicht rückwirkend verschieben (Migrations-Regel). **Neue Fits gehören auf
|
||||
diese Fassung**, zusammen mit `core.sr_levels.linien` statt `_cluster`.
|
||||
"""
|
||||
nm = len(H) // _M30
|
||||
mh = [max(H[i * _M30:(i + 1) * _M30]) for i in range(nm)]
|
||||
ml = [min(L[i * _M30:(i + 1) * _M30]) for i in range(nm)]
|
||||
bh = [[] for _ in range(nm)]; bl = [[] for _ in range(nm)]
|
||||
for p in range(_PIV_K, nm - _PIV_K):
|
||||
if mh[p] == max(mh[p - _PIV_K:p + _PIV_K + 1]) and p + _PIV_K < nm:
|
||||
bh[p + _PIV_K].append(mh[p])
|
||||
if ml[p] == min(ml[p - _PIV_K:p + _PIV_K + 1]) and p + _PIV_K < nm:
|
||||
bl[p + _PIV_K].append(ml[p])
|
||||
out = []
|
||||
wh = deque(); wl = deque(); ch = []; cl = []
|
||||
for m in range(nm):
|
||||
wh.append(bh[m]); ch.extend(bh[m])
|
||||
wl.append(bl[m]); cl.extend(bl[m])
|
||||
while len(wh) > _PIV_LOOKBACK:
|
||||
for x in wh.popleft():
|
||||
try: ch.remove(x)
|
||||
except ValueError: pass
|
||||
while len(wl) > _PIV_LOOKBACK:
|
||||
for x in wl.popleft():
|
||||
try: cl.remove(x)
|
||||
except ValueError: pass
|
||||
out.append({"ph": sorted(ch), "pl": sorted(cl)})
|
||||
return out
|
||||
|
||||
|
||||
def _cluster(levels, atr):
|
||||
"""Nahe Pivots zu einer Linie zusammenfassen (~0,5×ATR) — wie `_draw_levels`."""
|
||||
"""Nahe Pivots zu einer Linie zusammenfassen (~0,5×ATR).
|
||||
|
||||
⚠ NICHT „wie `_draw_levels`" — das stand hier jahrelang und war falsch: live
|
||||
wird je Seite geclustert und auf 3 Stellen gerundet, hier über den gemischten
|
||||
Pool und ohne Rundung. Für neue Arbeit `core.sr_levels.linien` benutzen;
|
||||
diese Funktion bleibt nur, damit die dokumentierten Zahlen reproduzierbar
|
||||
sind."""
|
||||
if not levels:
|
||||
return []
|
||||
out = []; grp = [levels[0]]
|
||||
|
||||
+13
-24
@@ -2483,16 +2483,17 @@ class TradingEngine:
|
||||
bid = (self.data.snapshot() or {}).get("bid")
|
||||
if not bid:
|
||||
return {"res": [], "sup": []}
|
||||
# Cluster-Toleranz 0,5×ATR: nahe Pivot-Zwillinge (z. B. 80,50 + 80,59)
|
||||
# verschmelzen zu EINER Linie.
|
||||
# ⚠ Clusterung/Auswahl/Hysterese liegen seit 2026-08-07 in
|
||||
# `core/sr_levels.py` — die Backtests bauten sie vorher NACH und
|
||||
# wichen dabei ab (gemischter Rohpool statt ph/pl getrennt; 4,5 %
|
||||
# der Zeitpunkte hatten einen anderen Widerstand). Verhalten hier
|
||||
# unverändert, bitgenau geprüft in `tests/test_sr_levels.py`.
|
||||
from core.sr_levels import cluster as _cluster_lv
|
||||
from core.sr_levels import halten as _halten_lv
|
||||
from core.sr_levels import naechste as _naechste_lv
|
||||
|
||||
def cluster(vals):
|
||||
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]
|
||||
return _cluster_lv(vals, atr)
|
||||
# Fix 2026-07-23 (User-Frage „Widerstand/Support-Retest von der falschen
|
||||
# Seite"): Widerstand/Support werden NACH AKTUELLER KURSPOSITION gewählt
|
||||
# (über/unter bid), NICHT mehr strikt nach Entstehungstyp (Swing-Hoch `ph`
|
||||
@@ -2512,27 +2513,15 @@ class TradingEngine:
|
||||
pl_c = cluster(lv.get("pl") or [])
|
||||
both = ph_c + pl_c
|
||||
# NUR die nächste je Seite (2 Linien total = wie im Dashboard)
|
||||
r = min((p for p in both if p > bid), default=None)
|
||||
s = max((p for p in both if p < bid), default=None)
|
||||
r, s = _naechste_lv(both, bid)
|
||||
# Hysterese „klebrig" (Fix 2026-07-21): die gezeichnete Linie bleibt
|
||||
# stehen, solange der Kurs sie nicht WIRKLICH durchbricht (bid > R+0,1×ATR
|
||||
# bzw. bid < S−0,1×ATR). Ein neuer, näherer Pivot löst KEINEN Wechsel aus
|
||||
# — genau das war die Zitter-Quelle (auf M5 bilden sich laufend neue Pivots).
|
||||
# Ausnahme: ist die alte Linie weit weg (>1,5×ATR) UND ein deutlich näheres
|
||||
# Level existiert, wird sie übernommen (sonst bleibt sie irrelevant hängen).
|
||||
def hold(new, old, 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 # echter Durchbruch → nächste Linie (kann None sein)
|
||||
if (new is not None and abs(old - bid) > 1.5 * atr
|
||||
and abs(new - bid) < abs(old - bid) - 0.5 * atr):
|
||||
return new # alte Linie veraltet/weit weg → näheres Level
|
||||
return old # sonst: Linie HALTEN (kein Springen)
|
||||
r = hold(r, self._draw_last.get("r"), True)
|
||||
s = hold(s, self._draw_last.get("s"), False)
|
||||
r = _halten_lv(r, self._draw_last.get("r"), bid, atr, True)
|
||||
s = _halten_lv(s, self._draw_last.get("s"), bid, atr, False)
|
||||
self._draw_last = {"r": r, "s": s}
|
||||
out = {"res": [r] if r is not None else [],
|
||||
"sup": [s] if s is not None else []}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,114 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user