Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Czy LLM potrafi znaleźć czynniki alpha?

LLM może napisać sto czynników alpha w godzinę. Sprawdzono, jak 240 losowych czynników wypada przez dziesięć lat na rzeczywistych cenach i jak testować najlepsze.

LLM może generować czynniki alpha przez cały dzień. Wystarczy udostępnić odpowiednio przygotowanemu modelowi słownik danych i narzędzie do oceny, a przed południem napisze sto wiarygodnie wyglądających wyrażeń czynnikowych. Trudniejsze pytanie brzmi: jak kiedykolwiek ustalić, czy któryś z nich jest rzeczywisty, skoro wyszukiwanie, które go wyłoniło, służy do produkowania zwycięzców z szumu?

Czym jest czynnik alpha?

Czynnik to reguła, która przekształca dane rynkowe w jedną liczbę dla każdej akcji w każdym dniu. Dwunastomiesięczna zmiana ceny jest czynnikiem. Podobnie jak relacja zadłużenia do kapitału własnego. Czynnik staje się strategią, gdy na jego podstawie porządkuje się określony uniwersum, kupuje górny segment, sprzedaje dolny segment i dokonuje rebalansowania według ustalonego harmonogramu. Alpha to stopa zwrotu pozostała po odjęciu tego, co zapewniłaby sama ekspozycja na szeroki rynek.

Kandydatów ocenia się za pomocą wskaźnika Sharpe’a: średnią stopę zwrotu dzieli się przez odchylenie standardowe tej stopy zwrotu i skaluje do ujęcia rocznego. Jest to stopa zwrotu przypadająca na jednostkę zmienności. Długoterminowy Sharpe w pobliżu 1 dla aktywnej strategii jest przyzwoity. Warto o tym pamiętać przy kolejnym teście historycznym pokazującym wartość 3.

Jak w praktyce przebiega wyszukiwanie czynników przez LLM

Każdy projekt w tym obszarze realizuje podobną pętlę.

  1. Model tworzy wyrażenia czynnikowe w niewielkim języku, który może ocenić narzędzie testujące.
  2. Backtester ocenia każde wyrażenie na podstawie ustalonej historii cen i danych fundamentalnych.
  3. Wyrażenia przekraczające ustalony próg wyniku zostają zachowane. Pozostałe są odrzucane.
  4. Zachowane wyrażenia wracają do kontekstu modelu jako przykłady, a pętla uruchamia się ponownie.

Wieloczynnikowe systemy transakcyjne rozdzielają te zadania między odrębne role: jedna generuje propozycje, a druga je testuje. Infrastruktura jest rzeczywiście użyteczna, a umiejętności dotyczące danych rynkowych potrzebne agentowi AI są takie same jak te potrzebne człowiekowi.

W tej pętli nie ma nic nieuczciwego. Wyszukiwanie jest częścią procesu badawczego. Problem ma jednak charakter arytmetyczny i pojawia się natychmiast, gdy krok 2 zostanie wykonany więcej niż kilka razy.

Dlaczego wyszukiwanie czynników alpha przez LLM produkuje pozornych zwycięzców

Jedna historia cenowa. Tysiące tanich hipotez. Każda hipoteza jest oceniana na podstawie tej samej ograniczonej próby, a próba ta zawiera dużo przypadkowości. Przy dostatecznej liczbie reguł niektóre z nich dobrze dopasują się do tego przypadku. Wynik nie pozwala ustalić, jaki rodzaj dopasowania uzyskano, ponieważ reguła dopasowana do szumu i reguła dopasowana do rynku generują tę samą liczbę.

Poniżej przedstawiono hipotezę zerową, losowaną 240 razy. Każdy „czynnik” jest rzutem monetą: skrót wartości tickera, miesiąca i numeru próby dzieli 40 dużych spółek amerykańskich na dwie połowy w każdym miesiącu, a strategia zajmuje długą pozycję w jednej połowie i krótką w drugiej. Z założenia nie ma w tym żadnej informacji. Po ocenie na podstawie rzeczywistych stóp zwrotu na koniec miesiąca od stycznia 2016 r. do czerwca 2021 r. 240 prób rozkłada się następująco.

Zapytanie240 losowych czynników typu „orzeł czy reszka” ocenionych na rzeczywistych cenach: annualizowany wskaźnik Sharpe’a, styczeń 2016–czerwiec 2021
Dokładny kod SQL dla każdej liczby
WITH month_end AS (
    SELECT ticker,
           toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
           argMax(toFloat64(close), window_start) AS close_px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('AAPL','ADBE','AMZN','BA','CAT','COST','CRM','CSCO','CVX','DE',
                     'DUK','GE','GOOGL','HD','HON','IBM','INTC','JNJ','JPM','KO',
                     'LMT','MCD','MMM','MRK','MSFT','NKE','NVDA','ORCL','PEP','PFE',
                     'PG','QCOM','SO','T','TGT','TXN','UNP','VZ','WMT','XOM')
      AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2015-12-01')
      AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2021-06-30')
      AND toDayOfMonth(toTimeZone(window_start, 'America/New_York')) >= 22
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, month_start
),
lagged AS (
    SELECT ticker,
           month_start,
           close_px,
           lagInFrame(close_px) OVER (PARTITION BY ticker ORDER BY month_start
                                      ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
    FROM month_end
),
monthly_return AS (
    SELECT ticker, month_start, close_px / prev_px - 1 AS ret
    FROM lagged
    WHERE prev_px > 0
      AND month_start >= toDate('2016-01-01')
),
trial AS (
    SELECT arrayJoin(range(1, 241)) AS n
),
factor_month AS (
    SELECT t.n AS trial_id,
           m.month_start AS month_start,
           avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1)
         - avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) AS long_short_ret
    FROM monthly_return AS m
    CROSS JOIN trial AS t
    GROUP BY trial_id, month_start
    HAVING countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1) > 0
       AND countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) > 0
),
scored AS (
    SELECT trial_id,
           avg(long_short_ret) / stddevSamp(long_short_ret) * sqrt(12) AS sharpe
    FROM factor_month
    GROUP BY trial_id
    HAVING stddevSamp(long_short_ret) > 0
)
SELECT multiIf(sharpe < -1.2, 'below -1.2',
               sharpe < -0.8, '-1.2 to -0.8',
               sharpe < -0.4, '-0.8 to -0.4',
               sharpe <  0.0, '-0.4 to 0.0',
               sharpe <  0.4, '0.0 to 0.4',
               sharpe <  0.8, '0.4 to 0.8',
               sharpe <  1.2, '0.8 to 1.2',
               '1.2 and above') AS sharpe_bucket,
       count() AS factor_count,
       round(100 * count() / 240, 1) AS share_pct
FROM scored
GROUP BY sharpe_bucket
ORDER BY min(sharpe)
Run this yourself

Rozkład wyników jest tu najważniejszy. Żaden element wykresu niczego nie prognozuje, a mimo to 1 prób znalazło się w najwyższym przedziale (1.2 and above), 0.4% całego wyszukiwania, a 1 w najniższym przedziale (below -1.2). Badacz, który przeprowadziłby jedną szczęśliwą próbę i na tym zakończył, miałby wykres oraz wskaźnik Sharpe’a, ale nie miałby sposobu na odróżnienie odkrycia od przypadku. Stopy zwrotu są tu liczone od zamknięcia na koniec jednego miesiąca do zamknięcia na koniec kolejnego; sposób pomiaru miesięcznych stóp zwrotu opisuje te obliczenia.

Liczy się liczba przeprowadzonych prób

Samodzielnie prezentowany wynik testu historycznego nie zawiera mianownika. Te same 240 prób można odczytać jako stale poszerzane wyszukiwanie: na każdym etapie pokazuje się najlepszy wynik obok średniej ze wszystkich dotychczas przetestowanych reguł.

ZapytanieNajlepszy wynik rośnie wraz z rozmiarem wyszukiwania: najlepszy i średni wskaźnik Sharpe’a według liczby przeprowadzonych prób
Dokładny kod SQL dla każdej liczby
WITH month_end AS (
    SELECT ticker,
           toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
           argMax(toFloat64(close), window_start) AS close_px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('AAPL','ADBE','AMZN','BA','CAT','COST','CRM','CSCO','CVX','DE',
                     'DUK','GE','GOOGL','HD','HON','IBM','INTC','JNJ','JPM','KO',
                     'LMT','MCD','MMM','MRK','MSFT','NKE','NVDA','ORCL','PEP','PFE',
                     'PG','QCOM','SO','T','TGT','TXN','UNP','VZ','WMT','XOM')
      AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2015-12-01')
      AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2021-06-30')
      AND toDayOfMonth(toTimeZone(window_start, 'America/New_York')) >= 22
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, month_start
),
lagged AS (
    SELECT ticker,
           month_start,
           close_px,
           lagInFrame(close_px) OVER (PARTITION BY ticker ORDER BY month_start
                                      ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
    FROM month_end
),
monthly_return AS (
    SELECT ticker, month_start, close_px / prev_px - 1 AS ret
    FROM lagged
    WHERE prev_px > 0
      AND month_start >= toDate('2016-01-01')
),
trial AS (
    SELECT arrayJoin(range(1, 241)) AS n
),
factor_month AS (
    SELECT t.n AS trial_id,
           m.month_start AS month_start,
           avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1)
         - avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) AS long_short_ret
    FROM monthly_return AS m
    CROSS JOIN trial AS t
    GROUP BY trial_id, month_start
    HAVING countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1) > 0
       AND countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) > 0
),
scored AS (
    SELECT trial_id,
           avg(long_short_ret) / stddevSamp(long_short_ret) * sqrt(12) AS sharpe
    FROM factor_month
    GROUP BY trial_id
    HAVING stddevSamp(long_short_ret) > 0
),
ladder AS (
    SELECT arrayJoin([1, 2, 5, 10, 25, 50, 100, 160, 240]) AS n
)
SELECT l.n AS factors_tried,
       round(max(s.sharpe), 2) AS best_sharpe,
       round(avg(s.sharpe), 2) AS average_sharpe
FROM ladder AS l
CROSS JOIN scored AS s
WHERE s.trial_id <= l.n
GROUP BY factors_tried
ORDER BY factors_tried
Run this yourself

Maksimum narastające może tylko rosnąć, co dokładnie tworzy tę pułapkę. Pierwsza przetestowana reguła uzyskała wynik 0.44. Po 240 próbach najlepszy wynik wynosi 1.59, podczas gdy średnia wszystkich wyników to 0.01. Nagłówek poprawił się bez poprawy choćby jednej reguły. Narzędzie oceniające dziesięć tysięcy wyrażeń uruchamia tę krzywą znacznie dalej niż pokazano tutaj, a raportowana liczba jest jej najwyższym punktem.

Co okres poza próbą robi ze zwycięzcami

Standardową metodą obrony jest próba odłożona: najpierw przeprowadza się ocenę dla jednego okresu, a następnie ponownie ocenia się zwycięzców w późniejszym okresie, którego wyszukiwanie nie wykorzystywało. Dwanaście najlepszych rzutów monetą z okresu treningowego poddano działaniu tych samych reguł w ciągu pięciu kolejnych lat, od lipca 2021 r. do czerwca 2026 r.

ZapytanieDwanaście najlepszych prób w próbie treningowej, ponownie ocenionych na pięciu niewykorzystanych latach (lipiec 2021–czerwiec 2026)
Dokładny kod SQL dla każdej liczby
WITH month_end AS (
    SELECT ticker,
           toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
           argMax(toFloat64(close), window_start) AS close_px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('AAPL','ADBE','AMZN','BA','CAT','COST','CRM','CSCO','CVX','DE',
                     'DUK','GE','GOOGL','HD','HON','IBM','INTC','JNJ','JPM','KO',
                     'LMT','MCD','MMM','MRK','MSFT','NKE','NVDA','ORCL','PEP','PFE',
                     'PG','QCOM','SO','T','TGT','TXN','UNP','VZ','WMT','XOM')
      AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2015-12-01')
      AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-06-30')
      AND toDayOfMonth(toTimeZone(window_start, 'America/New_York')) >= 22
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, month_start
),
lagged AS (
    SELECT ticker,
           month_start,
           close_px,
           lagInFrame(close_px) OVER (PARTITION BY ticker ORDER BY month_start
                                      ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
    FROM month_end
),
monthly_return AS (
    SELECT ticker, month_start, close_px / prev_px - 1 AS ret
    FROM lagged
    WHERE prev_px > 0
      AND month_start >= toDate('2016-01-01')
),
trial AS (
    SELECT arrayJoin(range(1, 241)) AS n
),
factor_month AS (
    SELECT t.n AS trial_id,
           m.month_start AS month_start,
           avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1)
         - avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) AS long_short_ret
    FROM monthly_return AS m
    CROSS JOIN trial AS t
    GROUP BY trial_id, month_start
    HAVING countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1) > 0
       AND countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) > 0
),
scored AS (
    SELECT trial_id,
           avgIf(long_short_ret, month_start <  toDate('2021-07-01'))
             / stddevSampIf(long_short_ret, month_start <  toDate('2021-07-01')) * sqrt(12) AS in_sample_sharpe,
           avgIf(long_short_ret, month_start >= toDate('2021-07-01'))
             / stddevSampIf(long_short_ret, month_start >= toDate('2021-07-01')) * sqrt(12) AS out_of_sample_sharpe
    FROM factor_month
    GROUP BY trial_id
    HAVING countIf(month_start <  toDate('2021-07-01')) >= 24
       AND countIf(month_start >= toDate('2021-07-01')) >= 24
)
SELECT concat('trial ', toString(trial_id)) AS factor_label,
       round(in_sample_sharpe, 2) AS in_sample_sharpe,
       round(out_of_sample_sharpe, 2) AS out_of_sample_sharpe
FROM scored
ORDER BY in_sample_sharpe DESC
LIMIT 12
Run this yourself

Każda para słupków przedstawia jedną regułę. Lewy słupek pokazuje wynik, który zapewnił jej miejsce w raporcie. Prawy słupek pokazuje wynik tej samej reguły w kolejnych pięciu latach. Próba zajmująca pierwsze miejsce uzyskała w treningu 1.59, a później -0.51; próba zajmująca dwunaste miejsce uzyskała 0.74, a następnie 0.49.

Dwanaście reguł to również niewielka próba. Posortowanie wszystkich 240 prób według wyniku treningowego i podzielenie ich na piąte części, a następnie obliczenie średniego wyniku dla okresu odłożonego w każdej części, daje bardziej przejrzysty obraz.

ZapytanieRanking w próbie treningowej a wynik próby holdout: 240 prób podzielonych na kwintyle
Dokładny kod SQL dla każdej liczby
WITH month_end AS (
    SELECT ticker,
           toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
           argMax(toFloat64(close), window_start) AS close_px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('AAPL','ADBE','AMZN','BA','CAT','COST','CRM','CSCO','CVX','DE',
                     'DUK','GE','GOOGL','HD','HON','IBM','INTC','JNJ','JPM','KO',
                     'LMT','MCD','MMM','MRK','MSFT','NKE','NVDA','ORCL','PEP','PFE',
                     'PG','QCOM','SO','T','TGT','TXN','UNP','VZ','WMT','XOM')
      AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2015-12-01')
      AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-06-30')
      AND toDayOfMonth(toTimeZone(window_start, 'America/New_York')) >= 22
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, month_start
),
lagged AS (
    SELECT ticker,
           month_start,
           close_px,
           lagInFrame(close_px) OVER (PARTITION BY ticker ORDER BY month_start
                                      ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_px
    FROM month_end
),
monthly_return AS (
    SELECT ticker, month_start, close_px / prev_px - 1 AS ret
    FROM lagged
    WHERE prev_px > 0
      AND month_start >= toDate('2016-01-01')
),
trial AS (
    SELECT arrayJoin(range(1, 241)) AS n
),
factor_month AS (
    SELECT t.n AS trial_id,
           m.month_start AS month_start,
           avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1)
         - avgIf(m.ret, bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) AS long_short_ret
    FROM monthly_return AS m
    CROSS JOIN trial AS t
    GROUP BY trial_id, month_start
    HAVING countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 1) > 0
       AND countIf(bitAnd(cityHash64(m.ticker, toString(m.month_start), t.n), 1) = 0) > 0
),
scored AS (
    SELECT trial_id,
           avgIf(long_short_ret, month_start <  toDate('2021-07-01'))
             / stddevSampIf(long_short_ret, month_start <  toDate('2021-07-01')) * sqrt(12) AS in_sample_sharpe,
           avgIf(long_short_ret, month_start >= toDate('2021-07-01'))
             / stddevSampIf(long_short_ret, month_start >= toDate('2021-07-01')) * sqrt(12) AS out_of_sample_sharpe
    FROM factor_month
    GROUP BY trial_id
    HAVING countIf(month_start <  toDate('2021-07-01')) >= 24
       AND countIf(month_start >= toDate('2021-07-01')) >= 24
),
ranked AS (
    SELECT trial_id,
           in_sample_sharpe,
           out_of_sample_sharpe,
           row_number() OVER (ORDER BY in_sample_sharpe DESC) AS in_sample_rank
    FROM scored
)
SELECT multiIf(in_sample_rank <=  48, 'best fifth in training',
               in_sample_rank <=  96, 'second fifth',
               in_sample_rank <= 144, 'middle fifth',
               in_sample_rank <= 192, 'fourth fifth',
               'worst fifth in training') AS training_group,
       round(avg(in_sample_sharpe), 2) AS avg_in_sample_sharpe,
       round(avg(out_of_sample_sharpe), 2) AS avg_out_of_sample_sharpe
FROM ranked
GROUP BY training_group
ORDER BY min(in_sample_rank)
Run this yourself

W treningu grupy rozciągają się od 0.66 w górnej części do -0.63 w dolnej, tworząc szeroką i całkowicie uporządkowaną drabinę. Jest to gwarantowane, ponieważ grupy wyznaczono właśnie na podstawie tego wyniku. W okresie odłożonym te same dwa krańce osiągają średnio 0.01 i 0.13. Drabina się spłaszcza. Próba odłożona jest jedyną częścią procesu, której nie zoptymalizowano, dlatego warto wykorzystać ją oszczędnie.

Metody ochronne, które rzeczywiście działają

Próba odłożona wykorzystywana tylko raz. Każde spojrzenie na jej wynik przekształca ją w dane treningowe. Testowanie kroczące, w którym okno przesuwa się, a każdy wynik pochodzi z danych późniejszych niż dane wykorzystane do dopasowania, jest wersją odporną na wielokrotne użycie.

Korekta uwzględniająca wielokrotne testowanie. Skorygowany wskaźnik Sharpe’a, wprowadzony przez Baileya i Lópeza de Prado w 2014 r., obniża obserwowany Sharpe w zależności od liczby przeprowadzonych prób, długości próby, asymetrii stóp zwrotu oraz grubości ich ogonów. Przy uczciwie podanej liczbie prób główny Sharpe z wyszukiwania obejmującego dziesięć tysięcy wyrażeń często spada do zera.

Ścieżka audytowa obejmująca każde przetestowane wyrażenie, również te odrzucone. Jest to element kluczowy. Dlatego słowo „audytowalny” jest najważniejsze w opisie projektu badawczego dotyczącego czynników. Korekta wymaga liczby prób. Proces, który rejestruje wyłącznie zwycięzców, niszczy dane wejściowe do własnej korekty. Odrzucone wersje robocze, porzucone przeszukiwania parametrów, ponowne uruchomienia badacza i każda wcześniejsza wersja kodu oceny zwiększają tę liczbę.

Kontrole kosztów i obciążenia wyprzedzeniem informacji przed uznaniem wyniku za wiarygodny. Czynnik uszeregowany na podstawie danych fundamentalnych opatrzonych datą wprowadzenia przez dostawcę, zamiast datą, w której rynek mógł je poznać, daje świetny wynik w teście historycznym i słaby wynik w rzeczywistym obrocie.

Jak rozumieć słowo „audytowalny”

Nowe repozytoria w tym obszarze pojawiają się niemal co tydzień, a projekt z kilkudziesięcioma gwiazdkami jest prototypem, a nie historią wyników. Liczba gwiazdek również rośnie szybciej niż kod, dlatego niniejsza strona ocenia wzorzec działania, a nie konkretny projekt. Oto, co należy sprawdzić w pierwszej kolejności w każdym napotkanym projekcie.

  • Czy rejestrowany jest każdy kandydat wraz z jego wyrażeniem i wynikiem, opatrzony znacznikiem czasu, czy tylko zachowane kandydaty?
  • Czy próba odłożona jest egzekwowana przez narzędzie, czy przez samodyscyplinę badacza?
  • Czy obok każdego raportowanego wyniku podawana jest liczba prób?
  • Dla jakiego rynku projekt został zbudowany? Biblioteka dostrojona do chińskich akcji typu A uwzględnia dzienne limity zmian cen oraz ograniczenie dotyczące sprzedaży akcji kupionych podczas tej samej sesji. Zachowanie czynnika w takich warunkach nie przenosi się na akcje amerykańskie.
  • Czy można ponownie uruchomić projekt i odtworzyć wyniki? Należy przypiąć dokładny commit, który został przeanalizowany, ponieważ projekt na tym etapie może zmieniać kod oceny między kolejnymi weekendami.

Żadna z tych kwestii nie czyni LLM bezużytecznym w badaniach czynników. Generowanie hipotez jest rzeczywistym wąskim gardłem, a modele dobrze sobie z tym radzą. Zmienia się natomiast miejsce, w którym spoczywa ciężar dowodu: przypada ono na ewidencję liczby wykorzystanych hipotez. Zanim którakolwiek z nich trafi do rzeczywistego arkusza zleceń, handel papierowy pokazuje różnicę między testem historycznym a realizacją transakcji.

FAQ dotyczące czynników alpha generowanych przez LLM

Czy LLM może znaleźć czynniki alpha?

Może generować je tysiącami, ale propozycja nie jest odkryciem. Twierdzenie zostaje sformułowane na etapie oceny, a wynik uzyskany w szerokim wyszukiwaniu podlega problemowi selekcji, którego sam wynik nie potrafi wykryć. Przed samym wyrażeniem należy ocenić rygor stosowania próby odłożonej i rejestrowania liczby prób.

Czym jest skorygowany wskaźnik Sharpe’a?

Jest to korekta, która przekształca obserwowany wskaźnik Sharpe’a w prawdopodobieństwo, że wyszukiwanie o takiej skali wygenerowałoby ten wynik bez rzeczywistej przewagi. Bailey i López de Prado opublikowali tę metodę w 2014 r. Jej kluczowym parametrem jest liczba prób, czyli dokładnie ta liczba, której nie może dostarczyć nieaudytowalny proces badawczy.

Ile testów historycznych to zbyt wiele?

Nie ma ustalonego progu. Należy stosować odpowiednią korektę. Jeden test historyczny z wynikiem 1,0 i dziesięć tysięcy testów historycznych, z których najlepszy wynik wynosi 1,0, są odmiennymi twierdzeniami na temat rzeczywistości. Powyższe rzuty monetą osiągnęły 1.59 w ramach 240 prób, mimo całkowitego braku informacji w danych.

Dlaczego opublikowane czynniki słabną po publikacji?

Badania akademickie dokumentują osłabianie się opublikowanych anomalii w kolejnych latach. Jednym z proponowanych mechanizmów jest zatłoczenie transakcji, a innym nadmierne dopasowanie pierwotnego wyniku do własnej próby. Oba mechanizmy tworzą na wykresie podobny kształt. Hipoteza efektywności rynku opisuje pierwszy mechanizm, a przedstawione próby pokazują drugi.


Każdy wykres jest zapisanym zapytaniem dotyczącym rzeczywistych cen zamknięcia na koniec miesiąca, a kod SQL znajduje się pod nim. Należy skopiować zapytanie, zwiększyć liczbę prób i obserwować, jak najlepszy wynik rośnie na terminalu Strasmore.

#llm#factor research#overfitting#multiple testing#quant