功能

前向滚动与交叉验证

通过滚动、扩展和清除重叠信息的切分,在样本外测试优化参数

优化后的策略在从未见过的数据上能否成立?VBT 在 Python 中运行前向滚动优化和时间序列交叉验证:将历史切分为训练和测试区间,在各训练区间选择参数,并仅在之后区间评估。

Walk-forward optimize a moving average strategy over 16 quarters
from itertools import product

data = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
splitter = vbt.Splitter.from_rolling(  
    data.index,
    length=455,
    split=365,
    offset=90,
    offset_anchor="prev_start",
    set_labels=["train", "test"],
)
splitter.n_splits
16
pairs = [(f, s) for f, s in product(range(10, 55, 5), range(60, 220, 20))]
fast_windows, slow_windows = zip(*pairs)
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_above(slow)  
exits = fast.ma_below(slow)

bounds = splitter.get_bounds(index_bounds=True)
rows, oos_returns = [], []
for split in range(splitter.n_splits):
    train_start, train_end = bounds.loc[(split, "train")]
    test_start, test_end = bounds.loc[(split, "test")]
    train = slice(train_start, train_end - pd.Timedelta(days=1))
    test = slice(test_start, test_end - pd.Timedelta(days=1))
    train_pf = vbt.PF.from_signals(
        data.loc[train], entries.loc[train], exits.loc[train], fees=0.001
    )
    best = train_pf.sharpe_ratio.idxmax()  
    test_pf = vbt.PF.from_signals(
        data.loc[test], entries.loc[test, best], exits.loc[test, best], fees=0.001
    )
    rows.append(dict(
        test_start=test_start.date(),
        fast=best[0],
        slow=best[1],
        train_sharpe=train_pf.sharpe_ratio[best],
        test_sharpe=test_pf.sharpe_ratio,
    ))
    oos_returns.append(test_pf.returns)

summary = pd.DataFrame(rows)
summary.head(6).round(2)
   test_start  fast  slow  train_sharpe  test_sharpe
0  2020-12-31    10   200          3.19         3.56
1  2021-03-31    10   200          3.42        -2.00
2  2021-06-29    20    60          2.83         0.98
3  2021-09-27    20    60          2.97         0.12
4  2021-12-26    20    80          1.86        -2.01
5  2022-03-26    20    80          0.80        -1.74
summary[["train_sharpe", "test_sharpe"]].mean().round(2)
train_sharpe    1.92
test_sharpe     0.24
dtype: float64
oos = pd.concat(oos_returns)  
hold = data.close.loc[oos.index] / data.close.loc[oos.index].iloc[0]
print(round((1 + oos).prod(), 3), round(hold.iloc[-1], 3))
2.744 3.36
2021 至 2024 年前向滚动优化比特币均线策略与买入持有的权益比较 图表数据(JSON)

每个训练年中表现最佳的参数,样本内平均夏普比率为 1.92,而随后季度仅为 0.24。拼接后,样本外策略增长至 2.7 倍,持有比特币增长至 3.4 倍。揭示这种样本内外差距,正是前向滚动测试的目的。

前向滚动优化

前向滚动测试重复同一循环:在训练窗口优化,将结果应用于下一窗口,再向前移动。连接测试收益,评估重复选择过程。上方每个测试窗口独立模拟,若快均线高于慢均线,可在第一根 K 线入场。如果策略要求每个边界必须空仓,请在每个测试窗口最后一根 K 线添加退出;如果状态必须延续,请按实时模拟页面的方法,从一个窗口继续投资组合到下一个窗口。测试期投资组合可通过 vbt.PF.row_stack 沿时间连接,形成具备全部指标和图表的单个样本外投资组合。

您选择每次决策使用多少历史。滚动窗口保留固定长度的近期数据,例如最近一年。扩展窗口随新观测到来保留较早历史。按日历切分可匹配研究计划,例如每月选择参数或每季重新训练模型。

切分器

vbt.Splitter 构建切分:

构造器切分形式用途
from_rolling、from_n_rolling向前移动的固定长度窗口在近期历史上重新优化
from_expanding、from_n_expanding从固定起点增长的窗口保留全部可用训练历史
from_grouper月、季等日历区间按时期比较绩效
from_n_random用于稳健性检查的随机窗口测试不同历史起点
from_ranges、from_splits您定义的准确区间自定义研究计划
from_purged_walkforward、from_purged_kfold清除重叠信息的切分分离重叠的标签信息

每次切分可包含训练、测试等多个集合,以 K 线数、比例或时长指定大小,并在其间留出间隔。splitter.plot() 在时间轴上显示每次切分,get_coverage 和 get_overlap_matrix 衡量各集合覆盖多少数据,以及切分之间重叠多少,如下方切分器功能介绍所示。

在训练与测试之间留出间隔

扩展窗口可以在增长训练历史的同时,保留固定测试期及两者之间的间隔。这里每次测试覆盖三天,开始前留出两天。表格显示各集合第一个和最后一个包含的日期。

Inspect expanding windows with a gap
index = pd.date_range("2025-01-01", periods=18, freq="D")
splitter = vbt.Splitter.from_expanding(
    index,
    min_length=12,
    offset=3,
    split=(1.0, vbt.RelRange(length=2, is_gap=True), 3),
    split_range_kwargs=dict(backwards=True),
    set_labels=["train", "test"],
)
splitter.get_bounds(index_bounds=True, right_inclusive=True)
bound            start        end
split set
0     train 2025-01-01 2025-01-07
      test  2025-01-10 2025-01-12
1     train 2025-01-01 2025-01-10
      test  2025-01-13 2025-01-15
2     train 2025-01-01 2025-01-13
      test  2025-01-16 2025-01-18

is_gap=True 将这些日期排除在两个集合之外。反向切分先保留测试期和间隔,再将剩余历史分配给训练。使用同一切分器,从价格、特征和标签取得匹配切片。

应用切分

splitter.take(obj) 将数据、指标或投资组合切成各区间,splitter.apply 在每个区间运行函数。@vbt.split 装饰器为任意函数添加切分,@vbt.cv_split 将切分与参数搜索结合:一次调用在每个训练集优化、选择最佳组合,并在测试集评估,如下方 CV 装饰器和切分装饰器功能介绍所示。结果按切分和集合标注,可直接比较。默认 cv_split 保留结果最高的组合,因此函数应为每种组合返回一个数字,例如夏普比率。设置 selection="min" 则保留最低者;基于网格结果的模板可应用其他任意规则,例如由多个指标构成的评分。

检查完整参数网格

使用 return_grid=True 保留训练网格,或用 return_grid="all" 在每个集合评估完整网格。这样可比较周围参数,了解强劲训练结果来自广泛区域还是孤立组合。选择仍来自训练结果。额外测试结果可用于研究稳健性。

跨资产与时间周期验证

在 cv_split 外层再包一层参数化函数,就能为每个资产独立运行验证流程。外层搜索选择资产,内层搜索在每个训练窗口选择策略参数。结果保留标签,方便比较哪些设置跨标的有效,哪些依赖单一市场。

切分边界也可在一个时间索引上定义,再应用于其他频率的数据。例如,定义日频训练和测试边界,再切片回测使用的分钟 K 线。

保留指标历史

测试窗口可使用更早价格预热均线,而不将这些早期 K 线计入绩效。像开头示例一样,预先计算只使用当前及历史价格的指标,或为函数提供历史缓冲区,在模拟前裁剪。如果长回看期会使较短测试窗口的大部分时间没有信号,这尤其有用。

清除重叠信息与组合交叉验证

当标签像许多机器学习目标一样使用未来信息时,靠近测试期的训练样本可能包含该测试期信息。清除(purging)移除这类样本,禁运期(embargo)按照 López de Prado 的方法,在每个测试期之后留出间隔。PurgedWalkForwardCV 对前向滚动折叠清除重叠信息,PurgedKFoldCV 运行组合清除交叉验证:选定数量折叠的每种组合都成为测试集,因此一段历史产生多次样本外测试,而非仅一次,如下方清除重叠信息的 CV功能介绍所示。

对于结果在不同时间才可获知的标签,请提供预测时间和评估时间,让清除过程考虑每个样本的信息区间。按时间重新训练使用清除重叠信息的前向滚动切分,比较多种训练测试安排则使用组合清除切分。下方十折中取两折测试的示例产生 45 种组合。

scikit-learn

切分器与 scikit-learn 双向兼容。vbt.SplitterCV 将 VBT 切分器转换为用于 GridSearchCV 或 cross_val_score 的 scikit-learn 交叉验证器,vbt.Splitter.from_sklearn 则将 scikit-learn 切分器转换为 VBT 切分器,以便绘图和分析。

这也为现有 scikit-learn 模型接入流程提供途径。一起切分特征和标签,在训练数据拟合模型,再在匹配测试集上评估预测。如果拟合模型、生成信号和回测预测需要独立步骤,您可将相同切片传入自己的 Python 流水线。

教程

会员专属的交叉验证(英文)教程通过切分器和装饰器演示完整验证流程。

清除重叠信息的 CV

✅ 基于 Marcos Lopez de Prado 的《金融机器学习进阶》,新增支持带清除重叠信息的前向滚动交叉验证(CV),以及带清除与禁运期的组合 CV。

Create and plot a combinatorial splitter with purging and embargoing
splitter = vbt.Splitter.from_purged_kfold(
    vbt.date_range("2024", "2025"),
    n_folds=10,
    n_test_folds=2,
    purge_td="3 days",
    embargo_td="3 days"
)
splitter.plots().show()
2024 年包含训练集与测试集的组合清除交叉验证折叠 图表数据(JSON)

CV 装饰器

✅ 大多数交叉验证任务都需要在训练数据上测试参数组合网格、选择最佳组合,再在测试数据验证。每次切分都要重复此过程。交叉验证装饰器将参数化与切分装饰器组合,以自动完成这一任务。

Cross-validate a SMA crossover using random search
@vbt.cv_split(
    splitter="from_rolling",
    splitter_kwargs=dict(length=365, split=0.5, set_labels=["train", "test"]),
    takeable_args=["data"],
    parameterized_kwargs=dict(random_subset=100, seed=42),
    merge_func="concat"
)
def sma_crossover_cv(data, fast_period, slow_period, metric):
    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"),
    vbt.Param(np.arange(20, 50), condition="x < slow_period"),
    vbt.Param(np.arange(20, 50)),
    "trades.expectancy"
)
切分 7/7
split  set    fast_period  slow_period
0      train  20           25               8.015725
       test   20           23               0.573465
1      train  40           48              -4.356317
       test   39           40               5.666271
2      train  24           45              18.253340
       test   22           36             111.202831
3      train  20           31              54.626024
       test   20           25              -1.596945
4      train  25           48              41.328588
       test   25           30               6.620254
5      train  26           32               7.178085
       test   24           29               4.087456
6      train  22           23              -0.581255
       test   22           31              -2.494519
dtype: float64

教程

更多内容请参阅交叉验证教程。

切分装饰器

✅ 通常,要在每个切分上运行函数,必须针对传给函数的输入数据构建专用切分器。这意味着每次输入变化都要重建切分器。切分装饰器通过封装函数,获得全部参数并按需决定切分方式,从而自动化这一过程。它可以为任意 Python 函数“感染”上切分能力 🦠

Get total return from holding in each quarter
@vbt.split(
    splitter="from_grouper",
    splitter_kwargs=dict(by="Q"),
    takeable_args=["data"],
    merge_func="concat"
)
def get_quarter_return(data):
    return data.returns.vbt.returns.total()

data = vbt.YFData.pull("BTC-USD")
get_quarter_return(data.loc["2021"])
Date
2021Q1    1.005805
2021Q2   -0.407050
2021Q3    0.304383
2021Q4   -0.037627
Freq: Q-DEC, dtype: float64
get_quarter_return(data.loc["2022"])
Date
2022Q1   -0.045047
2022Q2   -0.572515
2022Q3    0.008429
2022Q4   -0.143154
Freq: Q-DEC, dtype: float64

教程

更多内容请参阅交叉验证教程。

切分器

✅ scikit-learn 中的切分器并不完全适合验证基于机器学习或规则的交易策略。VBT 提供强大类,支持许多对回测安全的切分方案,包括滚动窗口、扩展窗口、时间锚定窗口、用于分块自助法的随机窗口,甚至 Pandas 原生 groupby、resample 指令,例如表示月频的“M”。此外,生成的切分可轻松分析和可视化!例如,可检测切分或集合重叠,将全部切分转换为单一布尔掩码以自定义分析,对切分和集合分组,并分析它们的相互分布。这个类的代码行数超过整个 backtesting.py 软件包,别小看这位新王者!🦏

Roll a 360-day window and split it equally into train and test sets
data = vbt.YFData.pull("BTC-USD", start="4 years ago")
splitter = vbt.Splitter.from_rolling(
    data.index,
    length="360 days",
    split=0.5,
    set_labels=["train", "test"],
    freq="daily"
)
splitter.plots().show()
BTC-USD 滚动 360 天窗口,平均分为训练集和测试集 图表数据(JSON)

教程

更多内容请参阅交叉验证教程。

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。