Skip to content

Indicators

Signal unraveling

  • To backtest each signal in isolation, you can now "unravel" each signal or pair of entry and exit signals into a separate column. This will generate a wide, two-dimensional mask which, when backtested, returns performance metrics for each signal rather than entire column.
For each signal, create a separate position with own stop orders
>>> data = vbt.YFData.pull("BTC-USD")
>>> fast_sma = data.run("talib_func:sma", timeperiod=20)  # (1)!
>>> slow_sma = data.run("talib_func:sma", timeperiod=50)
>>> entries = fast_sma.vbt.crossed_above(slow_sma)
>>> exits = fast_sma.vbt.crossed_below(slow_sma)
>>> entries, exits = entries.vbt.signals.unravel_between(exits, relation="anychain")  # (2)!
>>> pf = vbt.PF.from_signals(
...     data, 
...     long_entries=entries, 
...     short_entries=exits, 
...     size=100,  # (3)!
...     size_type="value",
...     init_cash="auto",  # (4)!
...     tp_stop=0.2, 
...     sl_stop=0.1, 
...     group_by=vbt.ExceptLevel("signal"),  # (5)!
...     cash_sharing=True
... )
>>> pf.positions.returns.to_pd(ignore_index=True).vbt.barplot(
...     trace_kwargs=dict(marker=dict(colorscale="Spectral"))
... ).show()  # (6)!
  1. Run a TA-Lib function faster without building an indicator - "talib_func:sma" vs "talib:sma"
  2. Put each pair of entry->exit and exit->entry signals into a separate column
  3. Order $100 of asset
  4. Simulate with infinite cash
  5. Combine all columns (i.e., positions) under the same asset to a single portfolio
  6. Show position return by signal index

Lightweight TA-Lib

  • TA-Lib functions wrapped with the indicator factory are very powerful because in contrast to the official TA-Lib implementation they know how to broadcast, handle DataFrames, skip missing values, and even resample to a different timeframe. Because TA-Lib functions on their own are lightning fast, wrapping them with the indicator factory introduces an overhead. To retain both the speed of TA-Lib and the power of VBT, the added functionality has been outsourced into a separate lightweight function that can be called by the user just like a regular TA-Lib function.
Run and plot an RSI resampled to the monthly timeframe
>>> data = vbt.YFData.pull("BTC-USD")
>>> run_rsi = vbt.talib_func("rsi")
>>> rsi = run_rsi(data.close, timeperiod=12, timeframe="M")  # (1)!
>>> rsi
Date
2014-09-17 00:00:00+00:00          NaN
2014-09-18 00:00:00+00:00          NaN
2014-09-19 00:00:00+00:00          NaN
2014-09-20 00:00:00+00:00          NaN
2014-09-21 00:00:00+00:00          NaN
                                   ...
2024-01-18 00:00:00+00:00    64.210811
2024-01-19 00:00:00+00:00    64.210811
2024-01-20 00:00:00+00:00    64.210811
2024-01-21 00:00:00+00:00    64.210811
2024-01-22 00:00:00+00:00    64.210811
Freq: D, Name: Close, Length: 3415, dtype: float64

>>> plot_rsi = vbt.talib_plot_func("rsi")
>>> plot_rsi(rsi).show()
  1. Parameters are applied to the target timeframe

  • VectorBT PRO implements or integrates a total of over 500 indicators, which makes it increasingly difficult to keep an overview. To make them more discoverable, there is a bunch of new methods to globally search for indicators.
List all moving average indicators
>>> vbt.IF.list_indicators("*ma")
[
    'vbt:MA',
    'talib:DEMA',
    'talib:EMA',
    'talib:KAMA',
    'talib:MA',
    ...
    'technical:ZEMA',
    'technical:ZLEMA',
    'technical:ZLHMA',
    'technical:ZLMA'
]

>>> vbt.indicator("technical:ZLMA")  # (1)!
vectorbtpro.indicators.factory.technical.ZLMA
  1. Same as vbt.IF.get_indicator

Indicators for ML

  • Want to feed indicators as features to a machine-learning model? No more need to run them individually: you can instruct VBT to run all indicators of an indicator package on the given data instance; the data instance will recognize input names of each indicator and pass the required data. You can also easily change the defaults of each indicator.
Run all talib indicators on entire BTC-USD history
>>> data = vbt.YFData.pull("BTC-USD")
>>> features = data.run("talib", mavp=vbt.run_arg_dict(periods=14))
>>> features.shape
(3046, 175)

Signal detection

  • VectorBT PRO implements an indicator based on a robust peak detection algorithm using z-scores. This indicator can be used to identify outbreaks and outliers in any time-series data.
Detect sudden changes in the bandwidth of a Bollinger Bands indicator
>>> data = vbt.YFData.pull("BTC-USD")
>>> fig = vbt.make_subplots(rows=2, cols=1, shared_xaxes=True)
>>> bbands = data.run("bbands")
>>> bbands.loc["2022"].plot(add_trace_kwargs=dict(row=1, col=1), fig=fig)
>>> sigdet = vbt.SIGDET.run(bbands.bandwidth, factor=5)
>>> sigdet.loc["2022"].plot(add_trace_kwargs=dict(row=2, col=1), fig=fig)
>>> fig.show()

Pivot detection

  • Pivot detection indicator is a tool that can be used to find out when the price's trend is reversing. By determining the support and resistance areas, it helps to identify significant changes in price while filtering out short-term fluctuations, thus eliminating the noise. The workings are rather simple: register a peak when the price jumps above one threshold and a valley when the price falls below another threshold. Another advantage: in contrast to the regular Zig Zag indicator, which tends to look into the future, our indicator returns only the confirmed pivot points and is safe to use in backtesting.
Plot the last pivot value
>>> data = vbt.YFData.pull("BTC-USD", start="2020", end="2023")
>>> fig = data.plot(plot_volume=False)
>>> pivot_info = data.run("pivotinfo", up_th=1.0, down_th=0.5)
>>> pivot_info.plot(fig=fig, conf_value_trace_kwargs=dict(visible=False))
>>> fig.show()

Technical indicators

  • VectorBT PRO integrates most of the indicators and consensus classes from the freqtrade's technical library.
Compute and plot the summary consensus with a one-liner
>>> vbt.YFData.pull("BTC-USD").run("sumcon", smooth=100).plot().show()

Renko chart

  • In contrast to regular charts, Renko chart is built using price movements. Each "brick" in this chart is created when the price moves a specified price amount. Since the output has irregular time intervals, only one column can be processed at a time. As with everything, the VBT's implementation can translate a huge number of data points very fast thanks to Numba.
Resample closing price into a Renko format
>>> data = vbt.YFData.pull("BTC-USD", start="2021", end="2022")
>>> renko_ohlc = data.close.vbt.to_renko_ohlc(1000, reset_index=True)  # (1)!
>>> renko_ohlc.vbt.ohlcv.plot().show()
  1. Bitcoin is very volatile, hence the brick size of 1000

Rolling OLS

  • Rolling regressions are one of the models for analyzing changing relationships among variables over time. In VBT, it's implemented as an indicator that takes two time series and returns the slope, intercept, prediction, error, and the z-score of the error at each time step. Not only this indicator can be used for cointegration tests, such as to determine optimal rebalancing timings in pairs trading, but it's also (literally) 1000x faster than the statsmodels' equivalent RollingOLS 🔥
Determine the spread between BTC and ETH
>>> data = vbt.YFData.pull(
...     ["BTC-USD", "ETH-USD"], 
...     start="2022", 
...     end="2023",
...     missing_index="drop"
... )
>>> ols = vbt.OLS.run(
...     data.get("Close", "BTC-USD"), 
...     data.get("Close", "ETH-USD")
... )
>>> ols.plot_zscore().show()

TA-Lib time frames

  • Comparing indicators on different time frames involves a lot of nuances. Thankfully, all TA-Lib indicators now 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 - a parameterized MTF analysis has never been easier!

Tutorial

Learn more in the MTF analysis tutorial.

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

1D-native indicators

  • Previously, own indicators could be created only by accepting two-dimensional input arrays, which forced the user to adapt all functions accordingly. With the new feature, the indicator factory can split each input array along columns and pass one column at once, making it super-easy to design indicators that are meant to be natively run on one-dimensional data (such as TA-Lib!).
Create a TA-Lib powered STOCHRSI indicator
>>> import talib

>>> params = dict(
...     rsi_period=14, 
...     fastk_period=5, 
...     slowk_period=3, 
...     slowk_matype=0, 
...     slowd_period=3, 
...     slowd_matype=0
... )

>>> def stochrsi_1d(close, *args):
...     rsi = talib.RSI(close, args[0])
...     k, d = talib.STOCH(rsi, rsi, rsi, *args[1:])
...     return rsi, k, d

>>> STOCHRSI = vbt.IF(
...     input_names=["close"], 
...     param_names=list(params.keys()),
...     output_names=["rsi", "k", "d"]
... ).with_apply_func(stochrsi_1d, takes_1d=True, **params)

>>> data = vbt.YFData.pull("BTC-USD", start="2022-01", end="2022-06")
>>> stochrsi = STOCHRSI.run(data.close)
>>> fig = stochrsi.k.rename("%K").vbt.plot()
>>> stochrsi.d.rename("%D").vbt.plot(fig=fig)
>>> fig.show()

Parallelizable indicators

  • Processing of parameter combinations by the indicator factory can be distributed over multiple threads, processes, or even in the cloud. This helps immensely when working with slow indicators 🐌

Benchmark a serial and multithreaded rolling min-max indicator
>>> @njit
... def minmax_nb(close, window):
...     return (
...         vbt.nb.rolling_min_nb(close, window),
...         vbt.nb.rolling_max_nb(close, window)
...     )

>>> MINMAX = vbt.IF(
...     class_name="MINMAX",
...     input_names=["close"], 
...     param_names=["window"], 
...     output_names=["min", "max"]
... ).with_apply_func(minmax_nb, window=14)

>>> data = vbt.YFData.pull("BTC-USD")
>>> %%timeit
>>> minmax = MINMAX.run(
...     data.close, 
...     np.arange(2, 200),
...     jitted_loop=True
... )
420 ms ± 2.05 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %%timeit
>>> minmax = MINMAX.run(
...     data.close, 
...     np.arange(2, 200),
...     jitted_loop=True,
...     jitted_warmup=True,  # (1)!
...     execute_kwargs=dict(engine="threadpool", n_chunks="auto")  # (2)!
... )
120 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
  1. Run one parameter combination to compile the indicator before running others in a multithreaded fashion
  2. One Numba loop per thread, the same number of threads as there are cores

TA-Lib plotting

  • Every TA-Lib indicator knows how to be plotted - fully automatically based on output flags!
>>> data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")

>>> vbt.talib("MACD").run(data.close).plot().show()

Indicator expressions

  • Indicators can now be parsed from expressions. An indicator expression is a regular string that represents a Python code enhanced through various extensions. The indicator factory can derive all the required information such as inputs, parameters, outputs, NumPy, VBT, and TA-Lib functions, and even complex indicators thanks to a unique format and a built-in matching mechanism. Designing indicators has never been easier!
Build a MACD indicator from an expression
>>> data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")

>>> expr = """
... MACD:
... fast_ema = @talib_ema(close, @p_fast_w)
... slow_ema = @talib_ema(close, @p_slow_w)
... macd = fast_ema - slow_ema
... signal = @talib_ema(macd, @p_signal_w)
... macd, signal
... """
>>> MACD = vbt.IF.from_expr(expr, fast_w=12, slow_w=26, signal_w=9)  # (1)!
>>> macd = MACD.run(data.close)
>>> fig = macd.macd.rename("MACD").vbt.plot()
>>> macd.signal.rename("Signal").vbt.plot(fig=fig)
>>> fig.show()
  1. No more manually passing input_names, param_names, and other information!

WorldQuant Alphas

Run the first alpha
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD", "XRP-USD"], missing_index="drop")

>>> vbt.wqa101(1).run(data.close).out
symbol                      BTC-USD   ETH-USD   XRP-USD
Date                                                   
2017-11-09 00:00:00+00:00  0.166667  0.166667  0.166667
2017-11-10 00:00:00+00:00  0.166667  0.166667  0.166667
2017-11-11 00:00:00+00:00  0.166667  0.166667  0.166667
2017-11-12 00:00:00+00:00  0.166667  0.166667  0.166667
2017-11-13 00:00:00+00:00  0.166667  0.166667  0.166667
...                             ...       ...       ...
2023-01-31 00:00:00+00:00  0.166667  0.166667  0.166667
2023-02-01 00:00:00+00:00  0.000000  0.000000  0.500000
2023-02-02 00:00:00+00:00  0.000000  0.000000  0.500000
2023-02-03 00:00:00+00:00  0.000000  0.500000  0.000000
2023-02-04 00:00:00+00:00 -0.166667  0.333333  0.333333

[1914 rows x 3 columns]

Robust crossovers

  • Crossovers are now robust to NaNs.
Remove a bunch of data points and plot the crossovers
>>> data = vbt.YFData.pull("BTC-USD", start="2022-01", end="2022-03")
>>> fast_sma = vbt.talib("SMA").run(data.close, vbt.Default(5)).real
>>> slow_sma = vbt.talib("SMA").run(data.close, vbt.Default(10)).real
>>> fast_sma.iloc[np.random.choice(np.arange(len(fast_sma)), 5)] = np.nan
>>> slow_sma.iloc[np.random.choice(np.arange(len(slow_sma)), 5)] = np.nan
>>> crossed_above = fast_sma.vbt.crossed_above(slow_sma, dropna=True)
>>> crossed_below = fast_sma.vbt.crossed_below(slow_sma, dropna=True)

>>> fig = fast_sma.rename("Fast SMA").vbt.lineplot()
>>> slow_sma.rename("Slow SMA").vbt.lineplot(fig=fig)
>>> crossed_above.vbt.signals.plot_as_entries(fast_sma, fig=fig)
>>> crossed_below.vbt.signals.plot_as_exits(fast_sma, fig=fig)
>>> fig.show()