功能

订单与执行

模拟限价与止损入场订单、成交价格、时机、拒单及实际成交

回测究竟如何使订单成交,能否模拟等待指定价格的订单?VBT 以明确的价格、时机、到期和拒单规则模拟市价、限价与止损入场订单,让您在 Python 中回测限价单,并了解每笔为何成交或未成交。

Buy a breakout with a stop-entry order that expires after five bars
data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")
sma = data.close.rolling(50).mean()
setup = data.close.vbt.crossed_above(sma)

order_type = data.symbol_wrapper.fill("market")
order_type[setup] = "limit"  
price = data.symbol_wrapper.fill(np.inf)
price[setup] = data.high.rolling(10).max()[setup]  

breakout_pf = vbt.PF.from_signals(
    data,
    entries=setup,
    exits=data.close.vbt.crossed_below(sma),
    order_type=order_type,
    price=price,
    limit_reverse=True,  
    limit_tif=5,
    time_delta_format="rows",
    size=0.3,
    init_cash=10_000,
    fees=0.0004,
)
print(setup.sum(), breakout_pf.orders.side_buy.count())
6 3
cols = ["Signal Index", "Fill Index", "Price", "Side", "Type"]
breakout_pf.orders.readable[cols].head(4)
               Signal Index                Fill Index         Price  Side    Type
0 2023-03-13 00:00:00+00:00 2023-03-14 00:00:00+00:00  24550.837891   Buy   Limit
1 2023-05-01 00:00:00+00:00 2023-05-01 00:00:00+00:00  28091.568359  Sell  Market
2 2023-06-20 00:00:00+00:00 2023-06-21 00:00:00+00:00  28388.968750   Buy   Limit
3 2023-07-30 00:00:00+00:00 2023-07-30 00:00:00+00:00  29275.308594  Sell  Market

六次设置产生三笔交易,其余三次突破未在五根内发生,因此订单到期未成交,与券商处等待的止损单相同。

待成交限价与止损入场订单

设置 order_type="limit" 后,信号创建等待目标价的订单。目标来自 price,可按 limit_delta 偏移,并对后续每根最高与最低价检查。limit_reverse=True 反转检查方向,将限价变为止损入场订单。limit_tif 可按 K 线数或时长设置到期,limit_expiry 可按日期或周期边界设置,涵盖当日有效、撤销前有效和指定日期前有效订单。正 limit_delta 将普通买入限价放在参考价下方、卖出限价放在上方;反转则把触发放到另一侧,用于向上突破买入或向下突破卖出。

若希望限价单即使能在创建当根成交,也要从下一根才检查价格,可使用 limit_delay=True。这与到期不同:既能选择何时开始允许成交,也能选择何时停止等待。

价格跳空越过水平时,可选择成交价格:

Fill a buy limit after a gap down
index = pd.date_range("2026-01-01", periods=4)
pf = vbt.PF.from_signals(
    pd.Series([100.0, 100.0, 98.5, 99.0], index=index),
    pd.Series([True, False, False, False], index=index),
    open=pd.Series([100.0, 100.5, 97.0, 98.0], index=index),
    high=pd.Series([100.0, 101.0, 99.0, 99.5], index=index),
    low=pd.Series([100.0, 99.0, 96.5, 97.5], index=index),
    order_type="limit",
    limit_delta=0.02,  
    limit_order_price=vbt.Param(["autolimit", "limit"]),
)
pf.orders.readable[["Column", "Signal Index", "Fill Index", "Price"]]
      Column Signal Index Fill Index  Price
0  autolimit   2026-01-01 2026-01-03   98.0
1      limit   2026-01-01 2026-01-03   97.0

默认普通限价按限价成交,采用保守假设;止损入场若跳空越过水平,则按开盘成交。"limit" 允许普通限价按更有利的开盘价成交。

订单数量在成交时而非下单时解析,因此百分比数量使用当时可用现金。信号模拟器每列保留一个待成交限价单。等待期间,新同向信号默认忽略,订单保留最初价格。upon_adj_limit_conflict="cancelexecute" 则替换订单,随每个新信号重新报价:

Keep or re-quote a buy limit while the price rises
index = pd.date_range("2026-01-01", periods=5)
close = pd.Series([100.0, 101.0, 102.0, 103.0, 99.0], index=index)
open_ = pd.Series([100.0, 100.5, 101.5, 102.5, 102.0], index=index)
pf = vbt.PF.from_signals(
    close,
    pd.Series([True, True, True, True, False], index=index),
    open=open_,
    high=np.maximum(open_, close) + 0.5,
    low=np.minimum(open_, close) - 0.5,
    order_type="limit",
    limit_delta=0.02,
    upon_adj_limit_conflict=vbt.Param(["keepignore", "cancelexecute"]),
)
pf.orders.readable[["Column", "Signal Index", "Fill Index", "Price"]]
          Column Signal Index Fill Index   Price
0  cancelexecute   2026-01-04 2026-01-05  100.94

首个订单等待在 98 美元,即首次收盘下方 2%;最后一天最低仅到 98.50,因此从未成交。重新报价订单随收盘上涨,第四个信号后位于 100.94 美元,最终成交。upon_opp_limit_conflict 对反向信号做类似处理,默认取消待成交订单。

等待期间也可通过调整函数修改限价。网格或订单簿等同时需要多个挂单的策略,可在回调或自定义模拟器中管理。会员专享的自定义订单逻辑(英文)示例跟踪相反方向的突破水平。事件驱动回测展示如何保留自定义状态,并在一根内下多个订单。

调整待成交订单

随条件变化调整待成交订单。可以移动价格,或暂时停止成交而保留订单。例如,在预定新闻发布期间,或组合已有足够未平仓头寸时,阻止入场。

这里买入限价等待在 100 美元下方 2%。第二天价格到达 98 美元,但规则禁止当日成交。同一订单在价格再次达到 98 美元时成交:

Temporarily suspend a pending buy limit
index = pd.date_range("2026-01-01", periods=4)
close = pd.Series([100.0, 98.0, 99.0, 98.0], index=index)
can_fill = np.array([True, False, True, True])

@njit
def adjust_limit_nb(ctx, can_fill):
    if ctx.last_limit_info["creation_idx"][ctx.col] != -1:
        ctx.last_limit_info["delta"][ctx.col] = 0.02 if can_fill[ctx.i] else np.inf

pf = vbt.PF.from_signals(
    close,
    entries=[True, False, False, False],
    order_type="limit",
    limit_delta=0.02,
    adjust_func_nb=adjust_limit_nb,
    adjust_args=(can_fill,),
)
pf.orders.readable[["Signal Index", "Fill Index", "Price"]]
  Signal Index Fill Index  Price
0   2026-01-01 2026-01-04   98.0

将保存的 delta 设为无穷大,使买入限价无法触及;恢复 2% delta 后,订单重新具备成交资格,无须新的入场信号。

价格与时间阈值

止损和限价通过 delta_format 接受多种价位格式:

格式含义100 美元入场的示例
"percent"(默认)参考价格的比例0.05 相距 5 美元
"percent100"百分点5 相距 5 美元
"absolute"价格距离5 相距 5 美元
"target"价格本身95 即目标水平

时间限制通过 time_delta_format 使用类似方式:"index" 读取 "3D" 等时长,"rows" 计数 K 线。订单等待期间新信号到达时,upon_adj_limit_conflict 和 upon_opp_limit_conflict 决定保留或取消挂单,以及是否执行新信号。止损具有对应的 upon_adj_stop_conflict 和 upon_opp_stop_conflict。

执行价格与时机

默认订单在生成当根收盘成交。price 接受 "open"、"close"、"nextopen"、"nextclose" 或价格数组。若要将全部信息延迟若干根,from_ago 从指定根数之前读取订单信息。下方订单延迟在图中比较两种时机。

VBT 将每根分成开盘、开收盘之间和收盘三部分,先处理时间靠前的事件,因此开盘触发的挂单优先于收盘下达的订单。默认接受本根最低至最高区间外的价格;price_area_vio_mode="cap" 将其截到区间内,"error" 则报错。价格来自自定义计算时,"error" 可低成本发现本根不可能产生的成交。

买卖报价与其他数据一样是数组。可在入场处填卖价、离场处填买价,构成一个价格数组:

Buy at the ask and sell at the bid
index = pd.date_range("2026-01-01", periods=3)
bid = pd.Series([99.9, 101.9, 102.9], index=index)
ask = pd.Series([100.1, 102.1, 103.1], index=index)
entries = pd.Series([True, False, False], index=index)
exits = pd.Series([False, False, True], index=index)

price = (bid + ask) / 2  
price[entries] = ask[entries]
price[exits] = bid[exits]
pf = vbt.PF.from_signals(price, entries, exits, price=price, size=1)
pf.orders.readable[["Fill Index", "Side", "Price"]]
  Fill Index  Side  Price
0 2026-01-01   Buy  100.1
1 2026-01-03  Sell  102.9

对于挂单,也可在调整函数中添加买价或卖价检查,报价条件不满足时取消订单。这样可使用自己报价数据中实际可执行的一侧,决定允许哪些订单。

一根 K 线内的处理顺序

多个资产共享现金时,同根内交易顺序很重要。先卖出能释放买入所需现金。call_seq="auto" 按订单金额排序,使卖出在前:

Rotate from one asset into two others on the same bar
index = pd.date_range("2026-01-01", periods=2)
close = pd.DataFrame(
    {"A": [10.0, 10.0], "B": [20.0, 20.0], "C": [40.0, 40.0]},
    index=index
)
weights = pd.DataFrame(
    {"A": [0.0, 0.5], "B": [0.0, 0.5], "C": [1.0, 0.0]},
    index=index
)
kwargs = dict(
    size=weights,
    size_type="targetpercent",
    group_by=True,
    cash_sharing=True
)

vbt.PF.from_orders(close, **kwargs).allocations.iloc[-1]  
A    0.0
B    0.0
C    0.0
Name: 2026-01-02 00:00:00, dtype: float64
vbt.PF.from_orders(close, call_seq="auto", **kwargs).allocations.iloc[-1]
A    0.5
B    0.5
C    0.0
Name: 2026-01-02 00:00:00, dtype: float64

也可通过 call_seq 逐根传入自定义顺序,例如按资产排名。

成交、限制与拒单

VBT 成交前检查数量和可用现金。min_size 忽略过小订单,max_size 限制每笔数量,例如设为本根成交量的一部分;超过可用现金的订单默认部分成交,除非 allow_partial=False。传入 log=True,记录每笔请求、结果与原因:

Cap, partially fill, and ignore orders
index = pd.date_range("2026-01-01", periods=4)
close = pd.Series([10.0, 10.0, 10.0, 10.0], index=index)
pf = vbt.PF.from_orders(
    close,
    size=[50, 200, 0.5, -1000],
    min_size=1,
    max_size=100,
    init_cash=3000,
    log=True,
)
cols = ["Index", "[REQ] Size", "[RES] Size", "[RES] Status", "[RES] Status Info"]
pf.logs.readable[cols]
       Index  [REQ] Size  [RES] Size [RES] Status  [RES] Status Info
0 2026-01-01        50.0        50.0       Filled               None
1 2026-01-02       200.0       100.0       Filled        PartialFill
2 2026-01-03         0.5         NaN      Ignored  MinSizeNotReached
3 2026-01-04     -1000.0       100.0       Filled        PartialFill

数量限制遵循 size_type,因此 size_type="value" 时表示金额。cash_limit 限制单笔可用现金,size_granularity 将数量取整至整股或手数,reject_prob 随机拒绝部分订单以检验稳健性。若要在首个无法按请求成交的订单处停止,使用 raise_reject=True。NaN 数量不下单,因此可在再平衡时保留停牌资产持仓不动。

实盘成交与回测

将券商或其他回测器的成交作为订单记录传入,即可构建完整投资组合,便于衡量实盘偏离模拟多少。这里开篇突破策略的示例实际成交,两边价格各差 0.15%,手续费也更高:

Compare live fills with the simulated breakout
import io

csv = io.StringIO("""date,side,size,price,fees
2023-03-14,Buy,0.3,24587.67,7.38
2023-05-01,Sell,0.3,28049.43,8.41
2023-06-21,Buy,0.3,28431.55,8.53
2023-07-30,Sell,0.3,29231.40,8.77
2023-10-01,Buy,0.3,27529.99,8.26
""")
fills = pd.read_csv(csv, parse_dates=["date"])
fills["idx"] = data.index.get_indexer(fills.pop("date").dt.tz_localize("UTC"))  
live_pf = vbt.PF(data.close, order_records=fills, init_cash=10_000)

pd.DataFrame({
    "simulated": breakout_pf.trades.returns.values,
    "live": live_pf.trades.returns.values,
})
   simulated      live
0   0.143363  0.138652
1   0.030409  0.026104
2   0.537144  0.534242
pd.Series({"simulated": breakout_pf.total_return, "live": live_pf.total_return})
simulated    0.574451
live         0.565769
dtype: float64

现在可获得实盘账户的全部交易、回撤和度量,总收益率相差的 0.9 个百分点就是执行成本。下方从成交构建投资组合展示最简形式。订单记录按给定值接收,不检查现金。若希望将相同成交交给模拟器,使现金限制和手续费生效,应作为记录传入。

从成交构建投资组合

✅ 将执行记录转化为 VBT 投资组合,用与回测相同的工具探索交易、回撤与绩效。缺失字段采用默认值,可读方向名称自动映射,使设置保持简洁。

Calculate portfolio value and profit from two fills
close = pd.Series(
    [100.0, 105.0, 110.0],
    index=pd.date_range("2026-09-01", periods=3, freq="D")
)
fills = [
    dict(idx=0, size=2, price=100, fees=1, side="Buy"),
    dict(idx=2, size=2, price=110, fees=1, side="Sell"),
]
pf = vbt.PF(close, order_records=fills, init_cash=1000, freq="1D")

print(pf.total_profit)  
18.0
pf.value.tolist()  
[999.0, 1009.0, 1018.0]

目标价格

✅ 限价与止损单现在可以直接用目标价定义,无须使用距离。

Set the SL to the previous low in a random portfolio
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_random_signals(
    data,
    n=100,
    seed=42,
    sl_stop=data.low.vbt.ago(1),
    delta_format="target"
)
sl_orders = pf.orders.stop_type_sl
signal_index = pf.wrapper.index[sl_orders.signal_idx.values]
hit_index = pf.wrapper.index[sl_orders.idx.values]
hit_after = hit_index - signal_index
hit_after
TimedeltaIndex([ '7 days',  '3 days',  '1 days',  '5 days',  '4 days',
                 '1 days', '28 days',  '1 days',  '1 days',  '1 days',
                 '1 days',  '1 days', '13 days', '10 days',  '5 days',
                 '1 days',  '3 days',  '4 days',  '1 days',  '9 days',
                 '5 days',  '1 days',  '1 days',  '2 days',  '1 days',
                 '1 days',  '1 days',  '3 days',  '1 days',  '1 days',
                 '1 days',  '1 days',  '1 days',  '2 days',  '2 days',
                 '1 days', '12 days',  '3 days',  '1 days',  '1 days',
                 '1 days',  '1 days',  '1 days',  '3 days',  '1 days',
                 '1 days',  '1 days',  '4 days',  '1 days',  '1 days',
                 '2 days',  '6 days', '11 days',  '1 days',  '2 days',
                 '1 days',  '1 days',  '1 days',  '1 days',  '1 days',
                 '4 days', '10 days',  '1 days',  '1 days',  '1 days',
                 '2 days',  '3 days',  '1 days'],
               dtype='timedelta64[ns]', name='Date', freq=None)

订单延迟

✅ VBT 默认在当前 K 线结束时执行订单。此前,若想延迟到下一根,需要手动将全部订单相关数组后移一根,容易出错。现在只需指定从过去多少根读取订单信息。此外,price 参数支持 "nextopen" 和 "nextclose",一行即可完成设置。

Compare orders without and with a delay in a random portfolio
pf = vbt.PF.from_random_signals(
    vbt.YFData.pull("BTC-USD", start="2021-01", end="2021-02"),
    n=3,
    seed=42,
    price=vbt.Param(["close", "nextopen"])
)
fig = pf.orders["close"].plot(
    buy_trace_kwargs=dict(name="Buy (close)", marker=dict(symbol="triangle-up-open")),
    sell_trace_kwargs=dict(name="Sell (close)", marker=dict(symbol="triangle-down-open"))
)
pf.orders["nextopen"].plot(
    plot_ohlc=False,
    plot_close=False,
    buy_trace_kwargs=dict(name="Buy (nextopen)"),
    sell_trace_kwargs=dict(name="Sell (nextopen)"),
    fig=fig
)
fig.show()
BTC-USD OHLC 与随机订单,比较收盘执行和延迟至下一开盘 图表数据(JSON)

限价单

✅ 信号模拟终于支持限价单!包括 DAY、GTC、GTD、LOO 和 FOK 等有效期(TIF)订单 ⏰ 还可反转限价,或用距离创建,便于测试。

Explore how limit delta affects number of orders in a random portfolio
pf = vbt.PF.from_random_signals(
    vbt.YFData.pull("BTC-USD"),
    n=100,
    seed=42,
    order_type="limit",
    limit_delta=vbt.Param(np.arange(0.001, 0.1, 0.001)),  
)
pf.orders.count().vbt.plot(
    xaxis_title="Limit delta",
    yaxis_title="Order count"
).show()
固定随机种子的 BTC-USD 随机投资组合,不同限价距离下的订单数量 图表数据(JSON)

距离格式

✅ 此前止损只能以百分比提供。单个值尚可,数组往往需要额外转换,例如按 ATR 设置止损必须知道入场价。更一般地,若想锁定某笔交易的固定美元收益,可能希望使用固定价格距离的移动止损。为此,VBT 现在提供多种止损值格式,即“距离格式”。

Use ATR as SL
data = vbt.YFData.pull("BTC-USD")
atr = vbt.talib("ATR").run(data.high, data.low, data.close).real
pf = vbt.PF.from_holding(
    data.loc["2022-01-01":"2022-01-07"],
    sl_stop=atr.loc["2022-01-01":"2022-01-07"],
    delta_format="absolute"
)
pf.orders.plot().show()
BTC-USD OHLC,持有交易使用 ATR 作为绝对止损距离 图表数据(JSON)

跳过 K 线

✅ 订单和信号模拟现在可部分跳过没有定义订单的 K 线,对订单稀疏的策略通常显著提速。

Benchmark bar skipping in a buy-and-hold portfolio
data = vbt.BinanceData.pull("BTCUSDT", start="one month ago UTC", timeframe="minute")
size = data.symbol_wrapper.fill(np.nan)
size[0] = np.inf
%%timeit
vbt.PF.from_orders(data, size, ffill_val_price=True)  
5.92 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
vbt.PF.from_orders(data, size, ffill_val_price=False)
2.75 ms ± 16 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

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

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