Features
Custom indicators
Turn Python, pandas, or Numba calculations into reusable indicators with parameter grids
How do you turn your own formula into an indicator you can sweep over parameters and reuse? VBT's indicator factory wraps any Python or Numba function into an indicator class with parameter grids, labeled outputs, signal helpers, and plotting, so a custom indicator in Python behaves exactly like the built-in ones.
@njit
def vam_nb(close, fast, slow):
momentum = close / vbt.nb.fshift_nb(close, fast) - 1
returns = close / vbt.nb.fshift_nb(close, 1) - 1
volatility = vbt.nb.rolling_std_nb(returns, slow)
return momentum / (volatility * np.sqrt(fast))
VAM = vbt.IF(
class_name="VAM",
short_name="vam",
input_names=["close"],
param_names=["fast", "slow"],
output_names=["vam"],
).with_apply_func(vam_nb, takes_1d=False, fast=10, slow=50)
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2023-01-01", end="2025-01-01")
vam = VAM.run(
data.close,
fast=vbt.Param([5, 10, 20, 40], condition="fast < slow"),
slow=vbt.Param([10, 20, 40]),
param_product=True,
)
vam.vam.shape(731, 12)vam.vam.columns.droplevel("symbol").unique().tolist()[(5, 10), (5, 20), (5, 40), (10, 20), (10, 40), (20, 40)]entries = vam.vam_crossed_above(1)
exits = vam.vam_crossed_below(0)
pf = vbt.PF.from_signals(data, entries, exits)
pf.total_return.unstack("symbol").round(3)symbol BTC-USD ETH-USD
vam_fast vam_slow
5 10 0.717 -0.187
20 0.388 0.454
40 0.771 0.228
10 20 0.576 0.131
40 1.119 0.000
20 40 1.322 -0.004The function is ten lines. The factory adds the parameter grid, column labels, signal methods, and everything else an indicator needs, and the result goes straight into a backtest.
From a function to an indicator
vbt.IF takes the names of inputs, parameters, and outputs, and a function that computes them. The
function can work on one column at a time or on whole arrays, return one output or several, and keep
extra results as in-place outputs. The resulting class has a run method with defaults, readable
output properties, comparison helpers such as crossed_above and above for each output, and
plotting. Outputs behave like pandas objects and support arithmetic, so vam.vam * 2 or
fast.ma - slow.ma work directly.
The function receives NumPy arrays by default, which keeps it compilable. Pass keep_pd=True to
receive pandas objects instead, for example to call pandas rolling or group methods. The function
then has to be plain Python, because Numba cannot compile code that takes pandas objects. Regular
indicator outputs match the shape of the inputs for each parameter combination. A function that
returns something else, such as one number per run or arrays of other lengths, fits the
parameterized decorator better, as described under
Parameterize any function.
Reuse your pandas calculations
You can start with a calculation that already works in a notebook. Here, a plain Python function
returns a rolling mean and the price's percentage distance from it. keep_pd=True lets it use
pandas directly, while the factory handles the windows, symbols, and output labels.
def distance_from_mean(close, window):
mean = close.rolling(window).mean()
return mean, 100 * (close / mean - 1)
Distance = vbt.IF(
class_name="Distance",
short_name="dist",
input_names=["close"],
param_names=["window"],
output_names=["mean", "distance"],
).with_apply_func(distance_from_mean, keep_pd=True)
close = pd.DataFrame({
"A": [10, 11, 12, 11, 13, 15],
"B": [20, 19, 18, 19, 17, 15],
}, index=pd.date_range("2025-01-01", periods=6))
close.columns.name = "symbol"
result = Distance.run(close, window=[2, 3])
result.output_names('mean', 'distance')result.distance.iloc[-1].unstack("symbol").round(2)symbol A B
dist_window
2 7.14 -6.25
3 15.38 -11.76On these fictional prices, A finishes above its rolling mean and B below it. Both the mean and distance remain available for plotting, comparisons, or another calculation. You could use the distance as a model feature or a threshold for a mean-reversion strategy, then test that rule with Signal backtesting.
Parameters and grids
Parameters accept single values, lists, or vbt.Param objects. Lists are paired element by element
by default, and param_product=True builds every combination. Conditions filter combinations by
name, as above, and each parameter becomes a column level named after the indicator's short name, so
results can be selected and grouped by parameter.
Inputs
Inputs of different shapes broadcast against each other. A single price series combines with
parameters to produce one column per combination, and several assets multiply the grid, as the two
assets and six pairs above did. With takes_1d=True, the function receives one column at a time,
and the factory loops over the columns for you. If each asset needs its own setting instead of the
same grid, per_column=True pairs each input column with its parameter combination.
Formulas as indicators
An indicator can also be written as an expression, such as (close - low) / (high - low), with
inputs, parameters, and helper functions resolved by name, as the
Indicator expressions highlight below shows. Expressions suit quick ideas,
and the factory turns them into the same kind of class as a compiled function.
Performance
Compiled functions run at Numba speed on the full grid. When the same intermediate result is needed
for many combinations, such as one moving average shared by several pairs, the factory can compute
it once in a cache function and reuse it. Indicators that work column by column can run in parallel
threads, and indicators written for one dimension can be parallelized across columns automatically,
as the 1D-native indicators and
Parallelizable indicators highlights show. Rolling indicators need a
warm-up period, so the first values are NaN until enough bars are available.
Composition and reuse
Indicators combine: the output of one can be the input of another, several can be stacked into one
object, and the result can be indexed like a DataFrame. Once registered, a custom indicator is found
by name with vbt.indicator() and runs from a data object with data.run(), just like built-in
ones. The Indicator search highlight below shows how to find indicators across
all integrated libraries, and Indicators for ML turns many of them into a
feature matrix.
Registration can also organize indicators into named groups, such as your trend filters or volatility features. Run a whole group from a data object to rebuild the same feature set on a new dataset. This is useful when comparing models that should receive the same inputs.
You can add derived properties through lazy_outputs, calculating them only when requested. For
example, an indicator that returns several lines can expose their spread as another property. This
keeps related calculations together while letting you choose which results to inspect. For
calculations that update as each new bar arrives, see
Streaming indicators.
✅ VBT implements or integrates more than 500 indicators, making it hard to keep track of them all. To make indicators easier to find, several new methods are available for globally searching for 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") vectorbtpro.indicators.factory.technical.ZLMA✅ Want to feed indicators as features to a machine-learning model? There is no need to run them individually: you can tell VBT to run all indicators from an indicator package on the given data instance. The data instance will recognize the input names of each indicator and supply the required data. You can also easily change the defaults for each indicator.
data = vbt.YFData.pull("BTC-USD")
features = data.run("talib", mavp=vbt.run_arg_dict(periods=14))
features.shape(3046, 175)✅ Previously, custom indicators could only be created by accepting two-dimensional input arrays, which forced users to adapt all functions accordingly. Now, the indicator factory can split each input array along columns and pass one column at a time, making it much easier to design indicators that are meant to be run natively on one-dimensional data (such as TA-Lib!).
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()✅ Processing parameter combinations with the indicator factory can be distributed across multiple threads, processes, or even in the cloud. This is a huge help when working with slow indicators 🐌
@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,
execute_kwargs=dict(engine="threadpool", n_chunks="auto")
)120 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)✅ Indicators can now be created from expressions. An indicator expression is a regular string representing Python code enhanced with various extensions. The indicator factory automatically derives all required information, such as inputs, parameters, outputs, NumPy, VBT, and TA-Lib functions, and even complex indicators, thanks to a unique format and built-in matching mechanism. Designing indicators has never been easier!
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)
macd = MACD.run(data.close)
fig = macd.macd.rename("MACD").vbt.plot()
macd.signal.rename("Signal").vbt.plot(fig=fig)
fig.show()Related pages
- Technical indicatorsRun TA-Lib, pandas-ta, ta, SMC, WorldQuant Alphas, and native indicators at scale
- Optimization and validation › Parameter optimizationSweep millions of parameter combinations with grids, conditions, and random search
- Research toolkit › Multidimensional researchRun assets, parameters, and strategies as labeled columns, then index and stack them
- Research toolkit › Time-series operationsRun compiled rolling windows, reductions, and date logic on labeled pandas data
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.