功能
回测引擎
同时跨多个资产和参数模拟订单、信号与回调
能否在 Python 中回测自定义交易逻辑,同时保留速度和控制力?VBT 是一个 Python 回测引擎,用编译代码逐根模拟订单,将每个资产与参数组合放入同一次向量化模拟的不同列,并返回可检查每笔订单、交易和持仓的投资组合。可从入场与离场信号开始,回放已有订单列表,或构建在模拟期间响应现金与持仓的自定义逻辑。每种方式都提供相同的投资组合分析工具。
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2021-01-01", end="2024-01-01")
fast = vbt.MA.run(data.close, window=[10, 10, 20, 20], short_name="fast")
slow = vbt.MA.run(data.close, window=[50, 100, 50, 100], 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)
pf.total_return.unstack("symbol")symbol BTC-USD ETH-USD
fast_window slow_window
10 50 0.346309 0.720064
100 0.577382 -0.151935
20 50 -0.139554 0.017887
100 0.365233 -0.110708pf[(10, 50, "ETH-USD")].plot(settings=dict(bm_returns=False)).show() 每个信号在出现当根收盘成交,每笔订单收取 0.1% 手续费。若要在下一根执行,参见符合因果关系的订单时机。
三种模拟方式
每种模拟方法返回相同的 Portfolio 对象,差别在于如何描述交易决策:
| 方法 | 您提供 | 用于 |
|---|---|---|
vbt.PF.from_orders | 逐根数量、价格和方向 | 再平衡、目标权重、订单列表 |
vbt.PF.from_signals | 入场与离场信号,以及止损和限价 | 大多数规则策略 |
vbt.PF.from_order_func | 返回订单的编译函数 | 依赖投资组合状态的逻辑 |
from_orders 最直接:提供数量与价格,引擎处理现金和执行。若目标权重需结合止损与限价,from_signals 可通过 order_mode=True
接受,详见将目标作为信号。两种方法每列每根最多成交一个订单。若要多个,例如开盘买入、收盘卖出,可使用灵活订单函数,详见订单函数。
from_orders 和 from_signals 接受模拟前准备的数组,执行仍响应当前账户。例如,目标权重根据投资组合净值和已有持仓转化为订单数量。from_signals 和
from_order_func
还接受在模拟期间决策的回调,详见事件驱动回测。无论静态或动态,方法共享订单执行规则。这里,同一纯多头策略的两种表达得到相同订单:
close = data.close["BTC-USD"]
entries = close.vbt.crossed_above(close.rolling(50).mean())
exits = close.vbt.crossed_below(close.rolling(50).mean())
pf_signals = vbt.PF.from_signals(close, entries, exits)
size = pd.Series(np.nan, index=close.index)
size[entries] = np.inf
size[exits] = -np.inf
pf_orders = vbt.PF.from_orders(close, size, direction="longonly")
cols = ["Index", "Size", "Price", "Side"]
a = pf_signals.orders.readable.rename(columns={"Fill Index": "Index"})[cols]
b = pf_orders.orders.readable[cols]
a.equals(b), len(a)(True, 51)高层方法之下是接受普通 NumPy 数组的编译函数,可从自己的 Numba 代码调用,详见信号回测。
一次调用运行多个回测
输入相互广播。八列信号、两列价格和标量手续费产生八个回测,参数值成为可选择、分组和绘图的列层级。默认每列为独立账户。
分组可改变这一点。group_by 将多列合并为报告中的一个组合,cash_sharing=True
使它们使用同一现金余额,用于模拟多资产组合。按参数层级对首个网格分组,会将八个单资产回测转为四个双资产组合:
pf = vbt.PF.from_signals(
data,
fast.ma_crossed_above(slow),
fast.ma_crossed_below(slow),
size=0.5,
size_type="valuepercent",
fees=0.001,
group_by=["fast_window", "slow_window"],
cash_sharing=True,
)
pf.total_returnfast_window slow_window
10 50 0.570373
100 0.186409
20 50 -0.055170
100 0.133235
Name: total_return, dtype: float64模拟器在组内逐根延续现金与持仓。速度来自编译循环,以及多个独立组并排运行。可用并行执行与缓存页的选项扩展参数扫描。
模拟窗口
指标在首个信号之前需要历史数据。无需切片数据,传入 sim_start 和 sim_end 即可:指标看到完整历史,模拟则在指定根从空仓开始,并在结束根之前停止。
pf = vbt.PF.from_signals(close, entries, exits, sim_start="2022-01-01")
pf.stats(["start_index", "end_index", "total_return", "total_trades"])Start Index 2022-01-01 00:00:00+00:00
End Index 2023-12-31 00:00:00+00:00
Total Return [%] 25.991572
Total Trades 18
Name: BTC-USD, dtype: object窗口可逐列不同,sim_start="auto"
从各列首个信号开始,回调也可提前结束一组。度量、记录与图表都遵循窗口,详见下方模拟区间。也可在已有投资组合上分析不同日期区间,无须重跑回测。
以记录作为输入
可直接从时间戳与资产列匹配的 pandas Series 或 DataFrame 获取价格和信号。可使用已有数据流程或 VBT 的市场数据集成。
某些策略是一份事件列表:在某日期买入某品种。利用数据封装器容易构建稠密数组,例如先 data.symbol_wrapper.fill(False),再用 .loc
赋值。也可完全省略数组,直接以记录传入事件,见下方索引记录。
记录还可将券商、电子表格或其他回测器的成交日志转为投资组合。日志各列对应模拟方法的一个参数:
data = vbt.YFData.pull(["AAPL", "NVDA"], start="2024-01-01", end="2025-01-01")
fills = pd.DataFrame(
[
("2024-02-05", "AAPL", 50, 188.20, 1.0),
("2024-03-11", "NVDA", 100, 86.40, 1.0),
("2024-04-22", "AAPL", -50, 166.90, 1.0),
("2024-06-18", "NVDA", -60, 131.70, 1.0),
("2024-08-05", "NVDA", 60, 101.20, 1.0),
("2024-11-25", "NVDA", -100, 137.40, 1.0),
],
columns=["date", "symbol", "size", "price", "fixed_fees"],
)
pf = vbt.PF.from_orders(
data,
records=fills,
init_cash=20_000,
group_by=True,
cash_sharing=True,
)
pf.trades.readable[["Column", "Size", "Avg Entry Price", "Avg Exit Price", "PnL", "Return"]] Column Size Avg Entry Price Avg Exit Price PnL Return
0 AAPL 50.0 188.20 166.9 -1067.0 -0.113390
1 NVDA 60.0 86.40 131.7 2716.4 0.523997
2 NVDA 100.0 95.28 137.4 4209.6 0.441814pf.total_returnnp.float64(0.29294999999999993)NVDA 部分卖出将 60 股作为一笔交易平仓,余下 40 股与八月买入合并,平均价为 95.28。日志成为组合后,可检查度量与图表,或用不同手续费和离场日期重放订单,探索假设情景。
落在 K 线内部的时间戳会映射到数据中的下一根,因此应先将日内成交时间向下取整至 K 线起点,例如日线用
fills["date"].dt.floor("1D")。同一品种同根的后续记录覆盖前一条,因此传入前应合并同根成交。
运行前检查
模拟前,预处理器转换参数:字符串变枚举、输入广播至最终形状、模板填充、默认值应用。传入 return_prep_result=True
可取得该结果而非投资组合,检查或修改参数后再运行。下方投资组合预处理器逐步展示。
编译一次,快速重复运行
研究意味着反复运行同一模拟。以下设置有助于保持速度:
- 只编译一次。 带回调的模拟器在每个新 Python 会话中编译。
staticized=True让 VBT 为回调写出可缓存模拟器副本,后续会话跳过编译。回调源码、模拟设置或 VBT 版本改变时,副本自动重新生成。不会检测回调导入的其他模块改动,修改它们后应传入staticized=dict(override=True)。无回调模拟使用已缓存模拟器,因此每次安装只编译一次。 - 保存将要读取的内容。 默认模拟存储订单,在请求时重建余额和收益。
save_returns=True及相关标志改为模拟期间计算,以内存换速度。 - 注意内存。 每个完整
float64数组占行数 × 列数 × 8 字节。20 年加密货币日线、10,000 列,每个数组约 580 MB,模拟会保留多个。大网格应分块,参见并行执行与缓存。
以属性读取结果
每项结果都是投资组合属性。pf.sharpe_ratio 按默认设置返回并缓存,pf.get_sharpe_ratio(risk_free=0.02)
则接受参数。属性也可由您提供的数组计算,因此无需重跑模拟,就能测试不同基准或覆盖某个序列。
添加自定义度量或行为时,可继承 vbt.Portfolio。from_signals 等类方法返回子类实例。若需重新标记结果,可用
pf.replace(wrapper=dict(columns=...), nested_=True) 创建新组合。
文档
会员专享的投资组合文档(英文)介绍从订单执行到记录与封装的模拟生命周期。
✅ 参数传给 Portfolio.from_signals 等模拟方法时,会经过复杂预处理流程,转换为适合 Numba 的格式。通常包括枚举映射、广播、数据类型检查、模板替换等步骤。为使 VBT
更透明,该流程已移到独立类,让您完整控制到达 Numba 函数的参数!甚至可扩展预处理器,为自己的模拟器自动准备参数。
data = vbt.YFData.pull("BTC-USD", end="2017-01")
prep_result = vbt.PF.from_holding(
data,
stop_ladder="uniform",
tp_stop=vbt.Param([
[0.1, 0.2, 0.3, 0.4, 0.5],
[0.4, 0.5, 0.6],
], keys=["tp_ladder_1", "tp_ladder_2"]),
return_prep_result=True
)
prep_result.target_args["tp_stop"] array([[0.1, 0.4],
[0.2, 0.5],
[0.3, 0.6],
[0.4, nan],
[0.5, nan]])new_tp_stop = prep_result.target_args["tp_stop"] + 0.1
new_prep_result = prep_result.replace(target_args=dict(tp_stop=new_tp_stop), nested_=True)
new_prep_result.target_args["tp_stop"]array([[0.2, 0.5],
[0.3, 0.6],
[0.4, 0.7],
[0.5, nan],
[0.6, nan]])pf = vbt.PF.from_signals(new_prep_result)
pf.total_returntp_stop
tp_ladder_1 0.4
tp_ladder_2 0.6
Name: total_return, dtype: float64sim_out = new_prep_result.target_func(**new_prep_result.target_args)
pf = vbt.PF(sim_out=sim_out, **new_prep_result.pf_args)
pf.total_returntp_stop
tp_ladder_1 0.4
tp_ladder_2 0.6
Name: total_return, dtype: float64✅ Numba 的主要限制之一是:作为参数传入的函数(即回调)会使主函数无法缓存,每次新运行时都必须反复编译。这尤其影响模拟器,因为编译可能长达一分钟。现在可以使用“静态化”技巧:先用特殊语法标记函数源码,再提取(也称“裁切”✂️)代码,将回调从参数中移除,修改为可缓存版本并保存到 Python 文件。下次调用执行可缓存版本。听起来复杂?看下方示例!
from vectorbtpro import *
@njit
def signal_func_nb(ctx, fast_sma, slow_sma):
long = vbt.pf_nb.iter_crossed_above_nb(ctx, fast_sma, slow_sma)
short = vbt.pf_nb.iter_crossed_below_nb(ctx, fast_sma, slow_sma)
return long, False, short, Falsedata = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_signals(
data,
signal_func_nb="signal_func_nb.py",
signal_args=(vbt.Rep("fast_sma"), vbt.Rep("slow_sma")),
broadcast_named_args=dict(
fast_sma=data.run("sma", 20, hide_params=True, unpack=True),
slow_sma=data.run("sma", 50, hide_params=True, unpack=True)
),
staticized=True
)✅ 内存和执行速度之间存在取舍:1000 列数据通常比单列数据处理 1000 次快得多,但也需要 1000 倍内存。因此,模拟阶段 VBT 主要生成订单,余额、净值与收益等其他属性在分析阶段按需重建。性能最重要时,现在可通过参数在模拟期间预先计算这些属性!⏩
data = vbt.YFData.pull("BTC-USD")%%timeit
for n in range(1000):
pf = vbt.PF.from_random_signals(data, n=n, seed=42, save_returns=False)
pf.sharpe_ratio15 s ± 829 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)%%timeit
pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=False)
pf.sharpe_ratio855 ms ± 6.26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)%%timeit
pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=True)
pf.sharpe_ratio593 ms ± 7.07 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)✅ 投资组合属性现在可部分甚至全部由用户数组计算。模拟后分析更加可控,例如覆盖部分模拟数据、不重跑完整组合即可测试超参数,或在关闭缓存时避免重复重建。
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_random_signals(data.close, n=100, seed=42)
value = pf.get_value()
long_exposure = vbt.PF.get_gross_exposure(
asset_value=pf.get_asset_value(direction="longonly"),
value=value,
wrapper=pf.wrapper
)
short_exposure = vbt.PF.get_gross_exposure(
asset_value=pf.get_asset_value(direction="shortonly"),
value=value,
wrapper=pf.wrapper
)
del value
net_exposure = vbt.PF.get_net_exposure(
long_exposure=long_exposure,
short_exposure=short_exposure,
wrapper=pf.wrapper
)
del long_exposure
del short_exposure
net_exposureDate
2014-09-17 00:00:00+00:00 1.0
2014-09-18 00:00:00+00:00 1.0
2014-09-19 00:00:00+00:00 1.0
2014-09-20 00:00:00+00:00 1.0
2014-09-21 00:00:00+00:00 1.0
...
2023-02-08 00:00:00+00:00 0.0
2023-02-09 00:00:00+00:00 0.0
2023-02-10 00:00:00+00:00 0.0
2023-02-11 00:00:00+00:00 0.0
2023-02-12 00:00:00+00:00 0.0
Freq: D, Length: 3071, dtype: float64✅ 原地输出数组可覆盖普通投资组合属性。组合自动采用预计算数组,之后所有计算都使用它,避免重复重建。
data = vbt.YFData.pull("BTC-USD")
size = data.symbol_wrapper.fill(np.nan)
np.random.seed(42)
rand_indices = np.random.choice(np.arange(len(size)), 10)
size.iloc[rand_indices[0::2]] = -np.inf
size.iloc[rand_indices[1::2]] = np.inf
@njit
def post_segment_func_nb(ctx):
for col in range(ctx.from_col, ctx.to_col):
return_now = ctx.last_return[col]
return_now = 0.5 * return_now if return_now > 0 else return_now
ctx.in_outputs.returns[ctx.i, col] = return_now
pf = vbt.PF.from_def_order_func(
data.close,
size=size,
size_type="targetpercent",
post_segment_func_nb=post_segment_func_nb,
in_outputs=dict(
returns=vbt.RepEval("np.empty_like(close)")
)
)
pf.returns Date
2014-09-17 00:00:00+00:00 0.000000
2014-09-18 00:00:00+00:00 0.000000
2014-09-19 00:00:00+00:00 0.000000
2014-09-20 00:00:00+00:00 0.000000
2014-09-21 00:00:00+00:00 0.000000
...
2023-02-08 00:00:00+00:00 -0.015227
2023-02-09 00:00:00+00:00 -0.053320
2023-02-10 00:00:00+00:00 -0.008439
2023-02-11 00:00:00+00:00 0.005569 << modified
2023-02-12 00:00:00+00:00 0.001849 << modified
Freq: D, Length: 3071, dtype: float64pf.get_returns() Date
2014-09-17 00:00:00+00:00 0.000000
2014-09-18 00:00:00+00:00 0.000000
2014-09-19 00:00:00+00:00 0.000000
2014-09-20 00:00:00+00:00 0.000000
2014-09-21 00:00:00+00:00 0.000000
...
2023-02-08 00:00:00+00:00 -0.015227
2023-02-09 00:00:00+00:00 -0.053320
2023-02-10 00:00:00+00:00 -0.008439
2023-02-11 00:00:00+00:00 0.011138
2023-02-12 00:00:00+00:00 0.003697
Freq: D, Length: 3071, dtype: float64✅ 每次模拟都有起止点,通常与数据首末行一致。可事先设置不同区间,现在也能在模拟期间调整,无需继续处理时即可停止。日期区间保存在组合对象中,所有度量和子图都能识别。只处理相关日期可加快执行,并为分析增添独立时间窗口这一维度 🔬
@njit
def post_segment_func_nb(ctx):
value = vbt.pf_nb.get_group_value_nb(ctx, ctx.group)
if value <= 0:
vbt.pf_nb.stop_group_sim_nb(ctx, ctx.group)
pf = vbt.PF.from_random_signals(
"BTC-USD",
n=10,
seed=42,
sim_start="auto",
post_segment_func_nb=post_segment_func_nb,
leverage=10,
)
pf.plot_value() ✅ 如何回测“日期 Z 买入资产 Y 的 X 单位”等绑定时间与资产的指令?通常需要构建完整数组并手动设置每个细节。借助预处理器和重新设计的智能索引,现在可以用压缩记录格式提供全部信息!底层将记录数组转化为一组索引字典,每个参数一个。
data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
records = [
dict(date="2022", symbol="BTC-USD", long_entry=True),
dict(date="2022", symbol="ETH-USD", short_entry=True),
dict(row=-1, exit=True),
]
pf = vbt.PF.from_signals(data, records=records)
pf.orders.readable Order Id Column Signal Index Creation Index
0 0 BTC-USD 2022-01-01 00:00:00+00:00 2022-01-01 00:00:00+00:00 \
1 1 BTC-USD 2023-04-25 00:00:00+00:00 2023-04-25 00:00:00+00:00
2 0 ETH-USD 2022-01-01 00:00:00+00:00 2022-01-01 00:00:00+00:00
3 1 ETH-USD 2023-04-25 00:00:00+00:00 2023-04-25 00:00:00+00:00
Fill Index Size Price Fees Side Type
0 2022-01-01 00:00:00+00:00 0.002097 47686.812500 0.0 Buy Market \
1 2023-04-25 00:00:00+00:00 0.002097 27534.675781 0.0 Sell Market
2 2022-01-01 00:00:00+00:00 0.026527 3769.697021 0.0 Sell Market
3 2023-04-25 00:00:00+00:00 0.026527 1834.759644 0.0 Buy Market
Stop Type
0 None
1 None
2 None
3 None版权所有 © 2021–2026 Oleg Polakow。保留所有权利。
本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。