多市场行情 API 接入实战:一套接口打通股票/外汇/期货/加密货币 + WebSocket 实时推送
最近做一个多市场行情看板,需要同时拿到股票、外汇、期货、加密货币 四类数据,还要支持实时刷新。逐家对接不同数据源太重,于是我把整个接入过程抽象成一套通用范式:统一的 REST 请求 + WebSocket 推送。这篇把这套方法、可复用的客户端封装,以及踩过的坑整理出来,方便有同样需求的同学少走弯路。
文中接口地址统一用占位域名
https://api.example.com,你换成自己所用数据源的地址即可,方法是通用的。
目录
- 需求与选型思路
- 鉴权模型:一个 key 走天下
- 第一个请求:拉一页行情列表
- 指数 / K 线 / 排行榜 / 日历
- 外汇、期货、加密货币:同一套范式
- 实时行情:WebSocket 接入与心跳
- 工程化:带重试和限流的客户端封装
- 踩坑记录
- 小结
一、需求与选型思路
项目要做一个"多市场资产行情墙",核心诉求三点:
- 覆盖面:A 股之外还要美股、印度、东南亚、日韩等多个市场,加上外汇、商品期货、主流加密货币;
- 统一性:不想为每个市场对接不同的 SDK,最好是一套 HTTP 风格的接口 + 统一的返回结构;
- 实时性:列表页用轮询可以接受,但详情页和盯盘页需要 WebSocket 推送。
选数据源时,我把"接入成本"作为首要标准。最省事的那类接口往往有几个共同特征,可以作为选型 checklist:
- 全部
GET+ query 参数,无需复杂签名计算; - 返回统一的
{code, message, data}信封,错误处理可以收敛成一处; - 各类资产(股票/外汇/期货/加密)路径前缀不同、但参数与返回风格一致,封装一次即可复用;
- 同时提供 REST 与 WebSocket 两种接入方式。
下面的代码就以这种"统一信封 + query 鉴权"的接口为例。
二、鉴权模型:一个 key 走天下
这类接口通常共用一个 key 参数,放在 query 里即可,没有复杂的签名。先把它抽成环境变量,别硬编码进代码:
bash
# .env
QUOTE_API_KEY=your_api_key_here
QUOTE_API_BASE=https://api.example.com
QUOTE_WS_BASE=wss://ws-api.example.com
安全提醒:key 等同于访问凭证,不要提交进 Git,不要写在前端可见的代码里。前端需要实时数据时,建议由你自己的后端做一层转发代理,避免 key 直接暴露在浏览器。
三、第一个请求:拉一页行情列表
先用最基础的"市场列表"类接口验证连通性。它按市场分页返回股票快照:
GET /stock/stocks?countryId=42&pageSize=10&page=1&key=<KEY>
countryId:市场标识(不同数据源的取值不同,接入前先对一遍);pageSize/page:分页参数。
用 Python 跑一下:
python
import os, requests
BASE = os.environ["QUOTE_API_BASE"]
KEY = os.environ["QUOTE_API_KEY"]
def get(path, **params):
params["key"] = KEY
r = requests.get(f"{BASE}{path}", params=params, timeout=10)
r.raise_for_status()
body = r.json()
if body.get("code") != 200:
raise RuntimeError(f"API error: {body.get('code')} {body.get('message')}")
return body["data"]
data = get("/stock/stocks", countryId=42, pageSize=5, page=1)
for s in data["records"]:
print(f'{s["symbol"]:<8} {s["name"][:20]:<22} last={s["last"]} chgPct={s["chgPct"]}%')
典型返回是分页信封,核心字段:
jsonc
{
"code": 200,
"message": "ok",
"data": {
"records": [
{
"symbol": "MDCH", // 股票代码
"name": "Media Chinese Int",
"last": 0.12, // 最新价
"chgPct": 0, // 涨跌百分比
"technicalDay": "strong_sell", // 日线技术指标
"open": false // 是否开市
}
],
"total": 1000,
"pages": 500
}
}
字段通常挺全,连多周期技术指标(technicalDay/Week/Month)和基本面市值这类都会带上,做筛选器很省事。
四、指数 / K 线 / 排行榜 / 日历
把行情模块常用的几个接口串起来,基本就能拼出一个像样的详情页了。
4.1 指数行情
GET /stock/indices?countryId=14&flag=IN&key=<KEY>
返回某市场的指数列表,data 是数组:
jsonc
{
"id": 17940,
"name": "Nifty 50",
"symbol": "NSEI",
"last": 22967.65, // 最新价
"chg": 369.85, // 涨跌额
"chgPct": 1.64, // 涨跌幅
"isOpen": false // 是否开盘
}
4.2 K 线数据
GET /stock/kline?pid=7310&interval=PT15M&key=<KEY>
注意这里的 interval 用的是 ISO 8601 风格的时间区间,不是常见的 15m 写法:
| interval | 含义 |
|---|---|
PT5M |
5 分钟 |
PT15M |
15 分钟 |
PT1H |
1 小时 |
PT5H |
5 小时 |
P1D |
1 天 |
P1W |
1 周 |
P1M |
1 月 |
返回标准的 OHLCV 数组,直接喂给 ECharts / TradingView 的 K 线组件就行:
jsonc
{ "time": 1719818400000, "open": 239.42, "high": 239.6, "low": 239.42, "close": 239.6, "volume": 0 }
4.3 排行榜 & 日历
GET /stock/updownList?countryId=14&type=1&key=<KEY> # 1涨幅 2跌幅 3涨停 4跌停
GET /stock/getIpo?countryId=14&key=<KEY> # 新股/IPO 日历
涨跌榜适合做首页的"热门异动";日历类接口返回上市时间、发行价等,做"提醒"很合适。
五、外汇、期货、加密货币:同一套范式
最省心的一点:其它三类资产的接口风格和股票完全一致,换个路径前缀而已。把第三节那个 get() 函数复用即可。
5.1 外汇
python
fx = get("/market/currency", countryType="sg") # 实时汇率列表
get("/market/todayMarket", symbol="EUR=X") # 单个币对
get("/market/chart", symbol="EURUSD=X", interval="5m") # K 线
jsonc
{ "symbol": "EURUSD=X", "name": "EUR/USD", "lastPrice": "1.0765", "chg": "+0.0016", "chgPct": "+0.15%" }
5.2 期货
python
get("/futures/list") # 期货市场列表
get("/futures/querySymbol", symbol="XAG") # 单品种行情(如白银)
get("/futures/kline", symbol="EUA", interval="1") # K 线
买卖价、最高最低、成交量一应俱全,商品/能源类品种都覆盖。
5.3 加密货币
加密这块字段和主流交易所基本对齐,熟悉的同学几乎零学习成本:
python
get("/crypto/getCoinList", start=1, limit=1000) # 交易对列表
get("/crypto/tickerPrice", symbols="BTCUSDT,ETHUSDT") # 24h 行情
get("/crypto/lastPrice", symbols="BTCUSDT,ETHUSDT") # 最新价
get("/crypto/getKlines", symbol="BTCUSDT", interval="5m") # K 线
get("/crypto/getTrades", symbol="BTCUSDT") # 近期成交
jsonc
{
"symbol": "BTCUSDT",
"lastPrice": "66912.01000000", // 最新价
"highPrice": "67480.00000000",
"lowPrice": "63456.70000000",
"priceChangePercent": "4.304" // 涨跌幅
}
一套 get() 封装,四类资产全打通------这正是"统一信封"接口的价值所在。
六、实时行情:WebSocket 接入与心跳
轮询能扛住列表页,但盯盘页必须上推送。实时通道是一个 WebSocket 长连接:
wss://ws-api.example.com/connect?key=<KEY>
要点就一个:连上之后要定时发心跳维持连接,否则会被服务端断开。推送过来的是逐笔行情快照:
jsonc
{
"pid": "992844", // 产品 id
"last_numeric": "0.68", // 当前最新价
"high": "0.680",
"low": "0.650",
"pcp": "0.00", // 涨跌幅(需自行拼 %)
"ask": "0.680", // 卖一价
"bid": "0.675", // 买一价
"timestamp": "1717728251",
"type": 1 // 1股票 2指数
}
Python 端用 websockets 实现一个带自动重连 + 心跳的订阅器:
python
import asyncio, json, os, websockets
WS = f'{os.environ["QUOTE_WS_BASE"]}/connect?key={os.environ["QUOTE_API_KEY"]}'
async def stream(on_tick):
while True: # 断线自动重连
try:
async with websockets.connect(WS, ping_interval=None) as ws:
async def heartbeat():
while True:
await asyncio.sleep(15)
await ws.send("ping") # 定时心跳
hb = asyncio.create_task(heartbeat())
try:
async for msg in ws:
try:
on_tick(json.loads(msg))
except json.JSONDecodeError:
pass # 心跳回包等非 JSON 帧
finally:
hb.cancel()
except Exception as e:
print("ws reconnect in 3s:", e)
await asyncio.sleep(3)
def on_tick(t):
print(f'pid={t.get("pid")} last={t.get("last_numeric")} chgPct={t.get("pcp")}%')
asyncio.run(stream(on_tick))
浏览器端同理,new WebSocket(url) + setInterval 发心跳即可。生产环境记得把 key 藏在自己后端,前端连自己的代理层。
七、工程化:带重试和限流的客户端封装
把零散的 requests.get 收拢成一个可复用客户端,加上超时、重试、简单限流和统一错误处理,所有市场共用它:
python
import os, time, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class QuoteClient:
def __init__(self, base=None, key=None, qps=5):
self.base = base or os.environ["QUOTE_API_BASE"]
self.key = key or os.environ["QUOTE_API_KEY"]
self._min_interval = 1.0 / qps
self._last = 0.0
self.s = requests.Session()
retry = Retry(total=3, backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504])
self.s.mount("https://", HTTPAdapter(max_retries=retry))
def _throttle(self):
wait = self._min_interval - (time.time() - self._last)
if wait > 0:
time.sleep(wait)
self._last = time.time()
def get(self, path, **params):
self._throttle()
params["key"] = self.key
r = self.s.get(f"{self.base}{path}", params=params, timeout=10)
r.raise_for_status()
body = r.json()
if body.get("code") != 200:
raise RuntimeError(f"{body.get('code')}: {body.get('message')}")
return body["data"]
# 用法
q = QuoteClient()
print(q.get("/crypto/lastPrice", symbols="BTCUSDT"))
print(q.get("/stock/indices", countryId=14)[:2])
业务层只关心 q.get(path, **params),鉴权、限流、重试都被封装掉了。
八、踩坑记录
几个实际遇到、文档里不那么显眼的点,列出来给后来者:
- K 线
interval有两套写法 :股票模块常用PT15M / P1D(ISO8601),外汇/加密模块用5m / 1d,别搞混。 - WebSocket 不发心跳会被断:连上不等于一直在,务必起定时器发心跳。
code不等于 HTTP 状态码 :HTTP 200 不代表业务成功,要再判 body 里的code。- 市场标识先确认:不同市场用数字 ID 区分,接入前先把目标市场的 ID 对一遍。
- key 必须服务端持有:前端直连会泄露 key,统一走后端代理。
- 涨跌幅格式不统一 :有的字段是纯数字、有的是已拼好的字符串(带
%/符号),渲染时统一处理。
九、小结
这套"统一信封 + query 鉴权 + WebSocket 心跳"的范式,让四类资产的接入几乎收敛成同一套代码,REST 负责列表与历史、WebSocket 负责实时,工程上很干净。无论你最终用哪家数据源,这套封装(QuoteClient + 重连订阅器)都能直接套用,把对接成本降到最低。
如果这篇对你有帮助,欢迎点赞收藏。下一篇我会用这套封装 + ECharts 做一个实时 K 线看板的实战,有接入问题也可以评论区交流。