# 实时模拟 (/zh/features/backtesting/live-simulation)

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

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

>>> emitted = []
>>> for i in range(330, len(data.index)):
...     available = data.iloc[:i + 1]  # (2)
...     entries, exits = get_signals(available)
...     pf = pf.update(  # (3)
...         available.iloc[-1:],
...         entries=entries.iloc[-1:],
...         exits=exits.iloc[-1:],
...     )
...     orders = pf.orders.readable
...     emitted.append(orders[orders["Fill Index"] == available.index[-1]])  # (4)
>>> 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)  # (5)
>>> pf.orders.records.equals(full_pf.orders.records), pf.value.equals(full_pf.value)
(True, True)
```

1.  保留模拟参数，供后续更新复用。
2.  只存在截至新 K 线的数据。信号从这些数据计算，与实盘相同。
3.  从保存状态出发，只模拟新 K 线，并追加到历史。
4.  本根产生的订单，可交给执行系统。
5.  一次运行完整年度，以检查增量结果。

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

## 从研究更新到实时数据 \[#from-research-updates-to-live-data]

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

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

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

## 从最后状态继续 \[#continuing-from-the-last-state]

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

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

## 重启后恢复 \[#resume-after-a-restart]

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

## 串联与混合运行 \[#chaining-and-hybrid-runs]

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

## 逐根 K 线步进 \[#bar-by-bar-stepping]

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

!!! info "教程"
    会员专享的[从 Python 到 Rust（英文）](https://members.vectorbt.pro/tutorials/from-python-to-rust/live/)教程分别在 Python、Numba
    和原生 Rust 步进器中延续有状态策略，并检查三者一致。

## 实时流程 \[#live-pipelines]

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

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

## 连接券商 \[#connecting-to-a-broker]

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

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

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

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

1.  最后一根之后的现金、持仓、未平仓信息，以及所有待触发止损和限价单。
2.  入场以来最高价下方 8%。没有活动移动止损时，辅助函数返回 `NaN`。

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

## 投资组合延续 \[#portfolio-continuation]

新增于 v2026.9.5

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

=== "移动止损"
    ```python title="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,  # (1)
    ...     **pf_kwargs,
    ... )
    >>> for start in [2, 4]:
    ...     pf = pf.update(  # (2)
    ...         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)  # (3)
    >>> pf.orders.records.equals(full_pf.orders.records) and pf.value.equals(full_pf.value)
    True
    ```

    1.  保留模拟参数，供后续更新使用。
    2.  跨更新保留 110 美元移动最高价。5% 止损在 98 美元离场，即阈值之下的下一个可用收盘。更新默认连接历史，`stack=False` 只返回新片段。
    3.  将合并订单记录和组合净值与全数据单次运行比较。

=== "时间止损"
    ```python title="Preserve a three-day time stop across an update"
    >>> 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 = vbt.PF.from_signals(
    ...     close.iloc[:2],
    ...     entries=entries.iloc[:2],
    ...     td_stop="3 days",
    ...     size=1,
    ...     init_cash=1000,
    ...     attach_preparer=True,
    ...     freq="1D",
    ... )
    >>> pf = pf.update(close.iloc[2:], entries=entries.iloc[2:])  # (1)

    >>> print(pf.orders.readable[["Fill Index", "Side", "Stop Type"]])
      Fill Index  Side Stop Type
    0 2026-09-01   Buy      None
    1 2026-09-04  Sell        TD
    2 2026-09-05   Buy      None
    ```

    1.  入场时间戳仍为 9 月 1 日，因此三天止损在 9 月 4 日离场。9 月 5 日下一次入场开始新的持有期。

!!! info "教程"
    更多内容参见[实时模拟（英文）](https://members.vectorbt.pro/tutorials/from-python-to-rust/live/)教程。

## 串联模拟 \[#chaining-simulations]

新增于 v2026.3.1

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

=== "示例 1：按月"
    ```python title="Simulate a SMA crossover strategy on a monthly basis"
    >>> data = vbt.BinanceData.pull(  # (1)
    ...     "BTCUSDT",
    ...     start="one year ago",
    ...     timeframe="5 minutes",
    ...     cache=True
    ... )

    >>> fast_sma = data.run("talib_func:sma", timeperiod=20)  # (2)
    >>> 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(  # (3)
    ...     data,
    ...     long_entries=long_entries,
    ...     short_entries=short_entries,
    ... )

    >>> data_splits = data.split(by="month")  # (4)
    >>> 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)):  # (5)
    ...     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)  # (6)
    >>> print(stacked_pf.returns.equals(single_pf.returns))
    True
    ```

    1.  从 Binance 拉取过去一年 BTC 五分钟数据，并缓存以加速访问。
    2.  基于简单 SMA 交叉策略，为完整数据集生成多空入场信号。
    3.  对完整数据集运行单次模拟，作为参照。
    4.  按月切分数据与信号，逐月模拟。
    5.  遍历各月，以相应数据和信号运行，并将上次最终状态传给下一次。
    6.  将结果投资组合按行堆叠，结果与完整数据集单次模拟一致。

=== "示例 2：实时数据流"
    ```python title="Simulate a SMA crossover strategy on a live data stream"
    >>> def generate_signals(data):
    ...     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)
    ...     return long_entries, short_entries

    >>> data = vbt.BinanceData.pull(  # (1)
    ...     "BTCUSDT",
    ...     start="1 day ago",
    ...     end="1 minute ago",
    ...     timeframe="1 minute"
    ... )
    >>> long_entries, short_entries = generate_signals(data)
    >>> pf = vbt.PF.from_signals(  # (2)
    ...     data,
    ...     long_entries=long_entries,
    ...     short_entries=short_entries,
    ...     attach_preparer=True
    ... )

    >>> with vbt.ProgressBar() as pbar:
    ...     while True:
    ...         try:
    ...             vbt.wait("1 minute", floor=True)  # (3)
    ...         except KeyboardInterrupt:
    ...             break
    ...
    ...         n_bars = len(data.index)
    ...         data = data.update()  # (4)
    ...         if len(data.index) == n_bars:
    ...             pbar.set_postfix(str(data.index[-1]))
    ...             pbar.update()
    ...             continue
    ...
    ...         long_entries, short_entries = generate_signals(data)
    ...         new_pf = pf.update(  # (5)
    ...             data.iloc[n_bars:],
    ...             long_entries=long_entries.iloc[n_bars:],
    ...             short_entries=short_entries.iloc[n_bars:],
    ...             stack=False
    ...         )
    ...         new_orders = new_pf.orders.readable
    ...         if len(new_orders) > 0:  # (6)
    ...             for i in range(len(new_orders)):
    ...                 print("NEW ORDER:")
    ...                 print(new_orders.iloc[i])
    ...                 print()
    ...
    ...         pf = vbt.PF.row_stack(  # (7)
    ...             (pf, new_pf),
    ...             chained=True,
    ...             preparer=new_pf.preparer
    ...         )
    ...         pbar.set_postfix(str(data.index[-1]))
    ...         pbar.update()
    ```

    1.  从 Binance 拉取过去一天 BTC 一分钟数据。不包含最近一分钟，以避免不完整数据。
    2.  对全数据运行初始模拟，并附加预处理器，供之后访问原始参数。
    3.  等待下一分钟开始，确保有新数据。对等待时间向下取整，可对齐分钟起点。
    4.  更新数据取得最新 K 线。若没有新 K 线，跳过本轮其余部分。
    5.  使用新数据和信号更新投资组合，不与此前组合堆叠，以便独立保留。
    6.  检查更新产生的新订单，并打印。
    7.  将新组合与此前组合按行堆叠，无缝延续并保留完整模拟历史。
