Python外汇实时行情API接入:REST轮询改WebSocket推送,我把踩过的坑都写下来

AI摘要
该内容为外汇行情技术分享,介绍从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

我自己的判断信号,三个里中一个就值得动手:

  1. 要监控价格跳动,而不是最终结果——曲线、波动率、逐笔特征,轮询天然做不到

  2. 多货币对同时盯——五个货币对轮询就是五次请求,频率还得翻倍,限额先扛不住

  3. 延迟开始影响业务判断——比如根据报价触发提醒,晚两三秒可能就错过进场点

想明白之后就开始迁。实时行情这层从 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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
101
粉丝
4
喜欢
8
收藏
8
排名:1721
访问:1642
私信
所有博文
社区赞助商