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.
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 222pf.value.vbt.plot().show()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:
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 TrueWith 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 idea | How to build it |
|---|---|
| Trade hourly entries only when the daily trend agrees | Calculate the daily filter and align it to hourly closes, as above |
| Use a four-hour momentum signal with 15-minute entries | Compute on four-hour bars and realign the output to the 15-minute index |
| Compare the same indicator on several timeframes | Pass a list to the TA-Lib timeframe parameter |
| Review a minute-by-minute simulation as monthly returns | Resample 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.
✅ 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!
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()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.
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()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!
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()Tutorial
Learn more in the MTF analysis tutorial.
Related pages
- Backtesting › Intraday and tick backtestingBacktest minute bars, ticks, and sessions, and resolve stops on finer data
- Backtesting › Realistic backtestsAvoid look-ahead bias, impossible fills, and survivorship bias in your backtests
- Research toolkit › Multidimensional researchRun assets, parameters, and strategies as labeled columns, then index and stack them
- Backtesting › Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
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.