- 新增 十大流通股东/十大股东明细/股东户数 三个抓取模块并接入 API - 冒烟测试扩展至 30 用例, 全部通过 - 新增 SKILLS.md (给其他 agent 使用的工具说明书) - 补充待优化端点清单与文档对齐规范main
parent
11ddbd3c49
commit
797369bbed
@ -0,0 +1,139 @@ |
|||||||
|
# SKILLS.md — EMWeb API 使用说明书 |
||||||
|
|
||||||
|
> 本工具是一个 HTTP 财务数据服务,聚合了东方财富 CHOICE 的财务数据抓取能力。 |
||||||
|
> **无鉴权、无 cookie、无 key**,直接 GET 即可调用,返回结构化 JSON。 |
||||||
|
> 适合任何需要 A 股上市公司财务数据/股东/分红信息的 agent 使用。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 1. 基本信息 |
||||||
|
|
||||||
|
| 项 | 值 | |
||||||
|
|---|---| |
||||||
|
| Base URL | `http://<服务地址>:8765` | |
||||||
|
| 鉴权 | 无 | |
||||||
|
| 数据覆盖 | A 股财务三表、财务指标、主营构成、分红、股东数据 | |
||||||
|
| Swagger | `http://<服务地址>:8765/docs`(可视化调试) | |
||||||
|
|
||||||
|
## 2. 通用约定 |
||||||
|
|
||||||
|
- **股票代码格式**:`代码.市场`,如 `603233.SH`(上交所)、`000001.SZ`(深交所)。多只用英文逗号分隔:`603233.SH,600519.SH`。 |
||||||
|
- **`years` 参数**:拉取最近 N 年数据,范围 1~20。 |
||||||
|
- **返回结构**(成功): |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"success": true, |
||||||
|
"count": 10, |
||||||
|
"columns": ["SECUCODE", "REPORT_DATE", "..."], |
||||||
|
"data": [ { "SECUCODE": "603233.SH", ... } ] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
- **错误情况**: |
||||||
|
- HTTP `400`:参数不合法(如 `stocks` 为空)。 |
||||||
|
- HTTP `404` + `success: false`:请求成功但无数据(股票代码写错、或该股无此数据)。此时 `data` 为 `[]`,不要重试。 |
||||||
|
- HTTP `5xx`:上游接口异常,可稍后重试。 |
||||||
|
- 空值(NaN/NaT/±inf)在 JSON 中统一为 `null`。 |
||||||
|
|
||||||
|
## 3. 接口总览 |
||||||
|
|
||||||
|
| 类别 | 路径 | 一句话说明 | |
||||||
|
|---|---|---| |
||||||
|
| 健康 | `GET /health` | 探活 | |
||||||
|
| 财务摘要 | `GET /fin/summary` | 财务摘要(报告期口径,需 ORG_CODE) | |
||||||
|
| 财务摘要 | `GET /fin/quarterly-summary` | 财务摘要(单季度,只需代码) | |
||||||
|
| 主营 | `GET /fin/main-business` | 主营构成(按产品 / 按地区) | |
||||||
|
| 报表 | `GET /fin/balance` | 资产负债表 | |
||||||
|
| 报表 | `GET /fin/income` | 利润表 | |
||||||
|
| 报表 | `GET /fin/cashflow` | 现金流量表 | |
||||||
|
| 指标 | `GET /fin/profitability` | 盈利能力与收益质量 | |
||||||
|
| 指标 | `GET /fin/capital-structure` | 资本结构与偿债能力 | |
||||||
|
| 指标 | `GET /fin/operate-ability` | 营运能力 | |
||||||
|
| 指标 | `GET /fin/growth` | 成长能力 | |
||||||
|
| 指标 | `GET /fin/dupont` | 杜邦分析(ROE 拆解) | |
||||||
|
| 分红 | `GET /fin/dividend-detail` | 分红明细(方案/除权日等) | |
||||||
|
| 分红 | `GET /fin/dividend-statics` | 分红统计(按年汇总) | |
||||||
|
| 股东 | `GET /fin/free-holders` | 十大流通股东 | |
||||||
|
| 股东 | `GET /fin/holders` | 十大股东明细 | |
||||||
|
| 股东 | `GET /fin/holder-num` | 股东户数 | |
||||||
|
|
||||||
|
## 4. 接口详情 |
||||||
|
|
||||||
|
### 4.1 快速探活 |
||||||
|
|
||||||
|
```bash |
||||||
|
curl "http://127.0.0.1:8765/health" |
||||||
|
# => {"status":"ok"} |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.2 财务摘要 |
||||||
|
|
||||||
|
```bash |
||||||
|
# 单季度 (只需代码) —— 适合拿季度经营数据 |
||||||
|
curl "http://127.0.0.1:8765/fin/quarterly-summary?stocks=603233.SH&years=3" |
||||||
|
|
||||||
|
# 报告期口径 (推荐带 ORG_CODE, 否则可能查不到, 详见第 5 节) |
||||||
|
curl "http://127.0.0.1:8765/fin/summary?stocks=603233.SH:10500736&years=5" |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.3 主营构成 |
||||||
|
|
||||||
|
```bash |
||||||
|
# 按产品分类 (中文参数需要 URL 编码: 产品=%E4%BA%A7%E5%93%81, 地区=%E5%9C%B0%E5%8C%BA) |
||||||
|
curl "http://127.0.0.1:8765/fin/main-business?stocks=603233.SH&classify_type=%E4%BA%A7%E5%93%81&years=3" |
||||||
|
``` |
||||||
|
|
||||||
|
关键列:`STD_REPORT_NAME`(产品/地区名,含"合计"行)、`MAIN_BUSINESS_INCOME`、`MAIN_BUSINESS_RPOFIT`、`GROSS_RPOFIT_RATIO`(毛利率)。 |
||||||
|
|
||||||
|
### 4.4 三张财务报表 |
||||||
|
|
||||||
|
```bash |
||||||
|
curl "http://127.0.0.1:8765/fin/balance?stocks=603233.SH&years=3" # 资产负债表, 列含 TOTAL_ASSETS 等 |
||||||
|
curl "http://127.0.0.1:8765/fin/income?stocks=603233.SH&years=3" # 利润表, 列含 TOTAL_OPERATE_INCOME 等 |
||||||
|
curl "http://127.0.0.1:8765/fin/cashflow?stocks=603233.SH&years=3" # 现金流量表, 列含 SALES_SERVICES 等 |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.5 财务指标(四大分析 + 杜邦) |
||||||
|
|
||||||
|
```bash |
||||||
|
curl "http://127.0.0.1:8765/fin/profitability?stocks=603233.SH" # ROE/ROA/毛利率 |
||||||
|
curl "http://127.0.0.1:8765/fin/capital-structure?stocks=603233.SH" # 资产负债率/流动比率 |
||||||
|
curl "http://127.0.0.1:8765/fin/operate-ability?stocks=603233.SH" # 周转率/周转天数 |
||||||
|
curl "http://127.0.0.1:8765/fin/growth?stocks=603233.SH" # 营收/净利同比增速 |
||||||
|
curl "http://127.0.0.1:8765/fin/dupont?stocks=603233.SH" # ROE 三因素拆解 |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.6 分红 |
||||||
|
|
||||||
|
```bash |
||||||
|
curl "http://127.0.0.1:8765/fin/dividend-detail?stocks=603233.SH&years=5" # 明细: IMPL_PLAN_PROFILE(方案)/EX_DIVIDEND_DATE(除权日) |
||||||
|
curl "http://127.0.0.1:8765/fin/dividend-statics?stocks=603233.SH&years=5" # 统计: GXL(股息率)/AUALACCMDIV_ARD(每股股利) |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.7 股东数据 |
||||||
|
|
||||||
|
```bash |
||||||
|
curl "http://127.0.0.1:8765/fin/free-holders?stocks=603233.SH&years=3" # 十大流通股东: HOLDER_NAME/HOLD_RATIO |
||||||
|
curl "http://127.0.0.1:8765/fin/holders?stocks=603233.SH&years=3" # 十大股东明细: RANK/HOLDER_NAME/DIRECTION |
||||||
|
curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3" # 股东户数: HOLDER_TOTAL_NUM/户数增减 |
||||||
|
``` |
||||||
|
|
||||||
|
## 5. 使用注意事项 |
||||||
|
|
||||||
|
1. **只有 `/fin/summary` 需要 ORG_CODE**:格式 `代码:ORG_CODE`(如 `603233.SH:10500736`)。ORG_CODE 是东财内部机构编码,只传代码时服务会尝试自动查询但**不稳定**;如果 400 报错提示 ORG_CODE,请改用 `代码:ORG_CODE` 格式,或换用 `/fin/quarterly-summary`。 |
||||||
|
2. **中文参数要 URL 编码**:仅 `classify_type`(产品/地区)一处,浏览器或 curl 请编码后传。 |
||||||
|
3. **一次可查多只**:`stocks` 用逗号分隔,返回多股合并数据(每行带 `SECUCODE` 区分)。 |
||||||
|
4. **列名不翻译**:`columns` 与上游东财接口一致(英文大写,如 `REPORT_DATE`、`TOTAL_ASSETS`),`data` 每行是键值对象,值可能为 `null`。 |
||||||
|
5. **报告期**:财务数据按报告期返回,`REPORT_DATE`/`END_DATE` 字段为 `YYYY-MM-DD` 格式(JSON 中是字符串)。 |
||||||
|
6. **频率**:接口为实时抓取,没有缓存;高频批量调用请控制节奏。 |
||||||
|
|
||||||
|
## 6. 常见组合场景 |
||||||
|
|
||||||
|
| 任务 | 推荐调用 | |
||||||
|
|---|---| |
||||||
|
| 快速体检一家公司 | `quarterly-summary` + `main-business` + `profitability` | |
||||||
|
| 深度财务分析 | `balance` + `income` + `cashflow` + `dupont` | |
||||||
|
| 成长性判断 | `growth` + `quarterly-summary` | |
||||||
|
| 分红投资 | `dividend-detail` + `dividend-statics` | |
||||||
|
| 股东结构/筹码分析 | `holders` + `free-holders` + `holder-num` | |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
- 每次更新需要需要检查 |
||||||
|
- 代码和文档的对齐 |
||||||
|
- 代码/文档 和 skills 对齐 |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 你需要补的 EMWebApi 端点清单 |
||||||
|
|
||||||
|
按价值排序,按工作量排序(带推测的 CHOICE 接口 ID): |
||||||
|
|
||||||
|
### 🔴 高价值(建议优先补) |
||||||
|
|
||||||
|
| # | 端点名 | 对应路径 | 用途 | 推测 CHOICE 接口 | |
||||||
|
|---|--------|---------|------|-----------------| |
||||||
|
| 1 | `/fin/holder` | 股东户数 + 十大股东 | 筹码集中度 + 机构动向 | `RPT_F10_EH_EHD` / `RPT_F10_SHAREHOLDER` | |
||||||
|
| 2 | `/fin/valuation-history` | 历史 PE/PB 分位 | 当前估值贵不贵 | `RPT_VALUATION_ANALYSIS` | |
||||||
|
| 3 | `/fin/profit-forecast` | 卖方一致预期 | 未来 EPS 预测 + 评级 | `RPT_PROFIT_FORECAST` 或 `RPT_F10_PROFITNOTE` | |
||||||
|
| 4 | `/fin/industry-compare` | 行业横向对比 | 在行业里的位置 | `RPT_INDUSTRY_COMP` | |
||||||
|
|
||||||
|
### 🟡 中价值(看时间补) |
||||||
|
|
||||||
|
| # | 端点名 | 对应路径 | 用途 | 推测 CHOICE 接口 | |
||||||
|
|---|--------|---------|------|-----------------| |
||||||
|
| 5 | `/fin/cashflow-quarterly` | 单季度现金流量表 | 季度经营性现金流 | 在现有 `fin/cashflow` 上加 `reportType` 参数 | |
||||||
|
| 6 | `/fin/operating-data` | 经营数据(药房专属) | 门店数、同店增长、新开/关闭 | `RPT_F10_OPDATA`(按行业走分表) | |
||||||
|
| 7 | `/fin/notice` | 重大事项 | 重组/激励/回购/担保 | `RPT_F10_NOTICE` 或事件流 | |
||||||
|
|
||||||
|
### 🟢 低价值(看心情) |
||||||
|
|
||||||
|
| # | 端点名 | 对应路径 | 用途 | 推测 CHOICE 接口 | |
||||||
|
|---|--------|---------|------|-----------------| |
||||||
|
| 8 | `/fin/management` | 管理层介绍 + 持股 | 老板背景、薪酬 | `RPT_F10_MANAGEMENT` | |
||||||
|
| 9 | `/fin/top-flow` | 大宗交易 + 高管增减持 | 减持信号 | `RPT_F10_BIGTRADE` / `RPT_F10_HOLDERCHANGE` | |
||||||
|
|
||||||
|
### 💡 实现提示 |
||||||
|
|
||||||
|
1. **`fin/operating-data` 是药房/银行/地产的行业专属接口**,CHOICE 不会给一个通用端点,可能要按行业分多个子接口(`fin/op-data-pharmacy` / `fin/op-data-bank` ...) |
||||||
|
2. **`fin/holder` 通常分两张**:股东户数 + 十大股东明细,建议拆成两个端点更清晰 |
||||||
|
3. **接口 ID 不确定的话**:你先写一个能跑通的 demo(比如 `summary`),我这边按你的命名风格对照补其它4. **每个新接口都建议加 `org_code` 参数**(`stocks=603233.SH:10500736`格式),少走自动查询这一步,更稳 |
||||||
|
|
||||||
|
**优先级建议**:今晚先攻 1+2+3(持仓 + 估值历史 + 盈利预测),这三个对大参林的报告补全是**质变级别**的提升。其它周末慢慢来。 |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
"""东方财富 CHOICE API 十大流通股东抓取工具。 |
||||||
|
- 输入: 股票代码列表 (如 ['603233.SH']) |
||||||
|
- 输出: pandas DataFrame (按报告期末的十大流通股东) |
||||||
|
- 不需要登录, 不需要 cookie, 无文件落盘 |
||||||
|
""" |
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import pandas as pd |
||||||
|
|
||||||
|
from utils._emchoice import fetch_report |
||||||
|
|
||||||
|
|
||||||
|
_REPORT_NAME = "RPT_F10_EH_FREEHOLDERS" |
||||||
|
_COLUMNS = "F10_FREEHOLDERS" |
||||||
|
|
||||||
|
|
||||||
|
def _build_params( |
||||||
|
secucode: str, |
||||||
|
start_date: str, |
||||||
|
end_date: str, |
||||||
|
) -> dict[str, str]: |
||||||
|
"""构造 RPT_F10_EH_FREEHOLDERS 查询参数.""" |
||||||
|
return { |
||||||
|
"source": "CHOICE", |
||||||
|
"reportName": _REPORT_NAME, |
||||||
|
"columns": _COLUMNS, |
||||||
|
"quoteColumns": "", |
||||||
|
"pageNumber": "", |
||||||
|
"pageSize": "", |
||||||
|
"sortColumns": "END_DATE,HOLD_NUM", |
||||||
|
"client": "SW", |
||||||
|
"filter": f'(SECUCODE="{secucode}")(END_DATE>=\'{start_date}\')(END_DATE<=\'{end_date}\')', |
||||||
|
"sortTypes": "-1,-1", |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def _fetch_one(secucode: str, years: int) -> pd.DataFrame: |
||||||
|
"""拉一只股票的十大流通股东.""" |
||||||
|
today = pd.Timestamp.today() |
||||||
|
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d") |
||||||
|
end = today.strftime("%Y-%m-%d") |
||||||
|
|
||||||
|
rows = fetch_report(_build_params(secucode, start, end)) |
||||||
|
df = pd.DataFrame(rows) |
||||||
|
if not df.empty: |
||||||
|
df["SECUCODE"] = secucode |
||||||
|
return df |
||||||
|
|
||||||
|
|
||||||
|
def get_free_holders(secucodes: list[str], years: int = 3) -> pd.DataFrame: |
||||||
|
"""抓取指定股票的十大流通股东并按股票合并。""" |
||||||
|
codes = [code.strip() for code in secucodes if code.strip()] |
||||||
|
if not codes: |
||||||
|
return pd.DataFrame() |
||||||
|
|
||||||
|
frames: list[pd.DataFrame] = [] |
||||||
|
for code in codes: |
||||||
|
df = _fetch_one(code, years) |
||||||
|
if not df.empty: |
||||||
|
frames.append(df) |
||||||
|
|
||||||
|
if not frames: |
||||||
|
return pd.DataFrame() |
||||||
|
return pd.concat(frames, ignore_index=True) |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
"""东方财富 CHOICE API 股东户数抓取工具。 |
||||||
|
- 输入: 股票代码列表 (如 ['603233.SH']) |
||||||
|
- 输出: pandas DataFrame (按报告期末的股东户数变化) |
||||||
|
- 不需要登录, 不需要 cookie, 无文件落盘 |
||||||
|
""" |
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import pandas as pd |
||||||
|
|
||||||
|
from utils._emchoice import fetch_report |
||||||
|
|
||||||
|
|
||||||
|
_REPORT_NAME = "RPT_F10_EH_HOLDERNUM" |
||||||
|
_COLUMNS = "CHOICEF9_EH_HOLDERNUM" |
||||||
|
|
||||||
|
|
||||||
|
def _build_params( |
||||||
|
secucode: str, |
||||||
|
start_date: str, |
||||||
|
end_date: str, |
||||||
|
) -> dict[str, str]: |
||||||
|
"""构造 RPT_F10_EH_HOLDERNUM 查询参数.""" |
||||||
|
return { |
||||||
|
"source": "CHOICE", |
||||||
|
"reportName": _REPORT_NAME, |
||||||
|
"columns": _COLUMNS, |
||||||
|
"quoteColumns": "", |
||||||
|
"filter": f'(SECUCODE="{secucode}")(END_DATE>=\'{start_date}\')(END_DATE<=\'{end_date}\')', |
||||||
|
"pageNumber": "", |
||||||
|
"pageSize": "", |
||||||
|
"sortColumns": "END_DATE", |
||||||
|
"client": "SW", |
||||||
|
"sortTypes": "-1", |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def _fetch_one(secucode: str, years: int) -> pd.DataFrame: |
||||||
|
"""拉一只股票的股东户数.""" |
||||||
|
today = pd.Timestamp.today() |
||||||
|
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d") |
||||||
|
end = today.strftime("%Y-%m-%d") |
||||||
|
|
||||||
|
rows = fetch_report(_build_params(secucode, start, end)) |
||||||
|
df = pd.DataFrame(rows) |
||||||
|
if not df.empty: |
||||||
|
df["SECUCODE"] = secucode |
||||||
|
return df |
||||||
|
|
||||||
|
|
||||||
|
def get_holder_num(secucodes: list[str], years: int = 3) -> pd.DataFrame: |
||||||
|
"""抓取指定股票的股东户数并按股票合并。""" |
||||||
|
codes = [code.strip() for code in secucodes if code.strip()] |
||||||
|
if not codes: |
||||||
|
return pd.DataFrame() |
||||||
|
|
||||||
|
frames: list[pd.DataFrame] = [] |
||||||
|
for code in codes: |
||||||
|
df = _fetch_one(code, years) |
||||||
|
if not df.empty: |
||||||
|
frames.append(df) |
||||||
|
|
||||||
|
if not frames: |
||||||
|
return pd.DataFrame() |
||||||
|
return pd.concat(frames, ignore_index=True) |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
"""东方财富 CHOICE API 十大股东明细抓取工具。 |
||||||
|
- 输入: 股票代码列表 (如 ['603233.SH']) |
||||||
|
- 输出: pandas DataFrame (按报告期末的十大股东明细) |
||||||
|
- 不需要登录, 不需要 cookie, 无文件落盘 |
||||||
|
""" |
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import pandas as pd |
||||||
|
|
||||||
|
from utils._emchoice import fetch_report |
||||||
|
|
||||||
|
|
||||||
|
_REPORT_NAME = "RPT_DMSK_HOLDERS" |
||||||
|
# 列清单沿用 docs/api_docs.md 抓包值 |
||||||
|
_COLUMNS = ( |
||||||
|
"SECUCODE,SECURITY_CODE,ORG_CODE,SECURITY_TYPE_CODE,END_DATE,RANK," |
||||||
|
"HOLDER_CODE,HOLDER_NAME,HOLD_NUM,HOLD_RATIO,HOLD_NUM_CHANGE," |
||||||
|
"HOLD_RATIO_CHANGE,DIRECTION,SHARES_TYPE,HOLDER_NATURE,REPORT_TYPE," |
||||||
|
"HOLD_CHANGE,NOTICE_DATE,HOLD_RATIO_YOY,REPORT_DATE_NAME,REFERENCE_MARKET_CAP" |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def _build_params( |
||||||
|
secucode: str, |
||||||
|
start_date: str, |
||||||
|
end_date: str, |
||||||
|
) -> dict[str, str]: |
||||||
|
"""构造 RPT_DMSK_HOLDERS 查询参数.""" |
||||||
|
return { |
||||||
|
"reportName": _REPORT_NAME, |
||||||
|
"columns": _COLUMNS, |
||||||
|
"quoteColumns": "", |
||||||
|
"filter": f'(SECUCODE="{secucode}")(END_DATE>=\'{start_date}\')(END_DATE<=\'{end_date}\')', |
||||||
|
"pageNumber": "", |
||||||
|
"pageSize": "", |
||||||
|
"sortTypes": "-1,1", |
||||||
|
"sortColumns": "END_DATE,RANK", |
||||||
|
"client": "SW", |
||||||
|
"source": "CHOICE", |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def _fetch_one(secucode: str, years: int) -> pd.DataFrame: |
||||||
|
"""拉一只股票的十大股东明细.""" |
||||||
|
today = pd.Timestamp.today() |
||||||
|
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d") |
||||||
|
end = today.strftime("%Y-%m-%d") |
||||||
|
|
||||||
|
rows = fetch_report(_build_params(secucode, start, end)) |
||||||
|
df = pd.DataFrame(rows) |
||||||
|
if not df.empty: |
||||||
|
df["SECUCODE"] = secucode |
||||||
|
return df |
||||||
|
|
||||||
|
|
||||||
|
def get_holders(secucodes: list[str], years: int = 3) -> pd.DataFrame: |
||||||
|
"""抓取指定股票的十大股东明细并按股票合并。""" |
||||||
|
codes = [code.strip() for code in secucodes if code.strip()] |
||||||
|
if not codes: |
||||||
|
return pd.DataFrame() |
||||||
|
|
||||||
|
frames: list[pd.DataFrame] = [] |
||||||
|
for code in codes: |
||||||
|
df = _fetch_one(code, years) |
||||||
|
if not df.empty: |
||||||
|
frames.append(df) |
||||||
|
|
||||||
|
if not frames: |
||||||
|
return pd.DataFrame() |
||||||
|
return pd.concat(frames, ignore_index=True) |
||||||
Loading…
Reference in new issue