休市提醒日股将于 2026-11-03 休市(文化日),距今天还有 5 天。全部公告
文章

跨市场行情对齐的三个陷阱:时区、交易日历与停牌

跨市场的时间 bug 不会报错,只会静默出错。本文拆解三个陷阱——时间戳的时区语义、交易日历不等于工作日、停牌被误判为数据缺失,并给出统一 UTC 存储、交易所日历判断与可复用的 Python 实现。

单市场的数据处理很少出时间问题,因为所有数据天然共享一套时区和日历。一旦接入多个市场——美股、A 股、港股、日股、欧洲市场——时间对齐就会变成 bug 的高发区,而且这类 bug 往往不会报错,只是让结果静默地错掉。

这篇文章拆解跨市场时间对齐的三个陷阱,以及一套可落地的处理方案。

陷阱一:时间戳的时区语义

行情接口返回两种时间字段,语义不同,混用会直接错位:

  • timestampMs:REST 响应里的毫秒时间戳,是一个数值。数值本身没有时区概念,但它代表的是绝对时刻(UTC 基准)。
  • at:流式 tick 帧里的 RFC3339 时间字符串,带时区偏移,也是绝对时刻。

问题出在「本地化」这一步。如果代码里用「取当前本地时间」或「按服务器时区格式化」来处理,就会出现跨环境不一致——同一份数据在 UTC 服务器和东八区服务器上落库结果不同。

正确原则只有一条:存储一律用 UTC,只在展示层做时区转换。 不要在数据管道中间环节引入本地时区。

陷阱二:交易日历不是「工作日」

这是最容易被低估的一点。交易日历与自然日历的差异包括:

差异类型例子后果
周末不同中东部分市场周五、周六休市按「周一至周五」判断会漏掉或误判交易日
节假日不同各国节假日完全独立用本国节假日表会生成错误的时间轴
夏令时切换欧美市场开盘时间在本地时区上会平移一小时按固定本地时间对齐会错位一小时
半日市节假日前夕提前收盘缺数据被误判为数据缺失

夏令时是最隐蔽的一个。美股开盘在美东时间固定为 9:30,但换算到你所在时区后,会随夏令时切换平移。如果你的系统用「本地时间 21:30 开始拉取美股」这类硬编码,一年会有两次静默失效。

陷阱三:停牌与半日市造成的「数据缺失」

跨市场做数据质量校验时,最常见的误判是把正常的无交易当成数据缺失。

典型场景:某个港股当天停牌,K 线接口没有返回该日数据。如果校验逻辑是「交易日必须有数据」,就会持续告警;如果校验逻辑是「缺数据就向前填充」,就会用错误的历史价格污染下游计算。

正确的处理是把「无交易」显式建模为一种状态,而不是缺失:

  • 校验时区分「休市」「停牌」「数据缺失」三种情况,分别处理。
  • 停牌期间不要向前填充价格——填充会让回测产生虚假的连续持仓。
  • 在数据里保留 unavailable 这类显式标志,而不是用空值表示。

行情接口的响应里带有 unavailable 布尔字段,正是为了避免把「不可用」与「值为空」混为一谈,校验逻辑应该优先读它。

正确的时间对齐方案

把上面的原则整理成一套可执行的规则:

  1. 存储层统一 UTC:所有时间戳入库前转为 UTC,字段命名带 _utc 后缀以防误用。
  2. 用交易所日历而非自然日历:每个市场绑定自己的交易日历,包含节假日、半日市与夏令时规则。
  3. 时间轴按市场分别生成:不要用一张全局时间轴套所有市场,否则每个市场都会有大量伪缺失。
  4. 把无交易显式建模:休市、停牌、缺失三种状态分开存储与告警。
  5. 展示层再做本地化:只在最终渲染时按用户时区转换。
  6. 夏令时用规则而非硬编码:依赖时区数据库处理切换,不要写死偏移量。

Python 实现:跨市场时间对齐

Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 各市场的交易所时区(用 IANA 时区名,自动处理夏令时)
EXCHANGE_TZ = {
    "us": "America/New_York",     # 美股
    "hk": "Asia/Hong_Kong",       # 港股
    "cn": "Asia/Shanghai",        # A 股
    "jp": "Asia/Tokyo",           # 日股
    "uk": "Europe/London",        # 英股
    "de": "Europe/Berlin",        # 德股
}


def to_utc(ts_ms: int) -> datetime:
    """REST 的 timestampMs 是绝对时刻,直接转 UTC。"""
    return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)


def parse_rfc3339(value: str) -> datetime:
    """流式 tick 的 at 字段是 RFC3339,Python 3.11+ 可直接解析。"""
    return datetime.fromisoformat(value).astimezone(timezone.utc)


def local_session_time(market: str, moment_utc: datetime) -> str:
    """把 UTC 时刻换算到交易所本地时间,用于判断是否在交易时段。"""
    tz = ZoneInfo(EXCHANGE_TZ[market])
    return moment_utc.astimezone(tz).strftime("%Y-%m-%d %H:%M:%S %Z")


def classify(quote: dict) -> str:
    """区分休市 / 停牌 / 缺失 / 正常,而不是一律当缺失。"""
    if quote.get("unavailable"):
        return "unavailable"          # 停牌或不可用,显式标志
    if quote.get("last") is None:
        return "missing"              # 真的没有值,需要告警
    return "ok"


# 示例:同一 UTC 时刻在三个市场的本地时间
moment = to_utc(1789779600000)
for m in ("us", "hk", "jp"):
    print(f"{m:3} {local_session_time(m, moment)}")
# us  2026-09-18 21:00:00 EDT
# hk  2026-09-19 09:00:00 HKT
# jp  2026-09-19 10:00:00 JST

关键点是使用 IANA 时区名(如 America/New_York)而不是固定偏移量。zoneinfo 会自动应用夏令时规则,所以「美东 9:30 开盘」这类判断在全年都是正确的。

数据质量校验清单

  1. 时间戳单调性:同一标的的时间戳应严格递增,出现回退说明数据源或合并逻辑有问题。
  2. 交易日覆盖:对照交易所日历检查交易日数量,而非自然日。
  3. 无交易比例:统计 unavailable 占比,异常升高通常对应停牌或数据源故障。
  4. 时区一致性:抽查入库时间戳,确认没有混入本地时间。
  5. 夏令时边界:在切换日前后各抽查一天,确认开盘时间换算正确。

常见问题

为什么不能用「周一至周五」判断交易日?

因为部分市场的周末安排不同,且节假日完全独立。用自然日历判断会产生大量误判,必须使用交易所日历。

停牌期间的价格应该向前填充吗?

不应该。向前填充会让回测产生虚假的连续持仓与零波动区间,从而高估策略表现。正确做法是把停牌显式标记为无交易状态。

夏令时切换会导致什么具体问题?

最常见的是定时任务错位:按本地时间硬编码的拉取时间会在切换后偏移一小时,导致开盘初段数据缺失或重复拉取。用 IANA 时区名可以自动规避。

多市场数据应该用一张时间轴还是分市场?

分市场。每个市场有自己的交易日历与交易时段,强行对齐到同一张时间轴会产生大量伪缺失,也会让校验规则失效。

怎么区分「停牌」和「数据缺失」?

优先读响应里的 unavailable 字段——它是显式标志,专门用于避免把「不可用」与「值为空」混为一谈。只有在没有该标志且值为空时,才按缺失处理并告警。

需要额外存一份交易所本地时间吗?

计算上不需要。存 UTC 时间戳加市场标识就够了——交易所时区可以从市场代码推导出来。额外存一份本地时间只会制造两者不一致的机会,而且一旦不一致,很难判断该信任哪一个。

下一步

跨市场时间对齐的核心是两件事:存储层统一 UTC,判断层用交易所日历。交易日历可先用 交易日历工具 核对,多市场接入的整体设计见 多市场集成指南,具体市场的交易时段可查阅 港股行情 与 英股行情 页面,时间字段定义见 多市场行情接口文档。