New to Claude Skills? Learn how to install them →

simonlin1212 on GitHub

A股全栈数据工具包

Free

Access comprehensive A-share market data seamlessly.

Get this skill

Free · Opens the source repo

What A股全栈数据工具包 does

The A股全栈数据工具包 is a specialized toolkit designed for developers and analysts working with A-share market data. It provides access to a wide range of financial data sources, including real-time quotes, historical K-line data, research reports, and various market signals. With a robust architecture featuring ten layers and 47 tested endpoints, this tool is optimized for retrieving accurate and timely data from multiple sources, including Tencent and Eastmoney, while ensuring minimal risk of IP blocking.

This toolkit is particularly useful for those who need to pull data for quantitative analysis, trading strategies, or research purposes. The built-in functions allow users to retrieve specific market signals, financial statements, and announcements from various platforms, making it a comprehensive resource for anyone involved in A-share trading or investment analysis. The self-contained nature of the skill means it has no external dependencies, simplifying integration into existing workflows.

The A股全栈数据工具包 is ideal for developers and data analysts who require reliable access to A-share market data without the hassle of managing multiple data sources. It is built to handle various data retrieval scenarios, offering fallback options in case of source unavailability, thus ensuring continuous access to critical market information. This makes it an essential tool for anyone looking to enhance their data-driven decision-making process in the A-share market.

However, it is important to note that this toolkit should only be used in contexts where data retrieval is necessary. For discussions that do not require real-time data, such as general investment strategy discussions, this skill is not appropriate and may lead to unnecessary complexity in the conversation.

When to use it

Use this toolkit when you need to access real-time or historical A-share market data for analysis or trading purposes.

When not to use it

Avoid using this skill for discussions that do not require live data retrieval, as it may complicate the interaction unnecessarily.

What you can build with it

Real-time Market Analysis

Utilize the toolkit to pull real-time stock quotes and K-line data for immediate analysis during trading hours.

Historical Data Retrieval

Access historical financial data and research reports to inform long-term investment strategies.

Market Signal Monitoring

Set up alerts for specific market signals such as stock trends and fund flows using the skill's data endpoints.

How to install A股全栈数据工具包

View source

1. Install with the skills CLI

npx skills add simonlin1212/a-stock-data --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by simonlin1212

📦 项目主页:https://github.com/simonlin1212/a-stock-data — 更新、反馈、支持作者

作者:Simon 林 · X @linsizhen · 邮箱:simonlin0423@gmail.com

A股全栈数据工具包 V3.6.1

十层数据架构,47 个端点实测可用(44 主端点 + 3 官方备胎,2026-07 验证),覆盖主板/中小板/科创板/ST。每类数据在「备用源速查」列有独立备胎,主源被封时可降级。

V3.6.1(龙虎榜空窗口崩溃修复,2026-08-09 · #45):dragon_tiger_board() 在回看窗口内无上榜记录时抛 UnboundLocalError——而大市值 / 低换手率标的(如贵州茅台)常态无上榜,等于调用即崩,调用方还无法区分「无数据」与「接口异常」。已修,空窗口返回语义一致的空结构。

V3.6.0(静默失败修复 + 重点监控池/日内异动,2026-07-31 · #15):

  • 🔴 北交所老号段(43/83/87)会返回僵尸数据且不报错:实测在市 342 只中 336 只已迁至 920xxx(锦波生物 832982920982、贝特瑞 835185920185),老码在东财研报静默返回 0 篇、在腾讯行情返回定格报价(成交量 0,价差达 17%~100%+)却仍是 HTTP 200。新增全局警告章节;tencent_quote() 增加 is_stale/stale_reason 标志(实测老码 2/2 命中、正常票 4/4 无误报);eastmoney_reports() 遇老码抛 ValueError 而非返回空
  • 🔴 §2.1/§2.2 研报层 ticker 未归一化(静默空)eastmoney_reports("SH600519") / "600519.SH" 一律返回 0 篇(reportapi 只认纯 6 位),而文档「Ticker 格式归一化」明文承诺全接口支持带前缀写法——承诺与实现不符,且失败方式是静默空,调用方会误读成「该标的无研报覆盖」。新增 norm_ticker() 实现(解析失败抛 ValueError,绝不返回空串),eastmoney_reports() / ths_eps_forecast() 接入。
  • §8.4 em_stock_monitor() 东财重点监控池新增(#15):交易所风险警示 / 重点监控名单 + 生效时间窗,零鉴权静态 JSON。
  • §8.5 em_price_anomaly() / em_price_anomaly_count() 日内异动池新增(#15):交易所「严重异常波动」口径的异动明细与按标的聚合统计,含 12 条异动规则码全解释。⚠️ 必须带 team=h5 等固定参数,缺失返回 unknow team——已做 result!=0 冒泡而非静默返回空。
  • §2.1 行业研报去硬编码日期begin 默认由固定的 2024-01-01 改为「相对今天往前两年」,避免时间窗越用越旧。
  • 端点 44 → 47。实测 24/24 通过(含前缀格式 4/4、老号段拦截、新端点真数据、异动接口拒绝冒泡)。

V3.5.0(板块资金流向,2026-07-23 · #37):

  • §3.8 board_fund_flow() 板块资金流向新增:补上此前缺失的板块级资金流——行业/概念/地域三类板块 × 今日/5日/10日三周期,主力净流入额/净占比 + 超大/大/中/小单四档明细 + 领涨股。与 §3.7 板块排名同源同接口(东财 push2 clist),此前只请求了价格/涨跌家数字段,本版补请求 f62/f184/f66... 资金流字段即覆盖。走 em_get 限流防封。端点 43 → 44。
  • 实测(2026-07-23):行业今日 100 个板块主力净额降序(电力设备 64.66亿 = 超大 43.55亿 + 大 21.11亿)、概念 5 日、地域 10 日均真实返回;参数校验拒绝非法 board_type/period。

V3.4.1(前缀路由 + mootdx 验活修复,2026-07-23):

  • §1.2/§市场前缀规则 前缀路由修复(#40 #41)5 开头沪市 ETF(510300/588200 等)、沪深指数(000300/000016 等)此前落到 else → sz,腾讯接口返回空或错票000016 被误判为 sz000016 *ST康佳A,静默返回不相干标的的数据,比返空更危险)。get_prefix()tencent_quote() 两处同步修复:5x→sh、沪指数白名单、支持显式前缀(sh000001/sz000001)透传解决 000001(上证指数 vs 平安银行)歧义。
  • §1.1 tdx_client() 真实取数验活(#43)_probe() 仅做 TCP 握手,握手成功 ≠ 能取数——坏服务器可握手通过却回 2 字节空 body,导致静默返回空 DataFrame 或连接崩溃且走不到 fallback。新增 _validate():每个候选 server 必须真实拉一根 K 线成功才采用,并对 factory() 连接异常做 try/except 跳过,全部失败才抛明确错误。
  • 备用源速查 K线行新增腾讯 m5 分钟 K 线(#43):同花顺 K 线备胎只有 30/60 分,mootdx 一挂就无 5 分钟源。补腾讯 ifzq.gtimg.cn 分钟 K(m1/m5/m15/m30/m60,零鉴权不封 IP)。⚠️ 第 7 字段是换手率基点不是成交额(差 3 个数量级),成交额需自算。

V3.4.0(接口质量 + 备用源韧性,2026-07-11):

  • §5.2 财联社快讯复活:旧 nodeapi 2026-05 下线后,改走官方 v1/roll/get_roll_list + 本地签名(sign=md5(sha1(排序query)),零 key),V3.2 移除的全市场电报能力恢复,与东财 7×24 互为独立备份。实测 errno=0。
  • 新增「备用源速查 & 降级策略」章节:十层主源→独立备胎速查表(不同域名/不同风控面)+ 3 个实测备胎函数——dragon_tiger_backup()(沪深交易所官方龙虎榜,含营业部席位)、fund_flow_backup()(新浪日度资金流)、announcements_backup()(深市深交所官方/沪市东财公告+PDF)。端点 40 → 43,数据源 13 → 15(新增沪深交易所官方)。
  • §3.6 解禁字段修复:东财 RPT_LIFT_STAGE 改列名致 type/shares 恒空 → 改 FREE_SHARES_TYPE/FREE_SHARES,并新增 able_shares(实际可流通股数,更贴近真实抛压)。
  • §3.7 行业排名排序修复:clist 请求补 fid=f3top/bottom 现按涨跌幅真实排序(此前缺排序字段,切片结果非涨幅序)。
  • §3.2 深股通标注:北向盘中披露收紧后 sgt 分钟序列不可靠(hgt 可用),权威北向用 HKEX 官方日统计(见备用源速查)。
  • 体验:顶部新增「端点路由速查」总表(§→函数→用途→源,可按需局部读取);FAQ 新增东财被封对策 / 财联社复活 / mootdx 库烂尾说明。

V3.2.3(行业研报新增):

  • §2.1 东财行业研报 eastmoney_industry_reports():研报层补上行业研报端点(此前只有个股研报)。与个股研报同端点 reportapi.eastmoney.com/report/list,仅 qType=1industry_code="*" 拉全行业、传东财行业码(如 1238=IT服务Ⅱ)精确过滤,PDF 复用 download_pdf(),走 em_get 限流。端点数 27 → 28。
  • 实测(2026-06-20):全行业 hits=47928、按行业码 1238 过滤 hits=1863,首篇 PDF H3_{infoCode}_1.pdf 下载成功(2.5MB,%PDF 头);行业码表端点(bxpa 等)404 不存在,用 "*" 拉取后从结果反查行业码。

V3.2.2(失效接口替换 + 隐藏 Bug 修复):

  • §3.3 概念板块归属(#18):百度 PAE getrelatedblock 失效(ResultCode 10003 + 空数组)→ 改用东财 slistspt=3eastmoney_concept_blocks(),一次请求拿全个股所属板块(行业/概念/地域 + BK码 + 涨跌幅 + 龙头股),零鉴权走 em_get 限流。
  • §7.1 巨潮公告 orgId(#19):硬编码 gssx0{code} 致大量 601xxx 股票 totalAnnouncement=0 → 新增 _cninfo_orgid() 动态查官方映射表 szse_stock.json(6198 只股,模块级缓存),硬编码降为 fallback。
  • 综合示例修复:示例仍调用 v3.1 已删的 baidu_fund_flow_history → 改 eastmoney_fund_flow_minute
  • §4.5/§5.1 风控说明:部分大陆住宅 IP 被东财间歇风控(HTTP 000/空)非代码 Bug,加重试/换网络提示。
  • 新代码原样 exec smoke test 实测:板块归属 茅台27/五粮液28/绿的谐波21;公告 平安601318/工行601398 原失效股恢复。

V3.2.1(Bug 修复): 修复两个内嵌函数的解析逻辑(预先存在,非 V3.2 引入)——

  • §5.1 东财个股新闻:东财实际返回里 result.cmsArticleWebOld 直接就是文章列表,旧写法对 list 调 .get("list") 触发 AttributeError / 返回空 → 改为遍历 cmsArticleWebOld 列表本身。
  • §6.4 新浪财报三表:新浪实际结构是 result.data.report_list(按报告期为键的 dict,每期 data 才是行项列表),旧写法取 result.data.{lrb} 永久返回空 → 改为遍历 report_list 期次、从每期 dataitem_title 提取。
  • 两函数均用茅台 600519 公开 API(零 key)实测返回非空、字段正确。

V3.2(防封 + 失效修复):

  • 数据源优先级 + 东财防封:明确「通达信(mootdx)/腾讯不封IP 优先用,东财仅用于其独有数据」原则;新增统一节流入口 em_get(),所有东财接口内置串行限流(间隔≥1s+随机抖动)+ 会话复用,AI 抄代码即自带防封。详见「数据源优先级 & 东财防封」章节。
  • 财联社快讯下线(#14)cls.cn 旧 API 全面 404,标注弃用并改用东财全球资讯。

V3.1 修复: 替换 4 个失效接口(百度 PAE 资金流→东财 push2、大宗交易 RPT 报表名更新、机构席位改用 BUY/SELL 明细筛选)+ 修复东财全球资讯 req_trace 参数 + 修复巨潮公告 orgId 格式。

V3.0 Breaking Change:彻底移除 akshare 依赖,所有数据源改为直连 HTTP API(零第三方数据依赖,仅 mootdx 保留 TCP)。

使用方式: 将本文件放入 ~/.claude/skills/a-stock-data/SKILL.md,Claude Code 会自动识别并在 A 股相关对话中激活。

行情层(实时,不封IP)
├── mootdx        → K线 + 五档盘口 + 逐笔成交 (TCP 7709)
├── 腾讯财经 API   → PE/PB/市值/换手率/涨跌停/指数/ETF (HTTP)
└── 百度股市通     → K线带MA5/10/20 (V3.0 新增,HTTP)

研报层
├── 东财 reportapi → 个股研报 + 行业研报 + PDF下载 + 评级 + 三年EPS
├── 同花顺 THS     → 一致预期EPS (直连 basic.10jqka.com.cn)
└── iwencai        → NL语义搜索研报 (唯一能力,需X-Claw)

信号层
├── 同花顺热点     → 当日强势股 + 题材归因 reason tags (零鉴权 73ms)
├── 同花顺北向     → hgt/sgt 分钟资金流向 + 本地自缓存历史
├── 东财 slist     → 个股所属板块/概念归属 (V3.2.2 替换百度PAE)
├── 东财 push2     → 个股资金流向 分钟级 (V3.1 替换百度PAE)
├── 龙虎榜席位     → 上榜记录 + 买卖席位 TOP5 + 机构动向 (datacenter-web)
├── 全市场龙虎榜   → 每日全市场上榜股票 + 净买额排名 (datacenter-web)
├── 限售解禁日历   → 历史解禁 + 未来90天待解禁 (datacenter-web)
├── 行业板块排名   → 东财行业涨跌/上涨下跌家数 (V3.0 替换同花顺)
└── 板块资金流向   → 行业/概念/地域 × 今日/5日/10日 主力+超大/大/中/小四档 (push2, V3.5)

资金面 / 筹码层
├── 融资融券明细   → 日级融资余额/买入/偿还 + 融券 (datacenter-web)
├── 大宗交易       → 成交价/量 + 买卖方营业部 (datacenter-web)
├── 股东户数变化   → 季度股东户数 + 环比变化 (datacenter-web)
├── 分红送转       → 历史每股派息/送股/转增 (datacenter-web)
└── 个股资金流120日 → 主力/大单/中单/小单 日级净流入 (push2his)

新闻层
├── 东财个股新闻   → 个股相关新闻 (search-api-web JSONP)
├── 财联社快讯     → 全市场实时电报 (v1 API+本地签名零key,✅V3.4 复活)
└── 东财全球资讯   → 7×24 财经快讯 (np-weblist,与财联社互备)

基础数据层
├── mootdx finance → 季报快照 (37字段, EPS/ROE/净利)
├── mootdx F10     → 公司资料 (9大类文本)
├── 东财个股信息   → 行业/总股本/流通股/市值/上市日期 (push2)
└── 新浪财报三表   → 资产负债表/利润表/现金流量表 (quotes.sina.cn)

公告层
├── 巨潮 cninfo    → 公告全文检索+下载 (cninfo.com.cn)
└── mootdx F10     → 最新公告摘要

打板层 (V3.3 新增)
├── 东财涨停池     → 连板数/几天几板/封板资金/炸板次数/行业 (push2ex)
├── 东财炸板池     → 涨停后开板 + 振幅/涨速 (push2ex)
├── 东财跌停池     → 封单资金/连续跌停/开板次数 (push2ex)
├── 东财昨涨停池   → 昨涨停今表现,算晋级率/赚钱效应 (push2ex)
└── 同花顺涨停揭秘 → 涨停原因题材/封板成功率/板型 (10jqka)

ETF期权层 (V3.3 新增)
├── 合约清单       → 50ETF/300ETF/科创50/500ETF 各月认购认沽 (新浪)
├── T型报价        → 买卖五档/持仓量/行权价/最新价 (新浪)
└── 希腊字母+IV    → Delta/Gamma/Theta/Vega/隐含波动率 (新浪)

舆情互动层 (V3.3 新增)
├── 互动易问答     → 投资者提问+公司回复 (巨潮,AI问答独家)
├── 同花顺热榜     → 人气值/概念标签/排名变化 (10jqka)
├── 东财人气榜     → 排名+排名变化+名称价格 (emappdata)
└── 东财概念命中   → 个股被归到哪些概念在炒+热度 (emappdata)

端点路由速查(按需定位,不必通读全文)

只需一类数据时,按下表定位章节(§)局部读取。除 iwencai 需 API Key 外全部零 key。

§函数拿什么
前置norm_ticker(code)任意写法→纯6位(SH600519/600519.SH 皆可;解析失败抛错不返空)本地
1.1tdx_client().bars() / .quotes() / .transaction()K线(多周期,不复权) / 五档盘口 / 逐笔成交通达信
1.2tencent_quote(codes)实时价/PE/PB/市值/换手/涨跌停/指数/ETF(带 is_stale 僵尸报价标志)腾讯
1.3baidu_kline_with_ma(code)日K线带 MA5/10/20百度
2.1eastmoney_reports(code) / download_pdf(rec)个股研报+评级+三年EPS / 研报PDF东财
2.1eastmoney_industry_reports(industry_code)行业研报东财
2.2ths_eps_forecast(code)机构一致预期 EPS同花顺
2.3iwencai_search(query) / iwencai_query(query)NL 语义搜研报/选股(需 Key)iwencai
3.1ths_hot_reason()当日强势股+题材归因同花顺
3.2hsgt_realtime()北向分钟流向(hgt 可用 / sgt 仅参考)同花顺
3.3eastmoney_concept_blocks(code)个股所属板块/概念归属东财
3.4eastmoney_fund_flow_minute(code)个股资金流(分钟级)东财
3.5dragon_tiger_board(code, date)个股龙虎榜+买卖席位 TOP5东财
3.6lockup_expiry(code, date)解禁历史+未来90天待解禁东财
3.7industry_comparison()行业板块涨跌排名东财
3.8board_fund_flow(board_type, period)板块资金流向(行业/概念/地域 × 今日/5日/10日,主力+四档)东财
3.9daily_dragon_tiger(date)全市场龙虎榜+净买额排名东财
4.1margin_trading(code)融资融券明细东财
4.2block_trade(code)大宗交易+营业部东财
4.3holder_num_change(code)股东户数变化东财
4.4dividend_history(code)分红送转历史东财
4.5stock_fund_flow_120d(code)个股资金流(120日,日级)东财
5.1eastmoney_stock_news(code)个股新闻东财
5.2cls_telegraph()财联社电报(7×24,本地签名零key)财联社
5.3eastmoney_global_news()全球资讯(7×24)东财
6.1client.finance(symbol)季报快照 37 字段通达信
6.2client.F10(symbol, name)公司资料 9 大类文本通达信
6.3eastmoney_stock_info(code)行业/股本/市值/上市日期东财
6.4sina_financial_report(code, type)财报三表新浪
7.1cninfo_announcements(code)公告检索+PDF 下载巨潮
7.2client.F10(symbol, name='最新提示')最新公告摘要通达信
8.1em_zt_pool / em_zb_pool / em_dt_pool / em_yzt_pool涨停/炸板/跌停/昨涨停四池东财
8.2ths_limit_up_pool(date)涨停原因题材+封板成功率+板型同花顺
8.3limit_up_sentiment(date)炸板率/连板高度/连板梯队东财(四池组合)
8.4em_stock_monitor()重点监控池(风险警示名单+生效时间窗)东财
8.5em_price_anomaly() / em_price_anomaly_count()日内异动明细 / 按标的聚合异动统计(严重异常波动)东财
9.1sina_option_codes / sina_option_tquote / sina_option_greeksETF期权合约清单 / T型报价 / 希腊字母+IV新浪
10.1cninfo_irm(code)互动易问答(提问+公司回复)巨潮
10.2ths_hot_list() / em_hot_rank() / em_hot_concept(code)热榜/人气榜/概念命中同花顺+东财
备用源速查dragon_tiger_backup / fund_flow_backup / announcements_backup龙虎榜/资金流/公告官方备胎(主源被封时降级)交易所官方+新浪+东财(沪市公告)
估值公式forward_pe / pe_digestion / calc_peg / full_valuation(code)前向PE / PE消化时间 / PEG / 单票估值全景本地计算

数据源优先级 & 东财防封(重要,先读)

优先级原则:能用通达信/腾讯,就别用东财

优先级数据源协议封 IP 风险覆盖
1(首选)mootdx(通达信)TCP 7709 二进制不封 IPK线、五档盘口、逐笔成交、财务快照、F10
2腾讯财经HTTP GBK不封 IP实时价、PE/PB/市值/换手率/涨跌停、指数、ETF
3新浪 / 巨潮 / 同花顺HTTP财报三表、公告、一致预期/热点
4(仅独有数据才用)东财 eastmoneyHTTP有风控,会封 IP见下

凡是行情 / K线 / 实时价 / 市值 / 财务三表能从 mootdx 或腾讯拿到的,一律走它们——TCP 协议和腾讯接口实测不封 IP,可放心高频调用。

东财只用于它「独有、别处拿不到」的数据

下列数据只有东财有,通达信/腾讯/新浪都没有,必须用东财(但要限流):

龙虎榜席位 · 全市场龙虎榜 · 限售解禁日历 · 融资融券 · 大宗交易 · 股东户数 · 分红送转 · 个股资金流向(分钟/日级)· 行业板块排名 · 研报列表/PDF · 个股新闻 · 全球资讯

东财风控阈值(社区实测,2026-05)

行为触发封禁的阈值风险
每秒请求数> 5 次/秒
单 IP 并发连接≥ 10
1 分钟请求总数≥ 200 次中高
5 分钟请求总数≥ 300 次触发封禁
User-Agent空 UA / 无浏览器特征

被封表现:连续请求后 403 / 429 / 连接超时 / 返回空数据。临时封禁通常几分钟到几小时。

⚠️ 实测封禁案例(2026-06-30,一手数据,感谢 @luodada99 issue #36)

上表是社区口径;下面这条是真实踩到 IP 级封禁的完整记录,比阈值表更有参考价值:

  • 触发方式:选股脚本 10 线程并发、完全不走 em_get() 限流,1 小时内发出 45000+ 请求(三个版本的脚本同时跑全市场 5208 只)
  • 后果push2 / push2his 全系列 RemoteDisconnectedIP 级封禁持续 20+ 小时——不是"几分钟到几小时"那种临时限速
  • 关键观察一datacenter-web.eastmoney.com 不受影响——东财不同子域走不同 WAF,push2 被封不代表整个东财都不能用
  • 关键观察二腾讯 K 线(web.ifzq.gtimg.cn)连续 5000+ 次后会返回空,但这是限流不是封 IP,降速或换新浪即可恢复
  • 降级实测:东财被封时,第一只股票花 10.9s 完成"检测被封 + 降级",之后每只 0.4s 走腾讯,数据准确

这个案例正是「限流是铁律」的实证em_get() 的默认间隔(1s + 抖动、串行)下,1 小时最多约 3000 次请求,与踩坑者的 45000 次差一个数量级。

被封后的降级路径(各层备胎详见「备用源速查」章节):

被封端点替代方案差异
push2/clist/get(股票列表)datacenter-web + 腾讯行情批量行业字段来自 datacenter 的 BOARD_NAME
push2his/kline/get(K线)腾讯 fqkline/get(前复权)→ 新浪 getKLineData(不复权)腾讯有前复权,新浪没有
push2/stock/get(个股)腾讯 qt.gtimg.cn腾讯无行业/概念字段

防封铁律(调用东财时必须遵守)

  1. 串行,不并发——绝不对东财开多线程/协程并发请求
  2. 每次间隔 ≥ 1 秒 + 随机抖动(QPS ≤ 2),批量筛选时调大到 1.5~2 秒
  3. 复用 HTTP 会话(Keep-Alive),不要每次新建连接
  4. 带正常 UA + Referer(本 SKILL 各端点已配好)
  5. 批量场景每只股票之间 sleep——AI 跑批量循环(如筛选 100 只股逐个拉龙虎榜/资金流)是被封的头号元凶

已内置限流:所有东财请求走 em_get()

本 SKILL 提供统一的节流入口 em_get()(定义见下方「东财数据中心统一查询(共用 helper)」),它自动做到:串行限流(最小间隔 EM_MIN_INTERVAL=1.0s + 随机抖动)+ 复用 EM_SESSION(Keep-Alive)+ 默认 UA。所有 eastmoney.com 端点的代码块都已改用 em_get 而非裸 requests.get,AI 直接抄代码即自带防封。批量任务把 EM_MIN_INTERVAL 调大即可进一步降速。

注:em_get / EM_SESSION / EM_MIN_INTERVAL 是所有东财代码块共用的前置定义,使用任一东财端点前需先执行「共用 helper」代码块。


When to Activate

  • 用户要查 A 股个股估值(一致预期 / PE / PEG / PE消化)
  • 用户要拉实时行情(价格 / 五档盘口 / K线 / 涨跌停价)
  • 用户要搜研报(按主题 / 按标的 / 按行业 / 下载PDF)
  • 用户要看当日强势股 / 题材归因 / 概念热点
  • 用户要看北向资金动向(沪股通/深股通分钟流向)
  • 用户要看概念板块归属(行业/概念/地域)
  • 用户要看个股资金流向(主力/散户/超大单/大单分钟级)
  • 用户要看龙虎榜席位(营业部 + 机构买卖)
  • 用户要看全市场龙虎榜(当日所有上榜股票 + 净买额排名)
  • 用户要看限售解禁日历(历史解禁 + 未来待解禁)
  • 用户要做行业横向对比(涨跌排名 / 资金流入 / 领涨股)
  • 用户要看融资融券 / 两融数据(融资余额 + 融券余额)
  • 用户要看大宗交易(成交价/量 + 买卖方营业部)
  • 用户要看股东户数变化(筹码集中度)
  • 用户要看分红送转历史(每股派息 + 送股 + 转增)
  • 用户要看指数/ETF行情(上证指数 / 沪深300 / 创业板指 / ETF)
  • 用户要看涨停 / 打板情绪(涨停池 / 连板梯队 / 炸板率 / 跌停 / 涨停原因题材)
  • 用户要看ETF 期权(T型报价 / 希腊字母 Delta·Gamma·Theta·Vega / 隐含波动率 IV)
  • 用户要看投资者互动问答(公司如何回应某传闻/利好 · 互动易)
  • 用户要看市场热度 / 人气榜(同花顺热榜 / 东财人气榜 / 个股概念命中)
  • 用户要看新闻资讯(个股新闻 / 财联社快讯 / 全球资讯)
  • 用户要查公告(巨潮公告全文)
  • 用户要做产业链调研 / 批量横向对比
  • 关键词:估值、一致预期、机构预测、市盈率、PEG、市值、研报、产业链、行业研究、K线、盘口、公告、新闻、强势股、题材、热点、概念归因、北向资金、沪股通、深股通、概念板块、资金流向、主力、龙虎榜、席位、营业部、全市场龙虎榜、净买入、解禁、限售、行业对比、行业轮动、融资融券、两融、大宗交易、股东户数、筹码集中、分红、派息、送股、指数、ETF、涨停、打板、连板、炸板、跌停、涨停原因、封板、晋级率、ETF期权、希腊字母、隐含波动率、互动易、投资者关系、热榜、人气榜、市场热度

Prerequisites

pip install mootdx requests pandas stockstats
依赖版本要求用途
mootdx>= 0.10TCP行情+财务+F10(唯一非HTTP依赖);0.11.x 用 tdx_client() 规避 BESTIP bug,见上节
requestsany所有HTTP API直连
pandasany数据处理+HTML表格解析
stockstatsany技术指标计算(RSI/MACD/BOLL等)

V3.0 架构: 除 mootdx(TCP 二进制协议)外,所有数据源均为直连 HTTP API,零第三方数据封装依赖。每个端点的底层 URL/参数完全暴露,方便调试和定制。

iwencai API Key(仅语义搜索需要)

# 环境变量方式
export IWENCAI_API_KEY="your_key_here"
export IWENCAI_BASE_URL="https://openapi.iwencai.com"

# 申请地址: https://www.iwencai.com/skillhub
# 注册后安装 SkillHub CLI,再安装 report-search 技能即可获得 Key

其他数据源(mootdx / 腾讯 / 东财 / 同花顺 / 百度股市通 / 新浪 / 巨潮)全部免费,无需 key。

mootdx 客户端(必读,规避 0.11.x BESTIP 空串 bug)

已知 bug(mootdx 0.11.x): 全新安装后 Quotes.factory(market='std') 裸调用可能抛 ValueError: not enough values to unpack (expected 2, got 0)。 根因:~/.mootdx/config.jsonBESTIP.HQ 初始是空字符串 ""(不是缺失键),mootdx 用 dict.get(key, default) 取不到 default,拆包失败。老用户(config 曾填充过 IP)不会触发,所以容易漏测。 不要靠锁版本解决:mootdx==0.10.12 在部分环境(如干净的 Python 3.9)下 import mootdx 会因 numpy/pandas 二进制不兼容直接崩。正确做法是用下面的 tdx_client()——显式传 server 绕过 BESTIP,对 0.10 / 0.11 都适用。

统一用以下 helper 创建客户端(所有 mootdx 调用都走它):

import socket
from mootdx.quotes import Quotes

# 实测可用的备选服务器(按延迟排序,2026-06 验证)
_TDX_SERVERS = [
    ('119.97.185.59', 7709), ('124.70.133.119', 7709), ('116.205.183.150', 7709),
    ('123.60.73.44', 7709),  ('116.205.163.254', 7709), ('121.36.225.169', 7709),
    ('123.60.70.228', 7709), ('124.71.9.153', 7709),    ('110.41.147.114', 7709),
    ('124.71.187.122', 7709),
]

def _probe(ip, port, timeout=2.0):
    """TCP 握手探测(快速粗筛)。注意:握手成功 ≠ 能取数,必须再经 _validate 验活。"""
    try:
        with socket.create_connection((ip, port), timeout=timeout):
            return True
    except Exception:
        return False

def _validate(client, market: str = 'std') -> bool:
    """真实取数验活:坏服务器可 TCP 握手通过却回 2 字节空 body → 静默空表。用一次真实 K 线请求兜底。

    验活样本 '000001' 是 A 股代码,只对 market='std' 有意义。其它市场(如扩展行情 'ext')
    用它必然取不到数,会把所有正常服务器都判死、误报「全部不可达」,故非 std 时跳过验活。
    """
    if market != 'std':
        return True
    try:
        df = client.bars(symbol='000001', frequency=9, offset=1)
        return df is not None and not df.empty
    except Exception:
        return False

def tdx_client(market='std'):
    """
    创建 mootdx 客户端,规避 0.11.x BESTIP.HQ 空串 bug + 坏服务器静默空表(#43)。
    每个候选都必须「真实取数验活」通过才采用(_probe TCP 握手是假阳性来源):
      1) 顺序探测 _TDX_SERVERS,对 probe 通过者再 _validate 真实取数,取第一个验活成功的;
      2) 全部失败 → 回退 mootdx 自带 bestip 测速选优(同样验活);
      3) 再回退裸 factory(老用户 config 已有可用 BESTIP 时成立);
      4) 仍失败 → 抛 RuntimeError,明确报错而非静默返回空表 / 崩溃。
    """
    for ip, port in _TDX_SERVERS:
        if not _probe(ip, port):
            continue
        try:
            c = Quotes.factory(market=market, server=(ip, port))
            if _validate(c, market):
                return c
        except Exception:
            continue                                        # 握手过但取数崩 → 跳过下一台
    for kwargs in ({'bestip': True}, {}):                   # fallback: bestip 测速 / 裸 factory
        try:
            c = Quotes.factory(market=market, **kwargs)
            if _validate(c, market):
                return c
        except Exception:
            continue
    raise RuntimeError(
        "所有 mootdx 服务器均无法取到数据(TCP 可达但返回空 / 被 reset)。"
        "海外网络通常全部超时(TCP 7709),请走国内代理或更新 _TDX_SERVERS 列表。"
    )

# 用法:client = tdx_client()   # 替代所有 Quotes.factory(market='std')

海外 IP 用户: mootdx 走通达信 TCP 7709,海外环境通常全部超时。tdx_client() 会快速失败给出明确报错,而非死等。

市场前缀规则(全局通用)

# 沪市指数白名单:与深市 000xxx 个股同段,需白名单区分(沪深300/上证50/中证500/科创50/中证1000/上证180)
SH_INDEX = {"000300", "000905", "000016", "000688", "000852", "000010"}

def get_prefix(code: str) -> str:
    """6位代码 → 市场前缀(sh/sz/bj)。支持显式前缀 sh/sz/bj 透传以解决歧义。"""
    c = code.lower()
    if c.startswith(("sh", "sz", "bj")):     # 显式前缀透传(如 sh000001=上证指数 vs sz000001=平安银行)
        return c[:2]
    if c.startswith("92"):                   # 北交所 2024-10 起的新股号段,必须先于下面的 9x 判断
        return "bj"
    if c.startswith(("5", "6", "9")):        # 5x=沪 ETF/LOF,6/9=沪个股(900xxx=沪 B 股)
        return "sh"
    if c.startswith(("4", "8")):             # 4x/8x=北交所【老号段,多数已迁 920,见下方警告】
        return "bj"
    if c in SH_INDEX:                         # 沪深300/上证50 等沪指数(000xxx)
        return "sh"
    return "sz"                              # 深市个股/ETF(00/30/15x/16x/159 等),深指数 399xxx 亦走 sz

歧义说明: 000001 默认按个股→sz000001(平安银行);要上证指数请显式传 sh000001000016 默认按沪指数→上证50;要深康佳A 请传 sz000016

⚠️ 北交所老号段(43/83/87)已基本作废 — 会拿到僵尸数据且不报错

2026-07-31 实测: 东财北交所在市 342 只中 336 只已是 920xxx 号段,仅剩 3 只老码且全部停牌。存量公司代码已整体迁移(如 锦波生物 832982920982、贝特瑞 835185920185)。

危险在于老码不会报错,而是返回看似正常的脏数据:

接口传老码 832982传新码 920982
腾讯行情返回 112.60、成交量 0(定格在迁移日)131.74,正常成交 ✅
腾讯行情(贝特瑞)835185 → 45.91、成交量 0920185 → 21.05 ✅
东财研报0 篇(静默空)79 篇 ✅

老码行情价与真实价可差 17%~100%+,直接拿去算估值会得出完全错误的结论。

判定僵尸报价: 成交量 == 0 且 最新价 == 昨收 → 极可能是已迁移的废码(真停牌股同样满足,两者都不该用于估值)。tencent_quote() 已内置该检测并置 is_stale 标志,见 §1.2。

拿新码:push2 北交所全量清单 fs=m:0+t:81+s:2048 按名称反查现行代码。

Ticker 格式归一化

norm_ticker() 把下列写法统一成纯 6 位数字:

⚠️ 不是所有端点都自动归一化(V3.6.0 前这里写的是「所有接口统一支持」,与实现不符,已改正)。

  • 已内置归一化:§2.1 eastmoney_reports()、§2.2 ths_eps_forecast()
  • 只认纯 6 位或显式 sh/sz/bj 前缀(其余端点):tencent_quote() 等走 get_prefix() 路由的函数 支持 600519sh600519,但不认后缀式 600519.SH——实测会拼成 sh600519.SH 并返回空载荷(静默失败)。
  • 结论:拿到用户输入的代码,先过一遍 norm_ticker() 再传给任何端点,最省事也最安全。
输入归一化结果
688017688017
SH688017 / sh688017688017
688017.SH / 688017.sh688017
SZ000001000001
BJ920982920982
import re

# 整串锚定匹配,只认下表列出的写法;市场标识前缀、后缀**二选一,不能同时出现**。
# ⚠️ 两个坑都会造成「静默拿到另一只股票的数据」,比报错危险得多:
#   ① 别用 re.search(r"\d{6}") 从任意串里"捞"6 位:"6005190"/"foo600519bar" 会被截成 600519。
#   ② 别让前后缀同时可选:`SH000001.SZ` 这种自相矛盾的写法会被照单全收,
#      而 000001 恰是歧义码(sh000001=上证指数 / sz000001=平安银行),静默丢掉市场信息=选错标的。
# 捕获组:1=前缀市场 2=前缀式代码 | 3=后缀式代码 4=后缀市场
# 市场标识要**同时**从前缀和后缀取——只认 startswith("sh") 会漏掉 `000001.SH` 这种后缀写法。
_TICKER_RE = re.compile(
    r"^(?:(sh|sz|bj)(\d{6})|(\d{6})(?:\.(sh|sz|bj))?)$", re.IGNORECASE)

def _natural_market(digits: str) -> str:
    """6 位码的自然归属市场。仅用于校验显式前缀是否自相矛盾。
    注意 000xxx 是沪指数/深个股共用的歧义段,由调用处单独处理,不走这里。"""
    if digits.startswith("92") or digits[:2] in ("43", "83", "87"):
        return "bj"                      # 北交所(920 现行 / 43·83·87 老号段)
    if digits[0] in ("5", "6", "9"):
        return "sh"                      # 5x 沪 ETF/LOF,6xx 沪个股,9xx 沪 B 股
    return "sz"                          # 00x/30x/15x/16x/39x 等

def norm_ticker(code: str, stock_only: bool = False) -> str:
    """任意受支持写法 → 纯 6 位数字代码。

    支持 600519 / SH600519 / sh600519 / 600519.SH / BJ920982 等。
    stock_only=True:个股专用接口(研报、一致预期等)传这个,会拒绝显式指数写法。
    ⚠️ 不匹配时**抛 ValueError,绝不静默返回空串或猜一个代码**——
    否则调用方会把「代码格式写错」误读成「这只票没有数据」,
    或者更糟:拿到另一只股票的数据还以为是对的。
    """
    raw = str(code).strip()
    m = _TICKER_RE.match(raw)
    if not m:
        raise ValueError(
            f"无法把 {code!r} 解析为 6 位股票代码;"
            f"支持格式:600519 / SH600519 / sh600519 / 600519.SH"
            f"(前缀与后缀二选一,不能同时写)"
        )
    digits = m.group(2) or m.group(3)
    market = (m.group(1) or m.group(4) or "").lower()      # 前缀式与后缀式都要认
    # 归一化会丢掉市场标识,若标识与号段矛盾就会静默落到另一只票上,必须在这里拦。
    if market:
        if digits.startswith("000"):
            # 000xxx 是**沪市指数 / 深市个股共用**的歧义段,显式标识在这里是「消歧」不是「矛盾」:
            #   sh000001=上证指数 vs sz000001=平安银行;sh000016=上证50 vs sz000016=深康佳A。
            if market == "bj":
                raise ValueError(f"{code!r} 市场标识与号段矛盾:000xxx 不属北交所。")
            # 沪市个股只有 600/601/603/605/688/689(B 股 900),**不存在 000xxx 沪市个股**,
            # 所以「显式 sh + 000 段」必然是指数。实测不拦的话:sh000001→平安银行研报 100 篇、
            # sh000016→深康佳A、sh000039→中集集团 84 篇,全是别人的数据。
            if stock_only and market == "sh":
                raise ValueError(
                    f"{code!r} 指向沪市指数而非个股(沪市无 000xxx 个股),本接口只服务个股。"
                    f"要查同号段的深市个股请显式传 sz{digits}。"
                )
        else:
            nat = _natural_market(digits)
            if market != nat:
                raise ValueError(
                    f"{code!r} 的市场标识与号段矛盾:{digits} 属 {nat} 市,而不是 {market} 市。"
                    f"(改用 {nat}{digits} 或去掉市场标识)"
                )
    return digits

# 用法
norm_ticker("SH600519")      # '600519'
norm_ticker("600519.SH")     # '600519'
norm_ticker("bj920982")      # '920982'
norm_ticker("6005190")       # ValueError(7 位,不会被截成 600519)
norm_ticker("茅台")           # ValueError
norm_ticker("SH000001.SZ")   # ValueError(前后缀矛盾,不猜市场)
norm_ticker("SH000001", stock_only=True)     # ValueError(上证指数,不是平安银行)
norm_ticker("000001.SH", stock_only=True)    # ValueError(后缀写法同样拦下)
norm_ticker("SZ600519")                      # ValueError(600519 是沪市,标识矛盾)
norm_ticker("sz000016")                      # '000016'(深康佳A,000 段的显式消歧,合法)

东财数据中心统一查询(共用 helper)

龙虎榜/解禁/融资融券/大宗交易/股东户数/分红 共用同一 base URL:

import time
import random
import requests

UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
DATACENTER_URL = "https://datacenter-web.eastmoney.com/api/data/v1/get"

# ── 东财防封:全局节流 + 会话复用 ────────────────────────────────────
# 东财系 HTTP 接口(push2 / datacenter / reportapi / search / np-weblist)有风控:
#   每秒 >5 次 / 单 IP 并发 ≥10 / 1 分钟 ≥200 次  →  临时封 IP。
# 所有 eastmoney.com 请求一律走 em_get():串行限流(最小间隔 + 随机抖动)+ 复用
# Keep-Alive 会话,批量调用时自动降速,避免被封。详见「数据源优先级 & 东财防封」章节。
EM_SESSION = requests.Session()
EM_SESSION.headers.update({"User-Agent": UA})
# 连接级自动重试:瞬态连接错误 / 429 / 5xx 指数退避重试(住宅IP偶发风控更稳)。
# 注意:403 不重试(东财风控信号,重试无益反而加重;按下方 EM_MIN_INTERVAL 降频应对)。
try:
    from requests.adapters import HTTPAdapter
    from urllib3.util.retry import Retry
    _em_adapter = HTTPAdapter(max_retries=Retry(
        total=3, connect=3, backoff_factor=0.6,
        status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"]))
    EM_SESSION.mount("https://", _em_adapter)
    EM_SESSION.mount("http://", _em_adapter)
except Exception:
    pass  # 老版本 urllib3 缺 allowed_methods 等参数时降级为无重试,不影响主流程
EM_MIN_INTERVAL = 1.0          # 两次东财请求最小间隔(秒);批量筛选建议调大到 1.5~2
_em_last_call = [0.0]          # 模块级上次请求时间戳

def em_get(url: str, params: dict | None = None, headers: dict | None = None,
           timeout: int = 15, **kwargs):
    """东财统一请求入口:自动节流 + 复用 session + 默认 UA。
    所有 eastmoney.com 接口都应通过它请求,避免高频被封 IP。"""
    wait = EM_MIN_INTERVAL - (time.time() - _em_last_call[0])
    if wait > 0:
        time.sleep(wait + random.uniform(0.1, 0.5))
    try:
        return EM_SESSION.get(url, params=params, headers=headers, timeout=timeout, **kwargs)
    finally:
        _em_last_call[0] = time.time()

def eastmoney_datacenter(report_name: str, columns: str = "ALL",
                          filter_str: str = "", page_size: int = 50,
                          sort_columns: str = "", sort_types: str = "-1") -> list[dict]:
    """东财数据中心统一查询 — 龙虎榜/解禁/融资融券/大宗交易/股东户数/分红 共用(已内置限流)"""
    params = {
        "reportName": report_name, "columns": columns,
        "filter": filter_str, "pageNumber": "1", "pageSize": str(page_size),
        "sortColumns": sort_columns, "sortTypes": sort_types,
        "source": "WEB", "client": "WEB",
    }
    r = em_get(DATACENTER_URL, params=params, timeout=15)
    d = r.json()
    if d.get("result") and d["result"].get("data"):
        return d["result"]["data"]
    return []

Layer 1: 行情层(实时,不封IP)

1.1 mootdx — K线 + 五档盘口 + 逐笔成交

TCP 二进制协议,连通达信服务器(7709),无需注册,不封IP。

from mootdx.quotes import Quotes

client = tdx_client()  # 见 Prerequisites 的 tdx_client() helper(规避 0.11.x BESTIP bug;等价 Quotes.factory(market='std'))

# === K线数据 ===
# ⚠️ 参数名是 frequency(不是 category!传 category 会被 **kwargs 静默吞掉,
#    永远退化成默认 frequency=9 日线,拿不到分钟数据)。
# mootdx 0.11.7 实测频率值表:
#   0=5分钟  1=15分钟  2=30分钟  3=60分钟(1小时)  4=日线  5=周线  6=月线
#   8=1分钟  9=日线(默认)  10=季线  11=年线        (7=1分钟除权口径,少用)
klines = client.bars(symbol='688017', frequency=9, offset=10)    # 日线
min1   = client.bars(symbol='688017', frequency=8, offset=240)   # 1分钟(一个交易日≈240根)
min5   = client.bars(symbol='688017', frequency=0, offset=48)    # 5分钟
# 返回: open, close, high, low, vol, amount, datetime
# ⚠️ 复权:bars 返回【不复权】原始价(通达信原始数据,无 adjust 参数)。
#    跨除权除息日做估值/回测前需自行复权,或改用带前复权的日K数据源(腾讯财经)。

# === 实时报价 ===
quotes = client.quotes(symbol=['688017', '300476'])
# 返回 46 个字段:
#   price(现价), open, high, low, last_close(昨收)
#   bid1~bid5, ask1~ask5, bid_vol1~bid_vol5, ask_vol1~ask_vol5
#   vol(成交量), amount(成交额), servertime

# === 逐笔成交(非交易时间返回空)===
trades = client.transaction(symbol='688017', date='20260502')
# 返回: time, price, vol, num, buyorsell(0买/1卖/2中性)

mootdx 不提供 PE / PB / 市值 / 换手率 / 涨跌停价 — 这些走腾讯财经。

1.2 腾讯财经 API — PE/PB/市值/换手率/涨跌停/指数/ETF

HTTP GET,GBK 编码,~ 分隔 88 个字段,不封IP。

import urllib.request

def tencent_quote(codes: list[str]) -> dict[str, dict]:
    """
    批量拉取腾讯财经实时行情。
    codes: ["688017", "300476", "002463"]
    也支持指数: ["000001", "000300", "399006"]
    也支持ETF: ["510050", "510300"]
    返回: {code: {name, price, pe_ttm, pb, mcap, ...}}
    """
    # 前缀路由:与全局 get_prefix() 一致。5x 沪ETF / 000300 等沪指数不能落到 sz(会返回空或错票)。
    SH_INDEX = {"000300", "000905", "000016", "000688", "000852", "000010"}   # 沪指数白名单
    prefixed = []
    key_of = {}          # 带前缀的查询键 → 调用方原始写法,保证结果键与入参一一对应
    for c in codes:
        low = c.lower()
        if low.startswith(("sh", "sz", "bj")):        # 显式前缀透传,解决 000001 等歧义
            p = low
        elif c.startswith("92"):                      # 北交所 920 号段须先于 9x 判断
            p = f"bj{c}"
        elif c in SH_INDEX or c.startswith(("5", "6", "9")):
            p = f"sh{c}"
        elif c.startswith(("4", "8")):
            p = f"bj{c}"
        else:
            p = f"sz{c}"
        prefixed.append(p)
        key_of[p] = c    # 显式前缀入参原样返回,裸代码返回裸代码

    url = "https://qt.gtimg.cn/q=" + ",".join(prefixed)
    req = urllib.request.Request(url)
    req.add_header("User-Agent", "Mozilla/5.0")
    resp = urllib.request.urlopen(req, timeout=10)
    data = resp.read().decode("gbk")

    result = {}
    for line in data.strip().split(";"):
        if not line.strip() or "=" not in line or '"' not in line:
            continue
        key = line.split("=")[0].split("_")[-1]
        vals = line.split('"')[1].split("~")
        if len(vals) < 53:
            continue
        # 用入参原样做键:批量里同时传 sh000001 与 sz000001 时,若都退回裸 6 位码
        # 会撞成同一个键、后者静默覆盖前者,显式前缀这个特性就白做了。
        code = key_of.get(key, key[2:])
        result[code] = {
            "name":         vals[1],
            "price":        float(vals[3]) if vals[3] else 0,
            "last_close":   float(vals[4]) if vals[4] else 0,
            "open":         float(vals[5]) if vals[5] else 0,
            "change_amt":   float(vals[31]) if vals[31] else 0,
            "change_pct":   float(vals[32]) if vals[32] else 0,
            "high":         float(vals[33]) if vals[33] else 0,
            "low":          float(vals[34]) if vals[34] else 0,
            "amount_wan":   float(vals[37]) if vals[37] else 0,
            "turnover_pct": float(vals[38]) if vals[38] else 0,
            "pe_ttm":       float(vals[39]) if vals[39] else 0,
            "amplitude_pct":float(vals[43]) if vals[43] else 0,
            # ⚠️ 44=流通市值、45=总市值(曾标反)。总股本≠流通股本时差数倍,见上方踩坑提醒二
            "float_mcap_yi":float(vals[44]) if vals[44] else 0,
            "mcap_yi":      float(vals[45]) if vals[45] else 0,
            "pb":           float(vals[46]) if vals[46] else 0,
            "limit_up":     float(vals[47]) if vals[47] else 0,
            "limit_down":   float(vals[48]) if vals[48] else 0,
            "vol_ratio":    float(vals[49]) if vals[49] else 0,
            "pe_static":    float(vals[52]) if vals[52] else 0,
        }
        # 僵尸报价检测:腾讯对「已迁移的北交所老码 / 长期停牌股」照样返回 HTTP 200 +
        # 一份定格在最后交易日的报价(成交量 0、最新价==昨收),不报任何错。
        # 直接拿去算估值会得出完全错误的结论(实测 bj832982 报 112.60,真实新码 920982 为 131.74)。
        q = result[code]
        q["is_stale"] = (q["amount_wan"] == 0 and q["price"] == q["last_close"] and q["price"] > 0)
        if q["is_stale"] and key[2:4] in ("43", "83", "87"):
            q["stale_reason"] = "北交所老号段,多数已迁至 920xxx,请按名称反查现行代码"
        elif q["is_stale"]:
            q["stale_reason"] = "成交量为 0(停牌 / 未开盘 / 废码),报价非当日真实成交"
    return result

# 用法: 个股
quotes = tencent_quote(["688017", "300476", "002463"])
for code, q in quotes.items():
    print(f"{q['name']}({code}): {q['price']}元 PE={q['pe_ttm']} PB={q['pb']} 市值={q['mcap_yi']}亿")

# 用法: 指数 — sh000001=上证指数, sh000300=沪深300, sz399006=创业板指
index_quotes = tencent_quote(["000001", "000300", "399006"])

# 用法: ETF — sh510050=上证50ETF, sh510300=沪深300ETF
etf_quotes = tencent_quote(["510050", "510300"])

腾讯财经字段索引速查(实测校准 2026-05-03)

索引含义示例
1名称绿的谐波
3当前价224.12
4昨收215.01
5今开214.10
9-18买一~买五(价+量)
19-28卖一~卖五(价+量)
31涨跌额9.11
32涨跌幅%4.24
33最高229.62
34最低214.10
37成交额(万)187040
38换手率%4.55
39PE(TTM)300.45
43振幅%(不是PB!)7.22
44流通市值(亿)410.88
45总市值(亿)410.88
46PB(市净率)11.51
47涨停价258.01
48跌停价172.01
49量比1.20
52PE(静)314.76

踩坑提醒一: 网上很多教程把索引 43 写成 PB,实测是振幅%。PB 在索引 46。

踩坑提醒二(2026-07-26 修正): 44 是流通市值、45 才是总市值,此前本表标反了。 多数股票两者相等,所以看不出来;但总股本 ≠ 流通股本的票(科创板/次新股/有限售股)会差出数倍。 实测中船特气(688146):f[44]=356.15亿(流通股本 1.45亿股)、f[45]=1300.61亿(总股本 5.29亿股),差 3.65 倍。 用市值做筛选时取错会把大市值公司误判成小盘股。可用 f[45] ÷ 现价 反推总股本核对(与东财 f84 一致)。 参考:东财 push2 的 f116=总市值 / f117=流通市值 方向与腾讯相反,实测确认无误,勿混用。

1.3 百度股市通 K线 — 带MA5/MA10/MA20(V3.0 新增)

核心价值: 返回时自带均线数据,无需本地计算。

import requests

def baidu_kline_with_ma(code: str, start_time: str = "") -> dict:
    """百度股市通K线 — 独有能力: 返回时自带 ma5/ma10/ma20 均价"""
    url = "https://finance.pae.baidu.com/selfselect/getstockquotation"
    params = {
        "all": "1", "isIndex": "false", "isBk": "false", "isBlock": "false",
        "isFutures": "false", "isStock": "true", "newFormat": "1",
        "group": "quotation_kline_ab", "finClientType": "pc",
        "code": code, "start_time": start_time, "ktype": "1",
    }
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Accept": "application/vnd.finance-web.v1+json",
        "Origin": "https://gushitong.baidu.com",
        "Referer": "https://gushitong.baidu.com/",
    }
    r = requests.get(url, params=params, headers=headers, timeout=10)
    d = r.json()
    result = d.get("Result", {})
    md = result.get("newMarketData", {})
    keys = md.get("keys", [])  # includes: ma5avgprice, ma10avgprice, ma20avgprice
    rows = md.get("marketData", "").split(";")
    return {"keys": keys, "rows": rows}

# 用法
data = baidu_kline_with_ma("600519")
print("字段:", data["keys"][:10])
print("最近5根K线:", data["rows"][-5:])
# keys 包含: time, open, close, high, low, volume, amount, ma5avgprice, ma10avgprice, ma20avgprice 等

Layer 2: 研报层

2.1 东财研报 API — 研报列表 + PDF下载(主力)

A级接口(公开JSON API),reportapi.eastmoney.com,免费无key。

import requests
import re
import time
from datetime import date, timedelta   # 注意用 date/timedelta,不要 import datetime 模块:
                                       # 本 SKILL 多处有 `from datetime import datetime`,会把模块名遮蔽掉
from pathlib import Path

REPORT_API = "https://reportapi.eastmoney.com/report/list"
PDF_TPL = "https://pdf.dfcfw.com/pdf/H3_{info_code}_1.pdf"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"

def eastmoney_reports(code: str, max_pages: int = 5) -> list[dict]:
    """拉取指定股票的研报列表。

    code 支持 600519 / SH600519 / 600519.SH 等写法(内部归一化为纯 6 位)。
    ⚠️ reportapi 只认纯 6 位数字:传 "SH600519" 会返回 hits=0,
       看起来像「这只票没研报」,实际是格式没归一化——务必先过 norm_ticker()。
    返回 [] 仅表示东财确无该标的研报覆盖(格式错误已在上游抛 ValueError)。
    """
    code = norm_ticker(code, stock_only=True)   # 格式错/显式指数码直接抛错,不静默返回 []
    all_records = []
    for page in range(1, max_pages + 1):
        params = {
            "industryCode": "*", "pageSize": "100", "industry": "*",
            "rating": "*", "ratingChange": "*",
            "beginTime": "2000-01-01", "endTime": "2030-01-01",
            "pageNo": str(page), "fields": "", "qType": "0",
            "orgCode": "", "code": code, "rcode": "",
            "p": str(page), "pageNum": str(page), "pageNumber": str(page),
        }
        r = em_get(REPORT_API, params=params,
                   headers={"Referer": "https://data.eastmoney.com/"}, timeout=30)  # 已内置限流
        d = r.json()
        rows = d.get("data") or []
        if not rows:
            break
        all_records.extend(rows)
        if page >= (d.get("TotalPage", 1) or 1):
            break
    # 正向识别「查无结果」的真实原因,不把废码静默当成「无研报覆盖」
    if not all_records and code[:2] in ("43", "83", "87"):
        raise ValueError(
            f"{code} 属北交所老号段(43/83/87),东财研报库已不再按老码索引。"
            f"北交所存量标的已基本迁至 920xxx(如 832982→920982);"
            f"请按股票名称反查现行 920 代码后重试。详见「北交所老号段」警告。"
        )
    return all_records

def download_pdf(record: dict, target_dir: str = "./reports") -> str | None:
    """下载单份研报PDF,返回保存路径或None"""
    info_code = record.get("infoCode", "")
    if not info_code:
        return None
    date = (record.get("publishDate") or "")[:10]
    org = re.sub(r'[\\/:*?"<>|]', "_", record.get("orgSName") or "未知")[:40]
    title = re.sub(r'[\\/:*?"<>|]', "_", record.get("title", ""))[:80]
    fname = f"{date}_{org}_{title}.pdf"
    target = Path(target_dir) / fname
    if target.exists():
        return str(target)
    url = PDF_TPL.format(info_code=info_code)
    r = em_get(url, headers={"Referer": "https://data.eastmoney.com/"}, timeout=60)
    if r.status_code == 200 and len(r.content) >= 1024:
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_bytes(r.content)
        return str(target)
    return None

# 用法
reports = eastmoney_reports("688017")
print(f"共 {len(reports)} 篇研报")
for r in reports[:5]:
    print(f"  {r.get('publishDate','')[:10]} | {r.get('orgSName')} | {r.get('title','')[:60]}")

研报 record 关键字段

字段含义
title研报标题
publishDate发布日期
orgSName机构简称
infoCode用于拼 PDF URL
predictThisYearEps今年EPS预测
predictNextYearEps明年EPS预测
predictNextTwoYearEps后年EPS预测
emRatingName评级(买入/增持/...)
indvInduName行业分类

行业研报列表(qType=1)

与个股研报同一端点reportapi.eastmoney.com/report/list),仅 qType 不同:qType=0 个股研报,qType=1 行业研报。返回 record 可直接喂给上面的 download_pdf()(PDF 模板通用)。

from datetime import date, timedelta   # 本块单独拷贝也能跑;勿写成 import datetime(会被 §3+ 的
                                       # `from datetime import datetime` 遮蔽掉模块名)

def eastmoney_industry_reports(industry_code: str = "*", max_pages: int = 5,
                               begin: str = "") -> list[dict]:
    """拉取行业研报列表(qType=1)。
    industry_code="*" = 全行业;传东财行业码(如 "1238"=IT服务Ⅱ)= 单行业。
    行业名 / 行业码在每条 record 的 industryName / industryCode 字段。
    begin 留空 = 近两年(相对今天算,避免硬编码日期越用越旧)。"""
    if not begin:
        begin = (date.today() - timedelta(days=730)).isoformat()
    all_records = []
    for page in range(1, max_pages + 1):
        params = {
            "industryCode": industry_code, "pageSize": "100", "industry": "*",
            "rating": "*", "ratingChange": "*",
            "beginTime": begin, "endTime": "2030-01-01",
            "pageNo": str(page), "fields": "", "qType": "1",
        }
        r = em_get(REPORT_API, params=params,
                   headers={"Referer": "https://data.eastmoney.com/"}, timeout=30)  # 已内置限流
        d = r.json()
        rows = d.get("data") or []
        if not rows:
            break
        all_records.extend(rows)
        if page >= (d.get("TotalPage", 1) or 1):
            break
    return all_records

# 用法
# 1) 全行业最新研报
reports = eastmoney_industry_reports("*", max_pages=2)
print(f"共 {len(reports)} 篇行业研报")
for r in reports[:5]:
    print(f"  {r.get('publishDate','')[:10]} | {r.get('industryName')} | {r.get('orgSName')} | {r.get('title','')[:50]}")

# 2) 单行业(IT服务Ⅱ,行业码 1238)+ 下载首篇 PDF(复用 2.1 的 download_pdf)
it = eastmoney_industry_reports("1238", max_pages=1)
if it:
    download_pdf(it[0])

行业研报特有/常用字段(其余字段同 2.1 个股研报):

字段含义
industryName行业名称(如 IT服务Ⅱ、风电设备、光伏设备)
industryCode东财行业代码(用于 industry_code 精确过滤)
emRatingName行业评级(买入/增持/中性/...)
reportType报告类型
attachPages / attachSizePDF 页数 / 大小(KB)
infoCode喂给 download_pdf() 拼 PDF URL

行业码怎么拿: 东财行业码不是通用记忆码,没有公开的码表端点(bxpa 等已 404)。常用做法:先用 industry_code="*" 拉一批,从结果的 industryName/industryCode 找到目标行业的码,再用该码精确过滤。

2.2 同花顺一致预期EPS(直连 basic.10jqka.com.cn)

import requests
import pandas as pd
from io import StringIO

def ths_eps_forecast(code: str) -> pd.DataFrame:
    """
    同花顺机构一致预期EPS。
    直连 basic.10jqka.com.cn,解析HTML表格。
    code 支持 688017 / SH688017 / 688017.SH 等写法(内部归一化为纯 6 位)。
    返回 DataFrame: 年度, 预测机构数, 最小值, 均值, 最大值
    "均值" = 机构一致预期EPS
    """
    code = norm_ticker(code, stock_only=True)   # URL 路径只认纯 6 位,带前缀会 404 到空表
    url = f"https://basic.10jqka.com.cn/new/{code}/worth.html"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Referer": "https://basic.10jqka.com.cn/",
    }
    r = requests.get(url, headers=headers, timeout=15)
    r.encoding = "gbk"
    dfs = pd.read_html(StringIO(r.text))
    # 找含"每股收益"的表格
    for df in dfs:
        cols = [str(c) for c in df.columns]
        if any("每股收益" in c or "均值" in c for c in cols):
            return df
    # fallback: 返回第一个表
    return dfs[0] if dfs else pd.DataFrame()

# 用法
df = ths_eps_forecast("688017")
print(df)
# "预测机构数" < 3 的要谨慎

2.3 iwencai — NL语义搜索研报(唯一能力)

需要 API Key + X-Claw Headers(SkillHub 2.0 强制要求)。

import os
import json
import secrets
import requests

IWENCAI_BASE = os.environ.get("IWENCAI_BASE_URL", "https://openapi.iwencai.com")
IWENCAI_KEY = os.environ.get("IWENCAI_API_KEY", "")

def _claw_headers(call_type: str = "normal") -> dict:
    """SkillHub 2.0 必须的 X-Claw 鉴权头"""
    return {
        "X-Claw-Call-Type": call_type,
        "X-Claw-Skill-Id": "report-search",
        "X-Claw-Skill-Version": "2.0.0",
        "X-Claw-Plugin-Id": "none",
        "X-Claw-Plugin-Version": "none",
        "X-Claw-Trace-Id": secrets.token_hex(32),
    }

def iwencai_search(query: str, channel: str = "report", size: int = 50) -> list[dict]:
    """
    iwencai 语义搜索。
    channel: "report"(研报) / "announcement"(公告) / "news"(新闻)
    size: 默认10, 实测可调到50(隐藏参数)
    """
    headers = {
        "Authorization": f"Bearer {IWENCAI_KEY}",
        "Content-Type": "application/json",
        **_claw_headers(),
    }
    payload = {
        "channels": [channel],
        "app_id": "AIME_SKILL",
        "query": query,
        "size": size,
    }
    r = requests.post(
        f"{IWENCAI_BASE}/v1/comprehensive/search",
        json=payload, headers=headers, timeout=30,
    )
    if r.status_code != 200:
        raise RuntimeError(f"iwencai HTTP {r.status_code}: {r.text[:200]}")
    data = r.json()
    if data.get("status_code", 0) != 0:
        raise RuntimeError(f"iwencai error: {data.get('status_msg', '')}")
    return data.get("data") or []

def iwencai_query(query: str, page: int = 1, limit: int = 50) -> list[dict]:
    """
    iwencai NL数据查询(结构化字段)。
    例: "贵州茅台 ROE" → DataFrame-like rows
    """
    headers = {
        "Authorization": f"Bearer {IWENCAI_KEY}",
        "Content-Type": "application/json",
        **_claw_headers(),
    }
    payload = {
        "query": query,
        "page": str(page),
        "limit": str(limit),
        "is_cache": "1",
        "expand_index": "true",
    }
    r = requests.post(
        f"{IWENCAI_BASE}/v1/query2data",
        json=payload, headers=headers, timeout=30,
    )
    if r.status_code != 200:
        raise RuntimeError(f"iwencai HTTP {r.status_code}: {r.text[:200]}")
    data = r.json()
    if data.get("status_code", 0) != 0:
        raise RuntimeError(f"iwencai error: {data.get('status_msg', '')}")
    return data.get("datas") or []

def dedup_articles(articles: list[dict]) -> list[dict]:
    """同一uid仅保留score最高的段落"""
    best = {}
    for a in articles:
        uid = a.get("uid", "") or f"{a.get('title','')}|{a.get('publish_date','')}"
        score = float(a.get("score", 0))
        if uid not in best or score > float(best[uid].get("score", 0)):
            best[uid] = a
    return sorted(best.values(), key=lambda x: x.get("publish_date", ""), reverse=True)

# 用法: NL语义搜索研报
articles = iwencai_search("人形机器人 行星滚柱丝杠 2026", channel="report", size=50)
articles = dedup_articles(articles)
for a in articles[:5]:
    extra = a.get("extra") or {}
    if isinstance(extra, str):
        extra = json.loads(extra)
    print(f"{a.get('publish_date','')[:10]} | {extra.get('organization','')} | {a.get('title','')[:60]}")

iwencai 的唯一价值: NL 主题搜索。"人形机器人 行星滚柱丝杠" 这种跨主题检索只有 iwencai 能做。按标的搜研报走东财 reportapi 更稳定。


Layer 3: 信号层

3.1 同花顺热点 — 当日强势股 + 题材归因 reason tags(独家)

核心价值: 不只告诉你"哪些走强",还告诉你**"为什么走强"** —— 同花顺编辑部人工运营的题材标签。

import requests
import pandas as pd

def ths_hot_reason(date: str = None) -> pd.DataFrame:
    """
    同花顺当日强势股归因。
    date: 'YYYY-MM-DD' 格式,None=今天
    返回 DataFrame,含每只股票的题材标签 (reason)。

    实测: 73ms 拿到 ~125 只 + 完整字段
    """
    from datetime import date as _date
    if date is None:
        date = _date.today().strftime("%Y-%m-%d")

    url = (
        f"http://zx.10jqka.com.cn/event/api/getharden/"
        f"date/{date}/orderby/date/orderway/desc/charset/GBK/"
    )
    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "Chrome/117.0.0.0 Safari/537.36"
        )
    }
    r = requests.get(url, headers=headers, timeout=10)
    data = r.json()
    if data.get("errocode", 0) != 0:
        raise RuntimeError(f"同花顺热点错误: {data.get('errormsg', '')}")

    rows = data.get("data") or []
    df = pd.DataFrame(rows)
    if df.empty:
        return df

    # 字段重命名(中文友好)
    rename_map = {
        "name": "名称", "code": "代码", "reason": "题材归因",
        "close": "收盘价", "zhangdie": "涨跌额", "zhangfu": "涨幅%",
        "huanshou": "换手率%", "chengjiaoe": "成交额",
        "chengjiaoliang": "成交量", "ddejingliang": "大单净量",
        "market": "市场",
    }
    df = df.rename(columns=rename_map)
    return df

# 用法
df = ths_hot_reason("2026-05-09")
print(f"当日强势股: {len(df)} 只")
print(df[["代码", "名称", "涨幅%", "题材归因"]].head(10))

同花顺热点字段速查

原字段中文说明
code代码6 位股票代码
name名称简称
reason题材归因核心字段,人工运营 tags,如"算力租赁+Token工厂+AI政务"
zhangfu涨幅%当日涨幅
huanshou换手率%当日换手
chengjiaoe成交额
chengjiaoliang成交量
ddejingliang大单净量主力净流入指标
close收盘价
zhangdie涨跌额
market市场沪/深/北

3.2 同花顺北向资金 — hsgtApi 实时分钟流向 + 本地自缓存历史

⚠️ 深股通实时流向近期不可靠(2026-07 实测): 沪股通(hgt)分钟序列完整,但深股通(sgt) 常只回传零星几个点、末值量级异常。根因是北向自 2024-08 起收紧盘中实时披露,非本代码问题。 结论:hgt 可用于当日情绪,sgt 仅供参考;要权威北向数据用 HKEX 官方日统计 (hkex.com.hk/chi/csm/DailyStat/data_tab_daily_YYYYMMDDc.js,见文末「备用源速查」)。

已知行业性问题: eastmoney 全系北向数据自 2024-08 后净买额字段返回 NaN/0,属上游断供。已改为本地 CSV 自缓存模式——每次拉实时数据后自动写入本地 CSV,历史越跑越丰富。

import requests
import pandas as pd
from pathlib import Path

HSGT_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "Chrome/117.0.0.0 Safari/537.36"
    ),
    "Host": "data.hexin.cn",
    "Referer": "https://data.hexin.cn/",
}

def hsgt_realtime() -> pd.DataFrame:
    """
    沪深股通当日实时分钟流向(含集合竞价 09:10–15:00,262 个时间点)。
    返回字段: time, hgt(沪股通累计净买入), sgt(深股通累计净买入)
    单位: 亿元
    """
    url = "https://data.hexin.cn/market/hsgtApi/method/dayChart/"
    r = requests.get(url, headers=HSGT_HEADERS, timeout=10)
    d = r.json()
    times = d.get("time", [])
    hgt = d.get("hgt", [])
    sgt = d.get("sgt", [])

    n = len(times)
    return pd.DataFrame({
        "time": times,
        "hgt_yi": hgt[:n] + [None] * (n - len(hgt)),
        "sgt_yi": sgt[:n] + [None] * (n - len(sgt)),
    })

# === 自缓存辅助函数 ===

def _northbound_cache_path() -> Path:
    """北向资金本地 CSV 缓存路径"""
    p = Path.home() / ".tradingagents" / "cache" / "northbound_daily.csv"
    p.parent.mkdir(parents=True, exist_ok=True)
    return p

def _save_northbound_snapshot(date: str, hgt: float, sgt: float):
    """写入/更新当天北向收盘数据到 CSV"""
    path = _northbound_cache_path()
    rows = {}
    if path.exists():
        for line in path.read_text().strip().split("\n")[1:]:
            parts = line.split(",")
            if len(parts) == 3:
                rows[parts[0]] = line
    rows[date] = f"{date},{hgt},{sgt}"
    with open(path, "w") as f:
        f.write("date,hgt,sgt\n")
        for d in sorted(rows.keys()):
            f.write(rows[d] + "\n")

def _load_northbound_history(n: int = 20) -> pd.DataFrame:
    """读取最近 N 天北向历史"""
    path = _northbound_cache_path()
    if not path.exists():
        return pd.DataFrame()
    df = pd.read_csv(path)
    return df.tail(n)

# 用法 1: 实时分钟流向
df = hsgt_realtime()
print(f"分钟点数: {len(df)}")
print(df.tail(5))

# 用法 2: 自动缓存今日收盘数据
if not df.empty:
    last = df.dropna().iloc[-1]
    _save_northbound_snapshot("2026-05-17", last["hgt_yi"], last["sgt_yi"])

# 用法 3: 读取历史
hist = _load_northbound_history(20)
print(hist)

3.3 东财 slist — 个股所属板块/概念归属(V3.2.2 替换百度)

核心价值: 一次调用拿到个股所属的全部板块(行业 + 概念 + 地域混合),含板块代码(BK码)、当日涨跌幅、板块龙头股。题材归因、板块联动分析必备。

V3.2.2 替换说明: 百度 PAE getrelatedblock 接口已失效(实测返回 ResultCode 10003 + 空数组,#18),改用东财 slist 个股所属板块接口(spt=3,一次请求拿全,零鉴权)。东财把行业/概念/地域混在一个列表里返回,板块名本身已自解释(如「食品饮料」是行业、「贵州板块」是地域、「酿酒概念」是概念),AI 直接用板块名做题材归因即可。

def eastmoney_concept_blocks(code: str) -> dict:
    """
    个股所属板块/概念归属(东财 slist,一次请求拿全,已内置限流)。
    返回: {total, boards: [{name, code(BK码), change_pct, lead_stock}], concept_tags: [板块名...]}
    boards 混合 行业/概念/地域,板块名自解释;concept_tags 是所有板块名的便捷列表。
    """
    market_code = 1 if code.startswith("6") else 0
    params = {
        "fltt": "2", "invt": "2",
        "secid": f"{market_code}.{code}",
        "spt": "3", "pi": "0", "pz": "200", "po": "1",
        "fields": "f12,f14,f3,f128",
    }
    headers = {"User-Agent": UA, "Referer": "https://quote.eastmoney.com/"}
    try:
        r = em_get("https://push2.eastmoney.com/api/qt/slist/get",
                   params=params, headers=headers, timeout=15)
        d = r.json()
    except Exception as e:
        print(f"[WARN] 东财板块归属请求失败: {e}")
        return {"total": 0, "boards": [], "concept_tags": []}

    diff = (d.get("data") or {}).get("diff") or {}
    items = diff.values() if isinstance(diff, dict) else diff
    boards = []
    for it in items:
        boards.append({
            "name": it.get("f14", ""),         # 板块名
            "code": it.get("f12", ""),         # BK 板块代码
            "change_pct": it.get("f3", ""),    # 板块当日涨跌幅
            "lead_stock": it.get("f128", ""),  # 板块龙头股
        })
    return {
        "total": len(boards),
        "boards": boards,
        "concept_tags": [b["name"] for b in boards],
    }

# 用法
blocks = eastmoney_concept_blocks("600519")
print(f"共 {blocks['total']} 个板块")
print("板块归属:", blocks["concept_tags"])
# → ['食品饮料', '白酒Ⅲ', '白酒Ⅱ', '贵州板块', '酿酒概念', 'HS300_', ...]

注意: 东财不区分行业/概念/地域类型(混在一个列表返回)。如需精确分类可按板块名判断,或另查全市场板块清单(clist + m:90+t:1/2/3)——但后者每次需多发请求、大页易触发风控,不推荐在批量场景用。

3.4 东财 push2 — 个股资金流向(分钟级)

盘中实时分钟级资金流(主力/大单/中单/小单/超大单净流入)。

V3.1 替换说明: 百度 PAE fundflowfundsortlist 接口已于 2026-05 下线(返回 null),改用东财 push2 资金流 API。日级资金流见 Layer 4.5 stock_fund_flow_120d()

import requests

def eastmoney_fund_flow_minute(code: str) -> list[dict]:
    """
    个股资金流向(分钟级,当日盘中)。
    code: 6位股票代码
    返回: [{time, main_net, small_net, mid_net, large_net, super_net}, ...]
    单位: 元
    """
    secid = f"1.{code}" if code.startswith("6") else f"0.{code}"
    url = "https://push2.eastmoney.com/api/qt/stock/fflow/kline/get"
    params = {
        "secid": secid, "klt": 1,
        "fields1": "f1,f2,f3,f7",
        "fields2": "f51,f52,f53,f54,f55,f56,f57",
    }
    headers = {
        "User-Agent": UA,
        "Referer": "https://quote.eastmoney.com/",
        "Origin": "https://quote.eastmoney.com",
    }
    try:
        r = em_get(url, params=params, headers=headers, timeout=10)
        d = r.json()
    except Exception as e:
        print(f"[WARN] push2 资金流请求失败: {e}")
        return []

    rows = []
    for line in d.get("data", {}).get("klines", []):
        parts = line.split(",")
        if len(parts) >= 6:
            rows.append({
                "time": parts[0],
                "main_net": float(parts[1]),
                "small_net": float(parts[2]),
                "mid_net": float(parts[3]),
                "large_net": float(parts[4]),
                "super_net": float(parts[5]),
            })
    return rows

# 用法: 分钟级实时资金流
realtime = eastmoney_fund_flow_minute("000858")
if realtime:
    last = realtime[-1]
    signal = "bullish" if last["main_net"] > 0 else "bearish"
    print(f"主力净流入: {last['main_net']:.0f}元 → {signal}")
    # 统计全天主力净流入
    total = sum(r["main_net"] for r in realtime)
    print(f"全天主力累计: {total/1e4:.0f}万元")

注意: push2 资金流金额单位是(非万元),使用时注意换算。klt=1 分钟级,klt=101 日级。

3.5 龙虎榜席位 — 个股上榜记录 + 买卖席位 TOP5 + 机构动向

直连东财 datacenter API,不依赖第三方封装。

import requests
from datetime import datetime, timedelta

def dragon_tiger_board(code: str, trade_date: str, look_back: int = 30) -> dict:
    """
    龙虎榜数据聚合。
    trade_date: YYYY-MM-DD
    look_back: 回看天数
    返回: {records: [...], seats: {buy: [...], sell: [...]}, institution: {...}}
    """
    start = datetime.strptime(trade_date, "%Y-%m-%d") - timedelta(days=look_back)
    start_str = start.strftime("%Y-%m-%d")

    # 1. 上榜记录
    records = []
    data = eastmoney_datacenter(
        "RPT_DAILYBILLBOARD_DETAILSNEW",
        filter_str=f"(TRADE_DATE>='{start_str}')(TRADE_DATE<='{trade_date}')(SECURITY_CODE=\"{code}\")",
        page_size=50,
        sort_columns="TRADE_DATE", sort_types="-1",
    )
    for row in data:
        records.append({
            "date": str(row.get("TRADE_DATE", ""))[:10],
            "reason": row.get("EXPLANATION", ""),
            "net_buy": round((row.get("BILLBOARD_NET_AMT") or 0) / 10000, 1),
            "turnover": round(float(row.get("TURNOVERRATE") or 0), 2),
        })

    # 2. 最近上榜的买卖席位
    # ⚠️ buy_data/sell_data 必须在 if 之前初始化:第 3 步无条件遍历它们,
    # 而回看窗口内无上榜记录(大市值 / 低换手率标的的常态)时 if 分支不执行,
    # 变量从未绑定 → UnboundLocalError,调用方拿到的是崩溃而不是"空结果"(#45)。
    buy_data, sell_data = [], []
    seats = {"buy": [], "sell": []}
    if records:
        latest_date = records[0]["date"]
        # 买入席位
        buy_data = eastmoney_datacenter(
            "RPT_BILLBOARD_DAILYDETAILSBUY",
            filter_str=f"(TRADE_DATE='{latest_date}')(SECURITY_CODE=\"{code}\")",
            page_size=10,
            sort_columns="BUY", sort_types="-1",
        )
        for row in buy_data[:5]:
            seats["buy"].append({
                "name": row.get("OPERATEDEPT_NAME", ""),
                "buy_amt": round((row.get("BUY") or 0) / 10000, 1),
                "sell_amt": round((row.get("SELL") or 0) / 10000, 1),
                "net": round((row.get("NET") or 0) / 10000, 1),
            })
        # 卖出席位
        sell_data = eastmoney_datacenter(
            "RPT_BILLBOARD_DAILYDETAILSSELL",
            filter_str=f"(TRADE_DATE='{latest_date}')(SECURITY_CODE=\"{code}\")",
            page_size=10,
            sort_columns="SELL", sort_types="-1",
        )
        for row in sell_data[:5]:
            seats["sell"].append({
                "name": row.get("OPERATEDEPT_NAME", ""),
                "buy_amt": round((row.get("BUY") or 0) / 10000, 1),
                "sell_amt": round((row.get("SELL") or 0) / 10000, 1),
                "net": round((row.get("NET") or 0) / 10000, 1),
            })

    # 3. 机构买卖统计(从买卖席位明细中筛选 OPERATEDEPT_CODE="0" 即机构专用席位)
    institution = {"buy_amt": 0, "sell_amt": 0, "net_amt": 0}
    for detail_data, side in [(buy_data, "buy"), (sell_data, "sell")]:
        for row in detail_data:
            if str(row.get("OPERATEDEPT_CODE", "")) == "0":
                amt = (row.get("BUY") or 0) if side == "buy" else (row.get("SELL") or 0)
                if side == "buy":
                    institution["buy_amt"] += amt
                else:
                    institution["sell_amt"] += amt
    institution["buy_amt"] = round(institution["buy_amt"] / 10000, 1)
    institution["sell_amt"] = round(institution["sell_amt"] / 10000, 1)
    institution["net_amt"] = round(institution["buy_amt"] - institution["sell_amt"], 1)

    return {"records": records, "seats": seats, "institution": institution}

# 用法
data = dragon_tiger_board("002475", "2026-05-17")
print(f"近30日上榜 {len(data['records'])} 次")
for r in data["records"]:
    print(f"  {r['date']}: {r['reason']}")
if data["seats"]["buy"]:
    print("买入席位 TOP5:")
    for s in data["seats"]["buy"]:
        print(f"  {s['name']}: 买{s['buy_amt']}万 卖{s['sell_amt']}万 净{s['net']}万")

ST 股注意: 5% 涨跌停更容易触发龙虎榜("连续三日偏离值累计达12%"),科创板 20% 涨跌停则较少触发。

3.6 限售解禁日历 — 历史解禁 + 未来 90 天待解禁

from datetime import datetime, timedelta

def lockup_expiry(code: str, trade_date: str, forward_days: int = 90) -> dict:
    """
    限售解禁日历。
    返回: {history: [...], upcoming: [...]}
    """
    # 1. 历史解禁记录
    history_data = eastmoney_datacenter(
        "RPT_LIFT_STAGE",
        filter_str=f"(SECURITY_CODE=\"{code}\")",
        page_size=15,
        sort_columns="FREE_DATE", sort_types="-1",
    )
    history = []
    for row in history_data:
        history.append({
            "date": str(row.get("FREE_DATE", ""))[:10],
            "type": row.get("FREE_SHARES_TYPE", ""),        # 解禁类型(东财2026改列名, 旧LIMITED_STOCK_TYPE已废)
            "shares": row.get("FREE_SHARES", 0),             # 本次解禁股数(万股)
            "able_shares": row.get("ABLE_FREE_SHARES", 0),   # 实际可流通股数(万股, 更贴近真实抛压)
            "ratio": row.get("FREE_RATIO", 0),               # 占总股本比(小数, ×100 得百分比)
        })

    # 2. 未来待解禁
    end_date = datetime.strptime(trade_date, "%Y-%m-%d") + timedelta(days=forward_days)
    end_str = end_date.strftime("%Y-%m-%d")
    upcoming_data = eastmoney_datacenter(
        "RPT_LIFT_STAGE",
        filter_str=f"(SECURITY_CODE=\"{code}\")(FREE_DATE>='{trade_date}')(FREE_DATE<='{end_str}')",
        page_size=20,
        sort_columns="FREE_DATE", sort_types="1",
    )
    upcoming = []
    for row in upcoming_data:
        upcoming.append({
            "date": str(row.get("FREE_DATE", ""))[:10],
            "type": row.get("FREE_SHARES_TYPE", ""),        # 解禁类型(东财2026改列名, 旧LIMITED_STOCK_TYPE已废)
            "shares": row.get("FREE_SHARES", 0),             # 本次解禁股数(万股)
            "able_shares": row.get("ABLE_FREE_SHARES", 0),   # 实际可流通股数(万股, 更贴近真实抛压)
            "ratio": row.get("FREE_RATIO", 0),               # 占总股本比(小数, ×100 得百分比)
        })

    return {"history": history, "upcoming": upcoming}

# 用法
data = lockup_expiry("002475", "2026-05-17")
print(f"历史解禁 {len(data['history'])} 批")
for h in data["history"][:5]:
    print(f"  {h['date']}: {h['type']} 数量={h['shares']}")
if data["upcoming"]:
    print(f"未来90天待解禁 {len(data['upcoming'])} 批")
else:
    print("未来90天无待解禁")

限售股类型参考:

  • 首发原股东限售股份(IPO 后 1-3 年)
  • 首发机构配售股份(IPO 战略配售)
  • 定向增发机构配售股份(6-18 个月)
  • 股权激励限售股份

3.7 行业板块排名(V3.0 改用东财 — 同花顺加了反爬401)

东财行业板块涨跌幅排名,一次调用看全市场行业轮动。

import requests

def industry_comparison(top_n: int = 20) -> dict:
    """
    全行业涨跌幅排名(东财行业板块,~100 个行业)。
    返回: {top: [...], bottom: [...], total: int}
    """
    url = "https://push2.eastmoney.com/api/qt/clist/get"
    params = {
        "pn": "1", "pz": "100", "po": "1", "np": "1",
        "fltt": "2", "invt": "2", "fid": "f3",   # fid=f3 + po=1:按涨跌幅降序(缺 fid 时 top/bottom 切片非按涨幅排)
        "fs": "m:90+t:2",
        "fields": "f2,f3,f4,f12,f13,f14,f104,f105,f128,f136,f140,f141,f207",
    }
    headers = {"User-Agent": UA}
    r = em_get(url, params=params, headers=headers, timeout=15)
    d = r.json()
    items = d.get("data", {}).get("diff", [])
    if not items:
        return {"top": [], "bottom": [], "total": 0}

    rows = []
    for i, item in enumerate(items):
        rows.append({
            "rank": i + 1,
            "name": item.get("f14", ""),
            "change_pct": item.get("f3", 0),
            "code": item.get("f12", ""),
            "up_count": item.get("f104", 0),
            "down_count": item.get("f105", 0),
            "leader": item.get("f140", ""),
            "leader_change": item.get("f136", 0),
        })

    return {
        "top": rows[:top_n],
        "bottom": rows[-top_n:],
        "total": len(rows),
    }

# 用法
data = industry_comparison(20)
print(f"共 {data['total']} 个行业")
print("\nTOP 10 涨幅:")
for r in data["top"][:10]:
    print(f"  {r['rank']}. {r['name']}: {r['change_pct']}% 涨{r['up_count']}跌{r['down_count']} 领涨{r['leader']}")
print("\nBOTTOM 5 跌幅:")
for r in data["bottom"][-5:]:
    print(f"  {r['rank']}. {r['name']}: {r['change_pct']}%")

3.8 板块资金流向(行业/概念/地域 × 今日/5日/10日)

东财板块资金流向——主力净流入额/净占比 + 超大/大/中/小单四档,覆盖行业、概念、地域三类板块,今日/5日/10日三个周期。与 §3.7 板块排名同源同接口(push2 clist),只是补请求了资金流字段(f62/f184/f66...)。走 em_get 限流防封。

import requests

# 板块类型 → 东财 fs 参数
_BOARD_FS = {"industry": "m:90+t:2", "concept": "m:90+t:3", "region": "m:90+t:1"}
# 周期 → (排序fid, 主力净额, 主力净占比, 涨跌幅, 领涨股name);四档明细仅今日
_BOARD_PERIOD = {
    "today": ("f62",  "f62",  "f184", "f3",   "f204"),
    "5d":    ("f164", "f164", "f165", "f109", "f257"),
    "10d":   ("f174", "f174", "f175", "f160", None),   # 10日领涨股名称字段不稳定,省略
}

def board_fund_flow(board_type: str = "industry", period: str = "today",
                    

This file is truncated. Read the full SKILL.md on GitHub.

Frequently asked questions about A股全栈数据工具包

Similar skills