4. 交易日历的查询与使用
在交易系统中,交易日历不是一个静态的“节假日表”,而是贯穿行情接入、订单校验、清算交收、任务调度的基础时钟。它决定了“今天能不能下单”“这笔交易算哪一天”“结算日落在哪一天”。本节围绕交易日历的查询与使用,从API设计、缓存策略到性能优化逐层展开,帮助你把一个看似简单的查询接口做成稳定、低延迟、可扩展的基础服务。
一、核心原理:交易日历到底在回答什么问题
交易日历的本质是一组带属性的日期集合,常见查询语义包括:某日是否为交易日、某日属于哪个交易市场、两个日期之间的交易日序列、某日之后第N个交易日、某日之前第N个交易日、某笔交易对应的交收日(T+1、T+2)等。不同市场(A股、港股、、期货夜盘)规则不同,因此日历必须按“市场+日期”维度建模,而不是全局一张表。
从数据结构看,最有效的表示方式是“有序交易日数组 + 日期到索引的哈希映射”。交易日数组保证序列查询和偏移查询是O(1)或O(log n),哈希映射保证单日判断是O(1)。节假日、半日市、临时休市等特殊状态可作为附加属性挂在日期节点上。
二、API设计:把查询语义收敛成稳定契约
API设计的第一原则是“语义明确、参数可组合、返回可预测”。建议按查询类型拆分为独立方法,而不是用一个万能接口塞入大量可选参数。典型接口如下:
// 单日判断
boolean isTradingDay(String market, LocalDate date);
// 区间交易日列表
List<LocalDate> tradingDaysBetween(String market, LocalDate start, LocalDate end);
// 偏移查询:从某日起第n个交易日,n可为负
LocalDate shiftTradingDay(String market, LocalDate base, int n);
// 交收日计算
LocalDate settlementDate(String market, LocalDate tradeDate, int tPlus);
// 批量查询,减少网络往返
Map<LocalDate, Boolean> batchIsTradingDay(String market, List<LocalDate> dates);
参数设计上,market必须显式传入,避免默认市场带来的隐性错误;日期统一使用LocalDate,不要用字符串或时间戳混用。返回结果建议封装为带版本号的对象,例如{version, market, data},便于调用方判断数据新鲜度。
三、缓存策略:多级缓存与失效机制
交易日历数据具有“读多写少、按年稳定、偶发变更”的特点,非常适合缓存。推荐采用本地缓存 + 分布式缓存 + 持久层的三级结构。本地缓存使用Caffeine或Guava Cache,设置合理的最大条目数和过期时间;分布式缓存使用Redis,key按calendar:{market}:{year}组织。
| 层级 | 存储介质 | 典型TTL | 适用场景 |
|---|---|---|---|
| L1本地缓存 | Caffeine / Map | 5~30分钟 | 单日判断、高频偏移查询 |
| L2分布式缓存 | Redis | 1天 | 跨节点共享、批量查询 |
| L3持久层 | MySQL / 文件 | 永久 | 冷启动、缓存穿透兜底 |
失效策略是缓存设计的难点。交易日历的变更通常来自交易所临时公告,例如台风休市、临时调整。此时不能等待TTL自然过期,而应通过版本号机制主动失效:每次日历数据更新,版本号递增,各节点在查询时比对版本号,不一致则重新加载。版本号可存放在配置中心或Redis中,读取成本极低。
四、性能优化:从O(n)到O(1)的关键手段
性能优化的核心是减少重复计算和内存占用。首先,将某市场某年的交易日预计算为有序数组,并建立Map<LocalDate, Integer>索引。这样单日判断是O(1),偏移查询通过索引加减再取值,也是O(1)。区间查询则通过两次二分查找定位起止索引,再截取子数组,复杂度为O(log n + k)。
// 预计算结构示例
class MarketCalendar {
List<LocalDate> tradingDays; // 有序交易日
Map<LocalDate, Integer> indexMap; // 日期 -> 索引
int version;
}
// 偏移查询 O(1)
LocalDate shift(LocalDate base, int n) {
Integer idx = indexMap.get(base);
if (idx == null) return null;
int target = idx + n;
if (target < 0 || target >= tradingDays.size()) return null;
return tradingDays.get(target);
}
其次,控制内存占用。一个市场10年的交易日约2500条,每条LocalDate加索引开销约几十字节,整体不过百KB级别,完全可常驻内存。若市场数量多,可按需加载,避免启动时全量加载造成冷启动缓慢。
第三,批量查询合并。服务端收到批量日期后,先查本地缓存,未命中的日期合并为一次持久层查询,再统一回填。这样可将N次IO压缩为1次。
五、案例与复盘:一次临时休市引发的缓存故障
某交易系统在台风日收到交易所临时休市通知,但日历服务仍使用前一日加载的缓存,导致风控模块认为当天是交易日,继续接受订单,最终产生大量废单。复盘发现三个问题:一是缓存TTL设置为24小时,未考虑盘中变更;二是没有版本号机制,无法主动失效;三是调用方没有对“临时休市”状态做二次校验。
改进方案包括:引入版本号并在配置中心监听变更;将临时休市标记为高优先级事件,触发全节点缓存刷新;在API返回中增加status字段,区分正常交易日、周末、法定节假日、临时休市;调用方对临时休市状态执行拒绝下单逻辑。经过改造,同类故障未再发生。