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 以说明其“上班但不开市”的特殊身份。这正是字段冗余设计的价值所在。
四、加载与校验的实操要点
拿到日历数据后,第一步不是直接使用,而是做三项校验。第一,连续性校验:确认日期序列无缺失、无重复,覆盖目标回测区间。第二,一致性校验:检查 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 回答“这一天能不能交易”,而交易所维度则是连接二者的必要上下文。把这三者理清,后续的停复牌对齐、行情切片和订单撮合才有可靠的时间基准。