Features
Productivity
Automate workflows with the CLI, tasks, serialization, data utilities, and native execution
✅ Take your strategy directly to Rust. VBT's native simulators let the same strategy process a full price array or step through individual bars as they arrive. Strategy callbacks can inspect cash, positions, and orders during execution, keeping portfolio state within reach of your trading logic.
Note
The examples below share the strategy definition and require vectorbtpro-rust and ndarray = "0.16".
See the Rust setup guide for installation.
use vectorbtpro_rust::error::VbtResult;
use vectorbtpro_rust::portfolio::enums::Order;
use vectorbtpro_rust::portfolio::simulator::{
FnOrderStrategy, OrderContext, OrderStrategy,
};
fn buy_the_dip() -> impl OrderStrategy {
FnOrderStrategy::new(|ctx: &OrderContext<'_, '_>| {
let price = ctx.close(ctx.col());
let position = ctx.position(ctx.col());
let size = if price <= 100.0 && position == 0.0 {
1.0
} else if price >= 110.0 && position > 0.0 {
-position
} else {
return Ok(None);
};
Ok(Some(Order::builder().size(size).build()))
})
}use ndarray::array;
use vectorbtpro_rust::portfolio::simulator::{OrderSimulator, SimulationConfig};
fn main() -> VbtResult<()> {
let close = array![[100.0], [98.0], [105.0], [112.0]];
let groups = array![1];
let config = SimulationConfig::builder()
.target_shape(close.dim())
.group_lens(groups.view())
.close(close.view())
.init_cash(1000.0)
.build()?;
let simulator = OrderSimulator::builder().config(config).build()?;
let output = simulator.run_single(&mut buy_the_dip())?;
for order in &output.order_records {
println!("row {}: {:.0} share at ${:.0}", order.idx, order.size, order.price);
}
Ok(())
}row 0: 1 share at $100
row 3: 1 share at $112Tutorial
Learn more in the From Python to Rust tutorial.
✅ Install the optional vectorbtpro-rust extension and compatible jitted calls can take the Rust
fast lane automatically. VBT exposes the extension as vbt.rs, registers Rust kernels under jitted="rs",
and falls back to the normal implementation, usually Numba, when Rust is unavailable or unsupported.
data = vbt.YFData.pull("BTC-USD", start="2024")
fast_ma = data.close.vbt.rolling_mean(20)
slow_ma = data.close.vbt.rolling_mean(50)
entries = fast_ma.vbt.crossed_above(slow_ma)
exits = fast_ma.vbt.crossed_below(slow_ma)
pf = vbt.Portfolio.from_signals(
data,
entries=entries,
exits=exits,
sl_stop=0.05,
tp_stop=0.15,
fees=0.001,
)✅ VBT now ships with correctness-aware backend benchmarks for almost the entire codebase. The latest benchmark overview covers 748 functions, 768 variants, 1,976 kernels, 15,709 cases, and 59,600 benchmark runs across Numba, Rust, raw backend calls, AutoBench, and AutoBenchMixed. This makes performance inspectable: users can see which backend wins, at which input size, and whether parallel execution is worth it for their workload.
python -m vectorbtpro.benchmarks.bench_engine_cli \
--backend nb \
--backend rs \
--input-model 1d \
--pattern returnsresults = vbt.run_benchmarks(
input_model="1d",
backend_ids=("nb", "rs"),
patterns=["returns"],
)
print(results) function,task_id,ndim,shape,nb_s,rs_s,speedup,auto_bench_backend,nb_elapsed_s,rs_elapsed_s
...✅ VBT now ships with a Typer-based command-line interface that
lets you chat, search, and invoke MCP tools directly from the terminal. No Python script required.
After installing the package, you can run the vbt command in your terminal to see all available commands.
vbt chat "How to backtest a weekly rebalancing strategy?" The first time you run most of these commands, it may take a while to prepare documents. However, most of the preparation steps are cached and stored, so future calls will be much faster and will not require repeating the process.
✅ When analyzing complex codebases like VBT, it can be challenging to keep track of all the interdependencies between modules, classes, and functions. To address this, VBT now includes a functionality that can index the source code of any codebase for object references and generate reference graphs from any specified entry point. These graphs provide a visual representation of how different components relate to each other, making it easier to understand the overall structure and flow of the code.
ref_index = vbt.RefIndex(container_kinds=["module", "class"], incl_modules="pandas")
ref_graph = ref_index.build_graph("pandas")
ref_graph.plot(
interactive="dash",
to_dash_kwargs=dict(fit_to_window=True),
dash_run_kwargs=dict(jupyter_mode="tab")
)Online explorer
Explore the VBT's reference graph in the API → Reference graph.
✅ VBT extends popular configuration formats (INI, YAML, TOML) to define its own configuration format that allows users to save, introspect, modify, and load any complex in-house object. The main advantages of this format are readability and round-tripping: any object can be encoded and then decoded back without loss of information. The main features include nested structures, references, literal parsing, and evaluation of arbitrary Python expressions. Additionally, you can now create a configuration file for VBT and place it in the working directory— it will be used to update the default settings whenever the package is imported.
[plotting]
default_theme = "dark"
[portfolio]
init_cash = 5000
[data.custom.binance.client_config]
api_key = "<YOUR_API_KEY>"
api_secret = "<YOUR_API_SECRET>"
[data.custom.ccxt.exchanges.binance.exchange_config]
apiKey = &data.custom.binance.client_config.api_key
secret = &data.custom.binance.client_config.api_secretfrom vectorbtpro import *
vbt.settings.portfolio["init_cash"]5000✅ Thinking about parallelizing a for-loop? No need to hesitate—VBT has a decorator for that.
import calendar
data = vbt.YFData.pull("BTC-USD")
@vbt.iterated(over_arg="year", merge_func="column_stack", engine="pathos")
@vbt.iterated(over_arg="month", merge_func="concat")
def get_year_month_sharpe(data, year, month):
mask = (data.index.year == year) & (data.index.month == month)
if not mask.any():
return np.nan
year_returns = data.loc[mask].returns
return year_returns.vbt.returns.sharpe_ratio()
years = data.index.year.unique().sort_values().rename("year")
months = data.index.month.unique().sort_values().rename("month")
sharpe_matrix = get_year_month_sharpe(
data,
years,
{calendar.month_abbr[month]: month for month in months},
)
sharpe_matrix.transpose().vbt.heatmap(
trace_kwargs=dict(colorscale="RdBu", zmid=0),
yaxis=dict(autorange="reversed")
).show()✅ Testing multiple parameter combinations usually involves using the @vbt.parameterized decorator.
But what if you want to test entirely uncorrelated configurations or even different functions?
The latest addition to VBT lets you execute any sequence of unrelated tests in parallel
by assigning each test to a task.
data = vbt.YFData.pull("BTC-USD")
task1 = vbt.Task(
vbt.PF.from_random_signals,
data,
n=100, seed=42,
sl_stop=vbt.Param(np.arange(1, 51) / 100)
)
task2 = vbt.Task(
vbt.PF.from_random_signals,
data,
n=100, seed=42,
tsl_stop=vbt.Param(np.arange(1, 51) / 100)
)
task3 = vbt.Task(
vbt.PF.from_random_signals,
data,
n=100, seed=42,
tp_stop=vbt.Param(np.arange(1, 51) / 100)
)
pf1, pf2, pf3 = vbt.execute([task1, task2, task3], engine="pathos")
fig = pf1.trades.expectancy.rename("SL").vbt.plot()
pf2.trades.expectancy.rename("TSL").vbt.plot(fig=fig)
pf3.trades.expectancy.rename("TP").vbt.plot(fig=fig)
fig.show()✅ Progress bars are now aware of each other. When a new progress bar starts, it checks whether another progress bar with the same identifier has already finished its task. If so, the new progress bar will close itself and delegate its progress to the existing one.
symbols = ["BTC-USD", "ETH-USD"]
fast_windows = range(5, 105, 5)
slow_windows = range(5, 105, 5)
sharpe_ratios = dict()
with vbt.ProgressBar(total=len(symbols), bar_id="pbar1") as pbar1:
for symbol in symbols:
pbar1.set_description(dict(symbol=symbol), refresh=True)
data = vbt.YFData.pull(symbol)
with vbt.ProgressBar(total=len(fast_windows), bar_id="pbar2") as pbar2:
for fast_window in fast_windows:
pbar2.set_description(dict(fast_window=fast_window), refresh=True)
with vbt.ProgressBar(total=len(slow_windows), bar_id="pbar3") as pbar3:
for slow_window in slow_windows:
if fast_window < slow_window:
pbar3.set_description(dict(slow_window=slow_window), refresh=True)
fast_sma = data.run("talib_func:sma", fast_window)
slow_sma = data.run("talib_func:sma", slow_window)
entries = fast_sma.vbt.crossed_above(slow_sma)
exits = fast_sma.vbt.crossed_below(slow_sma)
pf = vbt.PF.from_signals(data, entries, exits)
sharpe_ratios[(symbol, fast_window, slow_window)] = pf.sharpe_ratio
pbar3.update()
pbar2.update()
pbar1.update()sharpe_ratios = pd.Series(sharpe_ratios)
sharpe_ratios.index.names = ["symbol", "fast_window", "slow_window"]
sharpe_ratiossymbol fast_window slow_window
BTC-USD 5 10 1.063616
15 1.218345
20 1.273154
25 1.365664
30 1.394469
...
ETH-USD 80 90 0.582995
95 0.617568
85 90 0.701215
95 0.616037
90 95 0.566650
Length: 342, dtype: float64✅ When writing a function, you can specify the meaning of each argument using an annotation immediately next to the argument. VBT now provides a rich set of in-house annotations tailored to specific tasks. For example, whether an argument is a parameter can be specified directly in the function instead of in the parameterized decorator.
@vbt.cv_split(
splitter="from_rolling",
splitter_kwargs=dict(length=365, split=0.5, set_labels=["train", "test"]),
parameterized_kwargs=dict(random_subset=100, seed=42),
)
def sma_crossover_cv(
data: vbt.Takeable,
fast_period: vbt.Param(condition="x < slow_period"),
slow_period: vbt.Param,
metric
) -> vbt.MergeFunc("concat"):
fast_sma = data.run("sma", fast_period, hide_params=True)
slow_sma = data.run("sma", slow_period, hide_params=True)
entries = fast_sma.real_crossed_above(slow_sma)
exits = fast_sma.real_crossed_below(slow_sma)
pf = vbt.PF.from_signals(data, entries, exits, direction="both")
return pf.deep_getattr(metric)
sma_crossover_cv(
vbt.YFData.pull("BTC-USD", start="4 years ago"),
np.arange(20, 50),
np.arange(20, 50),
"trades.expectancy"
)split set fast_period slow_period
0 train 25 26 2.822879
test 25 26 4.740533
1 train 22 37 16.967189
test 22 37 -16.101936
2 train 30 41 308.879034
test 30 41 14.445511
3 train 29 49 32.328702
test 29 49 13.546335
4 train 27 48 20.695292
test 27 48 6.539824
5 train 22 46 19.143686
test 22 46 -4.664139
6 train 34 45 5.235739
test 34 45 -2.697966
dtype: float64✅ 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'])✅ Serialized VBT objects can sometimes use a lot of disk space. With this update, VBT now supports a variety of compression algorithms to make files as light as possible! 🪶
data = vbt.RandomOHLCData.pull(
"RAND",
start="2022",
end="2023",
timeframe="1 minute",
seed=42
)
file_path = data.save()
print(vbt.file_size(file_path))21.0 MBfile_path = data.save(compression="blosc")
print(vbt.file_size(file_path))13.3 MB✅ If your pipeline does not need accessors, Plotly graphs, or most other optional features, you can disable the auto-import feature entirely to reduce VBT's loading time to under a second ⏳
[importing]
auto_import = Falsestart = utc_time()
from vectorbtpro import *
end = utc_time()
end - start0.580937910079956✅ Just like machine learning models, every native VBT object can be serialized and saved to a binary file. It has never been easier to share data and insights! Another benefit is that only the actual content of each object is serialized, not its class definition, so the loaded object always uses the most up-to-date class definition. There is also special logic implemented to help you "reconstruct" objects if VBT introduces any breaking API changes 🏗️
data = vbt.YFData.pull("BTC-USD", start="2022-01-01", end="2022-06-01")
def backtest_month(close):
return vbt.PF.from_random_signals(close, n=10, seed=42)
month_pfs = data.close.resample(vbt.offset("M")).apply(backtest_month)
month_pfsDate
2022-01-01 00:00:00+00:00 Portfolio(\n wrapper=ArrayWrapper(\n ...
2022-02-01 00:00:00+00:00 Portfolio(\n wrapper=ArrayWrapper(\n ...
2022-03-01 00:00:00+00:00 Portfolio(\n wrapper=ArrayWrapper(\n ...
2022-04-01 00:00:00+00:00 Portfolio(\n wrapper=ArrayWrapper(\n ...
2022-05-01 00:00:00+00:00 Portfolio(\n wrapper=ArrayWrapper(\n ...
Freq: MS, Name: Close, dtype: objectvbt.save(month_pfs, "month_pfs")
month_pfs = vbt.load("month_pfs")
month_pfs.apply(lambda pf: pf.total_return)Date
2022-01-01 00:00:00+00:00 -0.083672
2022-02-01 00:00:00+00:00 0.173909
2022-03-01 00:00:00+00:00 -0.006249
2022-04-01 00:00:00+00:00 -0.057868
2022-05-01 00:00:00+00:00 0.019209
Freq: MS, Name: Close, dtype: float64✅ Tired of passing open, high, low, and close as separate time series? Portfolio class methods now accept a data instance instead of just close and automatically extract the contained OHLC data. This small but handy feature saves you time!
data = vbt.YFData.pull("BTC-USD", start="2020-01", end="2020-03")
pf = vbt.PF.from_random_signals(data, n=10, seed=42)Index dictionaries
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: float64✅ Numba does not support datetime indexes (or any other Pandas objects). There are also no built-in Numba functions for working with datetime. So, how do you connect data to time? VBT addresses this gap by implementing a collection of functions to extract various information from each timestamp, such as the current time and day of the week, to determine whether the bar is during trading hours.
@njit
def month_start_pct_change_nb(arr, index):
out = np.full(arr.shape, np.nan)
for col in range(arr.shape[1]):
for i in range(arr.shape[0]):
if i == 0 or vbt.dt_nb.month_nb(index[i - 1]) != vbt.dt_nb.month_nb(index[i]):
month_start_value = arr[i, col]
else:
out[i, col] = (arr[i, col] - month_start_value) / month_start_value
return out
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2022", end="2023")
pct_change = month_start_pct_change_nb(
vbt.to_2d_array(data.close),
data.index.vbt.to_ns()
)
pct_change = data.symbol_wrapper.wrap(pct_change)
pct_change.vbt.plot().show()Tutorial
Learn more in the Signal development tutorial.
✅ Instead of writing Numba functions, comparing values at different bars can also be done in a vectorized way with Pandas. The problem is that there are no built-in functions to easily shift values based on timedeltas, nor are there rolling functions to check whether an event happened during a past period. This gap is filled by various new accessor methods.
data = vbt.YFData.pull("BTC-USD", start="2022-05", end="2022-08")
mask = (data.close < data.close.vbt.ago(1)).vbt.all_ago(5)
fig = data.plot(plot_volume=False)
mask.vbt.signals.ranges.plot_shapes(
plot_close=False,
fig=fig,
add_shape_kwargs=dict(fillcolor="orangered")
)
fig.show()Tutorial
Learn more in the Signal development tutorial.
✅ Look-ahead bias is an ongoing risk when working with array data, especially on multiple time frames. Using Pandas alone is strongly discouraged because it does not recognize that financial data mainly involves bars where timestamps are the opening times, and events may occur at any time between bars. Pandas thus incorrectly assumes that timestamps indicate the exact time of an event. In VBT, there is a complete collection of functions and classes for safely resampling and analyzing data!
def mtf_sma(close, close_freq, target_freq, timeperiod=5):
target_close = close.vbt.realign_closing(target_freq)
target_sma = vbt.talib("SMA").run(target_close, timeperiod=timeperiod).real
target_sma = target_sma.rename(f"SMA ({target_freq})")
return target_sma.vbt.realign_closing(close.index, freq=close_freq)
data = vbt.YFData.pull("BTC-USD", start="2020", end="2023")
fig = mtf_sma(data.close, "D", "daily").vbt.plot()
mtf_sma(data.close, "D", "weekly").vbt.plot(fig=fig)
mtf_sma(data.close, "D", "monthly").vbt.plot(fig=fig)
fig.show()Tutorial
Learn more in the MTF analysis tutorial.
✅ You can resample not only time series, but also complex VBT objects! Under the hood, each object is made up of a collection of array-like attributes, so resampling means aggregating all the related information together. This is especially helpful if you want to simulate at a higher frequency for maximum accuracy and then analyze at a lower frequency for better speed.
import calendar
data = vbt.YFData.pull("BTC-USD", start="2018", end="2023")
pf = vbt.PF.from_random_signals(data, n=100, direction="both", seed=42)
mo_returns = pf.resample("M").returns
mo_return_matrix = pd.Series(
mo_returns.values,
index=pd.MultiIndex.from_arrays([
mo_returns.index.year,
mo_returns.index.month
], names=["year", "month"])
).unstack("month")
mo_return_matrix.columns = mo_return_matrix.columns.map(lambda x: calendar.month_abbr[x])
mo_return_matrix.vbt.heatmap(
is_x_category=True,
trace_kwargs=dict(zmid=0, colorscale="Spectral")
).show()Tutorial
Learn more in the MTF analysis tutorial.
✅ VBT is a comprehensive library that defines thousands of classes, functions, and objects. When working with these, you may want to "look inside" an object to better understand its attributes and contents. Fortunately, there is a formatting engine that can accurately format any in-house object as a human-readable string. Did you know the API documentation is partly powered by this engine? 😉
data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")
vbt.pprint(data) YFData(
wrapper=ArrayWrapper(...),
data=symbol_dict({
'BTC-USD': <pandas.core.frame.DataFrame object at 0x7f7f1fbc6cd0 with shape (366, 7)>
}),
single_key=True,
classes=symbol_dict(),
fetch_kwargs=symbol_dict({
'BTC-USD': dict(
start='2020',
end='2021'
)
}),
returned_kwargs=symbol_dict({
'BTC-USD': dict()
}),
last_index=symbol_dict({
'BTC-USD': Timestamp('2020-12-31 00:00:00+0000', tz='UTC')
}),
tz_localize=datetime.timezone.utc,
tz_convert='UTC',
missing_index='nan',
missing_columns='raise'
)vbt.pdir(data) type path
attr
align_columns classmethod vectorbtpro.data.base.Data
align_index classmethod vectorbtpro.data.base.Data
build_feature_config_doc classmethod vectorbtpro.data.base.Data
... ... ...
vwap property vectorbtpro.data.base.Data
wrapper property vectorbtpro.base.wrapping.Wrapping
xs function vectorbtpro.base.indexing.PandasIndexervbt.phelp(data.get) YFData.get(
columns=None,
symbols=None,
**kwargs
):
Get one or more columns of one or more symbols of data.✅ Many methods, such as rolling apply, now come in two versions: regular (instance methods) and meta (class methods). Regular methods are bound to a single array and do not need metadata, while meta methods are not tied to any array and act as micro-pipelines with their own broadcasting and templating logic. Here, VBT solves one of the main Pandas limitations: the inability to apply a function to multiple arrays at once.
@njit
def zscore_nb(x):
return (x[-1] - np.mean(x)) / np.std(x)
data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")
data.close.rolling(14).apply(zscore_nb, raw=True) Date
2020-01-01 00:00:00+00:00 NaN
...
2020-12-27 00:00:00+00:00 1.543527
2020-12-28 00:00:00+00:00 1.734715
2020-12-29 00:00:00+00:00 1.755125
2020-12-30 00:00:00+00:00 2.107147
2020-12-31 00:00:00+00:00 1.781800
Freq: D, Name: Close, Length: 366, dtype: float64data.close.vbt.rolling_apply(14, zscore_nb) 2020-01-01 00:00:00+00:00 NaN
...
2020-12-27 00:00:00+00:00 1.543527
2020-12-28 00:00:00+00:00 1.734715
2020-12-29 00:00:00+00:00 1.755125
2020-12-30 00:00:00+00:00 2.107147
2020-12-31 00:00:00+00:00 1.781800
Freq: D, Name: Close, Length: 366, dtype: float64@njit
def corr_meta_nb(from_i, to_i, col, a, b):
a_window = a[from_i:to_i, col]
b_window = b[from_i:to_i, col]
return np.corrcoef(a_window, b_window)[1, 0]
data2 = vbt.YFData.pull(["ETH-USD", "XRP-USD"], start="2020", end="2021")
vbt.pd_acc.rolling_apply(
14,
corr_meta_nb,
vbt.Rep("a"),
vbt.Rep("b"),
broadcast_named_args=dict(a=data.close, b=data2.close)
)symbol ETH-USD XRP-USD
Date
2020-01-01 00:00:00+00:00 NaN NaN
... ... ...
2020-12-27 00:00:00+00:00 0.636862 -0.511303
2020-12-28 00:00:00+00:00 0.674514 -0.622894
2020-12-29 00:00:00+00:00 0.712531 -0.773791
2020-12-30 00:00:00+00:00 0.839355 -0.772295
2020-12-31 00:00:00+00:00 0.878897 -0.764446
[366 rows x 2 columns]✅ When combining multiple arrays, they often need to be aligned and broadcast before the operation itself. Pandas alone often falls short because it can be too strict. Fortunately, VBT includes an accessor class method that can take a regular Python expression, identify all variable names, extract the arrays from the current context, broadcast them, and then evaluate the expression (with support for NumExpr!) ⌨️
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])
low = data.low
high = data.high
bb = vbt.talib("BBANDS").run(data.close)
upperband = bb.upperband
lowerband = bb.lowerband
bandwidth = (bb.upperband - bb.lowerband) / bb.middleband
up_th = vbt.Param([0.3, 0.4])
low_th = vbt.Param([0.1, 0.2])
expr = """
narrow_bands = bandwidth < low_th
above_upperband = high > upperband
wide_bands = bandwidth > up_th
below_lowerband = low < lowerband
(narrow_bands & above_upperband) | (wide_bands & below_lowerband)
"""
mask = vbt.pd_acc.eval(expr)
mask.sum()low_th up_th symbol
0.1 0.3 BTC-USD 344
ETH-USD 171
0.4 BTC-USD 334
ETH-USD 158
0.2 0.3 BTC-USD 444
ETH-USD 253
0.4 BTC-USD 434
ETH-USD 240
dtype: int64✅ New profiling tools help you measure the execution time and memory usage of any code block 🧰
data = vbt.YFData.pull("BTC-USD")
with (
vbt.Timer() as timer,
vbt.MemTracer() as mem_tracer
):
print(vbt.PF.from_random_signals(data.close, n=100, seed=42).sharpe_ratio)1.0410760501518814print(timer.elapsed())74.15 millisecondsprint(mem_tracer.peak_usage())459.7 kB✅ It is easy to extend classes, but since VBT revolves around functions, how do we enhance them or change their workflow? The easiest way is to introduce a small function (i.e., callback) that the user can provide and that the main function calls at some point. However, this would require the main function to know what arguments to pass to the callback and how to handle its outputs. Here is a better idea: allow most arguments of the main function to become callbacks, then execute those to obtain their actual values. These arguments are called "templates" and this process is known as "substitution". Templates are especially useful when some arguments (such as arrays) should be built only once all required information is available, for example, when other arrays have already been broadcast. Each substitution opportunity has its own identifier so you can control when a template should be substituted. In VBT, templates are first-class citizens and are integrated into most functions for unmatched flexibility! 🧞
def resample_apply(index, by, apply_func, *args, template_context={}, **kwargs):
grouper = index.vbt.get_grouper(by)
results = {}
with vbt.ProgressBar() as pbar:
for group, group_idxs in grouper:
group_index = index[group_idxs]
context = {"group": group, "group_index": group_index, **template_context}
final_apply_func = vbt.substitute_templates(apply_func, context, eval_id="apply_func")
final_args = vbt.substitute_templates(args, context, eval_id="args")
final_kwargs = vbt.substitute_templates(kwargs, context, eval_id="kwargs")
results[group] = final_apply_func(*final_args, **final_kwargs)
pbar.update()
return pd.Series(results)
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
resample_apply(
data.index, "Y",
lambda x, y: x.corr(y),
vbt.RepEval("btc_close[group_index]"),
vbt.RepEval("eth_close[group_index]"),
template_context=dict(
btc_close=data.get("Close", "BTC-USD"),
eth_close=data.get("Close", "ETH-USD")
)
)2017 0.808930
2018 0.897112
2019 0.753659
2020 0.940741
2021 0.553255
2022 0.975911
2023 0.974914
Freq: A-DEC, dtype: float64And many more...
⏩ Look forward to more killer features being added every release!
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.