# 日内与逐笔回测 (/zh/features/backtesting/intraday-and-tick-backtesting)

一根 K 线只告诉您开盘、最高、最低和收盘价。日内策略的止损较紧时，内部价格路径决定结果。VBT 可在 Python 中基于任意大小的 K
线、不规则时间戳及交易时段进行日内与逐笔回测，并用比信号更细的数据判断止损。

```python title="Resolve the same stops on hourly and on minute bars"
>>> minute_data = vbt.GBMOHLCData.pull(  # (1)
...     "SYN",
...     start="2024-01-01",
...     end="2024-02-01",
...     timeframe="1min",
...     std=0.0001,
...     seed=42,
... )
>>> hourly_data = minute_data.resample("1h")
>>> hourly_entries = hourly_data.symbol_wrapper.fill(False)
>>> hourly_entries.iloc[::4] = True  # (2)

>>> minute_entries = minute_data.symbol_wrapper.fill(False)
>>> entry_minutes = hourly_entries.index[hourly_entries] + pd.Timedelta(minutes=59)
>>> minute_entries[entry_minutes] = True  # (3)

>>> kwargs = dict(sl_stop=0.002, tp_stop=0.002)
>>> hourly_pf = vbt.PF.from_signals(hourly_data, hourly_entries, **kwargs)
>>> minute_pf = vbt.PF.from_signals(minute_data, minute_entries, **kwargs)

>>> def summary(pf):
...     stop_types = pf.orders.readable["Stop Type"].value_counts()
...     return pd.Series({
...         "trades": pf.trades.count(),
...         "stop losses": stop_types.get("SL", 0),
...         "take profits": stop_types.get("TP", 0),
...         "win rate": round(pf.trades.win_rate, 3),
...         "total return": round(pf.total_return, 3),
...     })

>>> pd.DataFrame({"hourly bars": summary(hourly_pf), "minute bars": summary(minute_pf)})
              hourly bars  minute bars
trades            186.000      186.000
stop losses       134.000      102.000
take profits       52.000       84.000
win rate            0.280        0.452
total return       -0.152       -0.036
```

1.  一个月固定随机种子的合成分钟 K 线，典型小时振幅约 0.8%。
2.  每隔四小时在收盘时入场。
3.  分钟数据上的相同入场：在各入场小时最后一分钟的收盘入场。

两次运行都有相同 186 笔交易，使用相同的 0.2% 止损和止盈。小时 K 线中，许多根同时触及两者，无法判断先后时，模拟器假定止损先发生。分钟 K 线能区分更多此类情况，胜率从 28% 升至
45%，更接近随机游走配对称止损本应产生的 50%。

## 逐笔与不规则 K 线 \[#ticks-and-irregular-bars]

引擎不假设固定 K 线大小。逐笔价格作为 `close` 传入，每行作为独立时刻处理，无须开盘、最高和最低价。其他地方构建的成交量 K 线、价格区间 K 线、金额 K 线和成交笔数 K
线也同样适用，只需索引有序：

```python title="Trade on ticks with irregular timestamps"
>>> index = pd.date_range("2024-01-01 09:30", periods=8, freq="1s").vbt.randomize(
...     step="100ms",
...     seed=42,
... )
>>> prices = [100.0, 100.2, 100.1, 100.4, 100.3, 100.6, 100.5, 100.7]
>>> ticks = pd.Series(prices, index=index)
>>> ticks.index.to_series().diff().dt.total_seconds().tolist()
[nan, 0.8, 1.6, 1.2, 0.8, 0.6, 1.0, 1.0]

>>> entries = pd.Series([True] + [False] * 7, index=index)
>>> exits = pd.Series([False] * 6 + [True, False], index=index)
>>> pf = vbt.PF.from_signals(ticks, entries, exits)
>>> pf.orders.readable[["Fill Index", "Side", "Price"]]
           Fill Index  Side  Price
0 2024-01-01 09:30:00   Buy  100.0
1 2024-01-01 09:30:06  Sell  100.5
```

夏普比率等年化度量需要频率，而不规则索引没有固定频率。可显式传入 `freq`，用 `freq="index_mean"` 取时间戳平均间隔，或将投资组合收益重采样到规则网格再分析。时长按 K
线数量乘频率计算，因此采用平均间隔时只是估计。一年按多长计算也很重要，详见[收益率](/zh/features/analytics/performance-metrics/#returns)。

构建测试用合成逐笔数据时，`vbt.RandomOHLCData` 和 `vbt.GBMOHLCData` 生成价格，`.vbt.randomize()` 像上例一样将规则索引变成不规则时间戳。

!!! warning "逐笔窗口按行数，而非时间计数"
    多数指标以行数指定窗口。逐笔数据中的 20 行可能跨一秒或一小时，取决于活跃程度。可在时间 K 线上计算指标，或使用 `ticks.rolling("5min")`
    等时间窗口，再将结果对齐回逐笔索引。

## K 线内部 \[#inside-the-bar]

有三种方式观察 K 线内部。第一种就是上例：在交易周期上计算信号，在更细 K 线上模拟，使止损与限价以更高分辨率检查。报告时可将结果重采样回原始周期。

因此，小时策略可以使用分钟或逐笔价格执行，而无须将指标窗口改成分钟或逐笔。VBT
的对齐工具可将小时收盘信号放在该小时结束时，并只在日收盘后提供日线趋势过滤器。不同周期信号、过滤器与价格的组合，参见[多时间周期分析](/zh/features/data/multi-timeframe-analysis/)。

第二种是在同一根内下多个订单。信号模拟器每列每根最多处理一个订单；灵活订单函数则可返回任意数量订单，例如同根开盘买入、收盘卖出，详见[事件驱动回测](/zh/features/backtesting/event-driven-backtesting/#order-functions)。

第三种保留信号模拟器，将每根拆成三根子 K 线：开盘一根、中间区间一根、收盘一根。止损在前两根成交，自定义信号在最后一根执行，持仓便可在原来一根内平仓又重开。`pf.resample`
将结果映射回原始索引。

!!! info "实用示例"
    会员专享的[每根 K 线执行多个操作（英文）](https://members.vectorbt.pro/cookbook/portfolio/#perform-multiple-actions-per-bar)示例构建三根子
    K 线，再将组合重采样回原周期。

!!! warning "K 线无法告诉您的信息"
    一根 K
    线中只有开盘与收盘的时间先后已知。策略若依赖最高价与最低价谁先发生，应使用更细数据，或接受悲观假设。具体规则见[真实止损成交](/zh/features/backtesting/stop-loss-and-take-profit/#realistic-stop-fills)。

## 交易时段 \[#trading-sessions]

交易时段是索引上的掩码。将入场限制在时段内，并在每个时段最后一根添加离场，策略就能每晚空仓：

```python title="Trade a New York session and be flat at every close"
>>> data = minute_data.loc["2024-01-08":"2024-01-12"]
>>> local_index = data.index.tz_convert("America/New_York")
>>> local_time = local_index.time
>>> in_session = (local_time >= pd.Timestamp("09:30").time()) & (
...     local_time < pd.Timestamp("16:00").time()
... )
>>> session_end = local_time == pd.Timestamp("15:59").time()

>>> sma = data.close.rolling(30).mean()
>>> entries = data.close.vbt.crossed_above(sma) & in_session & ~session_end
>>> exits = data.close.vbt.crossed_below(sma) | session_end
>>> pf = vbt.PF.from_signals(data, entries, exits)

>>> print(pf.trades.count())
91
>>> pf.assets.groupby(local_index.date).last().tolist()  # (1)
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
```

1.  每个当地日期结束时的持仓。

没有价格的行不下单。使用 `price="nextopen"` 时，星期五信号若下一行是空的周末行，永远不会成交。应删除闭市行，例如拉取多个品种时使用
`missing_index="drop"`，使下一行就是下个时段开盘。

闭市期间出现的信号需要规则：可以丢弃，也可延续到下一时段第一根，使实盘持仓与回测对应。“入场当天不离场”等规则也可用掩码表示，将每个离场与此前入场日期比较。

若要构建只覆盖交易时段的 K 线，例如从 24 小时数据得到 09:30 至 16:00 的日线，应先过滤到交易时段，或在自定义边界之间重采样。

也可独立测试各时段。将等长时段放入不同列，每个时段使用新现金余额，比较不同日期的策略。这适合每日平仓策略，也可判断结果是否主要由少数时段贡献。

!!! info "教程"
    会员专享的[多时间周期分析（英文）](https://members.vectorbt.pro/tutorials/mtf-analysis/aggregation/)教程使用自定义边界，将日内数据聚合为交易时段 K 线。

## 更长的日内历史 \[#longer-intraday-histories]

先用逐笔数据模拟，再缩放观察全局。将已完成投资组合重采样为分钟、小时或日线分析。VBT
将订单记录映射到新索引，并在该频率重建净值与收益，使整体结果需要处理和绘制的数据更少。保留原始投资组合，以检查逐笔成交。

连续分批处理历史时，投资组合也可携带现金、持仓与止损继续下一批。[实时模拟](/zh/features/backtesting/live-simulation/)使用的同一机制，让历史运行跨越批次边界，无须每次边界都平仓。
