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

银行现场的语言

做一个计算起息日的引擎

在 TT Lab 中继续学习

目标

把受理时刻与日历日期分开,做出一个根据休业日日历和截止时间来计算结算日的引擎。要正确处理对方机构的时区,以及 1987 年和 1988 年首尔的夏令时区间,并且不覆盖账本,对全部笔数重新计算被错误标记的结算日。

为什么重要

时刻只有一个,但这个时刻是几号,要定下时区才有答案。在银行,再加上截止时间和营业日历,资金实际划转的日期就不再是存储下来的事实,而成了规则的产物。如果这个计算散落在代码的许多地方,“昨天的交易”的定义就会因系统而异,这种偏差不会被当作故障,而是一直留到结算对账。 休业日和截止时间逐年变化,所以应当放在数据里,而不是代码里。因此本实验的引擎要从通过 --db 接收的 SQLite 中读取日历和配置。 评分器不会相信你写出来的文字。它会放入由它自己做出的休业日日历和截止时间,挂上一个临时 DB 来运行你的引擎,并把判定与它自己算出的值对照。把答案写死的引擎无法通过。

步骤

  1. 创建并运行 /root/bizday/gen_bizday.py,生成 /root/bizday/txn.db。其中包含 300 笔转账受理、48 笔迁移交易、6 天休业日和 3 行配置。
  2. 把 UTC 日期与 KST 日期出现分歧的交易写入 /root/bizday/kst_shift.csv,并把三个数字写入 /root/bizday/bucket.txt。
  3. 在 /root/bizday/bizday.py 中实现 is-bizday、next-bizday 和 add-bizdays。日历从 --db 的 holiday 表中读取。
  4. 给 bizday.py 增加 value-date。截止时间和时区从 sys_param 中读取。
  5. 给 bizday.py 增加 localize,并把在首尔和伦敦日期出现分歧的交易写入 /root/bizday/counterparty.csv。
  6. 把 48 笔迁移交易连同本地时间和偏移量写入 /root/bizday/legacy_offsets.csv,并把三个数字写入 /root/bizday/dst_seoul.txt。
  7. 不要覆盖账本的 value_date,而是在 txn.db 中创建 value_date 表,重新计算 300 笔,并只把发生变化的交易写入 /root/bizday/redate.csv。
  8. 把重新计算后按结算日汇总的结果写入 /root/bizday/valuedate_summary.csv,把报告写入 /root/bizday/bizday_report.md。

参考

生成受理账本快照

创建并运行 /root/bizday/gen_bizday.py,生成 /root/bizday/txn.db。其中包含 300 笔转账受理、48 笔迁移交易、6 天休业日和 3 行配置。

共有四张表。txn 的 received_utc 形如 2026-06-29T01:02:03Z,而 value_date 按照现在核心系统的规则,直接使用 received_utc 的前 10 个字符。这条规则是错的,这就是本实验的出发点。

把时刻与日历日期分开

把 UTC 日期与 KST 日期互不相同的交易,连同 txn_id,received_utc,utc_date,kst_date 表头写入 /root/bizday/kst_shift.csv,并把 shift_count=、utc_days= 和 kst_days= 写入 /root/bizday/bucket.txt。

用 datetime.fromisoformat 读取时刻,用 astimezone(ZoneInfo("Asia/Seoul")) 转换后再取 .date()。顺序不能颠倒。utc_days 和 kst_days 分别是分桶后得到的互不相同的日期的个数。

把营业日历移进引擎

在 /root/bizday/bizday.py 中实现 is-bizday、next-bizday 和 add-bizdays。休业日要从通过 --db 接收的 DB 的 holiday 表中读取。

周末是 weekday() >= 5,休业日来自表。reason 是 bizday、weekend、holiday 三者之一。评分器会挂上一个装着与学习者 DB 不同的休业日的临时 DB 来运行,所以把日历写死在代码里就会失败。

把截止时间之后的请求顺延到下一个营业日

给 bizday.py 增加 value-date。时区和截止时间要从 sys_param 中读取,大于等于截止时间就加一天,再向后顺延到营业日。

截止时间的比较要在转换成本地时间之后进行。如果与 UTC 时间字符串比较,在曾经实行夏令时的区间会逐条差一小时。边界是 >=——整点收到的请求会被顺延。评分器每次运行都会抽取不同的截止时间。

转换成对方机构的日历

给 bizday.py 增加 localize,并把在首尔和伦敦日期出现分歧的交易,连同 txn_id,received_utc,seoul_date,london_date 表头写入 /root/bizday/counterparty.csv。

utc_offset 写成 +09:00 这样,带符号的两位小时和分钟。local 必须是带偏移量的 RFC 3339 字符串——去掉偏移量,这个字符串就不再是一个时刻了。伦敦在夏天是 UTC+1。

找出 1987 年夏天的那一小时

把 48 笔迁移交易按 txn_id,received_utc,seoul_local,utc_offset 写入 /root/bizday/legacy_offsets.csv,并把 kdt_rows=、kst_rows= 和 date_shift_rows= 写入 /root/bizday/dst_seoul.txt。

Asia/Seoul 现在是 UTC+9,但在 1987 年和 1988 年的夏天实行过夏令时。zoneinfo 知道这一事实,所以不要自己计算,直接问它。date_shift_rows 是 KST 日期与 UTC 日期不同的迁移交易的笔数。

不覆盖账本,重新计算结算日

在 txn.db 中创建 value_date(txn_id, value_date) 表,重新计算 300 笔的结算日并放进去,只把与旧结算日不同的交易,按 txn_id,received_utc,old_value_date,new_value_date 写入 /root/bizday/redate.csv。不要动 txn.value_date。

不要重新实现规则,而是把 bizday.py 作为模块导入使用(把 /root/bizday 加入 sys.path 再 import)。如果把账本记录的旧结算日抹掉,就无法知道它是按哪条规则标出来的值。重新计算的结果放进新表里。

转成汇总和报告

把重新计算后按结算日统计的笔数和金额,按 value_date,txn_count,amount_sum 升序写入 /root/bizday/valuedate_summary.csv,并在 /root/bizday/bizday_report.md 中写下 ## 무엇이 틀렸나、## 결제일을 어떻게 계산하나、## 상대방 표준시、## 과거 서머타임、## 재발 방지 五个小节。

汇总可以通过把 value_date 表与 txn 连接得到。报告中要用数字和名称写出变化的笔数、截止时间、按哪个时区计算、对方机构的时区,以及 1987 年和 1988 年的偏移量。夹着休业日的日期在汇总里是缺失的,这一点也值得确认。