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

资本市场与清结算

「今天」是哪一天,各系统说法不同

在 TT Lab 中继续学习

一句话总结

交易日不是日历日,交易时段因市场而异,而在有夏令时的市场里,同一个当地时间会出现两次,或者根本不存在。如果把这三件事全都用固定偏移量糊弄过去,那么每年两次、每逢切换的周末,都会悄悄出错。

为什么需要它

处理时间的代码,通常要写两次。第一次写的时候运转良好,在切换的周末被修一次。这中间的几个月里,没有人知道有问题,原因是错误的值不会抛出异常,只是带着一小时的偏差原样存了下来。

在资本市场里,这种偏差会直接触及金钱和报告。如果收盘批处理提前一小时运行,这期间的成交就会被推到第二天,对账会带着偏差被对平。而且,各市场的切换日期不同。每年总有几个星期,美国市场已经改成夏令时,而欧洲市场仍处于冬令时,在这几个星期里,两个市场的收盘间隔与平时不同。系统没有理由可以不知道这一点。

工作原理

第一,用规则而不是偏移量来转换。把当地时间转换成 UTC,不是“减去九小时”。必须看那个地区在那一天处于什么规则之下。这些规则的原始来源是 IANA 时区数据库,在 Python 里,由 zoneinfo 来读取它。这个模块进入标准库的背景,写在 PEP 615 里。如果把固定偏移量写死在代码里,切换日之后的所有记录都会差一小时。

第二,在切换日有墙上时钟不成立的瞬间。在春天把时钟拨快的市场里,被跳过的那一小时是整个不存在的。写着那个时间的记录,无法按当地时间读取,如果“先转换了再存”,就会悄悄变成一小时之后的值。在秋天拨回去的市场里,同一个墙上时钟的值会经过两次,所以仅凭这个时间,无法确定是两个瞬间中的哪一个。Python 用 fold 属性来区分这第二种情况,但用哪一边,是业务必须定下来的规则,而不是由库来决定的。

第三,交易时段和日历是单独的数据。常规交易时段的前后,有盘前和盘后的阶段,有半日市,有休市日。如果把它们作为常量写死在代码里,每到新的一年都需要部署。把日历作为数据放着,还有一个好处,就是可以对这份数据本身进行校验。半日市的收盘时间写得比常规收盘晚,或者同一天既是休市日又被写成了半日市,这样的事是真的会发生的。

第四,交易日是一个标签。在跨过午夜的夜盘里,凌晨一点的成交,按日历是今天,按交易日却是昨天。如果不确定这个标签,同一笔成交会被计入哪一天的汇总,每个系统都不一样,对账永远对不上。表示法本身的规则由 RFC 3339 来定,但交易日要怎么看,是市场的规则。

在现场相遇的样子

最常见的事故,是切换周末之后的收盘批处理。如果把某个市场的收盘固定为 UTC,那么切换之后,批处理就会在该市场仍然开市的时间运行。反过来,如果只按当地时间设定,就会与别的市场的批处理在同一瞬间重叠,两个作业为了同一个资源发生冲突。实际上,有一段时间,在夏令时期间,两个市场的收盘恰好落在同一个 UTC 时刻,只有那几个星期,批处理会被推后。

第二常见的,是没有偏移量的时间字符串。如果日志和报文里只写着 2025-03-30 01:30:00,那它属于哪个时区,就得从文档里去取。而且还必须处理这个时间在那个时区里可能并不存在这件事。

第三种,是日历写在代码里的情形。如果把休市日和半日市写成常量,每到新的一年都需要部署,而在紧急修改的时候,就没有人知道这个值是从哪里来的了。把日历抽成数据,从那时起就可以对日历本身进行校验。同一天既是休市日又被写成半日市,或者半日市的收盘比常规收盘还晚,这样的事是真的会发生的,而这样的一行,就足以改变那一天的整个汇总。这项校验只需要几行,可是如果日历在代码里,连写这几行的地方都没有。

而且,所有这些处理有一条共同的原则。时间在收到的那一刻只解释一次,此后一律只以绝对时间流转。如果带着当地时间的字符串到处走,需要时再解释,同一个值在系统的各层就会被读成不同的样子。只有在显示到画面上时,才重新转换成当地时间,那时是哪个市场的当地时间,由那个画面的上下文来确定。

下一项实验要做什么

做出四个市场的交易时段定义和日历,以及三十笔成交,并用时区规则把它们转换成 UTC。与用固定偏移量转换的结果作比较,找出出现偏差的日子,区分切换日里不存在的时间和出现两次的时间,然后标出交易时段阶段和交易日标签。最后,把各市场的收盘汇总成 UTC,找出批处理窗口重叠的日子,并找出日历本身的两种矛盾。