Powtarzalny backtest w Python bez klucza API
Stwórz powtarzalny backtest w Python bez użycia klucza API. Wykorzystaj deterministyczne dane oraz stałe wersje bibliotek, aby rzetelnie ocenić realne ryzyko strategii.
Powtarzalny backtest w języku Python to taki, który osoba trzecia może uruchomić na „czystym” komputerze i uzyskać identyczne wyniki bez konieczności posiadania konta czy klucza API. Większość poradników nie spełnia tego warunku już w pierwszym wierszu, gdzie pobieranie danych na żywo dostarcza czytelnikowi inną historię cen niż ta, którą otrzymał autor. Niniejszy przewodnik wykorzystuje silnik open-source quantjourney-bt w wersji 0.12.4, uruchamia dołączony do niego przykład bez żadnych danych uwierzytelniających, a następnie posługuje się rzeczywistymi danymi rynkowymi, aby wykazać, czego pojedyncze, poprawne uruchomienie testu nadal nie jest w stanie ujawnić.
Co decyduje o powtarzalności backtestu?
Powtarzalność ma w tym kontekście wąskie, weryfikowalne znaczenie: druga osoba uruchamia jedno polecenie na czystej maszynie i uzyskuje te same wyniki, co do miejsca po przecinku. Dwie typowe przyczyny uniemożliwiają osiągnięcie tego celu.
Pierwszą jest kod. Biblioteka w wersji 0.x nie gwarantuje kompatybilności między wydaniami typu minor. Zmiana nazwy, modyfikacja ustawień domyślnych czy zmiana kolejności kolumn sprawiają, że skrypt nadal działa, ale po cichu raportuje inne dane.
Drugą przyczyną są dane. Samouczek, którego pierwsza linijka polega na pobieraniu danych na żywo, nigdy nie był powtarzalny. Dostawcy korygują dane historyczne, uwzględniają podziały akcji i uzupełniają luki, przez co ten sam skrypt miesiąc później generuje inne wyniki. Po fakcie nie sposób odróżnić zmiany w kodzie od zmiany w danych.
Wzorem wartym naśladowania w tym projekcie jest powiązanie konkretnej wersji silnika z niewielkim, dołączonym zestawem danych, który jest dostarczany wewnątrz samego pakietu.
Instalacja wersji: pip install quantjourney-bt==0.12.4
quantjourney-bt to backtester QuantJourney, udostępniony na licencji Apache License 2.0, wymagający środowiska Python w wersji 3.11 lub nowszej. Wersja 0.12.4 została opublikowana 21 lipca 2026 roku i jest to wersja, do której odnosi się każde poniższe polecenie, zgodnie z dokumentacją z sierpnia 2026 roku.
Pracę należy wykonywać w izolowanym środowisku. python3 -m venv .venv tworzy takie środowisko, source .venv/bin/activate aktywuje je, a python -m pip install -U pip aktualizuje w nim instalator. Następnie należy pobrać konkretną wersję: pip install quantjourney-bt==0.12.4.
Projekt dokumentuje postać bez przypiętej wersji, pip install quantjourney-bt. Część ==0.12.4 pozostaje w gestii użytkownika, a przy wersji 0.x jest ona w pełni uzasadniona. Należy zapisać przypiętą wersję w miejscu dostępnym dla innych osób: pip freeze > requirements.txt rejestruje każdą rozwiązaną zależność, w tym te, które nie zostały wymienione bezpośrednio.
Dostępne są dwa opcjonalne dodatki. pip install "quantjourney-bt[wf]" dodaje bibliotekę Optuna na potrzeby przykładów typu walk-forward oraz optymalizacji. pip install "quantjourney-bt[data]" dodaje mechanizm yfinance wykorzystywany jako alternatywne źródło danych dla benchmarków.
Licencja Apache-2.0 jest licencją permisywną. Kod można wykorzystywać i modyfikować w celach komercyjnych, pod warunkiem zachowania plików licencji i informacji o prawach autorskich przy każdej redystrybucji, a współtwórcy wyraźnie udzielają praw patentowych.
Jak uruchomić dołączony przykład SMA bez klucza API
Repozytorium zawiera skrypt uruchomieniowy oraz pięćdziesiąt gotowych strategii, podzielonych na ścieżkę opartą na wagach i ścieżkę opartą na zleceniach, w tym pięć przepływów pracy typu walk-forward. ./strategy.sh --list wyświetla katalog. ./strategy.sh example_weights_01_sma_daily --check importuje pojedynczą strategię i nie przetwarza żadnych danych, co stanowi najszybsze potwierdzenie poprawności instalacji.
Samo uruchomienie wersji demonstracyjnej sprowadza się do jednego wiersza: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
Flaga --sample-data stanowi istotę tego rozwiązania. Projekt opisuje wykorzystany zestaw danych w następujący sposób:
Przykładowy zestaw danych jest celowo niewielki i odtwarzalny. Służy do sprawdzania instalacji, generowania raportów oraz analizy działania silnika bez konieczności zakładania konta.
Źródło: quantjourney-bt README, wersja 0.12.4, odczytano 6 sierpnia 2026 r.
Uruchomienie generuje katalog zamiast komunikatu w konsoli: summary.txt oraz summary.json, metrics.csv, equity_curve.csv obok equity_curve.png, dashboard.html, folder plots/ oraz run_metadata.json, w którym zapisano konfigurację uruchomienia. Ten ostatni plik jest najczęściej pomijany, a to właśnie on pozwala na audyt wyników po roku.
Wyniki metryk należy interpretować z rozwagą. Dołączony zestaw danych jest niewielki i ma charakter poglądowy, dlatego wskaźnik Sharpe’a oraz maksymalne obsunięcie kapitału wyświetlone w summary.txt dotyczą jedynie pliku próbnego. Nie stanowią one dowodu skuteczności strategii, a traktowanie ich jako ostatecznego wyniku jest pierwszym błędem, jaki można popełnić.
Uruchomienie to potwierdza jednak istotne kwestie: instalacja działa, a pełny przepływ silnika – od sygnału, przez wagi docelowe, aż po rekonstrukcję wartości portfela – generuje artefakty na lokalnej maszynie bez konieczności użycia jakichkolwiek danych uwierzytelniających. Istnieje ścieżka z autoryzacją do serwisu danych projektu, umożliwiająca przeprowadzanie backtestów na rzeczywistych danych historycznych. Ścieżka ta jest udokumentowana, a niniejszy przewodnik kończy się w tym miejscu, na etapie niewymagającym żadnych zewnętrznych zasobów.
Czego nie mówi pojedyncza krzywa kapitału z próby
Próba badawcza generuje jedną krzywą kapitału. Oto czego ta krzywa nie potrafi, w oparciu o rzeczywiste dane rynkowe, a nie pliki demonstracyjne.
Poniższy panel wykorzystuje tę samą koncepcję, co strategia przykładowa – przecięcie średniej kroczącej z dwudziestu sesji ze średnią z pięćdziesięciu sesji – stosuje ją do SPY i przedstawia każdy rok kalendarzowy oddzielnie, od 2017 do 2025 roku. Pozycja w każdej sesji jest ustalana na podstawie zamknięcia sesji poprzedniej, zatem reguła nigdy nie opiera się na danych, które nie były jeszcze dostępne.
Dokładny kod SQL dla każdej liczby
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearJedna niezmieniona reguła, mierzona 9 razy. Przed analizą czegokolwiek innego należy zapoznać się z dwiema kolumnami procentowymi. W 2017 reguła zakończyła rok wynikiem 16% wobec 19.4% dla strategii typu „kup i trzymaj” SPY w tym samym okresie. W 2025 te same dwie kolumny wskazują odpowiednio 10.4% oraz 16.4%. Kod w obu wierszach jest identyczny. Zmieniło się jedynie okno czasowe.
Kolumna przecięć pokazuje, jak skąpy staje się materiał dowodowy. 4 zmian pozycji w ciągu 2025 oznacza, że cały rok krzywej kapitału opiera się na zaledwie kilku decyzjach, co stanowi zbyt małą próbę, aby uznać wynik za miarodajny.
Czy ta sama reguła działa w ten sam sposób dla innych instrumentów?
Zmiana przedziału czasowego to jeden ze sposobów analizy pojedynczej krzywej. Zmiana uniwersum aktywów to drugi. Poniższe zestawienie utrzymuje parametry na stałym poziomie i uruchamia identyczną regułę dla pięciu płynnych instrumentów w okresie pięciu lat kalendarzowych od 2021 do 2025 roku.
Dokładny kod SQL dla każdej liczby
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ znajduje się na szczycie zestawienia z wynikiem 40.2%, a dolny wiersz, KO, osiąga 3.8%. Kolumna days_long_pct wskazuje, przez jaką część okresu każda z wersji utrzymywała jakąkolwiek pozycję, przy czym dla górnego wiersza wartość ta wynosi 67%. Jeden zestaw parametrów, pięć uniwersów i rozpiętość wyników na tyle duża, że wyłonienie zwycięzcy po fakcie nie mówi nic o strategii, której jeszcze nie przeprowadzono.
Żaden z powyższych punktów nie stanowi rekomendacji handlu w oparciu o przecięcie średnich. Przecięcie jest miarą dla backtestu, a backtest jest tym, co poddajemy pomiarowi.
Gdzie w backteście wag pojawia się błąd wyprzedzenia (look-ahead bias)
Silnik oparty na wagach przekształca sygnał w docelowe wagi, symuluje realizację zleceń dla tych wag, a następnie odtwarza wartość portfela na podstawie wynikających z tego pozycji. Błąd ukrywa się w punkcie połączenia sygnału z wagą. Jeśli dzisiejsza waga wynika z dzisiejszego kursu zamknięcia, a następnie generuje dzisiejszą stopę zwrotu, backtest opiera się na informacjach, które nie istniały w momencie składania zlecenia. Jest to błąd wyprzedzenia, który nie generuje komunikatu o błędzie. Sprawia on jedynie, że wyniki wyglądają na lepsze niż w rzeczywistości.
Poniższy panel przedstawia obie wersje tej samej reguły przetestowane na tej samej historii SPY.
Dokładny kod SQL dla każdej liczby
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearW 2017 wersja oparta na poprzedniej sesji wygenerowała 16%, podczas gdy wersja z tej samej sesji wykazała 17.1%, co stanowi różnicę 1.1 punktu procentowego. W 2025 dystans między tymi wynikami wyniósł 1.6 punktu procentowego. Tylko jedna z tych kolumn może zostać wygenerowana przez system, który nie posiadał wiedzy o kursie zamknięcia sesji, a różnica między nimi jest czystą księgowością, za którą nie stoi żaden zamysł, umiejętności ani transakcja.
Silnik ten jasno określa swoje stanowisko w kwestii harmonogramu, co jest warte więcej niż zapewnienia:
W przypadku realizacji zleceń na otwarciu, poślizg cenowy (slippage) uwzględnia jedynie poprzedni zakończony interwał, a prognoza wolumenu opiera się na opóźnionych obserwacjach; silnik nie wykorzystuje późniejszych wartości high, low, close ani pełnego wolumenu dziennego z danego dnia.
Źródło: quantjourney-bt README, wersja 0.12.4, odczytano 6 sierpnia 2026 r.
Udokumentowane założenie można zweryfikować w zainstalowanym kodzie źródłowym. Założenie nieudokumentowane jest jedynie domysłem.
Dlaczego istnieje dodatek walk-forward
Przykłady walk-forward, od WF01 do WF05, są dostarczane z dodatkiem [wf] oraz jego zależnością od Optuna. Metoda walk-forward dopasowuje parametry na jednym wycinku danych historycznych, mierzy je na kolejnym wycinku, a następnie przesuwa parę do przodu i powtarza proces. Warianty kroczący i rozszerzający różnią się tym, czy okno dopasowania odrzuca najstarsze dane w miarę przesuwania. Inny przykład dodaje usuwanie i blokadę na każdej granicy, odrzucając obserwacje znajdujące się najbliżej punktu styku, dzięki czemu dopasowany wycinek nie może przenikać do wycinka, na którym jest mierzony.
Nic z tego nie zmienia słabego pomysłu w działający. Zastępuje to pojedynczą liczbę rozkładem liczb, z którymi można polemizować, i na tym polega całe ulepszenie. Kolejny krok to wciąż nie są prawdziwe pieniądze: handel papierowy przed użyciem kapitału mierzy to, czego backtest strukturalnie nie jest w stanie dostrzec, zaczynając od tego, czy zlecenie zostanie zrealizowane w pobliżu ceny założonej przez symulator, a wyłączniki bezpieczeństwa dla botów transakcyjnych obejmują działania kodu w dniu, w którym nie funkcjonuje on zgodnie z planem. Jeśli chodzi o statystyki leżące u podstaw całości, nasze notatki dotyczące książki o ilościowym tradingu typu open-source wchodzą na głębszy poziom.
Najczęściej zadawane pytania
Czy można przeprowadzić backtesting strategii bez klucza API?
Tak. Pakiet quantjourney-bt zawiera przykładowy zestaw danych dostępny pod flagą --sample-data, a przykładowe strategie działają w oparciu o niego bez konieczności posiadania konta czy danych uwierzytelniających. Zestaw danych jest niewielki i ma charakter poglądowy, dlatego takie uruchomienie należy traktować jako weryfikację instalacji i potoku danych, a nie jako dowód skuteczności strategii.
Dlaczego należy przypisywać wersję pakietu do backtestingu w języku Python?
Pakiet w wersji 0.x nie gwarantuje kompatybilności między wydaniami typu minor, a zmiana wartości domyślnych lub nazewnictwa wskaźników nie jest komunikowana. Przypisanie wersji za pomocą pip install quantjourney-bt==0.12.4 oraz zapisanie środowiska w pliku wymagań oznacza, że wynik uzyskany dzisiaj będzie możliwy do odtworzenia w przyszłym roku na tym samym silniku, który go wygenerował.
Na jakiej licencji udostępniono quantjourney-bt?
Na licencji Apache License 2.0. Pozwala ona na komercyjne wykorzystanie i modyfikację, wymaga zachowania plików licencji i informacji o autorstwie przy każdej redystrybucji oraz zawiera wyraźne przyznanie praw patentowych przez współtwórców. Wersja 0.12.4 została opublikowana 21 lipca 2026 roku i wymaga języka Python w wersji 3.11 lub nowszej.
Czy silny wynik backtestu oznacza, że strategia działa?
Nie. Backtest jest pojedynczym pomiarem, wykonanym w jednym oknie czasowym, na jednym uniwersum aktywów. Powyższe panele pokazują, że ta sama niezmienna reguła generuje skrajnie różne wyniki roczne dla jednego tickera oraz zróżnicowane rezultaty dla pięciu różnych spółek. Jest to luka, którą mają ujawniać walidacja typu walk-forward oraz testy out-of-sample.
Jak obliczono powyższe panele
Kursy zamknięcia to ostatni print z sesji regularnej dla każdej daty, zarejestrowany według czasu nowojorskiego między 9:30 a 16:00, co pozwala na poprawne uwzględnienie dni o skróconej sesji bez konieczności sztywnego kodowania długości sesji. Średnia szybka obejmuje 20 sesji, a średnia wolna 50 sesji; obie są średnimi prostymi, a pierwsze 49 sesji każdej serii stanowi okres rozgrzewki, w którym nie zajmuje się pozycji. Wyniki roczne kumulują ruchy od zamknięcia do zamknięcia dla sesji, w których reguła utrzymywała długą pozycję, natomiast kolumna hold kumuluje każdą sesję w tym samym roku dla celów porównawczych. Pięć spółek w przekroju wybrano ze względu na ciągłość historii notowań bez podziałów akcji wewnątrz okna, dzięki czemu serie kursów zamknięcia nie wymagają korekty. Okna czasowe są sztywno ustalone w przeszłości, dlatego panele te zwracają te same liczby przy każdym ponownym wygenerowaniu.
Każdy panel w tym miejscu zawiera dokładne zapytanie SQL, na podstawie którego został wygenerowany, co sprawia, że liczby na tej stronie są równie powtarzalne, co przypisana wersja instalacji. Aby zmierzyć skuteczność reguły w wybranym przez siebie oknie czasowym przed napisaniem kodu backtestu, należy zadać pytanie w języku angielskim na terminalu Strasmore.