Features

Multidimensional research

Run assets, parameters, and strategies as labeled columns, then index and stack them

How does VBT test thousands of strategy variants in one pass, and how do you find your way around the result? Every asset, parameter value, and strategy becomes a labeled column. Inputs broadcast against each other, computation runs on NumPy arrays, and results come back as pandas objects you can select, group, and stack like any DataFrame.

Run 3 assets × 4 fast windows × 4 slow windows as 48 columns
from itertools import product

data = vbt.YFData.pull(
    ["BTC-USD", "ETH-USD", "SOL-USD"],
    start="2022-01-01",
    end="2025-01-01",
)
fast_windows, slow_windows = zip(*product([5, 10, 15, 20], [30, 50, 70, 90]))
fast = vbt.MA.run(data.close, window=list(fast_windows), short_name="fast")
slow = vbt.MA.run(data.close, window=list(slow_windows), short_name="slow")
entries = fast.ma_crossed_above(slow)
exits = fast.ma_crossed_below(slow)
entries.shape
(1096, 48)
entries.columns[:4]
MultiIndex([(5, 30, 'BTC-USD'),
            (5, 30, 'ETH-USD'),
            (5, 30, 'SOL-USD'),
            (5, 50, 'BTC-USD')],
           names=['fast_window', 'slow_window', 'symbol'])
pf = vbt.PF.from_signals(data, entries, exits, fees=0.001)  
pf.total_return.groupby("symbol").describe()[["mean", "min", "max"]].round(3)
          mean    min    max
symbol
BTC-USD  0.758  0.174  1.481
ETH-USD  0.051 -0.195  0.474
SOL-USD  1.394  0.104  4.285
print(round(pf[(10, 50, "ETH-USD")].total_return, 4))  
0.1389
pf.xs("ETH-USD", level="symbol", axis=1).wrapper.shape  
(1096, 16)

Each column is a complete backtest with its own orders, trades, and metrics, and the column labels carry every parameter, so results can be summarized with ordinary pandas.

That lets you ask several questions of the same experiment: which assets behave consistently, which parameter ranges hold up, and how much the result changes under different cost assumptions. You keep the labels from the inputs through to the final trades and performance tables.

The research model

A VBT object is a NumPy array plus a wrapper that remembers the index, the columns, the frequency, and any grouping. Computation runs on the raw arrays in compiled code, and results are wrapped back into pandas with the right labels, whether the result is a time series, one value per column, or one value per group. Series and DataFrames go through the same code, so a function written for one asset works for a thousand.

For market data, each feature has its own array: close prices in one table, highs in another, and entry signals in another. Columns identify symbols and experiments. A VBT data object can supply its price features directly, as in the opening example, or you can pass separate arrays from your own data pipeline.

Broadcasting

Inputs of different shapes are combined by broadcasting: a scalar applies everywhere, a Series applies to every column, a one-row DataFrame applies to every bar, and a full DataFrame applies element by element. Labeled columns are matched by their levels, so three price columns line up with 48 signal columns that share the symbol level, as above. Small inputs stay small: inside the simulation, a scalar fee is read as a scalar rather than expanded into a full array.

A one-dimensional NumPy array is a time series, so it must have one value per bar. Per-column values go in a one-row array such as np.array([[0.001, 0.002, 0.003]]), or a labeled object. Rows are matched by their dates: inputs with different dates are joined on all dates and filled with NaN, so data on different timeframes must be resampled to one index first, as the Multi-timeframe analysis page shows.

Plain pandas operators match labels exactly, so comparing two indicator outputs with different parameter levels fails. Putting .vbt in front of the left operand applies VBT's broadcasting instead, as the Trading signals page shows.

Match price and signal columns for one backtest per column, or expand your experiment with the DataFrame product below. It lets you compare combinations while keeping each symbol attached to its own prices.

The DataFrame product highlight below combines every column of one DataFrame with every column of another, and Index dictionaries set values at chosen dates and columns without building an array first.

Compare assumptions as labeled scenarios

Parameters can describe execution assumptions as well as indicators. Here, two made-up price series are tested with no fees and a 1% fee. Each fee becomes another column level, so the returns can be reshaped into a table without collecting results from separate runs:

Compare two fee assumptions across two assets
close = pd.DataFrame(
    {"A": [100.0, 105.0, 110.0], "B": [100.0, 98.0, 95.0]},
    index=pd.date_range("2025-01-01", periods=3),
).rename_axis(columns="symbol")
scenarios = vbt.PF.from_holding(close, fees=vbt.Param([0.0, 0.01], name="fee"))
scenarios.total_return.unstack("symbol").round(4)
symbol       A       B
fee
0.00    0.1000 -0.0500
0.01    0.0891 -0.0594

These are total returns as fractions, with the positions still open at the end. The fee reduces how much each backtest can buy at entry. The same approach lets you compare slippage or sizing assumptions. Parameter optimization covers larger searches, conditional combinations, and random samples.

Grouping

Columns can be grouped for reporting or for simulation. group_by combines columns into groups by a level, a list of labels, or True for everything, and cash_sharing=True turns a group into one account. Grouping set after the fact changes only how results are reported, so the same backtests can be read per asset, per parameter value, or as a whole, as long as each group's columns are next to each other.

In the opening example, the pandas summary groups the 16 returns for each symbol to compare parameter choices. That is different from grouping the portfolio itself, which can report the performance of several columns held together. Use Portfolio accounting for shared-capital portfolios and Multi-strategy portfolios for allocating between separate strategies.

Stacking runs

Separate runs can be joined into one object. Column stacking puts different experiments side by side, for example a strategy grid next to buy-and-hold:

Put buy-and-hold next to the 48 crossover backtests
hold_pf = vbt.PF.from_holding(data)
both = vbt.PF.column_stack(
    pf,
    hold_pf,
    wrapper_kwargs=dict(keys=pd.Index(["crossover", "hold"], name="strategy")),
)
both.wrapper.shape
(1096, 51)
both.total_return["hold"].round(3)
BTC-USD    0.959
ETH-USD   -0.116
SOL-USD    0.060
Name: total_return, dtype: float64

Row stacking joins consecutive periods, such as the test windows of a walk-forward run, into one history. Stacked portfolios keep their records and recompute cash and value from them. Each window simulated on its own starts with fresh capital: the first window's initial cash becomes the stacked portfolio's, and each later window's initial cash is added as a deposit at its start, so returns stay correct while the value jumps. combine_init_cash=True counts all of it as initial cash instead, which leaves the later windows' cash idle during the earlier ones and dilutes their returns. To carry one account through every window, continue each window from the previous one's final state and stack them with chained=True, as on the Live simulation page. The Column stacking and Row stacking highlights below show both on full portfolios.

This also lets you work with a long history in smaller segments, or simulate data windows around groups of entry and exit events. Keep the intended cash and position state at the boundaries, then row-stack the results for analysis. For large sets of independent experiments, Parallel execution and caching covers splitting the work into batches.

Indexing

Every VBT object can be indexed like pandas: by column labels, by .loc and .iloc on dates and positions, by parameter level, or with xs. Slicing a portfolio by date gives a new portfolio for that period without rerunning the simulation, with records and metrics adjusted to the window. Setters work the same way in reverse: assign values by date, time of day, label, or mask with the data's wrapper. The Slicing and Index alignment highlights below show both.

Select a symbol or parameter combination before inspecting its orders, plotting its value, or comparing its drawdowns. The labels let you move from a summary table back to the backtest that produced an interesting result. You can also rename, reorder, or drop index levels to keep reports readable when an experiment has many dimensions.

Pandas accessors

Importing VBT adds a .vbt accessor to every Series, DataFrame, and Index, with specialized namespaces such as .vbt.returns, .vbt.signals, and .vbt.ohlcv. Data stays a regular pandas object, so VBT operations and pandas operations can be mixed in one line. Because the accessors are registered at runtime, some editors do not complete them. Notebooks do. vbt.df_acc(df) and vbt.sr_acc(sr) return the same accessors as explicit objects, which editors can introspect.

DataFrame product

✅ Several parameterized indicators can produce DataFrames with different shapes and columns, which makes creating a Cartesian product tricky because they often share common column levels (such as "symbol") that should not be combined. There is now a method to cross-join multiple DataFrames block-wise.

Enter when SMA goes above WMA, exit when EMA goes below WMA
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
sma = data.run("sma", timeperiod=[10, 20], unpack=True)
ema = data.run("ema", timeperiod=[30, 40], unpack=True)
wma = data.run("wma", timeperiod=[50, 60], unpack=True)
sma, ema, wma = sma.vbt.x(ema, wma)  
entries = sma.vbt.crossed_above(wma)
exits = ema.vbt.crossed_below(wma)

entries.columns
MultiIndex([(10, 30, 50, 'BTC-USD'),
            (10, 30, 50, 'ETH-USD'),
            (10, 30, 60, 'BTC-USD'),
            (10, 30, 60, 'ETH-USD'),
            (10, 40, 50, 'BTC-USD'),
            (10, 40, 50, 'ETH-USD'),
            (10, 40, 60, 'BTC-USD'),
            (10, 40, 60, 'ETH-USD'),
            (20, 30, 50, 'BTC-USD'),
            (20, 30, 50, 'ETH-USD'),
            (20, 30, 60, 'BTC-USD'),
            (20, 30, 60, 'ETH-USD'),
            (20, 40, 50, 'BTC-USD'),
            (20, 40, 50, 'ETH-USD'),
            (20, 40, 60, 'BTC-USD'),
            (20, 40, 60, 'ETH-USD')],
           names=['sma_timeperiod', 'ema_timeperiod', 'wma_timeperiod', 'symbol'])

Manually creating arrays and setting their data with Pandas can often be challenging. Luckily, there is now a feature that offers much-needed assistance! Any broadcastable argument can become an index dictionary, which contains instructions on where to set values in the array and fills them in for you. It knows exactly which axis needs to be updated and does not create a full array unless necessary, saving RAM ❤️

1) Accumulate daily and exit on Sunday vs 2) accumulate weekly and exit on month end
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])
tile = pd.Index(["daily", "weekly"], name="strategy")  
pf = vbt.PF.from_orders(
    data.close,
    size=vbt.index_dict({  
        vbt.idx(
            vbt.pointidx(every="day"),
            vbt.colidx("daily", level="strategy")): 100,  
        vbt.idx(
            vbt.pointidx(every="sunday"),
            vbt.colidx("daily", level="strategy")): -np.inf,  
        vbt.idx(
            vbt.pointidx(every="monday"),
            vbt.colidx("weekly", level="strategy")): 100,
        vbt.idx(
            vbt.pointidx(every="monthend"),
            vbt.colidx("weekly", level="strategy")): -np.inf,
    }),
    size_type="value",
    direction="longonly",
    init_cash="auto",
    broadcast_kwargs=dict(tile=tile)
)
pf.sharpe_ratio
strategy  symbol
daily     BTC-USD    0.702259
          ETH-USD    0.782296
weekly    BTC-USD    0.838895
          ETH-USD    0.524215
Name: sharpe_ratio, dtype: float64

Slicing

✅ Similar to selecting columns, each VBT object can now slice rows using the same mechanism as in Pandas 🔪 This makes it easy to analyze and plot any subset of simulated data, without needing to re-simulate!

Analyze multiple date ranges of the same portfolio
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_holding(data, freq="d")

pf.sharpe_ratio
1.116727709477293
pf.loc[:"2020"].sharpe_ratio  
1.2699801554196481
pf.loc["2021": "2021"].sharpe_ratio  
0.9825161170278687
pf.loc["2022":].sharpe_ratio  
-1.0423271337174647

Column stacking

✅ Complex VBT objects of the same type can be easily stacked along columns. For example, you can combine multiple unrelated trading strategies into one portfolio for analysis. Under the hood, the final object is still represented as a monolithic multi-dimensional structure that can be processed even faster than separate merged objects 🫁

Analyze two trading strategies separately and then jointly
def strategy1(data):
    fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
    slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    return vbt.PF.from_signals(
        data.close,
        entries,
        exits,
        size=100,
        size_type="value",
        init_cash="auto"
    )

def strategy2(data):
    bbands = vbt.BBANDS.run(data.close, window=14)
    entries = bbands.close_crossed_below(bbands.lower)
    exits = bbands.close_crossed_above(bbands.upper)
    return vbt.PF.from_signals(
        data.close,
        entries,
        exits,
        init_cash=200
    )

data1 = vbt.BinanceData.pull("BTCUSDT")
pf1 = strategy1(data1)  
pf1.sharpe_ratio
0.9100317671866922
data2 = vbt.BinanceData.pull("ETHUSDT")
pf2 = strategy2(data2)  
pf2.sharpe_ratio
-0.11596286232734827
pf_sep = vbt.PF.column_stack((pf1, pf2))  
pf_sep.sharpe_ratio
0    0.910032
1   -0.115963
Name: sharpe_ratio, dtype: float64
pf_join = vbt.PF.column_stack((pf1, pf2), group_by=True)  
pf_join.sharpe_ratio
0.42820898354646514

Row stacking

✅ Complex VBT objects of the same type can be easily stacked along rows. For example, you can append new data to an existing portfolio, or concatenate in-sample portfolios with their out-of-sample counterparts 🧬

Analyze two date ranges separately and then jointly
def strategy(data, start=None, end=None):
    fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
    slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    return vbt.PF.from_signals(
        data.close[start:end],
        entries[start:end],
        exits[start:end],
        size=100,
        size_type="value",
        init_cash="auto"
    )

data = vbt.BinanceData.pull("BTCUSDT")

pf_whole = strategy(data)  
pf_whole.sharpe_ratio
0.9100317671866922
pf_sub1 = strategy(data, end="2019-12-31")  
pf_sub1.sharpe_ratio
0.7810397448678937
pf_sub2 = strategy(data, start="2020-01-01")  
pf_sub2.sharpe_ratio
1.070339534746574
pf_join = vbt.PF.row_stack((pf_sub1, pf_sub2))  
pf_join.sharpe_ratio
0.9100317671866922

Index alignment

✅ There is no longer a limitation requiring each Pandas array to have the same index. Indexes of all arrays that should broadcast against each other are automatically aligned, as long as they have the same data type.

Predict ETH price with BTC price using linear regression
btc_data = vbt.YFData.pull("BTC-USD")
btc_data.wrapper.shape
(2817, 7)
eth_data = vbt.YFData.pull("ETH-USD")  
eth_data.wrapper.shape
(1668, 7)
ols = vbt.OLS.run(  
    btc_data.close,
    eth_data.close
)
ols.pred
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
...                                  ...
2022-05-30 00:00:00+00:00    2109.769242
2022-05-31 00:00:00+00:00    2028.856767
2022-06-01 00:00:00+00:00    1911.555689
2022-06-02 00:00:00+00:00    1930.169725
2022-06-03 00:00:00+00:00    1882.573170
Freq: D, Name: Close, Length: 2817, dtype: float64

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.