功能

实时模拟

在新 K 线上延续回测、串联运行,并向外部交易系统提供结果

新 K 线到达时,回测能否不重算历史而继续运行?VBT 从最后状态延续投资组合:现金、持仓、待触发止损与限价单都会保留,每根新 K 线从上次停止处推进模拟。您可以回放历史,再让同一个模拟账户在新数据上继续,用于 Python 增量回测和模拟交易。

Replay the last 35 days of 2023 one bar at a time
data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")

def get_signals(data):
    sma = data.close.rolling(20).mean()
    return data.close.vbt.crossed_above(sma), data.close.vbt.crossed_below(sma)

history = data.iloc[:330]
entries, exits = get_signals(history)
pf = vbt.PF.from_signals(
    history,
    entries,
    exits,
    tsl_stop=0.05,
    attach_preparer=True,  
)

emitted = []
for i in range(330, len(data.index)):
    available = data.iloc[:i + 1]  
    entries, exits = get_signals(available)
    pf = pf.update(  
        available.iloc[-1:],
        entries=entries.iloc[-1:],
        exits=exits.iloc[-1:],
    )
    orders = pf.orders.readable
    emitted.append(orders[orders["Fill Index"] == available.index[-1]])  
pd.concat(emitted)[["Fill Index", "Side", "Price", "Stop Type"]]
                  Fill Index  Side         Price Stop Type
45 2023-12-11 00:00:00+00:00  Sell  42470.239844       TSL
46 2023-12-18 00:00:00+00:00   Buy  42623.539062      None
47 2023-12-26 00:00:00+00:00  Sell  42149.559180       TSL
48 2023-12-27 00:00:00+00:00   Buy  43442.855469      None
49 2023-12-28 00:00:00+00:00  Sell  42627.855469      None
entries, exits = get_signals(data)
full_pf = vbt.PF.from_signals(data, entries, exits, tsl_stop=0.05)  
pf.orders.records.equals(full_pf.orders.records), pf.value.equals(full_pf.value)
(True, True)

12 月 11 日触发的移动止损,属于回放开始前已开立的持仓。其峰值价格在每次更新中延续。经过 35 次单根更新,订单和净值曲线与一次性回测完全一致。

从研究更新到实时数据

让研究随着新数据推进。延续机制连接历史回测、增量分析和模拟交易:

您想做什么VBT 如何处理
新增一天价格后更新策略用 pf.update 延伸投资组合
回放历史数据流或监控模拟账户每次处理一根或一批新 K 线
在时期之间重训模型或修改参数串联模拟,同时延续账户
让原生模拟器在新 K 线到达间保持运行推进 Rust 步进器,并保存检查点

下文同时介绍 Python 投资组合更新和原生 Rust 步进。数据源与执行系统可以继续独立于模拟。

从最后状态继续

pf.update 接收新数据与新输入,从投资组合最后状态继续。状态包括现金、持仓、债务、估值价格、入场价、待成交限价单,以及全部止损,包括移动峰值和时间止损计数器。结果默认包含完整历史;stack=False 只包含新片段,仅需最新订单时成本更低。

延续也适用于自定义交易规则。例如,亏损后的冷却期可跨越多次更新。将回调记忆与投资组合状态一同保存,传给下次更新。事件驱动回测介绍此类规则,实时教程展示如何延续其记忆。

重启后恢复

状态也能存储。save_state=True 保留模拟期间计算的现金和持仓序列,pf.last_state 保存后续运行所需的最终状态。以 attach_preparer=True 构建的投资组合,可用 pf.save(path) 写入磁盘,再在后续会话用 vbt.PF.load(path) 恢复,准备下一次 pf.update。完整实时流程还应保留指标状态和应用自身拥有的策略记忆。下方投资组合延续验证移动止损与时间止损跨更新保持一致。

串联与混合运行

一些工作流需要有意暂停模拟:每月重训模型、重新优化权重,或在时期之间修改参数。分段回测,让每段从上一段继续,再用 vbt.PF.row_stack 合成一段历史。下方串联模拟构建此类链条;同一模式也让投资组合优化器在每次再平衡时重新估计权重,同时延续账户。

逐根 K 线步进

原生 Rust 引擎提供步进器:驻留内存、每次推进一根 K 线的模拟器对象,在步与步之间保留策略及执行状态。步进器可克隆或序列化为检查点,之后恢复,例如重启后;输出可存为 NumPy 文件,由 Python 加载回投资组合。

教程

会员专享的从 Python 到 Rust(英文)教程分别在 Python、Numba 和原生 Rust 步进器中延续有状态策略,并检查三者一致。

实时流程

实时循环有三个阶段,各自保留状态:数据、指标和投资组合。data.update() 获取新 K 线,设置 return_meta=True 时报告哪些行被追加、哪些被修订。指标可重算所需窗口,流式指标也可从最后状态更新。投资组合用 pf.update 继续。vbt.wait(timeframe, floor=True) 等待下一个 K 线边界,让循环在每根收盘后立即运行:

Structure of a polling loop
while True:
    vbt.wait("1h", floor=True)
    update = data.update(return_meta=True)
    data = update["data"]
    for row in update["appended"]:
        ...  # compute signals for the closed bar, then call pf.update

连接券商

VBT 不向券商发单。它负责决策与模拟,由您的执行层路由订单、处理成交并反馈。以下规则帮助两者保持一致:

  • 基于已收盘 K 线决策。 信号只使用完整 K 线,避免执行后决策又变化。许多数据源还返回仍在形成的 K 线,因此计算前应删除它。
  • 成交前规划。 下一开盘执行时,应下订单来自最后已收盘 K 线的信号。模拟器只在下一根到达后才记录它,止损或冲突信号仍可能改变订单。
  • 对账。 每次更新后比较券商成交与持仓和模拟结果;K 线修订时避免重复发单。实际成交也可作为独立投资组合分析,详见订单与执行。

订单记录只含已成交订单。待触发止损和限价单保存在最终状态,因此券商端止损可以对应模拟止损:

Read the trailing stop level to place at the broker
data = vbt.YFData.pull("ETH-USD", start="2024-09-01", end="2024-11-13")
sma = data.close.rolling(20).mean()
pf = vbt.PF.from_signals(
    data,
    data.close.vbt.crossed_above(sma),
    data.close.vbt.crossed_below(sma),
    tsl_stop=0.08,
)
state = pf.last_state  
position = float(state.last_position[0])
tsl_info = state.last_tsl_info[0]
position, float(tsl_info["peak_price"])
(0.0333110557990236, 3444.154052734375)
vbt.pf_nb.get_tsl_info_target_price_nb(tsl_info, position)  
3168.621728515625

持仓在 11 月 6 日开立,11 月 12 日收盘后仍未平仓。若下一根穿过 3168.62,回测将在该水平卖出;若开盘已低于它,则按开盘卖出。执行层可在券商设置相同止损水平,并将实际成交与模拟对账。

投资组合延续

✅ 新数据到达时,从上次模拟结束处继续。VBT 将持仓、订单 ID 和止损状态带入每次更新,包括入场时间戳和移动最高价。移动止损记住峰值,时间止损继续计数,每次更新返回包含合并历史的新投资组合。

Preserve a trailing stop across two updates
close = pd.Series(
    [100.0, 110.0, 108.0, 98.0, 103.0, 115.0],
    index=pd.date_range("2026-09-01", periods=6, freq="D")
)
entries = pd.Series([True, False, False, False, True, False], index=close.index)
pf_kwargs = dict(size=1, init_cash=1000, tsl_stop=0.05, freq="1D")

pf = vbt.PF.from_signals(
    close.iloc[:2],
    entries=entries.iloc[:2],
    attach_preparer=True,  
    **pf_kwargs,
)
for start in [2, 4]:
    pf = pf.update(  
        close.iloc[start:start + 2],
        entries=entries.iloc[start:start + 2],
    )

print(pf.orders.readable[["Order Id", "Fill Index", "Side", "Price", "Stop Type"]])
   Order Id Fill Index  Side  Price Stop Type
0         0 2026-09-01   Buy  100.0      None
1         1 2026-09-04  Sell   98.0       TSL
2         2 2026-09-05   Buy  103.0      None
full_pf = vbt.PF.from_signals(close, entries=entries, **pf_kwargs)  
pf.orders.records.equals(full_pf.orders.records) and pf.value.equals(full_pf.value)
True

教程

更多内容参见实时模拟(英文)教程。

串联模拟

✅ 每个内置模拟函数现在处理完全部数据后都会返回最终模拟状态,包括最后现金、持仓、待处理订单及其他相关信息。该状态可传入下一次运行,从上次结束处继续,实现跨不同时期或数据集的无缝串联。

Simulate a SMA crossover strategy on a monthly basis
data = vbt.BinanceData.pull(  
    "BTCUSDT",
    start="one year ago",
    timeframe="5 minutes",
    cache=True
)

fast_sma = data.run("talib_func:sma", timeperiod=20)  
slow_sma = data.run("talib_func:sma", timeperiod=50)
long_entries = fast_sma.vbt.crossed_above(slow_sma)
short_entries = fast_sma.vbt.crossed_below(slow_sma)

single_pf = vbt.PF.from_signals(  
    data,
    long_entries=long_entries,
    short_entries=short_entries,
)

data_splits = data.split(by="month")  
long_entries_splits = long_entries.vbt.split(by="month", into=None)
short_entries_splits = short_entries.vbt.split(by="month", into=None)

pf_list = []
last_state = None
for i in range(len(data_splits)):  
    pf = vbt.PF.from_signals(
        data_splits.iloc[i],
        long_entries=long_entries_splits.iloc[i],
        short_entries=short_entries_splits.iloc[i],
        last_state=last_state,
    )
    pf_list.append(pf)
    last_state = pf.last_state

stacked_pf = vbt.PF.row_stack(*pf_list, chained=True)  
print(stacked_pf.returns.equals(single_pf.returns))
True

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

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