做 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 示例
上交所(含科创板)11.600519(贵州茅台)
深交所(含创业板)、北交所00.000001(平安银行)
港股116116.00700(腾讯控股)
美股·纳斯达克105105.AAPL(苹果)
美股·纽交所106106.xxx

注意这里有个很多人都没发现的坑:代码相同、市场编号不同,返回的是另一个品种,而且不会报错。 比如 0.000001 是平安银行,1.000001 是上证指数。我们实测,把平安银行误写成 1.000001,接口会正常返回上证指数的数据,程序不会有任何提示。

所以个股可以按代码首位推断市场,指数切记要手动指定:

Bash
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)

先建一个通用的请求函数。东财接口偶尔会主动断开连接,所以这里一开始就加上会话复用、浏览器请求头和重试退避,原因在后面会细讲。

Bash
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涨跌幅(%)
Bash
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"))

返回结果:

Bash
{"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 用逗号拼接,可以混合不同市场:

Bash
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)

Bash
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 线是一个逗号分隔的字符串,列顺序是:

列012345678910
含义日期开盘收盘最高最低成交量成交额振幅涨跌幅涨跌额换手率

大多数人习惯按 OHLC(开高低收)的顺序读,但东财是 开、收、高、低。按 OHLC 解析不会报错,只是画出来的 K 线全是错的,回测结果也跟着错,而且很难发现。

3.4 全市场列表与涨幅榜(clist/get)

想拿“全部 A 股实时行情”,或者做涨幅榜、选股扫描,用的是 clist/get:

Bash
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 / 返回空

典型报错是:

Bash
requests.exceptions.ConnectionError: ('Connection aborted.',
RemoteDisconnected('Remote end closed connection without response'))

这通常不是你的代码有问题,而是服务端主动断开了连接,多半是请求太密集触发了风控。用 AKShare 时,stock_zh_a_spot_em 这类需要扫描全市场的函数最容易遇到。

解决办法:

  1. 降频并加随机抖动:顺序请求,每次间隔 1 秒左右再加一个随机值,不要多线程并发去拉东财;
  2. 复用会话:用 requests.Session(),避免每次请求都重新建立 TCP/TLS 连接;
  3. 带浏览器请求头:设置 User-Agent 和 Referer;
  4. 失败重试用指数退避:第三章的 em_get 已经实现;
  5. 能缓存就缓存:日线这类数据收盘后不会变,存到本地数据库,不要每次运行都从头拉。

如果用了代理还是被封,先确认请求真的走了代理(先请求一个返回出口 IP 的地址检查一下)。社区里不少案例是代理配置没生效,被封的仍然是本机 IP。

4.2 返回 data: null

Bash
{"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 用后缀:

市场东财 secidInfoway 代码
上交所1.600519600519.SH
深交所0.000001000001.SZ
上证指数1.000001000001.SH
港股116.0070000700.HK(补足 5 位)
美股105.AAPL / 106.xxxAAPL.US

批量转换可以直接用这个函数:

Bash
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)

Bash
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"])

返回示例:

Bash
{
  "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说明
代码f57f12s带交易所后缀
最新价f43(需 fltt=2)f2p无需换算
成交量f47f5v—
成交额f48f6vw—
时间f86(秒)—t毫秒时间戳
主动买卖方向无无td1 主动买 / 2 主动卖

7.3 五档盘口

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

输出:

Bash
买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 2

7.4 历史 K 线:解决1 分钟线只有当天

Infoway 的 K 线接口支持 1 分钟到年线共 12 种周期,单个产品一次最多 500 根,可以用 timestamp 参数向前翻页。返回的字段带名字(o h l c),不会有东财那种列顺序问题。

Bash
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 的榜单接口:

Bash
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 客户端,可以直接运行:

Bash
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())

运行后的输出:

Bash
成交 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 试一试。