błąd perspektywy czasowej w backteście
Błąd perspektywy czasowej (look-ahead bias) to przeciek przyszłych danych do backtestu. Omówiono decyzje w tym samym słupku, błąd przeżycia i poprawione dane, które zawyżają wyniki
Błąd perspektywy czasowej (look-ahead bias) polega na wykorzystaniu w backteście informacji, których nikt nie mógł znać w momencie składania symulowanego zlecenia. Krzywa kapitału rośnie, statystyki wyglądają doskonale, a żadna z nich nie przetrwa kontaktu z realnym zleceniem. Niniejsza strona definiuje ten błąd, omawia trzy mechanizmy, które go powodują, i mierzy każdy z nich na rzeczywistych danych rynkowych.
Czym jest błąd perspektywy czasowej
Backtest to twierdzenie o decyzji: w tym znaczniku czasu, mając te informacje, reguła zrobiłaby to. Twierdzenie to jest prawdziwe tylko wtedy, gdy każdy argument, który reguła odczytuje, istniał już w swojej ostatecznej formie przed tym znacznikiem czasu. Błąd perspektywy czasowej to każde naruszenie tego warunku i jest on cichy. Nic w arkuszu kalkulacyjnym ani skrypcie nie zgłasza problemu, gdy wiersz zwraca wartość opatrzoną znacznikiem czasu późniejszym niż decyzja, którą zasila.
Trzy naruszenia obejmują większość tego, co pojawia się w praktyce. Decyzja odczytująca świecę cenową, w ramach której również handluje. Uniwersum złożone z nazw, które są notowane do dziś. Pole danych, które zostało zrewidowane po dacie, którą nosi.
Mechanizm pierwszy: decyzja na świecy, w ramach której handlujesz
Zacznijmy od reguły, która brzmi nieszkodliwie. Kupuj na sile, czyli na sesjach, na których fundusz kończy powyżej otwarcia. Nieumiejętnie zakodowany test wchodzi na otwarciu tej sesji i wychodzi na jej zamknięciu, podczas gdy warunek, na którym testuje, to cena zamknięcia tej samej świecy. Nikt nie zna ceny zamknięcia na dzwonku otwarcia.
Panel poniżej uruchamia obie wersje na SPY, trackerze S&P 500, w ciągu dziesięciu lat kalendarzowych. Pierwsza kolumna to ta niemożliwa. Druga opóźnia identyczny warunek o jedną sesję, co jest najwcześniejszym momentem, w którym sfinansowane konto mogłoby na nim zadziałać.
Dokładny kod SQL dla każdej liczby
WITH bars AS (
SELECT date,
toFloat64(open) AS o,
toFloat64(close) AS c,
toFloat64(close) > toFloat64(open) AS up_day
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= toDate('2015-12-01')
AND date <= toDate('2025-12-31')
),
lagged AS (
SELECT date, o, c, up_day,
any(up_day) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prior_up
FROM bars
)
SELECT toYear(date) AS year,
countIf(up_day) AS signal_days,
round(100 * avgIf(c / o - 1, up_day), 3) AS same_bar_avg_pct,
round(100 * avgIf(c / o - 1, prior_up), 3) AS lagged_avg_pct,
round(100 * (avgIf(c / o - 1, up_day) - avgIf(c / o - 1, prior_up)), 3) AS gap_pp
FROM lagged
WHERE date >= toDate('2016-01-01')
GROUP BY year
ORDER BY yearRóżnica nie jest subtelna. W 2016 wersja na tej samej świecy osiągnęła średnio 0.447% na kwalifikującą się sesję, podczas gdy wersja opóźniona osiągnęła średnio -0.008%, co daje spread 0.455 punktu procentowego dziennie. W 2025 obie odczytały 0.636% i -0.03%. Przy mniej więcej 140 kwalifikujących się sesjach w roku, spread wynoszący pół punktu procentowego na sesję kumuluje się w krzywą kapitału, której żadne realne konto nigdy nie wydrukowało.
Cała przewaga ma charakter definicyjny. Wybór sesji, które zamknęły się powyżej otwarcia, a następnie pomiar od otwarcia do zamknięcia na tych samych sesjach, mierzy wielkość z góry gwarantowaną jako dodatnią. Reguła odkryła własny filtr.
Odmiany tego ukrywają się w danych dziennych przez cały czas: średnia krocząca, która zawiera dzisiejsze zamknięcie i jest używana do handlu dzisiaj, sygnał obliczony na cenach już skorygowanych o splitu, który jeszcze nie nastąpił, stop intraday umieszczony na maksimum sesji, które jeszcze nie zostało wydrukowane. Wspólny kształt to znacznik czasu decyzji znajdujący się wcześniej niż jeden z jej własnych argumentów.
Mechanizm drugi: uniwersum złożone z dzisiejszych ocalałych
Drugi mechanizm nigdy nie dotyka reguły. Leży na liście nazw, które reguła może zobaczyć.
Pobierz listę tickerów od obecnego dostawcy danych, uruchom ją dziesięć lat wstecz, a próbka po cichu wyklucza każdą spółkę, która zbankrutowała, została przejęta lub została wycofana z obrotu po drodze. Reguła nigdy nie otrzymała przegranych. Panel mierzy wielkość tego pominięcia: każda nazwa notowana w USA, która handlowała co najmniej 200 sesji powyżej 1 USD w danym roku kalendarzowym, udział tej kohorty wciąż drukującej ceny w lipcu 2026 r. oraz medianę stopy zwrotu w roku kalendarzowym całej kohorty obok mediany samych ocalałych.
Dokładny kod SQL dla każdej liczby
WITH recent AS (
SELECT DISTINCT ticker
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2026-06-15')
AND date <= toDate('2026-07-28')
),
per_name AS (
SELECT toYear(date) AS year,
ticker,
argMin(toFloat64(close), date) AS first_close,
argMax(toFloat64(close), date) AS last_close,
count() AS sessions
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2015-01-01')
AND date <= toDate('2024-12-31')
AND close > 1
GROUP BY year, ticker
HAVING sessions >= 200
)
SELECT year,
uniqExact(ticker) AS names_trading,
round(100 * uniqExactIf(ticker, ticker IN (SELECT ticker FROM recent)) / uniqExact(ticker), 1) AS still_listed_pct,
round(100 * quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker)), 2) AS median_return_all_pct,
round(100 * quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent)), 2) AS median_return_survivors_pct,
round(100 * (quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent))
- quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker))), 2) AS survivor_gap_pp
FROM per_name
GROUP BY year
ORDER BY yearZ 7063 nazw, które przekroczyły poprzeczkę w 2015, 52.5% wciąż handlowało w lipcu 2026 r. Dla kohorty 2024 liczba ta wynosi 87.5%, co wynika głównie z upływu czasu: nazwa z 2024 r. miała dwa lata, by zniknąć, a nazwa z 2015 r. miała jedenaście lat.
Kolumny zwrotu niosą sam błąd. W 2017 mediana nazwy zwróciła 7.51%, podczas gdy mediana ocalałego zwróciła 11.32%, co daje lukę 3.81 punktu procentowego otrzymaną za darmo. Luka jest dodatnia w każdym z 10 przedstawionych lat. Strategia testowana na ocalałych dziedziczy ją przed podjęciem jakiejkolwiek decyzji, a reguły, które przesiewają pod kątem słabości, dziedziczą więcej niż mediana, ponieważ nazwy, które zniknęły, znajdują się nieproporcjonalnie w tym koszyku.
Korektą jest uniwersum punktowe (point-in-time): lista nazw, jaka istniała w każdym dniu rebalansowania, z uwzględnieniem wycofań, przy czym pozycja wycofana jest zamykana po ostatnim kursie, a nie usuwana z zapisu.
Mechanizm trzeci: dane, które zostały zrewidowane po swojej dacie
Ceny są stemplowane raz. Dane fundamentalne nie. Wartość kwartalna nosi datę końca kwartału, zostaje zmieniona w późniejszym zgłoszeniu, a baza danych przechowująca tylko bieżącą wersję chętnie poda zmienioną liczbę pod oryginalną datą. Reguła przesiewająca na podstawie tej liczby odczytuje korektę, której nadejście zajęło miesiące.
Ten sam wzór przebiega przez listy członkowskie indeksów odbudowane do bieżących składników, historie szacunków analityków nadpisane ostatecznym konsensusem oraz korekty działań korporacyjnych zastosowane wstecz na całej serii cen. W każdym przypadku przechowywany zapis jest migawką w czasie teraźniejszym noszącą datę czasu przeszłego.
Dwie praktyki go powstrzymują. Używaj źródła, które przechowuje roczniki (vintages), czyli wartość taką, jaką znano w każdej dacie, a nie taką, jaką znana jest teraz. Gdy roczniki są niedostępne, zastosuj opóźnienie raportowania wystarczająco duże, by pokryć rzeczywiste, i traktuj długość tego opóźnienia jako parametr wartym testowania, a nie szczegół wartym zgadywania.
Jak wygląda krzywa, gdy przyszłość wycieka
Błąd perspektywy czasowej ma sygnaturę: wyniki znacznie przewyższające to, co oferował bazowy rynek. Użytecznym punktem odniesienia jest sufit perspektywy wstecznej (hindsight ceiling), czyli wzrost, jaki doskonała prognoza jednodniowa wygenerowałaby na pojedynczym instrumencie.
Dokładny kod SQL dla każdej liczby
WITH bars AS (
SELECT date,
toFloat64(close) AS c,
any(toFloat64(close)) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= toDate('2015-12-01')
AND date <= toDate('2025-12-31')
),
rets AS (
SELECT toYear(date) AS year,
c / prev_c - 1 AS r
FROM bars
WHERE date >= toDate('2016-01-01')
AND prev_c > 0
)
SELECT year,
count() AS sessions,
round(100 * (exp(sum(log(1 + r))) - 1), 1) AS buy_and_hold_pct,
round(100 * (exp(sumIf(log(1 + r), r < 0.02)) - 1), 1) AS without_big_up_days_pct,
round(exp(sum(log(1 + abs(r)))), 1) AS hindsight_ceiling_multiple
FROM rets
GROUP BY year
ORDER BY yearSufit jest ogromny. W 2020 doskonała jednodniowa perspektywa na SPY pomnożyła kapitał 27.7 razy w ciągu 253 sesji, wobec 16.2% za samo trzymanie funduszu. W najspokojniejszym roku z dziesięciu, 2017, ta sama doskonała prognoza zwróciła 2.2 razy kapitał.
Środkowa kolumna pokazuje, jak skoncentrowany jest ten sufit. Usuń sesje, które zyskały więcej niż 2%, a 2020 zmienia się z 16.2% na -44.9%. Wyciek wart zaledwie kilku takich sesji rocznie przesuwa backtest bardzo daleko. Krzywa z płytkimi spadkami, wysokim wskaźnikiem trafień i najlepszymi dniami skupionymi na najbardziej gwałtownych sesjach rynku zasługuje na audyt.
Kalibracja ma większe znaczenie niż jakikolwiek pojedynczy próg. Reguła twierdząca, że osiąga gładkie 40% rocznie przez dekadę, twierdzi, że stanowi dużą część sufitu perspektywy wstecznej, a uczciwe pytanie brzmi: który argument powiedział jej, co ma się wydarzyć.
Jak testować backtest pod kątem błędu perspektywy czasowej
- Opóźnij wszystko o jedną świecę. Przesuń każdy sygnał o jeden pełny okres później i uruchom ponownie. Prawdziwe przewagi słabną nieznacznie. Wycieki załamują się, często do zera.
- Zaburz przyszłość. Weź dane po każdym znaczniku czasu decyzji, zastąp je szumem lub przetasowaniem i uruchom ponownie logikę decyzyjną. Każda decyzja powinna wyjść identyczna. Każda decyzja, która się zmienia, odczytała coś, czego nie mogła znać.
- Traktuj znaczniki czasu jako dane. Dla każdego argumentu przechowuj moment, w którym wartość stała się dostępna, obok momentu, który opisuje. Złączenie, które nigdy nie porównuje tych dwóch kolumn, nie może egzekwować reguły.
- Odbuduj uniwersum na daną datę. Uwzględnij nazwy wycofane i przejęte, zamknij je po ostatnim kursie i uruchom tę samą regułę na obu uniwersach, aby określić wielkość wkładu z tytułu przetrwania (survivorship bias).
- Zmierz względem sufitu. Porównaj zwrot strategii z wartością kup i trzymaj oraz wartością doskonałej prognozy dla tego samego okna. Odległość od sufitu to kontekst, którego sam wskaźnik Sharpe'a nie daje.
Określanie wielkości pozycji leży poniżej tego wszystkiego. Zawyżony wskaźnik wygranych zasila zawyżoną wielkość zakładu, co jest miejscem, w którym określanie wielkości pozycji według kryterium Kelly'ego zamienia błąd księgowy w spadek. Ta sama dyscyplina ma zastosowanie, zanim ruszy realny kapitał: handel papierowy przed realnymi pieniędzmi testuje mechanikę zleceń, a zautomatyzowane stosy niosą to samo zagrożenie z szybkością maszyny, co opisano w wieloagentowych systemach handlu AI. Efekty, które przetrwają staranne testy punktowe (point-in-time), takie jak anomalia niskiej zmienności, są interesujące właśnie z tego powodu.
FAQ
Czym jest błąd perspektywy czasowej w backtestingu?
Jest to wykorzystanie danych w symulowanej transakcji, które nie były dostępne w momencie, gdy transakcja zostałaby złożona. Typowe formy to decydowanie na świecy cenowej, w ramach której transakcja jest również realizowana, przesiewanie uniwersum spółek, które są notowane do dziś, oraz odczytywanie danych finansowych, które zostały zrewidowane po dacie do nich dołączonej.
Jak można wykryć błąd perspektywy czasowej?
Opóźnij każdy sygnał o jeden pełny okres i uruchom ponownie: wyciek zwykle się załamuje, podczas gdy prawdziwa przewaga słabnie łagodnie. Silniejszy test zastępuje wszystkie dane po każdym znaczniku czasu decyzji szumem i potwierdza, że każda decyzja pozostaje niezmieniona.
Czy błąd przetrwania (survivorship bias) to to samo co błąd perspektywy czasowej?
Są odrębne, ale podróżują razem. Błąd przetrwania to próbka zbudowana z nazw, które przetrwały, a wiedza o tym, które nazwy przetrwały, sama w sobie jest informacją z przyszłości, więc uniwersum składające się tylko z ocalałych jest wyciekiem perspektywy czasowej zlokalizowanym w danych, a nie w regule.
Dlaczego backtesty z błędem perspektywy czasowej wyglądają tak dobrze?
Wyciek wybiera wyniki, a nie je przewiduje. W powyższym panelu przesiewanie sesji pod kątem ich własnej ceny zamknięcia dało średni zysk sesyjny około 0.455 punktu procentowego powyżej wersji opóźnionej w 2016, co jest przewagą definicyjną, a nie wypracowaną.
Czym są dane punktowe (point-in-time data)?
Dane punktowe przechowują każdą wartość taką, jaką znano w każdej historycznej dacie, w tym oryginalną liczbę i każdą późniejszą rewizję, wraz z uniwersum papierów wartościowych, jakie istniało tego dnia. Jest to punkt odniesienia, którego backtest potrzebuje, aby wykazać, że jego argumenty istniały przed jego decyzjami.
Każda powyższa liczba to przechowywane, wersjonowane zapytanie na rzeczywistych świecach dziennych. Rozwiń SQL dowolnego panelu lub uruchom test opóźnienia na własnej regule w terminalu Strasmore.