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.

Build a volatility-adjusted momentum indicator and test six parameter pairs
@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.004

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

Turn a pandas calculation into a reusable indicator
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.76

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

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")  
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.

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)

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

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()
TA-Lib-powered stochastic RSI percent K and percent D for BTC-USD Figure data (JSON)

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

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

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)  
macd = MACD.run(data.close)
fig = macd.macd.rename("MACD").vbt.plot()
macd.signal.rename("Signal").vbt.plot(fig=fig)
fig.show()
Expression-built MACD and signal lines for BTC-USD during 2020 Figure data (JSON)

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.