2. 交易日历的数据结构解析

上一节我们明确了交易日历在量化数据体系中的基础地位。本节要把视角从“为什么需要”切换到“它长什么样”,也就是拆解交易日历的数据结构。理解字段构成与编码规则,是后续正确加载、对齐和校验日历数据的前提;如果字段含义理解偏差,哪怕只有一个日期类型判断错误,都可能导致回测中某笔订单被错误地允许或拒绝。

一、一条日历记录由哪些字段构成

交易所日历通常以“每个自然日一条记录”的方式组织,而不是只列出交易日。这样做的原因是:只有把非交易日也显式记录下来,才能完整表达“某天为什么不能交易”。一条典型的日历记录包含以下字段:

  • 日期(date):自然日,通常为 YYYY-MM-DD 格式,是记录的主键。
  • 是否交易日(is_trading_day):布尔值,最核心的字段,直接决定该日能否撮合与清算。
  • 日期类型标识(day_type):枚举值,用于区分工作日、周末、法定节假日、调休工作日等。
  • 交易所代码(exchange):标识该日历属于上交所、深交所、北交所或银行间市场等,不同交易所的休市安排可能不同。
  • 前一交易日(prev_trading_day)与后一交易日(next_trading_day):冗余但极实用的指针字段,便于快速定位相邻交易日。
  • 所属交易周/月/年(week、month、year):用于按周期聚合统计。
  • 节假日名称(holiday_name):如“春节”“国庆节”,便于人工核对与展示。

需要强调的是,is_trading_day 与 day_type 存在冗余关系,但二者不能互相替代。前者是面向撮合逻辑的最终判断,后者是面向解释与审计的语义标签。工程上应保证二者一致,并在数据加载时做交叉校验。

二、日期类型标识的编码设计

日期类型标识是日历数据中最容易被低估的字段。一个实用的枚举通常至少包含以下几类:

编码含义是否交易日典型场景
TRADING正常交易日是普通周一至周五
WEEKEND周末休市否周六、周日
HOLIDAY法定节假日否春节、国庆
MAKEUP_WORKDAY调休工作日视交易所而定周末被调为上班日
SUSPENDED临时休市否极端天气、系统故障

这里有一个关键陷阱:调休工作日并不必然等于交易日。以中国证券市场为例,国务院办公厅公布的调休安排中,部分周六周日被调为“上班日”,但证券交易所并不因此开市。也就是说,MAKEUP_WORKDAY 在银行间市场可能是交易日,在沪深交易所却往往仍是休市日。因此,日期类型必须与交易所维度联合判断,不能仅凭“是否上班”推断“是否可交易”。

三、节假日与调休规则的数据表达

节假日规则有两种常见表达方式。第一种是枚举式:直接为每一年生成完整的日期清单,逐日标注类型。这种方式简单直接、查询快,但每年需要更新一次数据。第二种是规则式:用“固定日期 + 农历换算 + 调休偏移”的规则动态推导。规则式灵活,但实现复杂,且遇到临时调整时容易失效。

工程实践中更稳妥的做法是“规则生成 + 人工校准”的混合模式:先用规则批量生成候选日历,再对照交易所官方公告逐条修正,最后固化为枚举数据入库。下面是一段示意性的数据结构定义,用于表达单条日历记录:

{
  "date": "2025-02-08",
  "exchange": "SSE",
  "is_trading_day": false,
  "day_type": "MAKEUP_WORKDAY",
  "holiday_name": "春节调休上班",
  "prev_trading_day": "2025-02-07",
  "next_trading_day": "2025-02-10",
  "week": 6,
  "month": 2,
  "year": 2025
}

注意这条记录:2025年2月8日是周六,被调为上班日,但上交所并不开市,因此 is_trading_day 为 false,而 day_type 仍保留 MAKEUP_WORKDAY 以说明其“上班但不开市”的特殊身份。这正是字段冗余设计的价值所在。

交易日历记录结构与日期类型判定关系 单条日历记录字段 date(自然日主键) exchange(交易所代码) is_trading_day(是否交易日) day_type(日期类型标识) prev / next_trading_day holiday_name(节假日名称) week / month / year(周期聚合) 联合判定 日期类型 → 是否可交易 TRADING 正常交易日 → 可交易 WEEKEND 周末 → 不可交易 HOLIDAY 法定节假日 → 不可交易 MAKEUP_WORKDAY 调休上班 → 需按交易所判定 SUSPENDED 临时休市 → 不可交易 调休工作日的关键分歧 银行间市场:可能开市 → is_trading_day = true 沪深交易所:通常休市 → is_trading_day = false

四、加载与校验的实操要点

拿到日历数据后,第一步不是直接使用,而是做三项校验。第一,连续性校验:确认日期序列无缺失、无重复,覆盖目标回测区间。第二,一致性校验:检查 is_trading_day 与 day_type 是否矛盾,例如出现 HOLIDAY 却标记为可交易。第三,指针校验:验证每条记录的 prev_trading_day 与 next_trading_day 确实指向相邻交易日,防止因数据拼接产生断链。

一个常见的实现方式是把日历加载为以日期为索引的字典,再封装两个查询函数:is_trading(date) 返回布尔值,shift(date, n) 返回向前或向后偏移 n 个交易日的日期。后者在计算“T+1 交割”“持有 5 个交易日”等逻辑时几乎无处不在。实现 shift 时优先使用预计算的交易日序列做二分查找,而不是逐日循环判断,这样在长区间回测中性能差异非常明显。

建议在数据入库时同时保存“原始公告版本号”和“生效区间”。交易所偶尔会临时调整休市安排,保留版本信息可以让历史回测复现当时真实可用的日历,而不是被事后修订的数据污染。

总结来看,交易日历的数据结构并不复杂,但字段之间的语义关系需要精确把握:day_type 解释“这一天是什么”,is_trading_day 回答“这一天能不能交易”,而交易所维度则是连接二者的必要上下文。把这三者理清,后续的停复牌对齐、行情切片和订单撮合才有可靠的时间基准。