我们在帮专业交易团队和基金公司开发部门做港股行情相关模块时,发现一个非常典型的问题:WebSocket 连接本身并不难,真正反复让开发人员返工的,是“时间”这个看似基础、实则容易踩坑的边界判断。
一、为什么时间边界容易成为隐性故障点
港股并不是全天连续交易的市场。如果我们只是把实时行情当成一条条价格、成交量和时间戳的流,而不对“数据时间”和“交易时间”做分层,系统在开市前、午间休市、收市后甚至跨日期时就会出现状态漂移。
例如,有一次我们接收港股 api 推送的 tick 数据,在早上 9:05 收到一条报价,前端却显示为“连续交易中”,导致一个基金客户的内部风控模块触发了错误的预警。排查后发现,代码里只是简单判断“当前时间大于 9:30”就认为开市,完全忽略了 9:00 到 9:30 其实是竞价阶段,也忽略了下午 16:00 之后的时间区间。
这类问题的本质是:我们把时间当成了一个点,而不是一个区间。
二、从交易时段定义开始拆分
我们现在的做法是,先把港股一天的时段拆成清晰的配置,而不是散落在业务代码里。下面是我们内部统一维护的时段表:
| 时段 | 香港时间 | 程序处理 |
|---|---|---|
| 开市前竞价 | 09:00–09:30 | 预开市数据 |
| 上午交易 | 09:30–12:00 | 正常交易 |
| 午间休市 | 12:00–13:00 | 非连续交易 |
| 下午交易 | 13:00–16:00 | 正常交易 |
| 收市后 | 16:00 之后 | 非正常交易 |
这个表看起来简单,但很多团队会忽略“边界”本身。比如下面这种写法:
if current_time >= time(9, 30):
market_open = True
表面上看似乎合理,实际上下午五点、晚上九点都会被判定成开市。这就是我们后来强制改成区间判断的原因:
def is_market_open(t):
morning = time(9, 30) <= t < time(12, 0)
afternoon = time(13, 0) <= t < time(16, 0)
return morning or afternoon
从工程化角度看,我们更倾向把这类判断抽成独立的时间模块,供实时计算、风控、K 线聚合等多个任务复用,而不是在每个微服务里复制一份。
三、API 时间戳与本地时间的错位问题
我们对接过多个港股 api,返回的数据里时间戳通常是 Unix timestamp,比如:
{
"symbol": "00700.HK",
"price": 520.5,
"timestamp": 1788226200
}
这个数字本身不携带任何时区信息。如果服务器部署在 UTC 或新加坡时区,而我们直接用 datetime.now() 去比较,就会出现同一时刻在不同环境得到不同结果的情况。
我们的统一处理是:所有进入交易时段判断之前的时间,先转换为 Asia/Hong_Kong 时区:
from datetime import datetime
from zoneinfo import ZoneInfo
timestamp = 1788226200
dt = datetime.fromtimestamp(
timestamp,
tz=ZoneInfo("Asia/Hong_Kong")
)
print(dt)
这样做的好处非常明显:无论服务跑在哪个地区,只要经过这个转换,后续判断的语义就是一致的。我们在接入 AllTick 的行情接口时,也会沿用这一层时区标准化逻辑,确保数据管道里的时间始终是香港时间。
四、开市前数据不能与连续成交混淆
9:00 到 9:30 这个阶段是“有行情数据”但“市场不连续成交”的阶段。如果我们把它简单标成 market_open=True,对行情展示和策略计算都会产生误导。
我们内部用的状态判断是:
def get_market_status(t):
if time(9, 0) <= t < time(9, 30):
return "pre_open"
if time(9, 30) <= t < time(12, 0):
return "morning"
if time(12, 0) <= t < time(13, 0):
return "break"
if time(13, 0) <= t < time(16, 0):
return "afternoon"
return "closed"
这样前端可以显示“开市前”,量化策略只接受 morning 和 afternoon 的数据,风控模块也能对 pre_open 做单独处理。不同模块各取所需,不会互相干扰。
五、跨日期和交易日历是另一层问题
还有一个容易被忽视的地方:午夜和周末。比如下面这种判断:
if current_time > time(16, 0):
status = "closed"
在凌晨一点执行时,确实会被判为收市后,这没错。但如果任务同时要判断“当前交易日”,那就不能只看 time,还需要完整的日期信息。
我们的经验是:时间段用 datetime.time,交易日用 datetime.date,两者分开维护。不要在一个变量里既表达时间又表达日期,否则跨午夜时很容易出错。
另外,周一到周五并不等于交易日。香港的公众假期、台风休市等特殊情况都需要独立维护交易日历。生产环境里,交易日历应该是一个可更新的配置,而不是硬编码的星期几判断。
六、实时行情与 K 线的时间边界需要区分
如果我们要从实时 tick 数据聚合 1 分钟 K 线,边界问题会放大。例如 09:29:59 和 09:30:00 只差一秒,但分属两个完全不同的 K 线周期。
我们内部约定使用左闭右开区间:
09:30:00 <= tick_time < 09:31:00
这样每条 tick 只会进入唯一一个 K 线周期,不会出现同一条数据同时被两个周期统计的问题。
七、可扩展的数据管道设计
现在我们的行情处理流程已经固化下来,大致如下:
API实时数据
↓
解析 timestamp
↓
转换为 Asia/Hong_Kong
↓
判断交易日期
↓
判断交易时段
↓
过滤/分类行情
↓
K线聚合或策略计算
这个流程的最大好处是:交易时段、时区、交易日历都做成了可配置项,后续新增市场或调整规则时,改配置即可,不需要动核心逻辑。
我们以前总觉得行情系统里价格才是核心,真正做起来才发现,时间边界处理得不严谨,价格再准确也没用。对专业交易者和基金公司开发部门来说,时间层是行情系统里非常基础、但极其关键的一层。
API Docs:https://alltick.co/apis/en
GitHub:https://github.com/alltick/alltick-realtime-forex-crypto-stock-tick-finance-websocket-api
