新增公司资料/指数/重大事项等 13 个接口, 文档全面对齐

- 新增 fin_mda/fin_equity/fin_company/fin_belong_index/fin_major_events 5 个模块
- 覆盖: 管理层讨论、股本结构、公司介绍、所属行业、指数分类/统计/成分、并购/股权激励/诉讼/关联交易/违规/担保
- _emchoice.fetch_report 支持"数据为空"按空结果处理
- api.py 增加 _simple_endpoint 统一生成纯 stocks 参数接口, 路由达 29 个 /fin 端点
- 冒烟测试扩展至 56 用例, 全部通过
- api_manual/blueprint/SKILLS/待优化 文档与代码路径 100% 对齐
This commit is contained in:
2026-08-15 11:34:11 +08:00
parent 797369bbed
commit 2ce68b7b5d
13 changed files with 1147 additions and 11 deletions
+46
View File
@@ -57,6 +57,19 @@
| 股东 | `GET /fin/free-holders` | 十大流通股东 |
| 股东 | `GET /fin/holders` | 十大股东明细 |
| 股东 | `GET /fin/holder-num` | 股东户数 |
| 资料 | `GET /fin/company-info` | 公司介绍(基本资料) |
| 资料 | `GET /fin/industry` | 所属行业 |
| 资料 | `GET /fin/equity` | 股本结构(全量历史) |
| 指数 | `GET /fin/index-classif` | 所属指数分类 |
| 指数 | `GET /fin/belong-index` | 纳入指数统计 |
| 指数 | `GET /fin/index-membership` | 指数成分权重 |
| 文本 | `GET /fin/mda` | 管理层讨论与分析(年报/中报索引) |
| 重大事项 | `GET /fin/acquisitions` | 并购事件 |
| 重大事项 | `GET /fin/equity-incentive` | 股权激励 |
| 重大事项 | `GET /fin/litigation` | 诉讼仲裁 |
| 重大事项 | `GET /fin/related-transaction` | 关联交易 |
| 重大事项 | `GET /fin/violation` | 违规处罚 |
| 重大事项 | `GET /fin/guarantee` | 对外担保 |
## 4. 接口详情
@@ -119,6 +132,34 @@ curl "http://127.0.0.1:8765/fin/holders?stocks=603233.SH&years=3" # 十
curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3" # 股东户数: HOLDER_TOTAL_NUM/户数增减
```
### 4.8 公司资料 / 股本 / 指数(无 years 参数,返回全量)
```bash
curl "http://127.0.0.1:8765/fin/company-info?stocks=603233.SH" # 公司介绍(每只1行): ORG_NAME/CHAIRMAN/REG_CAPITAL
curl "http://127.0.0.1:8765/fin/industry?stocks=603233.SH" # 所属行业: INDUSTRY_NAME
curl "http://127.0.0.1:8765/fin/equity?stocks=603233.SH" # 股本结构: TOTAL_SHARES/CHANGE_REASON
curl "http://127.0.0.1:8765/fin/index-classif?stocks=603233.SH" # 指数分类: INDEX_CLASSIF
curl "http://127.0.0.1:8765/fin/belong-index?stocks=603233.SH" # 纳入指数统计: INDEX_NUM
curl "http://127.0.0.1:8765/fin/index-membership?stocks=603233.SH" # 指数成分权重: INDEX_NAME_ABBR/WEIGHT
```
### 4.9 管理层讨论与分析
```bash
curl "http://127.0.0.1:8765/fin/mda?stocks=603233.SH&years=5" # 年报/中报索引: REPORT_NAME/TITLE/PAGE
```
### 4.10 重大事项(并购 / 股权激励 / 诉讼 / 关联交易 / 违规 / 担保)
```bash
curl "http://127.0.0.1:8765/fin/acquisitions?stocks=603233.SH" # 并购: NAME_OBJ/DEAL_AMT
curl "http://127.0.0.1:8765/fin/equity-incentive?stocks=603233.SH" # 股权激励(部分股票无记录→404)
curl "http://127.0.0.1:8765/fin/litigation?stocks=603233.SH" # 诉讼: CASE_NAME/CASE_AMOUNT
curl "http://127.0.0.1:8765/fin/related-transaction?stocks=603233.SH" # 关联交易: RELATED_PARTY/TRADE_AMT
curl "http://127.0.0.1:8765/fin/violation?stocks=603233.SH" # 违规: VIOLATE_TYPE/PUNISH_AMT
curl "http://127.0.0.1:8765/fin/guarantee?stocks=603233.SH" # 担保(部分股票无记录→404)
```
## 5. 使用注意事项
1. **只有 `/fin/summary` 需要 ORG_CODE**:格式 `代码:ORG_CODE`(如 `603233.SH:10500736`)。ORG_CODE 是东财内部机构编码,只传代码时服务会尝试自动查询但**不稳定**;如果 400 报错提示 ORG_CODE,请改用 `代码:ORG_CODE` 格式,或换用 `/fin/quarterly-summary`。
@@ -127,6 +168,8 @@ curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3" # 股
4. **列名不翻译**:`columns` 与上游东财接口一致(英文大写,如 `REPORT_DATE`、`TOTAL_ASSETS`),`data` 每行是键值对象,值可能为 `null`。
5. **报告期**:财务数据按报告期返回,`REPORT_DATE`/`END_DATE` 字段为 `YYYY-MM-DD` 格式(JSON 中是字符串)。
6. **频率**:接口为实时抓取,没有缓存;高频批量调用请控制节奏。
7. **无 years 的接口**:`company-info` / `industry` / `equity` / `index-classif` / `belong-index` / `index-membership` 只收 `stocks`,返回全量。
8. **部分股票可能查不到**:股权激励、担保等记录并非所有股票都有,此类接口对该股返回 `404`(`success:false`)是正常现象,不代表接口故障。
## 6. 常见组合场景
@@ -137,3 +180,6 @@ curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3" # 股
| 成长性判断 | `growth` + `quarterly-summary` |
| 分红投资 | `dividend-detail` + `dividend-statics` |
| 股东结构/筹码分析 | `holders` + `free-holders` + `holder-num` |
| 公司底细 | `company-info` + `industry` + `equity` + `mda` |
| 风险排查 | `litigation` + `violation` + `guarantee` + `related-transaction` |
| 指数/权重 | `index-classif` + `belong-index` + `index-membership` |
+74
View File
@@ -29,6 +29,18 @@ from utils.fin_dividend import get_dividend_detail, get_dividend_statics
from utils.fin_free_holders import get_free_holders
from utils.fin_holders import get_holders
from utils.fin_holder_num import get_holder_num
from utils.fin_mda import get_mda
from utils.fin_equity import get_equity
from utils.fin_company import get_company_info, get_industry
from utils.fin_belong_index import get_index_classif, get_belong_index, get_index_membership
from utils.fin_major_events import (
get_acquisitions,
get_equity_incentive,
get_litigation,
get_related_transaction,
get_violation,
get_guarantee,
)
app = FastAPI(
@@ -137,6 +149,19 @@ def root():
"GET /fin/free-holders": "拉取十大流通股东",
"GET /fin/holders": "拉取十大股东明细",
"GET /fin/holder-num": "拉取股东户数",
"GET /fin/mda": "拉取管理层讨论与分析(年报/中报)",
"GET /fin/equity": "拉取股本结构",
"GET /fin/company-info": "拉取公司介绍",
"GET /fin/industry": "拉取所属行业",
"GET /fin/index-classif": "拉取所属指数分类",
"GET /fin/belong-index": "拉取纳入指数统计",
"GET /fin/index-membership": "拉取指数成分权重",
"GET /fin/acquisitions": "拉取并购事件",
"GET /fin/equity-incentive": "拉取股权激励",
"GET /fin/litigation": "拉取诉讼仲裁",
"GET /fin/related-transaction": "拉取关联交易",
"GET /fin/violation": "拉取违规处罚",
"GET /fin/guarantee": "拉取对外担保",
"GET /health": "健康检查",
},
"usage": {
@@ -145,6 +170,8 @@ def root():
"fin_main_business": "/fin/main-business?stocks=603233.SH&classify_type=产品&years=3",
"fin_balance": "/fin/balance?stocks=603233.SH&years=3",
"fin_dividend_detail": "/fin/dividend-detail?stocks=603233.SH&years=5",
"fin_mda": "/fin/mda?stocks=603233.SH&years=5",
"fin_company_info": "/fin/company-info?stocks=603233.SH",
},
}
@@ -564,6 +591,53 @@ def fin_holder_num(
return payload
def _split_codes(stocks: str) -> list[str]:
"""解析逗号分隔的股票代码参数."""
codes = [code.strip() for code in stocks.split(",") if code.strip()]
if not codes:
raise HTTPException(400, detail="stocks 参数不能为空")
return codes
def _simple_endpoint(path: str, fetch, label: str, default_years: int, has_years: bool = True):
"""生成统一的 GET 端点: 只有 stocks (+years) 参数的接口."""
def endpoint(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: Optional[int] = Query(default_years if has_years else None, ge=1, le=20, description=f"拉取最近多少年的{label}"),
):
codes = _split_codes(stocks)
df = fetch(codes) if years is None else fetch(codes, years=years)
if df.empty:
extra = {"years": years} if years is not None else {}
return _empty_response(f"无{label}数据, 请检查股票代码或筛选条件", codes, **extra)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
if years is not None:
payload["years"] = years
return payload
app.get(path)(endpoint)
return endpoint
# ---- 管理层讨论与分析 / 股本结构 (无 years) ----
_simple_endpoint("/fin/mda", get_mda, "管理层讨论与分析", default_years=5)
_simple_endpoint("/fin/equity", get_equity, "股本结构", default_years=3, has_years=False)
_simple_endpoint("/fin/company-info", get_company_info, "公司介绍", default_years=3, has_years=False)
_simple_endpoint("/fin/industry", get_industry, "所属行业", default_years=3, has_years=False)
_simple_endpoint("/fin/index-classif", get_index_classif, "所属指数分类", default_years=3, has_years=False)
_simple_endpoint("/fin/belong-index", get_belong_index, "纳入指数统计", default_years=3, has_years=False)
_simple_endpoint("/fin/index-membership", get_index_membership, "指数成分权重", default_years=3, has_years=False)
# ---- 重大事项六类 ----
_simple_endpoint("/fin/acquisitions", get_acquisitions, "并购事件", default_years=3)
_simple_endpoint("/fin/equity-incentive", get_equity_incentive, "股权激励", default_years=3)
_simple_endpoint("/fin/litigation", get_litigation, "诉讼仲裁", default_years=3)
_simple_endpoint("/fin/related-transaction", get_related_transaction, "关联交易", default_years=3)
_simple_endpoint("/fin/violation", get_violation, "违规处罚", default_years=3)
_simple_endpoint("/fin/guarantee", get_guarantee, "对外担保", default_years=3)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8765, log_level="info")
+246
View File
@@ -335,3 +335,249 @@ Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
## 管理层讨论与分析
---
GET /api/data/v1/get?source=CHOICE&reportName=RPT_BUSINESSANALYSIS&columns=ORG_CODE%2CSECUCODE%2CSECURITY_CODE%2CSECURITY_INNER_CODE%2CSECURITY_NAME_ABBR%2CREPORT_DATE%2CREPORT_NAME%2CNOTICE_DATE%2CSOURCE%2CRELINFOCODE%2CTITLE%2CPAGE&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(REPORT_NAME+in+(%22%E5%B9%B4%E6%8A%A5%22%2C%22%E4%B8%AD%E6%8A%A5%22))(REPORT_DATE%3C%3D%272026-08-15%27)(REPORT_DATE%3E%3D%272021-08-15%27)&pageNumber=1&pageSize=50&sortTypes=-1&sortColumns=REPORT_DATE&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
## 股本结构
---
GET /api/data/v1/get?source=CHOICE&reportName=RPT_F10_EH_EQUITY&columns=CHOICE_F9_EH_EQUITY&quoteColumns=&pageNumber=1&pageSize=&sortColumns=END_DATE&client=SW&filter=(SECUCODE=%22603233.SH%22)(LISTING_DATE%3E=%271900-01-01%27)(LISTING_DATE%3C=%272026-12-31%27)&sortTypes=-1 HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
version:
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
token:
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
## 按地区分类
---
GET /stock/f9/ZYGC_AFLZS?SecurityCode=603233.SH&StartDate=2023-08-15&EndDate=2026-08-15&ReportPeriodType=%221%22%2C%225%22%2C%223%22%2C%226%22&Magnitude=4&ClassifyType=%E5%9C%B0%E5%8C%BA&ReportShowForm=0&ExchangeRate=1&Digits=2&Sort=-1&IsShowEmptyRow=true HTTP/2
host: emchoicews2.eastmoney.com
sec-ch-ua: "Chromium";v="104"
accept: application/json
sec-ch-ua-mobile: ?0
user-agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
origin: https://emchoicew.eastmoney.com
accept-language: zh-CN,zh;q=0.9
sec-fetch-site: same-site
sec-fetch-mode: cors
sec-fetch-dest: empty
referer: https://emchoicew.eastmoney.com/
accept-encoding: gzip, deflate, br
## 公司介绍 + 所属行业与指数
---
GET /api/data/v1/get?source=CHOICE&reportName=RPT_HSF9_BASIC_ORGINFO&columns=HSF9_ORGINFO&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)&pageNumber=1&pageSize=200&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---
GET /api/data/v1/get?reportName=RPT_STOCKF9_INDUSTRY&columns=SECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CINDUSTRY_TYPE_CODE%2CINDUSTRY_TYPE%2CINDUSTRY_NAME%2CINDUSTRY_CODE%2CSECURITY_INNER_CODE%2CINDUSTRY_NAME_MIN%2CINDUSTRY_CODE_MIN%2CENTRY_DATE%2COUT_DATE%2CSECURITY_TYPE_CODE%2CSTATE%2CSTATE_CODE%2CRN&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(STATE_CODE%3D%221%22)&pageNumber=&pageSize=&sortTypes=1&sortColumns=RN&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---
GET /api/data/v1/get?reportName=RPT_HSF9_BELONGINDEX_CLASSIF&columns=INDEX_CLASSIF_CODE%2CINDEX_CLASSIF&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(STR_TYPE%3D%221%22)&distinct=INDEX_CLASSIF_CODE%2CINDEX_CLASSIF&pageNumber=&pageSize=&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---
GET /api/data/v1/get?reportName=RPT_CUSTOM_HSF9_BELONG_INDEX&filter=(SECUCODE%3D%22603233.SH%22)&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---
GET /api/data/v1/get?reportName=RPT_HSF9_BELONG_INDEX&columns=INDEX_CODE%2CINDEX_INNER_CODE%2CTRADE_DATE%2CSECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CINCLUDE_DATE%2CMAKER_NAME%2CINDEX_NAME_ABBR%2CSECURITY_INNER_CODE%2CINDEX_CLASSIF_CODE%2CINDEX_CLASSIF%2CWEIGHT%2CNUM%2CTOTAL_SCALE%2CWEIGHT_DATE%2CCURRENCY&filter=(SECUCODE%3D%22603233.SH%22)((INDEX_CLASSIF_CODE+like+%22%25001001%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001002%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001003%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001004%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001005%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001007%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001009%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001010%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001011%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001017%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001020%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001022%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001025%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001030%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001006%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25001024%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25013003%25%22)(%7CINDEX_CLASSIF_CODE+like+%22%25014%25%22))&sortColumns=NUM%2CINDEX_CODE&sortTypes=-1%2C1&pageNumber=1&pageSize=50&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
## 重大事项(重组/激励/回购/担保/违规/诉讼)
---并购事件
GET /api/data/v1/get?reportName=RPT_HS_ACQUISITIONS_EVENT&columns=SECUCODE%2CSECURITY_CODE%2CFIRST_NOTICE_DATE%2CNOTICE_DATE%2CPLAN_PROCESS%2CNAME_OBJ%2CNAME_BUY%2CMXID%2CNAME_SELL%2CDEAL_AMT%2CTRANSFER_RATIO%2CTRADTE_CURRENCY&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(FIRST_NOTICE_DATE%3E%3D%272023-08-15%27)(FIRST_NOTICE_DATE%3C%3D%272026-08-15%27)&sortTypes=-1%2C1&sortColumns=FIRST_NOTICE_DATE%2CMXID&requestParams=1&pageNumber=1&pageSize=50&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---股权激励
GET /api/data/v1/get?reportName=RPT_DMSK_SH_EQUITYINCENTIVE&columns=SECUCODE%2CORG_CODE%2CPLAN_PROCESS%2CEI_TARGET%2CEI_WAY%2CTOTAL_INCENTIVE_SHARES%2CTOTAL_INCENTIVE_TSR%2CINITIAL_EXERCISE_PRICE%2CVALID_EXPIRE%2CPLAN_NOTICE_DATE%2CGMDECISION_NOTICE_DATE%2CFIRST_NOTICE_DATE%2CPLAN_EXPLAIN%2CPRICE_EXPLAIN%2CINCENTIVE_SELL_EXPLAIN%2CINCENTIVE_GRANT_CONDITION%2CEXERCISE_SPECIAL_CONDITION%2CNOTICE_DATE%2CSTR_YEAR%2CSECURITY_NAME_ABBR%2CSECURITY_CODE%2CINDUSTRYCSRC%2CEM_INDUSTRY%2CPLAN_NUM&filter=(SECUCODE%3D%22603233.SH%22)(PLAN_NOTICE_DATE%3E%3D%272023-08-15%27)(PLAN_NOTICE_DATE%3C%3D%272026-08-15%27)&sortTypes=-1&sortColumns=PLAN_NOTICE_DATE&source=CHOICE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---诉讼仲裁
GET /api/data/v1/get?source=CHOICE&reportName=RPT_LITIGATION_ARBITRATION_BSINFO&columns=ORG_CODE%2CSECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CNOTICE_DATE%2CCASE_NAME%2CCASE_PROFILE%2CPLAINTIFF%2CDEFENCE%2CLITIGATION_TYPE%2CFI_PROSECUTE_DATE%2CFI_ACCEPTANCE_COURT%2CCASE_AMOUNT%2CFI_JUDGMENT_DATE%2CFI_JUDGMENT_CONTENT%2CFI_IS_APPEAL%2CSI_APPELLEE%2CSI_ACCEPTANCE_COURT%2CSI_JUDGMENT_DATE%2CSI_JUDGMENT_CONTENT%2CEXECUTE_SITUATION%2CCURRENCY%2CNOTICE_YEAR&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(NOTICE_YEAR%3E%3D%221900%22)(NOTICE_YEAR%3C%3D%222026%22)&pageNumber=&pageSize=&sortTypes=-1&sortColumns=NOTICE_DATE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---关联交易
GET /api/data/v1/get?reportName=RPT_RELATED_TRANSACTION&columns=SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,ORG_CODE,NOTICE_DATE,RELATED_PARTY,RELATED_RELATION,IS_CONTROL,TRADE_AMT,PAY_WAY,TRADE_PROFILE,PRICE_BASIS,TRADE_START_DATE,TRADE_END_DATE,CURRENCY,STR_YEAR&quoteColumns=&filter=(SECUCODE=%22603233.SH%22);(NOTICE_DATE%3E=%271900-01-01%27)(NOTICE_DATE%3C=%272026-12-31%27)&pageNumber=&pageSize=&sortTypes=-1&sortColumns=NOTICE_DATE&client=SW&source=CHOICE HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---违规
GET /api/data/v1/get?source=CHOICE&reportName=RPT_HSF9_OP_VIOLATION&columns=ORG_CODE%2CNOTICE_DATE%2CNOTICE_YEAR%2CCURRENCY%2CSECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CSOLVE_ORG%2CVIOLATE_TYPE%2CPUNISH_TYPE%2CPUNISH_AMT%2CVIOLATE_BEHAVIOUR%2CPUNISH_STEP%2CPUNISH_OBJECT&quoteColumns=&filter=(secucode%3D%22603233.SH%22)(NOTICE_DATE%3E%3D%271900-01-01%27)(NOTICE_DATE%3C%3D%272026-12-31%27)&pageNumber=1&pageSize=&sortTypes=-1&sortColumns=NOTICE_DATE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
---担保
GET /api/data/v1/get?source=CHOICE&reportName=RPT_F9_GUARANTEE&columns=SECURITY_CODE%2CSECUCODE%2CSECURITY_NAME_ABBR%2CREPORT_DATE%2CGUAR_NAME%2CGUARANTEED_NAME%2CGUARANTEE_START_DATE%2CGUARANTEE_END_DATE%2CIS_PERFORM%2CTRADE_DATE%2CGUARANTEE_WAY%2CGUARANTEE_AMT%2CGUARANTEE_EXPIRE%2CIS_RELATED_TRADE%2CCURRENCY%2CREPORT_YEAR%2CHISTORYCHG%2CORG_CODE&quoteColumns=&filter=(SECUCODE%3D%22603233.SH%22)(REPORT_YEAR%3E%3D%221900%22)(REPORT_YEAR%3C%3D%222026%22)&sortTypes=-1&sortColumns=REPORT_DATE&client=SW HTTP/1.1
Host: datacenter-choice.eastmoney.com
Connection: keep-alive
sec-ch-ua: "Chromium";v="104"
Accept: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: https://emchoicew.eastmoney.com
Accept-Language: zh-CN,zh;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://emchoicew.eastmoney.com/
Accept-Encoding: gzip, deflate, br
+118 -1
View File
@@ -285,6 +285,110 @@ curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3"
---
## 9. 公司资料 / 股本 / 指数
以下接口均只需 `stocks` 参数(无 `years`,返回全量),公司介绍每只 1 行。
**示例**
```bash
# 公司介绍 (基本资料: 注册资本/董事长/行业等)
curl "http://127.0.0.1:8765/fin/company-info?stocks=603233.SH"
# 股本结构 (各报告期末股本变动全量历史)
curl "http://127.0.0.1:8765/fin/equity?stocks=603233.SH"
# 所属行业 (当前生效的行业分类)
curl "http://127.0.0.1:8765/fin/industry?stocks=603233.SH"
# 所属指数分类
curl "http://127.0.0.1:8765/fin/index-classif?stocks=603233.SH"
# 纳入指数统计 (指数/基金数量)
curl "http://127.0.0.1:8765/fin/belong-index?stocks=603233.SH"
# 指数成分权重 (纳入主要指数的权重)
curl "http://127.0.0.1:8765/fin/index-membership?stocks=603233.SH"
```
**各接口常见列**
| 接口 | 常见列 |
|---|---|
| 公司介绍 | `ORG_NAME`(公司全称)、`FOUND_DATE`、`REG_CAPITAL`、`LISTING_DATE`、`CHAIRMAN`、`PRESIDENT`、`INDUSTRYCSRC1` 等 |
| 股本结构 | `END_DATE`、`CHANGE_REASON`(变动原因)、`LIMITED_SHARES`(限售股)、`UNLIMITED_SHARES`、`TOTAL_SHARES` 等 |
| 所属行业 | `INDUSTRY_TYPE`(行业类型)、`INDUSTRY_NAME`、`INDUSTRY_CODE`、`ENTRY_DATE` 等 |
| 指数分类 | `INDEX_CLASSIF_CODE`、`INDEX_CLASSIF` |
| 纳入指数统计 | `INDEX_NUM`(指数数量)、`INDEX_FUND_NUM`、`FUND_NUM`、`FUND_TOTAL_SCALE` 等 |
| 指数成分权重 | `INDEX_NAME_ABBR`(指数名)、`INDEX_CLASSIF`、`WEIGHT`(权重)、`NUM`、`INCLUDE_DATE` 等 |
---
## 10. 管理层讨论与分析(MD&A)
年报/中报的董事会报告索引(含标题与页码),默认拉最近 5 年。
**参数**
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `stocks` | string | ✅ | — | 逗号分隔股票代码,如 `603233.SH` |
| `years` | int | | `5` | 拉取最近多少年的报告(1~20) |
**示例**
```bash
curl "http://127.0.0.1:8765/fin/mda?stocks=603233.SH&years=5"
```
**常见列**:`REPORT_DATE`、`REPORT_NAME`(年报/中报)、`NOTICE_DATE`、`TITLE`(标题)、`PAGE`(页码)、`RELINFOCODE`(报告关联编码)。
---
## 11. 重大事项(并购 / 股权激励 / 诉讼 / 关联交易 / 违规 / 担保)
**公共参数**
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `stocks` | string | ✅ | — | 逗号分隔股票代码,如 `603233.SH`。仅需代码,不需要 ORG_CODE |
| `years` | int | | `3` | 拉取最近多少年的数据(1~20) |
**示例**
```bash
# 并购事件
curl "http://127.0.0.1:8765/fin/acquisitions?stocks=603233.SH&years=3"
# 股权激励 (部分股票可能无数据, 如无记录返回 404)
curl "http://127.0.0.1:8765/fin/equity-incentive?stocks=603233.SH&years=3"
# 诉讼仲裁
curl "http://127.0.0.1:8765/fin/litigation?stocks=603233.SH&years=3"
# 关联交易
curl "http://127.0.0.1:8765/fin/related-transaction?stocks=603233.SH&years=3"
# 违规处罚
curl "http://127.0.0.1:8765/fin/violation?stocks=603233.SH&years=3"
# 对外担保 (部分股票可能无数据)
curl "http://127.0.0.1:8765/fin/guarantee?stocks=603233.SH&years=3"
```
**各接口常见列**
| 接口 | 常见列 |
|---|---|
| 并购 | `PLAN_PROCESS`(进展)、`NAME_OBJ`(标的)、`NAME_BUY`(收购方)、`DEAL_AMT`(交易金额)、`TRANSFER_RATIO` 等 |
| 股权激励 | `PLAN_PROCESS`、`EI_TARGET`(对象)、`EI_WAY`(方式)、`TOTAL_INCENTIVE_SHARES`、`INITIAL_EXERCISE_PRICE`(行权价)等 |
| 诉讼仲裁 | `CASE_NAME`(案件名)、`CASE_PROFILE`、`PLAINTIFF`、`DEFENCE`、`CASE_AMOUNT`(涉诉金额)、`LITIGATION_TYPE` 等 |
| 关联交易 | `RELATED_PARTY`(关联方)、`RELATED_RELATION`(关系)、`TRADE_AMT`(金额)、`TRADE_PROFILE` 等 |
| 违规 | `VIOLATE_TYPE`(违规类型)、`PUNISH_TYPE`(处罚类型)、`PUNISH_AMT`、`SOLVE_ORG`(处理机构)等 |
| 担保 | `GUAR_NAME`(担保方)、`GUARANTEED_NAME`(被担保方)、`GUARANTEE_AMT`、`GUARANTEE_WAY`、`IS_RELATED_TRADE` 等 |
---
## 接口一览
| 路径 | 功能 | 数据来源报表 |
@@ -306,9 +410,22 @@ curl "http://127.0.0.1:8765/fin/holder-num?stocks=603233.SH&years=3"
| `/fin/free-holders` | 十大流通股东 | `RPT_F10_EH_FREEHOLDERS` |
| `/fin/holders` | 十大股东明细 | `RPT_DMSK_HOLDERS` |
| `/fin/holder-num` | 股东户数 | `RPT_F10_EH_HOLDERNUM` |
| `/fin/mda` | 管理层讨论与分析 | `RPT_BUSINESSANALYSIS` |
| `/fin/equity` | 股本结构 | `RPT_F10_EH_EQUITY` |
| `/fin/company-info` | 公司介绍 | `RPT_HSF9_BASIC_ORGINFO` |
| `/fin/industry` | 所属行业 | `RPT_STOCKF9_INDUSTRY` |
| `/fin/index-classif` | 所属指数分类 | `RPT_HSF9_BELONGINDEX_CLASSIF` |
| `/fin/belong-index` | 纳入指数统计 | `RPT_CUSTOM_HSF9_BELONG_INDEX` |
| `/fin/index-membership` | 指数成分权重 | `RPT_HSF9_BELONG_INDEX` |
| `/fin/acquisitions` | 并购事件 | `RPT_HS_ACQUISITIONS_EVENT` |
| `/fin/equity-incentive` | 股权激励 | `RPT_DMSK_SH_EQUITYINCENTIVE` |
| `/fin/litigation` | 诉讼仲裁 | `RPT_LITIGATION_ARBITRATION_BSINFO` |
| `/fin/related-transaction` | 关联交易 | `RPT_RELATED_TRANSACTION` |
| `/fin/violation` | 违规处罚 | `RPT_HSF9_OP_VIOLATION` |
| `/fin/guarantee` | 对外担保 | `RPT_F9_GUARANTEE` |
## 测试
```bash
uv run pytest tests/ -q # 冒烟测试: 连真实接口验证各模块能取到数据 (30 用例)
uv run pytest tests/ -q # 冒烟测试: 连真实接口验证各模块能取到数据 (56 用例)
```
+37 -6
View File
@@ -39,6 +39,11 @@ EMWebApi/
│ ├── fin_free_holders.py # 十大流通股东
│ ├── fin_holders.py # 十大股东明细
│ ├── fin_holder_num.py # 股东户数
│ ├── fin_mda.py # 管理层讨论与分析(MD&A)
│ ├── fin_equity.py # 股本结构
│ ├── fin_company.py # 公司介绍 + 所属行业
│ ├── fin_belong_index.py # 所属指数分类/统计/成分权重
│ └── fin_major_events.py # 重大事项(并购/激励/诉讼/关联/违规/担保)
├── tests/
│ └── test_interfaces.py # 冒烟测试 (连真实接口)
└── docs/
@@ -49,7 +54,8 @@ EMWebApi/
**依赖方向**:
- `fin_quarterly.py` / `fin_main_business.py` 复用 `fin_summary.py` 里的 `HEADERS` / `TIMEOUT` / `BASE_URL`(顺带触发其模块级的 SSL 自动修复)
- 其余 9 个新模块复用 `_emchoice.py` 的 `get_data_url` / `fetch_report`(同样会触发 SSL 修复)
- 其余新模块复用 `_emchoice.py` 的 `get_data_url` / `fetch_report`(同样会触发 SSL 修复)
- `fin_major_events.py` / `fin_company.py` / `fin_belong_index.py` 内部有 `_run` / `_fetch_one` 复用,避免六接口重复样板
---
@@ -71,6 +77,19 @@ EMWebApi/
| 12 | 十大流通股东 | `RPT_F10_EH_FREEHOLDERS` | ✅ 已实现 |
| 13 | 十大股东明细 | `RPT_DMSK_HOLDERS` | ✅ 已实现 |
| 14 | 股东户数 | `RPT_F10_EH_HOLDERNUM` | ✅ 已实现 |
| 15 | 管理层讨论与分析 | `RPT_BUSINESSANALYSIS` | ✅ 已实现 |
| 16 | 股本结构 | `RPT_F10_EH_EQUITY` | ✅ 已实现 |
| 17 | 公司介绍 | `RPT_HSF9_BASIC_ORGINFO` | ✅ 已实现 |
| 18 | 所属行业 | `RPT_STOCKF9_INDUSTRY` | ✅ 已实现 |
| 19 | 指数分类 | `RPT_HSF9_BELONGINDEX_CLASSIF` | ✅ 已实现 |
| 20 | 纳入指数统计 | `RPT_CUSTOM_HSF9_BELONG_INDEX` | ✅ 已实现 |
| 21 | 指数成分权重 | `RPT_HSF9_BELONG_INDEX` | ✅ 已实现 |
| 22 | 并购事件 | `RPT_HS_ACQUISITIONS_EVENT` | ✅ 已实现 |
| 23 | 股权激励 | `RPT_DMSK_SH_EQUITYINCENTIVE` | ✅ 已实现 |
| 24 | 诉讼仲裁 | `RPT_LITIGATION_ARBITRATION_BSINFO` | ✅ 已实现 |
| 25 | 关联交易 | `RPT_RELATED_TRANSACTION` | ✅ 已实现 |
| 26 | 违规处罚 | `RPT_HSF9_OP_VIOLATION` | ✅ 已实现 |
| 27 | 对外担保 | `RPT_F9_GUARANTEE` | ✅ 已实现 |
**文档外补充**:财务摘要(报告期) 用 `RPT_CUSTOM_ORGF9_FIN_SUMMARY`,需 ORG_CODE(不在 api_docs.md 里)。
@@ -78,10 +97,11 @@ EMWebApi/
## 4. 实施进度
### ✅ 已完成(11 类接口全部落地)
### ✅ 已完成(api_docs.md 全部 27 项落地)
| 功能 | 核心文件 | HTTP 接口 |
|------|----------|-----------|
| 健康检查 | `api.py` | `GET /health` |
| 财务摘要(报告期) | `utils/fin_summary.py` | `GET /fin/summary` |
| 财务摘要(单季度) | `utils/fin_quarterly.py` | `GET /fin/quarterly-summary` |
| 主营构成(产品/地区) | `utils/fin_main_business.py` | `GET /fin/main-business` |
@@ -93,11 +113,22 @@ EMWebApi/
| 营运能力 | `utils/fin_operate.py` | `GET /fin/operate-ability` |
| 成长能力 | `utils/fin_growth.py` | `GET /fin/growth` |
| 杜邦分析 | `utils/fin_dupont.py` | `GET /fin/dupont` |
| 分红明细 + 统计 | `utils/fin_dividend.py` | `GET /fin/dividend-detail` / `dividend-statics` |
| 分红明细 + 统计 | `utils/fin_dividend.py` | `GET /fin/dividend-detail` / `GET /fin/dividend-statics` |
| 十大流通股东 | `utils/fin_free_holders.py` | `GET /fin/free-holders` |
| 十大股东明细 | `utils/fin_holders.py` | `GET /fin/holders` |
| 股东户数 | `utils/fin_holder_num.py` | `GET /fin/holder-num` |
| 测试 | `tests/test_interfaces.py` | `uv run pytest tests/ -q` (30 用例) |
| 管理层讨论与分析 | `utils/fin_mda.py` | `GET /fin/mda` |
| 股本结构 | `utils/fin_equity.py` | `GET /fin/equity` |
| 公司介绍 | `utils/fin_company.py` | `GET /fin/company-info` |
| 所属行业 | `utils/fin_company.py` | `GET /fin/industry` |
| 指数分类/统计/成分 | `utils/fin_belong_index.py` | `GET /fin/index-classif` / `GET /fin/belong-index` / `GET /fin/index-membership` |
| 并购事件 | `utils/fin_major_events.py` | `GET /fin/acquisitions` |
| 股权激励 | `utils/fin_major_events.py` | `GET /fin/equity-incentive` |
| 诉讼仲裁 | `utils/fin_major_events.py` | `GET /fin/litigation` |
| 关联交易 | `utils/fin_major_events.py` | `GET /fin/related-transaction` |
| 违规处罚 | `utils/fin_major_events.py` | `GET /fin/violation` |
| 对外担保 | `utils/fin_major_events.py` | `GET /fin/guarantee` |
| 测试 | `tests/test_interfaces.py` | `uv run pytest tests/ -q` (56 用例) |
### 🧹 待清理
@@ -112,8 +143,8 @@ EMWebApi/
2. **结构**:`_build_*(参数构造)` → `_fetch_one(单只拉取)` → `get_<主题>(批量主入口)`
3. **共享**:`from utils.fin_summary import HEADERS, TIMEOUT`,不重复定义;不同 base_url 的接口在本文件内定义新常量;emws 跳转接口用 `_emchoice.get_data_url`,datacenter 报表用 `_emchoice.fetch_report`
4. **数据流**:内部统一 `pandas.DataFrame`,每行冗余 `SECUCODE` 列便于多股票合并
5. **校验**:`payload.get("success")` 失败抛 `RuntimeError`;空数据返回空 DataFrame 而非报错
6. **接口暴露**:api.py 中新增 `GET /fin/<主题>`,参数用 FastAPI `Query`,返回 `{success, count, columns, data, errors}` 风格,NaN/NaT/inf 转 None(复用 `_df_to_json_payload`),无数据统一走 `_empty_response`
5. **校验**:`fetch_report` 中 `success=False` 且 message 为"返回数据为空/暂无数据/无数据"时按空结果处理(返回空列表),其余失败抛 `RuntimeError`;空数据返回空 DataFrame 而非报错
6. **接口暴露**:api.py 中新增 `GET /fin/<主题>`,参数用 FastAPI `Query`,返回 `{success, count, columns, data, errors}` 风格,NaN/NaT/inf 转 None(复用 `_df_to_json_payload`),无数据统一走 `_empty_response`;仅 `stocks`(+`years`) 参数的接口可用 `_simple_endpoint` 统一生成
7. **礼貌抓取**:批量循环中加随机延时(参考 `SLEEP_RANGE`)
8. **注释语言**:中文,解释"为什么"(如接口编码映射差异)
9. **测试**:新模块在 `tests/test_interfaces.py` 注册冒烟用例(连真实接口取 1 年数据,断言非空 + 关键列)
+1 -1
View File
@@ -20,7 +20,7 @@
|---|--------|---------|------|-----------------|
| 5 | `/fin/cashflow-quarterly` | 单季度现金流量表 | 季度经营性现金流 | 在现有 `fin/cashflow` 上加 `reportType` 参数 |
| 6 | `/fin/operating-data` | 经营数据(药房专属) | 门店数、同店增长、新开/关闭 | `RPT_F10_OPDATA`(按行业走分表) |
| 7 | `/fin/notice` | 重大事项 | 重组/激励/回购/担保 | `RPT_F10_NOTICE` 或事件流 |
| 7 | ~~`/fin/notice`~~ 重大事项 | ~~重组/激励/回购/担保~~ | ✅ 已实现 | 已拆为 6 个端点:`/fin/acquisitions`(并购)、`/fin/equity-incentive`(股权激励)、`/fin/litigation`(诉讼)、`/fin/related-transaction`(关联交易)、`/fin/violation`(违规)、`/fin/guarantee`(担保),模块 `utils/fin_major_events.py` |
### 🟢 低价值(看心情)
+65 -3
View File
@@ -3,25 +3,58 @@
运行: uv run pytest tests/ -q
注意: 依赖外网可达东财接口, 网络不通时用例会失败.
"""
import inspect
import pandas as pd
import pytest
from utils.fin_balance import get_balance
from utils.fin_belong_index import get_belong_index, get_index_classif, get_index_membership
from utils.fin_capital_structure import get_capital_structure
from utils.fin_cashflow import get_cashflow
from utils.fin_company import get_company_info, get_industry
from utils.fin_dividend import get_dividend_detail, get_dividend_statics
from utils.fin_dupont import get_dupont
from utils.fin_equity import get_equity
from utils.fin_free_holders import get_free_holders
from utils.fin_growth import get_growth
from utils.fin_holder_num import get_holder_num
from utils.fin_holders import get_holders
from utils.fin_income import get_income
from utils.fin_main_business import get_main_business
from utils.fin_major_events import (
get_acquisitions,
get_equity_incentive,
get_guarantee,
get_litigation,
get_related_transaction,
get_violation,
)
from utils.fin_mda import get_mda
from utils.fin_operate import get_operate_ability
from utils.fin_profitability import get_profitability
STOCK = "603233.SH"
# 部分接口需要更多年份或其它股票才能取到数据 (否则按 1 年 + 默认股票)
CASE_OVERRIDES = {
"get_mda": {"years": 5},
"get_litigation": {"years": 3},
"get_violation": {"years": 3},
"get_equity_incentive": {"years": 5, "stock": "300750.SZ"},
"get_guarantee": {"years": 3, "stock": "300750.SZ"},
}
def _call(func, stock=STOCK):
"""按函数签名与覆盖配置调用."""
over = CASE_OVERRIDES.get(func.__name__, {})
s = over.get("stock", stock)
if "years" in inspect.signature(func).parameters:
return func([s], years=over.get("years", 1))
return func([s])
# (模块函数, 该接口应返回的关键列, 测试名)
CASES = [
pytest.param(get_main_business, "MAIN_BUSINESS_INCOME", id="main_business"),
@@ -38,13 +71,26 @@ CASES = [
pytest.param(get_free_holders, "HOLDER_NAME", id="free_holders"),
pytest.param(get_holders, "HOLDER_NAME", id="holders"),
pytest.param(get_holder_num, "HOLDER_TOTAL_NUM", id="holder_num"),
pytest.param(get_mda, "REPORT_NAME", id="mda"),
pytest.param(get_equity, "TOTAL_SHARES", id="equity"),
pytest.param(get_company_info, "ORG_NAME", id="company_info"),
pytest.param(get_industry, "INDUSTRY_NAME", id="industry"),
pytest.param(get_index_classif, "INDEX_CLASSIF", id="index_classif"),
pytest.param(get_belong_index, "INDEX_NUM", id="belong_index"),
pytest.param(get_index_membership, "INDEX_NAME_ABBR", id="index_membership"),
pytest.param(get_acquisitions, "PLAN_PROCESS", id="acquisitions"),
pytest.param(get_equity_incentive, "PLAN_PROCESS", id="equity_incentive"),
pytest.param(get_litigation, "CASE_NAME", id="litigation"),
pytest.param(get_related_transaction, "RELATED_PARTY", id="related_transaction"),
pytest.param(get_violation, "VIOLATE_TYPE", id="violation"),
pytest.param(get_guarantee, "GUAR_NAME", id="guarantee"),
]
@pytest.mark.parametrize("func,key_col", CASES)
def test_fetch_data(func, key_col):
"""拉 1 年数据, 应返回非空 DataFrame 且含关键列."""
df = func([STOCK], years=1)
"""拉数据, 应返回非空 DataFrame 且含关键列."""
df = _call(func)
assert isinstance(df, pd.DataFrame)
assert not df.empty, f"{func.__name__} 未返回数据"
assert "SECUCODE" in df.columns, f"{func.__name__} 缺少 SECUCODE 列"
@@ -54,7 +100,10 @@ def test_fetch_data(func, key_col):
@pytest.mark.parametrize("func,_key_col", CASES)
def test_empty_codes(func, _key_col):
"""空代码列表应返回空 DataFrame, 不抛异常."""
df = func([], years=1)
if "years" in inspect.signature(func).parameters:
df = func([], years=1)
else:
df = func([])
assert isinstance(df, pd.DataFrame)
assert df.empty
@@ -89,5 +138,18 @@ def test_api_routes():
"/fin/free-holders",
"/fin/holders",
"/fin/holder-num",
"/fin/mda",
"/fin/equity",
"/fin/company-info",
"/fin/industry",
"/fin/index-classif",
"/fin/belong-index",
"/fin/index-membership",
"/fin/acquisitions",
"/fin/equity-incentive",
"/fin/litigation",
"/fin/related-transaction",
"/fin/violation",
"/fin/guarantee",
]:
assert route in paths, f"缺少路由 {route}"
+4
View File
@@ -34,5 +34,9 @@ def fetch_report(url_or_params: str | dict[str, str]) -> list[dict]:
r.raise_for_status()
payload = r.json()
if not payload.get("success"):
# 上游明确提示"数据为空"时按空结果处理 (如某股无股权激励/担保记录), 其余视为失败
message = str(payload.get("message") or "")
if any(keyword in message for keyword in ("返回数据为空", "暂无数据", "无数据")):
return []
raise RuntimeError(f"接口返回失败: {payload}")
return payload.get("result", {}).get("data") or []
+120
View File
@@ -0,0 +1,120 @@
"""东方财富 CHOICE API 所属指数抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (指数分类 / 纳入指数统计 / 指数成分权重)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
def _fetch_one(secucode: str, params: dict[str, str]) -> pd.DataFrame:
"""按给定参数拉一只股票的数据."""
rows = fetch_report(params)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def _concat(frames: list[pd.DataFrame]) -> pd.DataFrame:
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
# ---------------------------------------------------------------- 指数分类
def _build_classif_params(secucode: str) -> dict[str, str]:
"""构造 RPT_HSF9_BELONGINDEX_CLASSIF 查询参数 (股票所属的指数分类)."""
return {
"reportName": "RPT_HSF9_BELONGINDEX_CLASSIF",
"columns": "INDEX_CLASSIF_CODE,INDEX_CLASSIF",
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(STR_TYPE="1")',
"distinct": "INDEX_CLASSIF_CODE,INDEX_CLASSIF",
"pageNumber": "",
"pageSize": "",
"source": "CHOICE",
"client": "SW",
}
def get_index_classif(secucodes: list[str]) -> pd.DataFrame:
"""抓取指定股票所属的指数分类列表."""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, _build_classif_params(code)) for code in codes]
return _concat([df for df in frames if not df.empty])
# ---------------------------------------------------------------- 纳入指数统计
def _build_belong_index_params(secucode: str) -> dict[str, str]:
"""构造 RPT_CUSTOM_HSF9_BELONG_INDEX 查询参数 (纳入指数数量统计)."""
return {
"reportName": "RPT_CUSTOM_HSF9_BELONG_INDEX",
"filter": f'(SECUCODE="{secucode}")',
"source": "CHOICE",
"client": "SW",
}
def get_belong_index(secucodes: list[str]) -> pd.DataFrame:
"""抓取指定股票纳入指数的统计 (指数/基金数量)."""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, _build_belong_index_params(code)) for code in codes]
return _concat([df for df in frames if not df.empty])
# ---------------------------------------------------------------- 指数成分权重
# 只保留主要指数分类 (剔除行业/风格类), 沿用 docs/api_docs.md 抓包值
_INDEX_CLASSIF_LIKE = (
'(INDEX_CLASSIF_CODE like "%001001%")(|INDEX_CLASSIF_CODE like "%001002%")'
'(|INDEX_CLASSIF_CODE like "%001003%")(|INDEX_CLASSIF_CODE like "%001004%")'
'(|INDEX_CLASSIF_CODE like "%001005%")(|INDEX_CLASSIF_CODE like "%001007%")'
'(|INDEX_CLASSIF_CODE like "%001009%")(|INDEX_CLASSIF_CODE like "%001010%")'
'(|INDEX_CLASSIF_CODE like "%001011%")(|INDEX_CLASSIF_CODE like "%001017%")'
'(|INDEX_CLASSIF_CODE like "%001020%")(|INDEX_CLASSIF_CODE like "%001022%")'
'(|INDEX_CLASSIF_CODE like "%001025%")(|INDEX_CLASSIF_CODE like "%001030%")'
'(|INDEX_CLASSIF_CODE like "%001006%")(|INDEX_CLASSIF_CODE like "%001024%")'
'(|INDEX_CLASSIF_CODE like "%013003%")(|INDEX_CLASSIF_CODE like "%014%")'
)
def _build_membership_params(secucode: str) -> dict[str, str]:
"""构造 RPT_HSF9_BELONG_INDEX 查询参数 (指数成分及权重)."""
return {
"reportName": "RPT_HSF9_BELONG_INDEX",
"columns": (
"INDEX_CODE,INDEX_INNER_CODE,TRADE_DATE,SECUCODE,SECURITY_CODE,"
"SECURITY_NAME_ABBR,INCLUDE_DATE,MAKER_NAME,INDEX_NAME_ABBR,"
"SECURITY_INNER_CODE,INDEX_CLASSIF_CODE,INDEX_CLASSIF,WEIGHT,NUM,"
"TOTAL_SCALE,WEIGHT_DATE,CURRENCY"
),
"filter": f'(SECUCODE="{secucode}")({_INDEX_CLASSIF_LIKE})',
"sortColumns": "NUM,INDEX_CODE",
"sortTypes": "-1,1",
"pageNumber": "1",
"pageSize": "50",
"source": "CHOICE",
"client": "SW",
}
def get_index_membership(secucodes: list[str]) -> pd.DataFrame:
"""抓取指定股票所属的指数成分及权重."""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, _build_membership_params(code)) for code in codes]
return _concat([df for df in frames if not df.empty])
+84
View File
@@ -0,0 +1,84 @@
"""东方财富 CHOICE API 公司介绍 + 所属行业抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (公司基本资料 / 所属行业)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
def _fetch_one(secucode: str, params: dict[str, str]) -> pd.DataFrame:
"""按给定参数拉一只股票的数据."""
rows = fetch_report(params)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def _concat(frames: list[pd.DataFrame]) -> pd.DataFrame:
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
# ---------------------------------------------------------------- 公司介绍
def _build_company_params(secucode: str) -> dict[str, str]:
"""构造 RPT_HSF9_BASIC_ORGINFO 查询参数 (公司基本资料)."""
return {
"source": "CHOICE",
"reportName": "RPT_HSF9_BASIC_ORGINFO",
"columns": "HSF9_ORGINFO",
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")',
"pageNumber": "1",
"pageSize": "200",
"client": "SW",
}
def get_company_info(secucodes: list[str]) -> pd.DataFrame:
"""抓取指定股票的公司介绍 (基本资料, 每只 1 行)."""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, _build_company_params(code)) for code in codes]
return _concat([df for df in frames if not df.empty])
# ---------------------------------------------------------------- 所属行业
def _build_industry_params(secucode: str) -> dict[str, str]:
"""构造 RPT_STOCKF9_INDUSTRY 查询参数 (当前生效的行业分类)."""
return {
"reportName": "RPT_STOCKF9_INDUSTRY",
"columns": (
"SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,INDUSTRY_TYPE_CODE,"
"INDUSTRY_TYPE,INDUSTRY_NAME,INDUSTRY_CODE,SECURITY_INNER_CODE,"
"INDUSTRY_NAME_MIN,INDUSTRY_CODE_MIN,ENTRY_DATE,OUT_DATE,"
"SECURITY_TYPE_CODE,STATE,STATE_CODE,RN"
),
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(STATE_CODE="1")',
"pageNumber": "",
"pageSize": "",
"sortTypes": "1",
"sortColumns": "RN",
"source": "CHOICE",
"client": "SW",
}
def get_industry(secucodes: list[str]) -> pd.DataFrame:
"""抓取指定股票的所属行业 (当前生效分类)."""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, _build_industry_params(code)) for code in codes]
return _concat([df for df in frames if not df.empty])
+57
View File
@@ -0,0 +1,57 @@
"""东方财富 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_EQUITY"
_COLUMNS = "CHOICE_F9_EH_EQUITY"
def _build_params(secucode: str, end_date: str) -> dict[str, str]:
"""构造 RPT_F10_EH_EQUITY 查询参数."""
return {
"source": "CHOICE",
"reportName": _REPORT_NAME,
"columns": _COLUMNS,
"quoteColumns": "",
"pageNumber": "1",
"pageSize": "",
"sortColumns": "END_DATE",
"client": "SW",
"filter": f'(SECUCODE="{secucode}")(LISTING_DATE>=\'1900-01-01\')(LISTING_DATE<=\'{end_date}\')',
"sortTypes": "-1",
}
def _fetch_one(secucode: str) -> pd.DataFrame:
"""拉一只股票的股本结构 (全量历史)."""
end_date = pd.Timestamp.today().strftime("%Y-%m-%d")
rows = fetch_report(_build_params(secucode, end_date))
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_equity(secucodes: list[str]) -> 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)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+222
View File
@@ -0,0 +1,222 @@
"""东方财富 CHOICE API 重大事项抓取工具 (并购/股权激励/诉讼/关联交易/违规/担保)。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame, 六类事项各一个入口函数
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
def _date_range(years: int) -> tuple[str, str]:
"""按年份数生成 [start, end] 日期区间."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
return start, end
def _year_range(years: int) -> tuple[str, str]:
"""按年份数生成 [start_year, end_year] 区间 (字符串)."""
end_year = pd.Timestamp.today().year
start_year = end_year - years + 1
return str(start_year), str(end_year)
def _fetch_one(secucode: str, years: int, build_params) -> pd.DataFrame:
"""按给定参数构造器拉一只股票的数据."""
rows = fetch_report(build_params(secucode, years))
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def _run(secucodes: list[str], years: int, build_params) -> pd.DataFrame:
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames = [_fetch_one(code, years, build_params) for code in codes]
frames = [df for df in frames if not df.empty]
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
# ---------------------------------------------------------------- 并购事件
def _build_acquisitions_params(secucode: str, years: int) -> dict[str, str]:
start, end = _date_range(years)
return {
"reportName": "RPT_HS_ACQUISITIONS_EVENT",
"columns": (
"SECUCODE,SECURITY_CODE,FIRST_NOTICE_DATE,NOTICE_DATE,PLAN_PROCESS,"
"NAME_OBJ,NAME_BUY,MXID,NAME_SELL,DEAL_AMT,TRANSFER_RATIO,TRADTE_CURRENCY"
),
"quoteColumns": "",
"filter": (
f'(SECUCODE="{secucode}")(FIRST_NOTICE_DATE>=\'{start}\')'
f"(FIRST_NOTICE_DATE<='{end}')"
),
"sortTypes": "-1,1",
"sortColumns": "FIRST_NOTICE_DATE,MXID",
"requestParams": "1",
"pageNumber": "1",
"pageSize": "50",
"client": "SW",
}
def get_acquisitions(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的并购事件。"""
return _run(secucodes, years, _build_acquisitions_params)
# ---------------------------------------------------------------- 股权激励
def _build_equity_incentive_params(secucode: str, years: int) -> dict[str, str]:
start, end = _date_range(years)
return {
"reportName": "RPT_DMSK_SH_EQUITYINCENTIVE",
"columns": (
"SECUCODE,ORG_CODE,PLAN_PROCESS,EI_TARGET,EI_WAY,TOTAL_INCENTIVE_SHARES,"
"TOTAL_INCENTIVE_TSR,INITIAL_EXERCISE_PRICE,VALID_EXPIRE,PLAN_NOTICE_DATE,"
"GMDECISION_NOTICE_DATE,FIRST_NOTICE_DATE,PLAN_EXPLAIN,PRICE_EXPLAIN,"
"INCENTIVE_SELL_EXPLAIN,INCENTIVE_GRANT_CONDITION,EXERCISE_SPECIAL_CONDITION,"
"NOTICE_DATE,STR_YEAR,SECURITY_NAME_ABBR,SECURITY_CODE,INDUSTRYCSRC,"
"EM_INDUSTRY,PLAN_NUM"
),
"filter": (
f'(SECUCODE="{secucode}")(PLAN_NOTICE_DATE>=\'{start}\')'
f"(PLAN_NOTICE_DATE<='{end}')"
),
"sortTypes": "-1",
"sortColumns": "PLAN_NOTICE_DATE",
"source": "CHOICE",
"client": "SW",
}
def get_equity_incentive(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的股权激励方案。"""
return _run(secucodes, years, _build_equity_incentive_params)
# ---------------------------------------------------------------- 诉讼仲裁
def _build_litigation_params(secucode: str, years: int) -> dict[str, str]:
start, end = _year_range(years)
return {
"source": "CHOICE",
"reportName": "RPT_LITIGATION_ARBITRATION_BSINFO",
"columns": (
"ORG_CODE,SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,NOTICE_DATE,"
"CASE_NAME,CASE_PROFILE,PLAINTIFF,DEFENCE,LITIGATION_TYPE,"
"FI_PROSECUTE_DATE,FI_ACCEPTANCE_COURT,CASE_AMOUNT,FI_JUDGMENT_DATE,"
"FI_JUDGMENT_CONTENT,FI_IS_APPEAL,SI_APPELLEE,SI_ACCEPTANCE_COURT,"
"SI_JUDGMENT_DATE,SI_JUDGMENT_CONTENT,EXECUTE_SITUATION,CURRENCY,NOTICE_YEAR"
),
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(NOTICE_YEAR>="{start}")(NOTICE_YEAR<="{end}")',
"pageNumber": "",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "NOTICE_DATE",
"client": "SW",
}
def get_litigation(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的诉讼仲裁信息。"""
return _run(secucodes, years, _build_litigation_params)
# ---------------------------------------------------------------- 关联交易
def _build_related_transaction_params(secucode: str, years: int) -> dict[str, str]:
start, end = _date_range(years)
return {
"reportName": "RPT_RELATED_TRANSACTION",
"columns": (
"SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,ORG_CODE,NOTICE_DATE,"
"RELATED_PARTY,RELATED_RELATION,IS_CONTROL,TRADE_AMT,PAY_WAY,"
"TRADE_PROFILE,PRICE_BASIS,TRADE_START_DATE,TRADE_END_DATE,CURRENCY,STR_YEAR"
),
"quoteColumns": "",
# 抓包中该报表 filter 含分号, 保留原样
"filter": (
f'(SECUCODE="{secucode}");(NOTICE_DATE>=\'{start}\')'
f"(NOTICE_DATE<='{end}')"
),
"pageNumber": "",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "NOTICE_DATE",
"client": "SW",
"source": "CHOICE",
}
def get_related_transaction(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的关联交易。"""
return _run(secucodes, years, _build_related_transaction_params)
# ---------------------------------------------------------------- 违规
def _build_violation_params(secucode: str, years: int) -> dict[str, str]:
start, end = _date_range(years)
return {
"source": "CHOICE",
"reportName": "RPT_HSF9_OP_VIOLATION",
"columns": (
"ORG_CODE,NOTICE_DATE,NOTICE_YEAR,CURRENCY,SECUCODE,SECURITY_CODE,"
"SECURITY_NAME_ABBR,SOLVE_ORG,VIOLATE_TYPE,PUNISH_TYPE,PUNISH_AMT,"
"VIOLATE_BEHAVIOUR,PUNISH_STEP,PUNISH_OBJECT"
),
"quoteColumns": "",
# 抓包中该报表 secucode 为小写, 保留原样
"filter": (
f'(secucode="{secucode}")(NOTICE_DATE>=\'{start}\')'
f"(NOTICE_DATE<='{end}')"
),
"pageNumber": "1",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "NOTICE_DATE",
"client": "SW",
}
def get_violation(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的违规处罚记录。"""
return _run(secucodes, years, _build_violation_params)
# ---------------------------------------------------------------- 担保
def _build_guarantee_params(secucode: str, years: int) -> dict[str, str]:
start, end = _year_range(years)
return {
"source": "CHOICE",
"reportName": "RPT_F9_GUARANTEE",
"columns": (
"SECURITY_CODE,SECUCODE,SECURITY_NAME_ABBR,REPORT_DATE,GUAR_NAME,"
"GUARANTEED_NAME,GUARANTEE_START_DATE,GUARANTEE_END_DATE,IS_PERFORM,"
"TRADE_DATE,GUARANTEE_WAY,GUARANTEE_AMT,GUARANTEE_EXPIRE,"
"IS_RELATED_TRADE,CURRENCY,REPORT_YEAR,HISTORYCHG,ORG_CODE"
),
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(REPORT_YEAR>="{start}")(REPORT_YEAR<="{end}")',
"sortTypes": "-1",
"sortColumns": "REPORT_DATE",
"client": "SW",
}
def get_guarantee(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的对外担保记录。"""
return _run(secucodes, years, _build_guarantee_params)
+73
View File
@@ -0,0 +1,73 @@
"""东方财富 CHOICE API 管理层讨论与分析(MD&A)抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (年报/中报的董事会报告索引, 含标题与页码)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
_REPORT_NAME = "RPT_BUSINESSANALYSIS"
# 列清单沿用 docs/api_docs.md 抓包值
_COLUMNS = (
"ORG_CODE,SECUCODE,SECURITY_CODE,SECURITY_INNER_CODE,SECURITY_NAME_ABBR,"
"REPORT_DATE,REPORT_NAME,NOTICE_DATE,SOURCE,RELINFOCODE,TITLE,PAGE"
)
# 只取年报/中报的讨论与分析
_REPORT_NAMES = '"年报","中报"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
) -> dict[str, str]:
"""构造 RPT_BUSINESSANALYSIS 查询参数."""
return {
"source": "CHOICE",
"reportName": _REPORT_NAME,
"columns": _COLUMNS,
"quoteColumns": "",
"filter": (
f'(SECUCODE="{secucode}")(REPORT_NAME in ({_REPORT_NAMES}))'
f"(REPORT_DATE<='{end_date}')(REPORT_DATE>='{start_date}')"
),
"pageNumber": "1",
"pageSize": "50",
"sortTypes": "-1",
"sortColumns": "REPORT_DATE",
"client": "SW",
}
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_mda(secucodes: list[str], years: int = 5) -> 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)