Cache Parquet Aggregata: Come Accelerare i Backtest Multi-Timeframe di Centinaia di Volte
Una strategia multi-timeframe utilizza simultaneamente diversi timeframe: il giornaliero determina la direzione del trend, l'orario identifica i punti di ingresso e il 5 minuti individua i tempi di esecuzione. Ogni timeframe richiede i propri indicatori: medie mobili, oscillatori, livelli.
Per un singolo backtest, tutto è semplice — ricalcolare i timeframe dai dati al minuto, calcolare gli indicatori, eseguire la strategia. Ma durante l'ottimizzazione di massa — quando si devono testare migliaia di combinazioni di parametri — ricalcolare i timeframe e gli indicatori ad ogni iterazione diventa un collo di bottiglia. Un singolo passaggio attraverso i dati al minuto su due anni significa elaborare oltre un milione di barre, e ripetere questa operazione mille volte è uno spreco.
La soluzione: precalcolare tutto una volta e salvarlo in un file parquet.
Il Problema: Calcoli Ridondanti Durante l'Ottimizzazione
Una tipica pipeline di backtest multi-timeframe si presenta così:
for params in parameter_grid:
df_1m = load_candles("ETHUSDT", "1m", start, end)
df_5m = resample_ohlcv(df_1m, "5m")
df_1h = resample_ohlcv(df_1m, "1h")
df_4h = resample_ohlcv(df_1m, "4h")
df_1d = resample_ohlcv(df_1m, "D")
ma_1h = compute_ma(df_1h["close"], length=params["ma_1h_len"])
ma_4h = compute_ma(df_4h["close"], length=params["ma_4h_len"])
ma_1d = compute_ma(df_1d["close"], length=params["ma_1d_len"])
result = run_strategy(df_1m, ma_1h, ma_4h, ma_1d, params)
Ad ogni iterazione, i passi 1-3 vengono ricalcolati anche se i dati sono gli stessi. Cambiano solo i parametri di soglia della strategia (passo 4). È come ricostruire un'intera casa ogni volta che si vuole semplicemente provare un colore diverso per le pareti.
L'Idea: Calcolare una Volta, Salvare, Riutilizzare Molte Volte
L'osservazione chiave: i timeframe e gli indicatori dipendono solo dai dati al minuto e dai parametri degli indicatori, non dai parametri della strategia. Se fissiamo l'insieme degli indicatori richiesti, possiamo calcolarli una volta e salvarli.
Lo schema:
Passo 1 (una volta):
Candele al minuto -> Ricampionamento timeframe -> Calcolo indicatori -> File parquet
Passo 2 (molte volte):
File parquet -> Strategia con parametri diversi -> Risultato
Emulazione dei Timeframe dalle Candele al Minuto

Abbiamo un archivio completo di candele al minuto. Da esso possiamo riprodurre accuratamente qualsiasi timeframe superiore. Ma c'è una sfumatura: con un resample standard, otteniamo una riga per periodo (una riga all'ora, una ogni 4 ore, ecc.). Questo non funziona per il backtest minuto per minuto — dobbiamo conoscere il valore dell'indicatore ad ogni minuto.
Quindi emuliamo i valori dei timeframe superiori per ogni candela al minuto, modellando come il bot vede i dati in tempo reale:
- Il bot riceve la prossima candela al minuto
- Aggiorna la barra corrente (non ancora chiusa) del timeframe superiore — ricalcola High, Low, Close, Volume
- Ricalcola l'indicatore su tutte le barre chiuse più la barra parziale corrente
- Quando il periodo termina — la barra è finalizzata e ne inizia una nuova
Questo approccio garantisce che il backtest veda esattamente gli stessi dati del bot in tempo reale. Nessuno sguardo nel futuro — ogni candela al minuto viene elaborata strettamente con i dati che sarebbero stati disponibili in quel momento.
class RunningCandleBuffer:
"""
Emula gli aggiornamenti in tempo reale di una barra di timeframe superiore
usando candele a 1 minuto.
"""
def __init__(self, period_seconds: int):
self.period = period_seconds # 86400 per Daily, 3600 per 1h
self.closed_bars = []
self.current_bar = None
def update(self, timestamp, open_, high, low, close, volume):
bar_start = self._align_to_period(timestamp)
if self.current_bar is None or bar_start != self.current_bar['start']:
if self.current_bar is not None:
self.closed_bars.append(self.current_bar)
self.current_bar = {
'start': bar_start,
'open': open_, 'high': high,
'low': low, 'close': close,
'volume': volume,
}
else:
self.current_bar['high'] = max(self.current_bar['high'], high)
self.current_bar['low'] = min(self.current_bar['low'], low)
self.current_bar['close'] = close
self.current_bar['volume'] += volume
return self.closed_bars + [self.current_bar]
Un RunningCandleBuffer separato viene creato per ogni timeframe superiore. Ad ogni candela al minuto, tutti i buffer vengono aggiornati, fornendo lo stato corrente di ogni timeframe — come se il bot stesse operando in tempo reale.
Struttura della Cache Parquet
Il risultato del precalcolo è un singolo file parquet dove ogni riga corrisponde a una candela al minuto, e le colonne contengono:
timestamp — timestamp della candela al minuto
open, high, low, — OHLCV della candela al minuto
close, volume
close_5m — Close della candela 5m emulata in questo momento
close_1h — Close della candela 1h emulata
close_4h — Close della candela 4h emulata
close_1d — Close della candela giornaliera emulata
ma_20_1h — MA(20) su 1h, ricalcolata in questo minuto
ma_50_1h — MA(50) su 1h
ma_20_4h — MA(20) su 4h
ma_50_4h — MA(50) su 4h
ma_6_1d — MA(6) sul Daily
ma_12_1d — MA(12) sul Daily
cross_ma_1h — Segnale di incrocio MA su 1h ('buy'/'sell'/None)
cross_ma_4h — Segnale di incrocio MA su 4h
cross_ma_1d — Segnale di incrocio MA sul Daily
separation_1h — Divergenza MA in % su 1h
separation_4h — Divergenza MA in % su 4h
separation_1d — Divergenza MA in % sul Daily
Ogni valore riflette lo stato reale dell'indicatore al momento della corrispondente candela al minuto — tenendo conto delle barre non ancora chiuse dei timeframe superiori.
Precalcolo: Costruzione della Cache
def precompute_cache(
df_1m: pd.DataFrame,
timeframes: dict[str, int], # {"5m": 300, "1h": 3600, "4h": 14400, "D": 86400}
indicators: dict, # {"ma_20": 20, "ma_50": 50}
) -> pd.DataFrame:
"""
Singolo passaggio attraverso tutte le candele al minuto.
Restituisce un DataFrame con timeframe emulati e indicatori.
"""
buffers = {tf: RunningCandleBuffer(secs) for tf, secs in timeframes.items()}
n = len(df_1m)
result = {}
for tf_name, buf in buffers.items():
closes = np.zeros(n)
ma_values = {name: np.full(n, np.nan) for name in indicators}
for i in range(n):
row = df_1m.iloc[i]
bars = buf.update(
df_1m.index[i],
row['open'], row['high'], row['low'], row['close'], row['volume']
)
all_closes = [b['close'] for b in bars]
closes[i] = all_closes[-1]
for ind_name, length in indicators.items():
if len(all_closes) >= length:
ma_values[ind_name][i] = np.mean(all_closes[-length:])
result[f'close_{tf_name}'] = closes
for ind_name in indicators:
result[f'{ind_name}_{tf_name}'] = ma_values[ind_name]
cache_df = pd.DataFrame(result, index=df_1m.index)
cache_df = pd.concat([df_1m[['open', 'high', 'low', 'close', 'volume']], cache_df], axis=1)
return cache_df
cache = precompute_cache(
df_1m,
timeframes={"5m": 300, "1h": 3600, "4h": 14400, "D": 86400},
indicators={"ma_20": 20, "ma_50": 50, "ma_6": 6, "ma_12": 12},
)
cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")
Utilizzo della Cache Durante l'Ottimizzazione

Ora l'ottimizzazione si presenta così:
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
for params in parameter_grid:
result = run_strategy(cache, params)
La strategia lavora con colonne pre-costruite — nessun passaggio ripetuto attraverso un milione di barre, nessun ricalcolo delle MA, nessuna emulazione dei timeframe. Solo lettura da un DataFrame e verifica delle condizioni di ingresso/uscita.
Perché Parquet
Parquet è un formato di archiviazione dati colonnare, ottimale per questo compito:
- Compressione. Parquet comprime i dati numerici da 5 a 10 volte. Una cache di 1,1 milioni di righe con 30 colonne occupa ~50 MB invece di ~500 MB in CSV.
- Lettura colonnare. Se la strategia utilizza solo
ma_20_4hema_50_4h, parquet legge solo quelle colonne, saltando il resto. - Preservazione dei tipi. I tipi di dati (float64, int64, string) vengono preservati senza perdita — non è necessario analizzare le stringhe al caricamento.
- Velocità di lettura. Il caricamento di parquet in pandas richiede decine di millisecondi, un ordine di grandezza più veloce del CSV.
Estensione della Cache: Aggiunta di Nuovi Indicatori
Se la strategia richiede un nuovo indicatore (RSI, MACD, Bollinger Bands), è sufficiente:
- Ricalcolare solo il nuovo indicatore dagli stessi dati al minuto
- Aggiungere le colonne al file parquet esistente
- Tutte le colonne precedentemente calcolate rimangono intatte
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
rsi_cols = compute_rsi_for_timeframes(df_1m, timeframes, length=14)
cache = pd.concat([cache, rsi_cols], axis=1)
cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")
Riepilogo: Confronto degli Approcci
| Approccio Naive | Cache Aggregata | |
|---|---|---|
| Ricampionamento dei timeframe | Ad ogni iterazione | Una volta |
| Calcolo degli indicatori | Ad ogni iterazione | Una volta |
| Tempo per iterazione | Minuti | Meno di un secondo |
| 1000 iterazioni | Giorni | Minuti |
| Consumo di memoria | Carica 1m + ricalcola | Un singolo DataFrame |
| Parità backtest-live | Dipende dall'implementazione | Garantita (emulazione = tempo reale) |
Conclusione
L'approccio della cache parquet aggregata risolve due problemi simultaneamente:
-
Correttezza. L'emulazione dei timeframe dalle candele al minuto tramite RunningCandleBuffer garantisce che il backtest veda gli stessi dati del bot in tempo reale — nessuno sguardo nel futuro e nessun ritardo artificiale.
-
Velocità. I timeframe e gli indicatori precalcolati consentono di testare migliaia di combinazioni di parametri in minuti invece di giorni.
L'idea è semplice: calcolare una volta — riutilizzare molte volte. Le candele al minuto sono i dati sorgente. Tutto il resto è derivato e può essere precalcolato e memorizzato nella cache. Parquet rende questa cache compatta, veloce e conveniente.
Per ulteriori informazioni su come migliorare l'accuratezza della simulazione di esecuzione con drill-down adattivo dai minuti ai secondi e millisecondi, vedere l'articolo Drill-down adattivo: backtest con granularità variabile.
Link Utili
- Apache Parquet — formato di archiviazione dati
- pandas — lavorare con parquet
- Lopez de Prado — Advances in Financial Machine Learning
- Ernest Chan — Quantitative Trading
Citazione
@article{soloviov2026parquetcache,
author = {Soloviov, Eugen},
title = {Cache Parquet Aggregata: Come Accelerare i Backtest Multi-Timeframe di Centinaia di Volte},
year = {2026},
url = {https://marketmaker.cc/it/blog/post/parquet-cache-multitimeframe-backtest},
description = {Come precalcolare i timeframe e gli indicatori dalle candele al minuto, salvarli in parquet e utilizzarli per il testing di massa delle strategie senza ricalcoli ridondanti.}
}
Autori
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.