Skip to content

实时行情 WebSocket 推送接口文档

概述

本文档描述如何通过 WebSocket 协议连接行情推送服务,订阅并接收实时行情数据(市场行情、涨停预警等)。

  • 传输协议:WSS(WebSocket over TLS)
  • 连接地址wss://zhunData.cn/ws/
  • 鉴权方式:URL 查询参数 key,如 wss://zhunData.cn/ws/?key=your_api_key
  • 数据压缩:服务端推送的行情数据使用 zstandard 压缩 + msgpack 序列化
  • 文本消息:控制类消息(订阅确认、错误等)使用 JSON 文本格式

客户端 Demo 下载

下载 client_demo.py

功能包含:WSS 连接、自动重连、订阅/取消订阅、zstandard 解压 + msgpack 反序列化、Key 过期提醒处理、内存与流量监控日志。


1. 建立连接

连接地址

wss://zhunData.cn/ws/?key={AUTH_KEY}

鉴权 Key 通过 URL 查询参数 key 传递。

连接示例(Python)

python
import asyncio
import ssl
import websockets

WS_URI = "wss://zhunData.cn/ws/"
AUTH_KEY = "your_api_key"

async def connect():
    ws_uri = f"{WS_URI}?key={AUTH_KEY}"
    ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ssl_ctx.check_hostname = False
    ssl_ctx.verify_mode = ssl.CERT_NONE
    async with websockets.connect(ws_uri, ssl=ssl_ctx) as ws:
        print("连接成功!")
        # ... 后续订阅与接收逻辑

连接被拒绝的关闭码

关闭码说明
4001缺少鉴权 Key(URL 中未提供 key 参数)
4003Key 无效或已过期
4009Key 已被其他连接占用(同一 Key 不允许重复连接)
1013背压断开(客户端发送缓冲区超过 1MB,服务端主动断开)
4011Key 过期超过宽限期,服务端主动断开

2. 订阅行情

连接成功后,客户端需发送订阅消息才能接收行情推送。

2.1 订阅请求

json
{
  "action": "subscribe",
  "channel": "market_quote",
  "symbols": ["tick.stock.sz.000001", "tick.stock.sh.600744"]
}
字段类型必填说明
actionstring固定为 "subscribe"
channelstring频道名称,见下方频道列表
symbolsstring[]订阅标的符号列表,见下方符号格式说明

2.2 频道列表

频道说明
market_quote市场行情数据
limit_alert涨停预警数据

2.3 标的符号格式

符号格式为 tick.{asset_type}.{suffix}.{code},支持通配符。

完整格式:

tick.{asset_type}.{suffix}.{code}

合法资产类型(asset_type):

资产类型说明
stock股票
index指数
fund基金
bond债券

合法交易所后缀(suffix):

后缀说明
sz深交所
sh上交所
bj北交所
*通配(所有交易所)

符号示例:

符号说明
tick.stock.sz.000001深市平安银行
tick.stock.sh.600744沪市华银电力
tick.fund.sh.516100沪市基金
tick.fund.sz.159227深市基金
tick.stock.*订阅所有股票(通配)
tick.index.sz.*订阅深市所有指数(通配)
tick.fund.*订阅所有基金(通配)

2.4 订阅权限

订阅请求需通过权限校验,权限由 Key 绑定的订阅类型决定:

订阅类型说明权限范围
{asset}_all全量订阅可订阅该资产类型的通配符和任意单个标的
{asset}_100百标的订阅仅可订阅该资产类型的单个标的,最多 100 个

权限校验规则:

  • 通配符订阅(如 tick.stock.*):需要 {asset}_all 权限
  • 单标的订阅(如 tick.stock.sz.000001):需要 {asset}_all{asset}_100 权限
  • {asset}_100 类型下,单标的总数不可超过 100 个

2.5 订阅确认

订阅成功:

json
{
  "type": "subscribe_ok",
  "channel": "market_quote",
  "symbols": ["tick.stock.sz.000001", "tick.stock.sh.600744"]
}

订阅被拒绝:

json
{
  "type": "subscribe_rejected",
  "channel": "market_quote",
  "symbols": ["tick.stock.sz.000001"],
  "msg": "Permission denied for tick.stock.sz.000001: no stock data subscription"
}

拒绝原因包括:

  • 符号格式不合法
  • 无该资产类型的订阅权限
  • {asset}_100 类型超出 100 个单标的限制
  • 频道无效或符号列表为空

3. 取消订阅

取消订阅请求

json
{
  "action": "unsubscribe",
  "channels": ["market_quote"]
}
字段类型必填说明
actionstring固定为 "unsubscribe"
channelsstring[]要取消的频道列表

取消订阅确认

json
{
  "type": "unsubscribe_ok",
  "channels": ["market_quote"]
}

注意:取消订阅是频道级别的,会清除该频道下所有已订阅的标的。


4. 接收推送数据

服务端推送的数据分为两类:文本消息(JSON)和二进制消息(压缩行情数据)。

4.1 文本消息

文本消息为 JSON 格式,用于控制类通知。

4.1.1 服务端错误

json
{
  "type": "error",
  "msg": "Invalid JSON format"
}

4.1.2 Key 即将过期提醒

当 Key 距过期不足 7 天时,连接成功后服务端会主动推送:

json
{
  "type": "key_expiring_soon",
  "key": "f7dbf1ee-xxxx",
  "expired_at": "2026-07-30T00:00:00",
  "days_remaining": 5.2,
  "msg": "Key will expire in 5.2 days, please renew"
}

4.1.3 Key 已过期警告

Key 已过期但仍在 8 小时宽限期内时,服务端会定期推送:

json
{
  "type": "key_expired_warning",
  "key": "f7dbf1ee-xxxx",
  "expired_at": "2026-07-25T00:00:00",
  "grace_hours_remaining": 6.5,
  "msg": "Key expired, will disconnect in 6.5 hours"
}

4.1.4 Key 过期断开连接

Key 过期超过宽限期后,服务端主动断开连接:

json
{
  "type": "key_expired_disconnect",
  "msg": "Key f7dbf1ee-xxxx expired over 8 hours, disconnecting"
}

随后连接将被关闭,关闭码为 4011

4.2 二进制消息(行情数据)

行情数据以二进制帧推送,需经过以下步骤解析:

二进制数据 → zstandard 解压 → msgpack 反序列化 → 行情字典/列表

解压后的数据可能是单个字典或字典列表(批量推送)。

解析示例(Python):

python
import msgpack
import zstandard as zstd

dctx = zstd.ZstdDecompressor()

async for message in ws:
    if isinstance(message, bytes):
        raw_bytes = dctx.decompress(message)
        result = msgpack.unpackb(raw_bytes, raw=False)
        if isinstance(result, list):
            for item in result:
                handle_tick(item)
        else:
            handle_tick(result)

4.2.1 市场行情数据(market_quote)

外层结构:

字段类型说明
typestring消息类型,值为 "market_quote"
keystring标的符号,如 "tick.stock.sz.000001"
dataobject行情明细,字段如下

data 行情明细字段:

字段类型说明
timelong行情时间戳(毫秒)
lastPricefloat最新成交价
openfloat今日开盘价
highfloat今日最高价
lowfloat今日最低价
lastClosefloat昨日收盘价
amountfloat累计成交金额
volumefloat累计成交数量
pvolumefloat原始成交数量(未复权)
openIntfloat持仓量(期货适用)
askPricefloat卖一委托价
bidPricefloat买一委托价
askVolfloat卖一委托量
bidVolfloat买一委托量

4.2.2 涨停预警数据(limit_alert)

字段类型说明
typestring消息类型,值为 "limit_alert"
keystring标的符号
dataobject预警数据

5. Key 过期机制

阶段时间节点行为
正常使用过期前正常推送行情
即将过期提醒距过期不足 7 天连接时推送 key_expiring_soon
过期宽限期过期后 8 小时内推送 key_expired_warning,仍可接收行情
过期断开超过宽限期推送 key_expired_disconnect,关闭连接(4011)

6. 断线重连建议

服务端不保证消息持久化,断线期间的数据不会重传。建议客户端实现自动重连机制:

python
import random

RECONNECT_MIN = 5
RECONNECT_MAX = 10

async def run_client():
    while True:
        try:
            ws_uri = f"{WS_URI}?key={AUTH_KEY}"
            ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            ssl_ctx.check_hostname = False
            ssl_ctx.verify_mode = ssl.CERT_NONE
            async with websockets.connect(ws_uri, ssl=ssl_ctx) as ws:
                await subscribe_client(ws)
                await receive_data(ws)
        except (websockets.ConnectionClosed, ConnectionRefusedError, OSError, asyncio.TimeoutError):
            wait_time = random.randint(RECONNECT_MIN, RECONNECT_MAX)
            await asyncio.sleep(wait_time)

重连注意事项:

  • 重连后需重新发送订阅请求,服务端不会保留上次的订阅状态
  • 建议使用随机退避时间(5~10 秒),避免大量客户端同时重连
  • 同一 Key 同时只允许一个连接,新连接会因 Key 已被占用而被拒绝(关闭码 4009)

7. 完整客户端示例

python
import asyncio
import json
import random
import ssl
import websockets
import msgpack
import zstandard as zstd

WS_URI = "wss://zhunData.cn/ws/"
AUTH_KEY = "your_api_key"
RECONNECT_MIN = 5
RECONNECT_MAX = 10

dctx = zstd.ZstdDecompressor()

async def subscribe_client(ws):
    sub_msg = {
        "action": "subscribe",
        "channel": "market_quote",
        "symbols": [
            "tick.stock.*",
        ]
    }
    await ws.send(json.dumps(sub_msg))
    response = await ws.recv()
    print(f"服务端确认: {response}")

async def receive_data(ws):
    async for message in ws:
        try:
            if isinstance(message, str):
                text_data = json.loads(message)
                msg_type = text_data.get("type")
                if msg_type == "subscribe_ok":
                    print(f"订阅成功: {text_data}")
                elif msg_type == "subscribe_rejected":
                    print(f"订阅拒绝: {text_data}")
                elif msg_type == "key_expiring_soon":
                    print(f"Key即将过期: 剩余{text_data.get('days_remaining')}天")
                elif msg_type == "key_expired_warning":
                    print(f"Key已过期: 宽限剩余{text_data.get('grace_hours_remaining')}小时")
                elif msg_type == "key_expired_disconnect":
                    print(f"Key过期断开: {text_data.get('msg')}")
                elif msg_type == "error":
                    print(f"服务端错误: {text_data.get('msg')}")
                else:
                    print(f"文本消息: {text_data}")
            else:
                raw_bytes = dctx.decompress(message)
                result = msgpack.unpackb(raw_bytes, raw=False)
                if isinstance(result, list):
                    for item in result:
                        handle_tick(item)
                else:
                    handle_tick(result)
        except Exception as e:
            print(f"解析失败: {e}")

def handle_tick(tick_data):
    msg_type = tick_data.get("type")
    key = tick_data.get("key", "N/A")
    data = tick_data.get("data", {})
    if msg_type == "limit_alert":
        print(f"涨停预警: {key}")
    elif msg_type == "market_quote":
        tick_time = data.get("time")
        if tick_time:
            dt = datetime.fromtimestamp(tick_time / 1000)
            print(f"行情: {key} time={dt}")

async def run_client():
    while True:
        try:
            ws_uri = f"{WS_URI}?key={AUTH_KEY}"
            ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            ssl_ctx.check_hostname = False
            ssl_ctx.verify_mode = ssl.CERT_NONE
            async with websockets.connect(ws_uri, ssl=ssl_ctx) as ws:
                print("连接成功!")
                await subscribe_client(ws)
                await receive_data(ws)
        except (websockets.ConnectionClosed, ConnectionRefusedError, OSError, asyncio.TimeoutError) as e:
            print(f"连接断开: {e}")
            wait_time = random.randint(RECONNECT_MIN, RECONNECT_MAX)
            print(f"{wait_time}秒后重连...")
            await asyncio.sleep(wait_time)

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

8. 消息速查表

客户端 → 服务端

action字段说明
subscribechannel, symbols订阅指定频道的标的
unsubscribechannels取消指定频道的订阅

服务端 → 客户端(文本)

type说明
subscribe_ok订阅成功
subscribe_rejected订阅被拒绝
unsubscribe_ok取消订阅成功
error服务端错误
key_expiring_soonKey 即将过期提醒
key_expired_warningKey 已过期宽限期警告
key_expired_disconnectKey 过期断开连接通知

服务端 → 客户端(二进制)

type (解压后)说明
market_quote市场行情推送
limit_alert涨停预警推送