休市提醒日股将于 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 流式接口文档,多市场接入的整体设计见 多市场集成指南,各市场的交易时段可查阅 美股行情 与 港股行情 页面,交易日历可用 交易日历工具 核对。订阅规模与 套餐权限 相关,规划时建议先确认标的数量上限。