
Rq Earnings Preview
- 1 installs
- 43 repo stars
- Updated June 23, 2026
- ricequant/ricequant-skills
rq-earnings-preview is an agent skill that turns RiceQuant JSON exports into a structured pre-earnings preview report with forecasts, expectations, scenarios, and trade setup.
About
rq-earnings-preview is an agent skill for quant developers and traders who already export RiceQuant fundamentals and price history as JSON and need a repeatable earnings-week research artifact instead of ad-hoc chat summaries. Point generate_report.py at a data directory satisfying the documented contract, and the workflow stitches company metadata, industry context, multi-quarter financials, ROE history, and recent closes into one citable preview report. The template forces an executive summary, explicit forecast framing, how the name is priced versus expectations, management/announcement clues, scenario bands with plausible market reactions, and a pre-disclosure trading setup plus verification risks. It matters because margin and cash-quality rules are encoded for real-world gaps (missing gross profit on banks/insurers), so agents do not collapse entire sections to “no data.” Use it in the research phase when a disclosure date and target quarter are known and your JSON bundle is current; it does not replace live data APIs or discretionary judgment on position size.
- Fills a fixed markdown report template (executive summary through appendix) from templated section placeholders
- Reads five JSON contracts from --data-dir: company_info, industry, historical_financials, roe_history, price_recent
- Computes historical trends, YoY margins, fee ratios, cash conversion, and ~3-month price/volume context from exports
- Margin logic prefers gross_profit, falls back to profit_from_operation, then net margin for financial/insurance names
- Includes forecast framework, sell-side expectations, announcements, scenario reactions, pre-earnings trade setup, and ri
Rq Earnings Preview by the numbers
- 1 all-time installs (skills.sh)
- Ranked #909 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ricequant/ricequant-skills --skill rq-earnings-previewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 43 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 23, 2026 |
| Repository | ricequant/ricequant-skills ↗ |
What it does
Generate a structured pre-earnings preview memo from local RiceQuant JSON exports before a target quarter disclosure.
Who is it for?
Best when you're automating equity research memos from RiceQuant --data-dir bundles ahead of a known target quarter earnings date.
Skip if: Skip if you need live streaming market data, broker execution, or full post-earnings recap workflows without preparing the five JSON input files first.
When should I use this skill?
Before a target quarter earnings disclosure when RiceQuant JSON files are available under --data-dir for generate_report.py.
What you get
You get a completed earnings-preview markdown report with scenario analysis, trading setup, and risk verification points ready for the disclosure window.
- Markdown earnings-preview report from the SKILL template
- Scenario analysis and pre-earnings trading setup section
- Historical operating and financial trend appendix with metric fallbacks documented
By the numbers
- 5 JSON input files in the earnings-preview data contract
- Report template spans executive summary through appendix with dedicated scenario and trading-setup sections
Files
RQ 股票研究 - 财报预览
核心原则
- 所有内容必须遵循三阶段流程:数据采集 -> 报告生成 -> HTML 渲染
assets/template.md是唯一报告模板来源;Python 只做数据归一化、指标计算、占位符填充和结构校验- skill 必须自包含,不能依赖仓库级
utils - 财报预览重在“预测框架”,不是把历史财务复述一遍
- 报告必须显式区分“历史事实”“市场预期”“分析师估算”
- 市场预期应优先来自
stock cn consensus与stock cn research-reports,不得伪造 consensus - 目标季度预计披露日、电话会和近期行业动态若需要写入正文,必须来自
web_search,不能靠训练记忆补写 - 缺失数据时必须明确写“无数据 / 未提供 / 未验证”,不能留空
数据源分工
RQData CLI 负责
- 公司信息、行业、历史财务、ROE、价格与成交额
- 一致预期、研报、目标价和公告主数据
- 历史股价反应、卖方分歧和预测锚点
web_search 负责
- 目标季度预计披露日
- 电话会 / 业绩会安排
- 近期行业动态、政策变化和公司新闻
web_search 禁止替代的内容
- 财务、价格、估值、公告、卖方预期和一致预期主数据
- 情景分析的核心数值框架
- 任何本应由 RQData 提供的结构化金融数据
web_search 使用规则
详细字段、来源等级、落盘示例和 fallback 规则见 references/web_search.md。
允许补充的内容:
- 目标季度预计披露日和电话会安排
- 财报前近期行业与政策动态
- 与本次财报预览直接相关的公司新闻
落盘要求:
- 所有网络搜索结果必须先写入
web_search_findings.json - 只写结构化记录,不把搜索草稿直接塞进报告
- 若未提供该文件,报告仍可交付,但涉及时间安排和近期动态的部分必须保持“未验证”边界
- 即便提供了
web_search_findings.json,它也只能补充预测背景,不能替代卖方预期与历史数据
硬性规则
以下任一条违反,视为输出失败:
[MUST-1]历史财务数据、分析师预期、研报和公告必须来自RQData CLI[MUST-2]目标季度预计披露日必须通过web_search获取实时信息,不能使用训练记忆[MUST-3]金额类数据必须统一转换为“亿元”等客户可读口径[MUST-4]consensus的t1/t2/t3是年度 forward buckets,不得直接伪装成目标季度单季预测[MUST-5]情景分析必须包含乐观 / 中性 / 悲观三个情景及对应股价反应区间[MUST-6]研报若进入正文,必须优先消费summaries.core_view等客户可读摘要层[MUST-7]每个关键数据点或关键结论都要标数据来源:XXX,置信度X[MUST-8]客户稿不得暴露LLM、skill、文件名、JSON 字段名或内部 workflow 术语[MUST-9]低置信度网络搜索结果不能单独支撑交易结论或发布日期结论
确信度评级
5:RQData CLI、交易所公告、上市公司官网、官方监管披露4:政府 / 监管 / 行业协会 / 官方机构、权威财经媒体3:一般新闻源,但来源清晰且与其他来源一致2:单一来源、细节不完整、时点未充分验证1:推断、估算、未验证信息
使用规则:
- 混合结论的置信度取关键来源中的最低等级
- 预测与情景分析统一标注为“分析师估算,置信度4”
- 低置信度网络搜索结果只能作为时间安排或行业背景线索
图表 / 图片需求
当前实现以结构化表格完成最小可交付版本,但财报预览仍需明确图表 / 表格需求。
- 图表 / 表格名称:共识预测表
- 目的:展示历史已披露季度与卖方年度预期口径
- 使用的数据文件:
historical_financials.json、consensus.json、research_reports.json - 关键字段:
revenue、net_profit、con_targ_price、comp_con_*、net_profit_t - 建议形式:表格
- 回答问题:市场当前预期高还是低、预期是否分歧
- 放置位置:
## 卖方预期与市场定价 - 若图表缺失:必须以表格保留
- 图表 / 表格名称:三情景预测表
- 目的:展示乐观 / 中性 / 悲观三情景和预期市场反应
- 使用的数据文件:
historical_financials.json、price_recent.json - 关键字段:
revenue、net_profit、gross_profit/profit_from_operation(如有)或可回退的利润率口径、close - 建议形式:表格
- 回答问题:财报前风险收益比如何
- 放置位置:
## 情景分析与市场反应 - 若图表缺失:必须以表格保留
目标产出
- 报告长度:5-8 页
- 输出文件:
- Markdown 报告
- HTML 报告(若本地已安装渲染器)
- 输出目录必须由
--data-dir/--output指定,不能写死固定路径
目录结构
earnings-preview/
├── SKILL.md
├── scripts/
│ ├── extract_announcements.py
│ └── generate_report.py
├── assets/
│ └── template.md
└── references/
├── data_contract.md
└── web_search.md输入文件契约
原始数据目录由 --data-dir 指定,脚本会按下列文件名查找输入:
company_info.jsonindustry.jsonhistorical_financials.jsonroe_history.jsonprice_recent.jsonhs300_recent.jsonconsensus.jsonresearch_reports.jsonannouncement_raw.json:可选,建议提供announcement_extracts.json:可选peers_financials.json:可选peers_instruments.json:可选web_search_findings.json:可选,但若正文要写预计披露日 / 电话会 / 行业动态,则应提供
完整字段说明见 references/data_contract.md。
工作流
步骤 1:准备参数
REPORT_DATE="${REPORT_DATE:-$(date +%F)}"
ORDER_BOOK_ID="${ORDER_BOOK_ID:-600519.XSHG}"
TARGET_QUARTER="${TARGET_QUARTER:-2026q1}"
PRICE_START_DATE="$(python3 - <<PY
from datetime import date, timedelta
report_date = date.fromisoformat("${REPORT_DATE}")
print((report_date - timedelta(days=90)).isoformat())
PY
)"
REPORT_LOOKBACK_START="$(python3 - <<PY
from datetime import date, timedelta
report_date = date.fromisoformat("${REPORT_DATE}")
print((report_date - timedelta(days=120)).isoformat())
PY
)"
ROE_START_DATE="$(python3 - <<PY
from datetime import date, timedelta
report_date = date.fromisoformat("${REPORT_DATE}")
print((report_date - timedelta(days=540)).isoformat())
PY
)"
HISTORY_START_QUARTER="$(python3 - <<PY
from datetime import date
report_date = date.fromisoformat("${REPORT_DATE}")
print(f"{report_date.year - 2}q1")
PY
)"
HISTORY_END_QUARTER="$(python3 - <<PY
from datetime import date
report_date = date.fromisoformat("${REPORT_DATE}")
print(f"{report_date.year}q4")
PY
)"
TARGET_FISCAL_YEAR="$(python3 - <<PY
print("${TARGET_QUARTER}"[:4])
PY
)"
PREV_FISCAL_YEAR="$(python3 - <<PY
print(str(int("${TARGET_QUARTER}"[:4]) - 1))
PY
)"
DATA_DIR="${DATA_DIR:-$HOME/rq_equities_reports/earnings_preview}"
OUTPUT_MD="${OUTPUT_MD:-$DATA_DIR/earnings_preview_${ORDER_BOOK_ID}_${TARGET_QUARTER}_${REPORT_DATE}.md}"
PEER_STOCKS_JSON="${PEER_STOCKS_JSON:-[]}"步骤 2:采集公司、行业与历史财务
mkdir -p "$DATA_DIR"
rqdata stock cn instruments --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"]
}" --format json > "$DATA_DIR/company_info.json"
rqdata stock cn industry --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"date\": \"$REPORT_DATE\"
}" --format json > "$DATA_DIR/industry.json"
rqdata stock cn financial --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"fields\": [\"revenue\", \"net_profit\", \"gross_profit\", \"operating_expense\", \"cash_from_operating_activities\"],
\"start_quarter\": \"$HISTORY_START_QUARTER\",
\"end_quarter\": \"$HISTORY_END_QUARTER\",
\"statements\": \"all\"
}" --format json > "$DATA_DIR/historical_financials.json"
rqdata stock cn financial-indicator --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"factor\": \"return_on_equity_weighted_average\",
\"start_date\": \"$ROE_START_DATE\",
\"end_date\": \"$REPORT_DATE\"
}" --format json > "$DATA_DIR/roe_history.json"步骤 3:采集市场表现与预期数据
rqdata stock cn price --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"start_date\": \"$PRICE_START_DATE\",
\"end_date\": \"$REPORT_DATE\",
\"fields\": [\"close\", \"volume\", \"total_turnover\"],
\"adjust_type\": \"none\"
}" --format json > "$DATA_DIR/price_recent.json"
rqdata index price --payload "{
\"order_book_ids\": [\"000300.XSHG\"],
\"start_date\": \"$PRICE_START_DATE\",
\"end_date\": \"$REPORT_DATE\",
\"fields\": [\"close\"]
}" --format json > "$DATA_DIR/hs300_recent.json"
rqdata stock cn consensus --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"start_date\": \"$REPORT_LOOKBACK_START\",
\"end_date\": \"$REPORT_DATE\",
\"report_range\": 3
}" --format json > "$DATA_DIR/consensus.json"
rqdata stock cn research-reports --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"fiscal_year\": \"$TARGET_FISCAL_YEAR\",
\"start_date\": \"$REPORT_LOOKBACK_START\",
\"end_date\": \"$REPORT_DATE\",
\"date_rule\": \"create_tm\"
}" --format json > "$DATA_DIR/research_reports_current_year.json"
rqdata stock cn research-reports --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"fiscal_year\": \"$PREV_FISCAL_YEAR\",
\"start_date\": \"$REPORT_LOOKBACK_START\",
\"end_date\": \"$REPORT_DATE\",
\"date_rule\": \"create_tm\"
}" --format json > "$DATA_DIR/research_reports_prev_year.json"
python3 - "$DATA_DIR/research_reports_prev_year.json" "$DATA_DIR/research_reports_current_year.json" "$DATA_DIR/research_reports.json" <<'PY'
import json
import sys
from pathlib import Path
merged = []
for path in sys.argv[1:3]:
payload = json.loads(Path(path).read_text())
items = payload if isinstance(payload, list) else payload.get("data", [])
if isinstance(items, list):
merged.extend(items)
Path(sys.argv[3]).write_text(json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8")
PY
ANNOUNCEMENT_START="$(python3 - <<PY
from datetime import date, timedelta
report_date = date.fromisoformat("${REPORT_DATE}")
print((report_date - timedelta(days=240)).isoformat())
PY
)"
rqdata stock cn announcement --payload "{
\"order_book_ids\": [\"$ORDER_BOOK_ID\"],
\"start_date\": \"$ANNOUNCEMENT_START\",
\"end_date\": \"$REPORT_DATE\"
}" --format json > "$DATA_DIR/announcement_raw.json"步骤 3.5:网络搜索目标季度时间安排与近期行业动态
当正文需要写目标季度预计披露日、电话会或近期行业动态时,应执行该步骤。
- 使用
web_search获取目标季度预计披露日 - 可选补充电话会安排、近期行业动态和公司新闻
- 结果必须写入
web_search_findings.json
步骤 4:可选采集可比公司数据
若已有明确可比公司列表,可补充:
if [ "$PEER_STOCKS_JSON" != "[]" ]; then
rqdata stock cn instruments --payload "{
\"order_book_ids\": $PEER_STOCKS_JSON
}" --format json > "$DATA_DIR/peers_instruments.json"
rqdata stock cn financial --payload "{
\"order_book_ids\": $PEER_STOCKS_JSON,
\"fields\": [\"revenue\", \"net_profit\", \"gross_profit\"],
\"start_quarter\": \"$HISTORY_START_QUARTER\",
\"end_quarter\": \"$HISTORY_END_QUARTER\",
\"statements\": \"all\"
}" --format json > "$DATA_DIR/peers_financials.json"
fi步骤 5:整理研报观点摘要(必须)
说明:
- 不新增额外摘要文件,直接在
research_reports.json原记录上回写摘要 - 摘要输入位置:
research_reports.json -> records[].summary - 摘要回写位置:
research_reports.json -> records[].summaries.core_view - 仅处理当前股票、且
data_source=0的公司报告主样本 - 摘要应为客户可读表述,压缩原始研报的核心判断、盈利预期变化、估值或关键观察点
- 最终报告只消费这些摘要,不直接展示或截断原始
summary
步骤 6:提取公告原文片段(可选但推荐)
python3 earnings-preview/scripts/extract_announcements.py \
--stock "$ORDER_BOOK_ID" \
--data-dir "$DATA_DIR" \
--report-date "$REPORT_DATE"说明:
- 该步骤会从近期正式财报、主要经营数据、业绩说明会等公告中提取可复用正文片段
announcement_extracts.json采用两层结构:raw_sections:保存较长原文段落,仅作为后续总结输入summaries:保存面向最终报告的精炼总结公司背景 / 管理层表述 / 经营展望主要针对年报、半年报正文;季报和临时公告保留原文链接为主- 若源站阻断或 PDF 不可读,也必须保留失败状态和原文链接
步骤 7:生成 Markdown 报告
python3 earnings-preview/scripts/generate_report.py \
--stock "$ORDER_BOOK_ID" \
--quarter "$TARGET_QUARTER" \
--data-dir "$DATA_DIR" \
--report-date "$REPORT_DATE" \
--output "$OUTPUT_MD"步骤 8:渲染 HTML
脚本会优先尝试调用本地安装的 rq-report-renderer;若未安装则回退到仓库内 report-renderer/scripts/render_report.py;两者都不可用时才保留 Markdown 并打印警告。
阶段门控
Gate 1:历史与预期主数据齐备
- 历史财务、股价、卖方预期主数据存在
- 历史事实和市场预期可以明确分层
- 即使没有网络搜索结果,也能形成基础财报预览框架
Gate 2:预测框架完成
- 已识别最新已披露季度
- 已形成目标季度基准预测
- 三情景和股价反应区间可解释
Gate 3:可选网络搜索结果完成
- 若启用网络搜索结果,
web_search_findings.json已落盘 - 预计披露日和电话会若进入正文,来源等级可解释
- 网络搜索结果只补充时间安排和行业背景,不越权替代主数据
Gate 4:成稿完成
- Markdown 已生成
- 若本地渲染器存在,HTML 已生成
- 客户稿不暴露内部术语
- 长度、章节、来源标注和预测框架达标
模板规则
- 报告必须严格基于 template.md 生成
- 占位符采用
[[TOKEN]]语法,不使用 Jinja - 当前模板仅允许以下占位符:
[[REPORT_DATE]][[COMPANY_NAME]][[STOCK_CODE]][[TARGET_QUARTER]][[EARNINGS_EVENT_CONTEXT]][[EXEC_SUMMARY]][[HISTORICAL_TREND]][[FORECAST_FRAMEWORK]][[EXPECTATION_SECTION]][[ANNOUNCEMENT_SECTION]][[SCENARIO_SECTION]][[TRADING_SETUP]][[RISK_SECTION]][[APPENDIX]]
报告质量要求
- 完整包含模板中的主章节
- 必须显式区分历史数据、卖方预期和分析师预测
- 不能只写“市场预期高/低”,必须给出对应数据
- 必须回答 4 个预测核心问题:
- 目标季度预计看什么
- 市场当前预期是什么
- 乐观 / 中性 / 悲观三情景分别长什么样
- 财报前交易定位偏向抢跑、保守还是中性
- 若正文写了预计披露日或电话会,必须来自网络搜索结果
- 若提供公告数据,正文必须保留原文链接;若提供提炼结果,应优先展示管理层和经营展望片段
- 近期研报样本若进入正文,必须展示整理后的客户可读摘要,不得直接截断原始
summary - 不得残留
[XX]、[需要补充]、[风险1]这类占位文本
阶段验收清单
- [ ] Markdown 文件存在
- [ ] 若本地渲染器存在则 HTML 文件存在
- [ ] 模板占位符无残留
- [ ] 主章节完整
- [ ] 预测章节和情景章节带有“分析师估算,置信度4”
- [ ] 市场预期章节确实引用了
consensus.json或research_reports.json - [ ] 若存在相关公告,正文保留原文链接
- [ ] 若启用
web_search,正文真实出现网络搜索结果而不是只多一个 sidecar JSON
常见错误
- 把
consensus当作季度预期直接使用,却没有说明它是 forward annual buckets - 写“市场预期”“交易建议”却没有任何研报、目标价或价格行为依据
- 目标季度预计披露日直接靠训练记忆补写
- 使用固定季度、固定日期或固定同业名单
- 把网络搜索结果直接写成主结论,反而压过 RQData 主数据
财报预览报告
- 报告日期:[[REPORT_DATE]]
- 公司:[[COMPANY_NAME]](
[[STOCK_CODE]]) - 目标季度:
[[TARGET_QUARTER]] - 预计披露信息:[[EARNINGS_EVENT_CONTEXT]]
执行摘要
[[EXEC_SUMMARY]]
预览季度预测框架
[[FORECAST_FRAMEWORK]]
卖方预期与市场定价
[[EXPECTATION_SECTION]]
公告与管理层线索
[[ANNOUNCEMENT_SECTION]]
情景分析与市场反应
[[SCENARIO_SECTION]]
财报前交易定位
[[TRADING_SETUP]]
关键风险与验证点
[[RISK_SECTION]]
历史经营与财务趋势
[[HISTORICAL_TREND]]
附录:口径说明
[[APPENDIX]]
earnings-preview 数据契约
earnings-preview/scripts/generate_report.py 默认从 --data-dir 读取以下 JSON 文件。
1. company_info.json
典型字段:
order_book_idsymbollisted_datesector_code_name
用途:
- 获取公司名称、股票代码和基础元数据
2. industry.json
典型字段:
order_book_idfirst_industry_codefirst_industry_name
用途:
- 补充行业信息
3. historical_financials.json
典型字段:
order_book_idquarterinfo_daterevenuenet_profitgross_profitprofit_from_operation(可选)operating_expensecash_from_operating_activities
用途:
- 历史趋势分析
- 计算同比、利润率、费用率、现金转化率
- 作为目标季度预测锚点
说明:
- 若
gross_profit可用,则优先使用毛利率 - 若
gross_profit缺失但profit_from_operation可用,则回退到营业利润率 - 若两者都缺失,则回退到
net_profit / revenue的净利率 - 金融股、保险股等样本常见
gross_profit缺失,不应因此整段利润率分析退化为“无数据”
4. roe_history.json
典型字段:
order_book_iddatereturn_on_equity_weighted_average
用途:
- 分析资本效率趋势
5. price_recent.json
典型字段:
order_book_iddatetimeclosevolumetotal_turnover
用途:
- 计算近 3 个月股价涨跌幅
- 估算财报前 realized volatility
- 分析成交额变化
说明:
close应使用未复权现价口径(例如adjust_type=none)- 若使用后复权价格,和卖方目标价直接比较会导致隐含空间失真
6. hs300_recent.json
典型字段:
order_book_iddatetimeclose
用途:
- 计算相对沪深300的超额收益
7. consensus.json
典型字段:
datecreate_tmcon_targ_pricecomp_con_operating_revenue_t1comp_con_operating_revenue_t2comp_con_operating_revenue_t3comp_con_net_profit_t1comp_con_net_profit_t2comp_con_net_profit_t3
用途:
- 获取最新一致预期快照
- 作为 market expectations 的结构化参考
注意:
t1/t2/t3是 RQData 提供的 forward buckets,不应直接假装成目标季度预测值
8. research_reports.json
典型字段:
datecreate_tmdata_sourcereport_titleinstituteauthorsummarysummaries.core_viewtarg_pricenet_profit_tnet_profit_t1revenue_trevenue_t1
用途:
- 获取近期研报标题、摘要、目标价和年度利润口径
- 形成卖方预期与市场定价章节
summaries.core_view保存面向最终报告的精炼观点摘要
注意:
- 若存在
data_source,应优先保留data_source=0的样本作为公司报告主样本 data_source!=0的记录可能混入行业、策略或其他公司内容,不应直接进入近期研报样本表- 最终报告不应直接截断原始
summary;若要展示研报观点,应优先读取summaries.core_view
9. peers_financials.json / peers_instruments.json
可选文件。
用途:
- 若已提供可比公司列表,可对比最新收入、利润和可用利润率水平
10. announcement_raw.json
可选文件。
典型字段:
info_datetitleinfo_typemediafile_typeannouncement_link
用途:
- 保留近期正式财报、主要经营数据公告、业绩说明会等原文链接
- 为财报前预览补充管理层近期沟通与经营线索
11. announcement_extracts.json
可选文件,可由 earnings-preview/scripts/extract_announcements.py 生成。
典型字段:
records[].titlerecords[].info_daterecords[].announcement_linkrecords[].is_annual_or_interim_reportrecords[].fetch_statusrecords[].extract_statusrecords[].raw_sections.company_introrecords[].raw_sections.management_discussionrecords[].raw_sections.risk_warningrecords[].raw_sections.outlookrecords[].summaries.company_introrecords[].summaries.management_discussionrecords[].summaries.risk_warningrecords[].summaries.outlook
用途:
raw_sections保存较长原文段落,供后续总结使用summaries保存可直接进入最终报告的精炼片段company_intro/management_discussion/outlook主要面向年报、半年报正文;季报和临时公告默认以链接留痕为主- 若源站拦截或 PDF 不可读,也必须保留失败状态和原文链接
解析约定
- 所有文件都允许
{"data": [...]}、{"data": {...}}、[...]、{...}四种包装方式 - 同一股票同一季度若存在多条财务记录,脚本会按
info_date选择最新披露版本 - 若卖方预期文件缺失,报告必须明确说明“未提供卖方口径数据”
- 若公告提炼文件缺失,报告仍应保留相关公告原文链接
12. web_search_findings.json
可选文件。
典型字段:
querysource_namesource_typetitleurlpublished_atretrieved_atsummarywhy_relevantconfidencefinding_typeevent_date(可选)expected_window(可选)
用途:
- 补充目标季度预计披露日与电话会安排
- 补充近期行业动态和公司新闻
- 为财报预览提供时间窗口与前瞻背景,但不替代财务、价格和一致预期主数据
允许的 finding_type:
earnings_release_dateconference_callindustry_contextcompany_news
允许的 source_type:
officialgovernmentassociationauthoritative_mediageneral_newsinference
置信度上限:
official:5government:4association:4authoritative_media:4general_news:3inference:1
Earnings Preview Web Search Reference
Purpose
Use web_search only to supplement earnings release date, conference-call arrangement, recent company developments, and industry context that RQData CLI does not directly provide for an earnings-preview report.
Allowed Coverage
- Expected earnings release date for the target quarter
- Conference call / results briefing time and registration details
- Recent industry trend or policy changes relevant to the upcoming quarter
- Recent company news that may affect the quarter preview or guidance discussion
Prohibited Usage
- Do not replace historical financials, prices, consensus data, research reports, or announcements
- Do not fabricate release dates, conference calls, or guidance
- Do not let low-confidence network-search findings replace the structured prediction framework
Required Output File
All network-search findings must be written to web_search_findings.json.
Each record must contain:
querysource_namesource_typetitleurlpublished_atretrieved_atsummarywhy_relevantconfidencefinding_type
Recommended fields:
subjectrelated_entitiesevent_dateexpected_window
Allowed finding_type
earnings_release_dateconference_callindustry_contextcompany_news
Source Types And Confidence Ceiling
official: max confidence5government: max confidence4association: max confidence4authoritative_media: max confidence4general_news: max confidence3inference: max confidence1
Search Workflow
1. Confirm the needed information is not directly available from RQData CLI. 2. For release dates and conference calls, prefer exchange, company IR, and official announcement sources first. 3. Save the findings into web_search_findings.json with structured metadata. 4. Keep summaries factual and tie the relevance note to the quarter preview, guidance risk, or timing risk. 5. Use the findings only to supplement the prediction framework, not to replace it.
Fallback
1. Use the native web_search tool when available. 2. Otherwise use the configured network search tool in the current environment. 3. If neither is available:
- do not fabricate real-time dates or call arrangements
- explicitly mark the relevant timing information as unavailable or unverified
- keep the report at the structured preview level
Example
{
"data": [
{
"query": "贵州茅台 2026Q1 业绩发布日期",
"source_name": "上交所",
"source_type": "official",
"title": "定期报告预约披露日公告",
"url": "https://www.example.com/sse",
"published_at": "2026-04-10",
"retrieved_at": "2026-04-11",
"summary": "公司披露 2026Q1 定期报告预约披露日。",
"why_relevant": "可用于确定财报前窗口和交易节奏安排。",
"confidence": 5,
"finding_type": "earnings_release_date",
"event_date": "2026-04-28",
"subject": "定期报告预约披露日"
}
]
}#!/usr/bin/env python3
"""Extract structured announcement snippets for earnings-preview."""
from __future__ import annotations
import argparse
import json
import re
import zlib
from datetime import date
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple
import requests
from generate_report import (
dedupe_financial_records,
extract_records,
parse_iso_date,
read_json_file,
select_relevant_announcements,
)
USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)
ACW_POS_LIST = [
0x0F,
0x23,
0x1D,
0x18,
0x21,
0x10,
0x01,
0x26,
0x0A,
0x09,
0x13,
0x1F,
0x28,
0x1B,
0x16,
0x17,
0x19,
0x0D,
0x06,
0x0B,
0x27,
0x12,
0x14,
0x08,
0x0E,
0x15,
0x20,
0x1A,
0x02,
0x1E,
0x07,
0x04,
0x11,
0x05,
0x03,
0x1C,
0x22,
0x25,
0x0C,
0x24,
]
ACW_MASK = "3000176000856006061501533003690027800375"
OBJ_RE = re.compile(rb"(\d+)\s+(\d+)\s+obj\b(.*?)endobj", re.S)
STREAM_RE = re.compile(rb"<<(.*?)>>\s*stream\r?\n(.*?)\r?\nendstream", re.S)
PAGE_RE = re.compile(rb"/Type\s*/Page\b")
TEXT_OP_RE = re.compile(
r"/([A-Za-z0-9]+)\s+[0-9.]+\s+Tf|"
r"<([0-9A-Fa-f\s]+)>\s*Tj|"
r"\[(.*?)\]\s*TJ|"
r"\(((?:\\.|[^\\)])*)\)\s*Tj|"
r"(-?[0-9.]+)\s+(-?[0-9.]+)\s+T[Dd]|"
r"T\*|BT|ET",
re.S,
)
TEXT_SECTION_STOP_MARKERS = [
"重要内容提示",
"一、主要财务数据",
"二、股东信息",
"三、其他提醒事项",
"四、季度财务报表",
"五、重要事项",
"六、其他事项",
"风险提示",
"重大风险提示",
"经营情况讨论与分析",
"管理层讨论与分析",
"投资者关系活动主要内容介绍",
"未来展望",
"经营计划",
"发展战略",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="提取公告 PDF 正文片段并生成 announcement_extracts.json")
parser.add_argument("--stock", required=True, help="股票代码")
parser.add_argument("--quarter", required=True, help="目标预览季度,例如 2026q1")
parser.add_argument("--data-dir", required=True, help="原始 JSON 数据目录")
parser.add_argument("--report-date", required=True, help="报告日期 (YYYY-MM-DD)")
parser.add_argument("--output", help="输出 JSON 路径,默认写到 data-dir/announcement_extracts.json")
parser.add_argument("--timeout", type=float, default=20.0, help="公告抓取超时时间,默认 20 秒")
return parser.parse_args()
def calc_sse_acw_cookie(arg1: str) -> str:
out = [""] * len(ACW_POS_LIST)
for idx, char in enumerate(arg1):
for out_idx, pos in enumerate(ACW_POS_LIST):
if pos == idx + 1:
out[out_idx] = char
break
arg2 = "".join(out)
pieces = []
for idx in range(0, min(len(arg2), len(ACW_MASK)), 2):
pieces.append(f"{int(arg2[idx:idx + 2], 16) ^ int(ACW_MASK[idx:idx + 2], 16):02x}")
return "".join(pieces)
def fetch_pdf_bytes(url: str, timeout: float) -> Tuple[Optional[bytes], str]:
referer = "https://www.cninfo.com.cn/"
if "sse.com.cn" in url:
referer = "http://www.sse.com.cn/"
elif "szse.cn" in url:
referer = "https://www.szse.cn/"
headers = {"User-Agent": USER_AGENT, "Accept": "application/pdf,text/html,*/*", "Referer": referer}
session = requests.Session()
try:
response = session.get(url, timeout=timeout, headers=headers, allow_redirects=True)
except requests.RequestException as exc:
return None, f"network_error:{type(exc).__name__}"
content_type = (response.headers.get("content-type") or "").lower()
if response.ok and (content_type.startswith("application/pdf") or response.content.startswith(b"%PDF-")):
return response.content, "ok"
if "static.sse.com.cn" in response.url and "text/html" in content_type:
match = re.search(r"arg1='([^']+)'", response.text)
if not match:
return None, "source_blocked:sse_html_without_arg1"
cookie = calc_sse_acw_cookie(match.group(1))
session.cookies.set("acw_sc__v2", cookie, domain="static.sse.com.cn", path="/")
try:
retry = session.get(
url,
timeout=timeout,
headers={"User-Agent": USER_AGENT, "Accept": "application/pdf,*/*", "Referer": "http://www.sse.com.cn/"},
allow_redirects=True,
)
except requests.RequestException as exc:
return None, f"network_error:{type(exc).__name__}"
retry_type = (retry.headers.get("content-type") or "").lower()
if retry.ok and (retry_type.startswith("application/pdf") or retry.content.startswith(b"%PDF-")):
return retry.content, "ok"
return None, f"source_blocked:sse_retry_{retry.status_code}"
if not response.ok:
return None, f"http_{response.status_code}"
return None, f"unsupported_content_type:{content_type or 'unknown'}"
def parse_pdf_objects(pdf_bytes: bytes) -> Dict[int, bytes]:
return {int(match.group(1)): match.group(3) for match in OBJ_RE.finditer(pdf_bytes)}
def parse_stream(raw_object: bytes) -> Tuple[Optional[bytes], Optional[bytes]]:
match = STREAM_RE.search(raw_object)
if not match:
return None, None
stream_dict = match.group(1)
stream_data = match.group(2)
if b"/FlateDecode" in stream_dict:
stream_data = zlib.decompress(stream_data)
return stream_dict, stream_data
def decode_utf16be_hex(value: str) -> str:
return bytes.fromhex(value).decode("utf-16-be", "ignore")
def build_cmap(stream_text: str) -> Dict[str, str]:
cmap: Dict[str, str] = {}
for block in re.findall(r"beginbfchar\s*(.*?)\s*endbfchar", stream_text, re.S):
for src, dst in re.findall(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>", block):
cmap[src.upper()] = decode_utf16be_hex(dst)
for block in re.findall(r"beginbfrange\s*(.*?)\s*endbfrange", stream_text, re.S):
for start, end, dst in re.findall(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>", block):
start_int = int(start, 16)
end_int = int(end, 16)
dst_int = int(dst, 16)
width = len(start)
out_len = len(dst) // 2
for idx, code in enumerate(range(start_int, end_int + 1)):
cmap[f"{code:0{width}X}"] = (dst_int + idx).to_bytes(out_len, "big").decode("utf-16-be", "ignore")
for start, _end, arr in re.findall(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*\[(.*?)\]", block, re.S):
start_int = int(start, 16)
width = len(start)
for idx, dst in enumerate(re.findall(r"<([0-9A-Fa-f]+)>", arr)):
cmap[f"{start_int + idx:0{width}X}"] = decode_utf16be_hex(dst)
return cmap
def decode_pdf_hex(hex_text: str, cmap: Dict[str, str]) -> str:
hex_text = re.sub(r"\s+", "", hex_text)
if not hex_text:
return ""
key_lengths = sorted({len(key) for key in cmap}, reverse=True) if cmap else [2]
cursor = 0
output: List[str] = []
while cursor < len(hex_text):
matched = False
for width in key_lengths:
key = hex_text[cursor:cursor + width].upper()
if len(key) == width and key in cmap:
output.append(cmap[key])
cursor += width
matched = True
break
if matched:
continue
chunk = hex_text[cursor:cursor + 2]
if len(chunk) == 2:
try:
output.append(bytes.fromhex(chunk).decode("latin1"))
except ValueError:
pass
cursor += 2
return "".join(output)
def decode_pdf_literal(text: str) -> str:
return (
text.replace(r"\(", "(")
.replace(r"\)", ")")
.replace(r"\n", "\n")
.replace(r"\r", "")
.replace(r"\t", "\t")
.replace(r"\\", "\\")
)
def extract_pdf_text(pdf_bytes: bytes) -> str:
objects = parse_pdf_objects(pdf_bytes)
font_cmaps: Dict[int, Dict[str, str]] = {}
for obj_num, raw_object in objects.items():
match = re.search(rb"/ToUnicode\s+(\d+)\s+0\s+R", raw_object)
if not match:
continue
stream_ref = int(match.group(1))
if stream_ref not in objects:
continue
_stream_dict, stream_data = parse_stream(objects[stream_ref])
if not stream_data:
continue
font_cmaps[obj_num] = build_cmap(stream_data.decode("latin1", "ignore"))
pages: List[Tuple[int, List[int], Dict[str, int]]] = []
for obj_num, raw_object in objects.items():
if not PAGE_RE.search(raw_object):
continue
content_refs = [int(value) for value in re.findall(rb"/Contents\s+(\d+)\s+0\s+R", raw_object)]
if not content_refs:
array_match = re.search(rb"/Contents\s*\[(.*?)\]", raw_object, re.S)
if array_match:
content_refs = [int(value) for value in re.findall(rb"(\d+)\s+0\s+R", array_match.group(1))]
font_map: Dict[str, int] = {}
font_block = re.search(rb"/Font\s*<<(.+?)>>", raw_object, re.S)
if font_block:
for font_name, font_ref in re.findall(rb"/([A-Za-z0-9]+)\s+(\d+)\s+0\s+R", font_block.group(1)):
font_map[font_name.decode("ascii", "ignore")] = int(font_ref)
pages.append((obj_num, content_refs, font_map))
pages.sort(key=lambda item: item[0])
lines: List[str] = []
current_font: Optional[str] = None
for _page_num, content_refs, font_map in pages:
for content_ref in content_refs:
if content_ref not in objects:
continue
_stream_dict, stream_data = parse_stream(objects[content_ref])
if not stream_data:
continue
content_text = stream_data.decode("latin1", "ignore")
current_line: List[str] = []
for match in TEXT_OP_RE.finditer(content_text):
token = match.group(0)
if " Tf" in token:
current_font = match.group(1)
continue
if token == "BT":
current_line = []
continue
if token == "ET":
line = "".join(current_line).strip()
if line:
lines.append(line)
current_line = []
continue
if token == "T*" or token.endswith("TD") or token.endswith("Td"):
if match.group(6) and abs(float(match.group(6))) > 1e-6:
line = "".join(current_line).strip()
if line:
lines.append(line)
current_line = []
continue
if token.endswith("Tj") and token.startswith("<"):
font_ref = font_map.get(current_font or "")
cmap = font_cmaps.get(font_ref, {})
current_line.append(decode_pdf_hex(match.group(2), cmap))
continue
if token.endswith("TJ"):
font_ref = font_map.get(current_font or "")
cmap = font_cmaps.get(font_ref, {})
segment = match.group(3) or ""
for hex_group in re.findall(r"<([0-9A-Fa-f\s]+)>", segment):
current_line.append(decode_pdf_hex(hex_group, cmap))
for literal in re.findall(r"\(((?:\\.|[^\\)])*)\)", segment):
current_line.append(decode_pdf_literal(literal))
continue
current_line.append(decode_pdf_literal(match.group(4)))
text = "\n".join(line for line in lines if line.strip())
text = text.replace("\r", "\n").replace("\u3000", "")
text = re.sub(r"[ \t]+\n", "\n", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def squash_text(text: str) -> str:
return re.sub(r"\s+", "", text or "")
def clip_text(text: str, limit: int = 260) -> str:
text = str(text or "").strip()
if len(text) <= limit:
return text
return text[: limit - 1].rstrip(",、;: ") + "…"
def normalize_section_text(text: str, limit: Optional[int] = None) -> str:
text = str(text or "")
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)
text = re.sub(r"\s+", "", text)
if not text:
return ""
meaningful_chars = re.findall(r"[\u4e00-\u9fffA-Za-z0-9,。!?;:、“”‘’()()\-%./]", text)
if len(meaningful_chars) < max(20, int(len(text) * 0.6)):
return ""
if not re.search(r"[\u4e00-\u9fffA-Za-z]", text):
return ""
if limit is None:
return text
return clip_text(text, limit)
def is_annual_or_interim_report(title: str, info_type: str) -> bool:
title = str(title or "")
info_type = str(info_type or "")
if not re.search(r"(年度报告|年报|半年度报告|半年报|中报)", title):
return False
if re.search(r"(摘要|英文版|公告|业绩说明会|主要经营数据|信息披露公告)", title):
return False
return "定期报告" in info_type or bool(re.search(r"(年度报告|年报|半年度报告|半年报|中报)", title))
def find_marker_window(
text: str,
markers: Sequence[str],
stop_markers: Sequence[str],
max_chars: int,
forbidden_patterns: Sequence[str] = (),
) -> str:
candidates: List[Tuple[int, str]] = []
for marker in markers:
start = 0
while True:
idx = text.find(marker, start)
if idx < 0:
break
candidates.append((idx, marker))
start = idx + len(marker)
if not candidates:
return ""
candidates.sort(key=lambda item: item[0])
for best_start, matched_marker in candidates:
local_context = text[max(0, best_start - 80): min(len(text), best_start + 120)]
if re.search(r"[..。…]{12,}", local_context):
continue
search_start = best_start + len(matched_marker)
end_positions = [
text.find(stop_marker, search_start)
for stop_marker in stop_markers
if stop_marker not in markers and text.find(stop_marker, search_start) >= 0
]
end = min(end_positions) if end_positions else min(len(text), best_start + max_chars)
end = min(end, best_start + max_chars)
snippet = normalize_section_text(text[best_start:end], max_chars)
if snippet and forbidden_patterns and any(pattern in snippet for pattern in forbidden_patterns):
continue
if snippet:
return snippet
return ""
def find_sentence_by_keywords(text: str, keywords: Sequence[str], max_chars: int) -> str:
sentences = re.split(r"(?<=[。!?;])", text)
for sentence in sentences:
sentence = sentence.strip()
if sentence and any(keyword in sentence for keyword in keywords):
return normalize_section_text(sentence, max_chars)
collapsed = text
for keyword in keywords:
idx = collapsed.find(keyword)
if idx >= 0:
start = max(0, idx - 40)
end = min(len(collapsed), idx + max_chars)
return normalize_section_text(collapsed[start:end], max_chars)
return ""
def extract_sections(title: str, info_type: str, raw_text: str) -> Dict[str, str]:
squashed = squash_text(raw_text)
stop_markers = TEXT_SECTION_STOP_MARKERS
long_form_report = is_annual_or_interim_report(title, info_type)
company_intro = ""
management_discussion = ""
outlook = ""
if long_form_report:
intro_end = len(squashed)
for marker in ("重要内容提示", "一、主要财务数据"):
idx = squashed.find(marker)
if idx >= 0:
intro_end = min(intro_end, idx)
company_intro = normalize_section_text(squashed[:intro_end] or squashed[:220], 220)
company_intro_marked = find_marker_window(
squashed,
["公司简介", "公司基本情况", "发行人基本情况"],
stop_markers,
220,
)
if company_intro_marked:
company_intro = company_intro_marked
management_discussion = find_marker_window(
squashed,
[
"管理层讨论与分析",
"经营情况讨论与分析",
"董事会报告",
"经营回顾",
],
stop_markers,
280,
)
if not management_discussion:
management_discussion = find_sentence_by_keywords(
squashed,
["经营", "销量", "需求", "增长", "盈利能力", "毛利率", "渠道", "产能"],
240,
)
outlook = find_marker_window(
squashed,
["未来展望", "经营计划", "发展战略", "未来规划", "下半年展望", "后续规划"],
stop_markers,
220,
forbidden_patterns=("前瞻性陈述", "注意投资风险"),
)
if not outlook:
outlook = find_sentence_by_keywords(
squashed,
["未来", "展望", "预计", "计划", "规划", "目标", "将继续", "后续"],
220,
)
risk_warning = find_marker_window(
squashed,
["风险提示", "重大风险提示", "风险因素", "重大风险"],
stop_markers,
220,
)
return {
"company_intro": company_intro,
"management_discussion": management_discussion,
"risk_warning": risk_warning,
"outlook": outlook,
}
def build_raw_sections(title: str, info_type: str, raw_text: str) -> Dict[str, str]:
squashed = squash_text(raw_text)
stop_markers = TEXT_SECTION_STOP_MARKERS
long_form_report = is_annual_or_interim_report(title, info_type)
company_intro = ""
management_discussion = ""
outlook = ""
if long_form_report:
intro_end = len(squashed)
for marker in ("重要内容提示", "一、主要财务数据"):
idx = squashed.find(marker)
if idx >= 0:
intro_end = min(intro_end, idx)
company_intro = normalize_section_text(squashed[:intro_end] or squashed[:1200], 1200)
company_intro_marked = find_marker_window(
squashed,
["公司简介", "公司基本情况", "发行人基本情况"],
stop_markers,
1400,
)
if company_intro_marked:
company_intro = company_intro_marked
management_discussion = find_marker_window(
squashed,
["管理层讨论与分析", "经营情况讨论与分析", "董事会报告", "经营回顾"],
stop_markers,
2600,
)
if not management_discussion:
management_discussion = find_sentence_by_keywords(
squashed,
["经营", "销量", "需求", "增长", "盈利能力", "毛利率", "渠道", "产能"],
1600,
)
outlook = find_marker_window(
squashed,
["未来展望", "经营计划", "发展战略", "未来规划", "下半年展望", "后续规划"],
stop_markers,
1800,
forbidden_patterns=("前瞻性陈述", "注意投资风险"),
)
if not outlook:
outlook = find_sentence_by_keywords(
squashed,
["未来", "展望", "预计", "计划", "规划", "目标", "将继续", "后续"],
1200,
)
risk_warning = find_marker_window(
squashed,
["风险提示", "重大风险提示", "风险因素", "重大风险"],
stop_markers,
1400,
)
return {
"company_intro": company_intro,
"management_discussion": management_discussion,
"risk_warning": risk_warning,
"outlook": outlook,
}
def choose_extract_status(sections: Dict[str, str], title: str, info_type: str) -> str:
populated = sum(1 for value in sections.values() if value)
if not is_annual_or_interim_report(title, info_type) and populated == 0:
return "skipped_non_annual_interim"
if populated >= 4:
return "ok"
if populated >= 1:
return "partial"
return "no_sections"
def main() -> None:
args = parse_args()
report_date = date.fromisoformat(args.report_date)
data_dir = Path(args.data_dir).expanduser()
output_path = Path(args.output).expanduser() if args.output else data_dir / "announcement_extracts.json"
financial_records = extract_records(read_json_file(data_dir / "historical_financials.json"))
announcement_records = extract_records(read_json_file(data_dir / "announcement_raw.json"))
if not financial_records:
raise ValueError("缺少 historical_financials.json,无法定位最近已披露季度")
deduped_financials = dedupe_financial_records(financial_records, args.stock, report_date)
latest_financial = deduped_financials[-1] if deduped_financials else None
latest_info_date = parse_iso_date(latest_financial.get("info_date")) if latest_financial else None
selected_announcements = select_relevant_announcements(
announcement_records,
args.stock,
report_date,
args.quarter.lower(),
)
records: List[Dict[str, Any]] = []
for item in selected_announcements:
title = str(item.get("title") or "")
link = str(item.get("announcement_link") or "")
info_date = parse_iso_date(item.get("info_date") or item.get("date") or item.get("create_tm"))
empty_sections = {
"company_intro": "",
"management_discussion": "",
"risk_warning": "",
"outlook": "",
}
record: Dict[str, Any] = {
"title": title,
"info_date": info_date.isoformat() if info_date else str(item.get("info_date") or ""),
"announcement_link": link,
"media": item.get("media"),
"info_type": item.get("info_type"),
"is_annual_or_interim_report": is_annual_or_interim_report(title, str(item.get("info_type") or "")),
"fetch_status": "skipped",
"extract_status": "not_started",
"raw_sections": dict(empty_sections),
"summaries": dict(empty_sections),
"sections": dict(empty_sections),
}
if str(item.get("file_type") or "").upper() != "PDF":
record["fetch_status"] = "unsupported_file_type"
record["extract_status"] = "unsupported"
records.append(record)
continue
pdf_bytes, fetch_status = fetch_pdf_bytes(link, args.timeout)
record["fetch_status"] = fetch_status
if not pdf_bytes:
record["extract_status"] = "fetch_failed"
records.append(record)
continue
try:
extracted_text = extract_pdf_text(pdf_bytes)
except Exception as exc: # pragma: no cover - defensive branch for malformed PDFs
record["extract_status"] = f"pdf_parse_failed:{type(exc).__name__}"
records.append(record)
continue
raw_sections = build_raw_sections(title, str(item.get("info_type") or ""), extracted_text)
record["raw_sections"] = raw_sections
record["summaries"] = dict(empty_sections)
record["sections"] = raw_sections
record["extract_status"] = choose_extract_status(raw_sections, title, str(item.get("info_type") or ""))
records.append(record)
output_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"stock": args.stock,
"report_date": args.report_date,
"event_date": latest_info_date.isoformat() if latest_info_date else "",
"record_count": len(records),
"records": records,
}
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"✅ 公告提取结果已写入:{output_path}")
print(f"相关公告样本:{len(records)} 条")
if __name__ == "__main__":
main()
Related skills
How it compares
Use as a local JSON-driven report generator tied to RiceQuant exports, not as a real-time market data MCP or generic chat equity summary.
FAQ
Who is rq-earnings-preview for?
It is for quant-oriented developers and traders who maintain RiceQuant JSON snapshots and want agent-assisted, sectioned earnings preview reports before quarterly disclosures.
When should I use rq-earnings-preview?
Use it during Idea-phase research when you know the stock, target quarter, and report date and have populated company_info, industry, historical_financials, roe_history, and price_recent under --data-dir.
Is rq-earnings-preview safe to install?
Treat it like any third-party agent skill: review the Security Audits panel on this Prism page and inspect generate_report.py and data paths before running on sensitive machines.