# 配置与持久化 (/zh/features/tooling/configuration-and-persistence)

如何保存研究状态，并使其可复现？VBT 可将任意自身对象，从数据到 10,000 列投资组合，保存为一个可选压缩的文件，再重新加载分析；设置则集中放在配置文件中，而非散落于代码各处。

```python title="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)  # (1)
...     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)
```

1.  压缩名称成为文件扩展名，因此 `vbt.PF.load` 和 `vbt.load` 读回文件时会自动识别。

每个文件重新加载后都是完整投资组合，包含全部 10,000 个策略，可直接计算度量和绘图。在 Apple M3 上，blosc2 将文件缩小至五分之一，保存和加载仍均不到 0.1 秒。lzma
文件最小，但写入约需两秒。

## 保存与加载 \[#saving-and-loading]

每个 VBT 对象，包括数据、指标、投资组合和切分器，都有 `save` 和 `load`；`vbt.save` 和 `vbt.load` 可处理任意 Python
对象，例如投资组合字典。只保存对象状态，不保存类，因此文件用当前 VBT 版本加载；加载旧文件时，重命名或移动过的类可映射到新名称。VBT
还提供重建钩子，在类参数变化后加载旧对象。跨机器迁移实验时使用匹配的软件包版本，旧对象有需要时则注册状态转换。

*   **压缩：** 支持 zip、gzip、bz2 和 lzma，安装后还支持 lz4、blosc 和 blosc2。
*   **格式：** 默认为 pickle，也可用 `file_format` 选择可读的 `.cfg`、`.yaml` 和 `.toml` 文件，并无损还原为同一对象。
*   **容器：** `vbt.pdict` 是可以像其他 VBT 对象一样保存和加载的字典。
*   **数据更新：** `vbt.CSVDataSaver`、`vbt.HDFDataSaver` 和 `vbt.DuckDBDataSaver`
    持续更新磁盘数据集，详见[数据存储](/zh/features/data/data-storage-and-databases/)。

### 将实验内容放在一起 \[#keep-an-experiment-together]

将输入、参数和结果保存到同一字典中，重新打开回测时就能知道它如何产生。对象保留各自形状和标签，因此价格与参数化指标输出可以共存，无须强行塞进同一张表：

```python title="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
```

临时目录使示例可独立运行。若要在会话间保留文件，应使用项目路径。可将指标输出、信号或自己的笔记添加到同一字典，之后加载投资组合绘制新图或计算新度量，无须重跑回测。

### 重新启动数据更新 \[#restart-data-updates]

保存的研究对象与持续增长的市场数据存储用途不同。重新启动数据保存任务时，加载已有数据，围绕它创建新的 saver，并以 `init_save=False`
恢复定时更新。这样可以继续保存周期，无须重复写入初始数据集。[数据存储](/zh/features/data/data-storage-and-databases/)介绍存储选项，[工作流自动化](/zh/features/tooling/workflow-automation/)介绍调度。

## 设置 \[#settings]

`vbt.settings` 按领域保存整个软件包的默认值，例如 `vbt.settings.portfolio["fees"]` 或
`vbt.settings.plotting["default_theme"]`。修改一次后，后续使用该默认值的调用就会采用新值。显式调用参数则可为单次实验覆盖默认值。若要永久保存，可将设置放入工作目录中的
`vbt.cfg`、`vbt.yml` 或 `vbt.toml`，或用 `VBT_SETTINGS_PATH` 环境变量指定文件。VBT
导入时应用配置，见下方[配置文件](#configuration-files)。工作目录中的 `.env` 文件也会在导入时加载，使 API 密钥无需写入代码。

可以只保存某个设置部分，例如投资组合默认值，再用 `load_update` 加载。对该部分调用 `reset()` 可恢复初始设置。这便于在多个 notebook
中复用项目手续费、初始资金和绘图偏好。

## 配置化对象与模板 \[#configured-objects-and-templates]

多数 VBT 对象不可变：投资组合、指标或数据实例创建后不再改变。`replace` 返回修改了部分参数的副本，例如
`pf.replace(init_cash=500)`，原对象保持不变，因此始终可以检查早期结果。`replace`
根据配置重建对象，不会重新运行策略的订单决策，因此新交易规则应使用新模拟测试。不可变性是约定，并非复制：输入按传入方式保存，因此由 DataFrame
创建对象后再原地修改它，可能使对象与已缓存结果不一致。应复制输入或重新创建对象。

设置和参数通过 `vbt.merge_dicts` 合并，逐键组合嵌套字典，因此传入参数只覆盖默认值中的对应键。以 `vbt.unsetkey` 为值可移除默认键；用
`vbt.atomic_dict` 包装字典则可整体替换。

模板将值的求解推迟到所需信息齐备时。`vbt.Rep("name")` 替换为当前上下文中的值，`vbt.RepEval("expression")`
在上下文中计算表达式，`vbt.RepFunc(func)`
则以该上下文调用函数。[投资组合优化](/zh/features/optimization/portfolio-optimization/)页用模板为每个再平衡窗口提供相应收益切片，详见下方[模板](#templates)。

!!! info "实用示例"
    会员专享的[持久化（英文）](https://members.vectorbt.pro/cookbook/persistence/)和[配置（英文）](https://members.vectorbt.pro/cookbook/configuration/)页面收录保存对象与修改设置的简短示例。

## 配置文件 \[#configuration-files]

新增于 v2025.10.15

✅ VBT
扩展常见配置格式（INI、YAML、TOML），形成自己的配置格式，使用户可保存、内省、修改及加载复杂内部对象。其主要优点是可读性和无损往返：任何对象都可编码后再解码，不丢失信息。主要特性包括嵌套结构、引用、字面量解析，以及任意
Python 表达式求值。此外，可为 VBT 创建配置文件并放在工作目录中，每次导入软件包时都用它更新默认设置。

=== "INI"
    ```ini title="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
    ```

=== "YAML"
    ```yaml title="Define global settings in vbt.yml"
    plotting:
      default_theme: dark

    portfolio:
      init_cash: 5000

    data.custom.binance.client_config:
      api_key: &api_key <YOUR_API_KEY>
      api_secret: &api_secret <YOUR_API_SECRET>

    data.custom.ccxt.exchanges.binance.exchange_config:
      apiKey: *api_key
      secret: *api_secret
    ```

=== "TOML"
    ```toml title="Define global settings in vbt.toml"
    [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 = { __vbt_ref__ = "data.custom.binance.client_config.api_key" }
    secret = { __vbt_ref__ = "data.custom.binance.client_config.api_secret" }
    ```

```python title="Verify that the settings have been loaded correctly"
>>> from vectorbtpro import *

>>> vbt.settings.portfolio["init_cash"]
5000
```

## 压缩 \[#compression]

新增于 1.10.0

✅ 序列化的 VBT 对象有时会占用大量磁盘空间。此次更新支持多种压缩算法，让文件尽可能轻巧！🪶

```python title="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]

新增于 1.10.0

✅ 若工作流不需要访问器、Plotly 图表或多数其他可选功能，可完全关闭自动导入，将 VBT 加载时间缩短到一秒以内 ⏳

=== "INI"
    ```ini title="Define importing settings in vbt.cfg"
    [importing]
    auto_import = False
    ```

=== "YAML"
    ```yaml title="Define importing settings in vbt.yml"
    importing:
      auto_import: false
    ```

=== "TOML"
    ```toml title="Define importing settings in vbt.toml"
    [importing]
    auto_import = false
    ```

```python title="Measure the loading time"
>>> start = utc_time()
>>> from vectorbtpro import *
>>> end = utc_time()
>>> end - start
0.580937910079956
```

## 序列化 \[#serialization]

新增于 1.9.0

✅ 与机器学习模型一样，每个原生 VBT 对象都可序列化并保存为二进制文件，分享数据与研究见解更加方便！另一个好处是只序列化对象实际内容，不保存类定义，因此加载后的对象始终使用最新类定义。VBT
还实现了专门逻辑，在 API 出现破坏性变更时帮助“重建”对象 🏗️

```python title="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")  # (1)

>>> month_pfs = vbt.load("month_pfs")  # (2)
>>> 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
```

1.  保存到磁盘。
2.  之后从磁盘加载。

## 模板 \[#templates]

新增于 1.0.0

✅ 扩展类很容易，但 VBT
以函数为中心，如何增强函数或改变流程？最简单的方式是引入用户提供的小函数（即回调），由主函数在某个时刻调用。然而，主函数需要知道向回调传什么参数，以及如何处理输出。更好的方式是：允许主函数的大多数参数变成回调，再执行这些回调以取得实际值。这些参数称为“模板”，过程称为“替换”。某些参数（如数组）只有在信息齐备后才适合构建，例如其他数组已完成广播，此时模板尤其有用。每个替换时机都有自己的标识符，便于控制何时替换模板。模板在
VBT 中是一等公民，集成到大多数函数中，提供出色的灵活性！🧞

```python title="Design a template-enhanced resampling functionality"
>>> def resample_apply(index, by, apply_func, *args, template_context={}, **kwargs):
...     grouper = index.vbt.get_grouper(by)  # (1)
...     results = {}
...     with vbt.ProgressBar() as pbar:
...         for group, group_idxs in grouper:  # (2)
...             group_index = index[group_idxs]
...             context = {"group": group, "group_index": group_index, **template_context}  # (3)
...             final_apply_func = vbt.substitute_templates(apply_func, context, eval_id="apply_func")  # (4)
...             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),  # (5)
...     vbt.RepEval("btc_close[group_index]"),  # (6)
...     vbt.RepEval("eth_close[group_index]"),
...     template_context=dict(
...         btc_close=data.get("Close", "BTC-USD"),  # (7)
...         eth_close=data.get("Close", "ETH-USD")
...     )
... )
```

1.  构建分组器，接受分组和重采样指令。
2.  遍历分组器中的各组。每组包含标签（如 `2017-01-01 00:00:00+00:00`）及对应行索引。
3.  用当前组信息和用户提供的外部信息创建新上下文。
4.  使用新填充的上下文替换函数和参数。
5.  计算两个数组相关系数的简单函数。
6.  将两个参数定义为表达式模板，分别为每组选择数据。表达式中的所有变量会自动识别，并用当前上下文替换。求值后，模板替换为输出。
7.  指定模板依赖的其他信息。

分组 7/7: 100%

```text title="Output"
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
```
