"""东方财富 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)