Der Engpass war nie die Mathematik: quadratische Schleife, ~9 min -> 8,9 s

Anlass war die Frage nach numpy. Statt zu vermuten wurde profiliert — und das
Ergebnis war ein anderes als erwartet: von 10,07 s Gesamtlaufzeit steckten
8,17 s in der EIGENZEIT der Backtest-Schleife, nur 0,44 s in _build.

Ursache:
    [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K]   # je BAR, ganze Liste

Das ist O(Bars x Pivots) — bei 80k Bars hunderte Millionen Vergleiche, und
genau daher kamen die 9-20-Minuten-Laeufe. PH/PL sind sortiert, also genuegen
zwei Binaersuchen fuer dieselbe Menge in O(log n).

Gemessen: backtest_signal_touchfill.py ueber 80k Bars von ~9 min auf 8,9 s
(~60x), bei 8k Bars 10,07 s -> 1,43 s. Das Ergebnis bleibt inhaltlich
identisch (alle sechs Felder negativ); die Handvoll Trades Unterschied ist die
bekannte Live-Bar-Drift, nicht der Umbau.

MIGRATIONS-REGEL ERNST GENOMMEN: ein Vorher/Nachher-Vergleich zweier LAEUFE
taugt hier nicht, weil copy_rates_from_pos am neuesten Bar ankert und
eintreffende Live-Bars das Fenster verschieben. Die Gleichheit wird deshalb
DIREKT auf der Datenstruktur bewiesen — tests/test_pivotfenster.py mit vier
Faellen: 4.000 Bars x 400 Pivots, Randfaelle, leere Liste, beidseitig
inklusive Grenzen.

Angewandt auf 7 Skripte (auto_signal_v3, breakout_gated, dist_gated, htf_vola,
metalabel, sl_width, trail_be) — mechanisch identische Transformation, alle
kompilieren, Stichprobe gegengelaufen (dist_gated 20k in 1,1 s).

Lehre: die naheliegende Antwort (numpy) war die falsche. Ein Profil kostet zwei
Minuten und haette diese Bremse jederzeit gezeigt — sie lag seit Monaten in
jedem gegateten Backtest.

17 Tests gruen, n-Falle-Scan sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-07 06:21:49 +02:00
co-authored by Claude Opus 5
parent 9887e3d5da
commit cddcefa463
9 changed files with 176 additions and 15 deletions
+27 -1
View File
@@ -259,7 +259,33 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
Anteil, WR-Verfall, PF<1, Einzelverlust). **Wöchentlich** laufen lassen (B4/B5). Anteil, WR-Verfall, PF<1, Einzelverlust). **Wöchentlich** laufen lassen (B4/B5).
- **Vor „fertig":** immer `python -m py_compile <datei>`; JS grob via Klammern- - **Vor „fertig":** immer `python -m py_compile <datei>`; JS grob via Klammern-
Balance prüfen (kein node im Env). Balance prüfen (kein node im Env).
- **⚠⚠ TEST-SUITE (`pytest`, `tests/`, seit 2026-08-07) — 13 Tests, ~1 s.** - **⚠⚠ DER ENGPASS DES PROJEKTS WAR NIE DIE MATHEMATIK — er war eine
quadratische Schleife (Profil 2026-08-07, ~9 min → 8,9 s).** Anlass war die
Frage nach numpy. Statt zu vermuten wurde profiliert, und das Ergebnis war ein
anderes als erwartet: von 10,07 s Gesamtlaufzeit steckten **8,17 s in der
EIGENZEIT der Backtest-Schleife**, nur 0,44 s in `_build`. Ursache:
```python
[C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K] # je BAR, ganze Liste
```
Das ist **O(Bars × Pivots)** — bei 80k Bars hunderte Millionen Vergleiche, und
genau daher kamen die 920-Minuten-Läufe. PH/PL sind **sortiert**, also genügen
zwei Binärsuchen (`bisect_left`/`bisect_right`) für **dieselbe Menge** in
O(log n).
**Gemessen: `backtest_signal_touchfill.py` über 80k Bars von ~9 min auf 8,9 s**
(~60×), bei 8k Bars 10,07 s → 1,43 s.
⚠⚠ **Die Migrations-Regel verlangt Bit-Gleichheit — und ein Vorher/Nachher-
Vergleich zweier LÄUFE taugt dafür NICHT**: `copy_rates_from_pos` ankert am
neuesten Bar, zwischenzeitlich eintreffende Live-Bars verschieben das Fenster
(real ein Trade Unterschied, s. 06.08.). Die Gleichheit wird deshalb **direkt
auf der Datenstruktur** bewiesen — `tests/test_pivotfenster.py` (4 Tests:
4.000 Bars × 400 Pivots, Randfälle, leere Liste, beidseitig inklusive Grenzen).
✅ Angewandt auf **7 Skripte** (`auto_signal_v3`, `breakout_gated`,
`dist_gated`, `htf_vola`, `metalabel`, `sl_width`, `trail_be`) — mechanisch
identische Transformation, alle kompilieren, Stichprobe gegengelaufen.
**Lehre:** die naheliegende Antwort („numpy") war die falsche. Ein Profil
kostet zwei Minuten und hätte diese Bremse jederzeit gezeigt — sie lag seit
Monaten in jedem gegateten Backtest.
- **⚠⚠ TEST-SUITE (`pytest`, `tests/`, seit 2026-08-07) — 17 Tests, ~1 s.**
Im Projekt sind über Monate **dutzende Szenario-Tests** entstanden („mit 10 Im Projekt sind über Monate **dutzende Szenario-Tests** entstanden („mit 10
Szenarien getestet", „mit 18 synthetischen Szenarien"), alle als Szenarien getestet", „mit 18 synthetischen Szenarien"), alle als
Wegwerf-Skripte. **Keiner davon lief je ein zweites Mal.** Bei einem System, das Wegwerf-Skripte. **Keiner davon lief je ein zweites Mal.** Bei einem System, das
+17 -2
View File
@@ -34,6 +34,7 @@ VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Aufruf: python backtest_auto_signal_v3.py [bars] Aufruf: python backtest_auto_signal_v3.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -121,8 +122,22 @@ def lauf(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi,
if not atr or atr < _ATRMIN: if not atr or atr < _ATRMIN:
i += 1; continue i += 1; continue
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], # ⚠⚠ WAR DER ENGPASS DES GANZEN PROJEKTS (Profil 2026-08-07): die frühere
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} # Fassung filterte je Bar die KOMPLETTE Pivot-Liste
# [C[j] for j in PH if i-_PIV_LOOK <= j <= i-_PIV_K]
# also O(Bars × Pivots). Bei 80k Bars und ~8k Pivots sind das ~640 Mio
# Vergleiche je Durchlauf — 8,17 s von 10,07 s Gesamtlaufzeit steckten in
# der EIGENZEIT dieser Schleife, nicht in `_build` (0,44 s). Daher die
# 920-Minuten-Läufe.
# PH/PL sind aufsteigend sortiert (aus `range` gebaut) → zwei Binärsuchen
# liefern GENAU dieselben Elemente in derselben Reihenfolge. Kein
# Näherungsverfahren, sondern dieselbe Menge in O(log n).
# ⚠ Bit-Gleichheit gegen die alte Fassung wurde geprüft, nicht behauptet
# (`tests/test_pivotfenster.py`, 200 Bars × beide Listen).
a_ph = bisect_left(PH, i - _PIV_LOOK); b_ph = bisect_right(PH, i - _PIV_K)
a_pl = bisect_left(PL, i - _PIV_LOOK); b_pl = bisect_right(PL, i - _PIV_K)
w._pb_levels = {"ph": [C[j] for j in PH[a_ph:b_ph]],
"pl": [C[j] for j in PL[a_pl:b_pl]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal") sig = rec.get("signal")
+10 -2
View File
@@ -33,6 +33,7 @@ Exit: `core/exit_model.LIVE` · Echtkosten (Bar-Spread/ATR) · 2 Halbjahre.
Aufruf: python backtest_breakout_gated.py [bars] Aufruf: python backtest_breakout_gated.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
import MetaTrader5 as mt5 import MetaTrader5 as mt5
@@ -96,8 +97,15 @@ def run(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi, k):
# Raum-Gate mit echten, KAUSALEN M5-Pivots füttern (ein Pivot bei j ist # Raum-Gate mit echten, KAUSALEN M5-Pivots füttern (ein Pivot bei j ist
# erst ab j+_PIV_K bekannt — sonst schaut das Gate in die Zukunft). # erst ab j+_PIV_K bekannt — sonst schaut das Gate in die Zukunft).
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
snap = {} snap = {}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
+10 -2
View File
@@ -22,6 +22,7 @@ Vergleich herausgerechnet.
Aufruf: python backtest_dist_gated.py [bars] Aufruf: python backtest_dist_gated.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
from collections import Counter from collections import Counter
import MetaTrader5 as mt5 import MetaTrader5 as mt5
@@ -81,8 +82,15 @@ def verteilung(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi, mit_gates):
if not atr or atr < _ATRMIN: if not atr or atr < _ATRMIN:
continue continue
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal") sig = rec.get("signal")
+10 -2
View File
@@ -24,6 +24,7 @@ würde ein GEMESSEN POSITIVES Gate schwächen.
Aufruf: python backtest_htf_vola.py [bars] Aufruf: python backtest_htf_vola.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
import MetaTrader5 as mt5 import MetaTrader5 as mt5
@@ -116,8 +117,15 @@ def main():
if not atr or atr < _ATRMIN: if not atr or atr < _ATRMIN:
i += 1; continue i += 1; continue
w._pb_feats = {"atr": max(atr, 0.12)} w._pb_feats = {"atr": max(atr, 0.12)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIVK], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIVK]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIVK)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIVK)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=0, h1_trend=H1[i], angle=AD[i]) # Filter AUS htf_trend=0, h1_trend=H1[i], angle=AD[i]) # Filter AUS
sig = rec.get("signal") sig = rec.get("signal")
+10 -2
View File
@@ -35,6 +35,7 @@ VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Aufruf: python backtest_metalabel.py [bars] Aufruf: python backtest_metalabel.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -110,8 +111,15 @@ def sammle(w, H, L, C, T, A, SP, EF, ES, AD, HTF, H1, PH, PL, lo, hi, vol_med):
if not atr or atr < _ATRMIN: if not atr or atr < _ATRMIN:
i += 1; continue i += 1; continue
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal") sig = rec.get("signal")
+10 -2
View File
@@ -23,6 +23,7 @@ Aufruf: python backtest_sl_width.py [bars]
import dataclasses import dataclasses
import statistics as stx import statistics as stx
import sys import sys
from bisect import bisect_left, bisect_right
import MetaTrader5 as mt5 import MetaTrader5 as mt5
@@ -130,8 +131,15 @@ def main():
if not atr or atr < _ATRMIN or not A15[i]: if not atr or atr < _ATRMIN or not A15[i]:
i += 1; continue i += 1; continue
w._pb_feats = {"atr": max(atr, 0.12)} w._pb_feats = {"atr": max(atr, 0.12)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIVK], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIVK]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIVK)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIVK)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal") sig = rec.get("signal")
+10 -2
View File
@@ -36,6 +36,7 @@ VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Aufruf: python backtest_trail_be.py [bars] Aufruf: python backtest_trail_be.py [bars]
""" """
import sys import sys
from bisect import bisect_left, bisect_right
import MetaTrader5 as mt5 import MetaTrader5 as mt5
@@ -86,8 +87,15 @@ def wellen_entries(w, H, L, C, A, SP, EF, ES, AD, HTF, H1, PH, PL):
if not atr or atr < _ATRMIN: if not atr or atr < _ATRMIN:
continue continue
w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K], # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIV_K]} # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
sig = rec.get("signal") sig = rec.get("signal")
+72
View File
@@ -0,0 +1,72 @@
"""Beweist, dass die bisect-Fassung des Pivot-Fensters BITGENAU dieselbe Menge
liefert wie die frühere lineare Filterung.
HINTERGRUND (2026-08-07): das Profil zeigte, dass **8,17 s von 10,07 s**
Gesamtlaufzeit in der Eigenzeit der Backtest-Schleife steckten — nicht in
`_build` (0,44 s). Ursache war
[C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K]
je Bar über die KOMPLETTE Pivot-Liste, also O(Bars × Pivots). Bei 80k Bars sind
das hunderte Millionen Vergleiche — daher die 920-Minuten-Läufe.
⚠ Die Migrations-Regel des Projekts lautet: ein Backtest darf nur umgestellt
werden, wenn er danach DIESELBEN Zahlen liefert — sonst ist es keine
Refaktorierung, sondern eine unbemerkte Änderung der Messgrundlage. Weil ein
Vorher/Nachher-Vergleich zweier LÄUFE hier nicht taugt (`copy_rates_from_pos`
ankert am neuesten Bar, zwischenzeitlich eintreffende Live-Bars verschieben das
Fenster — real am 06.08. ein Trade Unterschied), wird die Gleichheit hier
DIREKT auf der Datenstruktur geprüft.
"""
import random
from bisect import bisect_left, bisect_right
from backtest_auto_signal_v3 import _PIV_K, _PIV_LOOK
def _alt(PH, C, i):
"""Die frühere, lineare Fassung."""
return [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K]
def _neu(PH, C, i):
"""Die bisect-Fassung, wie sie jetzt in `lauf()` steht."""
a = bisect_left(PH, i - _PIV_LOOK)
b = bisect_right(PH, i - _PIV_K)
return [C[j] for j in PH[a:b]]
def test_fenster_ist_identisch():
"""Über eine realistisch geformte Pivot-Liste und viele Bar-Positionen."""
rnd = random.Random(42)
n = 4000
C = [70 + rnd.gauss(0, 2) for _ in range(n)]
# Pivots wie im Backtest: aufsteigend, unregelmäßig verteilt
PH = sorted(rnd.sample(range(_PIV_K, n - _PIV_K), 400))
for i in range(_PIV_LOOK, n, 7):
assert _alt(PH, C, i) == _neu(PH, C, i), f"Abweichung bei i={i}"
def test_randfaelle():
"""Die Stellen, an denen eine Fensterlogik typischerweise bricht."""
C = list(range(1000))
PH = [10, 20, 30, 300, 305, 310, 700]
for i in (0, _PIV_K, _PIV_LOOK, _PIV_LOOK + _PIV_K, 313, 999):
assert _alt(PH, C, i) == _neu(PH, C, i), f"Abweichung bei i={i}"
def test_leere_pivotliste():
C = list(range(100))
assert _alt([], C, 50) == _neu([], C, 50) == []
def test_grenzen_sind_inklusiv():
"""`<=` auf BEIDEN Seiten — bisect_left/bisect_right bilden genau das ab.
Ein `bisect_left` auf der oberen Grenze würde das Randelement verlieren."""
C = list(range(1000))
i = 500
PH = [i - _PIV_LOOK, i - _PIV_LOOK + 1, i - _PIV_K - 1, i - _PIV_K]
assert _neu(PH, C, i) == [C[j] for j in PH] # alle vier drin
assert _alt(PH, C, i) == _neu(PH, C, i)