做港股量化系统三年多,行情接口的稳定性一直是最让我头疼的问题。记得有一次凌晨三点被报警叫醒,发现策略系统因为某个字段突然变成字符串而不是数字导致整个风控模块崩溃。那次教训让我深刻意识到,行情接口的选择和实现方式会直接影响整个系统的可靠性。
港股市场相比其他市场确实有些特殊。交易时间集中在上午9:30到12:00,下午1:00到4:00,在这短短几个小时内会产生大量交易数据。如果用传统的HTTP轮询方式,要么数据延迟严重,要么很快就会被接口限制。这也是为什么我现在所有项目都采用WebSocket协议来获取实时行情。
港股行情有几个关键特点需要特别注意:
交易时段集中:主要交易集中在4小时内,这意味着单位时间内的数据密度很高。我们曾经统计过,像腾讯(0700.HK)这样的热门股票,在高峰时段每秒可能产生10-20笔成交记录。
接口限制严格:大多数免费接口对轮询频率有严格限制,通常每分钟不超过5-10次请求。对于实时性要求高的策略来说,这样的频率远远不够。
数据结构复杂:完整的行情数据包含买卖盘、成交记录、指数成分等,不同接口返回的字段名称和格式差异很大。
经过多个项目的迭代,我总结出一个稳定的三层架构:
code复制行情模块架构:
1. 连接层 - WebSocket连接管理
│
2. 订阅层 - 标的订阅与心跳维护
│
3. 处理层 - 数据解析与事件触发
这种架构的最大优势是职责分离。当需要增加新的股票标的或者切换到其他市场时,只需要修改订阅层的配置,处理层的业务逻辑完全不需要改动。
使用Python的websocket-client库可以快速建立连接,但有几个关键点需要注意:
python复制import websocket
import json
import threading
import time
class HKMarketData:
def __init__(self):
self.ws = None
self.connected = False
self.reconnect_interval = 5 # 重连间隔(秒)
def on_message(self, ws, message):
try:
data = json.loads(message)
if "data" in data:
self.process_tick(data["data"])
except Exception as e:
print(f"处理消息出错: {e}")
def on_open(self, ws):
self.connected = True
print("连接已建立")
self.subscribe(["HKEX:HSI", "HKEX:00700"]) # 恒指和腾讯
def on_close(self, ws, close_status_code, close_msg):
self.connected = False
print(f"连接关闭: {close_status_code} - {close_msg}")
self.schedule_reconnect()
def on_error(self, ws, error):
print(f"连接错误: {error}")
def connect(self):
self.ws = websocket.WebSocketApp(
"wss://stream.alltick.co/hk",
on_open=self.on_open,
on_message=self.on_message,
on_close=self.on_close,
on_error=self.on_error
)
wst = threading.Thread(target=self.ws.run_forever)
wst.daemon = True
wst.start()
关键提示:一定要在单独的线程中运行WebSocket,否则会阻塞主线程。同时要设置daemon=True,这样主程序退出时连接线程会自动终止。
在实际运行中,网络波动是不可避免的。我们需要实现自动重连机制:
python复制def schedule_reconnect(self):
if not self.connected:
print(f"{self.reconnect_interval}秒后尝试重连...")
time.sleep(self.reconnect_interval)
self.connect()
def keepalive(self):
while True:
if self.connected:
# 发送心跳包维持连接
try:
self.ws.send(json.dumps({"cmd": "ping"}))
except:
self.connected = False
time.sleep(30) # 每30秒发送一次心跳
心跳机制不仅可以维持连接,还能及时发现连接异常。我通常会单独启动一个线程来执行keepalive()函数。
不同接口返回的数据结构差异很大,我们需要在数据处理层进行标准化:
python复制def process_tick(self, tick):
standardized = {
"symbol": tick.get("symbol"),
"price": float(tick.get("last_price", 0)),
"volume": int(tick.get("volume", 0)),
"timestamp": self.parse_timestamp(tick.get("timestamp")),
"bid": float(tick.get("bid_price", 0)),
"ask": float(tick.get("ask_price", 0)),
"bid_volume": int(tick.get("bid_volume", 0)),
"ask_volume": int(tick.get("ask_volume", 0))
}
self.dispatch_event(standardized)
def parse_timestamp(self, ts):
# 统一转换为毫秒时间戳
if isinstance(ts, int):
return ts if ts > 1e12 else ts * 1000 # 判断是否为毫秒级
elif isinstance(ts, str):
return int(datetime.strptime(ts, "%Y-%m-%d %H:%M:%S").timestamp() * 1000)
return int(time.time() * 1000)
经验之谈:一定要对价格和成交量做类型转换。很多接口返回的数值类型不稳定,可能在某个时刻突然从数字变成字符串,导致后续计算出错。
处理高频行情数据时,性能优化很重要:
python复制def __init__(self):
self.tick_buffer = []
self.buffer_size = 50 # 每50条处理一次
self.last_flush = time.time()
def process_tick(self, tick):
self.tick_buffer.append(tick)
now = time.time()
if len(self.tick_buffer) >= self.buffer_size or now - self.last_flush > 1.0:
self.dispatch_event(self.tick_buffer)
self.tick_buffer = []
self.last_flush = now
python复制import numpy as np
class OrderBook:
def __init__(self):
self.bids = np.zeros((10, 2)) # 价格, 数量
self.asks = np.zeros((10, 2))
def update(self, bids, asks):
self.bids = np.array(sorted(bids, key=lambda x: -x[0])[:10])
self.asks = np.array(sorted(asks, key=lambda x: x[0])[:10])
python复制last_price = 0
def process_tick(self, tick):
global last_price
if abs(tick["price"] - last_price) > tick["price"] * 0.0001: # 价格变动超过0.01%
self.trigger_strategies(tick)
last_price = tick["price"]
问题现象:连接频繁断开,特别是在市场波动剧烈时
解决方案:
python复制def schedule_reconnect(self):
self.reconnect_attempts += 1
delay = min(self.reconnect_interval * (2 ** self.reconnect_attempts), 300)
print(f"{delay}秒后尝试重连...")
time.sleep(delay)
self.connect()
python复制endpoints = [
"wss://stream1.alltick.co/hk",
"wss://stream2.alltick.co/hk",
"wss://stream3.alltick.co/hk"
]
current_endpoint = 0
def connect(self):
global current_endpoint
url = self.endpoints[self.current_endpoint]
print(f"尝试连接 {url}...")
self.ws = websocket.WebSocketApp(
url,
on_open=self.on_open,
on_message=self.on_message,
on_close=self.on_close,
on_error=self.on_error
)
# ...其余代码...
def on_error(self, ws, error):
print(f"连接错误: {error}")
self.current_endpoint = (self.current_endpoint + 1) % len(self.endpoints)
self.schedule_reconnect()
问题现象:行情数据比实际市场延迟明显
排查步骤:
优化方案:
python复制def enable_low_latency(self):
self.ws = websocket.WebSocketApp(
self.url,
on_open=self.on_open,
on_message=self.on_message,
on_close=self.on_close,
on_error=self.on_error,
enable_multithread=True,
socket_options=[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] # 禁用Nagle算法
)
问题现象:长时间运行后内存占用持续增长
排查工具:
python复制import tracemalloc
tracemalloc.start()
# ...运行一段时间后...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
常见原因:
在正式环境中,建议监控以下关键指标:
| 指标名称 | 正常范围 | 检查频率 |
|---|---|---|
| 连接状态 | 持续连接 | 实时 |
| 数据延迟 | <500ms | 每分钟 |
| 消息处理延迟 | <100ms | 每分钟 |
| 内存使用 | <500MB | 每小时 |
| 重连次数 | <5次/天 | 每天 |
完善的日志有助于问题排查:
python复制import logging
from logging.handlers import TimedRotatingFileHandler
def setup_logger():
logger = logging.getLogger("hkmarket")
logger.setLevel(logging.INFO)
handler = TimedRotatingFileHandler(
"hkmarket.log",
when="midnight",
backupCount=7
)
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
为确保系统高可用,建议实施以下措施:
python复制class FailoverManager:
def __init__(self, primary, secondary):
self.primary = primary
self.secondary = secondary
self.active = primary
self.switch_time = 0
def get_active(self):
if time.time() - self.switch_time < 300: # 5分钟内不切换
return self.active
if not self.active.is_healthy() and self.secondary.is_healthy():
self.active = self.secondary
self.switch_time = time.time()
return self.active
在实际项目中,行情接口的稳定性往往决定了整个系统的可靠性。经过多次迭代,我发现与其追求功能的丰富性,不如确保核心功能的稳定性。选择那些经过市场检验的接口服务,虽然初期可能需要更多时间熟悉,但长期来看会大大降低维护成本。