功能
信号回测
将入场与离场信号转化为多头、空头、反转和金字塔加仓持仓
已有入场和离场信号,想知道它们如何转化为交易,以及相互冲突时如何处理?VBT 可通过向 vbt.PF.from_signals 传入布尔数组,在 Python
中回测买卖信号,并将它们转化为订单、持仓和可完整检查的投资组合。
信号可以来自技术指标、自定义 pandas 计算,或转化为入场与离场条件的模型预测。可以做多或做空、分批建仓,并将自定义信号与内置止损和限价单结合。
data = vbt.YFData.pull("BTC-USD", start="2021-01-01", end="2024-01-01")
fast = data.run("talib:sma", timeperiod=20).real
slow = data.run("talib:sma", timeperiod=50).real
entries = fast.vbt.crossed_above(slow)
exits = fast.vbt.crossed_below(slow)
pf = vbt.PF.from_signals(data, entries, exits, price="nextopen", fees=0.001)
pf.trades.readable[["Entry Index", "Exit Index", "Return"]].head(3) Entry Index Exit Index Return
0 2021-08-02 00:00:00+00:00 2021-09-24 00:00:00+00:00 0.122841
1 2021-10-12 00:00:00+00:00 2021-11-28 00:00:00+00:00 -0.049127
2 2022-02-20 00:00:00+00:00 2022-03-08 00:00:00+00:00 -0.053252pf.stats(["total_return", "total_trades", "win_rate", "max_dd"])Total Return [%] -13.87894
Total Trades 12
Win Rate [%] 18.181818
Max Drawdown [%] 58.6206
dtype: objectpf.plot(settings=dict(bm_returns=False)).show()多资产与参数组合
同一方法既适用于单个信号 Series,也适用于包含多个资产和参数组合的 DataFrame。在一次模拟中比较指标设置、仓位大小与冲突规则,再按列标签选择各个结果。
默认每列拥有独立现金。若要把信号作为多资产组合测试,应为资产列分组并启用现金共享。回测引擎展示两种方式,参数优化展示如何构建更大搜索。
从信号到订单
信号是请求,并非订单。每根 K 线上,模拟器读取信号、检查当前持仓,并将最终生效信号转化为带数量、价格和方向的订单。默认空仓时入场会开仓;已有同方向持仓时再次入场会被忽略;离场则全部平仓。
每个订单参数都与信号广播,因此可以是标量、单行、单列或完整数组。若每个信号使用不同值,只在信号位置填充数组:
close = pd.Series(
[10.0, 11.0, 12.0, 11.0, 10.0, 12.0],
index=pd.date_range("2026-01-01", periods=6)
)
entries = pd.Series([True, False, False, False, True, False], index=close.index)
exits = pd.Series([False, False, True, False, False, True], index=close.index)
size = close.vbt.wrapper.fill(np.nan)
size[entries] = [5, 10]
pf = vbt.PF.from_signals(close, entries, exits, size=size, init_cash=1000)
pf.orders.readable[["Fill Index", "Size", "Side"]] Fill Index Size Side
0 2026-01-01 5.0 Buy
1 2026-01-03 5.0 Sell
2 2026-01-05 10.0 Buy
3 2026-01-06 10.0 Sell未指定数量时,每次入场使用全部可用现金。"targetpercent" 等目标持仓数量类型会被拒绝,因为目标可能与信号方向矛盾。若按投资组合净值比例确定每次入场规模,使用
"valuepercent",或按将目标作为信号的方法传入目标。
逐信号设置方式同样适用于 price、order_type、fees 和其他订单参数。例如,价格数组可让离场在自定义逻辑计算的价位成交:
price = close.copy()
price[exits] = [11.8, 11.7]
pf = vbt.PF.from_signals(close, entries, exits, price=price)
pf.orders.readable[["Fill Index", "Size", "Price", "Side"]] Fill Index Size Price Side
0 2026-01-01 10.0 10.0 Buy
1 2026-01-03 10.0 11.8 Sell
2 2026-01-05 11.8 10.0 Buy
3 2026-01-06 11.8 11.7 Sell价格数组中,-np.inf 表示本根开盘,np.inf 表示本根收盘。在填满 np.inf 的数组上设置 price[entries] = -np.inf,即可开盘入场、收盘离场。
执行时机
默认订单在信号出现的 K 线收盘成交。若信号正由同一收盘价计算,应使用 price="nextopen" 或
price="nextclose",或将信号后移一根,避免回测以当时来不及知晓的价格交易。
止损与限价单
入场信号可配合止损、移动止损、止盈目标和时间离场。模拟器同时跟踪这些规则和您的信号,因此不必自行编写持仓跟踪逻辑,就能测试离场规则。不同资产和参数组合可设置不同止损水平。参见止损与止盈。
信号也可创建等待指定价格、而非立即成交的限价单。入场和离场可使用不同订单类型,并可设定未成交限价单保持有效的时长。限价、到期和成交行为详见订单与执行。
多头、空头与反转
有两种方式描述方向。使用 entries 和 exits 两个掩码时,direction 决定它们的含义。默认 "longonly","shortonly"
反转方向,"both" 使每次离场反转为空头。使用 long_entries、long_exits、short_entries 和 short_exits
四个掩码时,信号自身指定方向,因此交易之间也能空仓,direction 被忽略。
这种区别很重要。下方布林带策略在下轨以下买入、上轨以上做空、中轨离场。将其合并为两个掩码并设置 direction="both" 会去掉离场,使策略始终在场:
data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")
bb = data.run("bbands", window=20, alpha=2)
long_entries = data.close.vbt.crossed_below(bb.lower)
long_exits = data.close.vbt.crossed_above(bb.middle)
short_entries = data.close.vbt.crossed_above(bb.upper)
short_exits = data.close.vbt.crossed_below(bb.middle)
pf_four = vbt.PF.from_signals(
data,
long_entries=long_entries,
long_exits=long_exits,
short_entries=short_entries,
short_exits=short_exits,
)
pf_two = vbt.PF.from_signals(
data,
entries=long_entries,
exits=short_entries,
direction="both"
)
metrics = ["total_trades", "total_time_exposure", "total_return"]
pd.DataFrame({
"four masks": pf_four.stats(metrics),
"two masks": pf_two.stats(metrics),
}) four masks two masks
Total Trades 15 6
Position Coverage [%] 44.931507 89.315068
Total Return [%] 14.886316 -40.748144使用四个掩码时,15 笔交易分为 6 笔多头和 9 笔空头,策略超过半年持有现金。整列由同一个方向规则控制时可用两个掩码;需要平仓而不反转时,应使用四个。
信号冲突
不同规则生成的信号常在同一根触发。模拟器按固定顺序解决冲突,每一步各有参数:
| 冲突 | 参数 | 默认值 |
|---|---|---|
| 同方向入场与离场同时出现 | upon_long_conflict、upon_short_conflict | "ignore" |
| 多头入场与空头入场同时出现 | upon_dir_conflict | "ignore" |
| 与当前持仓相反方向的入场 | upon_opposite_entry | "reversereduce" |
同方向冲突可选择 "entry"、"exit",或与当前持仓 "adjacent"(同侧)或 "opposite"(相反)的信号。方向冲突可选择
"long"、"short"、"adjacent" 或 "opposite"。反向入场可以忽略、平仓或反转。与其他参数一样,各模式可逐根逐列变化,vbt.Param
可一次测试多个:
close = pd.Series(
[10.0, 11.0, 12.0, 11.0],
index=pd.date_range("2026-01-01", periods=4)
)
entries = pd.Series([True, False, True, False], index=close.index)
exits = pd.Series([False, False, True, False], index=close.index)
pf = vbt.PF.from_signals(
close,
entries,
exits,
upon_long_conflict=vbt.Param(["ignore", "exit"]),
size=1,
)
pf.assetsupon_long_conflict ignore exit
2026-01-01 1.0 1.0
2026-01-02 1.0 1.0
2026-01-03 1.0 0.0
2026-01-04 1.0 0.0反转只有一个订单
反转在同一订单中平掉旧仓并开立新仓,因此两部分共享一个价格和订单类型。若要用市价平仓、用限价重开,应在一根 K 线上发出离场,下一根发出反向入场,并按信号设置
order_type。交易记录仍显示两笔交易,一笔平仓、一笔开仓;小于两部分合计数量的 max_size 可能阻止反转。
加减仓与阶梯入场
未启用加减仓时,模拟器每列最多保留一个持仓,并忽略重复入场。设置 accumulate=True 后,每个信号都视为订单,入场增加持仓,带数量的离场则减少持仓:
close = pd.Series(
[10.0, 11.0, 12.0, 13.0, 12.0, 14.0],
index=pd.date_range("2026-01-01", periods=6)
)
entries = pd.Series([True, True, True, True, False, False], index=close.index)
exits = pd.Series([False, False, False, False, False, True], index=close.index)
pf = vbt.PF.from_signals(
close,
entries,
exits,
size=1,
accumulate=vbt.Param([False, True])
)
pf.assetsaccumulate False True
2026-01-01 1.0 1.0
2026-01-02 1.0 2.0
2026-01-03 1.0 3.0
2026-01-04 1.0 4.0
2026-01-05 1.0 4.0
2026-01-06 0.0 3.0accumulate 还可设为 "addonly",只允许加仓、每次离场全部平仓;或设为 "removeonly",一次入场后分批减仓。使用 size_type="percent"
时,离场数量是当前持仓的比例,因此先减半再全部平仓需要两次离场:
close = pd.Series(
[10.0, 11.0, 12.0, 11.5, 13.0, 12.0],
index=pd.date_range("2026-01-01", periods=6)
)
entries = pd.Series([True, False, False, False, False, False], index=close.index)
exits = pd.Series([False, False, True, False, True, False], index=close.index)
size = close.vbt.wrapper.fill(np.nan)
size[entries] = 1.0
size[exits] = [0.5, 1.0]
pf = vbt.PF.from_signals(
close,
entries,
exits,
size=size,
size_type="percent",
accumulate="removeonly",
init_cash=1000,
)
pf.orders.readable[["Fill Index", "Size", "Price", "Side"]] Fill Index Size Price Side
0 2026-01-01 100.0 10.0 Buy
1 2026-01-03 50.0 12.0 Sell
2 2026-01-05 50.0 13.0 Sell带上限的金字塔加仓依赖已有持仓,不能预先计算为掩码。Numba 编写的信号函数能逐根读取模拟状态。这里每次创 20 日新高加一单位,最多三单位,跌破 20 日均线时离场:
@njit
def pyramid_signal_nb(c, new_high, exits, max_units):
position = c.last_position[c.col]
if position > 0 and vbt.pf_nb.select_nb(c, exits):
return False, True, False, False
if position < max_units and vbt.pf_nb.select_nb(c, new_high):
return True, False, False, False
return False, False, False, False
new_high = data.close >= data.close.rolling(20).max()
exits = data.close.vbt.crossed_below(data.close.rolling(20).mean())
pf = vbt.PF.from_signals(
data,
signal_func_nb=pyramid_signal_nb,
signal_args=(vbt.Rep("new_high"), vbt.Rep("exits"), 3),
broadcast_named_args=dict(new_high=new_high, exits=exits),
size=1,
init_cash=100_000,
accumulate="addonly",
)
pf.orders.readable[["Fill Index", "Size", "Price", "Side"]].head(4) Fill Index Size Price Side
0 2023-01-20 00:00:00+00:00 1.0 22676.552734 Buy
1 2023-01-21 00:00:00+00:00 1.0 22777.625000 Buy
2 2023-01-23 00:00:00+00:00 1.0 22934.431641 Buy
3 2023-02-06 00:00:00+00:00 3.0 22760.109375 Sell同一技术可构建阶梯入场和定投计划。信号函数记录首次入场,随后经过指定 K 线数量或到达指定价位时,释放分阶段入场,各自拥有数量。
也可以在盈利持仓达到收益阈值后加仓。信号回调可读取未平仓头寸的收益,在启用加减仓时发出新的入场。逻辑可用的实时持仓度量详见持仓信息。
文档
会员专享的从信号构建投资组合(英文)指南,逐步构建参数化阶梯入场。
将目标作为信号
有时您知道想持有多少,而非具体信号。设置 order_mode=True 后,数量数组作为订单列表读取,模拟器推导匹配信号,使止损和限价功能仍可应用。pf.get_signals()
返回实际执行的四个信号掩码:
close = pd.Series(
[10.0, 11.0, 12.0, 11.0, 13.0],
index=pd.date_range("2026-01-01", periods=5)
)
target = pd.Series([5, np.nan, 10, 0, -5], index=close.index)
pf = vbt.PF.from_signals(
close,
size=target,
size_type="targetamount",
order_mode=True,
accumulate=True,
direction="both",
init_cash=1000,
)
long_entries, long_exits, short_entries, short_exits = pf.get_signals()
pd.DataFrame({
"long entry": long_entries,
"long exit": long_exits,
"short entry": short_entries,
}) long entry long exit short entry
2026-01-01 True False False
2026-01-02 False False False
2026-01-03 True False False
2026-01-04 False True False
2026-01-05 False False True直接调用内核
vbt.PF.from_signals 准备参数、广播并封装结果。底层引擎是接收普通 NumPy 数组的编译函数,可直接调用,例如放入自己的 Numba 代码或大型优化循环:
close_arr = np.array([[10.0], [11.0], [12.0], [11.0], [13.0]])
long_entries = np.array([[True], [False], [False], [True], [False]])
long_exits = np.array([[False], [False], [True], [False], [True]])
sim_out = vbt.pf_nb.from_signals_nb(
target_shape=close_arr.shape,
group_lens=np.array([1]),
close=close_arr,
init_cash=100.0,
long_entries=long_entries,
long_exits=long_exits,
)
pd.DataFrame(sim_out.order_records)[["idx", "size", "price"]] idx size price
0 0 10.000000 10.0
1 2 10.000000 12.0
2 3 10.909091 11.0
3 4 10.909091 13.0此层级需要自行准备参数:字符串选项转为枚举整数,数组必须已具有目标形状或能广播至该形状。回调版本 from_signal_func_nb 接受与 from_signals
相同的信号函数,交易记录与度量也可用编译函数从订单记录重建。
教程
会员专享的从 Python 到 Rust(英文)教程将一个策略从
vbt.PF.from_signals 下沉到 Numba 内核,再迁移至 Rust。
✅ 可以使用专门信号函数,将目标仓位转化为信号,从而使用止损和限价单功能。例如,这对投资组合优化尤其有用。
data = vbt.YFData.pull(
["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
start="2022",
end="2023",
missing_index="drop"
)
pfo = vbt.PFO.from_riskfolio(data.returns, every="M")
pf = pfo.simulate(
data,
pf_method="from_signals",
sl_stop=0.05,
tp_stop=0.1,
stop_exit_price="close"
)
pf.plot_allocations().show()版权所有 © 2021–2026 Oleg Polakow。保留所有权利。
本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。