Backtest riproducibile in Python senza API key
Esegui un backtest riproducibile in Python senza API key: installazione fissata, dati campione deterministici e limiti di ciò che nasconde una sola equity curve.
Un backtest riproducibile in Python è un backtest che un’altra persona può rieseguire su una macchina pulita e ottenere esattamente gli stessi numeri, senza che un account o una API key costituiscano un ostacolo. La maggior parte dei tutorial fallisce già alla prima riga, quando un download live fornisce al lettore successivo uno storico dei prezzi leggermente diverso da quello ricevuto dall’autore. Questa guida fissa un unico engine open source, quantjourney-bt alla versione 0.12.4, esegue l’esempio incluso senza credenziali e utilizza poi dati di mercato reali per mostrare che cosa una singola esecuzione pulita non può comunque dirti.
Che cosa rende riproducibile un backtest?
Qui la riproducibilità ha un significato preciso e verificabile: una seconda persona esegue un comando su una macchina pulita e ottiene i tuoi numeri, fino all’ultima cifra decimale. Due elementi comuni possono comprometterla.
Il primo è il codice. Una libreria alla versione 0.x non offre alcuna garanzia di compatibilità tra release minor. Basta rinominare un elemento, modificare un valore predefinito o riordinare una colonna perché lo script continui a funzionare, producendo però risultati diversi senza segnalarlo.
Il secondo sono i dati. Un tutorial la cui prima riga esegue un download live non è mai riproducibile. I vendor modificano la serie storica, applicano gli aggiustamenti per gli split e colmano le lacune. Di conseguenza, lo stesso script può restituire numeri diversi un mese dopo. A posteriori, non puoi distinguere una modifica al codice da una modifica ai dati.
Il modello da replicare in questo progetto è associare una versione fissa dell’engine a un piccolo dataset incluso direttamente nel package.
Fissa la versione: pip install quantjourney-bt==0.12.4
quantjourney-bt è il backtester QuantJourney, distribuito con licenza Apache 2.0 e compatibile con Python 3.11 o versioni successive. La versione 0.12.4 è stata pubblicata il 21 luglio 2026 ed è quella a cui fanno riferimento tutti i comandi riportati di seguito, come documentato nell’agosto 2026.
Lavora in un ambiente isolato. python3 -m venv .venv ne crea uno, source .venv/bin/activate vi accede e python -m pip install -U pip aggiorna l’installer al suo interno. Poi installa la versione esatta: pip install quantjourney-bt==0.12.4.
La documentazione del progetto riporta la forma senza versione fissata, pip install quantjourney-bt. La parte ==0.12.4 è sotto la tua responsabilità e, nelle versioni 0.x, è essenziale. Registra il pin in un punto facilmente reperibile: pip freeze > requirements.txt salva ogni dipendenza risolta, comprese quelle che non hai indicato esplicitamente.
Sono disponibili due extra opzionali. pip install "quantjourney-bt[wf]" aggiunge Optuna per gli esempi di walk-forward e ottimizzazione. pip install "quantjourney-bt[data]" aggiunge un fallback basato su yfinance, utilizzato per i benchmark.
La licenza Apache-2.0 è permissiva. Puoi utilizzare e modificare il codice anche per finalità commerciali, devi mantenere i file della licenza e delle note in caso di redistribuzione, e i contributori concedono esplicitamente diritti sui brevetti.
Come eseguire l’esempio SMA incluso senza una chiave API
Il repository include uno script di avvio insieme a cinquanta strategie di esempio eseguibili, suddivise in un percorso basato sui pesi e uno basato sugli ordini. Tra queste figurano cinque workflow walk-forward. ./strategy.sh --list stampa il catalogo. ./strategy.sh example_weights_01_sma_daily --check importa una singola strategia e non accede ad alcun dato: è il controllo più rapido per verificare che l’installazione sia corretta.
L’esecuzione della demo richiede una sola riga: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
Il flag --sample-data è il punto centrale. Il progetto descrive così il dataset utilizzato:
Il dataset di esempio è intenzionalmente ridotto e riproducibile. È utile per verificare l’installazione, generare report e analizzare il flusso dell’engine senza creare un account.
Fonte: README di quantjourney-bt, versione 0.12.4, consultato il 6 agosto 2026.
L’esecuzione crea una directory, non un esito mostrato nella console: summary.txt e summary.json, un metrics.csv, un equity_curve.csv accanto al relativo equity_curve.png, un dashboard.html, una cartella plots/ e un run_metadata.json che registra la configurazione dell’esecuzione. Quest’ultimo file è quello che molti trascurano e che consente di rendere verificabile un risultato anche a un anno di distanza.
Interpreta correttamente le metriche ottenute. Il dataset incluso è ridotto e serve a scopo illustrativo, quindi lo Sharpe ratio e il massimo drawdown riportati in summary.txt descrivono un file di esempio. Non costituiscono una prova sulla strategia. Trattarli come un risultato sarebbe il primo errore possibile.
L’esecuzione consente comunque di verificare aspetti importanti: l’installazione funziona e l’intero flusso dell’engine, dal segnale ai pesi target fino alla ricostruzione del valore del portafoglio, produce i propri artefatti sulla macchina senza richiedere alcuna credenziale. Per i backtest su serie storiche reali è disponibile un percorso che utilizza le credenziali per accedere al servizio dati del progetto. Il percorso è documentato. Questa guida si ferma qui, alla parte che non richiede nulla da nessuno.
Cosa non può dirti una singola curva azionaria in-sample
L’esecuzione sul campione produce una sola curva azionaria. Ecco cosa quella curva non può dimostrare, misurato su dati di mercato reali anziché su un file dimostrativo.
Il riquadro seguente applica a SPY la stessa idea usata dalla strategia di esempio: l’incrocio tra una media mobile a 20 sessioni e una a 50 sessioni. I risultati sono riportati separatamente per ogni anno solare dal 2017 al 2025. La posizione di ciascuna sessione viene determinata sulla base della chiusura della sessione precedente. La regola non opera quindi su un dato che non era ancora disponibile.
L'esatto SQL dietro ogni numero
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearUna sola regola invariata, misurata 9 volte separate. Prima di esaminare qualsiasi altro elemento, leggi le due colonne delle percentuali dall’alto verso il basso. Nel 2017 la regola ha chiuso l’anno a 16%, contro 19.4% per chi ha mantenuto SPY nello stesso periodo. Nel 2025 le stesse due colonne riportano 10.4% e 16.4%. Il codice è identico in entrambe le righe. È cambiata soltanto la finestra.
La colonna degli incroci mostra quanto siano limitate le evidenze alla base del risultato. 4 variazioni di posizione distribuite su 2025 implicano che un’intera curva azionaria annuale poggi su una manciata di decisioni. È un campione molto ridotto per poterlo definire un risultato.
La stessa regola si comporta allo stesso modo anche su altri titoli?
Modificare la finestra temporale è un modo per analizzare una singola curva. Modificare l’universo investibile è l’altro. Il pannello seguente mantiene fissi i parametri ed esegue la stessa regola su cinque titoli liquidi nei cinque anni solari dal 2021 al 2025.
L'esatto SQL dietro ogni numero
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ si trova in cima al pannello, a 40.2%, mentre la riga in fondo, KO, arriva a 3.8%. La colonna days_long_pct indica per quale quota della finestra ciascuna versione ha mantenuto una posizione, 67% per la riga superiore. Un solo set di parametri, cinque universi e uno spread abbastanza ampio: scegliere il vincitore a posteriori non dice nulla sull’esecuzione che non avete ancora effettuato.
Nulla di tutto questo costituisce una raccomandazione a negoziare un crossover. Il crossover è un riferimento per il backtest, e il backtest è ciò che stiamo misurando.
Dove si insinua il look-ahead bias in un backtest basato sui pesi
Un motore basato sui pesi trasforma un segnale in pesi obiettivo, simula le esecuzioni su quei pesi e poi ricostruisce il valore del portafoglio a partire dalle posizioni risultanti. L’errore si nasconde nel collegamento tra segnale e peso. Se il peso di oggi deriva dalla chiusura di oggi e poi beneficia del rendimento di oggi, il backtest ha operato su informazioni che non esistevano ancora quando sarebbe stato inserito l’ordine. Questo è il look-ahead bias, e non genera alcun errore. Fa semplicemente apparire tutto migliore.
Il pannello seguente esegue entrambe le versioni della stessa regola sulla medesima serie storica di SPY.
L'esatto SQL dietro ogni numero
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearIn 2017 la versione basata sulla sessione precedente ha prodotto 16%, mentre la versione basata sulla stessa sessione ha prodotto 17.1%, con una differenza di 1.1 punti percentuali. In 2025 la distanza tra le due misure era di 1.6 punti percentuali. Solo una di queste colonne può essere prodotta da una macchina che non conosceva già il livello di chiusura della sessione, e la differenza tra le due è pura contabilità, senza alcuna idea, competenza o operazione alla base.
Questo motore dichiara la propria impostazione sulla tempistica degli ordini, e vale più di una promessa:
Per le esecuzioni all’apertura, lo slippage sensibile al range utilizza solo la barra precedente già completata, mentre la capacità in termini di volume viene stimata sulla base di osservazioni ritardate; il motore non utilizza il massimo, il minimo, la chiusura o il volume dell’intera giornata successivi di quella sessione.
Fonte: README di quantjourney-bt, versione 0.12.4, consultato il 6 agosto 2026.
Un’ipotesi documentata può essere verificata rispetto al codice sorgente già installato. Un’ipotesi non documentata è una supposizione.
Perché esiste l’extra walk-forward
Gli esempi walk-forward, da WF01 a WF05, vengono forniti con l’extra [wf] e la relativa dipendenza Optuna. Il walk-forward stima i parametri su una porzione della serie storica, li misura sulla porzione successiva, poi fa avanzare la coppia di finestre e ripete il processo. Le varianti rolling ed expanding differiscono per il fatto che la finestra di stima elimini o meno i dati più vecchi mentre avanza. Un altro esempio aggiunge un purge e un embargo a ogni confine, eliminando le osservazioni più vicine alla giunzione. In questo modo, la porzione usata per la stima non può contaminare quella su cui viene misurata.
Nulla di tutto questo trasforma un’idea debole in una strategia funzionante. Sostituisce un singolo numero con una distribuzione di numeri, che può essere analizzata e messa in discussione. Questo è l’intero miglioramento. Il passaggio successivo non consiste ancora nell’impiegare denaro reale: il paper trading prima del denaro reale misura ciò che un backtest, per sua struttura, non può rilevare. Si parte dal verificare se gli ordini vengono eseguiti a un prezzo anche solo vicino a quello ipotizzato dal simulatore. I circuit breaker per i trading bot gestiscono invece ciò che fa il codice nel giorno in cui qualcosa va storto. Per le statistiche alla base di tutto questo, i nostri appunti sul libro open source sul quant trading approfondiscono il tema.
FAQ
È possibile eseguire il backtest di una strategia senza una chiave API?
Sì. quantjourney-bt include un dataset di esempio dietro un flag --sample-data e le strategie di esempio vengono eseguite su quel dataset senza account né credenziali. Il dataset è piccolo e ha finalità illustrative. Considera quindi questa esecuzione come un controllo dell’installazione e della pipeline, non come una prova dell’efficacia della strategia.
Perché bloccare la versione di un pacchetto Python per il backtesting?
Un pacchetto alla versione 0.x non offre garanzie di compatibilità tra release minor. Una modifica al valore predefinito o il cambio del nome di una metrica potrebbero non essere segnalati. Bloccare la versione con pip install quantjourney-bt==0.12.4 e registrare l’ambiente in un file requirements consente di ricostruire l’anno prossimo un risultato prodotto oggi, usando lo stesso motore che lo ha generato.
Con quale licenza viene distribuito quantjourney-bt?
Apache License 2.0. Consente l’uso commerciale e la modifica del software, richiede di conservare i file di licenza e gli avvisi in caso di redistribuzione e include un’esplicita concessione di brevetto da parte dei contributori. La versione 0.12.4 è stata pubblicata il 21 luglio 2026 e richiede Python 3.11 o versioni successive.
Un risultato positivo del backtest significa che la strategia funziona?
No. Un backtest è una singola misurazione, effettuata su una sola finestra temporale e su un solo universo. I pannelli sopra mostrano che una stessa regola invariata può produrre risultati annuali molto diversi su un singolo ticker e risultati molto diversi su cinque titoli. È proprio questa variabilità che la validazione walk-forward e i test out-of-sample cercano di mettere in evidenza.
Come sono stati calcolati i pannelli sopra
Le chiusure giornaliere corrispondono all’ultimo print dell’ultima minuto della sessione regolare per ciascuna data, rilevato secondo l’orario di New York tra le 9:30 e le 16:00. Questo mantiene corretti anche i giorni con chiusura anticipata, senza codificare una durata fissa della sessione. La media veloce copre 20 sessioni e quella lenta 50; entrambe sono semplici. Le prime 49 sessioni di ogni serie sono il periodo di warm-up e non prevedono alcuna posizione. I dati annuali compongono il movimento da chiusura a chiusura di ciascuna sessione in cui la regola era long, mentre la colonna hold compone ogni sessione dello stesso anno a fini comparativi. I cinque titoli della sezione trasversale sono stati scelti perché presentavano serie storiche continue e nessuno split nel periodo analizzato; la serie delle chiusure non richiede quindi aggiustamenti. Le finestre sono fisse nel passato, perciò questi pannelli restituiscono gli stessi numeri a ogni rigenerazione.
Ogni pannello qui riportato contiene l’SQL esatto sottostante. I numeri della pagina sono quindi nuovamente elaborabili, come lo è l’installazione con versione bloccata. Per misurare una regola sulla tua finestra temporale prima di scrivere codice per il backtest, formula la domanda in linguaggio naturale sul terminale Strasmore.