新增公司资料/指数/重大事项等 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:
@@ -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