Features

Multi-timeframe analysis

Resample and realign data across timeframes by when each value became available

How do you use a daily indicator on hourly bars without peeking into the future? When a daily bar is labeled with the time it opens, its close is still only known when it ends. VBT resamples and realigns data by when each value became available, helping you avoid look-ahead bias in multi-timeframe backtests in Python.

Filter hourly entries with a daily trend, aligned naively and safely
h1_data = vbt.GBMOHLCData.pull(  
    "SYN",
    start="2024-01-01",
    end="2025-01-01",
    timeframe="1h",
    std=0.002,
    seed=7,
)
d1_data = h1_data.resample("1d")  
d1_uptrend = d1_data.close > d1_data.close.rolling(10).mean()

naive_filter = d1_uptrend.reindex(h1_data.index, method="ffill")  
safe_filter = (
    d1_uptrend.vbt.realign_closing(h1_data.index)  
    .fillna(False)
    .astype(bool)
)

h1_sma = h1_data.close.rolling(24).mean()
entries = h1_data.close.vbt.crossed_above(h1_sma)
exits = h1_data.close.vbt.crossed_below(h1_sma)
pf = vbt.PF.from_signals(
    h1_data,
    entries=pd.concat({
        "naive": entries & naive_filter,
        "safe": entries & safe_filter,
    }, axis=1),
    exits=exits,
    fees=0.0005,
)
pf.stats(["total_return", "sharpe_ratio", "win_rate", "total_trades"], agg_func=None)
       Total Return [%]  Sharpe Ratio  Win Rate [%]  Total Trades
naive         42.179045      0.863159     26.050420           239
safe         -56.766534     -1.172668     18.552036           222
pf.value.vbt.plot().show()
Portfolio value of an hourly strategy with a daily trend filter aligned naively and safely in 2024 Figure data (JSON)

The naive version looks profitable, and it is wrong. On a day when the daily close crosses above its average, the naive filter turns on at midnight, 23 hours before that close exists:

Compare when each filter turns on
filters = pd.DataFrame({"naive": naive_filter, "safe": safe_filter})
filters.loc["2024-01-28 23:00":"2024-01-30 00:00"].iloc[[0, 1, 2, 22, 23, 24, 25]]
                           naive   safe
2024-01-28 23:00:00+00:00  False  False
2024-01-29 00:00:00+00:00   True  False
2024-01-29 01:00:00+00:00   True  False
2024-01-29 21:00:00+00:00   True  False
2024-01-29 22:00:00+00:00   True  False
2024-01-29 23:00:00+00:00   True   True
2024-01-30 00:00:00+00:00   True   True

With the filter aligned to when the information existed, the same strategy loses money. The gap between the two curves is entirely look-ahead bias.

The hourly row labeled 23:00 represents the bar from 23:00 to midnight. Its close and the daily close become known together at the end of that bar. The safe filter on that row is for a decision at the bar's close. For execution on the next bar, the signal backtesting page covers delaying signals.

Combine timeframes in one strategy

Give each timeframe a job, then align the results to the bars on which you make decisions:

Research ideaHow to build it
Trade hourly entries only when the daily trend agreesCalculate the daily filter and align it to hourly closes, as above
Use a four-hour momentum signal with 15-minute entriesCompute on four-hour bars and realign the output to the 15-minute index
Compare the same indicator on several timeframesPass a list to the TA-Lib timeframe parameter
Review a minute-by-minute simulation as monthly returnsResample the completed portfolio for reporting

Start with data at the finest resolution your strategy needs. One minute-bar dataset can supply five-minute, hourly, and daily views. You can also combine data that was downloaded at different resolutions by realigning each result to a common target index.

Opening and closing alignment

Two rules cover most cases. Values derived from the open of a bar are known at its start and are realigned with realign_opening. Values derived from its close, high, low, or volume are known at its end and are realigned with realign_closing. Indicators computed from closes follow the closing rule. Both work in either direction: downsampling hourly data to daily bars, or bringing daily values back onto hourly bars.

Realigned masks come back as floats, with NaN on the bars before the first value is known. Fill them with False before casting, as safe_filter does above, because NaN cast to a boolean is True and would switch the filter on from the first bar.

data.resample() aggregates whole OHLCV datasets with the right function for each column, and vbt.Resampler maps between any two indexes, including irregular ones and custom bounds such as trading sessions. The Safe resampling highlight below runs one indicator on daily, weekly, and monthly closes and shows all three on daily bars.

Build daily bars from intraday data, or carry a completed daily level into an hourly strategy. VBT handles both aggregation and alignment, so each timeframe can play its own role in the same backtest.

For datasets with extra fields, custom aggregation rules can say how those fields behave when resampled. VBT's provider classes already include rules for fields such as Binance quote volume.

Resampling whole objects

Data is not the only thing that can change timeframe. Indicators, records, and whole portfolios resample too, each with rules for its parts: prices take the last value, cash flows are summed, and orders keep their timestamps. A common pattern is to simulate on fine bars for accurate fills and analyze on coarse bars for speed and readability, as in the Resamplable objects highlight below. Resampling the finished portfolio changes the reporting frequency. To test trading decisions on a different timeframe, resample the inputs and run the simulation at that timeframe.

Frequencies

Annualized metrics and resampling need the bar frequency. VBT infers it from a sample of consecutive timestamps, which works even with occasional gaps. When the data starts irregularly or has no regular spacing, pass freq explicitly or set it once in vbt.settings.wrapping["freq"]. Indexes with different datetime resolutions, such as milliseconds and nanoseconds, should be converted to the same resolution before realigning.

Indicators on other timeframes

Every TA-Lib indicator takes a timeframe argument. The input is resampled to that timeframe, the indicator is computed there, and the output is realigned safely to the original bars, so a list of timeframes becomes a parameter like any other. Other indicators take the same three steps by hand: resample the input, compute the indicator, and realign the output with realign_closing. The TA-Lib time frames highlight below runs one indicator on three timeframes.

The indicator's window is measured in bars of the calculation timeframe. A 14-period daily SMA uses 14 daily closes, while a 14-period hourly SMA uses 14 hourly closes. The daily result is then carried back onto the hourly index, where it changes as each new daily value becomes available. You can compare timeframe choices alongside other parameters using parameter optimization.

A completed higher-timeframe bar is only known at its end. Some platforms instead update the indicator on every lower bar using the bar that is still forming. That gives earlier values that change until the bar closes. If you need that behavior, compute it explicitly from the partial bar at each step, and backtest with the values exactly as they were at the time.

Tutorial

The members-only MTF analysis tutorial covers resampling, alignment, and aggregation in depth.

Safe resampling

✅ Look-ahead bias is an ongoing risk when working with array data, especially on multiple time frames. Using Pandas alone is strongly discouraged because it does not recognize that financial data mainly involves bars where timestamps are the opening times, and events may occur at any time between bars. Pandas thus incorrectly assumes that timestamps indicate the exact time of an event. In VBT, there is a complete collection of functions and classes for safely resampling and analyzing data!

Calculate SMA on multiple time frames and display on the same chart
def mtf_sma(close, close_freq, target_freq, timeperiod=5):
    target_close = close.vbt.realign_closing(target_freq)  
    target_sma = vbt.talib("SMA").run(target_close, timeperiod=timeperiod).real  
    target_sma = target_sma.rename(f"SMA ({target_freq})")
    return target_sma.vbt.realign_closing(close.index, freq=close_freq)  

data = vbt.YFData.pull("BTC-USD", start="2020", end="2023")
fig = mtf_sma(data.close, "D", "daily").vbt.plot()
mtf_sma(data.close, "D", "weekly").vbt.plot(fig=fig)
mtf_sma(data.close, "D", "monthly").vbt.plot(fig=fig)
fig.show()
Bitcoin simple moving averages calculated on daily, weekly, and monthly timeframes Figure data (JSON)

Tutorial

Learn more in the MTF analysis tutorial.

✅ You can resample not only time series, but also complex VBT objects! Under the hood, each object is made up of a collection of array-like attributes, so resampling means aggregating all the related information together. This is especially helpful if you want to simulate at a higher frequency for maximum accuracy and then analyze at a lower frequency for better speed.

Plot the monthly return heatmap of a random portfolio
import calendar

data = vbt.YFData.pull("BTC-USD", start="2018", end="2023")
pf = vbt.PF.from_random_signals(data, n=100, direction="both", seed=42)
mo_returns = pf.resample("M").returns  
mo_return_matrix = pd.Series(
    mo_returns.values,
    index=pd.MultiIndex.from_arrays([
        mo_returns.index.year,
        mo_returns.index.month
    ], names=["year", "month"])
).unstack("month")
mo_return_matrix.columns = mo_return_matrix.columns.map(lambda x: calendar.month_abbr[x])
mo_return_matrix.vbt.heatmap(
    is_x_category=True,
    trace_kwargs=dict(zmid=0, colorscale="Spectral")
).show()
Monthly return heatmap for a seeded random Bitcoin portfolio from 2018 through 2022 Figure data (JSON)

Tutorial

Learn more in the MTF analysis tutorial.

✅ Comparing indicators on different time frames involves many nuances. Now, all TA-Lib indicators support a parameter that resamples the input arrays to a target time frame, calculates the indicator, and then resamples the output arrays back to the original time frame. This makes parameterized MTF analysis easier than ever!

Run SMA on multiple time frames and display the whole thing as a heatmap
h1_data = vbt.BinanceData.pull(
    "BTCUSDT",
    start="3 months ago UTC",
    timeframe="1h"
)
mtf_sma = vbt.talib("SMA").run(
    h1_data.close,
    timeperiod=14,
    timeframe=["1d", "4h", "1h"],
    skipna=True
)
mtf_sma.real.vbt.ts_heatmap().show()
BTCUSDT simple moving averages across hourly, four-hour, and daily timeframes Figure data (JSON)

Tutorial

Learn more in the MTF analysis tutorial.

Copyright © 2021–2026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.