休市提醒日股將於 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,判斷層用交易所日曆。交易日曆可先用 交易日曆工具 核對,多市場接入的整體設計見 多市場集成指南,具體市場的交易時段可查閱 港股行情 與 英股行情 頁面,時間字段定義見 多市場行情接口文檔。