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>
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""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 9–20-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)
|