← Voltar aos artigos
March 16, 2026
5 min read

Cache Parquet Agregado: Como Acelerar Backtests Multi-Timeframe Centenas de Vezes

Cache Parquet Agregado: Como Acelerar Backtests Multi-Timeframe Centenas de Vezes
#algotrading
#backtest
#multi-timeframe
#parquet
#optimization
#caching
Part 3 of 10 · Collection
High-Performance Backtest Engines

Uma estratégia multi-timeframe usa vários timeframes simultaneamente: o diário determina a direção da tendência, o horário identifica os pontos de entrada, e o de 5 minutos define o momento exato de execução. Cada timeframe requer seus próprios indicadores: médias móveis, osciladores, níveis.

Para um único backtest, tudo é simples — recalcular os timeframes a partir de dados de um minuto, calcular os indicadores, executar a estratégia. Mas durante a otimização em massa — quando é preciso testar milhares de combinações de parâmetros — recalcular timeframes e indicadores a cada iteração se torna um gargalo. Uma única passagem por dados de um minuto ao longo de dois anos significa processar mais de um milhão de barras, e repetir isso mil vezes é um desperdício.

A solução: pré-calcular tudo uma vez e armazenar em cache num arquivo parquet.

O Problema: Cálculos Redundantes Durante a Otimização

Um pipeline típico de backtest multi-timeframe se parece com isto:

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)

A cada iteração, as etapas 1-3 são recalculadas mesmo que os dados sejam os mesmos. Apenas os parâmetros de limiar da estratégia mudam (etapa 4). É como reconstruir uma casa inteira toda vez que se quer apenas experimentar uma cor diferente de parede.

A Ideia: Calcular Uma Vez, Salvar, Reutilizar Muitas Vezes

A observação-chave: os timeframes e indicadores dependem apenas dos dados de um minuto e dos parâmetros dos indicadores, não dos parâmetros da estratégia. Se fixarmos o conjunto de indicadores necessários, podemos calculá-los uma vez e salvá-los.

O esquema:

Step 1 (once):
  Minute candles -> Timeframe resampling -> Indicator computation -> Parquet file

Step 2 (many times):
  Parquet file -> Strategy with different parameters -> Result

Emulando Timeframes a Partir de Candles de Um Minuto

Real-time timeframe emulation visualization

Dispomos de um arquivo completo de candles de um minuto. A partir dele, podemos reproduzir com precisão qualquer timeframe superior. Mas há uma sutileza: com um resample padrão, obtemos uma linha por período (uma linha por hora, uma por 4 horas, etc.). Isso não funciona para um backtesting minuto a minuto — precisamos conhecer o valor do indicador a cada minuto.

Por isso, emulamos os valores dos timeframes superiores para cada candle de um minuto, modelando como o bot vê os dados em tempo real:

  1. O bot recebe o próximo candle de um minuto
  2. Atualiza a barra atual (não fechada) do timeframe superior — recalcula High, Low, Close, Volume
  3. Recalcula o indicador sobre todas as barras fechadas mais a barra parcial atual
  4. Quando o período termina — a barra é finalizada e uma nova começa

Essa abordagem garante que o backtest veja exatamente os mesmos dados que o bot vê em tempo real. Sem olhar para o futuro — cada candle de um minuto é processado estritamente com os dados que estariam disponíveis naquele momento.

class RunningCandleBuffer:
    """
    Emulates real-time updates of a higher timeframe bar
    using 1-minute candles.
    """
    def __init__(self, period_seconds: int):
        self.period = period_seconds  # 86400 for Daily, 3600 for 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]

Um RunningCandleBuffer separado é criado para cada timeframe superior. A cada candle de um minuto, todos os buffers são atualizados, dando-nos o estado atual de cada timeframe — como se o bot estivesse rodando em tempo real.

Estrutura do Cache Parquet

O resultado do pré-cálculo é um único arquivo parquet onde cada linha corresponde a um candle de um minuto, e as colunas contêm:

timestampminute candle timestamp
open, high, low,       — minute candle OHLCV
close, volume

close_5m               — Close of the emulated 5m candle at this moment
close_1h               — Close of the emulated 1h candle
close_4h               — Close of the emulated 4h candle
close_1d               — Close of the emulated daily candle

ma_20_1h               — MA(20) on 1h, recalculated at this minute
ma_50_1h               — MA(50) on 1h
ma_20_4h               — MA(20) on 4h
ma_50_4h               — MA(50) on 4h
ma_6_1d                — MA(6) on Daily
ma_12_1d               — MA(12) on Daily

cross_ma_1h            — MA crossover signal on 1h ('buy'/'sell'/None)
cross_ma_4h            — MA crossover signal on 4h
cross_ma_1d            — MA crossover signal on Daily

separation_1h          — MA divergence in % on 1h
separation_4h          — MA divergence in % on 4h
separation_1d          — MA divergence in % on Daily

Cada valor reflete o estado real do indicador no momento do candle de um minuto correspondente — levando em conta as barras não fechadas de timeframes superiores.

Precompute: Construindo o 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:
    """
    Single pass through all minute candles.
    Returns a DataFrame with emulated timeframes and indicators.
    """
    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")

Usando o Cache Durante a Otimização

Cache-based optimization speedup comparison

Agora a otimização se parece com isto:

cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

for params in parameter_grid:
    result = run_strategy(cache, params)

A estratégia trabalha com colunas já construídas — sem passagens repetidas por um milhão de barras, sem recálculos de médias móveis, sem emulação de timeframes. Apenas leitura de um DataFrame e verificação das condições de entrada/saída.

Por Que Parquet

Parquet é um formato de armazenamento de dados colunar, ideal para essa tarefa:

  • Compressão. O Parquet comprime dados numéricos de 5 a 10 vezes. Um cache de 1,1 milhão de linhas com 30 colunas ocupa ~50 MB em vez de ~500 MB em CSV.
  • Leitura colunar. Se a estratégia usa apenas ma_20_4h e ma_50_4h, o parquet lê somente essas colunas, ignorando o restante.
  • Preservação de tipos. Os tipos de dados (float64, int64, string) são preservados sem perdas — não é necessário fazer parsing de strings ao carregar.
  • Velocidade de leitura. Carregar parquet no pandas leva dezenas de milissegundos, uma ordem de grandeza mais rápido que CSV.

Estendendo o Cache: Adicionando Novos Indicadores

Se a estratégia exigir um novo indicador (RSI, MACD, Bandas de Bollinger), basta:

  1. Recalcular apenas o novo indicador a partir dos mesmos dados de um minuto
  2. Adicionar as colunas ao arquivo parquet existente
  3. Todas as colunas previamente calculadas permanecem intactas
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")

Resumo: Comparação de Abordagens

Abordagem Ingênua Cache Agregado
Resampling de timeframes A cada iteração Uma vez
Cálculo de indicadores A cada iteração Uma vez
Tempo por iteração Minutos Menos de um segundo
1000 iterações Dias Minutos
Consumo de memória Carregar 1m + recalcular Um único DataFrame
Paridade backtest-live Depende da implementação Garantida (emulação = tempo real)

Conclusão

A abordagem de cache parquet agregado resolve dois problemas simultaneamente:

  1. Correção. A emulação de timeframes a partir de candles de um minuto via RunningCandleBuffer garante que o backtest veja os mesmos dados que o bot em tempo real — sem olhar para o futuro e sem atrasos artificiais.

  2. Velocidade. Timeframes e indicadores pré-calculados permitem testar milhares de combinações de parâmetros em minutos em vez de dias.

A ideia é simples: calcular uma vez — reutilizar muitas vezes. Os candles de um minuto são o dado de origem. Tudo o mais é derivado e pode ser pré-calculado e armazenado em cache. O Parquet torna esse cache compacto, rápido e conveniente.

Para mais informações sobre como melhorar a precisão da simulação de execuções com drill-down adaptativo de minutos para segundos e milissegundos, veja o artigo Drill-down adaptativo: backtest com granularidade variável.


Links Úteis

  1. Apache Parquet — data storage format
  2. pandas — working with parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. Ernest Chan — Quantitative Trading

Citação

@article{soloviov2026parquetcache,
  author = {Soloviov, Eugen},
  title = {Aggregated Parquet Cache: How to Speed Up Multi-Timeframe Backtests by Hundreds of Times},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/parquet-cache-multitimeframe-backtest},
  description = {How to precompute timeframes and indicators from minute candles, save them to parquet, and use them for mass strategy testing without redundant recalculations.}
}
blog.disclaimer

Authors

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

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.