Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Un LLM può trovare alpha factor?

Un LLM può scrivere un centinaio di alpha factor in un’ora. Scopri come si comportano 240 factor casuali su dieci anni di prezzi reali e come testare i migliori.

Un LLM può proporre alpha factor per tutta la giornata. Basta fornire a un modello capace un dizionario dei dati e un sistema di scoring, e prima di pranzo scriverà un centinaio di espressioni di factor plausibili. La domanda più difficile è un’altra: come si può sapere se uno di questi factor è reale, quando la ricerca che lo ha prodotto è una macchina per creare vincitori dal rumore?

Che cos’è un alpha factor?

Un factor è una regola che trasforma i dati di mercato in un numero per ogni titolo e per ogni data. La variazione del prezzo a dodici mesi è un factor. Lo è anche il rapporto tra debito e capitale proprio. Un factor diventa una strategia quando si ordina un universo in base a quel factor, si acquista la fascia superiore, si vende allo scoperto quella inferiore e si effettua il ribilanciamento secondo una certa frequenza. L’alpha è il rendimento che rimane dopo aver sottratto quello che una semplice esposizione al mercato avrebbe comunque prodotto.

I candidati vengono valutati con lo Sharpe ratio: il rendimento medio diviso per la deviazione standard di quel rendimento, annualizzato. È il rendimento per unità di oscillazione. Uno Sharpe di lungo periodo vicino a 1 su una strategia live è rispettabile. Conviene ricordarlo la prossima volta che un backtest mostra 3.

Come funziona davvero la ricerca di factor con gli LLM

Ogni progetto di questo settore esegue una variante dello stesso ciclo.

  1. Il modello scrive espressioni di factor in un linguaggio ridotto che il sistema di valutazione è in grado di eseguire.
  2. Un backtester valuta ogni espressione su una serie storica fissa di prezzi e fondamentali.
  3. Le espressioni sopra una determinata soglia vengono conservate. Le altre vengono scartate.
  4. Le espressioni conservate tornano nel contesto del modello come esempi già elaborati, e il ciclo riparte.

I sistemi di trading multi-agente distribuiscono queste attività tra ruoli separati: uno propone e l’altro verifica. L’infrastruttura è realmente utile, e le competenze sui dati di mercato necessarie a un agente AI sono le stesse che servono a una persona.

Nel ciclo non c’è nulla di scorretto. La ricerca si fa attraverso una ricerca. Il problema è aritmetico e compare nel momento in cui il passaggio 2 viene eseguito più di poche volte.

Perché una ricerca di alpha factor con un LLM crea vincitori dal rumore

Una sola serie storica dei prezzi. Migliaia di ipotesi a basso costo. Ogni ipotesi viene valutata sullo stesso campione finito, e quel campione contiene molta casualità. Se si testano abbastanza regole, alcune si adatteranno strettamente a quella casualità. Il punteggio non può dire quale tipo di adattamento si è ottenuto: una regola che ha seguito il rumore e una che ha seguito il mercato producono lo stesso numero.

Ecco l’ipotesi nulla, estratta 240 volte. Ogni “factor” qui sotto è un lancio di moneta: un hash del ticker, del mese e del numero della prova divide ogni mese 40 grandi titoli statunitensi in due metà, e la strategia mantiene una metà long e l’altra short. Per costruzione, non contiene alcuna informazione. Valutate sui rendimenti reali di fine mese da gennaio 2016 a giugno 2021, le 240 prove si distribuiscono così.

Query240 fattori coin flip valutati su prezzi reali: Sharpe annualizzato, gen. 2016–giu. 2021
L'esatto SQL dietro ogni numero
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

Lo spread è il punto centrale. Nulla in quel grafico prevede alcunché, eppure 1 prove sono finite nella fascia superiore (1.2 and above), 0.4% della ricerca, e 1 nella fascia inferiore (below -1.2). Un ricercatore che avesse eseguito una sola prova fortunata e si fosse fermato avrebbe in mano un grafico e uno Sharpe ratio, senza alcun modo di distinguere l’uno dall’altro da una scoperta. I rendimenti qui vanno dalla chiusura di fine mese alla chiusura di fine mese; come si misurano i rendimenti mensili illustra il calcolo.

Il numero importante è quante prove hai effettuato

Un backtest presentato da solo non indica il denominatore. Le stesse 240 prove, lette come una ricerca che continua ad ampliarsi: a ogni passaggio, il punteggio migliore sul tabellone accanto alla media di tutto ciò che è stato testato fino a quel momento.

QueryIl punteggio migliore cresce con l’ampiezza della ricerca: Sharpe migliore e medio per numero di prove
L'esatto SQL dietro ogni numero
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

Un massimo progressivo può solo aumentare, ed è proprio questa la trappola. La prima regola testata ha ottenuto 0.44. Dopo 240 prove, il risultato migliore sul tabellone è 1.59, mentre la media di tutte le prove è 0.01. Il titolo è migliorato senza che migliorasse una sola regola. Un sistema di valutazione che esamina diecimila espressioni sta tracciando questa curva molto più a destra di quanto mostrato qui, e il numero che riporta è il suo punto massimo.

Cosa succede ai vincitori in un periodo fuori campione

La difesa standard è un holdout: si valuta un periodo e poi si rivalutano i sopravvissuti su un periodo successivo che la ricerca non ha mai utilizzato. Prendiamo i dodici lanci di moneta migliori nella finestra di training ed eseguiamo le stesse regole sui cinque anni successivi, da luglio 2021 a giugno 2026.

QueryLe dodici prove migliori nel training, ricalcolate su cinque anni non utilizzati (lug. 2021–giu. 2026)
L'esatto SQL dietro ogni numero
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

Ogni coppia di barre rappresenta una regola. La barra a sinistra è il punteggio che le ha permesso di entrare nel report. Quella a destra è il risultato della stessa regola nei cinque anni successivi. La prova classificata al primo posto ha ottenuto 1.59 nel training e -0.51 dopo; la dodicesima ha ottenuto 0.74 e poi 0.49.

Dodici regole costituiscono a loro volta un campione ridotto. Ordinando tutte le 240 prove in quinti in base al punteggio di training e calcolando poi il punteggio medio di holdout per ciascun quinto, si ottiene una rappresentazione più chiara.

QueryPosizione nel training rispetto al risultato holdout: 240 prove suddivise in quinti
L'esatto SQL dietro ogni numero
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

Nel training, i gruppi vanno da 0.66 in alto a -0.63 in basso. È una scala ampia e perfettamente ordinata, garantita dal fatto che i gruppi sono stati creati proprio in base a quel punteggio. Nel periodo di holdout, le due estremità corrispondenti hanno una media rispettivamente di 0.01 e 0.13. La scala si appiattisce. L’holdout è l’unica parte della pipeline che non è stata ottimizzata sulla base dei risultati, ed è questo che rende importante usarlo con attenzione.

Difese che funzionano davvero

Un holdout da utilizzare una sola volta. Ogni consultazione lo trasforma in dati di training. Il walk-forward testing, in cui la finestra scorre e ogni punteggio deriva da dati successivi al fit, è la versione che resiste a un utilizzo ripetuto.

Una correzione per i test multipli. Il deflated Sharpe ratio, introdotto da Bailey e López de Prado nel 2014, riduce uno Sharpe osservato in funzione del numero di prove eseguite, della durata del campione, dell’asimmetria dei rendimenti e della pesantezza delle loro code. Se gli si fornisce un conteggio onesto delle prove, uno Sharpe headline ottenuto da una ricerca di diecimila espressioni spesso si riduce a nulla.

Un audit trail che copra ogni espressione testata, comprese quelle scartate. Questo è l’elemento portante, ed è per questo che “auditable” è la parola interessante nella descrizione di un progetto di ricerca sui factor. La deflazione richiede il conteggio delle prove. Una pipeline che registra solo i vincitori ha distrutto l’input necessario alla propria correzione. Le bozze scartate, le scansioni di parametri abbandonate, i riavvii del ricercatore e ogni versione precedente del codice di scoring contribuiscono tutti a quel numero.

Controlli su costi e look-ahead bias prima di prendere per buono il punteggio. Un factor ordinato in base a un dato fondamentale associato alla data in cui il vendor lo ha caricato, anziché alla data in cui il mercato avrebbe potuto conoscerlo, produce backtest eccellenti e risultati di trading negativi.

Come leggere la parola “auditable”

In questo settore compaiono nuovi repository quasi ogni settimana, e un progetto con qualche decina di stelle è un prototipo, non uno storico di risultati. Anche il numero di stelle aumenta più rapidamente del codice. Per questo questa pagina valuta lo schema, non un singolo progetto. Ecco cosa conviene aprire per primo, qualunque sia il progetto che si presenta.

  • Registra ogni candidato con la sua espressione e il relativo punteggio, corredati da un timestamp, oppure registra solo quelli conservati?
  • L’holdout è imposto dal sistema di valutazione o dalla disciplina personale del ricercatore?
  • Ogni punteggio pubblicato è accompagnato dal numero di prove?
  • Per quale mercato è stato costruito? Una libreria ottimizzata per le azioni A cinesi incorpora i limiti di variazione giornalieri e il divieto di vendere azioni acquistate nella stessa sessione. Il comportamento di un factor in presenza di queste regole non si trasferisce alle azioni statunitensi.
  • È possibile rieseguirlo e riprodurre i numeri? Blocca il commit esatto che hai letto, perché un progetto in questa fase riscrive il codice di scoring da un fine settimana all’altro.

Nulla di tutto questo rende inutile un LLM nella ricerca sui factor. La generazione di ipotesi è un vero collo di bottiglia e i modelli sono efficaci in questo compito. Cambia però il punto in cui ricade il lavoro: sulla contabilizzazione del numero di ipotesi consumate. Prima che una di queste arrivi su un order book live, il paper trading è il momento in cui diventa visibile la distanza tra un backtest e un’esecuzione.

FAQ sugli alpha factor generati da LLM

Un LLM può trovare alpha factor?

Può proporne a migliaia, ma una proposta non è una scoperta. L’affermazione viene fatta nella fase di scoring, e un punteggio ottenuto da una ricerca ampia presenta un problema di selezione che il punteggio stesso non è in grado di rilevare. Prima dell’espressione, bisogna valutare la disciplina dell’holdout e il conteggio registrato delle prove.

Che cos’è il deflated Sharpe ratio?

È una correzione che trasforma uno Sharpe ratio osservato nella probabilità che una ricerca di quelle dimensioni lo avrebbe prodotto in assenza di un vantaggio reale. Bailey e López de Prado lo hanno pubblicato nel 2014. Il suo input centrale è il numero di prove, esattamente il numero che un ciclo di ricerca non auditable non può fornire.

Quanti backtest sono troppi?

Non esiste una soglia. Esiste solo una correzione che deve essere applicata. Un singolo backtest con Sharpe pari a 1.0 e diecimila backtest il cui risultato migliore è 1.0 rappresentano affermazioni diverse sul mondo. I lanci di moneta qui sopra hanno raggiunto 1.59 su 240 prove, senza alcuna informazione nei dati.

Perché i factor pubblicati si indeboliscono dopo la pubblicazione?

La ricerca accademica ha documentato il deterioramento delle anomalie pubblicate negli anni successivi alla loro comparsa. Il crowding è uno dei meccanismi proposti, e un risultato originale sovradattato al proprio campione è un altro; entrambi producono la stessa forma su un grafico. L’ipotesi dei mercati efficienti inquadra il primo meccanismo, mentre le prove sopra dimostrano il secondo.


Ogni pannello qui riportato è una stored query su prezzi reali di fine mese, con il codice SQL visibile sotto. Copiane una, aumenta il numero di prove e osserva il numero migliore salire sul terminale Strasmore.

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