Using Multiple Indicators in Vectorbt

Using Multiple Indicators in Vectorbt

Running a single indicator is easy enough in vectorbt. Just pick one from the long list in the docs, plug it into one of the examples on the README, and job done.

Things get a little more dicey when you want to chain multiple indicators together. What if we want to backtest a strategy involving both an RSI and moving average crossover? In that case we'll need to use the indicator factory.

You can dig deep into the indicator factory in our blog post, but for our use case here I'll cover everything we need

Loading our data

As usual we'll want to grab some data here. I'm using the in-built yfinance connector which grabs data from yahoo finance.

 1>>> import pandas as pd
 2>>> import vectorbt as vbt
 3>>> import numpy as np
 4>>> btc_price_1d = vbt.YFData.download("BTC-USD",
 5...         missing_index='drop',
 6...         interval="1D").get('Close')
 7>>> btc_price_1d
 8Date
 92014-09-17 00:00:00+00:00      457.334015
102014-09-18 00:00:00+00:00      424.440002
112014-09-19 00:00:00+00:00      394.795990
122014-09-20 00:00:00+00:00      408.903992
132014-09-21 00:00:00+00:00      398.821014
14                                 ...     
152022-02-13 00:00:00+00:00    42197.515625
162022-02-14 00:00:00+00:00    42586.917969
172022-02-15 00:00:00+00:00    44575.203125
182022-02-16 00:00:00+00:00    43961.859375
192022-02-17 00:00:00+00:00    41088.687500

You can change the time-frame by using the interval parameter. You can also change the period that you gather data for using start and end, providing appropriate datetimes.

Make sure to import numpy, vectorbt, and pandas.

Creating the indicator

The next step is to go ahead and define our indicator as a normal python function. You can get creative with this. I've chosen to use the indicators built into vectorbt for compatibility. But there's no reason you couldn't use another library, or roll your own indicators in numpy like I do in our custom indicator tutorial. For the best possible performance I'd highly recommend using functions provided by TA-lib, but it can be difficult to configure on some systems so I've left it out here.

 1>>> def combine_indicators(close, window = 14, entry = 30, exit = 70, slow = 50, fast = 10):
 2...     trend = np.full(close.shape, np.nan)
 3...     rsi = vbt.RSI.run(close, window = window, short_name="rsi")
 4...     slow_ma = vbt.MA.run(close, slow)
 5...     fast_ma = vbt.MA.run(close, fast)
 6...     ma_signal = fast_ma.ma_above(slow_ma).to_numpy()
 7...     rsi_above = rsi.rsi_above(exit).to_numpy()
 8...     rsi_below = rsi.rsi_below(entry).to_numpy()
 9...     for x in range(len(close)):
10...         if rsi_above[x] == True and ma_signal[x] == True:
11...             trend[x] = 1
12...         elif rsi_below[x] == True or ma_signal[x] == False:
13...             trend[x] = -1
14...         else:
15...             trend[x] = 0
16...     return trend

This new indicator returns 1 for a buy signal, -1 for a sell signal, and 0 otherwise.

This might seem a bit much, so we'll break it down. We first create an empty numpy array, of the same length as our price array we pass in. This will eventually get filled with our buy or sell signals.

1trend = np.full(close.shape, np.nan)

Next we run our indicators using the in-built vectorbt indicators, which are in turn built with the indicator factory themselves. We avoid hard-coding any parameters, so that we can edit them and optimize the strategy later.

1rsi = vbt.RSI.run(close, window = window, short_name="rsi")
2    slow_ma = vbt.MA.run(close, slow)
3    fast_ma = vbt.MA.run(close, fast)

We then generate some signals based on the indicators. In particular we check if our fast_ma is above our slow_ma. And if our rsi is above or below our threshold. This produces a series of boolean values, the same as your normal entries or exits series would look in vbt. We then convert that into a numpy array for easier looping in the next section.

1ma_signal = fast_ma.ma_above(slow_ma).to_numpy()
2rsi_above = rsi.rsi_above(exit).to_numpy()
3rsi_below = rsi.rsi_below(entry).to_numpy()

Finally I just use a straight for loop here to iterate through the arrays and check our buy/sell conditions, setting the value of trend as appropriate.

1for x in range(len(close)):
2        if rsi_above[x] == True and ma_signal[x] == True:
3            trend[x] = 1 
4        elif rsi_below[x] == True or ma_signal[x] == False:
5            trend[x] = -1
6        else:
7            trend[x] = 0

Setting up the Indicator Factory

At this point we're ready to plug our function into the indicator factory. Most of the parameters are fairly straightforward. We're just telling vectorbt the names of our parameters and inputs.

 1>>> myInd = vbt.IndicatorFactory(
 2...         class_name="Combination",
 3...         short_name="comb",
 4...         input_names=["close"],
 5...         param_names=["window","entry","exit","slow","fast"],
 6...         output_names=["ind"]
 7...         ).from_apply_func(
 8...             combine_indicators,
 9...             window=14,
10...             entry=30,
11...             exit=70,
12...             slow=50,
13...             fast=10
14...         )

In from_apply_func we pass in firstly our function, then some default parameters to use.

Running our new indicator

Running our new indicator is just like running any other indicator in vbt.

 1>>> res = myInd.run(
 2...         btc_price_1d,
 3...         window=14,
 4...         entry = 30,
 5...         exit = 70,
 6...         slow = 50,
 7...         fast = 10,
 8...         param_product=True,
 9...         )
10>>> 
11>>> entries = res.ind == 1.0
12>>> exits = res.ind == -1.0

Note that you can provide arrays instead of atomic values to the parameters and it'll work just fine. The param_product parameter tells vbt to run all possible combinations of parameters that you've specified.

We can create an entries and exits series just by checking the values of the indicator. This can be plugged into the Portfolio.from_signals function quite nicely and voila!

 1>>> pf = vbt.Portfolio.from_signals(btc_price_1d, entries, exits, freq = "1D", fees=0.01)
 2>>> print(pf.stats())
 3Start                         2014-09-17 00:00:00+00:00
 4End                           2022-02-17 00:00:00+00:00
 5Period                               2711 days 00:00:00
 6Start Value                                       100.0
 7End Value                                   9355.399882
 8Total Return [%]                            9255.399882
 9Benchmark Return [%]                        8884.393498
10Max Gross Exposure [%]                            100.0
11Total Fees Paid                              977.147242
12Max Drawdown [%]                              58.325111
13Max Drawdown Duration                 547 days 00:00:00
14Total Trades                                         25
15Total Closed Trades                                  24
16Total Open Trades                                     1
17Open Trade PnL                              -895.330044
18.......

There we have our own custom indicator using a combination of built in functions. It's not the fastest or prettiest certainly. As I mentioned earlier, try out TA-lib and numba if you've got a need for speed.

Video tutorial

If you prefer a video tutorial, you can go through this free course on my youtube channel: