功能
订单与执行
模拟限价与止损入场订单、成交价格、时机、拒单及实际成交
回测究竟如何使订单成交,能否模拟等待指定价格的订单?VBT 以明确的价格、时机、到期和拒单规则模拟市价、限价与止损入场订单,让您在 Python 中回测限价单,并了解每笔为何成交或未成交。
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 3cols = ["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。这与到期不同:既能选择何时开始允许成交,也能选择何时停止等待。
价格跳空越过水平时,可选择成交价格:
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"
则替换订单,随每个新信号重新报价:
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 美元时成交:
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" 可低成本发现本根不可能产生的成交。
买卖报价与其他数据一样是数组。可在入场处填卖价、离场处填买价,构成一个价格数组:
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" 按订单金额排序,使卖出在前:
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: float64vbt.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,记录每笔请求、结果与原因:
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%,手续费也更高:
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.534242pd.Series({"simulated": breakout_pf.total_return, "live": live_pf.total_return})simulated 0.574451
live 0.565769
dtype: float64现在可获得实盘账户的全部交易、回撤和度量,总收益率相差的 0.9 个百分点就是执行成本。下方从成交构建投资组合展示最简形式。订单记录按给定值接收,不检查现金。若希望将相同成交交给模拟器,使现金限制和手续费生效,应作为记录传入。
✅ 将执行记录转化为 VBT 投资组合,用与回测相同的工具探索交易、回撤与绩效。缺失字段采用默认值,可读方向名称自动映射,使设置保持简洁。
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.0pf.value.tolist() [999.0, 1009.0, 1018.0]✅ 限价与止损单现在可以直接用目标价定义,无须使用距离。
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_afterTimedeltaIndex([ '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",一行即可完成设置。
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()✅ 信号模拟终于支持限价单!包括 DAY、GTC、GTD、LOO 和 FOK 等有效期(TIF)订单 ⏰ 还可反转限价,或用距离创建,便于测试。
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()✅ 此前止损只能以百分比提供。单个值尚可,数组往往需要额外转换,例如按 ATR 设置止损必须知道入场价。更一般地,若想锁定某笔交易的固定美元收益,可能希望使用固定价格距离的移动止损。为此,VBT 现在提供多种止损值格式,即“距离格式”。
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()✅ 订单和信号模拟现在可部分跳过没有定义订单的 K 线,对订单稀疏的策略通常显著提速。
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 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。