功能
参数优化
使用网格、条件和随机搜索遍历数百万种参数组合
哪些参数有效,如何在时间和内存限制内测试数百万种组合?VBT 通过一个装饰器将任意函数转为参数扫描,按条件构建网格,并可将编译函数分块运行于全部核心。从现有 Python 策略开始,从少量参数值扩展到数百万种组合。
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) 1000000sharpe.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: float64best_per_pair = sharpe.groupby(["fast_window", "slow_window"]).max()
best_per_pair.vbt.heatmap(x_level="fast_window", y_level="slow_window").show()每次回测仅保留一个数值,因此一百万次回测只需几兆字节。热图显示每组窗口在所有止损水平中的最佳夏普比率。在该牛市中,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 线,每次回测均包含手续费。
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.0results.index[results["Closed trades"] >= 3].tolist()[2, 3]最后一行选择至少三笔已平仓交易的窗口。您可按自己的标准修改筛选,或在返回的 Series 中添加绩效指标。一张结果表提供每种设置的收益、回撤和交易次数,可直接用于自己的选择规则。
✅ 每个可分析 VBT 对象(例如数据、指标或投资组合)现在都可拆成多个相同类型的小对象,每个仅包含一列或一个分组。这让 VBT 对象可作为独立参数,每次只处理部分信息,例如数据实例中的一个标的,或指标中的一种参数组合。
@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.columnsMultiIndex([(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)✅ 如果只关注所有参数组合的一个子集,参数化装饰器不再需要将参数网格实体化。无论全部潜在组合有多大,都几乎可以立即生成随机参数组合。
@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)89730269910test_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✅ 参数化装饰器现在支持将参数组合拆成“单体分块”,将每块中的参数值合并为单个值,并通过一次函数调用运行整块。这意味着不再局限于一次仅处理一种参数组合 🌪️ 请注意,函数必须调整为可处理多个参数值,合并函数也应按需修改。
@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()✅ 参数可以相互依赖。例如,测试均线交叉时,快速窗口长于慢速窗口没有意义。过滤这类情况后,只需评估大约一半的参数组合。
@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.columnsMultiIndex([( 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)✅ 网格搜索测试每种可能的超参数组合,而随机搜索选择并测试随机组合。这在参数组合数量巨大时尤其有用。研究也表明,随机搜索可以更少函数评估找到与网格搜索相当或更好的值。指标工厂、参数化装饰器和所有执行广播的方法,现在都内置支持随机搜索。
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 函数接受多种参数组合,即使函数本身只支持一种。装饰器封装函数并访问实参,识别充当参数的全部实参,构建网格,再对网格中每个组合调用底层函数。执行可以轻松并行化。全部输出就绪后,合并为单个对象。用途无穷:从运行无法通过指标工厂封装的指标,到参数化整个流水线!🪄
@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)))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 中许多参数,例如止损和止盈,都是类数组,可逐行、逐列甚至逐元素提供。内部连标量也视为普通时间序列,与其他真正的时间序列一起广播。此前,为测试多种参数组合,必须将其他时间序列平铺,使形状完全一致。现在平铺过程会自动完成!
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()✅ 新增用于处理参数的模块。
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 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。