# 订单与执行 (/zh/features/backtesting/orders-and-execution)

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

```python title="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"  # (1)
>>> price = data.symbol_wrapper.fill(np.inf)
>>> price[setup] = data.high.rolling(10).max()[setup]  # (2)

>>> 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,  # (3)
...     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
```

1.  入场成为待成交订单，离场仍为收盘成交的市价单。
2.  订单等待 10 日最高价，即设置订单时的突破水平。
3.  反向限价在价格上涨到该水平时买入，因此成为止损入场订单。

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

## 待成交限价与止损入场订单 \[#pending-limit-and-stop-entry-orders]

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

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

价格跳空越过水平时，可选择成交价格：

```python title="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,  # (1)
...     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
```

1.  在信号 K 线 100 美元收盘价下方 2% 买入，即 98 美元。

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

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

```python title="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` 对反向信号做类似处理，默认取消待成交订单。

等待期间也可通过调整函数修改限价。网格或订单簿等同时需要多个挂单的策略，可在回调或自定义模拟器中管理。会员专享的[自定义订单逻辑（英文）](https://members.vectorbt.pro/cookbook/portfolio/#custom-order-logic)示例跟踪相反方向的突破水平。[事件驱动回测](/zh/features/backtesting/event-driven-backtesting/)展示如何保留自定义状态，并在一根内下多个订单。

## 调整待成交订单 \[#adjusting-pending-orders]

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

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

```python title="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 后，订单重新具备成交资格，无须新的入场信号。

## 价格与时间阈值 \[#price-and-time-thresholds]

止损和限价通过 `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`。

## 执行价格与时机 \[#execution-price-and-timing]

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

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

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

```python title="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  # (1)
>>> 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
```

1.  交易之间用中间价对持仓估值。

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

## 一根 K 线内的处理顺序 \[#processing-order-within-a-bar]

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

```python title="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]  # (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
```

1.  各列从左到右交易。C 卖出前，A 和 B 已尝试买入但没有现金，因此轮动结束时只剩现金。

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

## 成交、限制与拒单 \[#fills-limits-and-rejections]

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

```python title="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`
数量不下单，因此可在再平衡时保留停牌资产持仓不动。

## 实盘成交与回测 \[#live-fills-versus-the-backtest]

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

```python title="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"))  # (1)
>>> 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
```

1.  订单记录通过价格数据中的 K 线位置定位每次成交。

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

## 从成交构建投资组合 \[#portfolio-from-fills]

新增于 v2026.9.5

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

```python title="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)  # (1)
18.0
>>> pf.value.tolist()  # (2)
[999.0, 1009.0, 1018.0]
```

1.  20 美元价格收益扣除两笔各 1 美元手续费后，变为 18 美元。
2.  净值包括两次成交之间的未实现利润。

## 目标价格 \[#target-price]

新增于 1.10.0

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

```python title="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)
```

## 订单延迟 \[#order-delays]

新增于 1.4.1

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

```python title="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 与随机订单，比较收盘执行和延迟至下一开盘. [Figure data (JSON)](/assets/figures/features/portfolio/order-delays.6a9760444aa9.json)

## 限价单 \[#limit-orders]

新增于 1.4.0

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

```python title="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)),  # (1)
... )
>>> pf.orders.count().vbt.plot(
...     xaxis_title="Limit delta",
...     yaxis_title="Order count"
... ).show()
```

1.  限价距离是收盘价（或指定价格）到目标限价的距离，以百分比表示。距离越大，最终触及概率越低。

固定随机种子的 BTC-USD 随机投资组合，不同限价距离下的订单数量. [Figure data (JSON)](/assets/figures/features/portfolio/limit-delta.7987726a54d4.json)

## 距离格式 \[#delta-formats]

新增于 1.4.0

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

```python title="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 作为绝对止损距离. [Figure data (JSON)](/assets/figures/features/portfolio/delta-formats.fdbb55d4608e.json)

## 跳过 K 线 \[#bar-skipping]

新增于 1.4.0

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

```python title="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
```

```python
>>> %%timeit
>>> vbt.PF.from_orders(data, size, ffill_val_price=True)  # (1)
5.92 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

1.  启用时（默认），此参数强制模拟逐根处理。

```python
>>> %%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)
```
