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

多市場看板數據架構:時段與時效性處理

多市場看板的難點不在前端圖表,而在數據層。本文拆解三個單市場看板不會遇到的問題——交易時段不重疊、刷新節奏不該統一、同一屏內時效性不一致,並給出按市場狀態驅動訂閱的架構與 Python 實現。

做一個「同時看美股、A 股、港股、外匯、加密」的監控看板,難點不在前端圖表,而在數據層如何把不同市場的數據拉到同一張頁面上。多市場看板會遇到三個單市場看板不會有的問題:交易時段不一致、刷新節奏不一致、數據時效性不一致。

這篇文章講這三個問題怎麼在架構上解決。

問題一:交易時段不一致

多市場看板最直接的體驗問題是:打開頁面時,一半的標的是「無數據」狀態。

原因很簡單——美股、A 股、港股、外匯、加密的交易時段幾乎不重疊。你在北京時間上午 10 點打開看板,A 股正在交易,美股已經收盤 10 小時,加密則在 7×24 運轉。

如果看板把「無數據」一律渲染成空白或 0,用戶會誤以為系統故障。正確的做法是把市場狀態作為一等數據建模:

狀態含義前端呈現
OPEN正在交易顯示實時價格,正常刷新
CLOSED休市顯示最後收盤價 + 休市標識
PRE / POST盤前 / 盤後顯示價格但標註時段
HALTED停牌顯示停牌標識,不顯示「最後更新」
UNAVAILABLE數據不可用明確的錯誤態,而不是空白

關鍵區別是:休市不是故障。 把休市渲染成空白,用戶會反覆刷新甚至報障;渲染成「收盤價 + 休市標識」,用戶就知道這是正常狀態。

問題二:刷新節奏不一致

看板的刷新策略不能「一刀切」,因為不同市場的合理刷新頻率差異極大:

  • 加密:7×24 交易,秒級刷新有意義。
  • 股票:僅在交易時段刷新,休市時應該停止請求。
  • 外匯:接近 7×24,但週末休市。

如果看板對所有標的一律秒級輪詢,會產生兩個後果:休市市場的請求全部是浪費,以及額度被無意義地消耗。以 200 個標的為例,全天候秒級輪詢是每月數億次請求,而其中大部分落在休市時段。

正確的架構是按市場狀態動態調整訂閱:

  1. 用交易所日曆判斷每個市場當前是否處於交易時段。
  2. 交易時段內:訂閱 WebSocket 流式推送,實時更新。
  3. 非交易時段:取消訂閱,只保留最後一次收盤快照。
  4. 市場開盤前若干分鐘:提前恢復訂閱,避免開盤瞬間的空窗。

這樣額度消耗與實際交易時段成正比,而不是與時鐘成正比。

問題三:時效性不一致

這是最容易被忽略、也最容易傷害用戶信任的一點。

看板上並排顯示的標可能來自不同時效的數據源:加密是實時的、美股是延遲的、某個市場可能是緩存快照。 如果前端不做區分,用戶會默認「這一屏都是實時數據」。

行情接口的響應裡帶有 delayed 與 unavailable 標誌,看板應該把它們一路透傳到界面:

  • 每個標的卡片上顯示數據時間,而不只是「最後更新於剛剛」。
  • 延遲數據用視覺標記區分(例如一個「延遲」角標)。
  • 同一屏內如果時效性差異較大,給出整體提示。

這條做法的價值在於避免用戶基於錯誤的時間假設做決策。在一個混合市場看板上,這一點比價格本身的精度更重要。

架構分層

把上面的原則落到架構上,可以分成四層:

層職責關鍵設計
接入層按市場狀態訂閱 / 退訂WebSocket + 交易所日曆驅動
狀態層維護每個標的最新快照內存或 Redis,帶時效性元數據
聚合層按看板視角組織數據支持按市場、按分組、按自選
推送層增量推給前端只推變化項,避免全量重繪

其中推送層只推變化項是關鍵優化。多市場看板往往有幾十到上百個標的,全量推送會讓前端每秒重繪整個頁面,而實際變化的可能只有兩三個。

Python 實現:按市場狀態驅動的訂閱管理

Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 各市場交易時段(交易所本地時間,簡化示例)
SESSIONS = {
    "us": ("America/New_York", "09:30", "16:00"),
    "hk": ("Asia/Hong_Kong", "09:30", "16:00"),
    "cn": ("Asia/Shanghai", "09:30", "15:00"),
    "crypto": (None, "00:00", "23:59"),          # 7×24
}

WARMUP_MINUTES = 5      # 開盤前提前訂閱


def market_state(market: str, now_utc: datetime | None = None) -> str:
    """返回 OPEN / CLOSED,用於決定是否訂閱。"""
    now_utc = now_utc or datetime.now(timezone.utc)
    tz_name, start, end = SESSIONS[market]

    if tz_name is None:                          # 7×24 市場
        return "OPEN"

    local = now_utc.astimezone(ZoneInfo(tz_name))
    if local.weekday() >= 5:                     # 週末
        return "CLOSED"

    hhmm = local.strftime("%H:%M")
    # 含開盤前預熱窗口
    warm = (datetime.strptime(start, "%H:%M")
            - __import__("datetime").timedelta(minutes=WARMUP_MINUTES)).strftime("%H:%M")
    return "OPEN" if warm <= hhmm <= end else "CLOSED"


def desired_symbols(portfolio: dict[str, list[str]], now_utc=None) -> list[str]:
    """按當前市場狀態,算出「此刻應該訂閱的標的」。"""
    wanted = []
    for market, symbols in portfolio.items():
        if market_state(market, now_utc) == "OPEN":
            wanted.extend(symbols)
    return wanted


PORTFOLIO = {
    "crypto": ["BTCUSDT", "ETHUSDT"],
    "us": ["AAPL", "MSFT"],
    "hk": ["00700"],
    "cn": ["600519"],
}

# 訂閱集合只隨市場狀態變化,不隨時鐘每秒變化
print("當前應訂閱:", desired_symbols(PORTFOLIO))
for m in PORTFOLIO:
    print(f"  {m:8} {market_state(m)}")

核心是 desired_symbols():它把「訂閱哪些標的」變成一個由市場狀態決定的函數。前端可以定時調用它做差集比較,只在集合變化時才調整 WebSocket 訂閱——這樣訂閱抖動降到最低,額度消耗也與真實交易時段對齊。

看板體驗的三個細節

  1. 顯示數據時間而不是「剛剛」:相對時間在多市場場景下會誤導,「2 分鐘前」在休市市場裡可能是「10 小時前」。
  2. 休市市場保留收盤價:空白會讓用戶以為加載失敗,收盤價加標識才是正確呈現。
  3. 區分「停牌」與「無數據」:前者是市場事件,後者是系統問題,前端表現和告警策略都應該不同。

常見問題

看板需要為每個市場建一條 WebSocket 連接嗎?

不一定。行情接口的流式訂閱支持按市場頻道訂閱,可以用較少連接覆蓋多個市場。關鍵在於按市場狀態動態調整訂閱集合,而不是連接數量。

休市時應該繼續輪詢嗎?

不應該。休市期間的請求不會返回新數據,純屬浪費額度。正確做法是休市時取消訂閱,只保留最後一次快照,並在開盤前幾分鐘提前恢復。

怎麼處理不同市場的時效性差異?

把接口返回的 delayed 與 unavailable 標誌一路透傳到前端,並在卡片上顯示數據時間。不要讓用戶在不知情的情況下把延遲數據當實時數據使用。

看板刷新頻率應該設多少?

按市場分別設定:加密可以秒級,股票只在交易時段刷新,外匯接近全天但週末休市。統一的高頻刷新在多市場場景下幾乎總是浪費。

前端應該全量刷新還是增量更新?

增量更新。多市場看板通常有幾十上百個標的,每秒真正變化的只有少數幾個,全量推送會導致不必要的重繪和帶寬消耗。

下一步

多市場看板的核心是讓訂閱集合跟著市場狀態走,而不是跟著時鐘走。流式訂閱協議見 WebSocket 流式接口文檔,多市場接入的整體設計見 多市場集成指南,各市場的交易時段可查閱 美股行情 與 港股行情 頁面,交易日曆可用 交易日曆工具 核對。訂閱規模與 套餐權限 相關,規劃時建議先確認標的數量上限。