实现全部 CHOICE 财务接口, 补充测试/文档/部署配置

- 新增 9 个抓取模块: 三大报表/盈利能力/资本结构/营运能力/成长能力/杜邦/分红
- api.py 新增 10 个 /fin/* 接口并统一无数据返回
- 新增 _emchoice.py 共享请求辅助 (emws 跳转取 URL / datacenter 报表拉取)
- 新增冒烟测试 tests/test_interfaces.py (24 用例)
- 新增接口文档 api_manual.md 与施工蓝图 blueprint.md
- 新增 Dockerfile / docker-compose.yml / deploy.sh 部署配置
This commit is contained in:
2026-08-15 09:19:11 +08:00
parent cd7ca92cff
commit 11ddbd3c49
21 changed files with 1806 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
"""东方财富 CHOICE 接口公共请求辅助 (仅内部模块复用, 不对外暴露)。"""
from __future__ import annotations
import requests
from utils.fin_summary import HEADERS, TIMEOUT
# emchoicews2 的 f9 页面接口, 返回带 urls 的跳转结构
_EMWS_URL = "https://emchoicews2.eastmoney.com"
# datacenter-choice 报表数据接口
_BASE_URL = "https://datacenter-choice.eastmoney.com/api/data/v1/get"
def get_data_url(path: str, params: dict[str, str]) -> str:
"""调用 emchoicews2 的 f9 路径, 返回真正的报表数据接口 URL."""
r = requests.get(_EMWS_URL + path, headers=HEADERS, params=params, timeout=TIMEOUT)
r.raise_for_status()
payload = r.json()
urls = payload.get("urls") or []
if not urls:
raise RuntimeError(f"{path}: 接口未返回数据 URL: {payload}")
return urls[0]
def fetch_report(url_or_params: str | dict[str, str]) -> list[dict]:
"""拉取 datacenter-choice 报表数据, 返回行 dict 列表.
支持直接传完整 URL (由 get_data_url 得到), 或传 reportName 等参数 dict.
"""
if isinstance(url_or_params, dict):
r = requests.get(_BASE_URL, headers=HEADERS, params=url_or_params, timeout=TIMEOUT)
else:
r = requests.get(url_or_params, headers=HEADERS, timeout=TIMEOUT)
r.raise_for_status()
payload = r.json()
if not payload.get("success"):
raise RuntimeError(f"接口返回失败: {payload}")
return payload.get("result", {}).get("data") or []
+80
View File
@@ -0,0 +1,80 @@
"""东方财富 CHOICE API 资产负债表抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的资产负债表数据)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWSJ_ZCFZB"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报
_DEFAULT_REPORT_PERIOD_TYPE = "1,5,3,6,7"
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWSJ_ZCFZB 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"ReportShowForm": "1",
"Digits": "2",
"Sort": "-1",
"ListedType": "0,1",
"IsShowEmptyRow": "true",
"ExchangeRateType": "1",
"Currency": "CNY:1",
"Magnitude": "4",
"ReportType": "1",
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的资产负债表."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_balance(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+75
View File
@@ -0,0 +1,75 @@
"""东方财富 CHOICE API 资本结构与偿债能力抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的资本结构/偿债能力指标)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWFX_ZBJGYCZNL"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报 (注意: 该接口需带引号)
_DEFAULT_REPORT_PERIOD_TYPE = '"1","5","3","6","7"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWFX_ZBJGYCZNL 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"ReportShowForm": "0",
"Sort": "-1",
"Digits": "2",
"IsShowEmptyRow": "true",
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的资本结构与偿债能力指标."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_capital_structure(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+80
View File
@@ -0,0 +1,80 @@
"""东方财富 CHOICE API 现金流量表抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的现金流量表数据)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWSJ_XJLLB"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报
_DEFAULT_REPORT_PERIOD_TYPE = "1,5,3,6,7"
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWSJ_XJLLB 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"ReportShowForm": "1",
"Digits": "2",
"Sort": "-1",
"ListedType": "0,1",
"IsShowEmptyRow": "true",
"ExchangeRateType": "1",
"Currency": "CNY:1",
"Magnitude": "4",
"ReportType": "1",
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的现金流量表."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_cashflow(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+126
View File
@@ -0,0 +1,126 @@
"""东方财富 CHOICE API 分红数据抓取工具 (分红明细 + 分红统计)。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按年度的分红明细 / 分红统计)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
_DETAIL_REPORT_NAME = "RPT_HSF9_ASSIGNPLAN_DETAIL"
# 列清单沿用 docs/api_docs.md 抓包值
_DETAIL_COLUMNS = (
"SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,REPORT_TYPE,ISSUE_YEAR,"
"IMPL_PLAN_PROFILE,ASSIGN_PROGRESS,ASSIGN_OBJECT,PRETAX_BONUS_RMB,"
"AFTERTAX_BONUS_RMB,BONUS_RATIO,IT_RATIO,CASHAMT,DIVSHAREBASE,"
"DAT_YAGGR,GMDECISION_NOTICE_DATE,EQUITY_RECORD_DATE,EX_DIVIDEND_DATE,"
"PAY_CASH_DATE,EQUITY_BASEDATE,NOTICE_DATE,NOTICE_ADDRES,INT_YEAR"
)
_STATICS_REPORT_NAME = "RPT_HSF9_ASSIGNPLAN_STATICS"
_STATICS_COLUMNS = (
"SECURITY_NAME_ABBR,SECURITY_CODE,SECUCODE,SECURITY_NAME,YEAR,"
"AUALACCMDIV_ARD,PARENTNETPROFIT,SFCFJCXG,NDLJMGGLSQ,CLOSE_PRICE,"
"GXL,SUM_AUALACCMDIV_ARD,SUM_PARENTNETPROFIT,ZFL"
)
def _year_range(years: int) -> tuple[int, int]:
"""按年份数算出 (起始年, 结束年), 结束年 = 当前年."""
end_year = pd.Timestamp.today().year
return end_year - years + 1, end_year
def _build_detail_params(
secucode: str,
start_year: int,
end_year: int,
) -> dict[str, str]:
return {
"reportName": _DETAIL_REPORT_NAME,
"columns": _DETAIL_COLUMNS,
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(INT_YEAR>={start_year})(INT_YEAR<={end_year})',
"pageNumber": "",
"pageSize": "",
"sortTypes": "-1,-1",
"sortColumns": "INT_YEAR,NOTICE_DATE",
"source": "CHOICE",
"client": "SW",
}
def _build_statics_params(
secucode: str,
start_year: int,
end_year: int,
) -> dict[str, str]:
return {
"reportName": _STATICS_REPORT_NAME,
"columns": _STATICS_COLUMNS,
"quoteColumns": "",
"filter": f'(SECUCODE="{secucode}")(YEAR>={start_year})(YEAR<={end_year})',
"pageNumber": "",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "YEAR",
"source": "CHOICE",
"client": "SW",
}
def _fetch_detail(secucode: str, years: int) -> pd.DataFrame:
"""拉一只股票的分红明细."""
start_year, end_year = _year_range(years)
rows = fetch_report(_build_detail_params(secucode, start_year, end_year))
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def _fetch_statics(secucode: str, years: int) -> pd.DataFrame:
"""拉一只股票的分红统计."""
start_year, end_year = _year_range(years)
rows = fetch_report(_build_statics_params(secucode, start_year, end_year))
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_dividend_detail(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_detail(code, years)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
def get_dividend_statics(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_statics(code, years)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+80
View File
@@ -0,0 +1,80 @@
"""东方财富 CHOICE API 杜邦分析抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的杜邦分析指标)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
_REPORT_NAME = "RPT_HSF9_FINA_DUPONT"
# 列清单沿用 docs/api_docs.md 抓包值
_COLUMNS = (
"SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,ORG_CODE,SECURITY_INNER_CODE,"
"TRADE_MARKET_CODE,TRADE_MARKET,SECURITY_TYPE,SECURITY_TYPE_CODE,"
"REPORT_DATE,ROE,NETPROFIT,TOTAL_OPERATE_INCOME,PARENT_NETPROFIT_RATIO,"
"ASSET_TURNOVER_RATIO,TOTAL_ASSETS,EQUITY_MULTIPLIER,ROE_AVERAGE_PRE,"
"ROE_DIF,NETPROFIT_TOI,NP_TP,TP_EBIT,EBIT_TOI,STR_YEAR,STR_MONTH,"
"TOTAL_PROFIT,EBIT,PARENT_NETPROFIT,PRE_TOTAL_ASSETS,AVG_TOTAL_ASSETS,"
"TOTAL_PARENT_EQUITY,PRETOTAL_PARENT_EQUITY,AVGTOTAL_PARENT_EQUITY,"
"PARENT_EQUITY_NETMARGIN,IS_NEWEST"
)
# 报告期: 1=一季报, 3=三季报, 5=中报, 6=年报
_REPORT_PERIOD_CODES = '"1","5","3","6","7"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
) -> dict[str, str]:
"""构造 RPT_HSF9_FINA_DUPONT 查询参数."""
return {
"reportName": _REPORT_NAME,
"columns": _COLUMNS,
"quoteColumns": "",
"filter": (
f'(SECUCODE="{secucode}")'
f"((REPORT_DATE>='{start_date}')(REPORT_DATE<='{end_date}')"
f"(STR_MONTH in ({_REPORT_PERIOD_CODES}))(|IS_NEWEST=''1''))"
),
"pageNumber": "1",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "REPORT_DATE",
"source": "choice",
"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_dupont(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的杜邦分析指标并按股票合并。"""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames: list[pd.DataFrame] = []
for code in codes:
df = _fetch_one(code, years)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+74
View File
@@ -0,0 +1,74 @@
"""东方财富 CHOICE API 成长能力抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的成长能力指标)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWFX_CZNL"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报
_DEFAULT_REPORT_PERIOD_TYPE = "1,5,3,6,7"
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWFX_CZNL 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"Digits": "2",
"Sort": "-1",
"IsShowEmptyRow": "true",
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的成长能力指标."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_growth(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+80
View File
@@ -0,0 +1,80 @@
"""东方财富 CHOICE API 利润表抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的利润表数据)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWSJ_LRB"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报 (注意: 该接口需带引号)
_DEFAULT_REPORT_PERIOD_TYPE = '"1","5","3","6","7"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWSJ_LRB 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"ReportShowForm": "0",
"Digits": "2",
"Sort": "-1",
"ListedType": '"0","1"',
"IsShowEmptyRow": "true",
"ExchangeRateType": "1",
"Currency": "CNY:1",
"Magnitude": "4",
"ReportType": '"1"',
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的利润表."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_income(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+110
View File
@@ -0,0 +1,110 @@
"""东方财富 CHOICE API 主营构成(按产品/按地区) 抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期/产品分类的主营构成数据)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
import requests
from utils.fin_summary import HEADERS, TIMEOUT
# ZYGC_AFLZS 返回一个带 urls 的跳转结构, 真正的数据在
# datacenter-choice 的 RPT_HSF9_FN_MAINOPBUSINESS 报表里.
_ZYGC_URL = "https://emchoicews2.eastmoney.com/stock/f9/ZYGC_AFLZS"
# 默认报告期类型: 1=一季报, 5=中报, 3=三季报, 6=年报 (注意: 与财务摘要的编码不同)
_DEFAULT_REPORT_PERIOD_TYPE = '"1","5","3","6"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
classify_type: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 ZYGC_AFLZS 请求参数."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"Magnitude": "4",
"ClassifyType": classify_type,
"ReportShowForm": "0",
"ExchangeRate": "1",
"Digits": "2",
"Sort": "-1",
"IsShowEmptyRow": "true",
}
def _fetch_data_url(
secucode: str,
start_date: str,
end_date: str,
classify_type: str,
report_period_type: str,
) -> str:
"""调用 ZYGC_AFLZS, 返回真正的主营构成数据接口 URL."""
params = _build_params(
secucode, start_date, end_date, classify_type, report_period_type
)
r = requests.get(_ZYGC_URL, headers=HEADERS, params=params, timeout=TIMEOUT)
r.raise_for_status()
payload = r.json()
urls = payload.get("urls") or []
if not urls:
raise RuntimeError(f"{secucode}: ZYGC_AFLZS 未返回数据 URL: {payload}")
return urls[0]
def _fetch_one(
secucode: str,
classify_type: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的主营构成."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
data_url = _fetch_data_url(secucode, start, end, classify_type, report_period_type)
response = requests.get(data_url, headers=HEADERS, timeout=TIMEOUT)
response.raise_for_status()
payload = response.json()
if not payload.get("success"):
raise RuntimeError(f"{secucode}: 接口返回失败: {payload}")
rows = payload.get("result", {}).get("data") or []
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
df["CLASSIFY_TYPE"] = classify_type # 冗余一列, 方便区分产品/地区
return df
def get_main_business(
secucodes: list[str],
classify_type: str = "产品",
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, classify_type, years, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+70
View File
@@ -0,0 +1,70 @@
"""东方财富 CHOICE API 营运能力抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的营运能力指标)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import fetch_report
_REPORT_NAME = "RPT_HSF9_FINA_OPERATEABILITY"
_COLUMNS = "OPERATEABILITY"
# 报告期: 1=一季报, 3=三季报, 5=中报, 6=年报
_REPORT_PERIOD_CODES = "1,5,3,6,7"
def _build_params(
secucode: str,
start_date: str,
end_date: str,
) -> dict[str, str]:
"""构造 RPT_HSF9_FINA_OPERATEABILITY 查询参数."""
return {
"reportName": _REPORT_NAME,
"columns": _COLUMNS,
"quoteColumns": "",
"filter": (
f'(SECUCODE="{secucode}")'
f"((REPORT_DATE>='{start_date}')(REPORT_DATE<='{end_date}')"
f"(STR_MONTH in({_REPORT_PERIOD_CODES}))(|IS_NEWEST=\"1\") )"
),
"pageNumber": "",
"pageSize": "",
"sortTypes": "-1",
"sortColumns": "REPORT_DATE",
"source": "CHOICE",
"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_operate_ability(secucodes: list[str], years: int = 3) -> pd.DataFrame:
"""抓取指定股票的营运能力指标并按股票合并。"""
codes = [code.strip() for code in secucodes if code.strip()]
if not codes:
return pd.DataFrame()
frames: list[pd.DataFrame] = []
for code in codes:
df = _fetch_one(code, years)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)
+74
View File
@@ -0,0 +1,74 @@
"""东方财富 CHOICE API 盈利能力与收益质量抓取工具。
- 输入: 股票代码列表 (如 ['603233.SH'])
- 输出: pandas DataFrame (按报告期的盈利能力/收益质量指标)
- 不需要登录, 不需要 cookie, 无文件落盘
"""
from __future__ import annotations
import pandas as pd
from utils._emchoice import get_data_url, fetch_report
_PATH = "/stock/f9/CWFX_YLNLYSYZL"
# 默认报告期类型: 1=一季报, 3=三季报, 5=中报, 6=年报 (注意: 该接口需带引号)
_DEFAULT_REPORT_PERIOD_TYPE = '"1","5","3","6","7"'
def _build_params(
secucode: str,
start_date: str,
end_date: str,
report_period_type: str,
) -> dict[str, str]:
"""构造 CWFX_YLNLYSYZL 请求参数 (其余参数沿用 docs/api_docs.md 抓包值)."""
return {
"SecurityCode": secucode,
"StartDate": start_date,
"EndDate": end_date,
"ReportPeriodType": report_period_type,
"Sort": "-1",
"Digits": "2",
"IsShowEmptyRow": "true",
}
def _fetch_one(
secucode: str,
years: int,
report_period_type: str,
) -> pd.DataFrame:
"""拉一只股票的盈利能力与收益质量指标."""
today = pd.Timestamp.today()
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
params = _build_params(secucode, start, end, report_period_type)
data_url = get_data_url(_PATH, params)
rows = fetch_report(data_url)
df = pd.DataFrame(rows)
if not df.empty:
df["SECUCODE"] = secucode
return df
def get_profitability(
secucodes: list[str],
years: int = 3,
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
) -> 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, report_period_type)
if not df.empty:
frames.append(df)
if not frames:
return pd.DataFrame()
return pd.concat(frames, ignore_index=True)