功能

多维研究

将资产、参数和策略作为带标签列运行,再对结果索引与堆叠

VBT 如何一次测试数千个策略变体,又如何浏览结果?每个资产、参数值和策略都成为带标签的列。输入相互广播,在 NumPy 数组上计算,再以 pandas 对象返回,可像普通 DataFrame 一样选择、分组和堆叠。

Run 3 assets × 4 fast windows × 4 slow windows as 48 columns
from itertools import product

data = vbt.YFData.pull(
    ["BTC-USD", "ETH-USD", "SOL-USD"],
    start="2022-01-01",
    end="2025-01-01",
)
fast_windows, slow_windows = zip(*product([5, 10, 15, 20], [30, 50, 70, 90]))
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_crossed_above(slow)
exits = fast.ma_crossed_below(slow)
entries.shape
(1096, 48)
entries.columns[:4]
MultiIndex([(5, 30, 'BTC-USD'),
            (5, 30, 'ETH-USD'),
            (5, 30, 'SOL-USD'),
            (5, 50, 'BTC-USD')],
           names=['fast_window', 'slow_window', 'symbol'])
pf = vbt.PF.from_signals(data, entries, exits, fees=0.001)  
pf.total_return.groupby("symbol").describe()[["mean", "min", "max"]].round(3)
          mean    min    max
symbol
BTC-USD  0.758  0.174  1.481
ETH-USD  0.051 -0.195  0.474
SOL-USD  1.394  0.104  4.285
print(round(pf[(10, 50, "ETH-USD")].total_return, 4))  
0.1389
pf.xs("ETH-USD", level="symbol", axis=1).wrapper.shape  
(1096, 16)

每列都是完整回测,拥有自己的订单、交易和度量;列标签携带全部参数,因此可用普通 pandas 汇总结果。

这样,同一实验可回答多个问题:哪些资产表现稳定、哪些参数范围经得起检验、不同成本假设会使结果变化多少。从输入到最终交易和绩效表,标签始终保留。

研究模型

VBT 对象由 NumPy 数组和封装器组成,封装器记录索引、列、频率与分组。编译代码在原始数组上计算,再按正确标签将结果封装回 pandas,无论结果是时间序列、每列一个值还是每组一个值。Series 和 DataFrame 使用相同代码,因此为单个资产编写的函数也适用于一千个资产。

市场数据的每个特征各有数组:收盘价一张表、最高价另一张表、入场信号又一张表。列标识交易品种和实验。VBT 数据对象可像开篇示例一样直接提供价格特征,也可从自己的数据流程传入独立数组。

广播

不同形状输入通过广播组合:标量应用于全部元素,Series 应用于每列,单行 DataFrame 应用于每根 K 线,完整 DataFrame 则逐元素应用。带标签列按层级匹配,因此上例三列价格可与共享 symbol 层级的 48 列信号对齐。小输入保持小规模:模拟内部将标量手续费作为标量读取,不会扩展成完整数组。

一维 NumPy 数组被视为时间序列,因此每根 K 线必须有一个值。逐列数值应放入 np.array([[0.001, 0.002, 0.003]]) 这样的单行数组,或带标签对象。行按日期匹配:日期不同的输入按全部日期连接,并以 NaN 填充,因此不同时间周期的数据应先重采样到同一索引,参见多时间周期分析。

普通 pandas 运算符严格匹配标签,因此不同参数层级的指标输出直接比较会失败。在左操作数前加 .vbt,改用 VBT 广播,详见交易信号。

匹配价格和信号列,即可每列运行一个回测;也可通过下方 DataFrame 笛卡尔积扩展实验,在比较组合时让每个品种始终对应自己的价格。

下方 DataFrame 笛卡尔积功能将一个 DataFrame 的每列与另一个的每列组合;索引字典则无需先构建数组,即可在选定日期和列设置值。

将假设作为带标签情景比较

参数不仅能描述指标,也能描述执行假设。这里用两条虚构价格序列,分别测试零手续费和 1% 手续费。各手续费成为一个额外列层级,因此无需从独立运行收集结果,就能将收益重塑为表格:

Compare two fee assumptions across two assets
close = pd.DataFrame(
    {"A": [100.0, 105.0, 110.0], "B": [100.0, 98.0, 95.0]},
    index=pd.date_range("2025-01-01", periods=3),
).rename_axis(columns="symbol")
scenarios = vbt.PF.from_holding(close, fees=vbt.Param([0.0, 0.01], name="fee"))
scenarios.total_return.unstack("symbol").round(4)
symbol       A       B
fee
0.00    0.1000 -0.0500
0.01    0.0891 -0.0594

这里是小数形式的总收益率,结束时持仓仍未平仓。手续费减少各回测入场时可以买入的数量。同一方法也能比较滑点或仓位假设。参数优化介绍更大的搜索、条件组合和随机抽样。

分组

列可为报告或模拟而分组。group_by 按层级、标签列表,或用 True 将全部列组合成组;cash_sharing=True 将一个组变成一个账户。事后设置分组只影响结果报告方式,因此只要各组列相邻,同一批回测即可按资产、参数值或整体读取。

开篇示例中,pandas 汇总按品种将各自 16 个收益分组,以比较参数选择。这与对投资组合本身分组不同,后者可报告多列共同持有的表现。共享资金投资组合参见投资组合记账,独立策略之间的配置参见多策略投资组合。

堆叠运行结果

独立运行可以合并为一个对象。按列堆叠将不同实验并排,例如在策略网格旁放上买入持有:

Put buy-and-hold next to the 48 crossover backtests
hold_pf = vbt.PF.from_holding(data)
both = vbt.PF.column_stack(
    pf,
    hold_pf,
    wrapper_kwargs=dict(keys=pd.Index(["crossover", "hold"], name="strategy")),
)
both.wrapper.shape
(1096, 51)
both.total_return["hold"].round(3)
BTC-USD    0.959
ETH-USD   -0.116
SOL-USD    0.060
Name: total_return, dtype: float64

按行堆叠将连续时期,例如前向滚动的各测试窗口,连接为一段历史。堆叠后的投资组合保留记录,并据此重新计算现金和净值。独立模拟的各窗口都从新资金开始:首个窗口的初始现金成为堆叠组合的初始现金,后续窗口的初始现金则在各自起点作为入金加入,因此收益率保持正确,但净值会跳变。combine_init_cash=True 改为将全部资金视为初始现金,使后续窗口的资金在早期闲置,稀释收益。若要让同一账户贯穿全部窗口,应从上一窗口最终状态继续,并以 chained=True 堆叠,参见实时模拟。下方按列堆叠和按行堆叠功能展示完整投资组合的两种方式。

这也让您可分段处理长历史,或围绕多组入场和离场事件模拟数据窗口。边界处保留预期现金与持仓状态,再按行堆叠结果分析。大批独立实验如何分批,参见并行执行与缓存。

索引

每个 VBT 对象都可像 pandas 一样索引:按列标签、用 .loc 和 .iloc 按日期与位置、按参数层级,或用 xs。按日期切片投资组合会得到该时期的新投资组合,无须重跑模拟,记录和度量均按窗口调整。设置器则反向使用同一方式:借助数据封装器,按日期、日内时间、标签或掩码赋值。下方切片与索引对齐功能展示两者。

检查订单、绘制净值或比较回撤前,先选择交易品种或参数组合。标签让您能从汇总表回到产生特定结果的回测。实验维度较多时,也可重命名、重排或删除索引层级,保持报告易读。

Pandas 访问器

导入 VBT 后,每个 Series、DataFrame 和 Index 都会增加 .vbt 访问器,以及 .vbt.returns、.vbt.signals 和 .vbt.ohlcv 等专门命名空间。数据仍是普通 pandas 对象,因此同一行中可混用 VBT 和 pandas 操作。访问器在运行时注册,部分编辑器无法自动补全,但 notebook 可以。vbt.df_acc(df) 和 vbt.sr_acc(sr) 以显式对象返回同一访问器,编辑器可以内省。

DataFrame 笛卡尔积

✅ 多个参数化指标可能产生形状与列不同的 DataFrame,而它们通常共享不应相互组合的列层级(如 "symbol"),使笛卡尔积构建变得复杂。现在可以通过新方法,分块交叉连接多个 DataFrame。

Enter when SMA goes above WMA, exit when EMA goes below WMA
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
sma = data.run("sma", timeperiod=[10, 20], unpack=True)
ema = data.run("ema", timeperiod=[30, 40], unpack=True)
wma = data.run("wma", timeperiod=[50, 60], unpack=True)
sma, ema, wma = sma.vbt.x(ema, wma)  
entries = sma.vbt.crossed_above(wma)
exits = ema.vbt.crossed_below(wma)

entries.columns
MultiIndex([(10, 30, 50, 'BTC-USD'),
            (10, 30, 50, 'ETH-USD'),
            (10, 30, 60, 'BTC-USD'),
            (10, 30, 60, 'ETH-USD'),
            (10, 40, 50, 'BTC-USD'),
            (10, 40, 50, 'ETH-USD'),
            (10, 40, 60, 'BTC-USD'),
            (10, 40, 60, 'ETH-USD'),
            (20, 30, 50, 'BTC-USD'),
            (20, 30, 50, 'ETH-USD'),
            (20, 30, 60, 'BTC-USD'),
            (20, 30, 60, 'ETH-USD'),
            (20, 40, 50, 'BTC-USD'),
            (20, 40, 50, 'ETH-USD'),
            (20, 40, 60, 'BTC-USD'),
            (20, 40, 60, 'ETH-USD')],
           names=['sma_timeperiod', 'ema_timeperiod', 'wma_timeperiod', 'symbol'])

索引字典

手动创建数组并用 Pandas 设置数据往往很繁琐。现在有新功能来帮忙!任意可广播参数都可变成索引字典,其中包含在数组何处设置值的指令,并自动填入。它知道应更新哪个轴,非必要时不创建完整数组,节省 RAM ❤️

1) Accumulate daily and exit on Sunday vs 2) accumulate weekly and exit on month end
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])
tile = pd.Index(["daily", "weekly"], name="strategy")  
pf = vbt.PF.from_orders(
    data.close,
    size=vbt.index_dict({  
        vbt.idx(
            vbt.pointidx(every="day"),
            vbt.colidx("daily", level="strategy")): 100,  
        vbt.idx(
            vbt.pointidx(every="sunday"),
            vbt.colidx("daily", level="strategy")): -np.inf,  
        vbt.idx(
            vbt.pointidx(every="monday"),
            vbt.colidx("weekly", level="strategy")): 100,
        vbt.idx(
            vbt.pointidx(every="monthend"),
            vbt.colidx("weekly", level="strategy")): -np.inf,
    }),
    size_type="value",
    direction="longonly",
    init_cash="auto",
    broadcast_kwargs=dict(tile=tile)
)
pf.sharpe_ratio
strategy  symbol
daily     BTC-USD    0.702259
          ETH-USD    0.782296
weekly    BTC-USD    0.838895
          ETH-USD    0.524215
Name: sharpe_ratio, dtype: float64

切片

✅ 与选列类似,每个 VBT 对象现在也能用与 Pandas 相同的机制切片行 🔪 无须重新模拟,即可轻松分析和绘制模拟数据的任意子集!

Analyze multiple date ranges of the same portfolio
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_holding(data, freq="d")

pf.sharpe_ratio
1.116727709477293
pf.loc[:"2020"].sharpe_ratio  
1.2699801554196481
pf.loc["2021": "2021"].sharpe_ratio  
0.9825161170278687
pf.loc["2022":].sharpe_ratio  
-1.0423271337174647

按列堆叠

✅ 同类型复杂 VBT 对象可以轻松按列堆叠。例如,将多个无关交易策略合并为一个投资组合分析。最终对象底层仍是单一的多维结构,甚至可以比多个独立合并对象处理得更快 🫁

Analyze two trading strategies separately and then jointly
def strategy1(data):
    fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
    slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    return vbt.PF.from_signals(
        data.close,
        entries,
        exits,
        size=100,
        size_type="value",
        init_cash="auto"
    )

def strategy2(data):
    bbands = vbt.BBANDS.run(data.close, window=14)
    entries = bbands.close_crossed_below(bbands.lower)
    exits = bbands.close_crossed_above(bbands.upper)
    return vbt.PF.from_signals(
        data.close,
        entries,
        exits,
        init_cash=200
    )

data1 = vbt.BinanceData.pull("BTCUSDT")
pf1 = strategy1(data1)  
pf1.sharpe_ratio
0.9100317671866922
data2 = vbt.BinanceData.pull("ETHUSDT")
pf2 = strategy2(data2)  
pf2.sharpe_ratio
-0.11596286232734827
pf_sep = vbt.PF.column_stack((pf1, pf2))  
pf_sep.sharpe_ratio
0    0.910032
1   -0.115963
Name: sharpe_ratio, dtype: float64
pf_join = vbt.PF.column_stack((pf1, pf2), group_by=True)  
pf_join.sharpe_ratio
0.42820898354646514

按行堆叠

✅ 同类型复杂 VBT 对象可以轻松按行堆叠。例如,为已有投资组合追加新数据,或将样本内投资组合与对应样本外投资组合连接起来 🧬

Analyze two date ranges separately and then jointly
def strategy(data, start=None, end=None):
    fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
    slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
    entries = fast_ma.ma_crossed_above(slow_ma)
    exits = fast_ma.ma_crossed_below(slow_ma)
    return vbt.PF.from_signals(
        data.close[start:end],
        entries[start:end],
        exits[start:end],
        size=100,
        size_type="value",
        init_cash="auto"
    )

data = vbt.BinanceData.pull("BTCUSDT")

pf_whole = strategy(data)  
pf_whole.sharpe_ratio
0.9100317671866922
pf_sub1 = strategy(data, end="2019-12-31")  
pf_sub1.sharpe_ratio
0.7810397448678937
pf_sub2 = strategy(data, start="2020-01-01")  
pf_sub2.sharpe_ratio
1.070339534746574
pf_join = vbt.PF.row_stack((pf_sub1, pf_sub2))  
pf_join.sharpe_ratio
0.9100317671866922

索引对齐

✅ 不再要求每个 Pandas 数组具有相同索引。只要数据类型一致,所有需要相互广播的数组,其索引都会自动对齐。

Predict ETH price with BTC price using linear regression
btc_data = vbt.YFData.pull("BTC-USD")
btc_data.wrapper.shape
(2817, 7)
eth_data = vbt.YFData.pull("ETH-USD")  
eth_data.wrapper.shape
(1668, 7)
ols = vbt.OLS.run(  
    btc_data.close,
    eth_data.close
)
ols.pred
Date
2014-09-17 00:00:00+00:00            NaN
2014-09-18 00:00:00+00:00            NaN
2014-09-19 00:00:00+00:00            NaN
2014-09-20 00:00:00+00:00            NaN
2014-09-21 00:00:00+00:00            NaN
...                                  ...
2022-05-30 00:00:00+00:00    2109.769242
2022-05-31 00:00:00+00:00    2028.856767
2022-06-01 00:00:00+00:00    1911.555689
2022-06-02 00:00:00+00:00    1930.169725
2022-06-03 00:00:00+00:00    1882.573170
Freq: D, Name: Close, Length: 2817, dtype: float64

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

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