多市场行情 API 接入实战:一套接口打通股票/外汇/期货/加密货币 + WebSocket 实时推送

最近做一个多市场行情看板,需要同时拿到股票、外汇、期货、加密货币四类数据,还要支持实时刷新。逐家对接不同数据源太重,于是我把整个接入过程抽象成一套通用范式:统一的 REST 请求 + WebSocket 推送。这篇把这套方法、可复用的客户端封装,以及踩过的坑整理出来,方便有同样需求的同学少走弯路。

文中接口地址统一用占位域名 https://api.example.com,你换成自己所用数据源的地址即可,方法是通用的。

目录

  • 需求与选型思路
  • 鉴权模型:一个 key 走天下
  • 第一个请求:拉一页行情列表
  • 指数 / K 线 / 排行榜 / 日历
  • 外汇、期货、加密货币:同一套范式
  • 实时行情:WebSocket 接入与心跳
  • 工程化:带重试和限流的客户端封装
  • 踩坑记录
  • 小结

一、需求与选型思路

项目要做一个"多市场资产行情墙",核心诉求三点:

  1. 覆盖面:A 股之外还要美股、印度、东南亚、日韩等多个市场,加上外汇、商品期货、主流加密货币;
  2. 统一性:不想为每个市场对接不同的 SDK,最好是一套 HTTP 风格的接口 + 统一的返回结构;
  3. 实时性:列表页用轮询可以接受,但详情页和盯盘页需要 WebSocket 推送。

选数据源时,我把"接入成本"作为首要标准。最省事的那类接口往往有几个共同特征,可以作为选型 checklist:

  • 全部 GET + query 参数,无需复杂签名计算;
  • 返回统一的 {code, message, data} 信封,错误处理可以收敛成一处;
  • 各类资产(股票/外汇/期货/加密)路径前缀不同、但参数与返回风格一致,封装一次即可复用;
  • 同时提供 REST 与 WebSocket 两种接入方式。

下面的代码就以这种"统一信封 + query 鉴权"的接口为例。

二、鉴权模型:一个 key 走天下

这类接口通常共用一个 key 参数,放在 query 里即可,没有复杂的签名。先把它抽成环境变量,别硬编码进代码:

# .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 跑一下:

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"]}%')

典型返回是分页信封,核心字段:

{
  "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 是数组:

{
  "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 线组件就行:

{ "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 外汇

fx = get("/market/currency", countryType="sg")        # 实时汇率列表
get("/market/todayMarket", symbol="EUR=X")             # 单个币对
get("/market/chart", symbol="EURUSD=X", interval="5m") # K 线
{ "symbol": "EURUSD=X", "name": "EUR/USD", "lastPrice": "1.0765", "chg": "+0.0016", "chgPct": "+0.15%" }

5.2 期货

get("/futures/list")                              # 期货市场列表
get("/futures/querySymbol", symbol="XAG")         # 单品种行情(如白银)
get("/futures/kline", symbol="EUA", interval="1") # K 线

买卖价、最高最低、成交量一应俱全,商品/能源类品种都覆盖。

5.3 加密货币

加密这块字段和主流交易所基本对齐,熟悉的同学几乎零学习成本:

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")                 # 近期成交
{
  "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>

要点就一个:连上之后要定时发心跳维持连接,否则会被服务端断开。推送过来的是逐笔行情快照:

{
  "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 实现一个带自动重连 + 心跳的订阅器:

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 收拢成一个可复用客户端,加上超时、重试、简单限流和统一错误处理,所有市场共用它:

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),鉴权、限流、重试都被封装掉了。

八、踩坑记录

几个实际遇到、文档里不那么显眼的点,列出来给后来者:

  1. K 线 interval 有两套写法:股票模块常用 PT15M / P1D(ISO8601),外汇/加密模块用 5m / 1d,别搞混。
  2. WebSocket 不发心跳会被断:连上不等于一直在,务必起定时器发心跳。
  3. code 不等于 HTTP 状态码:HTTP 200 不代表业务成功,要再判 body 里的 code
  4. 市场标识先确认:不同市场用数字 ID 区分,接入前先把目标市场的 ID 对一遍。
  5. key 必须服务端持有:前端直连会泄露 key,统一走后端代理。
  6. 涨跌幅格式不统一:有的字段是纯数字、有的是已拼好的字符串(带 %/符号),渲染时统一处理。

九、小结

这套"统一信封 + query 鉴权 + WebSocket 心跳"的范式,让四类资产的接入几乎收敛成同一套代码,REST 负责列表与历史、WebSocket 负责实时,工程上很干净。无论你最终用哪家数据源,这套封装(QuoteClient + 重连订阅器)都能直接套用,把对接成本降到最低。

如果这篇对你有帮助,欢迎点赞收藏。下一篇我会用这套封装 + ECharts 做一个实时 K 线看板的实战,有接入问题也可以评论区交流。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐