Backtest powerful intraday trading strategies
Backtest powerful intraday trading strategies
Multi-timeframe (MTF) analysis lets traders build powerful intraday trading strategies.
It does this by analyzing asset prices during different timeframes throughout the trading day.
The problem is most people get MTF wrong.
It requires a vector-based backtest to speed up the operations making it easy to introduce look-ahead bias.
When a backtest introduces look-ahead bias, it will overstate performance.
So how do we do MTF analysis “the right way?”
By reading today’s newsletter, you’ll be able to use VectorBT Pro (VBT) to to MTF the right way.
Backtest powerful intraday trading strategies
Many vector-based backtesters assume events take place at the same timestamp as the data provided by the exchange.
This is typically the opening time of a bar.
VBT takes a different approach.
It assumes the timing of most events is unknown and occurs between the opening (best-case) and closing (worst-case) times of a bar.
To do this, VBT uses a set of features designed to resample data in the most sensitive way, without looking into the future.
Sound complicated?
It’s only a few lines of Python code. Let’s see how it works!
Imports and set up
Using the latest version of VBT, the star-import (*) loads all the relevant libraries for us. (Don’t worry, it won’t pollute your namespace.)
Many vector-based backtesters assume events take place at the same timestamp as the data provided by the exchange.
This is typically the opening time of a bar.
VBT takes a different approach.
It assumes the timing of most events is unknown and occurs between the opening (best-case) and closing (worst-case) times of a bar.
To do this, VBT uses a set of features designed to resample data in the most sensitive way, without looking into the future.
Sound complicated?
It’s only a few lines of Python code. Let’s see how it works!
1from vectorbtpro import *
2vbt.settings.set_theme("dark")
3vbt.settings.plotting.auto_rangebreaks = True
Grab the data of a higher frequency for your favorite asset. We'll use hourly TSLA.
1data = vbt.YFData.pull("TSLA", start="2023", end="2024", timeframe="hourly")
Multi-timeframe indicators
Instruct VBT to calculate the fast and slow SMA indicators across multiple timeframes.
1fast_sma = data.run(
2 "talib:sma",
3 timeframe=["1h", "4h", "1d"],
4 timeperiod=vbt.Default(20),
5 skipna=True
6)
7slow_sma = data.run(
8 "talib:sma",
9 timeframe=["1h", "4h", "1d"],
10 timeperiod=vbt.Default(50),
11 skipna=True
12)
Under the hood, data is first resampled to the target timeframe. Then, the TA-Lib indicator is applied only to non-missing values. Finally, the result is realigned back to the original timeframe in a way that eliminates the possibility of look-ahead bias.
VBT makes it easy to visualize the results.
1fast_sma.real.vbt.plot().show_png()
We can see the line corresponding to the highest frequency is smooth, whereas the line representing the lowest frequency appears stepped since the indicator values are updated less frequently.
Build a unified portfolio
Next, we'll set up a portfolio that goes long whenever the fast SMA crosses above the slow SMA and go short when the opposite occurs, across each timeframe.
Since hourly signals occur more frequently than daily signals, we'll allocate less capital to more frequent signals.
We'll allocate 5% of the equity to hourly signals, 10% to 4-hour signals, and 20% to daily signals.
We'll begin with a cash balance of $10,000, shared across all timeframes. Additionally, we'll implement a 20% trailing stop loss (TSL).
1pf = vbt.PF.from_signals(
2 data,
3 long_entries=fast_sma.real_crossed_above(slow_sma),
4 short_entries=fast_sma.real_crossed_below(slow_sma),
5 size=[[0.05, 0.1, 0.2]],
6 size_type="valuepercent",
7 init_cash=10_000,
8 group_by=["pf", "pf", "pf"],
9 cash_sharing=True,
10 tsl_stop=0.2
11)
Plot the cumulative return for each timeframe and compare these to the cumulative return of the entire portfolio.
1fig = (
2 pf
3 .get_cumulative_returns()
4 .vbt
5 .plot(trace_kwargs=dict(line_color="gray", line_dash="dot"))
6)
7fig = pf.get_cumulative_returns(group_by=False).vbt.plot(fig=fig)
8fig.show_png()
To dive deeper into one of the timeframes, we can plot the indicators alongside the executed trade signals.
Here, we can observe that the majority of positions on the daily timeframe were closed out by the TSL.
1fig = fast_sma.real.vbt.plot(
2 column="1d",
3 trace_kwargs=dict(name="Fast", line_color="limegreen")
4)
5fig = slow_sma.real.vbt.plot(
6 column="1d",
7 trace_kwargs=dict(name="Slow", line_color="orangered"),
8 fig=fig
9)
10fig = pf.plot_trade_signals(column="1d", fig=fig)
11fig.show_png()
Timeframe product
Since our MTF indicators share the same index, we can combine one timeframe with another. Lets generate signals from the crossover of two timeframes and identify the pair of timeframes that yield the highest expectancy.
1fast_sma_real = fast_sma.real.vbt.rename_levels({"sma_timeframe": "fast_sma_timeframe"})
2slow_sma_real = slow_sma.real.vbt.rename_levels({"sma_timeframe": "slow_sma_timeframe"})
3fast_sma_real, slow_sma_real = fast_sma_real.vbt.x(slow_sma_real)
4long_entries = fast_sma_real.vbt.crossed_above(slow_sma_real)
5short_entries = fast_sma_real.vbt.crossed_below(slow_sma_real)
6pf = vbt.PF.from_signals(data, long_entries=long_entries, short_entries=short_entries)
7pf.trades.expectancy.sort_values(ascending=False)
I looks like the 1 hour fast MA and 4 hour slow MA achieve the highest expectancy of a profitable trade.
Next steps
Timeframe is yet another parameter of your strategy that can be tweaked. For example, you can go to uncharted territory and test more unconventional timeframes like "1h 30min" to discover potentially novel insights. Similar to other parameters, timeframes should also undergo cross-validation.
However, unlike regular parameters, timeframes should be regarded as a distinct dimension that provides a unique perspective on your strategy.