Files
AH-Oil-Trader/tests/test_pivotfenster.py
Axel HocksandClaude Opus 5 cddcefa463 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>
2026-08-07 06:21:49 +02:00

73 lines
2.7 KiB
Python
Raw Permalink 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.
"""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)