Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

LLM은 알파 팩터를 찾을 수 있는가

LLM이 한 시간에 백 개의 알파 팩터를 만들 수 있어도 충분하지 않습니다. 240개 팩터를 10년 실거래 가격으로 검증하고 생존자를 시험하는 방법을 설명합니다.

LLM은 하루 종일 알파 팩터를 제안할 수 있습니다. 유능한 모델에 데이터 사전과 스코어링 하네스를 제공하면, 점심 전까지 그럴듯한 팩터 식을 백 개 작성할 수 있습니다. 더 어려운 문제는 그 아래에 있습니다. 노이즈에서 승자를 만들어내는 검색 과정에서 나온 결과가 실제로 유효한지 어떻게 알 수 있겠습니까?

알파 팩터란 무엇인가?

팩터는 시장 데이터를 각 종목과 각 날짜마다 하나의 숫자로 변환하는 규칙입니다. 12개월 가격 변동률은 팩터입니다. 부채비율도 팩터입니다. 팩터로 유니버스를 순위화하고, 상위 구간을 매수하며, 하위 구간을 매도하고, 정해진 일정에 따라 리밸런싱하면 팩터는 전략이 됩니다. 알파는 단순한 시장 익스포저만으로 얻을 수 있었던 수익률을 차감한 뒤 남는 수익률입니다.

후보 팩터는 Sharpe ratio(샤프 비율)로 평가합니다. 평균 수익률을 해당 수익률의 표준편차로 나눈 뒤 연율화한 값입니다. 변동성 단위당 수익률을 의미합니다. 실거래 전략에서 장기 Sharpe가 1에 가까우면 준수한 수준입니다. 따라서 백테스트가 3이라는 수치를 제시할 때 이를 염두에 둘 필요가 있습니다.

LLM 팩터 리서치는 실제로 어떻게 작동하는가

이 분야의 프로젝트는 모두 같은 반복 과정을 변형해 사용합니다.

  1. 모델이 하네스에서 평가할 수 있는 소규모 언어로 팩터 식을 작성합니다.
  2. 백테스터가 고정된 가격 및 펀더멘털 이력에 대해 각 식을 평가합니다.
  3. 기준 점수 이상인 식은 유지하고 나머지는 폐기합니다.
  4. 유지된 식이 작업 예시로 모델의 컨텍스트에 다시 입력되고, 과정이 반복됩니다.

다중 에이전트 트레이딩 시스템은 이러한 작업을 별도 역할로 나눕니다. 한 역할은 제안하고 다른 역할은 검증합니다. 이 구조는 실제로 유용합니다. 또한 AI 에이전트에 필요한 시장 데이터 기술은 사람이 갖춰야 할 기술과 같습니다.

이 과정 자체에는 부정한 부분이 없습니다. 검색은 리서치를 수행하는 방식입니다. 문제는 산술에 있습니다. 2단계를 몇 번 이상 반복하는 순간 문제가 나타납니다.

LLM 알파 팩터 검색이 승자를 만들어내는 이유

하나의 가격 이력과 수천 개의 저비용 가설이 있습니다. 모든 가설은 동일한 유한 표본에 대해 평가됩니다. 그리고 그 표본에는 상당한 운이 섞여 있습니다. 규칙을 충분히 많이 테스트하면 일부 규칙은 그 운에 밀착해 적합됩니다. 점수만으로는 어떤 종류의 적합인지 구분할 수 없습니다. 노이즈에 맞은 규칙과 시장을 포착한 규칙이 동일한 숫자로 표시되기 때문입니다.

귀무가설을 240회 추출했습니다. 아래의 각 “팩터”는 동전 던지기와 같습니다. 티커, 월, 시도 번호를 해시한 값으로 미국 대형주 40개를 매월 두 그룹으로 나눕니다. 한 그룹은 롱으로 보유하고 다른 그룹은 숏으로 보유합니다. 설계상 정보는 전혀 없습니다. 2016년 1월부터 2021년 6월까지의 실제 월말 수익률로 평가한 240회 시도는 다음과 같이 분포합니다.

조회실제 가격으로 평가한 240개 동전 던지기 요인의 연환산 Sharpe: 2016년 1월~2021년 6월
모든 수치 뒤에 숨겨진 정확한 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

분포의 폭이 핵심입니다. 해당 차트의 어떤 요소도 예측력을 갖지 않지만, 1회의 시도가 상위 구간(1.2 and above)에 들어갔습니다. 이는 전체 검색의 0.4%입니다. 1회는 하위 구간(below -1.2)에 들어갔습니다. 운이 좋은 시도 하나를 실행하고 중단한 연구자는 차트와 Sharpe ratio를 보유하게 됩니다. 그러나 그것이 발견인지 우연인지 구분할 방법은 없습니다. 여기서 수익률은 월말 종가부터 다음 월말 종가까지의 수익률입니다. 월간 수익률을 측정하는 방법에서 그 산식을 설명합니다.

중요한 숫자는 시도한 횟수다

백테스트 결과만 단독으로 보고하면 분모가 빠집니다. 동일한 240회 시도를 검색 범위가 계속 넓어지는 과정으로 읽어 보겠습니다. 각 단계에서 지금까지 시도한 모든 결과의 평균과 함께 현재까지의 최고 점수를 표시합니다.

조회검색 규모가 커질수록 상승하는 최고 점수: 실행한 시행 횟수별 최고 및 평균 Sharpe
모든 수치 뒤에 숨겨진 정확한 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이었습니다. 규칙 하나도 개선되지 않았지만 헤드라인 수치는 좋아졌습니다. 만 개의 식을 평가하는 하네스는 이 차트보다 훨씬 오른쪽까지 같은 곡선을 그립니다. 하네스가 보고하는 숫자는 그 곡선의 꼭대기입니다.

홀드아웃 기간이 승자에게 미치는 영향

표준적인 방어 방법은 홀드아웃입니다. 한 기간에 대해 점수를 계산한 뒤, 검색이 전혀 사용하지 않은 이후 기간에 생존자들을 다시 평가합니다. 학습 기간에서 가장 높은 점수를 받은 동전 던지기 12개를 선택하고, 이후 5년인 2021년 7월부터 2026년 6월까지 동일한 규칙을 적용해 보겠습니다.

조회학습에서 선정한 상위 12개 시행을 미사용 5년(2021년 7월~2026년 6월)에 재평가
모든 수치 뒤에 숨겨진 정확한 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

각 막대 쌍은 하나의 규칙을 나타냅니다. 왼쪽 막대는 해당 규칙이 보고서에 포함되도록 만든 점수입니다. 오른쪽 막대는 이후 5년 동안 동일한 규칙으로 얻은 점수입니다. 1위 시도는 학습 기간에 1.59를 기록했고 이후에는 -0.51을 기록했습니다. 12위 시도는 각각 0.740.49를 기록했습니다.

규칙 12개만으로도 별도의 표본 문제가 발생합니다. 240개 규칙을 학습 점수에 따라 5개 그룹으로 나눈 뒤 각 그룹의 홀드아웃 점수를 평균하면 더 명확한 결과를 얻을 수 있습니다.

조회학습 순위와 홀드아웃 결과 비교: 240개 시행을 5분위로 구분
모든 수치 뒤에 숨겨진 정확한 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.010.13입니다. 사다리는 평탄해집니다. 홀드아웃은 파이프라인에서 최적화의 대상이 되지 않은 유일한 부분입니다. 따라서 신중하게 사용해야 할 가치가 있습니다.

실제로 효과가 있는 방어 수단

한 번만 사용하는 홀드아웃. 홀드아웃을 확인할 때마다 해당 데이터는 학습 데이터로 변합니다. 윈도우를 이동시키고 각 점수를 적합 이후의 데이터에서 계산하는 워크포워드 테스트가 반복 사용을 견딜 수 있는 방식입니다.

다중 검정 조정. Bailey와 López de Prado가 2014년에 소개한 deflated Sharpe ratio(조정 Sharpe ratio)는 실행한 시도 횟수, 표본 기간, 수익률의 왜도, 꼬리의 두께를 고려해 관측된 Sharpe를 할인합니다. 실제 시도 횟수를 입력하면, 만 개의 식을 검색한 결과로 제시된 Sharpe는 종종 아무 의미도 없는 수준으로 낮아집니다.

폐기한 식을 포함해 시도한 모든 식을 기록한 감사 추적. 이것이 핵심입니다. 팩터 리서치 프로젝트 설명에서 “감사 가능”이라는 단어가 중요한 이유이기도 합니다. 조정에는 시도 횟수가 필요합니다. 승자만 기록하는 파이프라인은 자체 조정에 필요한 입력값을 파괴합니다. 폐기한 초안, 중단한 매개변수 탐색, 연구자가 직접 다시 시작한 모든 과정, 이전 버전의 스코어링 코드까지 모두 해당 숫자에 포함됩니다.

점수를 신뢰하기 전에 비용과 룩어헤드 편향을 점검해야 합니다. 시장이 해당 정보를 볼 수 있었던 날짜가 아니라 공급업체가 펀더멘털 데이터를 적재한 날짜가 기록된 필드로 팩터 순위를 매기면 백테스트는 훌륭하게 보일 수 있습니다. 그러나 실제 거래 성과는 나빠집니다.

“감사 가능”이라는 단어를 읽는 방법

이 분야의 신규 저장소는 대부분의 주에 새로 등장합니다. 별이 수십 개 있는 프로젝트는 실적이라기보다 프로토타입에 가깝습니다. 별 개수는 코드보다 빠르게 증가하기도 합니다. 따라서 이 페이지는 특정 프로젝트가 아니라 패턴을 평가합니다. 다음 항목부터 확인해야 합니다.

  • 모든 후보의 식과 점수를 타임스탬프와 함께 기록하는가, 아니면 유지된 결과만 기록하는가?
  • 홀드아웃이 하네스에 의해 강제되는가, 아니면 연구자의 자기 통제에 의존하는가?
  • 보고된 점수 옆에 시도 횟수가 함께 표시되는가?
  • 어떤 시장을 대상으로 구축되었는가? 중국 A주에 맞춰진 라이브러리는 일일 가격 제한과 당일 매수한 주식의 매도 제한을 전제로 합니다. 이러한 규칙 아래에서의 팩터 행태는 미국 주식에 그대로 적용되지 않습니다.
  • 다시 실행해 수치를 재현할 수 있는가? 이 단계의 프로젝트는 주말 사이에도 스코어링 코드를 수정할 수 있으므로, 확인한 정확한 커밋을 고정해야 합니다.

이러한 점이 LLM을 팩터 리서치에서 무용하게 만드는 것은 아닙니다. 가설 생성은 실제 병목이며, 모델은 이 작업을 잘 수행합니다. 달라지는 것은 부담의 위치입니다. 얼마나 많은 가설을 소진했는지 기록하는 회계가 중요해집니다. 실제 주문장에 접속하기 전에 페이퍼 트레이딩을 통해 백테스트와 체결 사이의 차이를 확인할 수 있습니다.

LLM 알파 팩터 FAQ

LLM이 알파 팩터를 찾을 수 있는가?

수천 개를 제안할 수는 있지만 제안은 발견이 아닙니다. 주장은 평가 단계에서 제기됩니다. 광범위한 검색에서 나온 점수에는 점수 자체로는 확인할 수 없는 선택 문제가 포함됩니다. 식보다 홀드아웃 규율과 기록된 시도 횟수를 먼저 평가해야 합니다.

조정 Sharpe ratio란 무엇인가?

관측된 Sharpe ratio를 실제 초과수익이 없는 상태에서 동일한 규모의 검색이 해당 결과를 만들어냈을 확률로 변환하는 보정 방식입니다. Bailey와 López de Prado가 2014년에 발표했습니다. 핵심 입력값은 시도 횟수입니다. 감사되지 않은 리서치 과정은 바로 이 숫자를 제공할 수 없습니다.

백테스트는 몇 번부터 너무 많은 것인가?

정해진 기준은 없습니다. 적용해야 할 조정이 있을 뿐입니다. Sharpe 1.0을 기록한 백테스트 한 번과, 최고 Sharpe가 1.0인 백테스트 만 번은 세상에 대한 서로 다른 주장입니다. 위의 동전 던지기 결과는 데이터에 정보가 전혀 없었는데도 1.59에 도달했습니다. 전체 시도 횟수는 240회였습니다.

공표된 팩터는 공표 후 왜 약해지는가?

학술 연구는 이상현상이 발표된 뒤 몇 년 동안 약화되는 현상을 추적해 왔습니다. 혼잡 거래가 한 가지 원인으로 제시됩니다. 자체 표본에 과적합된 최초 결과도 또 다른 원인입니다. 두 경우 모두 차트에서는 같은 형태로 나타납니다. 효율적 시장 가설은 전자를 설명하고, 위의 시도 결과는 후자를 보여줍니다.


여기서 각 패널은 실제 월말 가격에 대한 저장된 쿼리입니다. SQL도 패널 아래에 공개되어 있습니다. 하나를 복사하고 시도 횟수를 늘려 보십시오. Strasmore 터미널에서 최고 수치가 상승하는 모습을 확인할 수 있습니다.

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