TT Lab
开始
学习 学习路径 课程

银行现场的语言

每个系统数出来的“昨天的交易”都不一样

在 TT Lab 中继续学习

一句话总结

时刻(instant)与日历日期是两回事。银行的结算日,是把时刻放到某个时区的日历上,再套用截止时间和营业日历计算出来的值。

为什么需要它

假设有人问“昨天的交易有多少笔”,三个系统给出了三个不同的数字。一个按 UTC 零点截断,一个按 KST 零点截断,一个按前一天的截止时间到今天的截止时间来统计。三个都按各自的标准是对的。所以这种偏差不会被当作故障,而是被当作“统计有点不一样”埋掉。它埋在那里,最后在结算对账时爆发。

原因几乎总是一样的。数据里只有受理时刻,而人和规定说的是日期。时刻在地球上无论哪里量都只有一个,但这个时刻是几号,必须先定下时区,才有答案。如果这个转换在代码里到处临时去做,转换规则就会和代码的个数一样多。

银行还有一个特殊情况。资金实际划转的那一天,也就是结算日(value date),可能与受理的那一天不同。截止时间(cutoff)之后收到的请求会顺延到下一个营业日,如果第二天是星期六或休业日,还要再顺延。所以结算日不是存下来的事实,而是规则的产物。

工作原理

设计分成三层。

  1. 存储用时刻。受理时间用 UTC,以 RFC 3339 的 2026-07-02T07:30:00Z 形式留下。RFC 3339 把没有偏移量的时间称为“本地时间”,并明确指出,仅凭它无法确定一个时刻。一旦把没有偏移量的字符串放进 DB,这一列就不再是时刻了。
  2. 显示和分桶用本地时间。Python 的 zoneinfo 会读取操作系统的 IANA 时区数据库,生成 ZoneInfo("Asia/Seoul")。用 astimezone() 转换后再取 .date(),就能得到这个时刻在首尔的日历上是几号。与固定加 9 小时的代码结果产生分歧的地方,正是这里。
  3. 结算日用规则。如果本地时间的时分大于等于截止时间,就加一天,从那个日期开始寻找营业日并向后顺延。营业日的判定来自周末和休业日的日历。

固定偏移量为什么危险,用韩国的资料就能直接看出来。Asia/Seoul 现在固定是 UTC+9,但并不是一直如此。在实验镜像里用 zoneinfo 亲自量一下,结果是这样。

1987-05-15 12:00 Asia/Seoul  ->  UTC+10 (KDT)   # 실습 이미지에서 실측
1987-12-15 12:00 Asia/Seoul  ->  UTC+9  (KST)   # 실습 이미지에서 실측
2026-07-15 12:00 Asia/Seoul  ->  UTC+9  (KST)   # 실습 이미지에서 실측
전환 순간: 1987-05-10 02:00 에 +10 으로, 1987-10-11 03:00 에 +9 로 (1988년도 같은 모양)

1987 年和 1988 年夏天韩国实行过夏令时,这一事实原样保存在 tzdb 里,所以 zoneinfo 会在那个区间返回 UTC+10。固定加 9 小时的代码,会把这个区间的迁移数据逐条读错一小时。如果是午夜附近的交易,日期会整整错一天。tzdb 的设计文档 写道,时区的定义会随着政治决定不断变化,过去的记录一旦有了新的发现也会被修正。所以规则应当放在数据里,而不是代码里,而这份数据由 IANA 时区数据库 来更新。

把日期运算交给 DB 时,也有同样的陷阱。SQLite 的 日期和时间函数 提供了 localtime 修饰符,但它是服务器的本地时间,并不是业务规定的时区。如果不想做出一个只因容器的 TZ 设置就会改变结算日的系统,就应当把时区作为配置值明确写出,由应用程序来做转换。

在现场相遇的样子

最常见的事故,是“把 UTC 日期直接当作业务日期”。在开发环境里,只在一整天 UTC 和 KST 日期看起来一样的时间段测试,到了生产环境,晚上 9 点(= UTC 12 点)之后的交易累积起来,问题才暴露出来。也就是 KST 09:00 之前,即 UTC 零点前后的交易,被算到了前一天。

第二种,是用 UTC 来比较截止时间。像 received_utc[11:16] >= "16:30" 这样的比较,现在也许碰巧答案是对的,但一旦用同一段代码去处理时区不同的对方机构的截止时间,就会崩溃。比较必须转换成本地时间之后再做。

第三种,是对方的日期。从首尔在 7 月 3 日早晨发出的报文,在伦敦还是 7 月 2 日。如果把对方机构文件里的 value_date 按我们的日历去读并做对账,整整一天的量都会作为未结算留下来。datetime 文档 所区分的 aware 与 naive 的差别,就是在实务中变成金钱的地方。

实际工作中真正重要的事

下一项实验要做什么

亲手做出十二天的转账受理账本,数一数现在的核心系统所使用的“UTC 日期 = 结算日”规则,会让多少笔出错。做出读取休业日日历和截止时间的结算日计算引擎,依次加上营业日加减、截止时间判定和对方时区转换,并在 1987 年和 1988 年的迁移数据里找出夏令时区间。最后,不覆盖账本,对全部笔数重新计算结算日,把什么为什么变化写成报告提交。