做 A 股相关的开发,十有八九会碰到东方财富的接口。AKShare 里大量以 _em 结尾的函数、efinance 这类开源库、网上各种“免费 A 股实时行情 API”教程,背后调用的基本都是东方财富网页端的数据地址——push2.eastmoney.com 和 push2his.eastmoney.com。不少人搜索时也会写成“东方财经 API”,说的其实是同一套东西。
它的优点很明显:免费、不用注册、返回 JSON、覆盖 A 股/港股/美股,还能拿到 K 线和全市场列表。但它和新浪财经接口有同一个本质问题:这不是一套对外开放的 API。没有官方文档,字段全是 f43、f170 这样的编号,单页条数、K 线深度、请求频率随时可能被调整,用在线上服务里很容易出问题。
我们做 Infoway 行情 API 的这几年,接到过不少东财接口的迁移咨询。这篇文章尽量把东方财富 API 讲清楚,以及开发者最常遇到的报错和解决办法。文中的代码我们在 2026 年 11 月实际跑过。最后再说说它在哪些场景下不够用,以及怎样迁移到 Infoway。
本文的上篇是 新浪财经 API 接口详解,如果你同时在用新浪接口,可以对照着看。
一、东方财富 API 是什么
东方财富网的行情页面(如 quote.eastmoney.com)由浏览器向几组数据接口发请求来渲染。这些接口返回 JSON,社区把它们整理出来后,就成了大家常说的“东方财富 API”。
常用的接口有四个:
| 用途 | 接口地址 | 说明 |
|---|---|---|
| 单只股票实时快照 | push2.eastmoney.com/api/qt/stock/get | 最新价、高低开收、成交量额、涨跌幅等 |
| 多只股票批量快照 | push2.eastmoney.com/api/qt/ulist.np/get | 一次传多个 secid |
| 全市场列表 / 排行 | push2.eastmoney.com/api/qt/clist/get | 涨幅榜、全市场扫描,需要分页 |
| 历史 K 线 | push2his.eastmoney.com/api/qt/stock/kline/get | 分钟线到月线,支持前/后复权 |
1.1 东方财富 API 接口的价格是多少?
东方财富 API 接口主要分两类,一是上面提到的网页数据接口,由广大网友参与“研发”的破解版,完全免费,本文主要介绍的是这一类。
而另一类,则是东方财富官方推出的付费数据接口,即 Choice 金融终端配套的量化接口(EmQuantAPI),需要联系客户经理开通 Choice 账号。网传的收费是个人版 12000/年,开户条件未知,有了解的同学可以和我们分享一下哈。
再次强调,本文讲的是大家免费在用的网页数据接口,不是Choice。
二、接入前必懂:secid 规则
东财接口不直接用股票代码,而是用 secid,格式是 市场编号.代码:
| 市场 | 市场编号 | secid 示例 |
|---|---|---|
| 上交所(含科创板) | 1 | 1.600519(贵州茅台) |
| 深交所(含创业板)、北交所 | 0 | 0.000001(平安银行) |
| 港股 | 116 | 116.00700(腾讯控股) |
| 美股·纳斯达克 | 105 | 105.AAPL(苹果) |
| 美股·纽交所 | 106 | 106.xxx |
注意这里有个很多人都没发现的坑:代码相同、市场编号不同,返回的是另一个品种,而且不会报错。 比如 0.000001 是平安银行,1.000001 是上证指数。我们实测,把平安银行误写成 1.000001,接口会正常返回上证指数的数据,程序不会有任何提示。
所以个股可以按代码首位推断市场,指数切记要手动指定:
def to_secid(code: str) -> str:
"""把 6 位 A 股代码转成东财 secid(仅适用于个股,指数请手动指定)"""
if code.startswith(("6", "9")): # 沪市主板、科创板、沪B
return f"1.{code}"
return f"0.{code}" # 深市主板、创业板、北交所三、东方财富 API 接入教程(Python)
先建一个通用的请求函数。东财接口偶尔会主动断开连接,所以这里一开始就加上会话复用、浏览器请求头和重试退避,原因在后面会细讲。
import random
import time
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Referer": "https://quote.eastmoney.com/",
})
def em_get(url, params, retries=3):
"""带重试与退避的 GET,专门应对东财接口的偶发断连"""
for i in range(retries):
try:
resp = session.get(url, params=params, timeout=10)
resp.raise_for_status()
return resp.json()
except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e:
wait = 2 ** i + random.random()
print(f"请求失败({e.__class__.__name__}),{wait:.1f}s 后重试")
time.sleep(wait)
raise RuntimeError("东财接口连续失败,可能已被限流")3.1 实时行情快照(stock/get)
通过 fields 参数指定要返回哪些字段。下面是最常用的一组字段编号:
| 字段 | 含义 | 字段 | 含义 |
|---|---|---|---|
f57 | 代码 | f58 | 名称 |
f43 | 最新价 | f60 | 昨收 |
f44 | 最高价 | f45 | 最低价 |
f46 | 开盘价 | f47 | 成交量(A 股的单位为“手”,港美股为“股”) |
f48 | 成交额(元) | f86 | 行情时间(秒级时间戳) |
f169 | 涨跌额 | f170 | 涨跌幅(%) |
QUOTE_FIELDS = {
"f57": "code", "f58": "name", "f43": "price", "f44": "high",
"f45": "low", "f46": "open", "f60": "prev_close", "f47": "volume",
"f48": "amount", "f169": "change", "f170": "pct_chg", "f86": "ts",
}
def em_quote(secid):
data = em_get(
"https://push2.eastmoney.com/api/qt/stock/get",
{"secid": secid, "fields": ",".join(QUOTE_FIELDS), "fltt": 2},
)
if data.get("data") is None: # rc=100:secid 不存在
raise ValueError(f"无效的 secid: {secid}")
return {QUOTE_FIELDS[k]: v for k, v in data["data"].items()}
print(em_quote(to_secid("600519")))
print(em_quote("116.00700"))返回结果:
{"rc":0,"data":{"f43":1440.11,"f44":1443.66,"f45":1431.26,"f46":1438.23,
"f47":12090,"f48":1738138295.0,"f57":"600519","f58":"贵州茅台",
"f60":1444.99,"f86":1794280415,"f169":-4.88,"f170":-0.34}}注意 fltt=2 这个参数。不传的话,价格会以整数返回,例如 "f43":144011,需要再除以 10^f59(f59 是小数位数,A 股一般是 2,港股是 3)。很多人看到茅台股价 14 万,就是因为漏了这个参数。
3.2 批量行情(ulist.np/get)
要同时看多只股票,用 ulist.np/get,secids 用逗号拼接,可以混合不同市场:
def em_batch_quote(secids):
data = em_get(
"https://push2.eastmoney.com/api/qt/ulist.np/get",
{"secids": ",".join(secids), "fields": "f12,f14,f2,f3,f4,f5,f6", "fltt": 2},
)
return [
{"code": d["f12"], "name": d["f14"], "price": d["f2"],
"pct_chg": d["f3"], "volume": d["f5"], "amount": d["f6"]}
for d in data["data"]["diff"]
]
for row in em_batch_quote(["1.600519", "0.000001", "116.00700", "105.AAPL"]):
print(row)这里又有一个坑:同一个含义,在不同接口里的字段编号不一样。单只快照里最新价是 f43,批量和列表接口里最新价却是 f2,涨跌幅是 f3(单只快照里是 f170)。从一个接口迁到另一个接口时,字段映射要重新对一遍。
3.3 历史 K 线(kline/get)
def em_kline(secid, klt=101, fqt=1, limit=120):
data = em_get(
"https://push2his.eastmoney.com/api/qt/stock/kline/get",
{
"secid": secid,
"fields1": "f1,f2,f3,f4,f5,f6",
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61",
"klt": klt, "fqt": fqt, "end": "20500101", "lmt": limit,
},
)
rows = []
for line in data["data"]["klines"]:
f = line.split(",")
rows.append({
"date": f[0],
"open": float(f[1]),
"close": float(f[2]), # 注意:第 3 列是收盘价,不是最高价
"high": float(f[3]),
"low": float(f[4]),
"volume": float(f[5]),
"amount": float(f[6]),
"pct_chg": float(f[8]),
"turnover": float(f[10]),
})
return rows
for r in em_kline("0.000001", klt=101, fqt=1, limit=5):
print(r)参数说明:
klt:周期。1/5/15/30/60为分钟线,101日线、102周线、103月线;fqt:复权方式。0不复权、1前复权、2后复权;end+lmt:从end日期往前取lmt根,end=20500101表示取到最新。
K 线是最容易解析错的地方。 每根 K 线是一个逗号分隔的字符串,列顺序是:
| 列 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 含义 | 日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 |
大多数人习惯按 OHLC(开高低收)的顺序读,但东财是 开、收、高、低。按 OHLC 解析不会报错,只是画出来的 K 线全是错的,回测结果也跟着错,而且很难发现。
3.4 全市场列表与涨幅榜(clist/get)
想拿“全部 A 股实时行情”,或者做涨幅榜、选股扫描,用的是 clist/get:
def em_all_a_shares(page_size=100, max_pages=None):
"""分页拉取沪深 A 股列表;东财单页最多返回 100 条"""
url = "https://push2.eastmoney.com/api/qt/clist/get"
fs = "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23" # 深主板、创业板、沪主板、科创板
result, page = [], 1
while True:
data = em_get(url, {
"pn": page, "pz": page_size, "po": 1, "np": 1, "fltt": 2,
"fid": "f3", "fs": fs, "fields": "f12,f14,f2,f3,f6",
})["data"]
result.extend(data["diff"])
if len(result) >= data["total"] or (max_pages and page >= max_pages):
break
page += 1
time.sleep(1 + random.random()) # 限速 + 随机抖动,降低被封概率
return result参数含义:pn 页码、pz 每页条数、fid 排序字段(f3 即按涨跌幅)、po=1 降序、fs 市场筛选。
很多老教程写 pz=5000,想一次拉完全市场。我们实测,现在不管 pz 传多少,单页最多只返回 100 条,而 total 显示沪深 A 股约 5,500 只,所以拉一次全市场要请求 50 多次。这也是近一年 stock_zh_a_spot_em 这类函数变慢、更容易被封的直接原因,原来一次请求能完成的事,现在要分几十次请求,而请求频率一高直接封IP。
四、东方财富 API 常见问题与解决方案
在动手写这篇教程之前,我们在Github issue、量化社区中搜集了一些经常被问到的问题,并整理出解决方案,希望对各位同学有帮助。
4.1 报错 RemoteDisconnected / Connection aborted / 返回空
典型报错是:
requests.exceptions.ConnectionError: ('Connection aborted.',
RemoteDisconnected('Remote end closed connection without response'))这通常不是你的代码有问题,而是服务端主动断开了连接,多半是请求太密集触发了风控。用 AKShare 时,stock_zh_a_spot_em 这类需要扫描全市场的函数最容易遇到。
解决办法:
- 降频并加随机抖动:顺序请求,每次间隔 1 秒左右再加一个随机值,不要多线程并发去拉东财;
- 复用会话:用
requests.Session(),避免每次请求都重新建立 TCP/TLS 连接; - 带浏览器请求头:设置
User-Agent和Referer; - 失败重试用指数退避:第三章的
em_get已经实现; - 能缓存就缓存:日线这类数据收盘后不会变,存到本地数据库,不要每次运行都从头拉。
如果用了代理还是被封,先确认请求真的走了代理(先请求一个返回出口 IP 的地址检查一下)。社区里不少案例是代理配置没生效,被封的仍然是本机 IP。
4.2 返回 data: null
{"rc":100,"data":null}rc=100 表示这个 secid 不存在,最常见的原因是市场编号写错,或者股票已经退市。另外要记住前面说的:返回了数据不代表代码正确,1.000001 会返回上证指数而不是平安银行。建议每次把返回的 f57(代码)和 f58(名称)与预期核对一下。
4.3 价格多了两位数 / 成交量对不上
- 价格是整数(如
144011):加上fltt=2,或者手动除以10^f59; - 成交量差 100 倍:A 股的
f47/f5单位是“手”,港股和美股是“股”,跨市场汇总前要先统一单位; - 成交额单位是“元”,展示时通常再除以 1 万或 1 亿。
4.4 K 线高低价看起来不对
列顺序是 开、收、高、低,不是 OHLC(开高低收),见 3.3 节。你可以检查一下:如果解析出来的最高价经常低于最低价,或者低于开盘价,基本就是列顺序读错了。
4.5 全市场只拿到 100 条
clist/get 单页最多 100 条,pz 传多大都没用,必须用 pn 翻页,直到累计条数达到 total,代码见 3.4 节,这个基本无解,只能分开多次请求。
4.6 1 分钟 K 线只有当天的数据
我们实测,东财的 1 分钟 K 线只返回当天的数据;5 分钟 K 线大约能取到最近两个月(约 1,500 根)。如果你要用分钟级数据做回测,东财的历史深度基本不够,只能每天收盘后自己存一份,慢慢积累。或者使用Infoway API的产品(我们提供三年的1分钟K线历史数据)。
4.7 返回内容被包在 jQuery123(...) 里
这是 JSONP 格式,因为从浏览器里复制的 URL 带了 cb=jQuery... 参数。直接去掉 cb 参数,接口就会返回纯 JSON。另外,网上的代码常带一个 ut=... 参数,我们测试时不传也能正常返回,但它会不会在某天变成必填项,没有人能保证。
4.8 字段编号去哪查?
查不到,因为东财没有公开过字段说明。目前的做法只有两种:在浏览器开发者工具里看行情页面发了哪些请求、带了哪些字段;或者参考社区整理的字段表(包括本文的表格)。东财调整字段时,你只能等程序出错了才知道。
4.9 能不能实时推送?
东财网页接口本质上是 HTTP 拉取。有个未公开的 api/qt/stock/sse 地址可以用 Server-Sent Events 的方式接收单只股票的快照更新,但它没有文档、没有订阅管理、也没有心跳和重连规范,不适合在生产环境依赖。想做实时盯盘,大多数人最后还是在轮询,而轮询频率越高越容易被封,又回到了 4.1 的问题。
五、东方财富 API 的边界:它什么时候不够用
先说结论:拿东财接口做个人学习、写一次性脚本、做收盘后的日线分析,完全没问题。 它数据全、返回 JSON、比新浪接口更好解析,资金流向、龙虎榜、A 股概念板块这类东财特色数据也很有价值。
但如果要把它放进需要长期稳定运行的系统里,下面这些问题绕不开:
1. 规则会在没有通知的情况下变化。 单页上限从几千条降到 100 条,就是一个典型例子:没有公告,依赖它的代码直接变慢、触发风控。字段编号、参数、频率阈值都可能这样变,你只能事后发现。
2. 没有真正的实时推送。 轮询的延迟由刷新间隔决定,而刷新越频繁越容易被封。逐笔成交、盘口变化、分钟 K 线实时更新这类需求,用轮询架构做不好。
3. 历史分钟数据太短。 1 分钟线只有当天,5 分钟线约两个月,做分钟级策略回测远远不够。
4. 风控对批量场景不友好。 全市场扫描本身就要发几十次请求,再加上多个周期、多个市场,很快就会碰到频率限制。没有配额说明,你不知道上限在哪里。
六、东方财富 API vs Infoway API
| 对比维度 | 东方财富网页接口 | Infoway API |
|---|---|---|
| 定位 | 网页行情数据接口(非正式 API) | 面向个人和企业的专业行情 API |
| 文档 | 无官方文档,字段靠社区整理 | 完整中英文文档 + Python/Java/Go/Node.js SDK |
| 字段 | f43、f170 编号,各接口不统一 | 统一短字段名:p 价格、v 量、vw 额 |
| 鉴权与配额 | 无鉴权,风控阈值不公开 | API Key 鉴权,按套餐公开频率限制,业务接口不按 IP 限制 |
| 实时推送 | 无(只有未公开的 SSE 地址) | WebSocket 推送成交、盘口、K 线 |
| 盘口 | 网页快照为主 | 五档买卖盘结构化返回 |
| 1 分钟 K 线历史 | 只有当天 | 单次 500 根,可按时间戳向前翻页 |
| K 线字段顺序 | 开、收、高、低(易读错) | 带字段名的 JSON:o h l c |
| 全市场排行 | clist 单页 100 条,需翻页 | 榜单接口,支持排序和翻页 |
| 市场覆盖 | A 股、港股、美股等 | A 股、港股、美股、日股、韩股、印度、台股、外汇、加密货币、期货等 30,000+ 品种 |
| 特色数据 | 资金流向、龙虎榜、概念板块 | 复权因子、财务数据、行业板块、市场温度、实时新闻 |
需要说明的是,东财的资金流向、龙虎榜、A 股概念板块等特色数据,Infoway 目前没有完全覆盖。如果你的核心需求就是这些数据,东财接口仍有它的价值。Infoway 主要解决的是另一类问题:实时行情、盘口、K 线这些核心行情数据要稳定、要实时、要能放心用在产品里。
七、迁移到 Infoway:东财代码一一对应
迁移主要改两件事:代码格式和字段读取方式。
7.1 secid → Infoway 代码
东财用前缀数字区分市场,Infoway 用后缀:
| 市场 | 东财 secid | Infoway 代码 |
|---|---|---|
| 上交所 | 1.600519 | 600519.SH |
| 深交所 | 0.000001 | 000001.SZ |
| 上证指数 | 1.000001 | 000001.SH |
| 港股 | 116.00700 | 00700.HK(补足 5 位) |
| 美股 | 105.AAPL / 106.xxx | AAPL.US |
批量转换可以直接用这个函数:
def secid_to_infoway(secid: str) -> str:
"""东财 secid → Infoway 代码,例如 1.600519 → 600519.SH"""
market, code = secid.split(".", 1)
if market == "1":
return f"{code}.SH"
if market == "0":
return f"{code}.SZ"
if market == "116":
return f"{code.zfill(5)}.HK"
if market in ("105", "106", "107"):
return f"{code}.US"
raise ValueError(f"暂未映射的市场: {secid}")Infoway 的代码自带交易所后缀,000001.SZ 和 000001.SH 不会混淆,4.2 节那种 代码对了 -> 市场错了 ->返回另一只股票 的问题就不存在了。
7.2 实时行情(对应 stock/get、ulist.np/get)
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://data.infoway.io"
HEADERS = {"apiKey": API_KEY}
def infoway_trade(codes):
url = f"{BASE}/stock/batch_trade/{','.join(codes)}"
resp = requests.get(url, headers=HEADERS, timeout=10).json()
if resp["ret"] != 200:
raise RuntimeError(resp["msg"])
return resp["data"]
for item in infoway_trade(["600519.SH", "000001.SZ", "00700.HK"]):
print(item["s"], "最新价:", item["p"], "成交量:", item["v"],
"成交额:", item["vw"], "方向:", item["td"])返回示例:
{
"ret": 200,
"msg": "success",
"data": [
{"s": "600519.SH", "t": 1794280452017, "p": "1440.31", "v": "1", "vw": "144031.00", "td": 2},
{"s": "000001.SZ", "t": 1794280455122, "p": "11.83", "v": "2", "vw": "2366.00", "td": 1}
]
}一次最多查 100 个产品,A 股、港股、美股可以混在同一个请求里。返回顺序不保证和请求顺序一致,请按 s 字段匹配。
东财字段 → Infoway 字段对照表:
| 含义 | 东财(stock/get) | 东财(ulist/clist) | Infoway | 说明 |
|---|---|---|---|---|
| 代码 | f57 | f12 | s | 带交易所后缀 |
| 最新价 | f43(需 fltt=2) | f2 | p | 无需换算 |
| 成交量 | f47 | f5 | v | — |
| 成交额 | f48 | f6 | vw | — |
| 时间 | f86(秒) | — | t | 毫秒时间戳 |
| 主动买卖方向 | 无 | 无 | td | 1 主动买 / 2 主动卖 |
7.3 五档盘口
def infoway_depth(codes):
url = f"{BASE}/stock/batch_depth/{','.join(codes)}"
return requests.get(url, headers=HEADERS, timeout=10).json()["data"]
d = infoway_depth(["600519.SH"])[0]
ask_prices, ask_vols = d["a"] # a = 卖盘
bid_prices, bid_vols = d["b"] # b = 买盘
for i in range(5):
print(f"买{i+1} {bid_prices[i]} x {bid_vols[i]} 卖{i+1} {ask_prices[i]} x {ask_vols[i]}")输出:
买1 1438.61 x 2 卖1 1439.23 x 1
买2 1438.50 x 2 卖2 1439.29 x 1
买3 1438.44 x 1 卖3 1439.30 x 1
买4 1438.43 x 1 卖4 1439.33 x 1
买5 1438.06 x 1 卖5 1439.35 x 27.4 历史 K 线:解决1 分钟线只有当天
Infoway 的 K 线接口支持 1 分钟到年线共 12 种周期,单个产品一次最多 500 根,可以用 timestamp 参数向前翻页。返回的字段带名字(o h l c),不会有东财那种列顺序问题。
import time
def infoway_kline_history(code, kline_type=1, pages=3):
"""单产品向前翻页拉取历史 K 线,每页最多 500 根"""
# kline_type: 1=1分 2=5分 3=15分 4=30分 5=1时 8=日 9=周 10=月
url = f"{BASE}/stock/v2/batch_kline"
all_rows, ts = [], None
for _ in range(pages):
body = {"klineType": kline_type, "klineNum": 500, "codes": code}
if ts:
body["timestamp"] = ts
resp = requests.post(url, json=body, headers=HEADERS, timeout=10).json()
rows = resp["data"][0]["respList"] if resp.get("data") else []
if not rows:
break
all_rows.extend(rows)
ts = int(rows[-1]["t"]) - 1 # 本页最早一根的时间戳 - 1,作为下一页的结束时间
time.sleep(1) # 留在套餐的每秒频率限制之内
return all_rows
bars = infoway_kline_history("600519.SH", kline_type=1, pages=3)
print(len(bars), "根 1 分钟 K 线")我们实测翻 3 页,拿到了 1,500 根茅台 1 分钟 K 线,覆盖最近一周左右的交易时段;继续翻页可以取更早的数据(分钟和小时 K 线的历史范围由套餐决定,最长支持最近三年的一分钟K线)。
两个小提示:多个产品一起查 K 线时,每个产品最多返回 2 根,所以拉长历史要单个产品查询;
respList按时间从新到旧排列,画图前记得反转。
7.5 涨幅榜(对应 clist/get)
如果你用 clist 主要是为了做涨跌幅榜、成交额榜,可以用 Infoway 的榜单接口:
def infoway_rank(market="CN", sort="chg", order="desc", limit=20):
url = f"{BASE}/common/v2/basic/market/rank/{market}/all"
params = {"sort": sort, "order": order, "limit": limit, "lang": "zh-CN"}
return requests.get(url, headers=HEADERS, params=params, timeout=10).json()
rank = infoway_rank(limit=5)
for s in rank["data"]:
ind = s["indicators"]
print(s["symbol"], s["name"], ind["last_done"], f"{float(ind['chg']) * 100:.2f}%", ind["industry"])sort=chg&order=desc 是涨幅榜,order=asc 是跌幅榜,sort=total_balance 是成交额榜;market 可选 CN、HK、US。返回里还带了换手率、振幅、量比、市值、所属行业等指标,不用再自己拼字段。注意 chg 是小数形式(0.2001 即 20.01%)。榜单适合做选股和扫描,盯盘级别的实时性请用下一章的 WebSocket。
八、东财做不到的:WebSocket 实时推送
这是东财网页接口和专业行情 API 差距最大的地方。Infoway 通过 WebSocket 长连接,由服务端主动推送成交、盘口和 K 线更新,不需要轮询,也就没有“刷新越快越容易被封”的问题。
下面是一个带心跳和断线重连的 Python 客户端,可以直接运行:
import asyncio
import json
import uuid
import websockets
API_KEY = "YOUR_API_KEY"
WS_URL = f"wss://data.infoway.io/ws?business=stock&apikey={API_KEY}"
CODES = "600519.SH"
def msg(code, data=None):
m = {"code": code, "trace": uuid.uuid4().hex}
if data:
m["data"] = data
return json.dumps(m)
async def heartbeat(ws):
while True:
await asyncio.sleep(30) # 每 30 秒发一次应用心跳 10010
await ws.send(msg(10010))
async def run_once():
async with websockets.connect(WS_URL, ping_interval=None) as ws:
# 每次(重新)连接后都要重新订阅
await ws.send(msg(10000, {"codes": CODES})) # 成交
await ws.send(msg(10003, {"codes": CODES})) # 盘口
await ws.send(msg(10006, {"arr": [{"type": 1, "codes": CODES}]})) # 1 分钟 K 线
hb = asyncio.create_task(heartbeat(ws))
try:
async for raw in ws:
if not raw.startswith("{"): # 例如 "Subscribe fail: ..." 这类文本帧
print("提示:", raw)
continue
data = json.loads(raw)
code = data.get("code")
if code == 10002:
t = data["data"]
print(f"成交 {t['s']} {t['p']} x {t['v']} 方向={t['td']}")
elif code == 10005:
d = data["data"]
print(f"盘口 {d['s']} 买一 {d['b'][0][0]} 卖一 {d['a'][0][0]}")
elif code == 10008:
k = data["data"]
print(f"K线 {k['s']} O{k['o']} H{k['h']} L{k['l']} C{k['c']}")
finally:
hb.cancel()
async def main():
delay = 1
while True: # 断线自动重连,指数退避
try:
await run_once()
delay = 1
except (websockets.ConnectionClosed, OSError) as e:
print(f"连接断开: {e},{delay}s 后重连")
await asyncio.sleep(delay)
delay = min(delay * 2, 60)
if __name__ == "__main__":
asyncio.run(main())运行后的输出:
成交 600519.SH 1439.30 x 1 方向=1
盘口 600519.SH 买一 1438.72 卖一 1439.49
K线 600519.SH O1438.62 H1439.30 L1438.50 C1439.30
成交 600519.SH 1438.72 x 1 方向=2
...几个生产环境要点:
- 协议号规律:订阅码 +2 就是推送码,10000→10002 成交,10003→10005 盘口,10006→10008 K 线;
- 心跳必须发:每 30 秒发一次
10010,超过 60 秒没有心跳会被断开。WebSocket 协议层的 Ping 不能代替它; - 断线后要重新订阅:上面的代码在每次重连后都会重新发送订阅;
- 控制消息频率:同一连接上的订阅、取消订阅和心跳合计每分钟不超过 60 条,不要在循环里反复订阅;
- 通道划分:股票用
business=stock,加密货币用crypto,外汇和期货用common。
如果不想自己处理心跳和重连,也可以直接用 Infoway 官方的 Python、Java、Go、Node.js SDK。
九、怎么选
结合我们服务客户的经验,给一个直接的建议:
- 个人学习、课程作业、一次性脚本:东方财富 API 够用,本文第三、四章的代码可以直接拿去用。
- 收盘后的日线分析:东财加本地缓存可以应付;但如果要分钟级回测、多市场数据,东财的历史深度和风控会成为瓶颈。
- 实时盯盘、量化实盘:需要推送,轮询从架构上就满足不了,建议用 Infoway 的 WebSocket。
- 对外的产品、付费平台、券商或交易所终端:这类场景首先是合规问题。数据来源需要有正式的服务协议,直接抓取网页接口不应该出现在这里。
Infoway 新注册账号可以免费试用 7 天,无需信用卡。试用期内可以查询部分代表性品种(A 股是 600519.SH 贵州茅台和 000001.SH 上证指数),本文的示例代码用 600519.SH 就能完整跑通。确认数据和接入方式没问题后,再根据需要的市场和产品数量选择付费套餐。
常见问题(FAQ)
东方财富有官方 API 吗?
有,但收费。东方财富官方的数据接口是 Choice 金融终端配套的量化接口,需要开通 Choice 账号。网上流传的免费“东方财富 API”是网页行情页面使用的数据接口,没有官方文档,也没有对第三方开放。
东财接口报 RemoteDisconnected 怎么办?
基本是请求太频繁,被服务端断开了连接。顺序请求、每次间隔 1 秒以上并加随机抖动、复用 Session、带浏览器请求头、失败后指数退避重试,能大幅减少这类问题。长期运行的服务建议换用有公开配额的数据源。
为什么 pz=5000 也只返回 100 条?
东财目前限制 clist/get 单页最多 100 条,需要用 pn 翻页,直到累计条数达到 total。
东财 K 线怎么解析?klines 里每个元素是逗号分隔的字符串,顺序是日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅、涨跌额、换手率。注意第 3 列是收盘价,不是最高价。
AKShare 的 _em 函数和东方财富 API 是什么关系?
AKShare 里以 _em 结尾的函数(如 stock_zh_a_spot_em、stock_zh_a_hist)底层调用的就是本文介绍的东财接口,所以东财接口的限流、分页、字段调整,都会直接影响这些函数。
从东方财富迁移到 Infoway,代码改动大吗?
主要改两处:把 secid(如 1.600519)换成带后缀的代码(如 600519.SH),以及把 f43、f170 这类字段编号换成 Infoway 的字段名。本文第七章的转换函数和字段对照表可以直接用。
Infoway 能做哪些东财接口做不到的事?
最主要的是 WebSocket 实时推送(成交、盘口、K 线),可翻页的分钟级历史 K 线,主动买卖方向字段 td,公开的频率配额和 API Key 鉴权,以及一套接口覆盖 A 股、港股、美股、日股、韩股、印度、台股、外汇、加密货币和期货。
东方财富 API 是很多人接触 A 股数据的第一站,把 secid、字段编号、K 线顺序和分页规则搞清楚,能少走很多弯路。等项目要上线、要对外服务时,再考虑换成有文档、有配额、有推送、有服务协议的行情 API。到那一步,欢迎来 Infoway 试一试。