多市场行情 API 接入实战:用 Python 打通股票与指数数据
本文面向需要接入全球市场数据的开发者,演示如何使用一组 HTTP API 完成市场列表、股票查询和指数行情的最小闭环,并给出分页、重试、缓存和 WebSocket 预留方案。示例中的密钥均为占位符,请替换为你自己的访问凭证。
一、为什么先把行情接入层做薄
在做量化回测、行情看板或资产监控时,最容易被低估的是数据接入层。业务代码往往只需要三个结果:
- 根据国家或交易所拿到可用标的列表;
- 根据
symbol或内部id查询单个标的; - 获取指数的最新价、涨跌额、涨跌幅和时间戳。
如果每个市场都单独对接一套 SDK,后续会出现字段不统一、时区处理分散、限流策略难以复用等问题。更稳妥的做法是先封装一个小型客户端,把鉴权、超时、重试和错误转换集中起来,业务层只处理标准化后的 JSON。
本文使用的接口同时覆盖股票、外汇、期货和加密资产场景;股票部分支持多个国家和地区,接口文档中列出了印度、马来西亚、印度尼西亚、美国、韩国、新加坡、巴西、哥伦比亚、日本、越南、沙特阿拉伯等市场。实际可用范围、频率限制和权限以当前文档为准。
二、接口模型与调用约定
从调用方角度,可以把接口分成两种模式:
- HTTP 拉取:适合初始化标的、定时刷新快照、生成日报或回测数据。
- WebSocket 推送:适合需要持续接收变更的监控和实时看板。连接地址、订阅报文和推送字段应以开通后文档为准,客户端需要预留心跳、重连和重复消息处理。
当前公开文档给出的通用约定如下:
- 请求方式以
GET为主; - 访问凭证通过查询参数
key传入; - 返回体为 JSON,通常包含
code、message和data; - 列表接口使用
pageSize、page分页; - 时间字段以 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 拉取市场列表并处理分页
市场列表接口的核心参数是 countryId、pageSize 和 page。返回的 data 中包含 records、total、pages 等分页信息。不要把 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"))
在生产环境中,不建议每次启动都拉取全量市场。可以把 symbol、id、countryId 和 exchangeId 保存到本地表,定时任务只同步新增或发生变化的记录。
4.3 查询单只股票
查询接口支持 id、name、symbol、url 等条件,均可按需传入。下面以代码和名称组合为例:
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"),
)
指数返回示例中包含 name、symbol、last、high、low、chg、chgPct、isOpen、flag 和 time。接入看板时可以用 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 中的 code 和 message,不要只看 HTTP 状态码。Key 失效、权限不足或参数缺失通常会体现在业务字段中。
7.2 查询不到标的
确认 countryId、symbol、id 是否来自同一市场;建议先调用市场列表接口,保存服务端返回的 symbol 和 id,再用这些值发起查询。
7.3 页面价格与计算结果有小数差异
使用接口返回的精度字段进行格式化,并避免用二进制浮点数直接做金额累计。需要高精度时,使用 Decimal 或整数最小单位。
7.4 请求偶发超时
为连接和读取设置独立超时,限制重试次数,采用指数退避,并为每个请求增加 trace id。不要在异常时无限重试。
八、总结
一个可维护的行情接入层,重点不在于把请求代码写得多复杂,而在于把鉴权、分页、错误处理、时间戳、新鲜度和重连策略统一起来。本文用 Python 完成了三个最小闭环:
- 按国家和分页拉取股票市场列表;
- 按
symbol/id查询股票并标准化字段; - 获取指数快照并检查交易状态与数据时间。
在此基础上,可以继续接入 K 线、新闻、外汇、期货和加密资产模块,再将 REST 快照与 WebSocket 推送汇入同一条数据管道。开发测试阶段建议先用少量标的验证字段和延迟,再逐步扩大订阅范围。
说明:接口仅用于个人开发、学习研究和经授权的业务场景,禁止未经许可的二次分发或违法用途;市场数据不构成投资建议。