Features

Configuration and persistence

Save any research object with compression, reload it fast, and keep settings in files

How do you save research state and make it reproducible? VBT can save any of its objects, from data to a 10,000-column portfolio, to one file with optional compression, reload it for analysis, and keep settings in configuration files instead of scattered through code.

Save a 10,000-column portfolio with four compression settings and reload it
import time
from itertools import product

data = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
pairs = list(product(range(2, 102), range(105, 305, 2)))
fast = vbt.MA.run(data.close, window=[f for f, _ in pairs], short_name="fast")
slow = vbt.MA.run(data.close, window=[s for _, s in pairs], short_name="slow")
entries = fast.ma_crossed_above(slow)
exits = fast.ma_crossed_below(slow)
pf = vbt.PF.from_signals(data, entries, exits, fees=0.001)

rows = {}
for compression in [None, "blosc2", "gzip", "lzma"]:
    start = time.perf_counter()
    path = pf.save("sweep", compression=compression)  
    save_s = time.perf_counter() - start
    start = time.perf_counter()
    loaded = vbt.PF.load(path)
    load_s = time.perf_counter() - start
    rows[str(compression)] = dict(
        file=str(path),
        size=vbt.file_size(path),
        save_s=round(save_s, 2),
        load_s=round(load_s, 2),
    )
pd.DataFrame(rows).T
                       file     size save_s load_s
None           sweep.pickle  11.7 MB   0.01    0.0
blosc2  sweep.pickle.blosc2   2.2 MB   0.06   0.09
gzip      sweep.pickle.gzip   1.9 MB   1.15   0.01
lzma      sweep.pickle.lzma   1.0 MB   1.93   0.06
loaded.wrapper.shape, np.allclose(loaded.total_return, pf.total_return)
((1827, 10000), True)

Every file reloads as a full portfolio with all 10,000 strategies, ready for metrics and charts. Measured on an Apple M3, blosc2 cut the file to a fifth of its size and still saved and loaded in under 0.1 seconds. lzma made the smallest file but took about two seconds to write.

Saving and loading

Every VBT object, including data, indicators, portfolios, and splitters, has save and load, and vbt.save and vbt.load handle any Python object, such as a dictionary of portfolios. Only an object's state is saved, not its class, so files load with the current version of VBT, and renamed or moved classes can be mapped to their new names when loading older files. VBT includes reconstruction hooks for loading older objects when class arguments change. Use matching package versions when moving an experiment between machines, or register a state conversion when an older object needs it.

  • Compression: zip, gzip, bz2, and lzma, plus lz4, blosc, and blosc2 when installed.
  • Formats: pickle by default, or readable .cfg, .yaml, and .toml files with file_format, which round-trip back to the same object.
  • Containers: vbt.pdict is a dictionary that saves and loads like any VBT object.
  • Data updates: vbt.CSVDataSaver, vbt.HDFDataSaver, and vbt.DuckDBDataSaver keep a dataset current on disk, as the Data storage page shows.

Keep an experiment together

Save the inputs, parameters, and result in one dictionary so reopening a backtest also tells you what produced it. The objects keep their own shapes and labels, so prices and parameterized indicator outputs can sit together without being forced into one table:

Save prices, settings, and a portfolio in one research bundle
import os
import tempfile

close = pd.Series([100.0, 105.0, 103.0], index=pd.date_range("2025-01-01", periods=3))
parameters = dict(fees=0.001, init_cash=1000)
pf = vbt.PF.from_holding(close, **parameters)
research = vbt.pdict(close=close, parameters=parameters, portfolio=pf, version=vbt.__version__)
with tempfile.TemporaryDirectory() as directory:
    path = research.save(os.path.join(directory, "research"), compression="gzip")
    restored = vbt.pdict.load(path)
print(list(restored))
['close', 'parameters', 'portfolio', 'version']
print(restored["parameters"])
{'fees': 0.001, 'init_cash': 1000}
print(restored["close"].equals(close), np.isclose(restored["portfolio"].total_return, pf.total_return))
True True

The temporary directory keeps this example self-contained. Use a project path to keep the file between sessions. Add indicator outputs, signals, or your own notes to the same dictionary, then load the portfolio for new charts and metrics without repeating the backtest.

Restart data updates

A saved research object and a growing market-data store serve different purposes. To restart a data-saving job, load the stored data, create a new saver around it, and resume scheduled updates with init_save=False. This continues the save cycle without writing the initial dataset again. Data storage covers the storage choices, and Workflow automation covers scheduling.

Settings

vbt.settings holds the defaults of the whole package, grouped by area, such as vbt.settings.portfolio["fees"] or vbt.settings.plotting["default_theme"]. Change a value once and later calls that use that default pick it up. Explicit call arguments let you override it for one experiment. To make the change permanent, put it in a vbt.cfg, vbt.yml, or vbt.toml file in the working directory, or point the VBT_SETTINGS_PATH environment variable at a file. VBT applies it on import, as the Configuration files highlight below shows. A .env file in the working directory is loaded on import as well, which keeps API keys out of code.

You can save just one settings section, such as the portfolio defaults, and reload it with load_update. Use reset() on that section to return to its initial settings. This makes it easy to reuse a project's fees, starting capital, and plotting preferences across notebooks.

Configured objects and templates

Most VBT objects are immutable: a portfolio, indicator, or data instance does not change after it is created. replace returns a copy with some arguments changed, such as pf.replace(init_cash=500), and the original stays untouched, so earlier results can always be inspected. replace rebuilds the object from its configuration. It does not rerun a strategy's order decisions, so test new trading rules with a new simulation. Immutability is a convention, not a copy: inputs are stored as passed, so changing a DataFrame in place after creating an object from it can leave the object out of step with results it has already cached. Copy the input or create the object again.

Settings and arguments are merged with vbt.merge_dicts, which combines nested dictionaries key by key, so an argument you pass overrides only that key of the default. Pass vbt.unsetkey as a value to remove a default key, and wrap a dictionary in vbt.atomic_dict to replace it whole.

Templates delay a value until the information it needs exists. vbt.Rep("name") is replaced by a value from the current context, vbt.RepEval("expression") evaluates an expression in it, and vbt.RepFunc(func) calls a function with it. The Portfolio optimization page uses one to give each rebalancing window its slice of returns, as the Templates highlight below explains.

Cookbook

The members-only Persistence and Configuration cookbook pages collect short recipes for saving objects and changing settings.

Configuration files

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

Define global settings in vbt.cfg
[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_secret
Verify that the settings have been loaded correctly
from vectorbtpro import *

vbt.settings.portfolio["init_cash"]
5000

Compression

✅ 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! 🪶

Save data without and with compression
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 MB
file_path = data.save(compression="blosc")
print(vbt.file_size(file_path))
13.3 MB

Faster loading

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

Define importing settings in vbt.cfg
[importing]
auto_import = False
Measure the loading time
start = utc_time()
from vectorbtpro import *
end = utc_time()
end - start
0.580937910079956

Serialization

✅ 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 🏗️

Backtest each month of data and save the results for later
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_pfs
Date
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: object
vbt.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

Templates

✅ 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! 🧞

Design a template-enhanced resampling functionality
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")
    )
)
Group 7/7
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: 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.