fd-cn-report
Wire (柏讯) product line · the open-data supply line of FindData — Chinese financial reports MCP
English | 中文
MCP server for Chinese financial reports — 31 申万 L1 industry AI rule system, outline extraction, AI structured extraction, Elasticsearch store + search, and interactive rules dashboard.
One-click install
fd-cn-report auto-registers as a datasource when installed alongside
fd-open-data-mcp. To install the entire finddata stack (hub + every
datasource + ontology DB) in one go:
pip install "fd-open-data-mcp[data]" fd-polygon fd-cn-report
fd-open-data-mcp migrate \
&& fd-open-data-mcp import-catalog \
&& fd-open-data-mcp consume-concepts \
&& fd-open-data-mcp propose-bindings \
&& fd-open-data-mcp seed-entities \
&& fd-open-data-mcp generate-schedules \
&& fd-open-data-mcp register-discovered
fd-open-data-mcp serve
To run fd-cn-report’s own MCP server standalone (without the hub):
uv sync && uv run python server.py — see Setup below.
Industry Rules System
21,698 LLM rules covering 31 申万 L1 industries with per-section extraction instructions generated from real annual report PDFs. Each rule maps an indicator to its exact section position in the periodic report.
Coverage
| 章节 | 内容 | 说明 |
|---|---|---|
| 资产负债表 | 全部科目 (112+ rules) | 从合并资产负债表提取 |
| 利润表 | 全部科目 (130+ rules) | 从合并利润表提取 |
| 现金流量表 | 全部科目 (42+ rules) | 从合并现金流量表提取 |
| 管理层讨论 | 主营业务分析、成本、产销量、研发 | 从实际报告第三节提取 |
| 主要财务指标 | ROE、EPS、毛利率、分季度数据 | 从第二节提取 |
| 报表附注/重要事项 | 关联交易、担保、诉讼、资产减值 | 从第六节+第十节附注提取 |
| 股东信息 | 前十大股东、持股变动、分红 | 从第七节提取 |
| 公司治理 | 董事会、高管薪酬 | 从第四节提取 |
| 环境 | 排污、碳排放、能源消耗 | 从第五节提取 |
Industry-Specific Rules
6 industries have industry-specific rules on top of the universal set:
| 行业 | 特有指标 | 来源 |
|---|---|---|
| 银行 | 不良率、净息差、资本充足率、贷款五级分类 | 工商银行年报 |
| 房地产 | 合同负债、存货-开发成本、土储、销售面积 | 保利发展年报 |
| 电力设备 | 在建工程、产能利用率、应收账款 | 宁德时代年报 |
| 医药生物 | 研发费用、销售费用、在研管线、无形资产 | 恒瑞医药年报 |
| 非银金融 | 保费收入、赔付支出、偿付能力、新业务价值 | 中国平安年报 |
| 农林牧渔 | 存货-消耗性生物资产 | 隆平高科年报 |
Rule Generation Pipeline
Rules are generated from real annual report PDFs via parallel LLM calls:
# Generate rules for all 31 industries from real reports
python scripts/generate_rules_from_real_reports.py --max-concurrent 5
# Single industry
python scripts/generate_rules_from_real_reports.py --industry 801120
# Run industry-specific sections only
python scripts/generate_rules_from_real_reports.py --industry 801780 --llm-only
Dashboard
Interactive web UI to browse, filter, and search all 21,698 rules:
# CLI
python scripts/industry_rules_dashboard.py
# or
fd-cn-report
# MCP tool
open_industry_rules_dashboard(port=8888)
Features:
- Industry filter — dropdown with all 31 申万 L1 industries
- Module filter — filter by balance_sheet, income_statement, etc.
- Keyword search — indicator name, section, instruction text
- Sortable columns — click header to sort
- Pagination — 50 rules per page
- Color-coded tags — module type visualization
- Real-time stats — filtered count, industry count, module count
Tools (44 MCP tools)
| Layer | Tool | Description |
|---|---|---|
| Company API | get_company |
Resolve ticker/name → company entry |
list_filings |
List CNINFO disclosures by form/category + year | |
get_filing |
One announcement’s metadata + PDF URL | |
get_financials |
Income/balance/cashflow via akshare | |
get_financial_statements |
三大报表 as text from the annual-report PDF | |
get_section |
(ticker, year, section) → section text |
|
list_report_types |
Browse CNINFO disclosure category catalog | |
get_special_report |
Special-type report (招股说明书, 收购报告书, …) | |
| HK Stock | get_hk_company |
Resolve HK stock by ticker/name |
list_hk_filings |
List HKEX filings | |
get_hk_financials |
HK financial statements | |
get_hk_section |
HK report section extraction | |
| Official-website (SSE/SZSE/BSE) | get_sse_company |
Resolve SSE-listed company by 6-digit ticker |
list_sse_filings |
List SSE disclosures from sse.com.cn | |
get_sse_section |
SSE annual-report section extraction | |
get_sse_interaction |
上证e互动 investor Q&A | |
get_szse_company |
Resolve SZSE-listed company by 6-digit ticker | |
list_szse_filings |
List SZSE disclosures from szse.cn | |
get_szse_section |
SZSE annual-report section extraction | |
get_szse_interaction |
互动易 investor Q&A | |
get_bse_company |
Resolve BSE-listed company by 6-digit ticker | |
list_bse_filings |
List BSE disclosures (BSE-native, CNINFO fallback) | |
get_bse_section |
BSE annual-report section extraction | |
| Official-website (CSRC) | list_csrc_filings |
List CSRC regulatory announcements |
get_csrc_ipo_review |
CSRC IPO (首发) review status | |
get_csrc_merger_review |
CSRC 并购重组 review status | |
list_csrc_enforcement |
CSRC enforcement actions | |
| Ministry statistics | list_ministries |
List supported ministry stat sources |
get_ministry_stat |
Ministry stats page -> HTML tables | |
get_nbs_stat |
NBS macro statistic by indicator code | |
| PDF / AI / ES | list_outline |
Parse 目录 from report URL or PDF path |
extract_section |
Body text by exact title / regex / ordinal | |
ai_extract |
LLM-structured extraction over section text | |
index_records |
Bulk index records into ES | |
search_reports |
BM25 + filter search with highlights | |
delete_index |
Drop cnreport-{year} index |
|
| Cache | list_cache |
List cached reports |
clear_cache |
Evict cached reports | |
| Indicators | list_indicators |
Browse indicator rule set |
get_indicator |
One indicator’s value | |
extract_indicators |
All applicable indicators in one pass | |
extract_indicators_by_position |
CSV-driven extraction | |
audit_rule_gaps |
Audit missing rules across industries/tickers | |
| Dashboard | open_industry_rules_dashboard |
Start the rules web dashboard |
extract_indicators_batchis a Python convenience function (not an MCP tool) — see Concurrency.
Typical Chain
# 1. Resolve company → 2. find latest annual → 3. pull MD&A → 4. LLM-extract revenue table
co = get_company("600519")
# {"stock_code": "600519", "name": "贵州茅台", "org_id": "gssh0600519", "exchange": "sse", ...}
filings = list_filings("600519", form="年度报告", year=2023, limit=3)
# [{"announcement_id": "1219730876", "pdf_url": "http://static.cninfo.com.cn/.../*.PDF", ...}]
sec = get_section("600519", year=2023, section="管理层讨论与分析")
# {"text": "<full MD&A body>", "pdf_url": "...", "outline_entry": {...}, ...}
records = ai_extract(
text=sec["text"],
schema={"type": "object", "properties": {
"segment": {"type": "string"},
"revenue_2023": {"type": "string"},
}, "required": ["segment", "revenue_2023"]},
)
# {"records": [{"segment": "茅台酒", "revenue_2023": "139,989,000,000"}, ...]}
Special Report Types
CNINFO exposes dozens of disclosure categories beyond the four periodic reports (招股说明书, 增发, 业绩预告, 收购报告书, 股权激励, …). Browse the catalog, then list or retrieve by category:
catalog = list_report_types()
# {"groups": [{"name": "定期报告", "categories": [...]}, {"name": "融资", ...}, ...], "count": 26}
list_report_types(group="融资")
# {"group": "融资", "categories": [{name: "首发", code: "category_sf_szsh", ...}, ...], "count": 6}
filings = list_filings("600519", category="首发", limit=3)
sec = get_special_report("600519", category="首发", section="募集资金运用")
三大报表 (Three Major Financial Statements)
get_financials returns akshare’s structured numeric tables. get_financial_statements
pulls the three major statement sections as text from the PDF:
stmts = get_financial_statements("600519", year=2023)
# {
# "stock_code": "600519", "company_name": "贵州茅台", "year": 2023,
# "form": "年度报告", "pdf_url": "...", "cached": False,
# "statements": {
# "income_statement": {"title": "2、 合并利润表", "outline_entry": {...}, "char_count": 4521, "text": "..."},
# "balance_sheet": {"title": "1、 合并资产负债表", ...},
# "cashflow": {"title": "3、 合并现金流量表", ...},
# },
# "missing": [],
# }
Report Cache
Every report fetch goes through an on-disk cache under .cache/reports/.
First fetch downloads PDF + extracts text + outline; subsequent fetches read from disk.
list_cache()
# {"cache_dir": ".../.cache/reports", "count": 2, "entries": [...]}
clear_cache() # evict everything
clear_cache(stock_code="600519") # evict one company
clear_cache(stock_code="600519", year=2023) # evict one company + year
Indicators
The indicator engine profiles each company, filters applicable rules, and routes each indicator to akshare / report-section / computed / external.
# Preview → pull one → pull all → CSV
list_indicators(company="工商银行") # rules applicable to 工商银行
get_indicator("资本充足率", "工商银行", 2023) # one value
extract_indicators("工商银行", 2023) # all applicable, one PDF fetch
extract_indicators("工商银行", 2023,
indicators=["资本充足率","不良率"]) # subset
extract_indicators("工商银行", 2023, extractor_mode="python") # LLM-free
extract_indicators_by_position("工商银行", 2023) # CSV-driven
Multi-form support (年度报告 / 半年度报告 / 第一季度报告 / 第三季度报告):
extract_indicators_by_position("工商银行", 2023, form="第一季度报告")
extract_indicators("贵州茅台", 2023, form="半年度报告")
Concurrency
extract_indicators("工商银行", 2023, concurrency=4) # explicit cap
extract_indicators("工商银行", 2023, concurrency=1) # sequential
# Batch: multi-company concurrent extraction
extract_indicators_batch([("601398", 2023), ("600519", 2023)],
concurrency=2, extract_concurrency=4)
# → {"results": {"601398_2023": {...}, ...}, "failures": [...], "concurrency": 2}
Section Cache
LLM responses are persisted to disk, keyed by (pdf_url, section_key, period, rules_hash).
Subsequent runs reuse cached records. Set LLM_SECTION_CACHE=off to disable.
Standalone CLIs
# Full engine extraction
python scripts/extract_indicators.py 601398 --year 2023 \
[--rules indicator_rules.json] [--extractor auto|llm|python] \
[--indicators 资本充足率,不良率] [--out-dir ./out]
# CSV-driven extraction
python scripts/extract_indicators_by_position.py 601398 --year 2023 \
[--csv docs/indicators_position.csv] [--extractor auto|llm|python] \
[--form 年度报告|半年度报告|第一季度报告|第三季度报告]
# Multi-year extraction
python scripts/extract_indicators_multiyear.py 601398 2023 2024
# Industry rules dashboard
python scripts/industry_rules_dashboard.py [port]
# or: fd-cn-report
# Generate industry rules from real reports
python scripts/generate_rules_from_real_reports.py
# Check industry coverage
python scripts/check_industry_coverage.py
# Seed industry rules
python scripts/seed_industry_rules.py
HK Stock Support
get_hk_company("00700") # → 腾讯控股
list_hk_filings("00700", year=2023) # → HKEX filings
get_hk_financials("00700") # → financial statements
get_hk_section("00700", year=2023, section="管理层讨论与分析")
Official-website datasources (SSE / SZSE / BSE)
Direct, primary-source disclosure paths complementing CNINFO. Each exchange
client resolves a 6-digit ticker locally (no network) and lists disclosures
from the exchange’s own site. BSE falls back to CNINFO when its own API is
thin, tagging each row with source: "bse" | "cninfo". Section extraction
reuses the same outline pipeline as CNINFO/HK.
# SSE (上交所) - 600/601/603/605/688/900 codes
get_sse_company("600519")
list_sse_filings("600519", year=2023) # -> sse.com.cn disclosures
get_sse_section("600519", year=2023, section="管理层讨论与分析")
get_sse_interaction("600519") # -> 上证e互动 Q&A
# SZSE (深交所) - 000/001/002/003/300/301 codes
get_szse_company("000001")
list_szse_filings("000001", year=2023) # -> szse.cn disclosures
get_szse_section("000001", year=2023, section="管理层讨论与分析")
get_szse_interaction("000001") # -> 互动易 Q&A (irm.cninfo.com.cn)
# BSE (北交所) - 430xxx / 83xxxx / 87xxxx / 88xxxx / 920xxx codes
get_bse_company("835185")
list_bse_filings("835185", year=2023) # -> BSE-native, else CNINFO fallback
get_bse_section("835185", year=2023, section="管理层讨论与分析") # result carries `source`
Endpoints are undocumented. SSE (
query.sse.com.cn), SZSE (www.szse.cn/api), BSE (www.bse.cn), and the Q&A hosts (sns.sseinfo.com,irm.cninfo.com.cn) expose no official API contract and shift over time. Clients are thin, retry 429/5xx, and bypass proxy env (trust_env=False). RunCNREPORT_SELFCHECK_LIVE=1 uv run python selfcheck.pyto ping each endpoint - 4xx/5xx are flagged per source without failing the suite. Name-fragment resolution is not supported by these clients; resolve the name viaget_company(CNINFO) first, then pass the 6-digit code.
CSRC regulatory data (证监会)
CSRC regulatory data CNINFO/exchanges do not cover: announcements, IPO / 并购重组
review status, and enforcement. HTML-parsed with lxml; endpoints undocumented.
list_csrc_filings(begin_date="2024-01-01") # -> regulatory announcements
get_csrc_ipo_review("贵州茅台") # -> IPO review status row
get_csrc_merger_review("某科技股份公司") # -> M&A review status row
list_csrc_enforcement() # -> administrative-penalty actions
Ministry statistics (部级部门)
Structured economic/financial statistics from ministry-level departments,
complementing fd-cn-gov’s catalog-archive scraping with data queries. NBS
has a JSON API; the others publish HTML tables. Results are TTL-cached under
.cache/stats/. Base URLs reuse fd-cn-gov’s registry when importable.
list_ministries() # -> [{id, label, en, transport, base}, ...]
get_nbs_stat("A0201") # -> NBS GDP series {period: value} (dbcode="hgnd" annual)
get_ministry_stat("gacc") # -> GACC trade page parsed into HTML tables
get_ministry_stat("pboc", limit=20) # -> PBoC monetary tables
# Supported ids: nbs mof pboc safe gacc nfra
Ministry stat-page paths and CSRC URLs are best-guess and undocumented - verify live via
CNREPORT_SELFCHECK_LIVE=1 uv run python selfcheck.py. Corrections are one-line edits inministry_stats_client._MINISTRIES/csrc_client._URLS.
Setup
uv sync # installs akshare, pypdf, fastmcp, ...
uv run python server.py # FastMCP over stdio
Self-check (no network):
uv run python selfcheck.py # DB + outline + company API + special reports
uv run python selfcheck_cache.py # report cache + three-statements extraction
Tests (offline):
uv run --with pytest python -m pytest test_cnreport.py -v -p no:logfire
Configuration
CNINFO and akshare are keyless. Other tools need env vars in .env:
| Var | Used by | Required? |
|---|---|---|
LLM_API_KEY, LLM_BASE_URL, LLM_MODEL |
ai_extract, rule generation |
Yes for AI |
ES_URL (+ optional ES_API_KEY or ES_USERNAME/ES_PASSWORD) |
index_records, search_reports, delete_index |
Yes for ES |
DAAS_DATABASE_URL |
provenance writes, rules storage | Defaults to daas.db |
CNREPORT_CACHE_DIR |
report cache | Defaults to .cache/reports/ |
CNREPORT_SAVE_DIR |
user-visible PDF save directory | Optional |
MINIO_UPLOAD_ENABLED (+ MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET / MINIO_SECURE) |
PDF upload to MinIO object store | Optional |
Architecture
fd-cn-report/
├── server.py # FastMCP server (@app.tool registrations)
├── cninfo_client.py # CNINFO API (keyless lookup + query)
├── hk_stock_client.py # HK stock API (akshare + HKEX)
├── financials_client.py # akshare financial statements (lazy)
├── cnreport_tools.py # Pure helpers (outline, extract, LLM, ES)
├── report_cache.py # On-disk PDF + text + outline cache
├── llm_section_cache.py # LLM response section cache
│
├── indicators_client.py # Rules engine (load, profile, route, extract)
├── indicators_extractors.py # Pluggable Python extractors
├── indicators_models.py # Pydantic extraction models
├── indicators_csv_migration.py # CSV → JSON rule migration
├── report_section_map.py # Section alias expansion + matching
│
├── rules_db.py # Rules database (SQLite via SQLAlchemy)
├── rules_models.py # Pydantic rule models
├── rules_skills.py # LLM rule generation + validation
├── cnreport_models.py # ORM models (LlmRule, ScriptRule)
├── cnreport_database.py # Database connection management
│
├── industry_taxonomy.py # 申万 L1 industry taxonomy
├── industry_coverage.py # Coverage checks for industry rules
│
├── docs/
│ ├── industry_taxonomy.json # 31 industry taxonomy
│ ├── industry_indicator_baseline.json # Baseline indicators per industry
│ ├── indicators_position.csv # Indicator catalog (CSV source)
│ └── indicators-methodology.md # Rendered methodology
│
├── scripts/
│ ├── industry_rules_dashboard.py # Web dashboard (CLI + MCP)
│ ├── generate_rules_from_real_reports.py # Per-industry rule generation
│ ├── generate_all_industry_rules.py # LLM rule generation
│ ├── seed_industry_rules.py # Universal rule seeding
│ ├── seed_missing_industry_rules.py # Industry-specific seeding
│ ├── extract_indicators.py # Standalone extraction CLI
│ ├── extract_indicators_by_position.py # CSV-driven extraction CLI
│ ├── extract_indicators_multiyear.py # Multi-year batch extraction
│ ├── check_industry_coverage.py # Coverage validation
│ ├── migrate_indicators_csv.py # CSV → DB migration
│ └── rules_dashboard.py # indicator_rules.json editor
│
└── .cache/reports/ # Downloaded PDFs + extracted text + outlines
Adding a New Industry
-
Add to
docs/industry_taxonomy.json:{"industry": "801xxx", "label": "行业名称", "report_kinds": ["annual-report", "interim-report", "quarterly-report"]} -
Add baseline indicators to
docs/industry_indicator_baseline.json:{"cn/801xxx/listed/annual-report": ["资产总计", "营业收入", "净利润", ...]} -
Add representative company to
scripts/generate_rules_from_real_reports.py:"801xxx": ("600xxx", "代表公司", "行业名称"), -
Define industry-specific sections:
"801xxx": [("section_name", "keyword", "keyword2", "guidance"), ...], -
Generate rules:
python scripts/generate_rules_from_real_reports.py --industry 801xxx
License
MIT