# Multi-timeframe analysis (/features/data/multi-timeframe-analysis)

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.

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

>>> naive_filter = d1_uptrend.reindex(h1_data.index, method="ffill")  # (3)
>>> safe_filter = (
...     d1_uptrend.vbt.realign_closing(h1_data.index)  # (4)
...     .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()
```

1.  A year of seeded synthetic hourly bars, so the result does not depend on a data provider.
2.  Aggregate hourly bars into daily bars: first open, highest high, lowest low, last close.
3.  The pandas way: each daily value is copied to every hour from the label of its day onward.
4.  The safe way: each daily value becomes available on the hourly bar where that day closes.

Portfolio value of an hourly strategy with a daily trend filter aligned naively and safely in 2024. [Figure data (JSON)](/assets/figures/features/data/mtf-naive-vs-safe.ead37995507f.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:

```python title="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](/features/backtesting/signal-backtesting/) covers delaying signals.

## Combine timeframes in one strategy \[#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 \[#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](#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 \[#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](#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 \[#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 \[#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](#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](/features/optimization/strategy-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.

!!! info "Tutorial"
    The members-only [MTF analysis](https://members.vectorbt.pro/tutorials/mtf-analysis/resampling/) tutorial covers
    resampling, alignment, and aggregation in depth.

## Safe resampling \[#safe-resampling]

New in 1.1.2

✅ [Look-ahead bias](https://www.investopedia.com/terms/l/lookaheadbias.asp) 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!

```python title="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)  # (1)
...     target_sma = vbt.talib("SMA").run(target_close, timeperiod=timeperiod).real  # (2)
...     target_sma = target_sma.rename(f"SMA ({target_freq})")
...     return target_sma.vbt.realign_closing(close.index, freq=close_freq)  # (3)

>>> 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()
```

1.  Resample the source frequency to the target frequency. Since Close occurs at the end of the bar,
    resample it as a "closing event".
2.  Calculate the SMA on the target frequency.
3.  Resample the target frequency back to the source frequency to show multiple time frames on the
    same chart. Because `close` contains gaps, you cannot simply resample to `close_freq` as this
    might produce unaligned series. Instead, resample directly to the index of `close`.

Bitcoin simple moving averages calculated on daily, weekly, and monthly timeframes. [Figure data (JSON)](/assets/figures/features/productivity/safe-resampling.5bdd558681a0.json)

!!! info "Tutorial"
    Learn more in the [MTF analysis](/tutorials/mtf-analysis) tutorial.

## Resamplable objects \[#resamplable-objects]

New in 1.1.2

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

```python title="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  # (1)
>>> 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()
```

1.  Resample the entire portfolio to monthly frequency and calculate the returns.

Monthly return heatmap for a seeded random Bitcoin portfolio from 2018 through 2022. [Figure data (JSON)](/assets/figures/features/productivity/monthly-return-heatmap.36c65fb0009f.json)

!!! info "Tutorial"
    Learn more in the [MTF analysis](/tutorials/mtf-analysis) tutorial.

## TA-Lib time frames \[#ta-lib-time-frames]

New in 1.2.0

✅ 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!

```python title="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)](/assets/figures/features/indicators/ta-lib-time-frames.a70363fe5509.json)

!!! info "Tutorial"
    Learn more in the [MTF analysis](/tutorials/mtf-analysis) tutorial.


## Related pages

*   [Intraday and tick backtesting](/features/backtesting/intraday-and-tick-backtesting/): Backtest minute bars, ticks, and sessions, and resolve stops on finer data
*   [Realistic backtests](/features/backtesting/backtest-realism/): Avoid look-ahead bias, impossible fills, and survivorship bias in your backtests
*   [Multidimensional research](/features/tooling/multidimensional-research/): Run assets, parameters, and strategies as labeled columns, then index and stack them
*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once