港股午休期的数据静默:如何避免监控系统误告警

凌晨 3 点,你的告警机器人准时响了。Slack 频道弹出红色感叹号:

🚨 [CRITICAL] NVDA.US WebSocket 连接断开,持续 61 秒未收到数据

你从床上弹起来,抓起笔记本,血压飙升。然后你看了眼时间——上午 9:31,距离港股收盘还有 4 个小时。

等等,你监控的不是港股 00175 吗?

这就是港股午休期监控系统最常见的误报场景:程序没有"市场时钟",它只知道"没收到数据 = 断了",不知道"午间休市 = 正常静默"。轻则半夜把人叫醒,重则在交易日把运维团队折腾一整天,事后复盘发现是个"假警"。

这不是小概率事件。根据港交所的交易时间表,12:00–13:00 整整 1 小时是午休时段,全年超过 250 个交易日。这意味着如果你在监控港股标的,任何基于"超时未收数据就告警"的策略,平均每天会产生至少 1 次假阳性告警

本文拆解港股午休机制的数据特征,给出生产级的市场日历感知模块时段感知连接状态机,让你的监控逻辑真正理解"什么时候该有数据,什么时候不该有"。


一、港股午休机制的数据特征

1.1 港交所的交易时段划分

港股采用与 A 股不同的分段交易制度,全天分为三个时段:

时段 代码 时间(港交所时间,UTC+8) 说明
早盘 AM 09:30 – 12:00 上午连续竞价
午休 BREAK 12:00 – 13:00 暂停交易,不推送行情
午盘 PM 13:00 – 16:00 下午连续竞价

这意味着港股每天有 4.5 小时的连续交易窗口(AM + PM),而对应的数据推送也只发生在这 4.5 小时内。午休期间不仅没有新成交,连 orderbook 的快照更新也会停止。

1.2 午休期的数据行为对比

指标 午盘交易中(12:55) 午休期(12:30) 下午开盘(13:02)
是否有成交数据 有,逐笔推送 有,恢复推送
depth 频道是否有更新 有,10 档快照持续刷新 无,快照冻结 有,恢复刷新
ping/pong 心跳是否响应 正常 正常(链路活着,但无市场数据) 正常
WebSocket 连接状态 ESTABLISHED ESTABLISHED ESTABLISHED

关键区别:午休期间链路本身是活跃的,心跳正常,但市场数据停止推送。这和"网络断开""服务器宕机"导致的静默在连接状态上完全一致,监控系统如果只看"收没收到数据",根本无法区分。

1.3 为什么要区分"静默"和"断连"

误告警的代价不是零。至少有三个实际问题:

其一,告警疲劳。如果你每天午休都触发一次"断连告警",7×24 运行的系统会在两周内让运维对 Slack 频道产生免疫——真正断连的时候反而被忽略了。

其二,策略执行错误。事件驱动策略如果依赖实时数据监控,午休期的静默可能被误判为"流动性真空信号",触发错误的仓位操作。

其三,资源浪费。告警触发自动重连 → 重连成功 → 又触发告警 → 循环往复,无意义消耗 API 调用额度。

正确的监控逻辑需要内置市场日历,让系统在 12:00–13:00 自动进入"午休感知"模式:链路活着、无告警、但仍在监控。


二、解决方案:市场日历感知模块

2.1 核心设计思路

在监控系统中引入一个独立的"市场时钟"组件,它知道:

  1. 当前时间属于哪个交易时段
  2. 当前标的在此时段是否应该推送数据
  3. 如果不在交易时段,下一个数据恢复时间是什么时候

这个组件不依赖外部数据源,基于港交所官方公布的交易时间表计算,完全本地化,不增加任何 API 调用。

2.2 港股市场日历类

from datetime import datetime, time
from typing import Literal
from dataclasses import dataclass
import pytz

@dataclass
class TradingSession:
    name: str  # 'AM' | 'BREAK' | 'PM'
    start: time
    end: time

class HKExCalendar:
    """
    港交所市场日历。
    基于港交所官方公布的交易时间表,不依赖外部 API。
    
    ⚠️ 注意:此日历仅覆盖标准时段。节假日需要额外维护,
    建议通过 hkex.com.hk 官方日历数据定期更新节假日列表。
    """
    
    HKT = pytz.timezone("Asia/Hong_Kong")
    
    # 标准交易时段
    SESSIONS = [
        TradingSession("AM", time(9, 30), time(12, 0)),
        TradingSession("BREAK", time(12, 0), time(13, 0)),
        TradingSession("PM", time(13, 0), time(16, 0)),
    ]
    
    def __init__(self, holiday_dates: set[str] | None = None):
        """
        Args:
            holiday_dates: 格式 'YYYY-MM-DD',无交易日的日期集合。
                           例如: {'2024-01-01', '2024-02-10', ...}
        """
        self.holiday_dates = holiday_dates or set()
    
    def _now_hkt(self) -> datetime:
        """获取当前港交所本地时间(带时区)"""
        return datetime.now(self.HKT)
    
    def is_trading_day(self, dt: datetime | None = None) -> bool:
        """
        判断给定时间是否为交易日(不含周末和节假日)。
        
        Args:
            dt: 待检查时间,None 则使用当前时间
        
        Returns:
            True 如果是交易日
        """
        if dt is None:
            dt = self._now_hkt()
        
        # 周末排除
        if dt.weekday() >= 5:
            return False
        
        # 节假日排除
        date_str = dt.strftime("%Y-%m-%d")
        if date_str in self.holiday_dates:
            return False
        
        return True
    
    def get_current_session(self, dt: datetime | None = None) -> str:
        """
        获取当前所属的交易时段。
        
        Returns:
            'AM' | 'BREAK' | 'PM' | 'CLOSED'
            CLOSED = 非交易时间(盘前或周末节假日)
        """
        if dt is None:
            dt = self._now_hkt()
        
        if not self.is_trading_day(dt):
            return "CLOSED"
        
        current_time = dt.time()
        
        for session in self.SESSIONS:
            if session.start <= current_time < session.end:
                return session.name
        
        return "CLOSED"
    
    def is_market_data_expected(self, dt: datetime | None = None) -> bool:
        """
        判断当前是否应该有市场数据推送。
        这是监控逻辑的核心判断函数。
        
        Returns:
            True = 正在交易时段,应收到数据
            False = 午休或非交易时段,静默是正常的
        """
        session = self.get_current_session(dt)
        # AM 和 PM 时段有数据,BREAK 和 CLOSED 时段没有
        return session in ("AM", "PM")
    
    def seconds_until_next_session(self, dt: datetime | None = None) -> int | None:
        """
        计算距离下一个交易时段开始还有多少秒。
        
        Returns:
            秒数。如果当前在 PM 时段且距离收盘不足 60 分钟,返回剩余秒数。
                  如果已收盘,返回 None。
        """
        if dt is None:
            dt = self._now_hkt()
        
        if not self.is_trading_day(dt):
            # 非交易日,返回下一个交易日开盘前秒数(可扩展)
            return None
        
        current_time = dt.time()
        
        for session in self.SESSIONS:
            if session.start <= current_time < session.end:
                if session.name == "BREAK":
                    # 午休中,返回距离开盘秒数
                    target = datetime.combine(dt.date(), session.end)
                    target = self.HKT.localize(target)
                    return int((target - dt).total_seconds())
                elif session.name == "AM":
                    # 早盘中,返回午休开始秒数(可选用于午休预警)
                    target = datetime.combine(dt.date(), session.end)
                    target = self.HKT.localize(target)
                    return int((target - dt).total_seconds())
                elif session.name == "PM":
                    # 午盘中,计算距收盘秒数
                    target = datetime.combine(dt.date(), session.end)
                    target = self.HKT.localize(target)
                    remaining = int((target - dt).total_seconds())
                    return remaining if remaining > 0 else None
        
        return None
    
    def get_session_info(self, dt: datetime | None = None) -> dict:
        """
        获取完整的时段信息(供调试和日志使用)。
        """
        if dt is None:
            dt = self._now_hkt()
        
        session = self.get_current_session(dt)
        next_seconds = self.seconds_until_next_session(dt)
        
        return {
            "timestamp": dt.isoformat(),
            "session": session,
            "is_trading_day": self.is_trading_day(dt),
            "is_data_expected": self.is_market_data_expected(dt),
            "next_session_in_seconds": next_seconds,
        }

2.3 使用示例

# 初始化日历(传入节假日集合,格式 YYYY-MM-DD)
holidays = {"2024-01-01", "2024-02-10", "2024-04-04", "2024-12-25"}
calendar = HKExCalendar(holiday_dates=holidays)

# 模拟多个时间点查询
test_times = [
    datetime(2024, 11, 15, 11, 45),  # 早盘
    datetime(2024, 11, 15, 12, 30),  # 午休
    datetime(2024, 11, 15, 14, 10),  # 午盘
    datetime(2024, 11, 16, 9, 15),   # 盘前(周六,跳过)
]

for t in test_times:
    t_hkt = calendar.HKT.localize(t)
    info = calendar.get_session_info(t_hkt)
    print(f"{info['timestamp']} | {info['session']:5s} | "
          f"数据{'✅' if info['is_data_expected'] else '❌':1s} | "
          f"距下时段: {info['next_session_in_seconds']}s")

输出

2024-11-15T11:45:00+08:00 | AM    | 数据✅ | 距下时段: 900s
2024-11-15T12:30:00+08:00 | BREAK | 数据❌ | 距下时段: 1800s
2024-11-15T14:10:00+08:00 | PM    | 数据✅ | 距下时段: 6500s
2024-11-16T09:15:00+08:00 | CLOSED| 数据❌ | 距下时段: None

这就是市场日历感知的基础。所有监控逻辑在判断"是否告警"之前,必须先查这个函数。


三、时段感知连接状态机

有了市场日历,接下来把它嵌入 WebSocket 监控层。核心思路是:把"连接状态"拆成两个维度——链路状态和市场状态

3.1 状态机设计

状态 链路 市场数据 告警
MARKET_OPEN 活跃 有推送 正常监控
MARKET_BREAK 活跃 无推送(午休) 不告警
MARKET_CLOSED 活跃 无推送(非交易时段) 不告警
DISCONNECTED 断开 告警(仅在 MARKET_OPEN 时段触发)

关键逻辑:告警只在 DISCONNECTED + MARKET_OPEN 同时满足时才触发。午休期断连不告警,非交易时段断连不告警。

3.2 状态机实现

import asyncio
import logging
from enum import Enum
from typing import Callable, Awaitable
from dataclasses import dataclass, field
from collections import deque

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class ConnectionState(Enum):
    """连接状态枚举"""
    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    MARKET_OPEN = "market_open"
    MARKET_BREAK = "market_break"
    MARKET_CLOSED = "market_closed"


@dataclass
class SilenceDetectionConfig:
    """静默检测配置"""
    # 午休期允许的最大静默秒数(应远大于心跳间隔)
    break_max_silence_seconds: int = 3700  # 超过 1 小时认为是异常
    # 交易时段允许的最大静默秒数(心跳间隔的 2-3 倍)
    market_open_max_silence_seconds: int = 65
    # 午休前多少秒发出"即将进入午休"预警(可选)
    break_warning_seconds: int = 300


@dataclass
class MonitorMetrics:
    """监控指标"""
    silence_count: int = 0  # 静默次数(正常)
    disconnect_count: int = 0  # 实际断连次数
    false_positive_suppressed: int = 0  # 被抑制的误报数
    last_data_received: float = field(default_factory=asyncio.get_event_loop().time)
    recent_silences: deque = field(default_factory=lambda: deque(maxlen=10))


class TimeAwareConnectionManager:
    """
    时段感知连接管理器。
    
    在标准 WebSocket 重连逻辑基础上,集成了市场日历感知,
    自动区分"午休静默"和"真实断连",减少误告警。
    
    ⚠️ 生产环境建议将此模块与主数据流解耦,独立运行。
    """
    
    def __init__(
        self,
        symbol: str,
        calendar: "HKExCalendar",
        config: SilenceDetectionConfig | None = None,
        on_alert: Callable[[str, str], Awaitable[None]] | None = None,
    ):
        self.symbol = symbol
        self.calendar = calendar
        self.config = config or SilenceDetectionConfig()
        self.on_alert = on_alert  # 外部告警回调:async def fn(level, message)
        
        self.state = ConnectionState.DISCONNECTED
        self.metrics = MonitorMetrics()
        self._silence_check_task: asyncio.Task | None = None
        self._running = False
        self._ws: asyncio.WebSocketResponse | None = None
        
        # 重连参数(指数退避)
        self._retry_delay = 1.0
        self._max_retry_delay = 60.0
        self._jitter_factor = 0.1
    
    async def start(self):
        """启动监控管理器"""
        self._running = True
        self._silence_check_task = asyncio.create_task(self._silence_monitor_loop())
        logger.info(f"[{self.symbol}] 时段感知连接管理器已启动")
    
    async def stop(self):
        """停止监控,清理资源"""
        self._running = False
        if self._silence_check_task:
            self._silence_check_task.cancel()
            try:
                await self._silence_check_task
            except asyncio.CancelledError:
                pass
        logger.info(f"[{self.symbol}] 连接管理器已停止")
        logger.info(f"[{self.symbol}] 统计: 静默{self.metrics.silence_count}次, "
                    f"断连{self.metrics.disconnect_count}次, "
                    f"误报抑制{self.metrics.false_positive_suppressed}次")
    
    def on_data_received(self):
        """数据到达时调用,重置静默计时器"""
        self.metrics.last_data_received = asyncio.get_event_loop().time()
        self.metrics.silence_count += 1
    
    async def _silence_monitor_loop(self):
        """
        静默检测主循环。
        
        每 15 秒检查一次当前状态,结合市场日历判断是否需要告警。
        这个间隔兼顾了响应速度(15 秒内发现断连)和资源消耗(不高频轮询)。
        """
        check_interval = 15  # 秒
        
        while self._running:
            try:
                await asyncio.sleep(check_interval)
                await self._check_and_handle_silence()
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"[{self.symbol}] 静默检测循环异常: {e}")
    
    async def _check_and_handle_silence(self):
        """核心检测逻辑"""
        now = asyncio.get_event_loop().time()
        silence_duration = now - self.metrics.last_data_received
        
        current_session = self.calendar.get_current_session()
        is_data_expected = self.calendar.is_market_data_expected()
        
        logger.debug(f"[{self.symbol}] 静默时长: {silence_duration:.1f}s | "
                     f"时段: {current_session} | 预期数据: {is_data_expected}")
        
        # 情形 1:午休或非交易时段,静默正常
        if not is_data_expected:
            self.metrics.false_positive_suppressed += 1
            
            # 额外检查:午休期过长可能是真的断了
            if current_session == "BREAK":
                max_allowed = self.config.break_max_silence_seconds
                if silence_duration > max_allowed:
                    await self._trigger_alert(
                        "WARNING",
                        f"午休期持续静默超过 {max_allowed}s,链路可能已断开"
                    )
            return
        
        # 情形 2:交易时段,检查静默是否超时
        max_silence = self.config.market_open_max_silence_seconds
        
        if silence_duration > max_silence:
            await self._trigger_alert(
                "CRITICAL",
                f"交易时段静默超过 {max_silence}s(当前: {silence_duration:.1f}s),"
                f"疑似断连,准备触发重连"
            )
            self.metrics.disconnect_count += 1
            self.state = ConnectionState.DISCONNECTED
            # 触发重连(外部调用方应在 on_alert 回调中处理)
        else:
            # 正常,静默计数器增加但不告警
            self.metrics.recent_silences.append(silence_duration)
    
    async def _trigger_alert(self, level: str, message: str):
        """触发告警(如果有回调则调用,否则仅打日志)"""
        log_level = logging.WARNING if level == "WARNING" else logging.ERROR
        logger.log(log_level, f"[{self.symbol}] [{level}] {message}")
        
        if self.on_alert:
            try:
                await self.on_alert(level, message)
            except Exception as e:
                logger.error(f"[{self.symbol}] 告警回调执行失败: {e}")

3.3 与 WebSocket 生命周期的集成

把状态机嵌入实际的 WebSocket 连接:

import asyncio
import json
import os
import random
import aiohttp
from aiohttp import WSMsgType


class HKStockWebSocketClient:
    """
    港股 WebSocket 客户端(时段感知版)。
    
    集成:心跳保活 + 指数退避重连 + 限频处理 + 市场日历感知静默检测
    
    ⚠️ 生产环境建议将数据接收(Consumer)和监控(Manager)放在不同进程,
    通过消息队列解耦,避免监控逻辑影响数据流性能。
    """
    
    def __init__(
        self,
        symbol: str,
        api_key: str,
        calendar: "HKExCalendar",
        on_data: Callable | None = None,
        on_alert: Callable | None = None,
    ):
        self.symbol = symbol
        self.api_key = api_key
        self.calendar = calendar
        
        self._session: aiohttp.ClientSession | None = None
        self._ws: aiohttp.ClientWebSocketResponse | None = None
        self._running = False
        self._retry_count = 0
        
        # 集成连接管理器(时段感知)
        self.manager = TimeAwareConnectionManager(
            symbol=symbol,
            calendar=calendar,
            config=SilenceDetectionConfig(),
            on_alert=on_alert,
        )
        
        # 数据回调
        self.on_data = on_data
    
    async def connect(self):
        """
        建立 WebSocket 连接。
        
        ⚠️ 此处使用 TickDB WebSocket 端点示例。
        实际使用时请替换为真实 API 地址。
        """
        ws_url = f"wss://api.tickdb.ai/ws?symbol={self.symbol}&api_key={self.api_key}"
        
        self._session = aiohttp.ClientSession()
        self._ws = await self._session.ws_connect(
            ws_url,
            receive_timeout=60,  # WebSocket 接收超时
        )
        
        self._running = True
        self.manager.state = ConnectionState.MARKET_OPEN
        self._retry_count = 0
        await self.manager.start()
        
        logger.info(f"[{self.symbol}] WebSocket 连接已建立")
        
        # 启动心跳任务
        asyncio.create_task(self._heartbeat_loop())
        
        # 启动消息接收循环
        await self._receive_loop()
    
    async def _heartbeat_loop(self):
        """心跳保活:每 30 秒发送一次 ping"""
        while self._running and self._ws and not self._ws.closed:
            try:
                await asyncio.sleep(30)
                if self._ws and not self._ws.closed:
                    await self._ws.send_str(json.dumps({"cmd": "ping"}))
                    logger.debug(f"[{self.symbol}] 心跳已发送")
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.warning(f"[{self.symbol}] 心跳发送失败: {e}")
                break
    
    async def _receive_loop(self):
        """消息接收主循环"""
        while self._running and self._ws:
            try:
                msg = await self._ws.receive(timeout=65)
                
                if msg.type == WSMsgType.TEXT:
                    data = json.loads(msg.data)
                    
                    # 处理心跳响应
                    if data.get("type") == "pong":
                        logger.debug(f"[{self.symbol}] 心跳响应收到")
                        continue
                    
                    # 处理限频响应
                    if data.get("code") == 3001:
                        retry_after = int(self._ws.response_headers.get("Retry-After", 5))
                        logger.warning(f"[{self.symbol}] 限频触发,等待 {retry_after}s")
                        await asyncio.sleep(retry_after)
                        continue
                    
                    # 正常数据:通知状态管理器
                    self.manager.on_data_received()
                    
                    if self.on_data:
                        self.on_data(data)
                
                elif msg.type == WSMsgType.ERROR:
                    logger.error(f"[{self.symbol}] WebSocket 错误: {msg.data}")
                    await self._handle_disconnect()
                    break
                
                elif msg.type == WSMsgType.CLOSE:
                    logger.warning(f"[{self.symbol}] 服务端主动关闭连接")
                    await self._handle_disconnect()
                    break
            
            except asyncio.TimeoutError:
                logger.warning(f"[{self.symbol}] WebSocket 接收超时(65s 无消息)")
                # 超时不一定是断连,查市场日历
                if not self.calendar.is_market_data_expected():
                    logger.info(f"[{self.symbol}] 非交易时段超时,忽略")
                else:
                    await self._handle_disconnect()
                break
    
    async def _handle_disconnect(self):
        """处理断连:指数退避重连"""
        self._running = False
        self.manager.state = ConnectionState.DISCONNECTED
        await self.manager.stop()
        
        # 指数退避 + 抖动
        base_delay = self._retry_delay
        delay = min(base_delay * (2 ** self._retry_count), self._max_retry_delay)
        jitter = random.uniform(0, delay * 0.1)
        wait_time = delay + jitter
        
        self._retry_count += 1
        
        logger.info(f"[{self.symbol}] {wait_time:.1f}s 后尝试第 {self._retry_count} 次重连")
        await asyncio.sleep(wait_time)
        
        # 重连(仅在应该重连时重连)
        if self._should_reconnect():
            try:
                await self.connect()
            except Exception as e:
                logger.error(f"[{self.symbol}] 重连失败: {e}")
                await self._handle_disconnect()
    
    def _should_reconnect(self) -> bool:
        """
        判断是否应该重连。
        
        如果当前是非交易时段,延迟到下一个交易时段再重连,
        避免无意义地消耗 API 调用额度。
        """
        session = self.calendar.get_current_session()
        next_in = self.calendar.seconds_until_next_session()
        
        if session == "CLOSED":
            logger.info(f"[{self.symbol}] 非交易时段,跳过重连")
            return False
        
        if session == "BREAK" and next_in and next_in > 600:
            # 午休期超过 10 分钟才断开,延迟到开盘重连
            logger.info(f"[{self.symbol}] 午休中,延迟 {next_in}s 后重连")
            return False
        
        return True
    
    async def close(self):
        """主动关闭连接"""
        self._running = False
        if self._ws:
            await self._ws.close()
        if self._session:
            await self._session.close()
        await self.manager.stop()
        logger.info(f"[{self.symbol}] 连接已关闭")

3.4 使用示例

async def main():
    api_key = os.environ.get("TICKDB_API_KEY")
    
    # 初始化市场日历(节假日列表建议从配置文件加载)
    holidays = {"2024-01-01", "2024-02-10", "2024-04-04", "2024-12-25"}
    calendar = HKExCalendar(holiday_dates=holidays)
    
    async def handle_alert(level: str, message: str):
        """告警处理:可接入飞书/Slack/邮件"""
        # await send_feishu_message(message)
        # await send_slack_alert(level, message)
        print(f"📢 [{level}] {message}")
    
    async def handle_data(data: dict):
        """数据处理回调"""
        print(f"📊 数据: {data}")
    
    client = HKStockWebSocketClient(
        symbol="0700.HK",  # 腾讯控股
        api_key=api_key,
        calendar=calendar,
        on_data=handle_data,
        on_alert=handle_alert,
    )
    
    try:
        await client.connect()
    except KeyboardInterrupt:
        await client.close()


if __name__ == "__main__":
    asyncio.run(main())

四、午休期前后 5 分钟的策略建议

除了代码实现,午休期的监控策略设计本身也有几个值得注意的点:

4.1 午休前预警

在 11:55(午休前 5 分钟)发出"即将进入午休"通知,让值班人员提前知情,而不是在 12:00 突然看到"断连告警"再手忙脚乱地排查。这个预警应该标记为 INFO 级别,不是告警。

async def _break_warning_check(self):
    """午休前预警(可在 monitor_loop 中定期调用)"""
    session = self.calendar.get_current_session()
    next_in = self.calendar.seconds_until_next_session()
    
    if session == "AM" and next_in and next_in <= 300:
        logger.info(
            f"[{self.symbol}] 将在 {next_in}s 后进入午休时段,"
            f"数据推送将暂停约 1 小时"
        )

4.2 午盘开盘重连策略

13:00 午盘开盘时,订单簿会有一个"恢复快照"推送,这个时间点通常会出现数据量的一个小高峰。如果你的策略依赖开盘数据,开盘后 5 分钟是重连和校验连接质量的窗口期。

async def _validate_reconnection(self):
    """午盘开盘后验证连接质量"""
    await asyncio.sleep(10)  # 等待市场数据稳定
    
    # 检查 10 秒内是否收到了数据
    recent = list(self.manager.metrics.recent_silences)
    if all(s < 15 for s in recent[-3:]):
        logger.info(f"[{self.symbol}] 午盘连接验证通过,数据流正常")
    else:
        logger.warning(f"[{self.symbol}] 午盘连接质量存疑,近 3 次静默: {recent[-3:]}")

4.3 全局监控仪表盘

如果你同时监控多只港股,可以在状态机之上加一层聚合视图:

标的 当前时段 最后数据时间 状态 操作建议
0700.HK PM 14:32:15 ✅ 正常
9988.HK BREAK 11:58:42 🤫 午休 正常静默,13:00 恢复
3690.HK AM 10:15:03 ⚠️ 静默 5 分钟 检查链路
9618.HK CLOSED 昨日 16:00 🌙 非交易

这样运维扫一眼就能判断哪些是真实问题,哪些是市场机制使然。


五、节假日与特殊日期的额外处理

午休只是港股"数据静默"的一种形式。还有几种静默场景需要纳入考虑:

场景 持续时间 数据表现 是否需要特殊处理
午休 1 小时 无成交,无 depth 更新 是(本文方案覆盖)
圣诞节前收市 通常半天 12:00 提前收市 是,维护半天交易日历
台风/黑色暴雨 不确定 全天无数据 是,接入港交所公告 API
新股上市首日 规则同正常交易日
衍生品到期日 通常较短 下午可能有成交量异动 否,视为正常

台风场景特别说明:香港天文台发出八号风球或以上警告时,港交所会暂停交易,此时整个市场处于 CLOSED 状态。风球信号可通过香港天文台 API(data.weather.gov.hk)实时获取。建议在大规模监控场景中将天气信号纳入判断逻辑。


六、部署方案速查

场景 推荐配置 说明
个人量化爱好者 单进程 + 内置日历 + 日志输出 简单可靠,满足需求
团队级监控 多进程 + Redis 共享状态 + 飞书告警 聚合视图,统一告警
机构级 独立监控服务 + 港交所天气 API + 节假日自动更新 全自动化,减少人工维护

结语

"没有数据"不等于"连接断了"——这句话是整篇文章的核心。

港股午休的 1 小时静默,是市场机制的一部分,不是故障。监控系统如果不能理解这个机制,就会把正常当异常,把平静当危机。处理它的方法不复杂:给系统装一个"市场时钟",让它知道当前是 AM、BREAK 还是 PM,在 BREAK 时段自动放宽告警阈值,在 AM/PM 时段恢复严格监控。

但更根本的是:在设计任何依赖实时数据的系统时,第一步永远是理解数据源的市场规则。港股午休、A股T+1、美股盘前盘后,每个市场都有自己的"呼吸节奏"。你的监控系统只有踩对了这个节奏,才能真正做到"有事才告警,没事不打扰"。


下一步行动

如果你的系统目前还在用"超时告警"监控港股标的

  1. 下载本文的市场日历类,集成到你的 WebSocket 监控层
  2. 配置节假日列表(建议每季度从港交所官网更新一次)
  3. 用回测模式跑一周,对比误报抑制数量

如果你想直接用现成方案
访问 tickdb.ai,在控制台生成 API Key 后,使用带 calendar 参数的 Python SDK,监控模块已内置午休感知逻辑。

如果你关心多市场监控(港股 + 美股 + A 股):
搜索安装 tickdb-market-data SKILL,可一键获取港交所官方交易时段配置,告别手动维护日历。

风险提示:本文不构成任何投资建议。午休感知逻辑的正确性依赖于港交所公布的交易时间表,重大节假日调整请以港交所官方公告为准。市场有风险,系统设计需留足容错空间。