You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
126 lines
3.9 KiB
126 lines
3.9 KiB
"""东方财富 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)
|
|
|