功能

参数优化

使用网格、条件和随机搜索遍历数百万种参数组合

哪些参数有效,如何在时间和内存限制内测试数百万种组合?VBT 通过一个装饰器将任意函数转为参数扫描,按条件构建网格,并可将编译函数分块运行于全部核心。从现有 Python 策略开始,从少量参数值扩展到数百万种组合。

Backtest 1,000,000 parameter combinations and keep only the Sharpe ratio
close = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01").close

@vbt.parameterized(  
    merge_func="concat",
    engine="threadpool",
    chunk_len="auto",
    warmup=True,
)
@njit(nogil=True)  
def sharpe_nb(close, fast_window, slow_window, sl_stops):
    fast = vbt.nb.rolling_mean_1d_nb(close, fast_window)
    slow = vbt.nb.rolling_mean_1d_nb(close, slow_window)
    entries = vbt.nb.crossed_above_1d_nb(fast, slow)
    exits = vbt.nb.crossed_above_1d_nb(slow, fast)
    n = len(sl_stops)
    sim_out = vbt.pf_nb.from_signals_nb(  
        target_shape=(len(close), n),
        group_lens=np.full(n, 1),
        close=close,
        long_entries=entries,
        long_exits=exits,
        sl_stop=sl_stops.reshape(1, -1),
        fees=0.001,
        save_returns=True,
    )
    return vbt.ret_nb.sharpe_ratio_nb(sim_out.in_outputs.returns, 365.0)

sl_stops = np.linspace(0.01, 0.30, 100)
sharpe = sharpe_nb(
    close.values,
    vbt.Param(np.arange(2, 102), name="fast_window"),
    vbt.Param(np.arange(105, 305, 2), name="slow_window"),
    sl_stops,
)
len(sharpe)  
1000000
sharpe.index = sharpe.index.set_levels(sl_stops.round(4), level=2)
sharpe.index = sharpe.index.set_names("sl_stop", level=2)
sharpe.nlargest(5).round(3)
fast_window  slow_window  sl_stop
2            107          0.0129     1.608
                          0.0159     1.607
             119          0.0246     1.603
             117          0.0510     1.594
                          0.0539     1.594
dtype: float64
best_per_pair = sharpe.groupby(["fast_window", "slow_window"]).max()
best_per_pair.vbt.heatmap(x_level="fast_window", y_level="slow_window").show()
2020 至 2024 年比特币 100 个快速和 100 个慢速均线窗口的最佳夏普比率热图 图表数据(JSON)

每次回测仅保留一个数值,因此一百万次回测只需几兆字节。热图显示每组窗口在所有止损水平中的最佳夏普比率。在该牛市中,95% 的组合夏普比率为正,因此孤立的最佳值本身说明不了太多。

参数化任意函数

@vbt.parameterized 封装任意函数,从几行 pandas 到完整回测。以 vbt.Param 封装的实参成为参数,各组合传入函数,结果合并为以参数值为索引层级的带标签对象。合并可拼接、按列堆叠或返回原始列表;参数可以嵌套、跨调用共享,或本身就是函数。下方参数化装饰器功能介绍在完整策略上展示这一模式。

您可以保留现有 pandas 计算和 Python 策略逻辑。参数值也可以是函数,因此同一流水线可比较不同入场规则及不同回看窗口。为每条规则添加标签,让结果易于识别。

跨组合复用计算

100 组窗口共用的一条均线只需计算一次。预先计算指标,再将输出传入搜索。下方可参数化对象示例先运行全部均线,再将指标对象组合成快慢配对。也可以分阶段搜索:先找到有用的入场设置,复用这些信号,再探索止损和仓位规模。

笛卡尔、配对与惰性网格

参数默认按笛卡尔积组合,共享 level 时则逐元素配对。条件在运行前移除不合理组合,例如快速窗口长于慢速窗口,如条件参数功能介绍所示。大型网格惰性生成,因此从数十亿潜在组合中随机抽取子集前,无需将完整网格装入内存,详见惰性参数网格。

搜索策略

穷举搜索只是一个选项。random_subset 测试网格的随机样本,通常以一小部分成本找到相同优良区域,如随机搜索功能介绍所示。分阶段搜索先使用粗网格,再缩小到最佳区域附近的细网格。抽样时设置随机种子,可以重复选择相同参数组合。

使用 Optuna 或 Hyperopt 优化 VBT 回测

对于贝叶斯或进化搜索,可将 VBT 回测用作 Optuna 或 Hyperopt 的目标函数。优化器提出参数组合,您的函数计算信号并运行回测,再返回待优化指标。无需先计算整个网格,就能搜索回看窗口、信号阈值、止损水平或模型设置。

目标由您定义。例如,最大化扣费后收益指标,或将收益和回撤组合为评分。初始网格搜索和后续优化器驱动搜索可使用同一个回测函数。VBT 负责模拟和指标,外部优化器选择接下来尝试哪些参数。

大规模扫描

三种设置让大规模扫描保持快速:

  • 分块。 chunk_len 将组合分为多个块,在并行线程或进程中运行。对于 Numba 函数,warmup=True 在分发其余任务前先运行第一个任务,以提前完成编译。
  • 单体分块。 将多个参数值合并为一次调用,让函数按列处理;上方止损水平示例和下方两个参数的单体分块功能介绍都使用此方式。
  • 仅返回指标。 每个组合返回单个指标而非投资组合,并在模拟时保存收益率,让指标无需重建收益率。

长时间运行时,分块缓存可将已完成结果保存到磁盘,在恢复相同搜索时复用。请参阅并行执行与缓存。流水线也可返回 vbt.NoResult,从合并结果中排除不需要的组合,例如交易过少而无法评估的策略。

在结果中选择

大型网格中最高夏普比率通常是异常值。请根据多个数值排名:返回的 pandas 对象按参数索引,只需几行代码,就能将夏普比率、最大回撤和交易次数等多个指标标准化后平均为评分。优先选择相邻参数也表现良好的区域;需要多个策略时,应选择彼此不同的组合,而非往往近乎重复的排名前几名。比较交易持续时间和敞口,判断变体是否真的以不同方式交易。前向滚动与交叉验证页面的滚动测试检查选择能否在选择数据之外成立。参数敏感性与选择偏差检验,见稳健性与过拟合。

在同一表格比较多个指标

保留选择所需数值,将完整投资组合留在函数内部。此小示例使用虚构价格和三个均线窗口,信号延后一根 K 线,每次回测均包含手续费。

Compare return, drawdown, and trade count
close = pd.Series(
    [100, 102, 105, 103, 99, 101, 106, 110, 107, 102, 104, 109, 114, 111, 106, 108, 115, 118, 113, 110],
    index=pd.date_range("2025-01-01", periods=20),
)

@vbt.parameterized(merge_func="column_stack")
def compare_windows(close, window):
    average = close.rolling(window).mean()
    entries = (close > average).shift(1, fill_value=False)
    exits = (close < average).shift(1, fill_value=False)
    pf = vbt.PF.from_signals(close, entries, exits, fees=0.001, freq="1D")
    return pd.Series({
        "Return [%]": pf.total_return * 100,
        "Max drawdown [%]": pf.max_drawdown * 100,
        "Closed trades": pf.trades.status_closed.count(),
    })

results = compare_windows(close, vbt.Param([2, 3, 5])).T
results.round(2)
        Return [%]  Max drawdown [%]  Closed trades
window
2           -16.28            -16.28            4.0
3           -23.36            -23.36            4.0
5           -16.92            -16.92            2.0
results.index[results["Closed trades"] >= 3].tolist()
[2, 3]

最后一行选择至少三笔已平仓交易的窗口。您可按自己的标准修改筛选,或在返回的 Series 中添加绩效指标。一张结果表提供每种设置的收益、回撤和交易次数,可直接用于自己的选择规则。

可参数化对象

✅ 每个可分析 VBT 对象(例如数据、指标或投资组合)现在都可拆成多个相同类型的小对象,每个仅包含一列或一个分组。这让 VBT 对象可作为独立参数,每次只处理部分信息,例如数据实例中的一个标的,或指标中的一种参数组合。

Combine outputs of a SMA indicator combinatorially
@vbt.parameterized(merge_func="column_stack")
def get_signals(fast_sma, slow_sma):  
    entries = fast_sma.crossed_above(slow_sma)
    exits = fast_sma.crossed_below(slow_sma)
    return entries, exits

data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])
sma = data.run("talib:sma", timeperiod=range(20, 50, 2))  
fast_sma = sma.rename_levels({"sma_timeperiod": "fast"})  
slow_sma = sma.rename_levels({"sma_timeperiod": "slow"})
entries, exits = get_signals(
    vbt.Param(fast_sma, condition="__fast__ < __slow__"),  
    vbt.Param(slow_sma)
)
entries.columns
MultiIndex([(20, 22, 'BTC-USD'),
            (20, 22, 'ETH-USD'),
            (20, 24, 'BTC-USD'),
            (20, 24, 'ETH-USD'),
            (20, 26, 'BTC-USD'),
            (20, 26, 'ETH-USD'),
            ...
            (44, 46, 'BTC-USD'),
            (44, 46, 'ETH-USD'),
            (44, 48, 'BTC-USD'),
            (44, 48, 'ETH-USD'),
            (46, 48, 'BTC-USD'),
            (46, 48, 'ETH-USD')],
           names=['fast', 'slow', 'symbol'], length=210)

惰性参数网格

✅ 如果只关注所有参数组合的一个子集,参数化装饰器不再需要将参数网格实体化。无论全部潜在组合有多大,都几乎可以立即生成随机参数组合。

Test a random subset of a huge number of parameter combinations
@vbt.parameterized(merge_func="concat")
def test_combination(data, n, sl_stop, tsl_stop, tp_stop):
    return data.run(
        "from_random_signals",
        n=n,
        sl_stop=sl_stop,
        tsl_stop=tsl_stop,
        tp_stop=tp_stop,
        seed=42,
    ).total_return

n = np.arange(10, 100)
sl_stop = np.arange(1, 1000) / 1000
tsl_stop = np.arange(1, 1000) / 1000
tp_stop = np.arange(1, 1000) / 1000
len(n) * len(sl_stop) * len(tsl_stop) * len(tp_stop)
89730269910
test_combination(
    vbt.YFData.pull("BTC-USD"),
    n=vbt.Param(n),
    sl_stop=vbt.Param(sl_stop),
    tsl_stop=vbt.Param(tsl_stop),
    tp_stop=vbt.Param(tp_stop),
    _random_subset=10,
    _seed=42
)
n   sl_stop  tsl_stop  tp_stop
18  0.476    0.485     0.862       2.233997
21  0.530    0.697     0.017       0.091041
49  0.499    0.560     0.954      15.987541
50  0.535    0.200     0.477       2.182477
72  0.763    0.360     0.129      -0.040304
78  0.503    0.071     0.533      10.851910
79  0.656    0.388     0.930      11.854461
80  0.746    0.042     0.644       0.702603
87  0.274    0.539     0.434       2.072847
97  0.806    0.206     0.426       7.136395
dtype: float64

单体分块

✅ 参数化装饰器现在支持将参数组合拆成“单体分块”,将每块中的参数值合并为单个值,并通过一次函数调用运行整块。这意味着不再局限于一次仅处理一种参数组合 🌪️ 请注意,函数必须调整为可处理多个参数值,合并函数也应按需修改。

Test 100 combinations of SL and TP values per thread
@vbt.parameterized(
    merge_func="concat",
    mono_chunk_len=100,  
    chunk_len="auto",  
    engine="threadpool",  
    warmup=True  
)
@njit(nogil=True)
def test_stops_nb(close, entries, exits, sl_stop, tp_stop):
    sim_out = vbt.pf_nb.from_signals_nb(
        target_shape=(close.shape[0], sl_stop.shape[1]),
        group_lens=np.full(sl_stop.shape[1], 1),
        close=close,
        long_entries=entries,
        short_entries=exits,
        sl_stop=sl_stop,
        tp_stop=tp_stop,
        save_returns=True
    )
    return vbt.ret_nb.total_return_nb(sim_out.in_outputs.returns)

data = vbt.YFData.pull("BTC-USD", start="2020")  
entries, exits = data.run("randnx", n=10, seed=42, hide_params=True, unpack=True)  
sharpe_ratios = test_stops_nb(
    vbt.to_2d_array(data.close),
    vbt.to_2d_array(entries),
    vbt.to_2d_array(exits),
    sl_stop=vbt.Param(np.arange(0.01, 1.0, 0.01), mono_merge_func=np.column_stack),  
    tp_stop=vbt.Param(np.arange(0.01, 1.0, 0.01), mono_merge_func=np.column_stack)
)
sharpe_ratios.vbt.heatmap().show()
BTC-USD 不同止损与止盈组合的总收益热图 图表数据(JSON)

条件参数

✅ 参数可以相互依赖。例如,测试均线交叉时,快速窗口长于慢速窗口没有意义。过滤这类情况后,只需评估大约一半的参数组合。

Test slow windows being longer than fast windows by at least 5
@vbt.parameterized(merge_func="column_stack")
def ma_crossover_signals(data, fast_window, slow_window):
    fast_sma = data.run("sma", fast_window, short_name="fast_sma")
    slow_sma = data.run("sma", slow_window, short_name="slow_sma")
    entries = fast_sma.real_crossed_above(slow_sma.real)
    exits = fast_sma.real_crossed_below(slow_sma.real)
    return entries, exits

entries, exits = ma_crossover_signals(
    vbt.YFData.pull("BTC-USD", start="one year ago UTC"),
    vbt.Param(np.arange(5, 50), condition="slow_window - fast_window >= 5"),
    vbt.Param(np.arange(5, 50))
)
entries.columns
MultiIndex([( 5, 10),
            ( 5, 11),
            ( 5, 12),
            ( 5, 13),
            ( 5, 14),
            ...
            (42, 48),
            (42, 49),
            (43, 48),
            (43, 49),
            (44, 49)],
           names=['fast_window', 'slow_window'], length=820)

✅ 网格搜索测试每种可能的超参数组合,而随机搜索选择并测试随机组合。这在参数组合数量巨大时尤其有用。研究也表明,随机搜索可以更少函数评估找到与网格搜索相当或更好的值。指标工厂、参数化装饰器和所有执行广播的方法,现在都内置支持随机搜索。

Test a random subset of SL, TSL, and TP combinations
data = vbt.YFData.pull("BTC-USD", start="2020")
stop_values = np.arange(1, 100) / 100  
pf = vbt.PF.from_random_signals(
    data,
    n=100,
    seed=42,
    sl_stop=vbt.Param(stop_values),
    tsl_stop=vbt.Param(stop_values),
    tp_stop=vbt.Param(stop_values),
    broadcast_kwargs=dict(random_subset=1000, seed=42)  
)
pf.total_return.sort_values(ascending=False)
sl_stop  tsl_stop  tp_stop
0.02     0.22      0.55       1.093685
0.93     0.60      0.99       1.091450
0.88     0.68      0.99       1.091450
0.79     0.69      0.99       1.091450
         0.62      0.99       1.091450
                                   ...
0.08     0.84      0.14      -0.466277
0.14     0.09      0.12      -0.502329
0.80     0.11      0.11      -0.509395
0.19     0.11      0.09      -0.529528
0.29     0.11      0.06      -0.563992
Name: total_return, Length: 1000, dtype: float64

参数化装饰器

✅ 特殊装饰器让任意 Python 函数接受多种参数组合,即使函数本身只支持一种。装饰器封装函数并访问实参,识别充当参数的全部实参,构建网格,再对网格中每个组合调用底层函数。执行可以轻松并行化。全部输出就绪后,合并为单个对象。用途无穷:从运行无法通过指标工厂封装的指标,到参数化整个流水线!🪄

Parameterize a basic SMA indicator without Indicator Factory
@vbt.parameterized(merge_func="column_stack")  
def sma(close, window):
    return close.rolling(window).mean()

data = vbt.YFData.pull("BTC-USD")
sma(data.close, vbt.Param(range(20, 50)))
组合 30/30
window                               20            21            22  \
Date
2014-09-17 00:00:00+00:00           NaN           NaN           NaN
2014-09-18 00:00:00+00:00           NaN           NaN           NaN
2014-09-19 00:00:00+00:00           NaN           NaN           NaN
...                                 ...           ...           ...
2024-03-07 00:00:00+00:00  57657.135156  57395.376488  57147.339134
2024-03-08 00:00:00+00:00  58488.990039  58163.942708  57891.045455
2024-03-09 00:00:00+00:00  59297.836523  58956.156064  58624.648793

...

window                               48            49
Date
2014-09-17 00:00:00+00:00           NaN           NaN
2014-09-18 00:00:00+00:00           NaN           NaN
2014-09-19 00:00:00+00:00           NaN           NaN
...                                 ...           ...
2024-03-07 00:00:00+00:00  49928.186686  49758.599330
2024-03-08 00:00:00+00:00  50483.072266  50303.123565
2024-03-09 00:00:00+00:00  51040.440837  50846.672353

[3462 rows x 30 columns]

类数组参数

✅ 广播机制已完全重构,现在支持参数。VBT 中许多参数,例如止损和止盈,都是类数组,可逐行、逐列甚至逐元素提供。内部连标量也视为普通时间序列,与其他真正的时间序列一起广播。此前,为测试多种参数组合,必须将其他时间序列平铺,使形状完全一致。现在平铺过程会自动完成!

Write a steep slope indicator without indicator factory
def steep_slope(close, up_th):
    r = vbt.broadcast(dict(close=close, up_th=up_th))
    return r["close"].pct_change() >= r["up_th"]

data = vbt.YFData.pull("BTC-USD", start="2020", end="2022")
fig = data.plot(plot_volume=False)
sma = vbt.talib("SMA").run(data.close, timeperiod=50).real
sma.rename("SMA").vbt.plot(fig=fig)
mask = steep_slope(sma, vbt.Param([0.005, 0.01, 0.015]))  

def plot_mask_ranges(column, color):
    mask.vbt.ranges.plot_shapes(
        column=column,
        plot_close=False,
        add_shape_kwargs=dict(fillcolor=color),
        fig=fig
    )
plot_mask_ranges(0.005, "orangered")
plot_mask_ranges(0.010, "orange")
plot_mask_ranges(0.015, "yellow")
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
fig.show()
2020 至 2021 年 BTC-USD OHLC、50 天 SMA 及三个阈值下的陡峭斜率区间 图表数据(JSON)

参数

✅ 新增用于处理参数的模块。

Generate 10,000 random parameter combinations for MACD
from itertools import combinations

window_space = np.arange(100)
fastk_windows, slowk_windows = list(zip(*combinations(window_space, 2)))  
window_type_space = list(vbt.enums.WType)
param_product = vbt.combine_params(
    dict(
        fast_window=vbt.Param(fastk_windows, level=0),  
        slow_window=vbt.Param(slowk_windows, level=0),
        signal_window=vbt.Param(window_space, level=1),
        macd_wtype=vbt.Param(window_type_space, level=2),  
        signal_wtype=vbt.Param(window_type_space, level=2),
    ),
    random_subset=10_000,
    seed=42,
    build_index=False
)
pd.DataFrame(param_product)
      fast_window  slow_window  signal_window  macd_wtype  signal_wtype
0               0            1              0           2             2
1               0            3              5           1             1
2               0            3             55           4             4
3               0            3             80           2             2
4               0            4             17           2             2
...           ...          ...            ...         ...           ...
9995           97           99             14           0             0
9996           97           99             35           2             2
9997           98           99              6           2             2
9998           98           99             44           4             4
9999           98           99             78           4             4

[10000 rows x 5 columns]

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

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