多市场行情 API 接入实战:用 Python 打通股票与指数数据

多市场行情 API 接入实战:用 Python 打通股票与指数数据

本文面向需要接入全球市场数据的开发者,演示如何使用一组 HTTP API 完成市场列表、股票查询和指数行情的最小闭环,并给出分页、重试、缓存和 WebSocket 预留方案。示例中的密钥均为占位符,请替换为你自己的访问凭证。

一、为什么先把行情接入层做薄

在做量化回测、行情看板或资产监控时,最容易被低估的是数据接入层。业务代码往往只需要三个结果:

  • 根据国家或交易所拿到可用标的列表;
  • 根据 symbol 或内部 id 查询单个标的;
  • 获取指数的最新价、涨跌额、涨跌幅和时间戳。

如果每个市场都单独对接一套 SDK,后续会出现字段不统一、时区处理分散、限流策略难以复用等问题。更稳妥的做法是先封装一个小型客户端,把鉴权、超时、重试和错误转换集中起来,业务层只处理标准化后的 JSON。

本文使用的接口同时覆盖股票、外汇、期货和加密资产场景;股票部分支持多个国家和地区,接口文档中列出了印度、马来西亚、印度尼西亚、美国、韩国、新加坡、巴西、哥伦比亚、日本、越南、沙特阿拉伯等市场。实际可用范围、频率限制和权限以当前文档为准。

二、接口模型与调用约定

从调用方角度,可以把接口分成两种模式:

  1. HTTP 拉取:适合初始化标的、定时刷新快照、生成日报或回测数据。
  2. WebSocket 推送:适合需要持续接收变更的监控和实时看板。连接地址、订阅报文和推送字段应以开通后文档为准,客户端需要预留心跳、重连和重复消息处理。

当前公开文档给出的通用约定如下:

  • 请求方式以 GET 为主;
  • 访问凭证通过查询参数 key 传入;
  • 返回体为 JSON,通常包含 codemessagedata
  • 列表接口使用 pageSizepage 分页;
  • 时间字段以 Unix 时间戳返回,落库前建议统一转换为 UTC 或业务时区。

不要把访问凭证硬编码到仓库、日志或前端代码中。本文所有示例都从环境变量读取 MARKET_API_KEY

三、接入前的最小准备

3.1 获取访问凭证

先阅读接口文档,确认账号权限、可用市场和请求频率,再申请自己的 key。测试环境与生产环境建议使用不同凭证。

3.2 选择一个明确的基地址

公开示例使用的 API 基地址为:

text 复制代码
https://api.stocktv.top

建议将它配置化,而不是散落在业务代码中:

bash 复制代码
export MARKET_API_BASE="https://api.stocktv.top"
export MARKET_API_KEY="replace-with-your-key"

3.3 先做一次可观测性检查

正式接入前,至少记录以下信息:请求路径、HTTP 状态码、接口返回的 code、耗时、重试次数和响应体大小。不要记录完整 key,也不要把整段行情响应无脱敏地写入公共日志。

四、Python 实战:市场列表、股票查询与指数快照

下面的客户端使用 Python 和 requests,把鉴权、超时、状态检查和有限重试放在同一处,便于后续扩展 K 线、新闻或其他资产类别。

4.1 封装一个可复用的 HTTP 客户端

python 复制代码
import os
import time
from typing import Any, Dict, Optional

import requests


class MarketApiError(RuntimeError):
    """接口返回业务错误或网络错误时抛出。"""


class MarketClient:
    def __init__(
        self,
        base_url: Optional[str] = None,
        api_key: Optional[str] = None,
        timeout: float = 8.0,
        max_retries: int = 2,
    ) -> None:
        self.base_url = (base_url or os.getenv("MARKET_API_BASE", "https://api.stocktv.top")).rstrip("/")
        self.api_key = api_key or os.getenv("MARKET_API_KEY")
        if not self.api_key:
            raise ValueError("请设置 MARKET_API_KEY 环境变量")

        self.timeout = timeout
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers.update({"Accept": "application/json"})

    def get(self, path: str, **params: Any) -> Dict[str, Any]:
        params["key"] = self.api_key
        url = f"{self.base_url}{path}"

        last_error: Optional[Exception] = None
        for attempt in range(self.max_retries + 1):
            try:
                response = self.session.get(url, params=params, timeout=self.timeout)
                response.raise_for_status()
                payload = response.json()
                if payload.get("code") not in (None, 200):
                    raise MarketApiError(
                        f"业务失败 code={payload.get('code')} message={payload.get('message')}"
                    )
                return payload
            except (requests.RequestException, ValueError, MarketApiError) as exc:
                last_error = exc
                if attempt >= self.max_retries:
                    break
                # 简单指数退避:0.5s、1s......避免瞬时网络抖动时连续打满接口
                time.sleep(0.5 * (2 ** attempt))

        raise MarketApiError(f"请求失败: {url}") from last_error

    def list_stocks(self, country_id: int, page: int = 1, page_size: int = 10):
        return self.get(
            "/stock/stocks",
            countryId=country_id,
            page=page,
            pageSize=page_size,
        )

    def query_stocks(
        self,
        *,
        stock_id: Optional[int] = None,
        name: Optional[str] = None,
        symbol: Optional[str] = None,
        url: Optional[str] = None,
    ):
        params = {
            "id": stock_id,
            "name": name,
            "symbol": symbol,
            "url": url,
        }
        # 不传空查询条件,避免不同网关对 null 的解释不一致
        return self.get("/stock/queryStocks", **{k: v for k, v in params.items() if v is not None})

    def list_indices(self, country_id: int, flag: Optional[str] = None):
        params = {"countryId": country_id}
        if flag:
            params["flag"] = flag
        return self.get("/stock/indices", **params)

4.2 拉取市场列表并处理分页

市场列表接口的核心参数是 countryIdpageSizepage。返回的 data 中包含 recordstotalpages 等分页信息。不要把 total 当成当前页记录数,应该以 records 为本页数据。

python 复制代码
def fetch_all_stocks(client: MarketClient, country_id: int, page_size: int = 100):
    first = client.list_stocks(country_id, page=1, page_size=page_size)
    page_data = first["data"]
    rows = list(page_data.get("records", []))

    total_pages = int(page_data.get("pages") or 1)
    for page in range(2, total_pages + 1):
        current = client.list_stocks(country_id, page=page, page_size=page_size)
        rows.extend(current["data"].get("records", []))
    return rows


client = MarketClient()
stocks = fetch_all_stocks(client, country_id=14, page_size=50)
print(f"拉取到 {len(stocks)} 条记录")
print(stocks[0].get("symbol"), stocks[0].get("last"))

在生产环境中,不建议每次启动都拉取全量市场。可以把 symbolidcountryIdexchangeId 保存到本地表,定时任务只同步新增或发生变化的记录。

4.3 查询单只股票

查询接口支持 idnamesymbolurl 等条件,均可按需传入。下面以代码和名称组合为例:

python 复制代码
result = client.query_stocks(symbol="MDCH")
for item in result.get("data", []):
    print({
        "symbol": item.get("symbol"),
        "name": item.get("name"),
        "last": item.get("last"),
        "chg_pct": item.get("chgPct"),
        "time": item.get("time"),
    })

行情字段中,last 表示最新价,high / low 表示最高价和最低价,chg / chgPct 表示涨跌额与涨跌幅,volume 表示成交量。价格展示时要参考 lastPairDecimal,避免直接用浮点数拼接页面。

4.4 获取指数快照

指数接口可按 countryId 查询,也可以增加 flag 做进一步筛选:

python 复制代码
indices = client.list_indices(country_id=14, flag="IN")
for index in indices.get("data", []):
    print(
        index.get("name"),
        index.get("symbol"),
        index.get("last"),
        f"{index.get('chgPct')}%",
        index.get("time"),
    )

指数返回示例中包含 namesymbollasthighlowchgchgPctisOpenflagtime。接入看板时可以用 isOpen 判断是否处于交易时段,用 time 检查数据的新鲜度。

五、把快照变成可用的数据管道

API 接通只是第一步。为了让下游策略稳定运行,建议增加三层处理:

5.1 标准化

将不同市场的字段映射到内部模型,例如:

python 复制代码
def normalize_quote(item: dict) -> dict:
    return {
        "instrument_id": item.get("id"),
        "symbol": item.get("symbol"),
        "name": item.get("name"),
        "market": item.get("countryNameTranslated"),
        "price": item.get("last"),
        "change": item.get("chg"),
        "change_pct": item.get("chgPct"),
        "volume": item.get("volume"),
        "timestamp": item.get("time"),
    }

这样,后续接入另一类资产时,策略层不需要感知原始字段命名差异。

5.2 新鲜度检查

不要只判断 HTTP 200。可以根据 time 计算数据年龄,超过阈值就标记为 stale,并在页面上显示"数据更新时间",而不是继续渲染成实时状态。

5.3 缓存与限流

市场列表和公司基础信息变化慢,可以使用 Redis 或本地缓存;指数和报价快照按业务刷新周期缓存数秒。重试只针对网络错误、超时和 5xx,4xx 或业务错误应直接记录并停止重试,避免无效请求放大。

六、WebSocket 实时订阅的客户端骨架

如果业务需要持续接收行情,可以使用文档中提供的 WebSocket 接入方式。由于不同账户的订阅权限和报文可能不同,下面只展示工程骨架,WS_URL、订阅动作和消息字段请以实际开通文档为准:

python 复制代码
import json
import os
import time

import websocket


WS_URL = os.getenv("MARKET_WS_URL", "wss://replace-with-your-ws-endpoint")
API_KEY = os.environ["MARKET_API_KEY"]
SYMBOLS = ["MDCH", "NSEI"]


def on_open(ws):
    # 示例结构仅用于说明位置,实际 action/字段名以接口文档为准
    ws.send(json.dumps({
        "action": "subscribe",
        "key": API_KEY,
        "symbols": SYMBOLS,
    }))


def on_message(ws, message):
    payload = json.loads(message)
    # 生产环境在这里做:序列号校验、去重、标准化、入队
    print("收到推送:", payload)


def run_once():
    client = websocket.WebSocketApp(
        WS_URL,
        on_open=on_open,
        on_message=on_message,
        on_error=lambda ws, err: print("连接异常:", err),
        on_close=lambda ws, code, reason: print("连接关闭:", code, reason),
    )
    client.run_forever(ping_interval=20, ping_timeout=10)


while True:
    try:
        run_once()
    except Exception as exc:
        print("订阅任务异常:", exc)
    time.sleep(3)  # 生产环境建议使用带上限的指数退避

长连接服务至少要实现以下保护:

  • 心跳保活,及时发现半开连接;
  • 断线重连,重连后重新发送订阅列表;
  • 消息去重和顺序校验,避免网络重试造成重复计算;
  • 接收线程与策略计算线程解耦,防止慢消费者阻塞网络读取;
  • WebSocket 不可用时,以短周期 HTTP 快照作为降级方案。

七、常见问题排查

7.1 返回 200 但业务失败

先检查 JSON 中的 codemessage,不要只看 HTTP 状态码。Key 失效、权限不足或参数缺失通常会体现在业务字段中。

7.2 查询不到标的

确认 countryIdsymbolid 是否来自同一市场;建议先调用市场列表接口,保存服务端返回的 symbolid,再用这些值发起查询。

7.3 页面价格与计算结果有小数差异

使用接口返回的精度字段进行格式化,并避免用二进制浮点数直接做金额累计。需要高精度时,使用 Decimal 或整数最小单位。

7.4 请求偶发超时

为连接和读取设置独立超时,限制重试次数,采用指数退避,并为每个请求增加 trace id。不要在异常时无限重试。

八、总结

一个可维护的行情接入层,重点不在于把请求代码写得多复杂,而在于把鉴权、分页、错误处理、时间戳、新鲜度和重连策略统一起来。本文用 Python 完成了三个最小闭环:

  1. 按国家和分页拉取股票市场列表;
  2. symbol / id 查询股票并标准化字段;
  3. 获取指数快照并检查交易状态与数据时间。

在此基础上,可以继续接入 K 线、新闻、外汇、期货和加密资产模块,再将 REST 快照与 WebSocket 推送汇入同一条数据管道。开发测试阶段建议先用少量标的验证字段和延迟,再逐步扩大订阅范围。

说明:接口仅用于个人开发、学习研究和经授权的业务场景,禁止未经许可的二次分发或违法用途;市场数据不构成投资建议。

参考资料


相关推荐
OPEN-F3 分钟前
C++STL教程:容器适配器与实用工具
开发语言·c++
yaoxin52112315 分钟前
507. Java 反射 - 在 BeanFactory 中实现依赖注入
java·开发语言
OPEN-F21 分钟前
C++模板教程:变参模板、折叠表达式与SFINAE
java·开发语言·c++
有点。23 分钟前
C++二叉搜索树进阶
开发语言·c++
HugoStudio_SWAN27 分钟前
【擦除重绘】C++ 控制台动画:弹跳 Logo DVD 屏保效果
开发语言·c++·学习·程序人生
荷蒲34 分钟前
【小白量化Qbuddy】用AI设计miniQMT指标公式计算量化平台
人工智能·python·机器人
阿童木写作38 分钟前
跨境电商图片翻译工具,批量翻译视频字幕一键抠图
人工智能·python·音视频
何以解忧,唯有..2 小时前
Pydantic 介绍与使用:Python 数据校验的现代方案
数据库·python·microsoft
kyle~2 小时前
C++_STL---迭代器失效
开发语言·c++
又幸福了哥3 小时前
Python入门到高级(知识点七)
python