# 真实回测 (/zh/features/backtesting/backtest-realism)

为什么回测看起来比实盘更好？可能订单在信号产生前就已成交，日线值在当天结束前就已出现，或交易成本吞噬了利润。VBT
让您控制订单时机、按信息实际可用时间对齐数据、模拟成本，并检查逐笔成交。下面介绍如何用这些工具避免前视偏差，在 Python 中构建更真实的回测。

```python title="Fill a limit order on the bar that created it, then on the next bar"
>>> data = vbt.BinanceData.pull(
...     "BTCUSDT", start="2020-01-01", end="2025-01-01", timeframe="1h"
... )
>>> dip = data.close < data.close.vbt.rolling_mean(24)  # (1)
>>> limit_price = data.close * 0.99
>>> exits = dict(
...     tp_stop=0.02,
...     td_stop=24,
...     time_delta_format="rows",
...     stop_entry_price="fillprice",  # (2)
...     fees=0.001,
... )

>>> same_bar = vbt.PF.from_signals(  # (3)
...     data, entries=dip & (data.low <= limit_price), price=limit_price, **exits
... )
>>> next_bar = vbt.PF.from_signals(  # (4)
...     data, entries=dip, price=limit_price, order_type="limit", limit_tif=2, **exits
... )
>>> metrics = ["total_trades", "win_rate", "total_return", "sharpe_ratio"]
>>> stats = {"same bar": same_bar.stats(metrics), "next bar": next_bar.stats(metrics)}
>>> pd.DataFrame(stats).astype(float).round(2)
                  same bar  next bar
Total Trades        802.00    663.00
Win Rate [%]         83.29     66.37
Total Return [%]  87489.57    -31.97
Sharpe Ratio          3.88      0.01
```

1.  信号使用每小时收盘价，因此只有该小时结束后才存在。买入限价设在该收盘价下方 1%。
2.  从限价成交价计算，盈利 2% 时离场，或持有 24 小时后离场。
3.  常见错误：同一小时最低价触及限价就视为成交。但这个最低价发生在产生信号的收盘之前。
4.  在收盘价下方创建的限价单不能在自身 K 线成交。`limit_tif=2` 使它仅保留到下一小时，因而与错误版本一样只有一次成交机会。

同根 K 线版本总能买到已经发生的低点，再卖出反弹，因此更快到达目标，交易也更频繁。它显示 87,000% 收益，而符合因果关系的版本亏损 32%。这种差距说明，花时间搜索参数之前应先检查订单时机。

## 符合因果关系的订单时机 \[#causal-order-timing]

决策只能使用作出决策时已知的信息，订单只能在该时刻或之后成交。`vbt.PF.from_signals` 的 `price` 参数指定每个订单在 K 线何处执行：

| 设置                                      | 信号使用    | 订单成交         |
| --------------------------------------- | ------- | ------------ |
| `price="close"`（默认）                     | 本根收盘价   | 在该收盘价        |
| `price="nextopen"`                      | 本根收盘价   | 在下一根开盘价      |
| `from_ago=1`                            | 上一根 K 线 | 在本根价格        |
| `order_type="limit"` 配合 `price="close"` | 本根收盘价   | 从下一根起，在限价触及时 |

限价单也遵循相同规则。开盘时下单，可在本根任何位置成交；收盘时下单，等待下一根；中间下单，则只能检查收盘价。止损对后续各根最高与最低价进行检查，`limit_delay`
或订单延迟会进一步推迟成交。全部选项见[订单与执行](/zh/features/backtesting/orders-and-execution/)，延迟见[订单延迟](/zh/features/backtesting/orders-and-execution/#order-delays)，同根内同时存在止损和目标时的处理见[真实止损成交](/zh/features/backtesting/stop-loss-and-take-profit/#realistic-stop-fills)。

## 止损成交与数据分辨率 \[#stop-fills-and-data-resolution]

只有提供最高价和最低价，才能检查它们。若仅传入收盘序列，而非数据对象或 `open`、`high` 和 `low`，所有止损都要等收盘价越过水平后才触发：

```python title="Test a 3% trailing stop on closes only and on full bars"
>>> btc = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
>>> entries = btc.close.vbt.crossed_above(btc.close.vbt.rolling_mean(20))
>>> pfs = {
...     "close only": vbt.PF.from_signals(btc.close, entries, tsl_stop=0.03),
...     "OHLC": vbt.PF.from_signals(btc, entries, tsl_stop=0.03),
... }
>>> stop_metrics = ["total_trades", "win_rate", "total_return", "max_dd"]
>>> pd.DataFrame({k: pf.stats(stop_metrics) for k, pf in pfs.items()}).astype(float).round(2)
                  close only    OHLC
Total Trades           90.00   96.00
Win Rate [%]           38.89   41.67
Total Return [%]      530.06  213.53
Max Drawdown [%]       33.67   21.40
```

只使用收盘价时，只要日内下跌后收盘仍在止损之上，交易便会继续持有，因此捕捉更多反弹，收益增加一倍以上。止损最终触发时，成交价是低于止损水平的收盘价，加深了回撤。两个结果都不等同于交易所止损单实际会产生的结果。

## 高时间周期对齐 \[#higher-timeframe-alignment]

更长时间周期的数据存在同样陷阱。日线值要到当天收盘才已知，但若用普通前向填充对齐到小时 K 线，就会将当天收盘价提前交给当天各小时：

```python title="Align a daily trend filter to hourly bars, naively and safely"
>>> daily_close = data.close.resample("1D").last()
>>> daily_trend = daily_close > daily_close.vbt.rolling_mean(20)
>>> naive_trend = daily_trend.reindex(data.index, method="ffill")  # (1)
>>> safe_trend = daily_trend.vbt.realign_closing(data.index, freq="1h")  # (2)
>>> safe_trend = safe_trend.fillna(False).astype(bool)

>>> pfs = {
...     "naive": vbt.PF.from_signals(data, naive_trend, ~naive_trend, fees=0.001),
...     "safe": vbt.PF.from_signals(data, safe_trend, ~safe_trend, fees=0.001),
... }
>>> pd.DataFrame({k: pf.stats(metrics) for k, pf in pfs.items()}).astype(float).round(2)
                      naive    safe
Total Trades         107.00  107.00
Win Rate [%]          93.46   25.23
Total Return [%]  854502.66  315.56
Sharpe Ratio           4.41    0.87
```

1.  日线以当天开始时间标记，因此当天每小时都获得由当天收盘价计算的趋势。
2.  将各日线值移至当天最后一根小时 K 线，此时日收盘价才已知。

2020 至 2024 年使用错误和安全对齐的日线趋势过滤器的比特币小时策略净值，对数刻度. [Figure data (JSON)](/assets/figures/features/backtesting/htf-alignment-equity.980edbc3a260.json)

同样 107 笔交易，错误对齐时胜率 93%，安全对齐时为 25%。[多时间周期分析](/zh/features/data/multi-timeframe-analysis/)详细介绍对齐与重采样。

## 时点资产池 \[#point-in-time-universes]

只在今天仍上市的股票上测试，策略永远不会持有倒闭或退出指数的公司。为避免幸存者偏差，应保留曾属于资产池的全部品种，在其非成员期间填充 NaN，让回测看到各日期当时实际存在的资产池。没有价格的 K
线，VBT 会忽略全部信号，包括离场，因此应在品种仍属于资产池的最后一根 K
线上平仓。[数据流程](/zh/features/data/financial-data-pipelines/#universes-and-futures)展示如何构建成员资格掩码。

## 交易成本与可用资金 \[#trading-costs-and-available-capital]

即使使用正确价格，策略仍可能花掉实际上没有的资金。VBT
处理订单时计入手续费和滑点，因此成本既影响最终收益，也影响可买入数量。使用比例佣金、固定手续费和滑点，检验策略在成本后是否仍有效。[手续费、滑点和杠杆](/zh/features/backtesting/fees-slippage-and-leverage/)介绍这些设置及借贷成本。

多个资产竞争同一笔资金时，使用[共享现金余额](/zh/features/backtesting/portfolio-accounting/#shared-cash)。数量限制、整股或手数取整及部分成交设置，可模拟符合账户能力的订单。[成交、限制与拒单](/zh/features/backtesting/orders-and-execution/#fills-limits-and-rejections)提供订单被限制、部分成交和忽略的示例。

## 回测与实盘一致性 \[#backtest-to-live-parity]

实盘与回测结果不同时，应逐步比较。相同输入、设置，以及使用随机性时的相同随机种子，可复现 VBT 模拟并找到首个差异。

*   **数据：** 检查实时 K 线与历史数据是否一致，包括最后一根仍在形成的 K 线。
*   **指标与信号：** 在实时数据上重新计算，比较数值与时间戳。各平台的指标定义也可能不同，例如 RSI 的平滑和预热方式，因此应先逐根比较数值，再比较交易。
*   **订单：** `pf.orders.readable` 列出每笔订单的信号、创建和成交 K 线，以及止损类型，可定位两次运行从哪里出现分歧。
*   **成本：** 检查回测遗漏的手续费、滑点和价差，详见[手续费、滑点和杠杆](/zh/features/backtesting/fees-slippage-and-leverage/)。

订单已到执行阶段，却未按要求成交时，可启用 `log=True` 并检查
`pf.logs.readable`。日志展示请求订单、执行结果及前后账户状态，帮助区分资金不足与数量限制导致的减单，无须从净值曲线猜测。

[实时模拟](/zh/features/backtesting/live-simulation/)在新数据上逐根继续回测，[日内与逐笔回测](/zh/features/backtesting/intraday-and-tick-backtesting/)则在各根内部用更细数据检验成交。

!!! tip "结果好得不寻常时"
    先在图上查看几笔交易。若恰在 K 线最低或最高价成交，检查订单是否在达到该价格前已存在。超过 80% 的胜率或远超市场的收益值得深入检查，但单凭其中任何一项都不能证明策略偷看未来。
