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 跨日持仓标签对齐流程图
3.5 标签口径校准清单
- 入场价格引用:始终使用
self.data.open[0]或order.executed.price(Backtrader 自动取开盘价) - 出场价格引用:始终使用
self.data.close[0](当前 Bar 收盘价) - 跨日持仓记录:使用
self.entry_price和self.entry_bar变量保存入场信息 - 盈亏计算:出场时用
order.executed.price - self.entry_price,确保跨 Bar 正确 - 滑点与手续费:在
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 会自动维护持仓均价。如果策略涉及加仓/减仓,入场标签应使用加权平均价格,而非单次开盘价。