Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Чи знаходить LLM альфа-фактори?

LLM може створити сотню альфа-факторів за годину. Дізнайтеся, як 240 випадкових факторів показали себе на десяти роках реальних цін і як тестувати найкращі.

LLM може цілими днями пропонувати альфа-фактори. Дайте потужній моделі словник даних і середовище для оцінювання — і до обіду вона напише сотню правдоподібних виразів факторів. Але складніше питання полягає в іншому: як взагалі зрозуміти, чи є якийсь із них реальним, якщо пошук, у результаті якого його отримано, перетворює шум на «переможців»?

Що таке альфа-фактор?

Фактор — це правило, яке перетворює ринкові дані на одне число для кожної акції на кожну дату. Зміна ціни за дванадцять місяців — це фактор. Так само як і співвідношення боргу до власного капіталу. Фактор стає стратегією, коли ви ранжуєте за ним певний набір акцій, купуєте верхню частину, продаєте нижню та регулярно ребалансуєте портфель. Альфа — це дохідність, що залишається після вирахування того, що в будь-якому разі забезпечила б звичайна ринкова експозиція.

Кандидатів оцінюють за допомогою коефіцієнта Шарпа: середню дохідність ділять на стандартне відхилення цієї дохідності та перераховують у річний показник. Це дохідність на одиницю коливань. Довгостроковий коефіцієнт Шарпа близько 1 для стратегії, що працює в реальному ринку, є гідним результатом. Про це варто пам’ятати щоразу, коли backtest показує значення 3.

Як насправді працює факторний аналіз за допомогою LLM

Кожен проєкт у цій сфері використовує один і той самий цикл.

  1. Модель записує факторні вирази невеликою мовою, яку може виконувати тестовий контур.
  2. Бектестер оцінює кожен вираз на фіксованій історії цін і фундаментальних показників.
  3. Вирази, що перевищують порогове значення оцінки, зберігаються. Решта відкидаються.
  4. Збережені вирази повертаються в контекст моделі як опрацьовані приклади, після чого цикл запускається знову.

Мультиагентні торгові системи розподіляють ці завдання між окремими ролями: одна пропонує вирази, інша їх тестує. Така інфраструктура справді корисна, а навички роботи з ринковими даними, потрібні AI-агенту — ті самі, що потрібні людині.

У цьому циклі немає нічого недоброчесного. Саме так і проводять дослідження: за допомогою пошуку. Проблема полягає в арифметиці, і вона виникає щойно крок 2 повторюється більше кількох разів.

Чому пошук альфа-факторів за допомогою LLM створює уявних переможців

Одна історія цін. Тисячі дешевих гіпотез. Кожну гіпотезу оцінюють на тій самій обмеженій вибірці, у якій значну роль відіграє випадковість. Якщо перевірити достатньо правил, деякі з них тісно підлаштуються під цю випадковість. Оцінка не показує, який саме тип відповідності ви отримали: правило, що збіглося з шумом, і правило, що відобразило ринок, матимуть однакове числове значення.

Ось нульова гіпотеза, перевірена

ЗапитОцінювання 240 випадкових факторів на реальних цінах: річний коефіцієнт Шарпа, січень 2016 — червень 2021
Точний SQL-код для кожного числа
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

разів. Кожен наведений нижче «фактор» — це підкидання монети: хеш тикера, місяця та номера випробування щомісяця ділить 40 великих американських акцій на дві половини, після чого стратегія відкриває довгу позицію в одній половині та коротку — в іншій. За конструкцією цей фактор не містить жодної інформації. Якщо оцінити його за фактичною дохідністю на кінець місяця в період із січня 2016 року до червня 2021 року, 240 випробувань розподіляються так.

У цьому і полягає суть розподілу. Жоден елемент на графіку нічого не прогнозує, але 1 випробувань потрапили до верхнього діапазону (1.2 and above), тобто до 0.4% усіх перевірених варіантів, а 1 — до нижнього діапазону (below -1.2). Дослідник, який провів одне вдале випробування й зупинився, отримав би графік і коефіцієнт Шарпа, але не мав би способу відрізнити їх від справжнього відкриття. Наведену тут дохідність розраховано від закриття одного місяця до закриття наступного; у матеріалі як вимірюють місячну дохідність описано цю арифметику.

Важливо, скільки варіантів ви перевірили

Результат бектесту без знаменника не дає повної картини. Ті самі 240 випробувань можна розглядати як пошук, що постійно розширюється: на кожному кроці найкращий результат порівнюється із середнім показником усіх варіантів, перевірених до цього моменту.

ЗапитНайкращий результат зростає зі збільшенням кількості перевірок: найкращий і середній коефіцієнти Шарпа за кількістю перевірок
Точний SQL-код для кожного числа
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

Поточний максимум може лише зростати — саме в цьому полягає пастка. Перше перевірене правило отримало результат 0.44. Після 240 випробувань найкращий показник становить 1.59, тоді як середнє значення за всіма випробуваннями дорівнює 0.01. Заголовковий показник покращився, хоча жодне окреме правило не стало кращим. Система, яка оцінює десять тисяч виразів, продовжує цю криву набагато правіше за межі, показані тут, а показник, який вона повідомляє, є її найвищою точкою.

Що дає період поза вибіркою для переможців

Стандартний захист — це відкладена вибірка: спочатку оцінити результати за один період, а потім повторно оцінити відібрані стратегії за пізніший період, якого пошук не використовував. Візьмімо дванадцять найкращих результатів підкидання монети з навчального періоду й застосуймо ті самі правила протягом наступних п’яти років — із липня 2021 року до червня 2026 року.

ЗапитДванадцять найкращих перевірок на навчальній вибірці, повторно оцінені на п’яти невикористаних роках (липень 2021 — червень 2026)
Точний SQL-код для кожного числа
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

Кожна пара стовпчиків відповідає одному правилу. Лівий стовпчик показує результат, завдяки якому правило потрапило до звіту. Правий стовпчик показує результат того самого правила за наступні п’ять років. Найвище оцінене випробування набрало 1.59 у навчальному періоді та -0.51 після нього; дванадцяте — 0.74, а потім 0.49.

Дванадцять правил — це сама по собі невелика вибірка. Якщо розподілити всі 240 правил на п’ять груп за результатом у навчальному періоді, а потім усереднити результат кожної групи у відкладеній вибірці, картина буде чіткішою.

ЗапитРанг на навчальній вибірці проти результату на відкладеній вибірці: 240 перевірок, поділених на п’яті
Точний SQL-код для кожного числа
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

У навчальному періоді групи розташовані від 0.66 у верхній частині до -0.63 у нижній. Це широкий і повністю впорядкований ряд, що закономірно, оскільки групи сформовано саме за цим показником. У відкладеній вибірці середні результати двох крайніх груп становлять 0.01 і 0.13. Ряд стає пласкішим. Відкладена вибірка — єдина частина процесу, яку не оптимізували під час пошуку. Саме тому її варто проводити обережно.

Захисні заходи, які справді працюють

Відкладена вибірка, яку використовують один раз. Кожен її перегляд перетворює її на навчальні дані. Тестування за принципом walk-forward, за якого вікно поступово зміщується, а кожен показник розраховується на даних після періоду підгонки, — це варіант, який витримує повторне використання.

Коригування на множинне тестування. Дефльований коефіцієнт Шарпа, запропонований Bailey та López de Prado у 2014 році, коригує спостережуваний коефіцієнт Шарпа з урахуванням кількості проведених випробувань, тривалості вибірки, асиметрії дохідності та товщини її хвостів розподілу. Якщо використати достовірну кількість випробувань, то заявлений коефіцієнт Шарпа, отриманий після пошуку серед десяти тисяч виразів, часто зводиться майже до нуля.

Журнал аудиту кожного випробуваного виразу, включно з відкинутими. Це ключовий елемент. Саме тому слово «аудитований» має значення в описі проєкту факторного дослідження. Для дефляції потрібна кількість випробувань. Якщо система фіксує лише переможців, вона знищує вхідні дані для власного коригування. До цього числа входять відкинуті чернетки, припинені перебори параметрів, повторні запуски самого дослідника та всі попередні версії коду оцінювання.

Перевірки витрат і упередження через використання майбутньої інформації до того, як показнику почнуть довіряти. Фактор, ранжований за фундаментальним показником із датою завантаження постачальником даних, а не з датою, коли ринок міг його побачити, чудово виглядає в бек-тесті, але погано працює під час торгівлі.

Розуміння слова «аудитопридатний»

Нові репозиторії в цій сфері з’являються майже щотижня, а проєкт із кількома десятками зірок є прототипом, а не підтвердженою історією результатів. Кількість зірок також зростає швидше, ніж змінюється код. Саме тому ця сторінка оцінює підхід, а не окремий проєкт. Ось що варто перевірити спочатку в будь-якому проєкті, який вам трапиться.

  • Чи записує він кожен кандидат разом із його формулою та оцінкою, із часовою позначкою, чи лише відібрані варіанти?
  • Чи забезпечує тестовий стенд використання відкладеної вибірки, чи це залежить від самодисципліни дослідника?
  • Чи вказано кількість випробувань поруч із кожною заявленою оцінкою?
  • Для якого ринку його створили? Бібліотека, налаштована на китайські A-shares, враховує щоденні цінові ліміти та заборону продавати акції, придбані в ту саму торгову сесію. Поведінка фактора за таких правил не переноситься на акції США.
  • Чи можна повторно запустити проєкт і відтворити ці показники? Зафіксуйте точний commit, який ви перевіряли, оскільки на цьому етапі проєкт може змінювати код оцінювання кожні вихідні.

Усе це не робить LLM непридатною для факторних досліджень. Генерування гіпотез справді є вузьким місцем, і моделі добре з цим справляються. Змінюється лише те, на кого припадає основний тягар: потрібно вести облік кількості використаних гіпотез. Перш ніж будь-яка з них потрапить до реального стакана заявок, паперова торгівля показує, наскільки результати backtest відрізняються від фактичного виконання угоди.

Поширені запитання про alpha-фактори, згенеровані LLM

Чи може LLM знаходити alpha-фактори?

LLM може запропонувати їх тисячами, але пропозиція ще не є знахідкою. Твердження виникає на етапі оцінювання, а оцінка, отримана внаслідок широкого пошуку, містить проблему відбору, яку сама оцінка не виявляє. Насамперед перевіряйте дисципліну роботи з holdout-вибіркою та зафіксовану кількість випробувань, а вже потім — сам вираз.

Що таке скоригований коефіцієнт Sharpe?

Це коригування, яке перетворює спостережуваний коефіцієнт Sharpe на ймовірність того, що пошук такого масштабу дав би цей результат за відсутності реальної переваги. Bailey і López de Prado опублікували цей показник у 2014 році. Його ключовим вхідним параметром є кількість випробувань. Саме це число не може надати неаудитований дослідницький цикл.

Скільки бек-тестів — це забагато?

Порогового значення немає. Є лише коригування, яке потрібно застосовувати. Один бек-тест із результатом 1.0 і десять тисяч бек-тестів, серед яких найкращий результат становить 1.0, — це різні твердження про реальність. У наведеному вище експерименті з підкиданням монети було отримано 1.59 у межах 240 випробувань, хоча в даних не було жодної закономірності.

Чому опубліковані фактори слабшають після публікації?

Академічні дослідження відстежували ослаблення опублікованих аномалій протягом років після їх появи. Одним із запропонованих пояснень є переповненість угодами, іншим — перенавчання початкового результату на власній вибірці. На графіку обидва механізми мають однакову форму. Гіпотеза ефективного ринку пояснює перший механізм, а наведені вище випробування демонструють другий.


Кожна панель тут є збереженим запитом до реальних цін на кінець місяця, а SQL-код відкритий під нею. Скопіюйте будь-який запит, збільште кількість випробувань і спостерігайте, як найкращий результат зростає в терміналі Strasmore.

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