3. 开盘价与收盘价口径:开盘价作为入场标签、收盘价作为出场标签、跨日持仓的标签处理

在量化回测中,标签口径决定了策略信号与成交价格之间的映射关系。最常见的口径组合是:以开盘价作为入场标签、以收盘价作为出场标签。这种组合在日内交易和跨日持仓场景下,会衍生出不同的标签处理逻辑,直接影响回测结果的真实性。

3.1 开盘价入场 vs 收盘价出场的核心差异

维度 开盘价入场 收盘价出场
信号触发时间 当日开盘瞬间(Bar 起始) 当日收盘瞬间(Bar 结束)
成交确定性 高(集合竞价或开盘价可成交) 高(尾盘撮合或收盘价可成交)
滑点影响 较小(开盘流动性集中) 较小(收盘流动性集中)
跨日持仓 需处理隔夜跳空风险 需处理收盘前平仓或移仓
Backtrader 默认模式 order.executed.price 取开盘价 order.executed.price 取收盘价
关键认知:开盘价入场 + 收盘价出场,本质上是“信号在 Bar 起始执行,在 Bar 末尾退出”。这种口径天然适合趋势跟踪策略,因为它避免了盘中噪音,但必须处理跨日持仓时的标签对齐问题。

3.2 跨日持仓的标签处理困境

当策略持仓跨越多个交易日时,入场标签(开盘价)和出场标签(收盘价)不再属于同一个 Bar。例如:

  • 第 1 天开盘:以开盘价买入
  • 第 2 天收盘:以收盘价卖出

此时,入场标签属于第 1 天的 Bar,出场标签属于第 2 天的 Bar。如果直接使用 self.datas[0].close[0] 作为出场价格,会错误地引用第 1 天的收盘价,导致标签错位。

3.3 标准处理流程(Backtrader 实现)

class OpenEntryCloseExitStrategy(bt.Strategy):
    def __init__(self):
        self.entry_price = None  # 记录入场价格
        self.entry_bar = None    # 记录入场 Bar 索引

    def next(self):
        # 入场条件:以开盘价买入
        if not self.position and self.buy_signal():
            self.buy(price=self.data.open[0])
            self.entry_price = self.data.open[0]
            self.entry_bar = len(self)  # 记录当前 Bar 索引

        # 出场条件:以收盘价卖出(可能跨日)
        if self.position and self.sell_signal():
            # 关键:使用当前 Bar 的收盘价,而非入场 Bar 的收盘价
            self.sell(price=self.data.close[0])
            self.entry_price = None
            self.entry_bar = None

    def notify_order(self, order):
        if order.status == order.Completed:
            if order.isbuy():
                self.entry_price = order.executed.price
            elif order.issell():
                # 出场后计算盈亏,入场价与出场价可能跨 Bar
                pnl = order.executed.price - self.entry_price
                self.log(f'跨日出场 PnL: {pnl:.2f}')
常见错误:在出场时使用 self.data.close[self.entry_bar] 引用入场 Bar 的收盘价。这会导致出场价格错误,因为出场标签必须是当前 Bar 的收盘价,而非历史 Bar 的收盘价。

3.4 跨日持仓标签对齐流程图

跨日持仓标签对齐流程 跨日持仓标签对齐流程 第1天 Bar 开盘价入场 第2天 Bar 持仓中 第3天 Bar 收盘价出场 标签对齐规则: • 入场标签 = 第1天开盘价(Bar 起始) • 出场标签 = 第3天收盘价(Bar 结束) • 中间第2天无交易,持仓跨日,标签不产生 T+0 开盘 T+1 持仓 T+2 收盘

3.5 标签口径校准清单

  1. 入场价格引用:始终使用 self.data.open[0] 或 order.executed.price(Backtrader 自动取开盘价)
  2. 出场价格引用:始终使用 self.data.close[0](当前 Bar 收盘价)
  3. 跨日持仓记录:使用 self.entry_price 和 self.entry_bar 变量保存入场信息
  4. 盈亏计算:出场时用 order.executed.price - self.entry_price,确保跨 Bar 正确
  5. 滑点与手续费:在 notify_order 中统一处理,避免在 next 中重复计算

3.6 常见陷阱与避坑

陷阱 错误表现 正确做法
出场引用历史收盘价 self.data.close[self.entry_bar] 使用 self.data.close[0]
入场价被后续 Bar 覆盖 跨日持仓后入场价丢失 用 self.entry_price 持久化存储
忽略 Bar 索引偏移 出场信号触发时索引错误 用 len(self) 记录入场 Bar 索引
滑点重复计算 在 next 和 notify_order 都加滑点 仅在 notify_order 中处理一次
最佳实践:在策略初始化时,使用 self.add_timer() 或 self.datas[0].datetime 记录每个 Bar 的时间戳,便于调试跨日持仓的标签对齐问题。同时,在 notify_order 中打印入场和出场的 Bar 索引,验证标签是否跨日。

3.7 跨日持仓的标签处理扩展(多日持仓)

当持仓超过 2 天时,标签处理逻辑不变,但需要额外注意:

  • 入场标签:始终为第一个持仓日的开盘价
  • 出场标签:始终为最后一个持仓日的收盘价
  • 中间持仓日:不产生任何交易标签,但需要记录每日的持仓状态
# 多日持仓示例:持仓 5 天
def next(self):
    if not self.position and self.buy_signal():
        self.buy(price=self.data.open[0])
        self.hold_days = 0  # 重置持仓天数
    
    if self.position:
        self.hold_days += 1
        # 第 5 天收盘出场
        if self.hold_days >= 5:
            self.sell(price=self.data.close[0])
性能提示:跨日持仓时,Backtrader 的 self.position.size 和 self.position.price 会自动维护持仓均价。如果策略涉及加仓/减仓,入场标签应使用加权平均价格,而非单次开盘价。