功能

信号回测

将入场与离场信号转化为多头、空头、反转和金字塔加仓持仓

已有入场和离场信号,想知道它们如何转化为交易,以及相互冲突时如何处理?VBT 可通过向 vbt.PF.from_signals 传入布尔数组,在 Python 中回测买卖信号,并将它们转化为订单、持仓和可完整检查的投资组合。

信号可以来自技术指标、自定义 pandas 计算,或转化为入场与离场条件的模型预测。可以做多或做空、分批建仓,并将自定义信号与内置止损和限价单结合。

Backtest a moving average crossover on Bitcoin
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.053252
pf.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: object
pf.plot(settings=dict(bm_returns=False)).show()
2021 至 2023 年比特币价格、均线交叉入场与离场、交易收益和累计收益 图表数据(JSON)

多资产与参数组合

同一方法既适用于单个信号 Series,也适用于包含多个资产和参数组合的 DataFrame。在一次模拟中比较指标设置、仓位大小与冲突规则,再按列标签选择各个结果。

默认每列拥有独立现金。若要把信号作为多资产组合测试,应为资产列分组并启用现金共享。回测引擎展示两种方式,参数优化展示如何构建更大搜索。

从信号到订单

信号是请求,并非订单。每根 K 线上,模拟器读取信号、检查当前持仓,并将最终生效信号转化为带数量、价格和方向的订单。默认空仓时入场会开仓;已有同方向持仓时再次入场会被忽略;离场则全部平仓。

每个订单参数都与信号广播,因此可以是标量、单行、单列或完整数组。若每个信号使用不同值,只在信号位置填充数组:

Use a different size for each entry
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 和其他订单参数。例如,价格数组可让离场在自定义逻辑计算的价位成交:

Exit at computed prices and enter at the close
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" 会去掉离场,使策略始终在场:

Compare four direction-aware masks with two reversing masks
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 可一次测试多个:

Resolve an entry and an exit on the same bar
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.assets
upon_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 后,每个信号都视为订单,入场增加持仓,带数量的离场则减少持仓:

Add one unit on each entry signal
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.assets
accumulate  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.0

accumulate 还可设为 "addonly",只允许加仓、每次离场全部平仓;或设为 "removeonly",一次入场后分批减仓。使用 size_type="percent" 时,离场数量是当前持仓的比例,因此先减半再全部平仓需要两次离场:

Buy with all cash, sell half, then sell the rest
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 日均线时离场:

Pyramid into new highs with a three-unit cap
@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() 返回实际执行的四个信号掩码:

Derive signals from target positions
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 代码或大型优化循环:

Run the compiled signal simulator on NumPy arrays
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。

目标仓位转信号

✅ 可以使用专门信号函数,将目标仓位转化为信号,从而使用止损和限价单功能。例如,这对投资组合优化尤其有用。

Perform the Mean-Variance Optimization with SL and TP
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()
2022 年具有止损和止盈保护的月度均值方差投资组合配置 图表数据(JSON)

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

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