新增公司资料/指数/重大事项等 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:
@@ -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` |
|
||||
|
||||
@@ -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")
|
||||
@@ -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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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"eColumns=&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
@@ -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
@@ -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 年数据,断言非空 + 关键列)
|
||||
|
||||
@@ -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` |
|
||||
|
||||
### 🟢 低价值(看心情)
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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])
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user