Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

LLM ค้นหาปัจจัยแอลฟาได้จริงหรือไม่

LLM เขียนปัจจัยแอลฟาได้เป็นร้อยรายการในหนึ่งชั่วโมง แต่ 240 ปัจจัยที่เสี่ยงเหมือนโยนเหรียญให้ผลอย่างไรเมื่อทดสอบกับราคาจริงนานสิบปี และควรทดสอบตัวที่ผ่านอย่างไร

LLM สามารถเสนอปัจจัยแอลฟาได้ตลอดทั้งวัน เพียงป้อนพจนานุกรมข้อมูลและชุดทดสอบการให้คะแนนให้โมเดลที่มีความสามารถ โมเดลดังกล่าวก็จะเขียนนิพจน์ปัจจัยที่ดูสมเหตุสมผลได้เป็นร้อยรายการก่อนมื้อกลางวัน คำถามที่ยากกว่านั้นอยู่เบื้องหลังว่า จะรู้ได้อย่างไรว่ารายการใดรายการหนึ่งเป็นของจริง ในเมื่อกระบวนการค้นหาที่ใช้สร้างมันขึ้นมาเป็นเครื่องจักรผลิตผู้ชนะจากสัญญาณรบกวน

ปัจจัยแอลฟาคืออะไร

ปัจจัย คือกฎที่แปลงข้อมูลตลาดให้เป็นตัวเลขหนึ่งตัวสำหรับหุ้นทุกตัวในทุกวัน การเปลี่ยนแปลงของราคาย้อนหลัง 12 เดือนเป็นปัจจัยประเภทหนึ่ง เช่นเดียวกับอัตราส่วนหนี้สินต่อส่วนของผู้ถือหุ้น ปัจจัยจะกลายเป็นกลยุทธ์เมื่อคุณจัดอันดับหุ้นในจักรวาลตามปัจจัยนั้น ซื้อกลุ่มบนสุด ขายกลุ่มล่างสุด และปรับพอร์ตตามกำหนดเวลา Alpha คือผลตอบแทนส่วนที่เหลือหลังหักผลตอบแทนที่การลงทุนตามตลาดทั่วไปจะให้มาอยู่แล้ว

ผู้สมัครแต่ละรายการจะได้รับการให้คะแนนด้วย Sharpe ratio ซึ่งคำนวณจากผลตอบแทนเฉลี่ยหารด้วยส่วนเบี่ยงเบนมาตรฐานของผลตอบแทนนั้น แล้วปรับเป็นรายปี นี่คือผลตอบแทนต่อความผันผวนหนึ่งหน่วย Sharpe ระยะยาวใกล้ 1 สำหรับกลยุทธ์ที่ใช้เงินจริงถือว่าอยู่ในระดับน่าพอใจ จึงควรระลึกไว้เมื่อเห็น backtest รายงานค่า 3

การวิจัยปัจจัยด้วย LLM ทำงานอย่างไร

ทุกโครงการในด้านนี้ใช้กระบวนการวนซ้ำรูปแบบเดียวกันโดยประมาณ

  1. โมเดลเขียนนิพจน์ปัจจัยในภาษาขนาดเล็กที่ชุดทดสอบสามารถประเมินผลได้
  2. ระบบ backtest ให้คะแนนนิพจน์แต่ละรายการโดยใช้ประวัติราคากับข้อมูลพื้นฐานในช่วงเวลาที่กำหนด
  3. เก็บนิพจน์ที่ได้คะแนนสูงกว่าเกณฑ์ และตัดรายการที่เหลือทิ้ง
  4. นำนิพจน์ที่ผ่านกลับเข้าไปในบริบทของโมเดลในฐานะตัวอย่างที่ทำเสร็จแล้ว แล้วเริ่มวนรอบใหม่

ระบบซื้อขายแบบหลายตัวแทน แบ่งงานเหล่านี้ไปยังบทบาทแยกกัน โดยมีบทบาทหนึ่งเสนอแนวคิดและอีกบทบาทหนึ่งทดสอบ ระบบเชื่อมต่อมีประโยชน์จริง และ ทักษะข้อมูลตลาดที่เอเจนต์ AI ต้องมี ก็เป็นทักษะเดียวกับที่มนุษย์ต้องใช้

กระบวนการนี้ไม่มีสิ่งใดไม่ซื่อสัตย์ การค้นหาเป็นวิธีที่ทำให้การวิจัยเกิดขึ้น ปัญหาอยู่ที่คณิตศาสตร์ และจะปรากฏทันทีที่ขั้นตอนที่ 2 ทำซ้ำมากกว่าสองสามครั้ง

เหตุใดการค้นหาปัจจัยแอลฟาด้วย LLM จึงผลิตผู้ชนะจากสัญญาณรบกวน

ประวัติราคาชุดเดียว สมมติฐานราคาถูกหลายพันรายการ สมมติฐานทุกข้อได้รับการให้คะแนนจากกลุ่มตัวอย่างจำกัดชุดเดียวกัน และกลุ่มตัวอย่างนั้นมีความบังเอิญอยู่มาก ทดสอบกฎมากพอ แล้วบางกฎจะปรับเข้ากับความบังเอิญได้อย่างใกล้ชิด คะแนนไม่สามารถบอกได้ว่าคุณได้การปรับแบบใด เพราะกฎที่จับสัญญาณรบกวนและกฎที่จับการเคลื่อนไหวของตลาดจะให้ตัวเลขเดียวกัน

สมมติฐานว่างต่อไปนี้สุ่มทั้งหมด 240 ครั้ง แต่ละ “ปัจจัย” ด้านล่างเป็นการโยนเหรียญ: แฮชของ ticker เดือน และหมายเลขการทดลองจะแบ่งหุ้นสหรัฐฯ ขนาดใหญ่ 40 ตัวออกเป็นสองส่วนในแต่ละเดือน จากนั้นกลยุทธ์จะถือส่วนหนึ่งเป็นสถานะ long และอีกส่วนเป็นสถานะ short การทดลองนี้ไม่มีข้อมูลอยู่ภายในโดยการออกแบบ เมื่อนำไปให้คะแนนด้วยผลตอบแทนจริง ณ สิ้นเดือนตั้งแต่ มกราคม 2016 ถึง มิถุนายน 2021 การทดลองทั้ง 240 รายการกระจายตัวดังนี้

คำสั่งดึงข้อมูล240 การสุ่มปัจจัยแบบโยนเหรียญ ประเมินจากราคาจริง: Sharpe รายปี มกราคม 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

ส่วนต่างคือประเด็นสำคัญ ไม่มีสิ่งใดในกราฟนั้นที่ใช้พยากรณ์ได้ แต่ 1 การทดลองกลับอยู่ในกลุ่มคะแนนสูงสุด (1.2 and above) หรือคิดเป็น 0.4% ของการค้นหา และ 1 อยู่ในกลุ่มคะแนนต่ำสุด (below -1.2) นักวิจัยที่ทำการทดลองซึ่งโชคดีเพียงครั้งเดียวแล้วหยุด จะมีทั้งกราฟและ Sharpe ratio แต่ไม่มีทางแยกได้ว่าสิ่งเหล่านี้เป็นการค้นพบจริงหรือไม่ ผลตอบแทนในที่นี้วัดจากราคาปิดสิ้นเดือนหนึ่งถึงราคาปิดสิ้นเดือนถัดไป ส่วน วิธีวัดผลตอบแทนรายเดือน อธิบายการคำนวณดังกล่าว

ตัวเลขสำคัญคือจำนวนครั้งที่คุณทดลอง

ผล backtest ที่รายงานเพียงลำพังขาดตัวส่วน การทดลอง 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 หัวข้อข่าวดูดีขึ้นโดยที่ไม่มีกฎใดดีขึ้นเลย ชุดทดสอบที่ประเมินนิพจน์ 10,000 รายการกำลังสร้างเส้นโค้งนี้ไปไกลทางขวามากกว่าที่แสดงในที่นี้ และตัวเลขที่รายงานก็คือจุดสูงสุดของเส้นโค้งนั้น

ช่วงเวลาที่กันไว้ทดสอบส่งผลต่อผู้ชนะอย่างไร

แนวป้องกันมาตรฐานคือ ชุดข้อมูลกันไว้ทดสอบ: ให้คะแนนในช่วงเวลาหนึ่ง แล้วให้คะแนนผู้รอดชีวิตซ้ำในช่วงเวลาหลังจากนั้น ซึ่งการค้นหาไม่เคยแตะต้อง นำการโยนเหรียญ 12 รายการที่ดีที่สุดจากช่วงฝึกอบรมไปใช้กับกฎเดียวกันตลอด 5 ปีถัดมา ตั้งแต่ กรกฎาคม 2021 ถึง มิถุนายน 2026

คำสั่งดึงข้อมูลการทดลองที่ดีที่สุด 12 รายการในการฝึก ประเมินซ้ำด้วยข้อมูล 5 ปีที่ไม่เคยใช้ (กรกฎาคม 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

แท่งกราฟแต่ละคู่แทนกฎหนึ่งรายการ แท่งซ้ายคือคะแนนที่ทำให้กฎนั้นมีสิทธิ์อยู่ในรายงาน แท่งขวาคือคะแนนของกฎเดียวกันในช่วง 5 ปีถัดมา การทดลองที่อยู่ในอันดับหนึ่งได้คะแนน 1.59 ในช่วงฝึกอบรม และ -0.51 ในช่วงหลังจากนั้น ส่วนการทดลองอันดับที่ 12 ได้ 0.74 แล้วลดลงมาอยู่ที่ 0.49

กฎ 12 รายการก็เป็นกลุ่มตัวอย่างขนาดเล็กในตัวเอง การจัดการทดลองทั้ง 240 รายการเป็น 5 กลุ่มตามคะแนนช่วงฝึกอบรม แล้วหาคะแนนช่วงกันไว้ทดสอบเฉลี่ยของแต่ละกลุ่ม จะให้ภาพที่ชัดเจนกว่า

คำสั่งดึงข้อมูลอันดับในการฝึกเทียบกับผลลัพธ์ holdout: การทดลอง 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.01 และ 0.13 ตามลำดับ ลำดับดังกล่าวแบนราบลง ช่วงกันไว้ทดสอบเป็นส่วนเดียวของกระบวนการที่ไม่ได้ถูกปรับให้เหมาะกับข้อมูลมาก่อน นั่นคือเหตุผลที่ควรใช้ช่วงดังกล่าวอย่างรอบคอบ

แนวป้องกันที่ใช้ได้จริง

ชุดข้อมูลกันไว้ทดสอบที่ใช้เพียงครั้งเดียว ทุกครั้งที่เปิดดูข้อมูล จะเปลี่ยนข้อมูลนั้นให้กลายเป็นข้อมูลฝึกอบรม การทดสอบแบบเดินหน้า ซึ่งหน้าต่างข้อมูลจะเลื่อนไปเรื่อย ๆ และคะแนนแต่ละชุดมาจากข้อมูลหลังช่วงที่ใช้ฟิตโมเดล คือรูปแบบที่รองรับการใช้งานซ้ำได้

การปรับค่าจากการทดสอบหลายครั้ง Deflated Sharpe ratio ซึ่ง Bailey และ López de Prado นำเสนอในปี 2014 จะลดทอน Sharpe ที่สังเกตได้ตามจำนวนครั้งที่ทดลอง ความยาวของกลุ่มตัวอย่าง ความเบ้ของผลตอบแทน และความหนาของหางการกระจาย หากป้อนจำนวนครั้งที่ทดลองอย่างซื่อตรง Sharpe ที่โดดเด่นจากการค้นหานิพจน์ 10,000 รายการมักลดลงจนแทบไม่มีความหมาย

เส้นทางการตรวจสอบที่ครอบคลุมนิพจน์ทุกตัวที่เคยทดลอง รวมถึงรายการที่ถูกทิ้ง ข้อนี้เป็นองค์ประกอบหลัก และเป็นเหตุผลที่คำว่า “ตรวจสอบได้” มีความสำคัญเมื่ออธิบายโครงการวิจัยปัจจัย การลดทอนค่าต้องใช้จำนวนครั้งที่ทดลอง หากกระบวนการบันทึกเฉพาะผู้ชนะ ก็ได้ทำลายข้อมูลนำเข้าของการปรับแก้ของตนเองแล้ว ร่างที่ถูกทิ้ง การกวาดหาพารามิเตอร์ที่ยกเลิก การเริ่มต้นการวิจัยใหม่ของนักวิจัย และโค้ดให้คะแนนทุกเวอร์ชันก่อนหน้า ล้วนต้องนับรวมในตัวเลขดังกล่าว

ตรวจสอบต้นทุนและ อคติจากการมองไปข้างหน้า ก่อนเชื่อคะแนน ปัจจัยที่จัดอันดับด้วยข้อมูลพื้นฐานซึ่งประทับวันที่ที่ผู้ให้บริการนำข้อมูลเข้า แทนวันที่ตลาดสามารถมองเห็นข้อมูลนั้น จะให้ผล backtest ที่สวยงาม แต่ซื้อขายจริงได้แย่

การอ่านคำว่า “ตรวจสอบได้”

คลังโค้ดใหม่ในด้านนี้เกิดขึ้นเกือบทุกสัปดาห์ และโครงการที่มีดาวอยู่ไม่กี่สิบดวงเป็นเพียงต้นแบบ ไม่ใช่ประวัติผลงาน จำนวนดาวยังเปลี่ยนเร็วกว่าตัวโค้ด นั่นคือเหตุผลที่หน้านี้ประเมินรูปแบบการทำงานแทนที่จะประเมินโครงการใดโครงการหนึ่ง ต่อไปนี้คือสิ่งที่ควรเปิดดูเป็นอันดับแรกในโครงการใดก็ตามที่อยู่ตรงหน้า

  • โครงการบันทึกผู้สมัครทุกตัวพร้อมนิพจน์และคะแนน รวมถึงเวลา หรือบันทึกเฉพาะรายการที่ผ่าน?
  • ชุดทดสอบบังคับใช้ช่วงกันไว้ทดสอบเอง หรืออาศัยวินัยของนักวิจัย?
  • คะแนนที่รายงานมีจำนวนครั้งที่ทดลองกำกับไว้ด้วยหรือไม่?
  • โครงการสร้างขึ้นสำหรับตลาดใด? ไลบรารีที่ปรับให้เหมาะกับหุ้น A ของจีนจะมีข้อจำกัดราคาประจำวันและข้อห้ามขายหุ้นที่ซื้อในช่วงเวลาเดียวกันติดมาด้วย และพฤติกรรมของปัจจัยภายใต้กฎเหล่านั้นไม่สามารถนำไปใช้กับหุ้นสหรัฐฯ ได้โดยตรง
  • คุณสามารถรันซ้ำและสร้างตัวเลขเดิมได้หรือไม่? ระบุ commit ที่แน่นอนที่คุณอ่านไว้ เพราะโครงการในระยะนี้อาจเขียนโค้ดให้คะแนนใหม่ระหว่างสุดสัปดาห์

ไม่มีข้อใดทำให้ LLM ไร้ประโยชน์ในการวิจัยปัจจัย การสร้างสมมติฐานเป็นคอขวดที่มีอยู่จริง และโมเดลทำเรื่องนี้ได้ดี สิ่งที่เปลี่ยนไปคือภาระตกอยู่ที่การทำบัญชีว่ามีการใช้สมมติฐานไปกี่รายการ ก่อนที่สิ่งใดจะพบกับสมุดคำสั่งซื้อขายจริง การซื้อขายจำลอง คือขั้นตอนที่ทำให้เห็นช่องว่างระหว่าง backtest กับการจับคู่คำสั่ง

คำถามที่พบบ่อยเกี่ยวกับปัจจัยแอลฟาของ LLM

LLM สามารถค้นหาปัจจัยแอลฟาได้หรือไม่

โมเดลสามารถเสนอได้เป็นพันรายการ แต่ข้อเสนอไม่ใช่การค้นพบ ข้ออ้างดังกล่าวเกิดขึ้นในขั้นตอนให้คะแนน และคะแนนจากการค้นหาที่กว้างย่อมมีปัญหาการคัดเลือกที่ตัวคะแนนเองมองไม่เห็น ควรประเมินวินัยในการใช้ช่วงกันไว้ทดสอบและจำนวนครั้งที่บันทึกไว้ก่อนพิจารณานิพจน์

Deflated Sharpe ratio คืออะไร

เป็นการปรับแก้ที่แปลง Sharpe ratio ที่สังเกตได้ให้เป็นความน่าจะเป็นว่า การค้นหาที่มีขนาดดังกล่าวจะสร้างผลลัพธ์นี้ได้แม้ไม่มีความได้เปรียบที่แท้จริง Bailey และ López de Prado เผยแพร่วิธีนี้ในปี 2014 ตัวแปรนำเข้าหลักคือจำนวนครั้งที่ทดลอง ซึ่งเป็นตัวเลขที่กระบวนการวิจัยที่ตรวจสอบไม่ได้ไม่สามารถจัดหาได้

Backtest กี่ครั้งจึงถือว่ามากเกินไป

ไม่มีเกณฑ์ตายตัว มีเพียงการปรับแก้ที่ต้องนำมาใช้ backtest หนึ่งครั้งที่ได้คะแนน 1.0 กับ backtest 10,000 ครั้งซึ่งครั้งที่ดีที่สุดได้ 1.0 เป็นข้อกล่าวอ้างต่อโลกคนละแบบกัน การโยนเหรียญด้านบนได้ 1.59 จากการทดลอง 240 ครั้ง ทั้งที่ไม่มีข้อมูลอยู่ในข้อมูลเลย

เหตุใดปัจจัยที่เผยแพร่แล้วจึงอ่อนแอลงหลังการเผยแพร่

งานวิชาการติดตามการเสื่อมลงของความผิดปกติที่เผยแพร่แล้วในช่วงหลายปีหลังปรากฏผลครั้งแรก การที่ผู้ลงทุนเข้ามาแออัดในกลยุทธ์เดียวกันเป็นกลไกหนึ่งที่ถูกเสนอ ส่วนผลลัพธ์ดั้งเดิมที่ปรับเข้ากับกลุ่มตัวอย่างของตนเองมากเกินไปก็เป็นอีกกลไกหนึ่ง ทั้งสองกรณีให้รูปทรงเดียวกันบนกราฟ สมมติฐานตลาดมีประสิทธิภาพ อธิบายกลไกแรก ส่วนการทดลองด้านบนแสดงให้เห็นกลไกที่สอง


แผงข้อมูลทุกแผงในที่นี้เป็น query ที่จัดเก็บไว้จากราคาจริง ณ สิ้นเดือน โดยมี SQL เปิดให้ตรวจสอบอยู่ด้านล่าง คัดลอกไปหนึ่งรายการ เพิ่มจำนวนครั้งที่ทดลอง แล้วดูตัวเลขที่ดีที่สุดไต่สูงขึ้นบนเทอร์มินัล Strasmore

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