☰
贵金属期货实时行情API接入指南:Python实现WebSocket数据流全解析
2026/10/20 8:18:11 网站建设 项目流程

做量化或者盯盘的朋友应该都有过这种经历:行情软件页面一卡,或者自己手忙脚乱刷新页面,结果错过了关键价位。后来把数据接入自动化之后,整个人就轻松多了。这篇我直接讲贵金属期货实时行情 API 的 Python 接入方法,从选型、鉴权、订阅、解析到断线重连、数据落盘和延迟排查,把自己踩过的坑和最终能跑的方案一起放出来。适合正在做程序化交易、风险监控、策略回测,或者单纯想用 Python 拿实时行情做数据分析的朋友。

贵金属期货实时行情 API 这件事,核心不是“调个接口拿数据”,而是要搞清楚你的使用场景到底需要多快、多稳定、多完整。只有明确需求边界,后面的技术选型和代码实现才不会跑偏。

1. 为什么需要实时行情 API:先搞清需求边界

1.1 实时行情到底解决什么问题

贵金属期货的行情数据有几个特点:更新频率高、盘中波动受宏观消息影响大、交易时段有连续竞价也有集合竞价。人工盯盘最大的问题不是看不到,而是看到之后来不及处理。程序化接入行情 API 后,数据可以直接进入策略逻辑、风控模块或监控看板,省掉中间“人眼识别—手工记录—再处理”的环节。

我最早接触这个需求,是想做一套贵金属期货的盘中波动提醒:当黄金主力合约在短时间内涨跌幅超过阈值,自动推送通知。刚开始我尝试直接抓网页数据,几分钟拉一次,结果发现两个问题:一是网页接口随时可能调整字段,二是轮询的频率根本跟不上盘中变化。后来换成实时行情 API,才发现真正的行情数据流是推送式的,不是请求式的。这个认知差异,直接决定了后面的方案设计。

所以先别急着写代码,先想清楚:你要实时行情,是为了自动下单、风控告警、页面展示,还是为了后续做 tick 级回测?不同目的对延迟、数据精度和历史留存的要求差别很大。

1.2 REST 与 WebSocket 怎么选

行情 API 从交互模式上分两类:一类是 HTTP 的 REST 接口,通过请求拿一次快照;另一类是 WebSocket 长连接,服务端主动往客户端推消息。两者的差异用一张表可以看得很清楚:

特性REST 轮询WebSocket 推送
数据时效秒级到分钟级,取决于轮询间隔毫秒级到百毫秒级
服务端压力轮询次数越多压力越大建连后推送,按订阅分发
实现复杂度简单,发请求解析即可需要处理连接、心跳、重连
适合场景低频行情、日线数据、盘后数据实时 tick、盘口、K线更新
典型频率参考1秒以上一次比较合适行情变化即刻推送

如果你只是每天收盘后拉一次日线,用 REST 就行。但要做盘中实时监控,WebSocket 是绕不开的。我见过有人硬用 REST 做高频监控,每 200 毫秒轮询一次,结果自己电脑和对方服务端都被折腾得不行,数据还会有间隙。贵金属期货的行情毛刺很多,中间少一跳,可能正好错过那一笔关键成交。

1.3 一个可落地的接入场景

我以当时做的模拟项目 X 为例:需求是同时监控黄金和白银两个主力合约的实时 tick、五档盘口和 1 分钟 K 线,数据要存下来供盘中复盘和策略回测使用。这套系统只需要在一台普通服务器上运行,不需要复杂的集群架构。

在这个场景下,我的选型结论是:WebSocket 作为主通道接收实时行情,少量 REST 接口用于启动时的合约信息补齐和数据对账。这样既保证了盘中数据的实时性,又不需要把所有功能都堆在 WebSocket 协议里,代码维护也更清晰。后面整篇的代码都是围绕这个场景展开的,你完全可以在此基础上改成自己需要的品种和频率。

2. 接入前准备:环境、鉴权与协议细节

2.1 Python 环境与依赖安装

Python 接入 WebSocket 行情,我建议直接用websockets库,它是异步的,天然适合同时处理连接、心跳和消息解析。不要在socket上自己造轮子,轮子看起来不难,但实际会遇到粘包、分帧、关闭状态判断等问题,没必要重复造。另外需要httpx或requests发认证和 REST 请求,pandas做数据处理,sqlite3或duckdb做本地落盘。

安装命令很简单:

pip install websockets httpx pandas

如果是 Python 3.10 以上,websockets异步写法会更顺手。我个人习惯把环境独立开,用python -m venv venv创建虚拟环境,避免依赖冲突。之前有一次在同一台机器上装了不同版本的项目依赖,结果 WebSocket 库版本不兼容,回调函数报错查了很久,后来养成了每个项目独立环境的习惯。

连接行情 API 之前,建议你先用官方文档或者测试环境把协议跑通。很多行情服务商会提供模拟账号和测试地址,先用测试环境把代码框架跑起来,再切到实盘地址,这个顺序能省很多事。千万不要在没测试数据的情况下直接上生产环境,否则连行情消息格式对不对都没法判断。

2.2 鉴权流程与请求签名

贵金属期货行情 API 一般都需要鉴权,常见方式是在请求头里带 API Key,或者用 API Key 加 Secret 做签名。签名的作用是防止请求被篡改,也方便服务端识别身份。

我遇到的一种签名流程是这样的:客户端生成毫秒级时间戳,把请求路径、请求参数、时间戳拼成一个字符串,用 HMAC-SHA256 和 Secret 计算签名,最后把 API Key、时间戳、签名放到请求头里。REST 请求示例:

import time import hmac import hashlib import json import httpx api_key = "your_api_key" api_secret = "your_api_secret" base_url = "https://market.example-metal.com" params = { "contract": "GOLD_M", "type": "tick", } millis = int(time.time() * 1000) message = f"{millis}GET/v1/market/tick?contract=GOLD_M&type=tick" sign = hmac.new( api_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256 ).hexdigest() headers = { "X-API-Key": api_key, "X-Timestamp": str(millis), "X-Signature": sign, } resp = httpx.get(f"{base_url}/v1/market/tick", params=params, headers=headers) print(json.dumps(resp.json(), ensure_ascii=False, indent=2))

签名串的具体拼接规则每个服务商都可能不同,但思路都一样:包含时间戳避免重放攻击,包含请求参数防止参数被篡改。调试的时候如果返回鉴权失败,优先检查三样:系统时间是否准确、签名串里的路径和实际请求是否一致、参数顺序是否和文档一致。很多签名失败不是算法写错了,而是因为复制代码时路径大小写、参数排序和文档不一致。

WebSocket 的鉴权通常有两种:一种是在连接 URL 上直接带 token,另一种是连接成功后先发一条认证消息。无论哪种,都建议把密钥放在环境变量或配置文件里,不要硬编码在代码中,更不要把 Secret 提交到代码仓库。我见过有人把密钥直接写在示例代码里发到群里,这种习惯非常危险。

2.3 行情消息格式与数据结构

实时行情 API 最常见的消息是 tick 级快照和逐笔成交。不同服务商的字段名会不一样,但核心信息基本是这几类:合约代码、最新价、买一卖一价和量、成交量、成交额、行情时间戳、交易日。还有一个容易被忽略的字段是本地序列号,用来判断消息顺序和做去重。

一段典型的 JSON tick 消息长这样:

{ "contract": "GOLD_M", "exchange_time": 1720000000123, "last_price": 588.32, "bid_price": 588.31, "bid_volume": 12, "ask_price": 588.33, "ask_volume": 8, "volume": 100234, "amount": 589123400, "trade_day": "20240701" }

这里我要特别提一个细节:价格用什么数据类型接收。有些行情服务商会把价格直接发成字符串,这是有原因的。二进制浮点数在表示某些十进制小数时会存在误差,比如0.1 + 0.2在 Python 里并不严格等于0.3。行情价格是钱,精度不能含糊。如果服务商返回的是字符串,先原样保留,等到计算时再用Decimal处理;如果返回的是整数,通常含义是“最小变动价位乘以某个倍数”,也需要看清楚单位。

除了 tick,很多场景还需要订阅 K 线和盘口。K 线消息一般是 Bar 结构,包含开高低收、成交量、开始时间和结束时间。盘口消息则包含多档买卖价格和数量。这三类数据的更新频率不同,解析时最好分开处理,避免一个数据流的逻辑影响另一个。

3. 快速实现:从连接、订阅到数据打印

3.1 先写一个能跑的 WebSocket 客户端

WebSocket 行情接入最核心的就是异步事件循环。websockets库提供一个connect接口,我用asyncio来跑主逻辑。先写一个最简版本,目标是连接成功、收到消息、打印消息:

import asyncio import json import websockets WS_URL = "wss://market.example-metal.com/ws" async def run(): async with websockets.connect(WS_URL) as ws: print("connected") async for message in ws: print(message) asyncio.run(run())

这段代码看着很简单,但它暴露了一个问题:行情服务端一般要求客户端定期发心跳,否则连接会被服务端断开。如果你只想着“发完订阅等着收就行”,几秒钟后可能就收不到数据了。所以更完整的客户端必须带上心跳逻辑。

3.2 自动心跳与超时检查

心跳的机制很容易理解:客户端每隔一段时间发一个ping消息,服务端收到后知道客户端还活着。服务端也会定期发心跳给客户端,客户端要能识别并回复pong。websockets库自带协议层的 ping/pong,但很多行情 API 是在业务消息层做心跳的,也就是发一个 JSON 消息,字段可能是{"type": "ping"}。

我习惯把心跳和订阅放在同一个循环里管理:

import asyncio import json import websockets WS_URL = "wss://market.example-metal.com/ws" async def send_heartbeat(ws, interval=10): while True: await asyncio.sleep(interval) await ws.send(json.dumps({"type": "ping"})) async def run(): async with websockets.connect(WS_URL) as ws: hb_task = asyncio.create_task(send_heartbeat(ws, 10)) await ws.send(json.dumps({ "type": "auth", "api_key": "your_api_key", "sign": "your_sign", "timestamp": "1720000000123" })) async for message in ws: data = json.loads(message) if data.get("type") == "pong": continue if data.get("type") == "tick": print(data) hb_task.cancel() asyncio.run(run())

心跳间隔怎么定?10 秒是很多服务商建议的节奏。太频繁会增加无用消息,太稀疏容易触发服务端超时断开。如果文档里没写,先看服务端的心跳超时时间,一般设置成超时时间的一半比较稳妥。我在实际运行中把心跳和重连逻辑分离:心跳循环只管发,主循环只管收,这样即使某次心跳发送失败,也不会阻塞数据接收。

3.3 订阅合约与行情类型

鉴权成功后,下一步是发送订阅消息。订阅消息一般包含两个信息:订阅什么合约、订阅什么数据类型。有的服务商允许一条消息订阅多个合约,有的则是一条消息只订阅一个主题。从代码可维护性角度,我更喜欢每个主题一条订阅消息,这样重连恢复时控制粒度更细。

async def subscribe(ws): subscribe_requests = [ {"type": "subscribe", "topic": "tick.GOLD_M"}, {"type": "subscribe", "topic": "tick.SILVER_M"}, {"type": "subscribe", "topic": "kline.GOLD_M.1m"}, {"type": "subscribe", "topic": "depth.GOLD_M"}, ] for req in subscribe_requests: await ws.send(json.dumps(req)) await asyncio.sleep(0.05)

订阅之间加一个很小的时间间隔,是为了避免一次性发送太快触发限流。这条经验是我在实际测试中发现的,原本一个循环不加延时地把所有订阅发出去,结果服务端丢了几条订阅回执,害得我排查了很久。订阅成功后,服务端通常会返回一条subscribed消息,里面包含你订阅的主题。收到这条消息再确认数据流是否正常,比盲等数据更可靠。

3.4 行情解析与 DataFrame 落表

收到行情消息后,不能直接打印就完事,要转成结构化数据。tick 消息特别频繁,如果每一条都逐字段用append往列表里塞,性能并不差,但要注意不要用 pandas 的concat在循环里反复调用,那样会越来越慢。我的做法是先存 Python 字典或列表,攒够一定数量再批量转成 DataFrame。

import pandas as pd tick_buffer = [] def parse_tick(raw): return { "contract": raw["contract"], "exchange_time": raw["exchange_time"], "local_time": pd.Timestamp.now(), "last_price": float(raw["last_price"]), "bid_price": float(raw["bid_price"]), "ask_price": float(raw["ask_price"]), "volume": int(raw["volume"]), } def flush_tick_buffer(): global tick_buffer if not tick_buffer: return df = pd.DataFrame(tick_buffer) tick_buffer = [] return df

这里我故意把local_time也存下来,它是客户端本地收到消息的时间,在延迟分析时会用到。很多新手只存服务端时间,后来查延迟完全没有参照物。服务端时间代表的是交易所侧的数据产生时间,本地时间代表你的程序收到数据的时间,两者之差就是网络和协议处理带来的延迟,但前提是两边时钟要大致对齐。

批量刷新 DataFrame 的逻辑也有讲究。实时行情数据如果要展示,没必要每一条都触发 UI 更新,每隔几百毫秒批量刷新一次即可。如果要落库,也是批量插入性能远好于逐条插入。所以行情解析写入缓冲区、定时批量刷新的模式,在实时数据处理里非常通用。

4. 实战加固:断线重连、限流与数据落盘

4.1 断线重连不能只写一个 while 循环

WebSocket 连接断掉是常态,不是异常。网络抖动、服务端重启、防火墙空闲断开,任何情况都可能把连接干掉。如果不做自动重连,半夜行情一断,第二天起来才发现数据少了一大段。

最简单的重连结构是写一个循环,捕获连接异常后等待几秒再重新连接。但要注意两点:第一,重连后要重新鉴权、重新订阅;第二,重连等待时间不能固定不变,否则服务端还没恢复,你的客户端就在疯狂重试,把服务器日志刷爆。建议用指数退避:第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,最多等 30 秒。如果连续重试超过一定次数,发个告警,不要再无限重试。

import asyncio import json import websockets async def run_with_reconnect(): retry = 0 while True: try: async with websockets.connect(WS_URL) as ws: retry = 0 hb_task = asyncio.create_task(send_heartbeat(ws, 10)) await subscribe(ws) async for message in ws: # process message pass except (websockets.ConnectionClosed, OSError) as e: retry += 1 wait = min(2 ** retry, 30) print(f"connection lost, retry={retry}, wait={wait}s") await asyncio.sleep(wait) asyncio.run(run_with_reconnect())

重连之后重新订阅也很容易遗漏。我见过有人的重连逻辑只恢复了连接,没有重新订阅,结果连接看起来是正常的,但就是没有数据。所以把“鉴权—订阅—确认订阅成功”这个过程封装成一个函数,重连时直接调用,这个习惯能帮你避免很多隐蔽问题。

4.2 限流与订阅频率控制

行情 API 通常都有限流策略。有的是限制每秒请求数,有的是限制同时订阅的合约数,还有的是限制单个连接的数据流量。你没法控制服务端限流,只能从客户端设计上规避。

我总结的几条经验:

  • 启动时先订阅少量合约确认链路,再逐步增加,不要一上来就订阅几十个合约。
  • 如果订阅失败,服务端一般会返回错误原因,比如订阅过多、合约代码不存在、鉴权过期。把错误信息完整记录下来,比瞎猜效率高。
  • 需要取消订阅时,发送退订消息,并等待确认。不要直接断开连接又重连,这样既浪费连接,又可能触发风控。
  • REST 接口和 WebSocket 尽量用同一个连接池或同一个 API Key 做频率控制,很多限流是按 Key 维度统计的。

我之前做多合约监控时,一次性订阅 20 个合约,结果服务端直接拒绝了一部分订阅。后来改成每 3 秒订阅 5 个合约,订阅完成后再检查回执,才把全部合约接进来。这个教训说明,订阅管理也是实时行情接入的重要一环。

4.3 数据落盘:CSV、SQLite 还是 Parquet

行情数据不落盘,策略回测和事后复盘就没有素材。落盘格式的选择取决于数据量和你要拿数据做什么。

存储方式优点缺点适合场景
CSV简单,可用 Excel 查看高频数据文件多,读写慢少量数据、调试
SQLite单文件、支持 SQL、事务方便高频写入需批量优化百万级以下数据
Parquet列式存储,压缩率高,读入 pandas 很快不便于直接查看,生态偏分析大量历史数据、回测

实时 tick 数据一天就能达到几十万条甚至上百万条。如果全部写成 CSV,文件数量多、单个文件大,后续读取会很痛苦。我建议在生产环境用 SQLite 或 Parquet。SQLite 对实时写入友好,支持事务,单文件不依赖额外服务。Parquet 更适合盘后批量归档。

用 SQLite 落盘,关键是批量插入。逐条 commit 非常慢,我一般是攒到 1000 条或者每 1 秒批量插入一次。这个数量级下,SQLite 的写性能完全够用。表结构可以设计成:

CREATE TABLE IF NOT EXISTS tick ( contract TEXT, exchange_time INTEGER, local_time INTEGER, last_price TEXT, bid_price TEXT, ask_price TEXT, volume INTEGER, PRIMARY KEY (contract, exchange_time) );

这里把价格字段设为 TEXT,是因为很多服务商的价格精度高,用浮点存容易丢精度。查询时再按需转换成需要的数值类型。

4.4 实时行情与历史行情拼接

如果你要回测,实时行情通常只是“增量数据”,历史数据还需要用 REST 接口或历史文件补齐。推荐的做法是:先用历史数据文件建立基础库,再用实时数据做增量追加。两个来源的时间戳字段必须对齐,否则回测时你会发现数据有缝隙或者重复。

特别是在合约换月的时候,主力合约代码会变化,如果不处理换月映射,后续拼接的数据就是断开的。我处理主力合约的方法是维护一张合约映射表,记录每个时间段的主力合约代码,拼接实时和历史数据时都通过这张表找到对应代码。这个细节不复杂,但很多人容易忽略。

5. 常见问题与排查技巧实录

5.1 连接报错与空数据原因

接入过程中最常见的报错有这么几类:连接被拒、连接断开、鉴权失败、订阅无回执。每一类问题的排查方向都不一样。

连接被拒,第一件事就是检查网络是不是能通到服务端地址。很多服务器不能直连海外行情地址,不是代码问题,是网络路由问题。然后检查协议头,wss和ws不能混用。鉴权失败,前面说过,优先看时间戳和签名串。对接 5 次签名失败大概率都是这两个原因。

还有一种很烦的情况:连接正常、订阅正常、但就是收不到行情。这种问题多半是合约代码不对。比如你订阅的代码是历史合约代码,当前已经没有成交,服务端自然不会推送数据。先用 REST 接口查一下当前活跃合约列表,再把代码复制到订阅消息里,基本就能解决。

5.2 延迟怎么测

贵金属期货行情对实时性要求高,延迟测试不能拍脑袋说感受。我的做法是同时记录服务端行情时间和本地接收时间,在数据流里统计两者的差。本地时间和服务端时间可能存在时钟偏差,所以要先用 NTP 同步系统时间,再参考多组数据的中位数而不是最大值。

我写了一个简单的延迟统计逻辑:

delay_ms = (local_time - exchange_time) / 1e6

注意单位换算。如果时间戳是毫秒,相减后要除 1000 得到秒,再乘 1000 得到毫秒;如果时间戳是纳秒,要小心溢出。实际采集数据时,延迟会存在抖动,单看某一跳意义不大,连续统计 1 分钟的中位数和 95 分位更可靠。如果延迟经常波动到几秒,就要检查是不是本机资源不足、GC 停顿或者网络不稳定。

5.3 多合约并发与消息乱序

当同时订阅多个合约时,服务端推送消息的顺序在同一个 WebSocket 连接上通常是先入先出,但不同数据类型之间或者重连恢复后,消息顺序可能有问题。比如重连后,晚订阅的合约先收到了新数据,早订阅的反而还没推送。我的处理方法是不要强依赖收到顺序,每条消息都要带合约代码,程序按合约分组处理。分组之后,再按时间排序。

如果对顺序要求严格,可以在缓冲区里暂存消息,等同一个合约的数据时间戳连续后再输出。但要注意,等待时间不能太长,否则实时性就没了。建议只对延迟要求高的 tick 消息做较短的排序窗口,比如 100 毫秒,对 K 线这类低频数据,排序窗口可以放宽到 1 秒。

5.4 数据重复与去重

实时行情连接断开再重连之后,很可能会收到一段重复数据。服务端可能已经开始推消息,你重连订阅后倒是从最新状态开始,但上一个连接未处理完的消息如果还在本地,就会重复。

去重最简单的方式是建联合主键,比如(contract, exchange_time)。写入数据库时用INSERT OR IGNORE,重复数据自然被忽略。如果只做实时策略不在乎重复,那可以不做去重,但做回测数据积累时,重复数据会让成交量、成交额虚增,必须处理。

5.5 线程与异步混用的大坑

这个方法要提醒一下:websockets是异步库,如果你想把收到的行情实时塞给另一个线程去计算,不要直接在回调里操作线程不安全的变量。我最初用asyncio.create_task分发消息到不同协程处理,但后来发现多个协程同时写入同一个 DataFrame 时会出问题。

解决方案是维护一个线程安全的队列,比如queue.Queue,主协程收到消息后往队列放,另一个工作线程从队列取数据解析和落库。这样 WebSocket 收消息的循环不会被计算逻辑阻塞,行情处理也更稳定。

6. 上线前后容易被忽略的几件事

6.1 行情断流监控与告警

实时行情接入做好了,还要监控它是否一直健康。最直接的监控指标是“N 秒内没有收到任何消息”和“消息延迟超过阈值”。这两个指标一旦触发,说明行情链路可能出了问题,需要告警。

我在代码里加了一个监控协程,每 10 秒检查一次最近一条消息的时间,如果超过 30 秒没有新消息,就把状态标记为异常,并触发通知。这里的阈值要根据数据频率调整,tick 行情通常每秒都有很多条,30 秒没数据肯定是异常;如果是盘中某个冷门合约,30 秒没数据可能正常。所以监控逻辑永远要结合合约和行情类型来看,不能一刀切。

6.2 密钥管理、日志与数据合规

密钥不要写在代码注释里、不要写在配置仓库里。建议用环境变量,或者专用的密钥管理服务。日志方面,打印行情数据时不要泄露完整密钥。很多服务商的日志打印会包含原始请求头,如果脚本执行失败,你可能顺手把整个请求信息拷到聊天工具求助,这就等于把密钥暴露了。处理方式是把敏感字段在日志里脱敏,比如只显示api_key的前四位和后四位。

另外,行情数据不是免费的,也不是随意分发的。接入时要仔细阅读数据使用协议,搞清楚行情数据能不能存储、能不能再分发、能不能用于策略交易。合规问题前期没想清楚,后面业务做大了才发现隐患,代价会非常大。

6.3 从行情接入到信号计算的扩展

行情接入只是第一步,把行情用起来才是目的。我建议在接入阶段就给数据打上足够丰富的元信息,比如合约代码、行情时间、本地时间、数据类型、数据来源。后续做信号计算时,这些字段都是不可或缺的。比如你要计算盘中波动率,就需要按合约分组、按时间排序,用 tick 或者分钟数据计算滚动标准差。如果不提前把这些字段设计好,后续清洗会非常痛苦。

更好的做法是把行情接入、存储、信号计算分成三层:接入层只负责拿数据和保证连接稳定;存储层负责把数据按标准格式落库;计算层从库里读取数据做策略计算。每层之间的接口定义清楚,哪一层出问题都能单独替换。这套架构虽然比“一个脚本跑到底”复杂一点,但对稳定性和后期扩展帮助很大。

最后分享一个我在实际运行中的体会:程序跑起来之后,最先要盯着的不是策略赚不赚钱,而是行情数据有没有断、有没有重复、有没有延迟异常。数据质量不过关,后面所有计算都是空中楼阁。我后来把行情接入、落库、监控这套流程固化成了一个模板,每次接入新行情源就直接套用,改动只限于鉴权和字段映射,省下了大量排查问题的时间。如果你也准备做贵金属期货的实时行情接入,不妨先把基础链路做扎实,再逐步往上叠加策略逻辑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询