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.
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.285print(round(pf[(10, 50, "ETH-USD")].total_return, 4)) 0.1389pf.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:
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.0594These 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:
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: float64Row 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.
✅ 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.
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.columnsMultiIndex([(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 ❤️
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_ratiostrategy symbol
daily BTC-USD 0.702259
ETH-USD 0.782296
weekly BTC-USD 0.838895
ETH-USD 0.524215
Name: sharpe_ratio, dtype: float64✅ 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!
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_holding(data, freq="d")
pf.sharpe_ratio1.116727709477293pf.loc[:"2020"].sharpe_ratio 1.2699801554196481pf.loc["2021": "2021"].sharpe_ratio 0.9825161170278687pf.loc["2022":].sharpe_ratio -1.0423271337174647✅ 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 🫁
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_ratio0.9100317671866922data2 = vbt.BinanceData.pull("ETHUSDT")
pf2 = strategy2(data2)
pf2.sharpe_ratio-0.11596286232734827pf_sep = vbt.PF.column_stack((pf1, pf2))
pf_sep.sharpe_ratio0 0.910032
1 -0.115963
Name: sharpe_ratio, dtype: float64pf_join = vbt.PF.column_stack((pf1, pf2), group_by=True)
pf_join.sharpe_ratio0.42820898354646514✅ 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 🧬
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_ratio0.9100317671866922pf_sub1 = strategy(data, end="2019-12-31")
pf_sub1.sharpe_ratio0.7810397448678937pf_sub2 = strategy(data, start="2020-01-01")
pf_sub2.sharpe_ratio1.070339534746574pf_join = vbt.PF.row_stack((pf_sub1, pf_sub2))
pf_join.sharpe_ratio0.9100317671866922✅ 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.
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.predDate
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: float64Related pages
- Documentation › FundamentalsUnderstand the core concepts behind VBT's vectorized architecture
- Backtesting › Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
- Data › Data pipelinesAlign, update, transform, and analyze multi-symbol market data in one object
- Optimization and validation › Parameter optimizationSweep millions of parameter combinations with grids, conditions, and random search
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.