Python外汇实时行情API接入:REST轮询改WebSocket推送,我把踩过的坑都写下来
手上有个外汇汇率面板的小项目,主要盯着 EURUSD、GBPUSD、USDJPY 这几个主流货币对。第一版图省事,直接用 REST 接口轮询,三秒拉一次报价,跑了一段时间也能用。直到有一次做波动率监控,发现轮询拿到的数据根本拼不出连续的跳动曲线——跳价全被”三秒一拉”给抹掉了。于是动了迁移的心思:把实时链路从 REST 轮询换成 WebSocket 推送。这篇把整个迁移过程记录下来,包括第一版方案的问题、迁移时踩的坑、以及现在 REST 和 WebSocket 怎么配合用。
第一版:REST 轮询,简单但问题不少
第一版代码很简单,循环里调一次报价接口,拿最新价更新页面:
import time
import requests
API_BASE = "https://api.itick.org"
headers = {"accept": "application/json", "token": "your_token_here"}
def get_forex_quote(code="EURUSD", region="GB"):
resp = requests.get(
f"{API_BASE}/forex/quote",
headers=headers,
params={"region": region, "code": code}
)
return resp.json()["data"]
while True:
q = get_forex_quote("EURUSD")
# 字段:ld最新价 o开盘 h最高 l最低 t时间戳(毫秒) v量 tu额 ch涨跌 chp涨跌幅 ts交易状态
print(q["ld"], q["t"])
time.sleep(3)
报价接口 /forex/quote 返回的就是一个报价对象,字段 s/ld/o/h/l/t/v/tu/ch/chp/ts,文档写得很清楚。轮询方案跑起来没毛病,但用着用着问题就出来了:
延迟是”轮询周期”的锅:三秒拉一次,行情变化的真实时刻和被你看见的时刻之间,最多能差三秒。做日内监控,这个延迟会让信号晚半拍
跳价会被漏掉:两次请求之间价格可能已经跳了好几跳,中间的过程你完全看不到,画出来的曲线是台阶不是曲线
请求大部分是浪费的:外汇市场 24 小时都在动,但三秒一次请求,大量请求拉回来的数据和上一次一模一样,带宽和限额都花在无意义的地方
轮询的代码量小,但天花板就摆在那。等要做波动率监控、要逐笔数据的时候,就绕不过去了。
迁移决策:什么时候意识到必须换 WebSocket
我自己的判断信号,三个里中一个就值得动手:
要监控价格跳动,而不是最终结果——曲线、波动率、逐笔特征,轮询天然做不到
多货币对同时盯——五个货币对轮询就是五次请求,频率还得翻倍,限额先扛不住
延迟开始影响业务判断——比如根据报价触发提醒,晚两三秒可能就错过进场点
想明白之后就开始迁。实时行情这层从 REST 切到 WebSocket 推送,数据源我用的 iTick,token 在它官网 itick.org 申请,REST 和 WebSocket 都是同一套 token,切换成本比想象中低。
WebSocket 接入:鉴权、订阅、心跳一个都不能少
外汇 WebSocket 的接入流程是固定的:连上 → 等鉴权成功 → 订阅 → 收消息 → 每 30 秒心跳。文档里最容易被忽略的一句话是:连接建立成功 ≠ 可以订阅,必须收到 resAc:"auth" 的鉴权成功消息之后才能发订阅请求。我第一次就栽在这上面,代码里 on_open 里直接订阅,结果服务端回了 subscribe 失败——连上就发太早了。
正确写法:
import json
import threading
import time
import websocket
WS_URL = "wss://api.itick.org/forex"
TOKEN = "your_token_here"
authenticated = False
heartbeat_alive = True
def on_message(ws, message):
global authenticated
payload = json.loads(message)
# 关键:等鉴权成功再订阅
if payload.get("resAc") == "auth" and payload.get("code") == 1 and not authenticated:
authenticated = True
ws.send(json.dumps({
"ac": "subscribe",
"params": "EURUSD$GB,GBPUSD$GB,USDJPY$GB", # 多标的逗号分隔,格式 symbol$region
"types": "quote" # quote/tick/depth/kline@1
}))
threading.Thread(target=heartbeat, args=(ws,), daemon=True).start()
return
if payload.get("resAc") == "pong":
return
data = payload.get("data") or {}
if data.get("type") == "quote":
# 推送的报价字段和REST一致:s/ld/o/h/l/t/v/tu
print(data["s"], data["ld"], data["t"])
def heartbeat(ws):
while heartbeat_alive:
time.sleep(30)
ws.send(json.dumps({
"ac": "ping",
"params": str(int(time.time() * 1000))
}))
ws = websocket.WebSocketApp(
WS_URL,
header=[f"token: {TOKEN}"],
on_message=on_message
)
ws.run_forever()
订阅格式 EURUSD$GB 是”标的代码$市场代码”,多个标的用英文逗号分隔,一条连接最多能订阅 500 个标的,几个主流货币对一条连接就全搞定了。消息里的 type 字段区分数据类型,订阅 quote 收到的是报价增量,tick 是逐笔成交,depth 是盘口,kline@1 是分钟 K 线增量。
迁移过程中踩的坑,逐个说
坑一:连接建立就订阅
上面说过了,on_open 里直接发订阅是错的,必须等 resAc:"auth"。判断标准就一个:把 authenticated 当作真正的”可订阅”状态。
坑二:心跳线程泄漏
第一版我把心跳写在 on_open 里,断线重连一次就多开一个心跳线程,老线程还在发 ping,服务端回了一堆 pong 没人处理。后来把心跳线程的存活标志和连接生命周期绑在一起,断线时置为 False,重连后再开新线程。
坑三:断线重连后状态没重置
authenticated 还是 True,重连后服务端重新鉴权,但代码以为已经订阅过了,直接跳过订阅步骤,结果一条行情都收不到。重连后必须把 authenticated 重置为 False,让流程重新走一遍”鉴权→订阅”。
坑四:消息太快,缓存爆炸
订阅 tick 逐笔成交之后,高峰期消息量很大,如果收消息的 handler 里做了重活(比如写数据库),消费速度跟不上,服务端缓冲区堆积,连接会被主动断开。处理方式是收消息只做最轻量的更新(更新内存里的价格),落库放到另一个队列慢慢写。
REST 和 WebSocket 的正确配合:快照 + 增量
迁移完我发现,REST 不是没用了,而是角色变了——REST 管快照,WebSocket 管增量。启动时用 REST 批量接口拉一次全量快照,把每个货币对的当前状态初始化好,之后靠 WebSocket 推送增量更新,只改有变化的部分:
def get_forex_quotes_batch():
# /forex/quotes 批量报价,codes 用逗号分隔
# 返回 dict,key 是产品代码
resp = requests.get(
f"{API_BASE}/forex/quotes",
headers=headers,
params={"region": "GB", "codes": "EURUSD,GBPUSD,USDJPY,GBPUSD,USDCNH"}
)
return resp.json()["data"]
snapshot = get_forex_quotes_batch()
for symbol, quote in snapshot.items():
print(symbol, quote["ld"], quote["t"])
批量接口 /forex/quotes 的参数是 codes(复数),一次最多能传的数量看套餐:基础 10、专业 20、高级 50。返回的 dict 以产品代码为 key,遍历更新到本地状态表里就行。这套”REST 快照 + WS 增量”的组合,比纯轮询省了 90% 的无效请求,数据还更连续。
踩坑清单速查
等鉴权成功再订阅,
authenticated为 True 才算可订阅状态心跳 30 秒一次,超 1 分钟没心跳会被服务端断开;断线重连后重置所有状态
心跳线程要能关,别在重连时叠罗汉
订阅多标的用逗号分隔,格式
symbol$region,一条连接上限 500 个REST 管快照、WS 管增量,别让 WS 消息 handler 里干重活
时间戳是毫秒,本地时间对比时先统一单位
批量接口参数是
codes,不是code,返回 dict 按代码取
小结
从 REST 轮询迁到 WebSocket 推送,核心变化不是换了个协议,而是把”定时拉取”改成了”事件驱动”——行情到了才处理,没有行情就不干活。这套结构跑下来,延迟从”最多三秒”变成”毫秒级”,请求量反而降了一个数量级。如果你也在做外汇实时行情相关的监控、告警或者数据采集,这个迁移路线可以直接参考。接口字段和订阅格式的细节,写代码的时候对照官方文档最省事。
GitHub:https://github.com/itick-org/
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: