← Torna agli articoli
March 16, 2026
5 min di lettura

Cache Parquet Aggregata: Come Accelerare i Backtest Multi-Timeframe di Centinaia di Volte

Cache Parquet Aggregata: Come Accelerare i Backtest Multi-Timeframe di Centinaia di Volte
#algotrading
#backtest
#multi-timeframe
#parquet
#ottimizzazione
#caching

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

Visualizzazione dell'emulazione dei timeframe in tempo reale

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:

  1. Il bot riceve la prossima candela al minuto
  2. Aggiorna la barra corrente (non ancora chiusa) del timeframe superiore — ricalcola High, Low, Close, Volume
  3. Ricalcola l'indicatore su tutte le barre chiuse più la barra parziale corrente
  4. 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

Confronto dell'accelerazione dell'ottimizzazione basata su cache

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_4h e ma_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:

  1. Ricalcolare solo il nuovo indicatore dagli stessi dati al minuto
  2. Aggiungere le colonne al file parquet esistente
  3. 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:

  1. 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.

  2. 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

  1. Apache Parquet — formato di archiviazione dati
  2. pandas — lavorare con parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. 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.}
}
Disclaimer: le informazioni fornite in questo articolo hanno solo scopo didattico e informativo e non costituiscono consulenza finanziaria, di investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.

Autori

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Resta un Passo Avanti al Mercato

Iscriviti alla nostra newsletter per approfondimenti esclusivi sul trading con IA, analisi di mercato e aggiornamenti sulla piattaforma.

Rispettiamo la tua privacy. Annulla l'iscrizione in qualsiasi momento.