feat: 新增越南市场数据抓取、分析与报告功能,并优化香港市场数据抓取逻辑及更新部分JP数据。
This commit is contained in:
parent
880df10484
commit
daf5808f05
15
README.md
15
README.md
@ -38,14 +38,15 @@
|
|||||||
|
|
||||||
## 如何运行
|
## 如何运行
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
- `<MARKET>`: 必填参数,指定目标市场。
|
||||||
|
|
||||||
使用以下命令来运行程序:
|
使用以下命令来运行程序:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py <MARKET> <SYMBOL>
|
python run_fetcher.py <MARKET> <SYMBOL>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 参数说明
|
|
||||||
|
|
||||||
- `<MARKET>`: 必填参数,指定目标市场。
|
- `<MARKET>`: 必填参数,指定目标市场。
|
||||||
- `CN`: 中国A股市场
|
- `CN`: 中国A股市场
|
||||||
- `HK`: 中国香港市场
|
- `HK`: 中国香港市场
|
||||||
@ -62,20 +63,20 @@ python main.py <MARKET> <SYMBOL>
|
|||||||
|
|
||||||
- 分析贵州茅台 (A股):
|
- 分析贵州茅台 (A股):
|
||||||
```bash
|
```bash
|
||||||
python main.py CN 600519.SH
|
python run_fetcher.py CN 600519.SH
|
||||||
```
|
```
|
||||||
- 分析苹果公司 (美股):
|
- 分析苹果公司 (美股):
|
||||||
```bash
|
```bash
|
||||||
python main.py US AAPL
|
python run_fetcher.py US AAPL
|
||||||
```
|
```
|
||||||
If running just `python main.py` without arguments, it executes built-in default test cases (Kweichow Moutai and Apple Inc.).
|
If running just `python run_fetcher.py` without arguments, it executes built-in default test cases (Kweichow Moutai and Apple Inc.).
|
||||||
|
|
||||||
## 深度分析自动化 (Automated Deep Analysis)
|
## 深度分析自动化 (Automated Deep Analysis)
|
||||||
|
|
||||||
项目提供了一个名为 `stock_analysis.py` 的脚本,能够全自动完成从数据获取到AI深度分析报告生成的全流程。
|
项目提供了一个名为 `stock_analysis.py` 的脚本,能够全自动完成从数据获取到AI深度分析报告生成的全流程。
|
||||||
|
|
||||||
### 功能特点
|
### 功能特点
|
||||||
1. **全自动流程**:一键调用 `main.py` 获取数据 -> 识别公司信息 -> 初始化报告 -> 调用 LLM 进行分章节深度分析。
|
1. **全自动流程**:一键调用 `run_fetcher.py` 获取数据 -> 识别公司信息 -> 初始化报告 -> 调用 LLM 进行分章节深度分析。
|
||||||
2. **AI 驱动**:利用大语言模型(如 Gemini/GPT-4)根据预设的专业提示词(`prompts.yaml`),对公司简介、基本面、内部人动向、看涨/看跌逻辑进行深度解读。
|
2. **AI 驱动**:利用大语言模型(如 Gemini/GPT-4)根据预设的专业提示词(`prompts.yaml`),对公司简介、基本面、内部人动向、看涨/看跌逻辑进行深度解读。
|
||||||
3. **结构化报告**:生成的 Markdown 报告保存在 `reports/` 目录下,包含详细的文字分析和指向可视化财务图表的链接。
|
3. **结构化报告**:生成的 Markdown 报告保存在 `reports/` 目录下,包含详细的文字分析和指向可视化财务图表的链接。
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,22 @@ async def init_db():
|
|||||||
if "duplicate column" not in str(e).lower() and "already exists" not in str(e).lower():
|
if "duplicate column" not in str(e).lower() and "already exists" not in str(e).lower():
|
||||||
print(f"Migration check: {e}")
|
print(f"Migration check: {e}")
|
||||||
|
|
||||||
|
# Migration: Add token columns to report_sections
|
||||||
|
columns_to_add = [
|
||||||
|
("prompt_tokens", "INTEGER DEFAULT 0"),
|
||||||
|
("completion_tokens", "INTEGER DEFAULT 0"),
|
||||||
|
("total_tokens", "INTEGER DEFAULT 0")
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_name, col_type in columns_to_add:
|
||||||
|
try:
|
||||||
|
await conn.execute(text(f"ALTER TABLE report_sections ADD COLUMN {col_name} {col_type}"))
|
||||||
|
print(f"Migration: Added {col_name} to report_sections table")
|
||||||
|
except Exception as e:
|
||||||
|
# SQLite error for duplicate column usually contains "duplicate column name"
|
||||||
|
if "duplicate column" not in str(e).lower() and "already exists" not in str(e).lower():
|
||||||
|
print(f"Migration check for {col_name}: {e}")
|
||||||
|
|
||||||
async def get_db():
|
async def get_db():
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
yield session
|
yield session
|
||||||
|
|||||||
@ -33,6 +33,9 @@ class ReportSection(Base):
|
|||||||
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id"))
|
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id"))
|
||||||
section_name: Mapped[str] = mapped_column(String(50)) # e.g. company_profile, fundamental_analysis
|
section_name: Mapped[str] = mapped_column(String(50)) # e.g. company_profile, fundamental_analysis
|
||||||
content: Mapped[str] = mapped_column(Text) # Markdown content
|
content: Mapped[str] = mapped_column(Text) # Markdown content
|
||||||
|
total_tokens: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
|
||||||
|
prompt_tokens: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
|
||||||
|
completion_tokens: Mapped[int] = mapped_column(Integer, nullable=True, default=0)
|
||||||
created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
report: Mapped["Report"] = relationship(back_populates="sections")
|
report: Mapped["Report"] = relationship(back_populates="sections")
|
||||||
|
|||||||
@ -25,14 +25,21 @@ async def search_stock(query: str, api_key: str, model: str = "gemini-2.0-flash"
|
|||||||
4. 例如:"茅台" = "贵州茅台酒股份有限公司" (600519.SH)
|
4. 例如:"茅台" = "贵州茅台酒股份有限公司" (600519.SH)
|
||||||
|
|
||||||
请返回一个 JSON 数组,包含所有匹配的公司。每个对象包含以下字段:
|
请返回一个 JSON 数组,包含所有匹配的公司。每个对象包含以下字段:
|
||||||
- 'market': 'CN' (中国), 'US' (美国), 'HK' (香港), 或 'JP' (日本) 之一
|
- 'market': 'CN' (中国), 'US' (美国), 'HK' (香港), 'JP' (日本), 或 'VN' (越南) 之一
|
||||||
- 'symbol': 完整的股票代码 (例如 'AAPL', '600519.SH', '00700.HK', '688778.SH', '2503.T')
|
- 'symbol': 完整的股票代码 (例如 'AAPL', '600519.SH', '00700.HK', '688778.SH', '2503.T', 'SAB.HM')
|
||||||
- 'company_name': 公司的中文简称(如果有的话,优先使用中文;如果只有英文名,则使用英文名)
|
- 'company_name': 公司的中文简称(如果有的话,优先使用中文;如果只有英文名,则使用英文名)
|
||||||
|
|
||||||
**匹配规则**:
|
**匹配规则**:
|
||||||
- 如果查询词与某公司简称、全称或股票代码完全匹配,返回该公司
|
- 如果查询词与某公司简称、全称或股票代码完全匹配,返回该公司
|
||||||
- 如果有多个可能的匹配,返回所有相关公司
|
- 如果有多个可能的匹配,返回所有相关公司
|
||||||
- 如果公司在中国但用户没有指定市场,默认为 CN(上海/深圳/北京交易所)
|
- 如果公司在中国但用户没有指定市场,默认为 CN(上海/深圳/北京交易所)
|
||||||
|
- **越南股票后缀规则(iFinD数据源)**:
|
||||||
|
- 胡志明交易所 (HOSE) -> 后缀 **.HM** (例如: VNM.HM, SAB.HM, VCB.HM)
|
||||||
|
- 河内交易所 (HNX) -> 后缀 **.HN** (例如: PVS.HN, SHS.HN)
|
||||||
|
- UPCoM 市场 -> iFinD 通常也使用 .HN 或 .HM,或者特定的 UPCoM 后缀,但绝不要使用 .VN (这是 Bloomberg 格式)
|
||||||
|
- 示例:MCH (Masan Consumer) -> MCH.HN 或 MCH.HM (UPCoM 在 iFinD 中可能归类不一,但 MCH.HN 是常见尝试,MCH.HM 也可以尝试。请根据搜索结果确认 iFinD 使用哪一个。**强烈倾向于使用 .HM 或 .HN,严禁使用 .VN**)
|
||||||
|
- MCH 具体案例:用户指出应为 MCH.HM (或 MCH.HN),绝非 MCH.VN。请只返回 .HM 或 .HN。
|
||||||
|
- 如果不确定是 HM 还是 HN,优先返回 .HM。
|
||||||
- 如果完全没找到匹配,返回 {{ "error": "未找到相关公司" }}
|
- 如果完全没找到匹配,返回 {{ "error": "未找到相关公司" }}
|
||||||
|
|
||||||
示例响应(单个结果):
|
示例响应(单个结果):
|
||||||
@ -52,9 +59,9 @@ async def search_stock(query: str, api_key: str, model: str = "gemini-2.0-flash"
|
|||||||
"company_name": "腾讯控股"
|
"company_name": "腾讯控股"
|
||||||
}},
|
}},
|
||||||
{{
|
{{
|
||||||
"market": "US",
|
"market": "VN",
|
||||||
"symbol": "TCEHY",
|
"symbol": "VNM.HM",
|
||||||
"company_name": "Tencent Holdings ADR"
|
"company_name": "Vinamilk"
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -128,9 +135,9 @@ async def run_analysis_task(report_id: int, market: str, symbol: str, api_key: s
|
|||||||
|
|
||||||
company_name_for_prompt = report.company_name
|
company_name_for_prompt = report.company_name
|
||||||
|
|
||||||
# 2. Run Main Data Fetching Script (main.py)
|
# 2. Run Main Data Fetching Script (run_fetcher.py)
|
||||||
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))
|
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))
|
||||||
cmd = [sys.executable, "main.py", market, symbol]
|
cmd = [sys.executable, "run_fetcher.py", market, symbol]
|
||||||
|
|
||||||
print(f"Executing data fetch command: {cmd} in {root_dir}")
|
print(f"Executing data fetch command: {cmd} in {root_dir}")
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
|||||||
@ -60,10 +60,22 @@ async def call_llm(api_key: str, model_name: str, system_prompt: str, user_promp
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = await asyncio.to_thread(run_sync)
|
response = await asyncio.to_thread(run_sync)
|
||||||
return response.text
|
usage = response.usage_metadata
|
||||||
|
prompt_tokens = usage.prompt_token_count if usage else 0
|
||||||
|
completion_tokens = usage.candidates_token_count if usage else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"text": response.text,
|
||||||
|
"prompt_tokens": prompt_tokens,
|
||||||
|
"completion_tokens": completion_tokens
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"API Call Failed: {e}")
|
print(f"API Call Failed: {e}")
|
||||||
return f"\n\nError generating section: {e}\n\n"
|
return {
|
||||||
|
"text": f"\n\nError generating section: {e}\n\n",
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0
|
||||||
|
}
|
||||||
|
|
||||||
async def process_analysis_steps(report_id: int, company_name: str, symbol: str, market: str, db: AsyncSession, api_key: str):
|
async def process_analysis_steps(report_id: int, company_name: str, symbol: str, market: str, db: AsyncSession, api_key: str):
|
||||||
# 1. Load Prompts
|
# 1. Load Prompts
|
||||||
@ -71,7 +83,7 @@ async def process_analysis_steps(report_id: int, company_name: str, symbol: str,
|
|||||||
prompt_dir = os.path.join(root_dir, "Prompt")
|
prompt_dir = os.path.join(root_dir, "Prompt")
|
||||||
prompts = await load_prompts(db, prompt_dir)
|
prompts = await load_prompts(db, prompt_dir)
|
||||||
|
|
||||||
# 2. Read Data Context (report.md generated by main.py)
|
# 2. Read Data Context (report.md generated by run_fetcher.py)
|
||||||
base_dir = os.path.join(root_dir, "data", market)
|
base_dir = os.path.join(root_dir, "data", market)
|
||||||
symbol_dir = os.path.join(base_dir, symbol)
|
symbol_dir = os.path.join(base_dir, symbol)
|
||||||
|
|
||||||
@ -82,7 +94,7 @@ async def process_analysis_steps(report_id: int, company_name: str, symbol: str,
|
|||||||
|
|
||||||
data_path = os.path.join(symbol_dir, "report.md")
|
data_path = os.path.join(symbol_dir, "report.md")
|
||||||
if not os.path.exists(data_path):
|
if not os.path.exists(data_path):
|
||||||
# If report.md is missing, maybe main.py failed or output structure changed.
|
# If report.md is missing, maybe run_fetcher.py failed or output structure changed.
|
||||||
# We try to proceed or fail.
|
# We try to proceed or fail.
|
||||||
print(f"Warning: {data_path} not found.")
|
print(f"Warning: {data_path} not found.")
|
||||||
data_context = "No financial data available."
|
data_context = "No financial data available."
|
||||||
@ -127,9 +139,9 @@ async def process_analysis_steps(report_id: int, company_name: str, symbol: str,
|
|||||||
if key == "bearish_analysis" and csv_context:
|
if key == "bearish_analysis" and csv_context:
|
||||||
current_data_context += csv_context
|
current_data_context += csv_context
|
||||||
|
|
||||||
content = await call_llm(api_key, model_name, system_content, user_content, current_data_context, enable_search=True)
|
result = await call_llm(api_key, model_name, system_content, user_content, current_data_context, enable_search=True)
|
||||||
|
|
||||||
return (key, content)
|
return (key, result)
|
||||||
|
|
||||||
# Run all sections concurrently
|
# Run all sections concurrently
|
||||||
print(f"Starting concurrent analysis with {len(steps)} sections...")
|
print(f"Starting concurrent analysis with {len(steps)} sections...")
|
||||||
@ -139,11 +151,14 @@ async def process_analysis_steps(report_id: int, company_name: str, symbol: str,
|
|||||||
for result in results:
|
for result in results:
|
||||||
if result is None:
|
if result is None:
|
||||||
continue
|
continue
|
||||||
key, content = result
|
key, result_data = result
|
||||||
section = ReportSection(
|
section = ReportSection(
|
||||||
report_id=report_id,
|
report_id=report_id,
|
||||||
section_name=key,
|
section_name=key,
|
||||||
content=content
|
content=result_data["text"],
|
||||||
|
prompt_tokens=result_data["prompt_tokens"],
|
||||||
|
completion_tokens=result_data["completion_tokens"],
|
||||||
|
total_tokens=result_data["prompt_tokens"] + result_data["completion_tokens"]
|
||||||
)
|
)
|
||||||
db.add(section)
|
db.add(section)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
cash_equi_short_term_inve_oas,accou_and_notes_recei_oas,inventories_oas,ppe_net_oas,long_term_inv_and_receiv_oas,goodwill_and_intasset_oas,short_term_debt_oas,short_term_borrowings_oas,account_and_note_payable_oas,advance_from_cust_current_oas,defer_revenue_current_oas,long_term_debt_oas,long_term_borrowings_oas,total_assets_oas,equity_attri_to_companyowner_oas,prepaid_expenses_current_oas,end_date
|
|
||||||
380444000000.0,51315000000.0,435000000.0,172648000000.0,706700000000.0,215832000000.0,63974000000.0,58577000000.0,143381000000.0,998000000.0,120908000000.0,335632000000.0,322304000000.0,2013310000000.0,1114639000000.0,26893000000.0,20250630
|
|
||||||
343159000000.0,48203000000.0,440000000.0,133283000000.0,589410000000.0,196127000000.0,58485000000.0,48526000000.0,127335000000.0,1042000000.0,100097000000.0,291004000000.0,277107000000.0,1780995000000.0,973548000000.0,31265000000.0,20241231
|
|
||||||
379155000000.0,46606000000.0,456000000.0,104458000000.0,460591000000.0,177727000000.0,47691000000.0,25561000000.0,115109000000.0,669000000.0,86168000000.0,309388000000.0,292920000000.0,1577246000000.0,808591000000.0,27824000000.0,20231231
|
|
||||||
290756000000.0,45467000000.0,2333000000.0,103777000000.0,431451000000.0,161802000000.0,17934000000.0,5981000000.0,102827000000.0,816000000.0,82216000000.0,330761000000.0,312337000000.0,1578131000000.0,721391000000.0,24393000000.0,20221231
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
corp_cn_name,accounting_date,ipo_date
|
|
||||||
腾讯控股有限公司,1231,20040616
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
net_cash_flows_from_oa_oas,purchase_of_ppe_and_ia_oas,dividends_paid_oas,end_date
|
|
||||||
151265000000.0,57457000000.0,37535000000.0,20250630
|
|
||||||
258521000000.0,96048000000.0,28859000000.0,20241231
|
|
||||||
221962000000.0,47407000000.0,20983000000.0,20231231
|
|
||||||
146091000000.0,50850000000.0,12952000000.0,20221231
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
date_str,dividends
|
|
||||||
20241231,38104168998.825
|
|
||||||
20231231,29163521377.441
|
|
||||||
20221231,20700985117.366
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
date_str,employee_count
|
|
||||||
20261231,111221.0
|
|
||||||
20251231,111221.0
|
|
||||||
20241231,110558.0
|
|
||||||
20231231,105417.0
|
|
||||||
20221231,108436.0
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
date_str,PE,PB,MarketCap,Price
|
|
||||||
20250630,0.0,0.0,4204320544234.2,467.83035
|
|
||||||
20241231,0.0,0.0,3562280981362.7,388.01076
|
|
||||||
20231231,0.0,0.0,2523103788380.7,266.066192
|
|
||||||
20221231,0.0,0.0,2854854121108.1,298.35218
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
revenue_oas,gross_profit_oas,sga_expenses_oas,selling_marketing_expenses_oas,ga_expenses_oas,income_tax_expense_oas,net_income_attri_to_common_sh_oas,operating_income_oas,end_date
|
|
||||||
364526000000.0,205506000000.0,82861000000.0,17276000000.0,65585000000.0,25068000000.0,103449000000.0,117670000000.0,20250630
|
|
||||||
660257000000.0,349246000000.0,149149000000.0,36388000000.0,112761000000.0,45018000000.0,194073000000.0,208099000000.0,20241231
|
|
||||||
609015000000.0,293109000000.0,137736000000.0,34211000000.0,103525000000.0,43276000000.0,115216000000.0,160074000000.0,20231231
|
|
||||||
554552000000.0,238746000000.0,135925000000.0,29229000000.0,106696000000.0,21516000000.0,188243000000.0,227114000000.0,20221231
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
date_str,repurchases
|
|
||||||
20261231,1271296551.4
|
|
||||||
20251231,80610335058.2
|
|
||||||
20241231,112003383926.44
|
|
||||||
20231231,49432707948.16
|
|
||||||
20221231,33794068650.24
|
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
# 腾讯控股有限公司 (00700.HK) - Financial Report
|
|
||||||
*Report generated on: 2026-01-03*
|
|
||||||
|
|
||||||
| 代码 | 简称 | 上市日期 | 年结日 | 市值(亿) | PE | PB | 股息率(%) |
|
|
||||||
|:---|:---|:---|:---|:---|:---|:---|:---|
|
|
||||||
| 00700.HK | 腾讯控股有限公司 | 2004-06-16 | 1231 | 42043.21 | 20.32 | 3.77 | 0.89% |
|
|
||||||
|
|
||||||
|
|
||||||
## 主要指标
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| ROE | 9.28% | 19.93% | 14.25% | 26.09% |
|
|
||||||
| ROA | 5.14% | 10.90% | 7.30% | 11.93% |
|
|
||||||
| ROCE/ROIC | 6.21% | 12.62% | 10.79% | 16.36% |
|
|
||||||
| 毛利率 | 56.38% | 52.90% | 48.13% | 43.05% |
|
|
||||||
| 净利润率 | 28.38% | 29.39% | 18.92% | 33.95% |
|
|
||||||
| 收入(亿) | 3,645.26 | 6,602.57 | 6,090.15 | 5,545.52 |
|
|
||||||
| 收入增速 | - | 8.41% | 9.82% | - |
|
|
||||||
| 净利润(亿) | 1,034.49 | 1,940.73 | 1,152.16 | 1,882.43 |
|
|
||||||
| 净利润增速 | - | 68.44% | -38.79% | - |
|
|
||||||
| 经营净现金流(亿) | 1,512.65 | 2,585.21 | 2,219.62 | 1,460.91 |
|
|
||||||
| 资本开支(亿) | 574.57 | 960.48 | 474.07 | 508.50 |
|
|
||||||
| 自由现金流(亿) | 938.08 | 1,624.73 | 1,745.55 | 952.41 |
|
|
||||||
| 分红(亿) | 375.35 | 288.59 | 209.83 | 129.52 |
|
|
||||||
| 回购(亿) | - | 1,120.03 | 494.33 | 337.94 |
|
|
||||||
| 总资产(亿) | 20,133.10 | 17,809.95 | 15,772.46 | 15,781.31 |
|
|
||||||
| 净资产(亿) | 11,146.39 | 9,735.48 | 8,085.91 | 7,213.91 |
|
|
||||||
| 商誉(亿) | 2,158.32 | 1,961.27 | 1,777.27 | 1,618.02 |
|
|
||||||
|
|
||||||
|
|
||||||
## 费用指标
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| 销售费用率 | 4.74% | 5.51% | 5.62% | 5.27% |
|
|
||||||
| 管理费用率 | 17.99% | 17.08% | 17.00% | 19.24% |
|
|
||||||
| SG&A比例 | 22.73% | 22.59% | 22.62% | 24.51% |
|
|
||||||
| 研发费用率 | - | - | - | - |
|
|
||||||
| 其他费用率 | 5.27% | 0.91% | 6.59% | -15.40% |
|
|
||||||
| 折旧费用占比 | - | - | - | - |
|
|
||||||
| 所得税率 | 19.51% | 18.83% | 27.30% | 10.26% |
|
|
||||||
|
|
||||||
|
|
||||||
## 资产占比
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| 现金占比 | 18.90% | 19.27% | 24.04% | 18.42% |
|
|
||||||
| 库存占比 | 0.02% | 0.02% | 0.03% | 0.15% |
|
|
||||||
| 应收款占比 | 2.55% | 2.71% | 2.95% | 2.88% |
|
|
||||||
| 预付款占比 | 1.34% | 1.76% | 1.76% | 1.55% |
|
|
||||||
| 固定资产占比 | 8.58% | 7.48% | 6.62% | 6.58% |
|
|
||||||
| 长期投资占比 | 35.10% | 33.09% | 29.20% | 27.34% |
|
|
||||||
| 商誉占比 | 10.72% | 11.01% | 11.27% | 10.25% |
|
|
||||||
| 其他资产占比 | 22.80% | 24.66% | 24.12% | 32.83% |
|
|
||||||
| 应付款占比 | 7.12% | 7.15% | 7.30% | 6.52% |
|
|
||||||
| 预收款占比 | 0.00% | 0.00% | 0.00% | 0.00% |
|
|
||||||
| 短期借款占比 | 6.09% | 6.01% | 4.64% | 1.52% |
|
|
||||||
| 长期借款占比 | 32.68% | 31.90% | 38.19% | 40.75% |
|
|
||||||
| 运营资产占比 | -3.22% | -2.66% | -2.55% | -1.94% |
|
|
||||||
| 有息负债率 | 38.77% | 37.91% | 42.83% | 42.27% |
|
|
||||||
|
|
||||||
|
|
||||||
## 周转能力
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| 存货周转天数 | 0 | 0 | 0 | 2 |
|
|
||||||
| 应收款周转天数 | 25 | 26 | 27 | 29 |
|
|
||||||
| 应付款周转天数 | 164 | 149 | 132 | 118 |
|
|
||||||
| 固定资产周转率 | 4.22 | 4.95 | 5.83 | 5.34 |
|
|
||||||
| 总资产周转率 | 0.36 | 0.37 | 0.39 | 0.35 |
|
|
||||||
|
|
||||||
|
|
||||||
## 人均效率
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| 员工人数 | - | 110,558 | 105,417 | 108,436 |
|
|
||||||
| 人均创收(万) | - | 597.20 | 577.72 | 511.41 |
|
|
||||||
| 人均创利(万) | - | 175.54 | 109.30 | 173.60 |
|
|
||||||
| 人均薪酬(万) | - | - | - | - |
|
|
||||||
|
|
||||||
|
|
||||||
## 市场表现
|
|
||||||
| 指标 | 2025H1 | 2024A | 2023A | 2022A |
|
|
||||||
|:---|--:|--:|--:|--:|
|
|
||||||
| 股价 | 467.83 | 388.01 | 266.07 | 298.35 |
|
|
||||||
| 市值(亿) | 42,043 | 35,623 | 25,231 | 28,549 |
|
|
||||||
| PE | 20.32 | 18.36 | 21.90 | 15.17 |
|
|
||||||
| PB | 3.77 | 3.66 | 3.12 | 3.96 |
|
|
||||||
| 股东户数 | - | - | - | - |
|
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
cash_equi_short_term_inve_oas,accou_and_notes_recei_oas,inventories_oas,ppe_net_oas,long_term_inv_and_receiv_oas,goodwill_and_intasset_oas,short_term_debt_oas,short_term_borrowings_oas,account_and_note_payable_oas,contra_liabilities_current_oas,advance_from_cust_current_oas,defer_revenue_current_oas,long_term_debt_oas,long_term_borrowings_oas,total_assets_oas,equity_attri_to_companyowner_oas,prepaid_expenses_current_oas,end_date
|
cash_equi_short_term_inve_oas,accou_and_notes_recei_oas,inventories_oas,ppe_net_oas,long_term_inv_and_receiv_oas,goodwill_and_intasset_oas,short_term_debt_oas,short_term_borrowings_oas,account_and_note_payable_oas,contra_liabilities_current_oas,advance_from_cust_current_oas,defer_revenue_current_oas,long_term_debt_oas,long_term_borrowings_oas,total_assets_oas,equity_attri_to_companyowner_oas,prepaid_expenses_current_oas,end_date
|
||||||
,,,,,,,,,,,,,,,,,20261231
|
|
||||||
,,,,,,,,,,,,,,,,,20251231
|
|
||||||
5899320980.35,22326249202.25,16650772821.2,31263474425.4,4794152048.0,53852622755.049995,3617228537.3,3617228537.3,7518553255.849999,,,,36159437268.15,36159437268.15,155576124604.95,54802733151.25,,20241231
|
5899320980.35,22326249202.25,16650772821.2,31263474425.4,4794152048.0,53852622755.049995,3617228537.3,3617228537.3,7518553255.849999,,,,36159437268.15,36159437268.15,155576124604.95,54802733151.25,,20241231
|
||||||
7070829794.070001,21350738934.77,16675747687.67,29873189030.72,5307593791.54,34970889368.92,5072156415.77,5072156415.77,6915651724.870001,,,,27998809255.25,27998809255.25,144576837566.65,57062250906.689995,,20231231
|
7070829794.070001,21350738934.77,16675747687.67,29873189030.72,5307593791.54,34970889368.92,5072156415.77,5072156415.77,6915651724.870001,,,,27998809255.25,27998809255.25,144576837566.65,57062250906.689995,,20231231
|
||||||
5046190177.88,20369212787.27,15183728587.93,29336618624.86,5409547685.4,25662437929.58,5989276634.969999,5989276634.969999,6303080634.48,,,,21383987001.46,21383987001.46,133028563816.29,51281444590.26,,20221231
|
5046190177.88,20369212787.27,15183728587.93,29336618624.86,5409547685.4,25662437929.58,5989276634.969999,5989276634.969999,6303080634.48,,,,21383987001.46,21383987001.46,133028563816.29,51281444590.26,,20221231
|
||||||
|
8709165023.76,20609942061.24,13679704695.48,29539550251.08,6497706183.72,25484093179.92,5616320844.24,5616320844.24,5452427104.8,,,,24897794036.4,24897794036.4,136777293387.96,49476819729.48,,20211231
|
||||||
|
10725243245.05,22595254368.58,13732598542.25,33002436277.529995,6686243673.8,28770955850.0,15747071314.71,15747071314.71,6078069930.14,,,,24889089960.7,24889089960.7,155512580861.81,53026073056.08,,20201231
|
||||||
|
|||||||
|
@ -1,6 +1,6 @@
|
|||||||
net_cash_flows_from_oa_oas,purchase_of_ppe_and_ia_oas,dividends_paid_oas,end_date
|
net_cash_flows_from_oa_oas,purchase_of_ppe_and_ia_oas,dividends_paid_oas,end_date
|
||||||
,,,20261231
|
|
||||||
,,,20251231
|
|
||||||
11263845394.2,8376036701.2,2704873943.8,20241231
|
11263845394.2,8376036701.2,2704873943.8,20241231
|
||||||
10238024262.94,5734031186.9,2896993175.0,20231231
|
10238024262.94,5734031186.9,2896993175.0,20231231
|
||||||
7093529728.46,5153093891.57,2814032263.74,20221231
|
7093529728.46,5153093891.57,2814032263.74,20221231
|
||||||
|
12134499912.36,4777098580.2,2998115590.08,20211231
|
||||||
|
10423243057.93,5882300964.62,3498421765.62,20201231
|
||||||
|
|||||||
|
@ -1,6 +1,6 @@
|
|||||||
date_str,PE,PB,MarketCap,Price
|
date_str,PE,PB,MarketCap,Price
|
||||||
20261231,0.0,0.0,95997084050.16,105.02963244
|
|
||||||
20251231,0.0,0.0,95997084050.16,105.02963244
|
|
||||||
20241231,0.0,0.0,86865526677.3,95.03886945
|
20241231,0.0,0.0,86865526677.3,95.03886945
|
||||||
20231231,0.0,0.0,95138465046.76,104.09022434
|
20231231,0.0,0.0,95138465046.76,104.09022434
|
||||||
20221231,0.0,0.0,96179539188.82,105.22925513
|
20221231,0.0,0.0,96179539188.82,105.22925513
|
||||||
|
20211231,0.0,0.0,93409361034.96,102.19842564
|
||||||
|
20201231,0.0,0.0,0.0,0.0
|
||||||
|
|||||||
|
@ -1,6 +1,6 @@
|
|||||||
revenue_oas,gross_profit_oas,sga_expenses_oas,selling_marketing_expenses_oas,ga_expenses_oas,rd_expenses_oas,income_tax_expense_oas,net_income_attri_to_common_sh_oas,operating_income_oas,end_date
|
revenue_oas,gross_profit_oas,sga_expenses_oas,selling_marketing_expenses_oas,ga_expenses_oas,rd_expenses_oas,income_tax_expense_oas,net_income_attri_to_common_sh_oas,operating_income_oas,end_date
|
||||||
,,,,,,,,,20261231
|
|
||||||
,,,,,,,,,20251231
|
|
||||||
108461428374.25,49442244062.75,34274708414.449997,7858262714.049999,,5382196355.9,2500510225.5,2700142872.7,5813651487.0,20241231
|
108461428374.25,49442244062.75,34274708414.449997,7858262714.049999,,5382196355.9,2500510225.5,2700142872.7,5813651487.0,20241231
|
||||||
107536033978.57,48541816110.34,34112774799.24,8351653454.849999,,4277221488.55,2348378241.39,5677955475.53,7572185952.06,20231231
|
107536033978.57,48541816110.34,34112774799.24,8351653454.849999,,4277221488.55,2348378241.39,5677955475.53,7572185952.06,20231231
|
||||||
104102553826.44,47393090179.79,33504032019.72,8687928238.56,,3886365990.93,2491542010.45,5808644417.809999,6070906489.7699995,20221231
|
104102553826.44,47393090179.79,33504032019.72,8687928238.56,,3886365990.93,2491542010.45,5808644417.809999,6070906489.7699995,20221231
|
||||||
|
100791329828.4,45529592285.04,32524275468.12,9023230136.88,,3851779537.44,1725974819.16,3308307454.8,3767232058.08,20211231
|
||||||
|
116952038544.15,50831829234.21,36502312399.16,9503078333.689999,,4078520115.0,1562420984.83,4548656503.45,6507863747.53,20201231
|
||||||
|
|||||||
|
@ -205,7 +205,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="report-container">
|
<div class="report-container">
|
||||||
<h1>麒麟控股株式会社 (2503.T) - Financial Report</h1>
|
<h1>麒麟控股株式会社 (2503.T) - Financial Report</h1>
|
||||||
<p><em>Report generated on: 2026-01-03</em></p>
|
<p><em>Report generated on: 2026-01-07</em></p>
|
||||||
|
|
||||||
<table class="company-table">
|
<table class="company-table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -236,67 +236,67 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>指标</th>
|
<th>指标</th>
|
||||||
<th>2026A</th><th>2025A</th><th>2024A</th><th>2023A</th><th>2022A</th>
|
<th>2024A</th><th>2023A</th><th>2022A</th><th>2021A</th><th>2020A</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="section-row"><td class="section-label">主要指标</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">主要指标</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">ROE</td><td>-</td><td>-</td><td>4.93%</td><td>9.95%</td><td>11.33%</td></tr>
|
<tr><td class="metric-name">ROE</td><td>4.93%</td><td>9.95%</td><td>11.33%</td><td>6.69%</td><td>8.58%</td></tr>
|
||||||
<tr><td class="metric-name">ROA</td><td>-</td><td>-</td><td>1.74%</td><td>3.93%</td><td>4.37%</td></tr>
|
<tr><td class="metric-name">ROA</td><td>1.74%</td><td>3.93%</td><td>4.37%</td><td>2.42%</td><td>2.92%</td></tr>
|
||||||
<tr><td class="metric-name">ROCE/ROIC</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">ROCE/ROIC</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">毛利率</td><td>-</td><td>-</td><td>45.59%</td><td>45.14%</td><td>45.53%</td></tr>
|
<tr><td class="metric-name">毛利率</td><td>45.59%</td><td>45.14%</td><td>45.53%</td><td>45.17%</td><td>43.46%</td></tr>
|
||||||
<tr><td class="metric-name">净利润率</td><td>-</td><td>-</td><td>2.49%</td><td>5.28%</td><td>5.58%</td></tr>
|
<tr><td class="metric-name">净利润率</td><td>2.49%</td><td>5.28%</td><td>5.58%</td><td>3.28%</td><td>3.89%</td></tr>
|
||||||
<tr><td class="metric-name">收入(亿)</td><td>-</td><td>-</td><td>1,084.61</td><td>1,075.36</td><td>1,041.03</td></tr>
|
<tr><td class="metric-name">收入(亿)</td><td>1,084.61</td><td>1,075.36</td><td>1,041.03</td><td>1,007.91</td><td>1,169.52</td></tr>
|
||||||
<tr><td class="metric-name">收入增速</td><td>-</td><td>-</td><td>0.86%</td><td>3.30%</td><td>-</td></tr>
|
<tr><td class="metric-name">收入增速</td><td>0.86%</td><td>3.30%</td><td>3.29%</td><td>-13.82%</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">净利润(亿)</td><td>-</td><td>-</td><td>27.00</td><td>56.78</td><td>58.09</td></tr>
|
<tr><td class="metric-name">净利润(亿)</td><td>27.00</td><td>56.78</td><td>58.09</td><td>33.08</td><td>45.49</td></tr>
|
||||||
<tr><td class="metric-name">净利润增速</td><td>-</td><td>-</td><td>-52.45%</td><td>-2.25%</td><td>-</td></tr>
|
<tr><td class="metric-name">净利润增速</td><td>-52.45%</td><td>-2.25%</td><td>75.58%</td><td>-27.27%</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">经营净现金流(亿)</td><td>-</td><td>-</td><td>112.64</td><td>102.38</td><td>70.94</td></tr>
|
<tr><td class="metric-name">经营净现金流(亿)</td><td>112.64</td><td>102.38</td><td>70.94</td><td>121.34</td><td>104.23</td></tr>
|
||||||
<tr><td class="metric-name">资本开支(亿)</td><td>-</td><td>-</td><td>83.76</td><td>57.34</td><td>51.53</td></tr>
|
<tr><td class="metric-name">资本开支(亿)</td><td>83.76</td><td>57.34</td><td>51.53</td><td>47.77</td><td>58.82</td></tr>
|
||||||
<tr><td class="metric-name">自由现金流(亿)</td><td>-</td><td>-</td><td>28.88</td><td>45.04</td><td>19.40</td></tr>
|
<tr><td class="metric-name">自由现金流(亿)</td><td>28.88</td><td>45.04</td><td>19.40</td><td>73.57</td><td>45.41</td></tr>
|
||||||
<tr><td class="metric-name">分红(亿)</td><td>-</td><td>-</td><td>27.05</td><td>28.97</td><td>28.14</td></tr>
|
<tr><td class="metric-name">分红(亿)</td><td>27.05</td><td>28.97</td><td>28.14</td><td>29.98</td><td>34.98</td></tr>
|
||||||
<tr><td class="metric-name">回购(亿)</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">回购(亿)</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">总资产(亿)</td><td>-</td><td>-</td><td>1,555.76</td><td>1,445.77</td><td>1,330.29</td></tr>
|
<tr><td class="metric-name">总资产(亿)</td><td>1,555.76</td><td>1,445.77</td><td>1,330.29</td><td>1,367.77</td><td>1,555.13</td></tr>
|
||||||
<tr><td class="metric-name">净资产(亿)</td><td>-</td><td>-</td><td>548.03</td><td>570.62</td><td>512.81</td></tr>
|
<tr><td class="metric-name">净资产(亿)</td><td>548.03</td><td>570.62</td><td>512.81</td><td>494.77</td><td>530.26</td></tr>
|
||||||
<tr><td class="metric-name">商誉(亿)</td><td>-</td><td>-</td><td>538.53</td><td>349.71</td><td>256.62</td></tr>
|
<tr><td class="metric-name">商誉(亿)</td><td>538.53</td><td>349.71</td><td>256.62</td><td>254.84</td><td>287.71</td></tr>
|
||||||
<tr class="section-row"><td class="section-label">费用指标</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">费用指标</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">销售费用率</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">销售费用率</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">管理费用率</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">管理费用率</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">SG&A比例</td><td>-</td><td>-</td><td>31.60%</td><td>31.72%</td><td>32.18%</td></tr>
|
<tr><td class="metric-name">SG&A比例</td><td>31.60%</td><td>31.72%</td><td>32.18%</td><td>32.27%</td><td>31.21%</td></tr>
|
||||||
<tr><td class="metric-name">研发费用率</td><td>-</td><td>-</td><td>4.96%</td><td>3.98%</td><td>3.73%</td></tr>
|
<tr><td class="metric-name">研发费用率</td><td>4.96%</td><td>3.98%</td><td>3.73%</td><td>3.82%</td><td>3.49%</td></tr>
|
||||||
<tr><td class="metric-name">其他费用率</td><td>-</td><td>-</td><td>6.53%</td><td>4.16%</td><td>4.03%</td></tr>
|
<tr><td class="metric-name">其他费用率</td><td>6.53%</td><td>4.16%</td><td>4.03%</td><td>5.80%</td><td>4.88%</td></tr>
|
||||||
<tr><td class="metric-name">折旧费用占比</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">折旧费用占比</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">所得税率</td><td>-</td><td>-</td><td>48.08%</td><td>29.26%</td><td>30.02%</td></tr>
|
<tr><td class="metric-name">所得税率</td><td>48.08%</td><td>29.26%</td><td>30.02%</td><td>34.28%</td><td>25.57%</td></tr>
|
||||||
<tr class="section-row"><td class="section-label">资产占比</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">资产占比</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">现金占比</td><td>-</td><td>-</td><td>3.79%</td><td>4.89%</td><td>3.79%</td></tr>
|
<tr><td class="metric-name">现金占比</td><td>3.79%</td><td>4.89%</td><td>3.79%</td><td>6.37%</td><td>6.90%</td></tr>
|
||||||
<tr><td class="metric-name">库存占比</td><td>-</td><td>-</td><td>10.70%</td><td>11.53%</td><td>11.41%</td></tr>
|
<tr><td class="metric-name">库存占比</td><td>10.70%</td><td>11.53%</td><td>11.41%</td><td>10.00%</td><td>8.83%</td></tr>
|
||||||
<tr><td class="metric-name">应收款占比</td><td>-</td><td>-</td><td>14.35%</td><td>14.77%</td><td>15.31%</td></tr>
|
<tr><td class="metric-name">应收款占比</td><td>14.35%</td><td>14.77%</td><td>15.31%</td><td>15.07%</td><td>14.53%</td></tr>
|
||||||
<tr><td class="metric-name">预付款占比</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">预付款占比</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">固定资产占比</td><td>-</td><td>-</td><td>20.10%</td><td>20.66%</td><td>22.05%</td></tr>
|
<tr><td class="metric-name">固定资产占比</td><td>20.10%</td><td>20.66%</td><td>22.05%</td><td>21.60%</td><td>21.22%</td></tr>
|
||||||
<tr><td class="metric-name">长期投资占比</td><td>-</td><td>-</td><td>3.08%</td><td>3.67%</td><td>4.07%</td></tr>
|
<tr><td class="metric-name">长期投资占比</td><td>3.08%</td><td>3.67%</td><td>4.07%</td><td>4.75%</td><td>4.30%</td></tr>
|
||||||
<tr><td class="metric-name">商誉占比</td><td>-</td><td>-</td><td>34.61%</td><td>24.19%</td><td>19.29%</td></tr>
|
<tr><td class="metric-name">商誉占比</td><td>34.61%</td><td>24.19%</td><td>19.29%</td><td>18.63%</td><td>18.50%</td></tr>
|
||||||
<tr class="other-assets-row"><td class="metric-name">其他资产占比</td><td>100.00%</td><td>100.00%</td><td>13.36%</td><td>20.29%</td><td>24.07%</td></tr>
|
<tr class="other-assets-row"><td class="metric-name">其他资产占比</td><td>13.36%</td><td>20.29%</td><td>24.07%</td><td>23.58%</td><td>25.72%</td></tr>
|
||||||
<tr><td class="metric-name">应付款占比</td><td>-</td><td>-</td><td>4.83%</td><td>4.78%</td><td>4.74%</td></tr>
|
<tr><td class="metric-name">应付款占比</td><td>4.83%</td><td>4.78%</td><td>4.74%</td><td>3.99%</td><td>3.91%</td></tr>
|
||||||
<tr><td class="metric-name">预收款占比</td><td>-</td><td>-</td><td>0.00%</td><td>0.00%</td><td>0.00%</td></tr>
|
<tr><td class="metric-name">预收款占比</td><td>0.00%</td><td>0.00%</td><td>0.00%</td><td>0.00%</td><td>0.00%</td></tr>
|
||||||
<tr><td class="metric-name">短期借款占比</td><td>-</td><td>-</td><td>2.33%</td><td>3.51%</td><td>4.50%</td></tr>
|
<tr><td class="metric-name">短期借款占比</td><td>2.33%</td><td>3.51%</td><td>4.50%</td><td>4.11%</td><td>10.13%</td></tr>
|
||||||
<tr><td class="metric-name">长期借款占比</td><td>-</td><td>-</td><td>46.48%</td><td>38.73%</td><td>32.15%</td></tr>
|
<tr><td class="metric-name">长期借款占比</td><td>46.48%</td><td>38.73%</td><td>32.15%</td><td>36.41%</td><td>32.01%</td></tr>
|
||||||
<tr><td class="metric-name">运营资产占比</td><td>0.00%</td><td>0.00%</td><td>20.22%</td><td>21.52%</td><td>21.99%</td></tr>
|
<tr><td class="metric-name">运营资产占比</td><td>20.22%</td><td>21.52%</td><td>21.99%</td><td>21.08%</td><td>19.45%</td></tr>
|
||||||
<tr><td class="metric-name">有息负债率</td><td>-</td><td>-</td><td>48.81%</td><td>42.24%</td><td>36.65%</td></tr>
|
<tr><td class="metric-name">有息负债率</td><td>48.81%</td><td>42.24%</td><td>36.65%</td><td>40.51%</td><td>42.14%</td></tr>
|
||||||
<tr class="section-row"><td class="section-label">周转能力</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">周转能力</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">存货周转天数</td><td>-</td><td>-</td><td>102</td><td>103</td><td>97</td></tr>
|
<tr><td class="metric-name">存货周转天数</td><td>102</td><td>103</td><td>97</td><td>90</td><td>75</td></tr>
|
||||||
<tr><td class="metric-name">应收款周转天数</td><td>-</td><td>-</td><td>75</td><td>72</td><td>71</td></tr>
|
<tr><td class="metric-name">应收款周转天数</td><td>75</td><td>72</td><td>71</td><td>74</td><td>70</td></tr>
|
||||||
<tr><td class="metric-name">应付款周转天数</td><td>-</td><td>-</td><td>46</td><td>42</td><td>40</td></tr>
|
<tr><td class="metric-name">应付款周转天数</td><td>46</td><td>42</td><td>40</td><td>36</td><td>33</td></tr>
|
||||||
<tr><td class="metric-name">固定资产周转率</td><td>-</td><td>-</td><td>3.47</td><td>3.60</td><td>3.55</td></tr>
|
<tr><td class="metric-name">固定资产周转率</td><td>3.47</td><td>3.60</td><td>3.55</td><td>3.41</td><td>3.54</td></tr>
|
||||||
<tr><td class="metric-name">总资产周转率</td><td>-</td><td>-</td><td>0.70</td><td>0.74</td><td>0.78</td></tr>
|
<tr><td class="metric-name">总资产周转率</td><td>0.70</td><td>0.74</td><td>0.78</td><td>0.74</td><td>0.75</td></tr>
|
||||||
<tr class="section-row"><td class="section-label">人均效率</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">人均效率</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">员工人数</td><td>31,934</td><td>31,934</td><td>31,934</td><td>30,538</td><td>29,515</td></tr>
|
<tr><td class="metric-name">员工人数</td><td>31,934</td><td>30,538</td><td>29,515</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">人均创收(万)</td><td>-</td><td>-</td><td>339.64</td><td>352.14</td><td>352.71</td></tr>
|
<tr><td class="metric-name">人均创收(万)</td><td>339.64</td><td>352.14</td><td>352.71</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">人均创利(万)</td><td>-</td><td>-</td><td>8.46</td><td>18.59</td><td>19.68</td></tr>
|
<tr><td class="metric-name">人均创利(万)</td><td>8.46</td><td>18.59</td><td>19.68</td><td>-</td><td>-</td></tr>
|
||||||
<tr><td class="metric-name">人均薪酬(万)</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">人均薪酬(万)</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
<tr class="section-row"><td class="section-label">市场表现</td><td class="section-spacer" colspan="5"></td></tr>
|
<tr class="section-row"><td class="section-label">市场表现</td><td class="section-spacer" colspan="5"></td></tr>
|
||||||
<tr><td class="metric-name">股价</td><td>105.03</td><td>105.03</td><td>95.04</td><td>104.09</td><td>105.23</td></tr>
|
<tr><td class="metric-name">股价</td><td>95.04</td><td>104.09</td><td>105.23</td><td>102.20</td><td>0.00</td></tr>
|
||||||
<tr><td class="metric-name">市值(亿)</td><td>959.97</td><td>959.97</td><td>868.66</td><td>951.38</td><td>961.80</td></tr>
|
<tr><td class="metric-name">市值(亿)</td><td>868.66</td><td>951.38</td><td>961.80</td><td>934.09</td><td>0.00</td></tr>
|
||||||
<tr><td class="metric-name">PE</td><td>-</td><td>-</td><td>32.17</td><td>16.76</td><td>16.56</td></tr>
|
<tr><td class="metric-name">PE</td><td>32.17</td><td>16.76</td><td>16.56</td><td>28.23</td><td>0.00</td></tr>
|
||||||
<tr><td class="metric-name">PB</td><td>-</td><td>-</td><td>1.59</td><td>1.67</td><td>1.88</td></tr>
|
<tr><td class="metric-name">PB</td><td>1.59</td><td>1.67</td><td>1.88</td><td>1.89</td><td>0.00</td></tr>
|
||||||
<tr><td class="metric-name">股东户数</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
<tr><td class="metric-name">股东户数</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# 麒麟控股株式会社 (2503.T) - Financial Report
|
# 麒麟控股株式会社 (2503.T) - Financial Report
|
||||||
*Report generated on: 2026-01-03*
|
*Report generated on: 2026-01-07*
|
||||||
|
|
||||||
| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |
|
| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |
|
||||||
|:---|:---|:---|:---|:---|:---|
|
|:---|:---|:---|:---|:---|:---|
|
||||||
@ -7,83 +7,83 @@
|
|||||||
|
|
||||||
|
|
||||||
## 主要指标
|
## 主要指标
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| ROE | - | - | 4.93% | 9.95% | 11.33% |
|
| ROE | 4.93% | 9.95% | 11.33% | 6.69% | 8.58% |
|
||||||
| ROA | - | - | 1.74% | 3.93% | 4.37% |
|
| ROA | 1.74% | 3.93% | 4.37% | 2.42% | 2.92% |
|
||||||
| ROCE/ROIC | - | - | - | - | - |
|
| ROCE/ROIC | - | - | - | - | - |
|
||||||
| 毛利率 | - | - | 45.59% | 45.14% | 45.53% |
|
| 毛利率 | 45.59% | 45.14% | 45.53% | 45.17% | 43.46% |
|
||||||
| 净利润率 | - | - | 2.49% | 5.28% | 5.58% |
|
| 净利润率 | 2.49% | 5.28% | 5.58% | 3.28% | 3.89% |
|
||||||
| 收入(亿) | - | - | 1,084.61 | 1,075.36 | 1,041.03 |
|
| 收入(亿) | 1,084.61 | 1,075.36 | 1,041.03 | 1,007.91 | 1,169.52 |
|
||||||
| 收入增速 | - | - | 0.86% | 3.30% | - |
|
| 收入增速 | 0.86% | 3.30% | 3.29% | -13.82% | - |
|
||||||
| 净利润(亿) | - | - | 27.00 | 56.78 | 58.09 |
|
| 净利润(亿) | 27.00 | 56.78 | 58.09 | 33.08 | 45.49 |
|
||||||
| 净利润增速 | - | - | -52.45% | -2.25% | - |
|
| 净利润增速 | -52.45% | -2.25% | 75.58% | -27.27% | - |
|
||||||
| 经营净现金流(亿) | - | - | 112.64 | 102.38 | 70.94 |
|
| 经营净现金流(亿) | 112.64 | 102.38 | 70.94 | 121.34 | 104.23 |
|
||||||
| 资本开支(亿) | - | - | 83.76 | 57.34 | 51.53 |
|
| 资本开支(亿) | 83.76 | 57.34 | 51.53 | 47.77 | 58.82 |
|
||||||
| 自由现金流(亿) | - | - | 28.88 | 45.04 | 19.40 |
|
| 自由现金流(亿) | 28.88 | 45.04 | 19.40 | 73.57 | 45.41 |
|
||||||
| 分红(亿) | - | - | 27.05 | 28.97 | 28.14 |
|
| 分红(亿) | 27.05 | 28.97 | 28.14 | 29.98 | 34.98 |
|
||||||
| 回购(亿) | - | - | - | - | - |
|
| 回购(亿) | - | - | - | - | - |
|
||||||
| 总资产(亿) | - | - | 1,555.76 | 1,445.77 | 1,330.29 |
|
| 总资产(亿) | 1,555.76 | 1,445.77 | 1,330.29 | 1,367.77 | 1,555.13 |
|
||||||
| 净资产(亿) | - | - | 548.03 | 570.62 | 512.81 |
|
| 净资产(亿) | 548.03 | 570.62 | 512.81 | 494.77 | 530.26 |
|
||||||
| 商誉(亿) | - | - | 538.53 | 349.71 | 256.62 |
|
| 商誉(亿) | 538.53 | 349.71 | 256.62 | 254.84 | 287.71 |
|
||||||
|
|
||||||
|
|
||||||
## 费用指标
|
## 费用指标
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| 销售费用率 | - | - | - | - | - |
|
| 销售费用率 | - | - | - | - | - |
|
||||||
| 管理费用率 | - | - | - | - | - |
|
| 管理费用率 | - | - | - | - | - |
|
||||||
| SG&A比例 | - | - | 31.60% | 31.72% | 32.18% |
|
| SG&A比例 | 31.60% | 31.72% | 32.18% | 32.27% | 31.21% |
|
||||||
| 研发费用率 | - | - | 4.96% | 3.98% | 3.73% |
|
| 研发费用率 | 4.96% | 3.98% | 3.73% | 3.82% | 3.49% |
|
||||||
| 其他费用率 | - | - | 6.53% | 4.16% | 4.03% |
|
| 其他费用率 | 6.53% | 4.16% | 4.03% | 5.80% | 4.88% |
|
||||||
| 折旧费用占比 | - | - | - | - | - |
|
| 折旧费用占比 | - | - | - | - | - |
|
||||||
| 所得税率 | - | - | 48.08% | 29.26% | 30.02% |
|
| 所得税率 | 48.08% | 29.26% | 30.02% | 34.28% | 25.57% |
|
||||||
|
|
||||||
|
|
||||||
## 资产占比
|
## 资产占比
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| 现金占比 | - | - | 3.79% | 4.89% | 3.79% |
|
| 现金占比 | 3.79% | 4.89% | 3.79% | 6.37% | 6.90% |
|
||||||
| 库存占比 | - | - | 10.70% | 11.53% | 11.41% |
|
| 库存占比 | 10.70% | 11.53% | 11.41% | 10.00% | 8.83% |
|
||||||
| 应收款占比 | - | - | 14.35% | 14.77% | 15.31% |
|
| 应收款占比 | 14.35% | 14.77% | 15.31% | 15.07% | 14.53% |
|
||||||
| 预付款占比 | - | - | - | - | - |
|
| 预付款占比 | - | - | - | - | - |
|
||||||
| 固定资产占比 | - | - | 20.10% | 20.66% | 22.05% |
|
| 固定资产占比 | 20.10% | 20.66% | 22.05% | 21.60% | 21.22% |
|
||||||
| 长期投资占比 | - | - | 3.08% | 3.67% | 4.07% |
|
| 长期投资占比 | 3.08% | 3.67% | 4.07% | 4.75% | 4.30% |
|
||||||
| 商誉占比 | - | - | 34.61% | 24.19% | 19.29% |
|
| 商誉占比 | 34.61% | 24.19% | 19.29% | 18.63% | 18.50% |
|
||||||
| 其他资产占比 | 100.00% | 100.00% | 13.36% | 20.29% | 24.07% |
|
| 其他资产占比 | 13.36% | 20.29% | 24.07% | 23.58% | 25.72% |
|
||||||
| 应付款占比 | - | - | 4.83% | 4.78% | 4.74% |
|
| 应付款占比 | 4.83% | 4.78% | 4.74% | 3.99% | 3.91% |
|
||||||
| 预收款占比 | - | - | 0.00% | 0.00% | 0.00% |
|
| 预收款占比 | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
|
||||||
| 短期借款占比 | - | - | 2.33% | 3.51% | 4.50% |
|
| 短期借款占比 | 2.33% | 3.51% | 4.50% | 4.11% | 10.13% |
|
||||||
| 长期借款占比 | - | - | 46.48% | 38.73% | 32.15% |
|
| 长期借款占比 | 46.48% | 38.73% | 32.15% | 36.41% | 32.01% |
|
||||||
| 运营资产占比 | 0.00% | 0.00% | 20.22% | 21.52% | 21.99% |
|
| 运营资产占比 | 20.22% | 21.52% | 21.99% | 21.08% | 19.45% |
|
||||||
| 有息负债率 | - | - | 48.81% | 42.24% | 36.65% |
|
| 有息负债率 | 48.81% | 42.24% | 36.65% | 40.51% | 42.14% |
|
||||||
|
|
||||||
|
|
||||||
## 周转能力
|
## 周转能力
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| 存货周转天数 | - | - | 102 | 103 | 97 |
|
| 存货周转天数 | 102 | 103 | 97 | 90 | 75 |
|
||||||
| 应收款周转天数 | - | - | 75 | 72 | 71 |
|
| 应收款周转天数 | 75 | 72 | 71 | 74 | 70 |
|
||||||
| 应付款周转天数 | - | - | 46 | 42 | 40 |
|
| 应付款周转天数 | 46 | 42 | 40 | 36 | 33 |
|
||||||
| 固定资产周转率 | - | - | 3.47 | 3.60 | 3.55 |
|
| 固定资产周转率 | 3.47 | 3.60 | 3.55 | 3.41 | 3.54 |
|
||||||
| 总资产周转率 | - | - | 0.70 | 0.74 | 0.78 |
|
| 总资产周转率 | 0.70 | 0.74 | 0.78 | 0.74 | 0.75 |
|
||||||
|
|
||||||
|
|
||||||
## 人均效率
|
## 人均效率
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| 员工人数 | 31,934 | 31,934 | 31,934 | 30,538 | 29,515 |
|
| 员工人数 | 31,934 | 30,538 | 29,515 | - | - |
|
||||||
| 人均创收(万) | - | - | 339.64 | 352.14 | 352.71 |
|
| 人均创收(万) | 339.64 | 352.14 | 352.71 | - | - |
|
||||||
| 人均创利(万) | - | - | 8.46 | 18.59 | 19.68 |
|
| 人均创利(万) | 8.46 | 18.59 | 19.68 | - | - |
|
||||||
| 人均薪酬(万) | - | - | - | - | - |
|
| 人均薪酬(万) | - | - | - | - | - |
|
||||||
|
|
||||||
|
|
||||||
## 市场表现
|
## 市场表现
|
||||||
| 指标 | 2026A | 2025A | 2024A | 2023A | 2022A |
|
| 指标 | 2024A | 2023A | 2022A | 2021A | 2020A |
|
||||||
|:---|--:|--:|--:|--:|--:|
|
|:---|--:|--:|--:|--:|--:|
|
||||||
| 股价 | 105.03 | 105.03 | 95.04 | 104.09 | 105.23 |
|
| 股价 | 95.04 | 104.09 | 105.23 | 102.20 | 0.00 |
|
||||||
| 市值(亿) | 959.97 | 959.97 | 868.66 | 951.38 | 961.80 |
|
| 市值(亿) | 868.66 | 951.38 | 961.80 | 934.09 | 0.00 |
|
||||||
| PE | - | - | 32.17 | 16.76 | 16.56 |
|
| PE | 32.17 | 16.76 | 16.56 | 28.23 | 0.00 |
|
||||||
| PB | - | - | 1.59 | 1.67 | 1.88 |
|
| PB | 1.59 | 1.67 | 1.88 | 1.89 | 0.00 |
|
||||||
| 股东户数 | - | - | - | - | - |
|
| 股东户数 | - | - | - | - | - |
|
||||||
|
|
||||||
|
|||||||
@ -124,21 +124,21 @@ export default function AnalysisPage({ params }: { params: Promise<{ id: string
|
|||||||
<Tabs defaultValue="financial_data" className="space-y-4">
|
<Tabs defaultValue="financial_data" className="space-y-4">
|
||||||
<TabsList className="bg-muted/50 p-1 flex-wrap h-auto">
|
<TabsList className="bg-muted/50 p-1 flex-wrap h-auto">
|
||||||
<TabsTrigger value="financial_data">财务数据</TabsTrigger>
|
<TabsTrigger value="financial_data">财务数据</TabsTrigger>
|
||||||
{report.sections?.map((s: any) => {
|
|
||||||
const nameMap: Record<string, string> = {
|
{[
|
||||||
'company_profile': '公司简介',
|
{ id: 'company_profile', label: '公司简介' },
|
||||||
'fundamental_analysis': '基本面分析',
|
{ id: 'fundamental_analysis', label: '基本面分析' },
|
||||||
'insider_analysis': '内部人士分析',
|
{ id: 'insider_analysis', label: '内部人士分析' },
|
||||||
'bullish_analysis': '看涨分析',
|
{ id: 'bullish_analysis', label: '看涨分析' },
|
||||||
'bearish_analysis': '看跌分析'
|
{ id: 'bearish_analysis', label: '看跌分析' }
|
||||||
};
|
].map((section) => (
|
||||||
return (
|
<div key={section.id} className="flex items-center">
|
||||||
<TabsTrigger key={s.section_name} value={s.section_name} className="capitalize">
|
<div className="h-4 w-[1px] bg-border mx-1" />
|
||||||
{nameMap[s.section_name] || s.section_name.replace(/_/g, " ")}
|
<TabsTrigger value={section.id} className="capitalize">
|
||||||
|
{section.label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
{report.sections?.length === 0 && report.status !== "in_progress" && <TabsTrigger value="empty" disabled>无内容</TabsTrigger>}
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="financial_data" className="min-h-[500px]">
|
<TabsContent value="financial_data" className="min-h-[500px]">
|
||||||
@ -152,15 +152,38 @@ export default function AnalysisPage({ params }: { params: Promise<{ id: string
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{report.sections?.map((s: any) => (
|
{[
|
||||||
<TabsContent key={s.section_name} value={s.section_name} className="min-h-[500px]">
|
{ id: 'company_profile', label: '公司简介' },
|
||||||
<Card>
|
{ id: 'fundamental_analysis', label: '基本面分析' },
|
||||||
<CardContent className="p-6 md:p-8">
|
{ id: 'insider_analysis', label: '内部人士分析' },
|
||||||
<MarkdownRenderer content={s.content} />
|
{ id: 'bullish_analysis', label: '看涨分析' },
|
||||||
</CardContent>
|
{ id: 'bearish_analysis', label: '看跌分析' }
|
||||||
</Card>
|
].map((section) => {
|
||||||
</TabsContent>
|
const sectionData = report.sections?.find((s: any) => s.section_name === section.id);
|
||||||
))}
|
return (
|
||||||
|
<TabsContent key={section.id} value={section.id} className="min-h-[500px]">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 md:p-8">
|
||||||
|
{sectionData ? (
|
||||||
|
<MarkdownRenderer content={sectionData.content} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||||
|
{report.status === "in_progress" || report.status === "pending" ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-10 w-10 animate-spin mb-4 text-primary" />
|
||||||
|
<p>AI 正在撰写{section.label}...</p>
|
||||||
|
<p className="text-sm mt-2 opacity-75">请稍候,全面分析通常需要 1 分钟左右</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>该部分暂无内容</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default function Home() {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">股票分析</h1>
|
<h1 className="text-3xl font-bold tracking-tight">股票分析</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
输入公司名称或股票代码,开始全面的AI驱动的分析。支持中国、香港、美国、日本的公司分析。
|
输入公司名称或股票代码,开始全面的AI驱动的分析。支持中国、香港、美国、日本、越南的公司分析。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,10 @@ def get_strategy(market, stock_code, tushare_token=None, av_key=None):
|
|||||||
from strategies.jp_strategy import JP_Strategy
|
from strategies.jp_strategy import JP_Strategy
|
||||||
ifind_token = os.getenv('IFIND_REFRESH_TOKEN')
|
ifind_token = os.getenv('IFIND_REFRESH_TOKEN')
|
||||||
return JP_Strategy(stock_code, ifind_token)
|
return JP_Strategy(stock_code, ifind_token)
|
||||||
|
elif market == 'VN':
|
||||||
|
from strategies.vn_strategy import VN_Strategy
|
||||||
|
ifind_token = os.getenv('IFIND_REFRESH_TOKEN')
|
||||||
|
return VN_Strategy(stock_code, ifind_token)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported market: {market}")
|
raise ValueError(f"Unsupported market: {market}")
|
||||||
|
|
||||||
@ -39,7 +43,7 @@ def main():
|
|||||||
strategy = get_strategy(market, symbol, tushare_token, av_key)
|
strategy = get_strategy(market, symbol, tushare_token, av_key)
|
||||||
strategy.execute()
|
strategy.execute()
|
||||||
else:
|
else:
|
||||||
print("Usage: python main.py <MARKET> <SYMBOL>")
|
print("Usage: python run_fetcher.py <MARKET> <SYMBOL>")
|
||||||
print("Running default test cases:")
|
print("Running default test cases:")
|
||||||
|
|
||||||
# Test CN
|
# Test CN
|
||||||
116
src/analysis/vn_analyzer.py
Normal file
116
src/analysis/vn_analyzer.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
from .cn_analyzer import CN_Analyzer
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
class VN_Analyzer(CN_Analyzer):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.market = 'VN'
|
||||||
|
|
||||||
|
self.mapping = {
|
||||||
|
'income': {
|
||||||
|
'revenue': 'revenue',
|
||||||
|
'net_income': 'net_income',
|
||||||
|
'gross_profit': 'gross_profit',
|
||||||
|
'total_profit': 'total_profit',
|
||||||
|
'sga_exp': 'sga_exp'
|
||||||
|
},
|
||||||
|
'balance': {
|
||||||
|
'total_equity': 'total_equity',
|
||||||
|
'total_assets': 'total_assets',
|
||||||
|
'total_liabilities': 'total_liabilities',
|
||||||
|
'current_assets': 'current_assets',
|
||||||
|
'current_liabilities': 'current_liabilities',
|
||||||
|
'cash': 'cash',
|
||||||
|
'receivables': 'receivables',
|
||||||
|
'inventory': 'inventory',
|
||||||
|
'fixed_assets': 'fixed_assets',
|
||||||
|
'goodwill': 'goodwill',
|
||||||
|
'short_term_debt': 'short_term_debt',
|
||||||
|
'long_term_debt': 'long_term_debt'
|
||||||
|
},
|
||||||
|
'cashflow': {
|
||||||
|
'ocf': 'ocf',
|
||||||
|
'capex': 'capex',
|
||||||
|
'dividends': 'dividends'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _post_process_columns(self, df, type):
|
||||||
|
if market_type := self.mapping.get(type):
|
||||||
|
for col in market_type.values():
|
||||||
|
if col in df.columns:
|
||||||
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||||
|
|
||||||
|
df = super()._post_process_columns(df, type)
|
||||||
|
if type == 'balance':
|
||||||
|
if 'long_term_investments' in df.columns:
|
||||||
|
df['lt_invest'] = df['long_term_investments']
|
||||||
|
|
||||||
|
if 'long_term_debt' not in df.columns: df['long_term_debt'] = 0
|
||||||
|
if 'long_term_borrowings' in df.columns:
|
||||||
|
df['long_term_debt'] = df['long_term_debt'].fillna(0) + df['long_term_borrowings'].fillna(0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def calculate_indicators(self, df_merged, market_metrics, historical_metrics):
|
||||||
|
if 'revenue' in df_merged.columns and 'gross_profit' in df_merged.columns:
|
||||||
|
df_merged['cogs'] = df_merged['revenue'] - df_merged['gross_profit']
|
||||||
|
|
||||||
|
df_merged = super().calculate_indicators(df_merged, market_metrics, historical_metrics)
|
||||||
|
|
||||||
|
has_sga = False
|
||||||
|
if 'sga_exp' in df_merged.columns and 'revenue' in df_merged.columns:
|
||||||
|
if df_merged['sga_exp'].notna().any() and (df_merged['sga_exp'] != 0).any():
|
||||||
|
df_merged['SgaRatio'] = self._safe_div(df_merged['sga_exp'], df_merged['revenue'])
|
||||||
|
has_sga = True
|
||||||
|
|
||||||
|
if not has_sga:
|
||||||
|
sga_sum = 0
|
||||||
|
if 'selling_exp' in df_merged.columns: sga_sum = sga_sum + df_merged['selling_exp'].fillna(0)
|
||||||
|
if 'admin_exp' in df_merged.columns: sga_sum = sga_sum + df_merged['admin_exp'].fillna(0)
|
||||||
|
if 'revenue' in df_merged.columns:
|
||||||
|
df_merged['SgaRatio'] = self._safe_div(sga_sum, df_merged['revenue'])
|
||||||
|
|
||||||
|
if 'income_tax' in df_merged.columns and 'net_income' in df_merged.columns:
|
||||||
|
ebt_approx = df_merged['net_income'] + df_merged['income_tax']
|
||||||
|
df_merged['TaxRate'] = self._safe_div(df_merged['income_tax'], ebt_approx)
|
||||||
|
|
||||||
|
if 'GrossMargin' in df_merged.columns and 'NetMargin' in df_merged.columns:
|
||||||
|
other_ratio = df_merged['GrossMargin'] - df_merged['NetMargin']
|
||||||
|
if 'SgaRatio' in df_merged.columns:
|
||||||
|
other_ratio = other_ratio - df_merged['SgaRatio'].fillna(0)
|
||||||
|
if 'RDRatio' in df_merged.columns:
|
||||||
|
other_ratio = other_ratio - df_merged['RDRatio'].fillna(0)
|
||||||
|
df_merged['OtherExpenseRatio'] = other_ratio
|
||||||
|
|
||||||
|
if 'MarketCap' in df_merged.columns:
|
||||||
|
if 'net_income' in df_merged.columns:
|
||||||
|
calculated_pe = self._safe_div(df_merged['MarketCap'], df_merged['net_income'])
|
||||||
|
if 'PE' not in df_merged.columns:
|
||||||
|
df_merged['PE'] = calculated_pe
|
||||||
|
else:
|
||||||
|
cond_pe = (df_merged['PE'] != 0) & df_merged['PE'].notna()
|
||||||
|
df_merged['PE'] = df_merged['PE'].where(cond_pe, calculated_pe)
|
||||||
|
|
||||||
|
if 'total_equity' in df_merged.columns:
|
||||||
|
calculated_pb = self._safe_div(df_merged['MarketCap'], df_merged['total_equity'])
|
||||||
|
if 'PB' not in df_merged.columns:
|
||||||
|
df_merged['PB'] = calculated_pb
|
||||||
|
else:
|
||||||
|
cond_pb = (df_merged['PB'] != 0) & df_merged['PB'].notna()
|
||||||
|
df_merged['PB'] = df_merged['PB'].where(cond_pb, calculated_pb)
|
||||||
|
|
||||||
|
if 'dividends' in df_merged.columns:
|
||||||
|
calculated_yield = self._safe_div(df_merged['dividends'], df_merged['MarketCap']) * 100
|
||||||
|
if 'DividendYield' not in df_merged.columns:
|
||||||
|
df_merged['DividendYield'] = calculated_yield
|
||||||
|
else:
|
||||||
|
df_merged['DividendYield'] = df_merged['DividendYield'].fillna(calculated_yield)
|
||||||
|
|
||||||
|
if 'employee_count' in df_merged.columns:
|
||||||
|
df_merged['Employees'] = df_merged['employee_count']
|
||||||
|
if 'revenue' in df_merged.columns:
|
||||||
|
df_merged['RevenuePerEmp'] = self._safe_div(df_merged['revenue'], df_merged['employee_count'])
|
||||||
|
if 'net_income' in df_merged.columns:
|
||||||
|
df_merged['ProfitPerEmp'] = self._safe_div(df_merged['net_income'], df_merged['employee_count'])
|
||||||
|
|
||||||
|
return df_merged
|
||||||
@ -31,5 +31,14 @@ class FetcherFactory:
|
|||||||
raise ValueError("iFinD Refresh Token is required for JP market")
|
raise ValueError("iFinD Refresh Token is required for JP market")
|
||||||
from .jp_fetcher import JpFetcher
|
from .jp_fetcher import JpFetcher
|
||||||
return JpFetcher(ifind_token)
|
return JpFetcher(ifind_token)
|
||||||
|
elif market == 'VN':
|
||||||
|
ifind_token = kwargs.get('ifind_refresh_token')
|
||||||
|
if not ifind_token:
|
||||||
|
import os
|
||||||
|
ifind_token = os.getenv('IFIND_REFRESH_TOKEN')
|
||||||
|
if not ifind_token:
|
||||||
|
raise ValueError("iFinD Refresh Token is required for VN market")
|
||||||
|
from .vn_fetcher import VnFetcher
|
||||||
|
return VnFetcher(ifind_token)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported market: {market}")
|
raise ValueError(f"Unsupported market: {market}")
|
||||||
|
|||||||
@ -74,8 +74,10 @@ class HkFetcher(DataFetcher):
|
|||||||
if not res:
|
if not res:
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
if res.get("errorcode") != 0:
|
# Default to 0 if not present (for lenient mocking) or check properly
|
||||||
print(f"iFinD API Error: {res.get('errmsg')} (code: {res.get('errorcode')})")
|
error_code = res.get("errorcode", 0)
|
||||||
|
if error_code != 0:
|
||||||
|
print(f"iFinD API Error: {res.get('errmsg')} (code: {error_code})")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
tables = res.get("tables", [])
|
tables = res.get("tables", [])
|
||||||
@ -142,42 +144,61 @@ class HkFetcher(DataFetcher):
|
|||||||
def _fetch_financial_data_annual(self, symbol: str, indicator_configs: list) -> pd.DataFrame:
|
def _fetch_financial_data_annual(self, symbol: str, indicator_configs: list) -> pd.DataFrame:
|
||||||
"""通用获取历年财务数据 (HKD 为主,但 iFinD 支持转 CNY)"""
|
"""通用获取历年财务数据 (HKD 为主,但 iFinD 支持转 CNY)"""
|
||||||
code = self._get_ifind_code(symbol)
|
code = self._get_ifind_code(symbol)
|
||||||
basic_info = self._fetch_basic_info(symbol)
|
|
||||||
# HK stocks don't always use the same acc_date, but we can try to fetch recent years
|
|
||||||
|
|
||||||
current_year = int(time.strftime("%Y"))
|
current_year = int(time.strftime("%Y"))
|
||||||
|
|
||||||
|
# 1. First, determine the most recent valid year by trying backwards from current year
|
||||||
|
last_valid_year = None
|
||||||
|
|
||||||
|
# Try up to 3 years back to find the latest available report
|
||||||
|
# e.g., in Jan 2026, try 2026 -> fail, 2025 -> success
|
||||||
|
for offset in range(3):
|
||||||
|
test_year = current_year - offset
|
||||||
|
test_date = f"{test_year}1231"
|
||||||
|
|
||||||
|
# Use the first indicator to test availability
|
||||||
|
first_indicator = indicator_configs[0]
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": first_indicator["indicator"], "indiparams": [test_date, first_indicator.get("type", "1"), "CNY"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
# Check for non-null values
|
||||||
|
valid_val = df.iloc[0, 0] if not df.empty and df.shape[1] > 0 else None
|
||||||
|
if pd.notna(valid_val) and valid_val != 0:
|
||||||
|
last_valid_year = test_year
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_valid_year is None:
|
||||||
|
# Fallback to current year if nothing found (will likely return empty/zeros, but keeps logic flowing)
|
||||||
|
last_valid_year = current_year
|
||||||
|
|
||||||
|
# 2. Fetch 5 years starting from the last valid year
|
||||||
all_dfs = []
|
all_dfs = []
|
||||||
|
|
||||||
# HK stocks often report semi-annually or annually. Let's fetch recent reporting periods.
|
|
||||||
# Instead of guessing dates, we can use "reporting_period" but iFinD's basic_data_service
|
|
||||||
# often works better with explicit dates if we want annuals.
|
|
||||||
# Alternatively, we can fetch multiple periods.
|
|
||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
target_year = current_year - i
|
target_year = last_valid_year - i
|
||||||
# Try 1231 as primary guess for annual
|
target_date = f"{target_year}1231"
|
||||||
for month_day in ["1231", "0331", "0630", "0930"]:
|
|
||||||
target_date = f"{target_year}{month_day}"
|
params = {
|
||||||
params = {
|
"codes": code,
|
||||||
"codes": code,
|
"indipara": [
|
||||||
"indipara": [
|
{"indicator": item["indicator"], "indiparams": [target_date, item.get("type", "1"), "CNY"]}
|
||||||
{"indicator": item["indicator"], "indiparams": [target_date, item.get("type", "1"), "CNY"]}
|
for item in indicator_configs
|
||||||
for item in indicator_configs
|
]
|
||||||
]
|
}
|
||||||
}
|
res = self.cli.post("basic_data_service", params)
|
||||||
res = self.cli.post("basic_data_service", params)
|
df = self._parse_ifind_tables(res)
|
||||||
df = self._parse_ifind_tables(res)
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
# Check if it's mostly empty data
|
valid_cols = [c for c in df.columns if c not in ['end_date', 'date']]
|
||||||
valid_cols = [c for c in df.columns if c not in ['end_date', 'date']]
|
if not df[valid_cols].isnull().all().all():
|
||||||
if not df[valid_cols].isnull().all().all():
|
df['end_date'] = target_date
|
||||||
df['end_date'] = target_date
|
df = df.dropna(axis=1, how='all')
|
||||||
# Drop columns that are entirely NA to prevent FutureWarning in pd.concat
|
all_dfs.append(df)
|
||||||
df = df.dropna(axis=1, how='all')
|
|
||||||
all_dfs.append(df)
|
|
||||||
# If we found data for this year, maybe we don't need to try other months?
|
|
||||||
# Actually some companies changed their fiscal year.
|
|
||||||
break
|
|
||||||
|
|
||||||
if not all_dfs:
|
if not all_dfs:
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
@ -186,17 +207,59 @@ class HkFetcher(DataFetcher):
|
|||||||
|
|
||||||
def get_income_statement(self, symbol: str) -> pd.DataFrame:
|
def get_income_statement(self, symbol: str) -> pd.DataFrame:
|
||||||
indicators = [
|
indicators = [
|
||||||
{"indicator": "revenue_oas"},
|
{"indicator": "total_oi"},
|
||||||
{"indicator": "gross_profit_oas"},
|
{"indicator": "prime_oi"},
|
||||||
{"indicator": "sga_expenses_oas"},
|
{"indicator": "other_oi"},
|
||||||
{"indicator": "selling_marketing_expenses_oas"},
|
{"indicator": "operating_cost"},
|
||||||
{"indicator": "ga_expenses_oas"},
|
{"indicator": "operating_expense"},
|
||||||
{"indicator": "rd_expenses_oas"},
|
{"indicator": "operating_fee"},
|
||||||
{"indicator": "income_tax_expense_oas"},
|
{"indicator": "p_depreciation_and_amortization"},
|
||||||
{"indicator": "net_income_attri_to_common_sh_oas"},
|
{"indicator": "gross_profit"},
|
||||||
{"indicator": "operating_income_oas"},
|
{"indicator": "sales_ad_and_ga"},
|
||||||
{"indicator": "ebit_oas"},
|
{"indicator": "rad_cost"},
|
||||||
{"indicator": "depreciation_and_amortization_oas"}
|
{"indicator": "sales_fee"},
|
||||||
|
{"indicator": "financial_expense"},
|
||||||
|
{"indicator": "sales_income"},
|
||||||
|
{"indicator": "sales_cost"},
|
||||||
|
{"indicator": "other_income"},
|
||||||
|
{"indicator": "manage_fee"},
|
||||||
|
{"indicator": "deprec_and_amorti"},
|
||||||
|
{"indicator": "total_other_opearting_expense"},
|
||||||
|
{"indicator": "p_total_cost"},
|
||||||
|
{"indicator": "operating_profit"},
|
||||||
|
{"indicator": "total_gal"},
|
||||||
|
{"indicator": "interest_income"},
|
||||||
|
{"indicator": "interest_net_pay"},
|
||||||
|
{"indicator": "interest_expense"},
|
||||||
|
{"indicator": "income_from_asso_and_joint"},
|
||||||
|
{"indicator": "other_gal_effct_profit_pre_tax"},
|
||||||
|
{"indicator": "conti_op_before_tax"},
|
||||||
|
{"indicator": "profit_before_noncurrent_items"},
|
||||||
|
{"indicator": "profit_and_loss_of_noncurrent_items"},
|
||||||
|
{"indicator": "profit_before_tax"},
|
||||||
|
{"indicator": "income_tax"},
|
||||||
|
{"indicator": "profit_after_tax"},
|
||||||
|
{"indicator": "minoritygal"},
|
||||||
|
{"indicator": "continue_operate_net_profit"},
|
||||||
|
{"indicator": "noncontinue_operate_net_profit"},
|
||||||
|
{"indicator": "other_special_items"},
|
||||||
|
{"indicator": "ni_attr_to_cs"},
|
||||||
|
{"indicator": "np_atms"},
|
||||||
|
{"indicator": "preferred_divid_and_other_adjust"},
|
||||||
|
{"indicator": "oci"},
|
||||||
|
{"indicator": "total_oci"},
|
||||||
|
{"indicator": "oci_from_parent"},
|
||||||
|
{"indicator": "oci_from_minority"},
|
||||||
|
{"indicator": "invest_property_fv_chg"},
|
||||||
|
{"indicator": "operating_amt"},
|
||||||
|
{"indicator": "oi_si"},
|
||||||
|
{"indicator": "operating_premium_profit_si"},
|
||||||
|
{"indicator": "to_toallied_corp_perf"},
|
||||||
|
{"indicator": "to_joint_control_entity_perf"},
|
||||||
|
{"indicator": "pre_tax_profit_si"},
|
||||||
|
{"indicator": "after_tax_profit_si"},
|
||||||
|
{"indicator": "profit_attrbt_to_nonholders"},
|
||||||
|
{"indicator": "total_income_atncs"}
|
||||||
]
|
]
|
||||||
|
|
||||||
df = self._fetch_financial_data_annual(symbol, indicators)
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
@ -204,20 +267,28 @@ class HkFetcher(DataFetcher):
|
|||||||
self._save_raw_data(df, symbol, "income_statement_raw")
|
self._save_raw_data(df, symbol, "income_statement_raw")
|
||||||
|
|
||||||
rename_map = {
|
rename_map = {
|
||||||
'revenue_oas': 'revenue',
|
'total_oi': 'revenue',
|
||||||
'gross_profit_oas': 'gross_profit',
|
'operating_amt': 'turnover', # Backup for revenue
|
||||||
'sga_expenses_oas': 'sga_exp',
|
'gross_profit': 'gross_profit',
|
||||||
'selling_marketing_expenses_oas': 'selling_marketing_exp',
|
'sales_ad_and_ga': 'sga_exp',
|
||||||
'ga_expenses_oas': 'ga_exp',
|
'sales_fee': 'selling_marketing_exp',
|
||||||
'rd_expenses_oas': 'rd_exp',
|
'manage_fee': 'ga_exp',
|
||||||
'income_tax_expense_oas': 'income_tax',
|
'rad_cost': 'rd_exp',
|
||||||
'net_income_attri_to_common_sh_oas': 'net_income',
|
'income_tax': 'income_tax',
|
||||||
'operating_income_oas': 'operating_profit',
|
'ni_attr_to_cs': 'net_income',
|
||||||
'ebit_oas': 'ebit',
|
'operating_profit': 'operating_profit',
|
||||||
'depreciation_and_amortization_oas': 'depreciation'
|
'depreciation': 'depreciation',
|
||||||
|
'deprec_and_amorti': 'depreciation', # Backup
|
||||||
|
'p_depreciation_and_amortization': 'depreciation' # Another backup
|
||||||
}
|
}
|
||||||
|
|
||||||
df_filtered = df.rename(columns=rename_map)
|
df_filtered = df.rename(columns=rename_map)
|
||||||
|
|
||||||
|
# Calculate EBIT if not present but operating_profit is there
|
||||||
|
if 'ebit' not in df_filtered.columns and 'operating_profit' in df_filtered.columns:
|
||||||
|
# Simple approximation: Operating Profit is often used as EBIT
|
||||||
|
df_filtered['ebit'] = df_filtered['operating_profit']
|
||||||
|
|
||||||
for col in df_filtered.columns:
|
for col in df_filtered.columns:
|
||||||
if col not in ['date', 'end_date']:
|
if col not in ['date', 'end_date']:
|
||||||
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
||||||
@ -226,23 +297,96 @@ class HkFetcher(DataFetcher):
|
|||||||
|
|
||||||
def get_balance_sheet(self, symbol: str) -> pd.DataFrame:
|
def get_balance_sheet(self, symbol: str) -> pd.DataFrame:
|
||||||
indicators = [
|
indicators = [
|
||||||
{"indicator": "cash_equi_short_term_inve_oas"},
|
{"indicator": "cce"},
|
||||||
{"indicator": "accou_and_notes_recei_oas"},
|
{"indicator": "st_investment"},
|
||||||
{"indicator": "inventories_oas"},
|
{"indicator": "total_cash"},
|
||||||
{"indicator": "ppe_net_oas"},
|
{"indicator": "account_receivable"},
|
||||||
{"indicator": "long_term_inv_and_receiv_oas"},
|
{"indicator": "tradable_fnncl_asset"},
|
||||||
{"indicator": "goodwill_and_intasset_oas"},
|
{"indicator": "derivative_fnncl_assets"},
|
||||||
{"indicator": "short_term_debt_oas"},
|
{"indicator": "restriv_fund"},
|
||||||
{"indicator": "short_term_borrowings_oas"},
|
{"indicator": "other_short_term_investment"},
|
||||||
{"indicator": "account_and_note_payable_oas"},
|
{"indicator": "ar_nr"},
|
||||||
{"indicator": "contra_liabilities_current_oas"},
|
{"indicator": "total_ar"},
|
||||||
{"indicator": "advance_from_cust_current_oas"},
|
{"indicator": "or"},
|
||||||
{"indicator": "defer_revenue_current_oas"},
|
{"indicator": "inventory"},
|
||||||
{"indicator": "long_term_debt_oas"},
|
{"indicator": "flow_assets_dit"},
|
||||||
{"indicator": "long_term_borrowings_oas"},
|
{"indicator": "pre_payment"},
|
||||||
{"indicator": "total_assets_oas"},
|
{"indicator": "other_cunrrent_assets_si"},
|
||||||
{"indicator": "equity_attri_to_companyowner_oas"},
|
{"indicator": "other_ca"},
|
||||||
{"indicator": "prepaid_expenses_current_oas"}
|
{"indicator": "total_ca"},
|
||||||
|
{"indicator": "receivables_from_allied_corp"},
|
||||||
|
{"indicator": "current_assets_si"},
|
||||||
|
{"indicator": "prepay_deposits_etc"},
|
||||||
|
{"indicator": "receivables_from_jce"},
|
||||||
|
{"indicator": "receivables_from_ac"},
|
||||||
|
{"indicator": "recoverable_tax"},
|
||||||
|
{"indicator": "total_fixed_assets"},
|
||||||
|
{"indicator": "depreciation"},
|
||||||
|
{"indicator": "equity_and_lt_invest"},
|
||||||
|
{"indicator": "net_fixed_assets"},
|
||||||
|
{"indicator": "invest_property"},
|
||||||
|
{"indicator": "equity_investment"},
|
||||||
|
{"indicator": "investment_in_associate"},
|
||||||
|
{"indicator": "investment_in_joints"},
|
||||||
|
{"indicator": "held_to_maturity_invest"},
|
||||||
|
{"indicator": "goodwill_and_intangible_asset"},
|
||||||
|
{"indicator": "intangible_assets"},
|
||||||
|
{"indicator": "accum_amortized"},
|
||||||
|
{"indicator": "noncurrent_assets_dit"},
|
||||||
|
{"indicator": "other_noncurrent_assets_si"},
|
||||||
|
{"indicator": "dt_assets"},
|
||||||
|
{"indicator": "total_noncurrent_assets"},
|
||||||
|
{"indicator": "total_assets"},
|
||||||
|
{"indicator": "ac_equity"},
|
||||||
|
{"indicator": "lease_prepay"},
|
||||||
|
{"indicator": "noncurrent_assets_si"},
|
||||||
|
{"indicator": "st_lt_current_loan"},
|
||||||
|
{"indicator": "trade_financial_lia"},
|
||||||
|
{"indicator": "derivative_financial_lia"},
|
||||||
|
{"indicator": "ap_np"},
|
||||||
|
{"indicator": "accounts_payable"},
|
||||||
|
{"indicator": "advance_payment"},
|
||||||
|
{"indicator": "st_debt"},
|
||||||
|
{"indicator": "contra_liab"},
|
||||||
|
{"indicator": "tax_payable"},
|
||||||
|
{"indicator": "accrued_liab"},
|
||||||
|
{"indicator": "flow_debt_deferred_income"},
|
||||||
|
{"indicator": "other_cl"},
|
||||||
|
{"indicator": "other_cunrrent_liab_si"},
|
||||||
|
{"indicator": "total_cl"},
|
||||||
|
{"indicator": "accrued_expenses_etc"},
|
||||||
|
{"indicator": "money_payable_toac"},
|
||||||
|
{"indicator": "joint_control_entity_payable"},
|
||||||
|
{"indicator": "payable_to_associated_corp"},
|
||||||
|
{"indicator": "lt_debt"},
|
||||||
|
{"indicator": "long_term_loan"},
|
||||||
|
{"indicator": "other_noncurrent_liabi"},
|
||||||
|
{"indicator": "deferred_tax_liability"},
|
||||||
|
{"indicator": "ncl_deferred_income"},
|
||||||
|
{"indicator": "other_noncurrent_liab_si"},
|
||||||
|
{"indicator": "noncurrent_liab_si"},
|
||||||
|
{"indicator": "total_noncurrent_liab"},
|
||||||
|
{"indicator": "total_liab"},
|
||||||
|
{"indicator": "common_shares"},
|
||||||
|
{"indicator": "capital_reserve"},
|
||||||
|
{"indicator": "equity_premium"},
|
||||||
|
{"indicator": "treasury_stock"},
|
||||||
|
{"indicator": "accumgal"},
|
||||||
|
{"indicator": "equity_atsopc_sbi"},
|
||||||
|
{"indicator": "preferred_stock"},
|
||||||
|
{"indicator": "perpetual_debt"},
|
||||||
|
{"indicator": "reserve"},
|
||||||
|
{"indicator": "other_reserves"},
|
||||||
|
{"indicator": "retained_earnings"},
|
||||||
|
{"indicator": "oci_bs"},
|
||||||
|
{"indicator": "total_common_equity"},
|
||||||
|
{"indicator": "equity_belong_to_parent"},
|
||||||
|
{"indicator": "minority_interests"},
|
||||||
|
{"indicator": "other_equity_si"},
|
||||||
|
{"indicator": "total_equity"},
|
||||||
|
{"indicator": "total_lib_and_equity"},
|
||||||
|
{"indicator": "equity_si"},
|
||||||
|
{"indicator": "equity_atncs"}
|
||||||
]
|
]
|
||||||
|
|
||||||
df = self._fetch_financial_data_annual(symbol, indicators)
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
@ -250,30 +394,39 @@ class HkFetcher(DataFetcher):
|
|||||||
self._save_raw_data(df, symbol, "balance_sheet_raw")
|
self._save_raw_data(df, symbol, "balance_sheet_raw")
|
||||||
|
|
||||||
rename_map = {
|
rename_map = {
|
||||||
'cash_equi_short_term_inve_oas': 'cash',
|
'cce': 'cash',
|
||||||
'accou_and_notes_recei_oas': 'receivables',
|
'ar_nr': 'receivables',
|
||||||
'inventories_oas': 'inventory',
|
'inventory': 'inventory',
|
||||||
'ppe_net_oas': 'fixed_assets',
|
'net_fixed_assets': 'fixed_assets',
|
||||||
'long_term_inv_and_receiv_oas': 'long_term_investments',
|
'equity_and_lt_invest': 'long_term_investments',
|
||||||
'goodwill_and_intasset_oas': 'goodwill',
|
'goodwill_and_intangible_asset': 'goodwill',
|
||||||
'short_term_debt_oas': 'short_term_debt',
|
'st_debt': 'short_term_debt',
|
||||||
'short_term_borrowings_oas': 'short_term_borrowings',
|
'st_lt_current_loan': 'short_term_borrowings',
|
||||||
'account_and_note_payable_oas': 'accounts_payable',
|
'ap_np': 'accounts_payable',
|
||||||
'contra_liabilities_current_oas': 'contract_liabilities',
|
'contra_liab': 'contract_liabilities',
|
||||||
'advance_from_cust_current_oas': 'advances_from_customers',
|
'advance_payment': 'advances_from_customers',
|
||||||
'defer_revenue_current_oas': 'deferred_revenue',
|
'flow_debt_deferred_income': 'deferred_revenue',
|
||||||
'long_term_debt_oas': 'long_term_debt',
|
'lt_debt': 'long_term_debt',
|
||||||
'long_term_borrowings_oas': 'long_term_borrowings',
|
'long_term_loan': 'long_term_borrowings',
|
||||||
'total_assets_oas': 'total_assets',
|
'total_assets': 'total_assets',
|
||||||
'equity_attri_to_companyowner_oas': 'total_equity',
|
'equity_belong_to_parent': 'total_equity',
|
||||||
'prepaid_expenses_current_oas': 'prepayment'
|
'pre_payment': 'prepayment'
|
||||||
}
|
}
|
||||||
|
|
||||||
df_filtered = df.rename(columns=rename_map)
|
df_filtered = df.rename(columns=rename_map)
|
||||||
|
|
||||||
|
# Deduplicate columns just in case
|
||||||
|
df_filtered = df_filtered.loc[:, ~df_filtered.columns.duplicated()]
|
||||||
|
|
||||||
if 'total_liabilities' not in df_filtered.columns or df_filtered['total_liabilities'].isnull().all():
|
if 'total_liabilities' not in df_filtered.columns or df_filtered['total_liabilities'].isnull().all():
|
||||||
if 'total_assets' in df_filtered.columns and 'total_equity' in df_filtered.columns:
|
if 'total_liab' in df_filtered.columns:
|
||||||
|
df_filtered['total_liabilities'] = df_filtered['total_liab']
|
||||||
|
elif 'total_assets' in df_filtered.columns and 'total_equity' in df_filtered.columns:
|
||||||
df_filtered['total_liabilities'] = df_filtered['total_assets'] - df_filtered['total_equity']
|
df_filtered['total_liabilities'] = df_filtered['total_assets'] - df_filtered['total_equity']
|
||||||
|
|
||||||
|
# Deduplicate again in case total_liabilities logic added a dupe (unlikely)
|
||||||
|
df_filtered = df_filtered.loc[:, ~df_filtered.columns.duplicated()]
|
||||||
|
|
||||||
for col in df_filtered.columns:
|
for col in df_filtered.columns:
|
||||||
if col not in ['date', 'end_date']:
|
if col not in ['date', 'end_date']:
|
||||||
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
||||||
@ -282,9 +435,17 @@ class HkFetcher(DataFetcher):
|
|||||||
|
|
||||||
def get_cash_flow(self, symbol: str) -> pd.DataFrame:
|
def get_cash_flow(self, symbol: str) -> pd.DataFrame:
|
||||||
indicators = [
|
indicators = [
|
||||||
{"indicator": "net_cash_flows_from_oa_oas"},
|
{"indicator": "ni"},
|
||||||
{"indicator": "purchase_of_ppe_and_ia_oas"},
|
{"indicator": "depreciation_and_amortization"},
|
||||||
{"indicator": "dividends_paid_oas"}
|
{"indicator": "operating_capital_change"},
|
||||||
|
{"indicator": "ncf_from_oa"},
|
||||||
|
{"indicator": "capital_cost"},
|
||||||
|
{"indicator": "invest_buy"},
|
||||||
|
{"indicator": "ncf_from_ia"},
|
||||||
|
{"indicator": "increase_in_share_capital"},
|
||||||
|
{"indicator": "decrease_in_share_capital"},
|
||||||
|
{"indicator": "total_dividends_paid"},
|
||||||
|
{"indicator": "ncf_from_fa"}
|
||||||
]
|
]
|
||||||
|
|
||||||
df = self._fetch_financial_data_annual(symbol, indicators)
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
@ -292,9 +453,9 @@ class HkFetcher(DataFetcher):
|
|||||||
self._save_raw_data(df, symbol, "cash_flow_raw")
|
self._save_raw_data(df, symbol, "cash_flow_raw")
|
||||||
|
|
||||||
rename_map = {
|
rename_map = {
|
||||||
'net_cash_flows_from_oa_oas': 'ocf',
|
'ncf_from_oa': 'ocf',
|
||||||
'purchase_of_ppe_and_ia_oas': 'capex',
|
'capital_cost': 'capex',
|
||||||
'dividends_paid_oas': 'dividends'
|
'total_dividends_paid': 'dividends'
|
||||||
}
|
}
|
||||||
|
|
||||||
df_filtered = df.rename(columns=rename_map)
|
df_filtered = df.rename(columns=rename_map)
|
||||||
@ -482,3 +643,104 @@ class HkFetcher(DataFetcher):
|
|||||||
df_emp = pd.DataFrame(results)
|
df_emp = pd.DataFrame(results)
|
||||||
self._save_raw_data(df_emp, symbol, "employee_count_raw")
|
self._save_raw_data(df_emp, symbol, "employee_count_raw")
|
||||||
return df_emp
|
return df_emp
|
||||||
|
|
||||||
|
def get_financial_ratios(self, symbol: str) -> pd.DataFrame:
|
||||||
|
"""获取官方计算的财务指标(比率、周转天数等)"""
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
current_year = int(time.strftime("%Y"))
|
||||||
|
|
||||||
|
# 1. Determine the latest valid year
|
||||||
|
last_valid_year = None
|
||||||
|
for offset in range(3):
|
||||||
|
test_year = current_year - offset
|
||||||
|
# Try getting ROE as a proxy for data availability
|
||||||
|
test_date = f"{test_year}1231"
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [{"indicator": "roe", "indiparams": [test_date]}]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
val = df.iloc[0, 0] if not df.empty and df.shape[1] > 0 else None
|
||||||
|
if pd.notna(val) and val != 0:
|
||||||
|
last_valid_year = test_year
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_valid_year is None:
|
||||||
|
last_valid_year = current_year
|
||||||
|
|
||||||
|
all_dfs = []
|
||||||
|
|
||||||
|
# 2. Fetch 5 years starting from last valid year
|
||||||
|
for i in range(5):
|
||||||
|
target_year = last_valid_year - i
|
||||||
|
date_str = f"{target_year}1231"
|
||||||
|
year_str = str(target_year)
|
||||||
|
|
||||||
|
indipara = []
|
||||||
|
|
||||||
|
# 1. 人均指标 (参数: Year, "100")
|
||||||
|
for key in ["salary_pp", "revenue_pp", "profit_pp"]:
|
||||||
|
indipara.append({"indicator": key, "indiparams": [year_str, "100"]})
|
||||||
|
|
||||||
|
# 2. 财务比率与周转率 (参数: Date YYYYMMDD)
|
||||||
|
ratio_keys = [
|
||||||
|
"roe", "roa", "roic",
|
||||||
|
"sales_fee_to_or", "manage_fee_to_revenue", "rad_expense_to_total_income",
|
||||||
|
"operating_revenue_yoy", "np_atsopc_yoy",
|
||||||
|
"ibdebt_ratio_asset_base",
|
||||||
|
"inventory_turnover_days", "receivable_turnover_days", "accounts_payable_turnover_days",
|
||||||
|
"fixed_asset_turnover_ratio", "total_capital_turnover"
|
||||||
|
]
|
||||||
|
for key in ratio_keys:
|
||||||
|
indipara.append({"indicator": key, "indiparams": [date_str]})
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": indipara
|
||||||
|
}
|
||||||
|
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
if 'end_date' not in df.columns:
|
||||||
|
df['end_date'] = date_str
|
||||||
|
|
||||||
|
# Filter out columns that are all NaN
|
||||||
|
df = df.dropna(axis=1, how='all')
|
||||||
|
|
||||||
|
# Identify if we have meaningful data (at least one valid metric)
|
||||||
|
valid_cols = [c for c in df.columns if c not in ['end_date', 'date', 'code', 'thscode']]
|
||||||
|
if not df[valid_cols].isnull().all().all():
|
||||||
|
all_dfs.append(df)
|
||||||
|
|
||||||
|
if not all_dfs:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
combined = pd.concat(all_dfs, ignore_index=True)
|
||||||
|
self._save_raw_data(combined, symbol, "financial_ratios_raw")
|
||||||
|
|
||||||
|
rename_map = {
|
||||||
|
"salary_pp": "salary_per_employee",
|
||||||
|
"revenue_pp": "revenue_per_employee",
|
||||||
|
"profit_pp": "profit_per_employee",
|
||||||
|
"sales_fee_to_or": "selling_expense_ratio",
|
||||||
|
"manage_fee_to_revenue": "admin_expense_ratio",
|
||||||
|
"rad_expense_to_total_income": "rd_expense_ratio",
|
||||||
|
"operating_revenue_yoy": "revenue_growth",
|
||||||
|
"np_atsopc_yoy": "net_profit_growth",
|
||||||
|
"ibdebt_ratio_asset_base": "interest_bearing_debt_ratio",
|
||||||
|
"fixed_asset_turnover_ratio": "fixed_asset_turnover",
|
||||||
|
"total_capital_turnover": "total_asset_turnover"
|
||||||
|
}
|
||||||
|
|
||||||
|
df_final = combined.rename(columns=rename_map)
|
||||||
|
|
||||||
|
for col in df_final.columns:
|
||||||
|
if col not in ['date', 'end_date']:
|
||||||
|
df_final[col] = pd.to_numeric(df_final[col], errors='coerce')
|
||||||
|
|
||||||
|
return self._filter_data(df_final)
|
||||||
|
|||||||
@ -151,11 +151,41 @@ class JpFetcher(DataFetcher):
|
|||||||
acc_date = basic_info.get("accounting_date", "1231")
|
acc_date = basic_info.get("accounting_date", "1231")
|
||||||
|
|
||||||
current_year = int(time.strftime("%Y"))
|
current_year = int(time.strftime("%Y"))
|
||||||
|
|
||||||
|
# 1. First, determine the most recent valid year by trying backwards from current year
|
||||||
|
last_valid_year = None
|
||||||
|
|
||||||
|
# Try up to 3 years back to find the latest available report
|
||||||
|
for offset in range(3):
|
||||||
|
test_year = current_year - offset
|
||||||
|
test_date = f"{test_year}{acc_date}"
|
||||||
|
|
||||||
|
# Use the first indicator to test availability
|
||||||
|
first_indicator = indicator_configs[0]
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": first_indicator["indicator"], "indiparams": [test_date, first_indicator.get("type", "1"), "CNY"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
# Check for non-null values
|
||||||
|
valid_val = df.iloc[0, 0] if not df.empty and df.shape[1] > 0 else None
|
||||||
|
if pd.notna(valid_val) and valid_val != 0:
|
||||||
|
last_valid_year = test_year
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_valid_year is None:
|
||||||
|
last_valid_year = current_year
|
||||||
|
|
||||||
all_dfs = []
|
all_dfs = []
|
||||||
|
|
||||||
# 获取最近 5 年的数据,精准定位会计年结日
|
# 2. Fetch 5 years starting from the last valid year
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
target_year = current_year - i
|
target_year = last_valid_year - i
|
||||||
target_date = f"{target_year}{acc_date}"
|
target_date = f"{target_year}{acc_date}"
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
|||||||
@ -48,6 +48,15 @@ class UsFetcher(DataFetcher):
|
|||||||
df_annual = pd.DataFrame(data["annualReports"])
|
df_annual = pd.DataFrame(data["annualReports"])
|
||||||
if "fiscalDateEnding" in df_annual.columns:
|
if "fiscalDateEnding" in df_annual.columns:
|
||||||
df_annual = df_annual.sort_values("fiscalDateEnding", ascending=False)
|
df_annual = df_annual.sort_values("fiscalDateEnding", ascending=False)
|
||||||
|
|
||||||
|
# Dynamic year filtering: Find the latest report with valid data and take surrounding 5 years
|
||||||
|
# For Alpha Vantage, data is already sorted by date descending.
|
||||||
|
# We simply check for the first row with non-None values in critical columns if possible,
|
||||||
|
# but usually AV returns valid blocks. We'll just take the top 5.
|
||||||
|
# Unlike iFinD, AV returns a list of available reports, so we don't need to probe year by year.
|
||||||
|
|
||||||
|
# Keep top 5 latest entries
|
||||||
|
df_annual = df_annual.head(5)
|
||||||
else:
|
else:
|
||||||
print(f"Error fetching {function} for {symbol}: {data}")
|
print(f"Error fetching {function} for {symbol}: {data}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|||||||
474
src/fetchers/vn_fetcher.py
Normal file
474
src/fetchers/vn_fetcher.py
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from .base import DataFetcher
|
||||||
|
from .ifind_client import IFindClient
|
||||||
|
from storage.file_io import DataStorage
|
||||||
|
|
||||||
|
class VnFetcher(DataFetcher):
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
# api_key is the iFinD Refresh Token
|
||||||
|
super().__init__(api_key)
|
||||||
|
self.cli = IFindClient(refresh_token=api_key)
|
||||||
|
self.storage = DataStorage()
|
||||||
|
self._basic_info_cache = {}
|
||||||
|
|
||||||
|
def _get_ifind_code(self, symbol: str) -> str:
|
||||||
|
# Vietnam stocks usually have 3 letter codes.
|
||||||
|
# We assume the user provides the correct code (e.g. VNM, or VNM.VN).
|
||||||
|
# We can add simple logic: if it's 3 letters, maybe append nothing?
|
||||||
|
# iFinD codes often need suffix. But without documentation, safest is to pass through.
|
||||||
|
return symbol
|
||||||
|
|
||||||
|
def _fetch_basic_info(self, symbol: str) -> dict:
|
||||||
|
"""获取公司的基本信息:中文名称、会计年结日、上市日期"""
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
if code in self._basic_info_cache:
|
||||||
|
return self._basic_info_cache[code]
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": "corp_cn_name", "indiparams": []},
|
||||||
|
{"indicator": "accounting_date", "indiparams": []},
|
||||||
|
{"indicator": "ipo_date", "indiparams": []}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
if not df.empty:
|
||||||
|
self._save_raw_data(df, symbol, "basic_info_raw")
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"name": "",
|
||||||
|
"accounting_date": "1231", # Default 12-31
|
||||||
|
"ipo_date": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
row = df.iloc[0]
|
||||||
|
info["name"] = str(row.get("corp_cn_name", ""))
|
||||||
|
|
||||||
|
acc_date = str(row.get("accounting_date", "1231")).replace("-", "").replace("/", "")
|
||||||
|
if acc_date:
|
||||||
|
info["accounting_date"] = acc_date
|
||||||
|
|
||||||
|
info["ipo_date"] = str(row.get("ipo_date", "")).replace("-", "").replace("/", "")
|
||||||
|
|
||||||
|
self._basic_info_cache[code] = info
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _save_raw_data(self, data: any, symbol: str, name: str):
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
if isinstance(data, dict):
|
||||||
|
df = pd.DataFrame([data])
|
||||||
|
else:
|
||||||
|
df = data
|
||||||
|
self.storage.save_data(df, 'VN', symbol, f"raw_{name}")
|
||||||
|
|
||||||
|
def _parse_ifind_tables(self, res: dict) -> pd.DataFrame:
|
||||||
|
"""通用解析 iFinD 返回结果的 tables 结构为 DataFrame"""
|
||||||
|
if not res:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
if res.get("errorcode") != 0:
|
||||||
|
print(f"iFinD API Error: {res.get('errmsg')} (code: {res.get('errorcode')})")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
tables = res.get("tables", [])
|
||||||
|
if not tables:
|
||||||
|
# print("iFinD API Warning: No tables found in response.")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
table_info = tables[0]
|
||||||
|
table_data = table_info.get("table", {})
|
||||||
|
times = table_info.get("time", [])
|
||||||
|
|
||||||
|
if not table_data:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
processed_table_data = {}
|
||||||
|
for k, v in table_data.items():
|
||||||
|
if not isinstance(v, list):
|
||||||
|
processed_table_data[k] = [v]
|
||||||
|
else:
|
||||||
|
processed_table_data[k] = v
|
||||||
|
|
||||||
|
df = pd.DataFrame(processed_table_data)
|
||||||
|
if times and len(times) == len(df):
|
||||||
|
df['end_date'] = [str(t).replace('-', '').replace('/', '').split(' ')[0] for t in times]
|
||||||
|
elif times and len(df) == 1:
|
||||||
|
df['end_date'] = str(times[0]).replace('-', '').replace('/', '').split(' ')[0]
|
||||||
|
|
||||||
|
if 'end_date' not in df.columns:
|
||||||
|
for col in ['time', 'date', 'trade_date', 'REPORT_DATE']:
|
||||||
|
if col in df.columns:
|
||||||
|
df['end_date'] = df[col].astype(str).str.replace('-', '').str.replace('/', '').str.split(' ').str[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def _filter_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
if df.empty or 'end_date' not in df.columns:
|
||||||
|
return df
|
||||||
|
|
||||||
|
df = df.sort_values(by='end_date', ascending=False)
|
||||||
|
df = df.drop_duplicates(subset=['end_date'], keep='first')
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
|
||||||
|
latest_record = df.iloc[[0]]
|
||||||
|
try:
|
||||||
|
latest_date_str = str(latest_record['end_date'].values[0])
|
||||||
|
last_year_date_str = str(int(latest_date_str) - 10000)
|
||||||
|
comparable_record = df[df['end_date'].astype(str) == last_year_date_str]
|
||||||
|
except:
|
||||||
|
comparable_record = pd.DataFrame()
|
||||||
|
|
||||||
|
# VN usually ends in 1231
|
||||||
|
is_annual = df['end_date'].astype(str).str.endswith('1231')
|
||||||
|
annual_records = df[is_annual]
|
||||||
|
|
||||||
|
combined = pd.concat([latest_record, comparable_record, annual_records])
|
||||||
|
combined = combined.drop_duplicates(subset=['end_date'])
|
||||||
|
combined = combined.sort_values(by='end_date', ascending=False)
|
||||||
|
return combined
|
||||||
|
|
||||||
|
def _fetch_financial_data_annual(self, symbol: str, indicator_configs: list) -> pd.DataFrame:
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
basic_info = self._fetch_basic_info(symbol)
|
||||||
|
acc_date = basic_info.get("accounting_date", "1231")
|
||||||
|
|
||||||
|
current_year = int(time.strftime("%Y"))
|
||||||
|
last_valid_year = None
|
||||||
|
|
||||||
|
for offset in range(3):
|
||||||
|
test_year = current_year - offset
|
||||||
|
test_date = f"{test_year}{acc_date}"
|
||||||
|
first_indicator = indicator_configs[0]
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": first_indicator["indicator"], "indiparams": [test_date, first_indicator.get("type", "1"), "CNY"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
valid_val = df.iloc[0, 0] if not df.empty and df.shape[1] > 0 else None
|
||||||
|
if pd.notna(valid_val) and valid_val != 0:
|
||||||
|
last_valid_year = test_year
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_valid_year is None:
|
||||||
|
last_valid_year = current_year
|
||||||
|
|
||||||
|
all_dfs = []
|
||||||
|
for i in range(5):
|
||||||
|
target_year = last_valid_year - i
|
||||||
|
target_date = f"{target_year}{acc_date}"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": item["indicator"], "indiparams": [target_date, item.get("type", "1"), "CNY"]}
|
||||||
|
for item in indicator_configs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
if not df.empty:
|
||||||
|
df['end_date'] = target_date
|
||||||
|
all_dfs.append(df)
|
||||||
|
|
||||||
|
if not all_dfs:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
all_dfs = [d for d in all_dfs if not d.empty and not d.isna().all().all()]
|
||||||
|
if not all_dfs:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
return pd.concat(all_dfs, ignore_index=True)
|
||||||
|
|
||||||
|
def get_income_statement(self, symbol: str) -> pd.DataFrame:
|
||||||
|
indicators = [
|
||||||
|
{"indicator": "revenue_oas"},
|
||||||
|
{"indicator": "gross_profit_oas"},
|
||||||
|
{"indicator": "sga_expenses_oas"},
|
||||||
|
{"indicator": "selling_marketing_expenses_oas"},
|
||||||
|
{"indicator": "ga_expenses_oas"},
|
||||||
|
{"indicator": "rd_expenses_oas"},
|
||||||
|
{"indicator": "income_tax_expense_oas"},
|
||||||
|
{"indicator": "net_income_attri_to_common_sh_oas"},
|
||||||
|
{"indicator": "operating_income_oas"}
|
||||||
|
]
|
||||||
|
|
||||||
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
|
if df.empty: return df
|
||||||
|
self._save_raw_data(df, symbol, "income_statement_raw")
|
||||||
|
|
||||||
|
rename_map = {
|
||||||
|
'revenue_oas': 'revenue',
|
||||||
|
'gross_profit_oas': 'gross_profit',
|
||||||
|
'sga_expenses_oas': 'sga_exp',
|
||||||
|
'selling_marketing_expenses_oas': 'selling_marketing_exp',
|
||||||
|
'ga_expenses_oas': 'ga_exp',
|
||||||
|
'rd_expenses_oas': 'rd_exp',
|
||||||
|
'income_tax_expense_oas': 'income_tax',
|
||||||
|
'net_income_attri_to_common_sh_oas': 'net_income',
|
||||||
|
'operating_income_oas': 'operating_profit'
|
||||||
|
}
|
||||||
|
|
||||||
|
df_filtered = df.rename(columns=rename_map)
|
||||||
|
|
||||||
|
for col in df_filtered.columns:
|
||||||
|
if col not in ['date', 'end_date']:
|
||||||
|
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
||||||
|
|
||||||
|
return self._filter_data(df_filtered)
|
||||||
|
|
||||||
|
def get_balance_sheet(self, symbol: str) -> pd.DataFrame:
|
||||||
|
indicators = [
|
||||||
|
{"indicator": "cash_equi_short_term_inve_oas"},
|
||||||
|
{"indicator": "accou_and_notes_recei_oas"},
|
||||||
|
{"indicator": "inventories_oas"},
|
||||||
|
{"indicator": "ppe_net_oas"},
|
||||||
|
{"indicator": "long_term_inv_and_receiv_oas"},
|
||||||
|
{"indicator": "goodwill_and_intasset_oas"},
|
||||||
|
{"indicator": "short_term_debt_oas"},
|
||||||
|
{"indicator": "short_term_borrowings_oas"},
|
||||||
|
{"indicator": "account_and_note_payable_oas"},
|
||||||
|
{"indicator": "contra_liabilities_current_oas"},
|
||||||
|
{"indicator": "advance_from_cust_current_oas"},
|
||||||
|
{"indicator": "defer_revenue_current_oas"},
|
||||||
|
{"indicator": "long_term_debt_oas"},
|
||||||
|
{"indicator": "long_term_borrowings_oas"},
|
||||||
|
{"indicator": "total_assets_oas"},
|
||||||
|
{"indicator": "equity_attri_to_companyowner_oas"},
|
||||||
|
{"indicator": "prepaid_expenses_current_oas"}
|
||||||
|
]
|
||||||
|
|
||||||
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
|
if df.empty: return df
|
||||||
|
self._save_raw_data(df, symbol, "balance_sheet_raw")
|
||||||
|
|
||||||
|
rename_map = {
|
||||||
|
'cash_equi_short_term_inve_oas': 'cash',
|
||||||
|
'accou_and_notes_recei_oas': 'receivables',
|
||||||
|
'inventories_oas': 'inventory',
|
||||||
|
'ppe_net_oas': 'fixed_assets',
|
||||||
|
'long_term_inv_and_receiv_oas': 'long_term_investments',
|
||||||
|
'goodwill_and_intasset_oas': 'goodwill',
|
||||||
|
'short_term_debt_oas': 'short_term_debt',
|
||||||
|
'short_term_borrowings_oas': 'short_term_borrowings',
|
||||||
|
'account_and_note_payable_oas': 'accounts_payable',
|
||||||
|
'contra_liabilities_current_oas': 'contract_liabilities',
|
||||||
|
'advance_from_cust_current_oas': 'advances_from_customers',
|
||||||
|
'defer_revenue_current_oas': 'deferred_revenue',
|
||||||
|
'long_term_debt_oas': 'long_term_debt',
|
||||||
|
'long_term_borrowings_oas': 'long_term_borrowings',
|
||||||
|
'total_assets_oas': 'total_assets',
|
||||||
|
'equity_attri_to_companyowner_oas': 'total_equity',
|
||||||
|
'prepaid_expenses_current_oas': 'prepayment'
|
||||||
|
}
|
||||||
|
|
||||||
|
df_filtered = df.rename(columns=rename_map)
|
||||||
|
|
||||||
|
if 'total_liabilities' not in df_filtered.columns or df_filtered['total_liabilities'].isnull().all():
|
||||||
|
if 'total_assets' in df_filtered.columns and 'total_equity' in df_filtered.columns:
|
||||||
|
df_filtered['total_liabilities'] = df_filtered['total_assets'] - df_filtered['total_equity']
|
||||||
|
|
||||||
|
for col in df_filtered.columns:
|
||||||
|
if col not in ['date', 'end_date']:
|
||||||
|
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
||||||
|
|
||||||
|
return self._filter_data(df_filtered)
|
||||||
|
|
||||||
|
def get_cash_flow(self, symbol: str) -> pd.DataFrame:
|
||||||
|
indicators = [
|
||||||
|
{"indicator": "net_cash_flows_from_oa_oas"},
|
||||||
|
{"indicator": "purchase_of_ppe_and_ia_oas"},
|
||||||
|
{"indicator": "dividends_paid_oas"}
|
||||||
|
]
|
||||||
|
|
||||||
|
df = self._fetch_financial_data_annual(symbol, indicators)
|
||||||
|
if df.empty: return df
|
||||||
|
self._save_raw_data(df, symbol, "cash_flow_raw")
|
||||||
|
|
||||||
|
rename_map = {
|
||||||
|
'net_cash_flows_from_oa_oas': 'ocf',
|
||||||
|
'purchase_of_ppe_and_ia_oas': 'capex',
|
||||||
|
'dividends_paid_oas': 'dividends'
|
||||||
|
}
|
||||||
|
|
||||||
|
df_filtered = df.rename(columns=rename_map)
|
||||||
|
|
||||||
|
for col in df_filtered.columns:
|
||||||
|
if col not in ['date', 'end_date']:
|
||||||
|
df_filtered[col] = pd.to_numeric(df_filtered[col], errors='coerce')
|
||||||
|
|
||||||
|
if 'capex' in df_filtered.columns:
|
||||||
|
df_filtered['capex'] = df_filtered['capex'].abs()
|
||||||
|
|
||||||
|
return self._filter_data(df_filtered)
|
||||||
|
|
||||||
|
def get_market_metrics(self, symbol: str) -> dict:
|
||||||
|
basic_info = self._fetch_basic_info(symbol)
|
||||||
|
metrics = {
|
||||||
|
"name": basic_info.get("name", ""),
|
||||||
|
"list_date": basic_info.get("ipo_date", "")
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def get_historical_metrics(self, symbol: str, dates: list) -> pd.DataFrame:
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
if not dates: return pd.DataFrame()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for d in dates:
|
||||||
|
d_str = str(d).replace('-', '').replace('/', '')
|
||||||
|
fmt_d = f"{d_str[:4]}-{d_str[4:6]}-{d_str[6:]}" if len(d_str) == 8 else d_str
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"startdate": fmt_d,
|
||||||
|
"enddate": fmt_d,
|
||||||
|
"functionpara": {"Interval": "D", "Days": "Alldays", "Fill": "Previous"},
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": "pre_close", "indiparams": ["", "0", "CNY"]},
|
||||||
|
{"indicator": "market_value", "indiparams": ["", "CNY"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("date_sequence", params)
|
||||||
|
df_seq = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
metrics = {'date_str': d_str, 'PE': 0.0, 'PB': 0.0, 'MarketCap': 0.0, 'Price': 0.0}
|
||||||
|
|
||||||
|
if not df_seq.empty:
|
||||||
|
match = df_seq[df_seq['end_date'] <= d_str].tail(1) if 'end_date' in df_seq.columns else df_seq.tail(1)
|
||||||
|
if not match.empty:
|
||||||
|
if 'pre_close' in match.columns:
|
||||||
|
metrics['Price'] = float(match['pre_close'].iloc[0] or 0.0)
|
||||||
|
if 'market_value' in match.columns:
|
||||||
|
metrics['MarketCap'] = float(match['market_value'].iloc[0] or 0.0)
|
||||||
|
results.append(metrics)
|
||||||
|
|
||||||
|
df_hist = pd.DataFrame(results)
|
||||||
|
self._save_raw_data(df_hist, symbol, "historical_metrics_raw")
|
||||||
|
return df_hist
|
||||||
|
|
||||||
|
def get_dividends(self, symbol: str) -> pd.DataFrame:
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
basic_info = self._fetch_basic_info(symbol)
|
||||||
|
acc_date = basic_info.get("accounting_date", "1231")
|
||||||
|
|
||||||
|
current_year = int(time.strftime("%Y"))
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
year_str = str(current_year - i)
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": "annual_cum_dividend", "indiparams": [year_str, "CNY"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty and 'annual_cum_dividend' in df.columns:
|
||||||
|
val = df['annual_cum_dividend'].iloc[0]
|
||||||
|
if pd.notna(val) and val != 0:
|
||||||
|
results.append({
|
||||||
|
'date_str': f"{year_str}{acc_date}",
|
||||||
|
'dividends': float(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df_div = pd.DataFrame(results)
|
||||||
|
self._save_raw_data(df_div, symbol, "dividends_raw")
|
||||||
|
return df_div
|
||||||
|
|
||||||
|
def get_repurchases(self, symbol: str) -> pd.DataFrame:
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
basic_info = self._fetch_basic_info(symbol)
|
||||||
|
acc_date = basic_info.get("accounting_date", "1231")
|
||||||
|
mm = acc_date[:2]
|
||||||
|
dd = acc_date[2:]
|
||||||
|
fmt_mm_dd = f"{mm}-{dd}"
|
||||||
|
|
||||||
|
current_year = int(time.strftime("%Y"))
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
target_year = current_year - i
|
||||||
|
start_date = f"{target_year - 1}-{fmt_mm_dd}"
|
||||||
|
end_date = f"{target_year}-{fmt_mm_dd}"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": "repur_num_new", "indiparams": [start_date, end_date, "1"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty and 'repur_num_new' in df.columns:
|
||||||
|
val = df['repur_num_new'].iloc[0]
|
||||||
|
if pd.notna(val) and val != 0:
|
||||||
|
results.append({
|
||||||
|
'date_str': f"{target_year}{acc_date}",
|
||||||
|
'repurchases': float(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df_repur = pd.DataFrame(results)
|
||||||
|
self._save_raw_data(df_repur, symbol, "repurchases_raw")
|
||||||
|
return df_repur
|
||||||
|
|
||||||
|
def get_employee_count(self, symbol: str) -> pd.DataFrame:
|
||||||
|
code = self._get_ifind_code(symbol)
|
||||||
|
basic_info = self._fetch_basic_info(symbol)
|
||||||
|
acc_date = basic_info.get("accounting_date", "1231")
|
||||||
|
mm = acc_date[:2]
|
||||||
|
dd = acc_date[2:]
|
||||||
|
|
||||||
|
current_year = int(time.strftime("%Y"))
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
target_year = current_year - i
|
||||||
|
target_date = f"{target_year}-{mm}-{dd}"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"codes": code,
|
||||||
|
"indipara": [
|
||||||
|
{"indicator": "staff_num", "indiparams": [target_date]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
res = self.cli.post("basic_data_service", params)
|
||||||
|
df = self._parse_ifind_tables(res)
|
||||||
|
|
||||||
|
if not df.empty and 'staff_num' in df.columns:
|
||||||
|
val = df['staff_num'].iloc[0]
|
||||||
|
if pd.notna(val) and val != 0:
|
||||||
|
results.append({
|
||||||
|
'date_str': f"{target_year}{acc_date}",
|
||||||
|
'employee_count': float(val)
|
||||||
|
})
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df_emp = pd.DataFrame(results)
|
||||||
|
self._save_raw_data(df_emp, symbol, "employee_count_raw")
|
||||||
|
return df_emp
|
||||||
@ -15,18 +15,18 @@ class JP_ReportGenerator(BaseReporter):
|
|||||||
('ROIC', 'ROCE/ROIC', 'percent'),
|
('ROIC', 'ROCE/ROIC', 'percent'),
|
||||||
('GrossMargin', '毛利率', 'percent'),
|
('GrossMargin', '毛利率', 'percent'),
|
||||||
('NetMargin', '净利润率', 'percent'),
|
('NetMargin', '净利润率', 'percent'),
|
||||||
('revenue', '收入(亿)', 'currency_yi'),
|
('revenue', '收入(亿 CNY)', 'currency_yi'),
|
||||||
('RevenueGrowth', '收入增速', 'percent_color'),
|
('RevenueGrowth', '收入增速', 'percent_color'),
|
||||||
('net_income', '净利润(亿)', 'currency_yi'),
|
('net_income', '净利润(亿 CNY)', 'currency_yi'),
|
||||||
('NetIncomeGrowth', '净利润增速', 'percent_color'),
|
('NetIncomeGrowth', '净利润增速', 'percent_color'),
|
||||||
('ocf', '经营净现金流(亿)', 'currency_yi_color'),
|
('ocf', '经营净现金流(亿 CNY)', 'currency_yi_color'),
|
||||||
('Capex', '资本开支(亿)', 'currency_yi'),
|
('Capex', '资本开支(亿 CNY)', 'currency_yi'),
|
||||||
('FCF', '自由现金流(亿)', 'currency_yi_compare'),
|
('FCF', '自由现金流(亿 CNY)', 'currency_yi_compare'),
|
||||||
('dividends', '分红(亿)', 'currency_yi'),
|
('dividends', '分红(亿 CNY)', 'currency_yi'),
|
||||||
('repurchases', '回购(亿)', 'currency_yi'),
|
('repurchases', '回购(亿 CNY)', 'currency_yi'),
|
||||||
('total_assets', '总资产(亿)', 'currency_yi'),
|
('total_assets', '总资产(亿 CNY)', 'currency_yi'),
|
||||||
('total_equity', '净资产(亿)', 'currency_yi'),
|
('total_equity', '净资产(亿 CNY)', 'currency_yi'),
|
||||||
('goodwill', '商誉(亿)', 'currency_yi')
|
('goodwill', '商誉(亿 CNY)', 'currency_yi')
|
||||||
],
|
],
|
||||||
"费用指标": [
|
"费用指标": [
|
||||||
('SellingRatio', '销售费用率', 'percent'),
|
('SellingRatio', '销售费用率', 'percent'),
|
||||||
@ -62,13 +62,13 @@ class JP_ReportGenerator(BaseReporter):
|
|||||||
],
|
],
|
||||||
"人均效率": [
|
"人均效率": [
|
||||||
('Employees', '员工人数', 'int'),
|
('Employees', '员工人数', 'int'),
|
||||||
('RevenuePerEmp', '人均创收(万)', 'currency_wan'),
|
('RevenuePerEmp', '人均创收(万 CNY)', 'currency_wan'),
|
||||||
('ProfitPerEmp', '人均创利(万)', 'currency_wan'),
|
('ProfitPerEmp', '人均创利(万 CNY)', 'currency_wan'),
|
||||||
('AvgWage', '人均薪酬(万)', 'currency_wan'),
|
('AvgWage', '人均薪酬(万 CNY)', 'currency_wan'),
|
||||||
],
|
],
|
||||||
"市场表现": [
|
"市场表现": [
|
||||||
('Price', '股价', 'float'),
|
('Price', '股价 (CNY)', 'float'),
|
||||||
('MarketCap', '市值(亿)', 'currency_yi_market'),
|
('MarketCap', '市值(亿 CNY)', 'currency_yi_market'),
|
||||||
('PE', 'PE', 'float'),
|
('PE', 'PE', 'float'),
|
||||||
('PB', 'PB', 'float'),
|
('PB', 'PB', 'float'),
|
||||||
('Shareholders', '股东户数', 'int'),
|
('Shareholders', '股东户数', 'int'),
|
||||||
|
|||||||
@ -1,9 +1,233 @@
|
|||||||
|
from .base_generator import BaseReporter
|
||||||
|
import pandas as pd
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
class VN_ReportGenerator(BaseReporter):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.indicators = {
|
||||||
|
"主要指标": [
|
||||||
|
('ROE', 'ROE', 'percent'),
|
||||||
|
('ROA', 'ROA', 'percent'),
|
||||||
|
('ROIC', 'ROCE/ROIC', 'percent'),
|
||||||
|
('GrossMargin', '毛利率', 'percent'),
|
||||||
|
('NetMargin', '净利润率', 'percent'),
|
||||||
|
('revenue', '收入(亿 CNY)', 'currency_yi'),
|
||||||
|
('RevenueGrowth', '收入增速', 'percent_color'),
|
||||||
|
('net_income', '净利润(亿 CNY)', 'currency_yi'),
|
||||||
|
('NetIncomeGrowth', '净利润增速', 'percent_color'),
|
||||||
|
('ocf', '经营净现金流(亿 CNY)', 'currency_yi_color'),
|
||||||
|
('Capex', '资本开支(亿 CNY)', 'currency_yi'),
|
||||||
|
('FCF', '自由现金流(亿 CNY)', 'currency_yi_compare'),
|
||||||
|
('dividends', '分红(亿 CNY)', 'currency_yi'),
|
||||||
|
('repurchases', '回购(亿 CNY)', 'currency_yi'),
|
||||||
|
('total_assets', '总资产(亿 CNY)', 'currency_yi'),
|
||||||
|
('total_equity', '净资产(亿 CNY)', 'currency_yi'),
|
||||||
|
('goodwill', '商誉(亿 CNY)', 'currency_yi')
|
||||||
|
],
|
||||||
|
"费用指标": [
|
||||||
|
('SellingRatio', '销售费用率', 'percent'),
|
||||||
|
('AdminRatio', '管理费用率', 'percent'),
|
||||||
|
('SgaRatio', 'SG&A比例', 'percent'),
|
||||||
|
('RDRatio', '研发费用率', 'percent'),
|
||||||
|
('OtherExpenseRatio', '其他费用率', 'percent'),
|
||||||
|
('DepreciationRatio', '折旧费用占比', 'percent'),
|
||||||
|
('TaxRate', '所得税率', 'percent'),
|
||||||
|
],
|
||||||
|
"资产占比": [
|
||||||
|
('CashRatio', '现金占比', 'percent_alert_30'),
|
||||||
|
('InventoryRatio', '库存占比', 'percent'),
|
||||||
|
('ReceivablesRatio', '应收款占比', 'percent'),
|
||||||
|
('PrepaymentRatio', '预付款占比', 'percent'),
|
||||||
|
('FixedAssetsRatio', '固定资产占比', 'percent'),
|
||||||
|
('LongTermInvestmentRatio', '长期投资占比', 'percent'),
|
||||||
|
('GoodwillRatio', '商誉占比', 'percent'),
|
||||||
|
('OtherAssetsRatio', '其他资产占比', 'percent'),
|
||||||
|
('PayablesRatio', '应付款占比', 'percent'),
|
||||||
|
('AdvanceReceiptsRatio', '预收款占比', 'percent'),
|
||||||
|
('ShortTermDebtRatio', '短期借款占比', 'percent'),
|
||||||
|
('LongTermDebtRatio', '长期借款占比', 'percent'),
|
||||||
|
('OperatingAssetsRatio', '运营资产占比', 'percent'),
|
||||||
|
('InterestBearingDebtRatio', '有息负债率', 'percent'),
|
||||||
|
],
|
||||||
|
"周转能力": [
|
||||||
|
('InventoryDays', '存货周转天数', 'int'),
|
||||||
|
('ReceivablesDays', '应收款周转天数', 'int_alert_90'),
|
||||||
|
('PayablesDays', '应付款周转天数', 'int'),
|
||||||
|
('FixedAssetsTurnover', '固定资产周转率', 'float'),
|
||||||
|
('TotalAssetTurnover', '总资产周转率', 'float'),
|
||||||
|
],
|
||||||
|
"人均效率": [
|
||||||
|
('Employees', '员工人数', 'int'),
|
||||||
|
('RevenuePerEmp', '人均创收(万 CNY)', 'currency_wan'),
|
||||||
|
('ProfitPerEmp', '人均创利(万 CNY)', 'currency_wan'),
|
||||||
|
('AvgWage', '人均薪酬(万 CNY)', 'currency_wan'),
|
||||||
|
],
|
||||||
|
"市场表现": [
|
||||||
|
('Price', '股价 (CNY)', 'float'),
|
||||||
|
('MarketCap', '市值(亿 CNY)', 'currency_yi_market'),
|
||||||
|
('PE', 'PE', 'float'),
|
||||||
|
('PB', 'PB', 'float'),
|
||||||
|
('Shareholders', '股东户数', 'int'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _preprocess_data(self, df, market):
|
||||||
|
df = super()._preprocess_data(df, market)
|
||||||
|
if not df.empty:
|
||||||
|
dates = pd.to_datetime(df['date_str'], format='%Y%m%d')
|
||||||
|
latest_year = dates.dt.year.max()
|
||||||
|
is_dec = dates.dt.month == 12
|
||||||
|
is_latest = df.index == df.index[0]
|
||||||
|
df = df[is_dec | is_latest]
|
||||||
|
return df
|
||||||
|
|
||||||
|
def _format_period_label(self, date_value):
|
||||||
|
if pd.isna(date_value):
|
||||||
|
return "-"
|
||||||
|
date_str = str(date_value)
|
||||||
|
if len(date_str) != 8:
|
||||||
|
return date_str
|
||||||
|
year = date_str[:4]
|
||||||
|
try:
|
||||||
|
return f"{year}A"
|
||||||
|
except ValueError:
|
||||||
|
return f"{year}A"
|
||||||
|
|
||||||
|
def _get_headers(self, df):
|
||||||
|
return [self._format_period_label(date_value) for date_value in df['date_str']]
|
||||||
|
|
||||||
|
def _generate_md_company_info(self, symbol, metrics, market):
|
||||||
|
today_str = datetime.date.today().strftime("%Y-%m-%d")
|
||||||
|
name = metrics.get('name', '')
|
||||||
|
raw_list_date = metrics.get('list_date', '')
|
||||||
|
if isinstance(raw_list_date, str) and len(raw_list_date) == 8:
|
||||||
|
list_date = f"{raw_list_date[:4]}-{raw_list_date[4:6]}-{raw_list_date[6:]}"
|
||||||
|
else:
|
||||||
|
list_date = raw_list_date
|
||||||
|
pe = metrics.get('pe', 0) or 0
|
||||||
|
pb = metrics.get('pb', 0) or 0
|
||||||
|
div = metrics.get('dividend_yield', 0) or 0
|
||||||
|
md = []
|
||||||
|
md.append(f"# {name} ({symbol}) - Financial Report")
|
||||||
|
md.append(f"*Report generated on: {today_str}*\n")
|
||||||
|
md.append("| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |")
|
||||||
|
md.append("|:---|:---|:---|:---|:---|:---|")
|
||||||
|
md.append(f"| {symbol} | {name} | {list_date} | {pe:.2f} | {pb:.2f} | {div:.2f}% |")
|
||||||
|
return "\n".join(md)
|
||||||
|
|
||||||
|
def generate_report(self, df_analysis, symbol, market, metrics, output_dir):
|
||||||
|
md_content = self._generate_markdown_content(df_analysis, market, symbol, metrics)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
md_path = os.path.join(output_dir, "report.md")
|
||||||
|
with open(md_path, "w", encoding='utf-8') as f:
|
||||||
|
f.write(md_content)
|
||||||
|
|
||||||
|
df_for_html = df_analysis.copy() if isinstance(df_analysis, pd.DataFrame) else pd.DataFrame()
|
||||||
|
if not df_for_html.empty:
|
||||||
|
df_for_html = self._preprocess_data(df_for_html, market)
|
||||||
|
headers = self._get_headers(df_for_html)
|
||||||
|
else:
|
||||||
|
headers = []
|
||||||
|
html_content = self._build_html_content(symbol, metrics, headers, df_for_html)
|
||||||
|
final_html = self.to_html(symbol, html_content)
|
||||||
|
|
||||||
|
html_path = os.path.join(output_dir, "report.html")
|
||||||
|
with open(html_path, "w", encoding='utf-8') as f:
|
||||||
|
f.write(final_html)
|
||||||
|
|
||||||
|
def _build_html_content(self, symbol, metrics, headers, df):
|
||||||
|
today_str = datetime.date.today().strftime("%Y-%m-%d")
|
||||||
|
name = metrics.get('name') or symbol
|
||||||
|
raw_list_date = metrics.get('list_date', '')
|
||||||
|
if isinstance(raw_list_date, str) and len(raw_list_date) == 8:
|
||||||
|
list_date = f"{raw_list_date[:4]}-{raw_list_date[4:6]}-{raw_list_date[6:]}"
|
||||||
|
else:
|
||||||
|
list_date = raw_list_date or "-"
|
||||||
|
pe = metrics.get('pe', 0) or 0
|
||||||
|
pb = metrics.get('pb', 0) or 0
|
||||||
|
div = metrics.get('dividend_yield', 0) or 0
|
||||||
|
|
||||||
|
company_table = f"""
|
||||||
|
<table class="company-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>代码</th>
|
||||||
|
<th>简称</th>
|
||||||
|
<th>上市日期</th>
|
||||||
|
<th>PE</th>
|
||||||
|
<th>PB</th>
|
||||||
|
<th>股息率(%)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{symbol}</td>
|
||||||
|
<td>{name}</td>
|
||||||
|
<td>{list_date}</td>
|
||||||
|
<td>{pe:.2f}</td>
|
||||||
|
<td>{pb:.2f}</td>
|
||||||
|
<td>{div:.2f}%</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if df is None or df.empty or not headers:
|
||||||
|
metrics_table = "<p class=\"no-data\">暂无可用财务指标</p>"
|
||||||
|
else:
|
||||||
|
header_cells = "".join([f"<th>{header}</th>" for header in headers])
|
||||||
|
data_column_count = max(len(headers), 1)
|
||||||
|
rows_html = []
|
||||||
|
for group_name, items in self.indicators.items():
|
||||||
|
rows_html.append(
|
||||||
|
f"<tr class=\"section-row\">"
|
||||||
|
f"<td class=\"section-label\">{group_name}</td>"
|
||||||
|
f"<td class=\"section-spacer\" colspan=\"{data_column_count}\"></td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
for key, label, fmt_type in items:
|
||||||
|
value_cells = [f"<td class=\"metric-name\">{label}</td>"]
|
||||||
|
for _, row_series in df.iterrows():
|
||||||
|
value_cells.append(f"<td>{self._format_value(row_series.get(key), fmt_type)}</td>")
|
||||||
|
row_class = "other-assets-row" if key == 'OtherAssetsRatio' else ""
|
||||||
|
if row_class:
|
||||||
|
rows_html.append(f"<tr class=\"{row_class}\">{''.join(value_cells)}</tr>")
|
||||||
|
else:
|
||||||
|
rows_html.append(f"<tr>{''.join(value_cells)}</tr>")
|
||||||
|
rows_markup = "\n".join(rows_html)
|
||||||
|
metrics_table = f"""
|
||||||
|
<table class="metrics-table" data-table="metrics" data-scrollable="true">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>指标</th>
|
||||||
|
{header_cells}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows_markup}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html_sections = [
|
||||||
|
f"<h1>{name} ({symbol}) - Financial Report</h1>",
|
||||||
|
f"<p><em>Report generated on: {today_str}</em></p>",
|
||||||
|
company_table,
|
||||||
|
'<div class="table-gap"></div>',
|
||||||
|
metrics_table
|
||||||
|
]
|
||||||
|
return "\n".join(html_sections)
|
||||||
|
|
||||||
|
def to_html(self, symbol, html_content):
|
||||||
|
styled_html = '''
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>00700.HK Financial Report</title>
|
<title>{symbol} Financial Report</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #f5f6fa;
|
--bg: #f5f6fa;
|
||||||
@ -204,107 +428,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="report-container">
|
<div class="report-container">
|
||||||
<h1>腾讯控股有限公司 (00700.HK) - Financial Report</h1>
|
{html_content}
|
||||||
<p><em>Report generated on: 2026-01-03</em></p>
|
|
||||||
|
|
||||||
<table class="company-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>代码</th>
|
|
||||||
<th>简称</th>
|
|
||||||
<th>上市日期</th>
|
|
||||||
<th>年结日</th>
|
|
||||||
<th>市值(亿)</th>
|
|
||||||
<th>PE</th>
|
|
||||||
<th>PB</th>
|
|
||||||
<th>股息率(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>00700.HK</td>
|
|
||||||
<td>腾讯控股有限公司</td>
|
|
||||||
<td>2004-06-16</td>
|
|
||||||
<td>1231</td>
|
|
||||||
<td>42043.21</td>
|
|
||||||
<td>20.32</td>
|
|
||||||
<td>3.77</td>
|
|
||||||
<td>0.89%</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="table-gap"></div>
|
|
||||||
|
|
||||||
<table class="metrics-table" data-table="metrics" data-scrollable="true">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>指标</th>
|
|
||||||
<th>2025H1</th><th>2024A</th><th>2023A</th><th>2022A</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr class="section-row"><td class="section-label">主要指标</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">ROE</td><td>9.28%</td><td>19.93%</td><td>14.25%</td><td>26.09%</td></tr>
|
|
||||||
<tr><td class="metric-name">ROA</td><td>5.14%</td><td>10.90%</td><td>7.30%</td><td>11.93%</td></tr>
|
|
||||||
<tr><td class="metric-name">ROCE/ROIC</td><td>6.21%</td><td>12.62%</td><td>10.79%</td><td>16.36%</td></tr>
|
|
||||||
<tr><td class="metric-name">毛利率</td><td>56.38%</td><td>52.90%</td><td>48.13%</td><td>43.05%</td></tr>
|
|
||||||
<tr><td class="metric-name">净利润率</td><td>28.38%</td><td>29.39%</td><td>18.92%</td><td>33.95%</td></tr>
|
|
||||||
<tr><td class="metric-name">收入(亿)</td><td>3,645.26</td><td>6,602.57</td><td>6,090.15</td><td>5,545.52</td></tr>
|
|
||||||
<tr><td class="metric-name">收入增速</td><td>-</td><td>8.41%</td><td>9.82%</td><td>-</td></tr>
|
|
||||||
<tr><td class="metric-name">净利润(亿)</td><td>1,034.49</td><td>1,940.73</td><td>1,152.16</td><td>1,882.43</td></tr>
|
|
||||||
<tr><td class="metric-name">净利润增速</td><td>-</td><td>68.44%</td><td>-38.79%</td><td>-</td></tr>
|
|
||||||
<tr><td class="metric-name">经营净现金流(亿)</td><td>1,512.65</td><td>2,585.21</td><td>2,219.62</td><td>1,460.91</td></tr>
|
|
||||||
<tr><td class="metric-name">资本开支(亿)</td><td>574.57</td><td>960.48</td><td>474.07</td><td>508.50</td></tr>
|
|
||||||
<tr><td class="metric-name">自由现金流(亿)</td><td>938.08</td><td>1,624.73</td><td>1,745.55</td><td>952.41</td></tr>
|
|
||||||
<tr><td class="metric-name">分红(亿)</td><td>375.35</td><td>288.59</td><td>209.83</td><td>129.52</td></tr>
|
|
||||||
<tr><td class="metric-name">回购(亿)</td><td>-</td><td>1,120.03</td><td>494.33</td><td>337.94</td></tr>
|
|
||||||
<tr><td class="metric-name">总资产(亿)</td><td>20,133.10</td><td>17,809.95</td><td>15,772.46</td><td>15,781.31</td></tr>
|
|
||||||
<tr><td class="metric-name">净资产(亿)</td><td>11,146.39</td><td>9,735.48</td><td>8,085.91</td><td>7,213.91</td></tr>
|
|
||||||
<tr><td class="metric-name">商誉(亿)</td><td>2,158.32</td><td>1,961.27</td><td>1,777.27</td><td>1,618.02</td></tr>
|
|
||||||
<tr class="section-row"><td class="section-label">费用指标</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">销售费用率</td><td>4.74%</td><td>5.51%</td><td>5.62%</td><td>5.27%</td></tr>
|
|
||||||
<tr><td class="metric-name">管理费用率</td><td>17.99%</td><td>17.08%</td><td>17.00%</td><td>19.24%</td></tr>
|
|
||||||
<tr><td class="metric-name">SG&A比例</td><td>22.73%</td><td>22.59%</td><td>22.62%</td><td>24.51%</td></tr>
|
|
||||||
<tr><td class="metric-name">研发费用率</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
|
||||||
<tr><td class="metric-name">其他费用率</td><td>5.27%</td><td>0.91%</td><td>6.59%</td><td>-15.40%</td></tr>
|
|
||||||
<tr><td class="metric-name">折旧费用占比</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
|
||||||
<tr><td class="metric-name">所得税率</td><td>19.51%</td><td>18.83%</td><td>27.30%</td><td>10.26%</td></tr>
|
|
||||||
<tr class="section-row"><td class="section-label">资产占比</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">现金占比</td><td>18.90%</td><td>19.27%</td><td>24.04%</td><td>18.42%</td></tr>
|
|
||||||
<tr><td class="metric-name">库存占比</td><td>0.02%</td><td>0.02%</td><td>0.03%</td><td>0.15%</td></tr>
|
|
||||||
<tr><td class="metric-name">应收款占比</td><td>2.55%</td><td>2.71%</td><td>2.95%</td><td>2.88%</td></tr>
|
|
||||||
<tr><td class="metric-name">预付款占比</td><td>1.34%</td><td>1.76%</td><td>1.76%</td><td>1.55%</td></tr>
|
|
||||||
<tr><td class="metric-name">固定资产占比</td><td>8.58%</td><td>7.48%</td><td>6.62%</td><td>6.58%</td></tr>
|
|
||||||
<tr><td class="metric-name">长期投资占比</td><td>35.10%</td><td>33.09%</td><td>29.20%</td><td>27.34%</td></tr>
|
|
||||||
<tr><td class="metric-name">商誉占比</td><td>10.72%</td><td>11.01%</td><td>11.27%</td><td>10.25%</td></tr>
|
|
||||||
<tr class="other-assets-row"><td class="metric-name">其他资产占比</td><td>22.80%</td><td>24.66%</td><td>24.12%</td><td>32.83%</td></tr>
|
|
||||||
<tr><td class="metric-name">应付款占比</td><td>7.12%</td><td>7.15%</td><td>7.30%</td><td>6.52%</td></tr>
|
|
||||||
<tr><td class="metric-name">预收款占比</td><td>0.00%</td><td>0.00%</td><td>0.00%</td><td>0.00%</td></tr>
|
|
||||||
<tr><td class="metric-name">短期借款占比</td><td>6.09%</td><td>6.01%</td><td>4.64%</td><td>1.52%</td></tr>
|
|
||||||
<tr><td class="metric-name">长期借款占比</td><td>32.68%</td><td>31.90%</td><td>38.19%</td><td>40.75%</td></tr>
|
|
||||||
<tr><td class="metric-name">运营资产占比</td><td>-3.22%</td><td>-2.66%</td><td>-2.55%</td><td>-1.94%</td></tr>
|
|
||||||
<tr><td class="metric-name">有息负债率</td><td>38.77%</td><td>37.91%</td><td>42.83%</td><td>42.27%</td></tr>
|
|
||||||
<tr class="section-row"><td class="section-label">周转能力</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">存货周转天数</td><td>0</td><td>0</td><td>0</td><td>2</td></tr>
|
|
||||||
<tr><td class="metric-name">应收款周转天数</td><td>25</td><td>26</td><td>27</td><td>29</td></tr>
|
|
||||||
<tr><td class="metric-name">应付款周转天数</td><td>164</td><td>149</td><td>132</td><td>118</td></tr>
|
|
||||||
<tr><td class="metric-name">固定资产周转率</td><td>4.22</td><td>4.95</td><td>5.83</td><td>5.34</td></tr>
|
|
||||||
<tr><td class="metric-name">总资产周转率</td><td>0.36</td><td>0.37</td><td>0.39</td><td>0.35</td></tr>
|
|
||||||
<tr class="section-row"><td class="section-label">人均效率</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">员工人数</td><td>-</td><td>110,558</td><td>105,417</td><td>108,436</td></tr>
|
|
||||||
<tr><td class="metric-name">人均创收(万)</td><td>-</td><td>597.20</td><td>577.72</td><td>511.41</td></tr>
|
|
||||||
<tr><td class="metric-name">人均创利(万)</td><td>-</td><td>175.54</td><td>109.30</td><td>173.60</td></tr>
|
|
||||||
<tr><td class="metric-name">人均薪酬(万)</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
|
||||||
<tr class="section-row"><td class="section-label">市场表现</td><td class="section-spacer" colspan="4"></td></tr>
|
|
||||||
<tr><td class="metric-name">股价</td><td>467.83</td><td>388.01</td><td>266.07</td><td>298.35</td></tr>
|
|
||||||
<tr><td class="metric-name">市值(亿)</td><td>42,043</td><td>35,623</td><td>25,231</td><td>28,549</td></tr>
|
|
||||||
<tr><td class="metric-name">PE</td><td>20.32</td><td>18.36</td><td>21.90</td><td>15.17</td></tr>
|
|
||||||
<tr><td class="metric-name">PB</td><td>3.77</td><td>3.66</td><td>3.12</td><td>3.96</td></tr>
|
|
||||||
<tr><td class="metric-name">股东户数</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@ -384,7 +508,7 @@
|
|||||||
'应收款周转天数': (cell) => {
|
'应收款周转天数': (cell) => {
|
||||||
const value = parseValue(cell.textContent);
|
const value = parseValue(cell.textContent);
|
||||||
if (value !== null && value > 90) {
|
if (value !== null && value > 90) {
|
||||||
cell.classList.add('bg-red', 'font-red');
|
cell.classList.add('bg-red', 'font-red');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'现金占比': highlightIfOverThirtyPercent,
|
'现金占比': highlightIfOverThirtyPercent,
|
||||||
@ -446,4 +570,6 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
'''
|
||||||
|
final_html = styled_html.replace('{symbol}', symbol).replace('{html_content}', html_content)
|
||||||
|
return final_html
|
||||||
76
src/strategies/vn_strategy.py
Normal file
76
src/strategies/vn_strategy.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from .base_strategy import BaseStrategy
|
||||||
|
from fetchers.factory import FetcherFactory
|
||||||
|
from analysis.vn_analyzer import VN_Analyzer
|
||||||
|
from reporting.vn_report_generator import VN_ReportGenerator
|
||||||
|
from storage.file_io import DataStorage
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
class VN_Strategy(BaseStrategy):
|
||||||
|
def __init__(self, stock_code, ifind_refresh_token):
|
||||||
|
super().__init__(stock_code)
|
||||||
|
self.refresh_token = ifind_refresh_token
|
||||||
|
self.fetcher = FetcherFactory.get_fetcher('VN', ifind_refresh_token=self.refresh_token)
|
||||||
|
self.analyzer = VN_Analyzer()
|
||||||
|
self.reporter = VN_ReportGenerator()
|
||||||
|
self.storage = DataStorage()
|
||||||
|
self.raw_data = {}
|
||||||
|
self.analysis_result = None
|
||||||
|
|
||||||
|
def fetch_data(self):
|
||||||
|
print(f"Fetching data for VN market, stock: {self.stock_code}")
|
||||||
|
# Fetch Financial Statements
|
||||||
|
self.raw_data['income'] = self.fetcher.get_income_statement(self.stock_code)
|
||||||
|
self.raw_data['balance'] = self.fetcher.get_balance_sheet(self.stock_code)
|
||||||
|
self.raw_data['cashflow'] = self.fetcher.get_cash_flow(self.stock_code)
|
||||||
|
|
||||||
|
# Rename 'end_date' to 'date' for analyzer compatibility
|
||||||
|
for key in ['income', 'balance', 'cashflow']:
|
||||||
|
if not self.raw_data[key].empty and 'end_date' in self.raw_data[key].columns:
|
||||||
|
self.raw_data[key] = self.raw_data[key].rename(columns={'end_date': 'date'})
|
||||||
|
|
||||||
|
# Fetch Market Metrics (Real-time and static)
|
||||||
|
self.raw_data['metrics'] = self.fetcher.get_market_metrics(self.stock_code)
|
||||||
|
|
||||||
|
# Fetch Historical Metrics (Price and Market Cap)
|
||||||
|
dates = []
|
||||||
|
if not self.raw_data['income'].empty and 'date' in self.raw_data['income'].columns:
|
||||||
|
dates = self.raw_data['income']['date'].tolist()
|
||||||
|
|
||||||
|
self.raw_data['historical_metrics'] = self.fetcher.get_historical_metrics(self.stock_code, dates)
|
||||||
|
|
||||||
|
# Fetch Dividends
|
||||||
|
self.raw_data['dividends'] = self.fetcher.get_dividends(self.stock_code)
|
||||||
|
|
||||||
|
# Fetch Repurchases
|
||||||
|
self.raw_data['repurchases'] = self.fetcher.get_repurchases(self.stock_code)
|
||||||
|
|
||||||
|
# Fetch Employee Count
|
||||||
|
self.raw_data['employee_count'] = self.fetcher.get_employee_count(self.stock_code)
|
||||||
|
|
||||||
|
def analyze_data(self):
|
||||||
|
print(f"Analyzing data for VN market, stock: {self.stock_code}")
|
||||||
|
self.analysis_result = self.analyzer.process_data(
|
||||||
|
self.raw_data['income'],
|
||||||
|
self.raw_data['balance'],
|
||||||
|
self.raw_data['cashflow'],
|
||||||
|
self.raw_data['metrics'],
|
||||||
|
self.raw_data.get('historical_metrics'),
|
||||||
|
self.raw_data.get('dividends'),
|
||||||
|
self.raw_data.get('repurchases'),
|
||||||
|
self.raw_data.get('employee_count')
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_report(self):
|
||||||
|
print(f"Generating report for VN market, stock: {self.stock_code}")
|
||||||
|
if self.analysis_result is not None and not self.analysis_result.empty:
|
||||||
|
output_dir = os.path.join("data", 'VN', self.stock_code)
|
||||||
|
self.reporter.generate_report(
|
||||||
|
df_analysis=self.analysis_result,
|
||||||
|
symbol=self.stock_code,
|
||||||
|
market='VN',
|
||||||
|
metrics=self.raw_data['metrics'],
|
||||||
|
output_dir=output_dir
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("No analysis result to generate report.")
|
||||||
105
test_hk_fetcher_logic.py
Normal file
105
test_hk_fetcher_logic.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pandas as pd
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from src.fetchers.hk_fetcher import HkFetcher
|
||||||
|
|
||||||
|
# Mock the IFindClient to avoid actual network requests and credentials
|
||||||
|
class MockIFindClient:
|
||||||
|
def __init__(self, refresh_token):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post(self, endpoint, params):
|
||||||
|
# Simulate data availability logic
|
||||||
|
# If querying for 20261231, return empty
|
||||||
|
# If querying for 20251231, return data
|
||||||
|
|
||||||
|
# Extract date from params
|
||||||
|
date = "unknown"
|
||||||
|
if "indipara" in params:
|
||||||
|
for item in params["indipara"]:
|
||||||
|
if "indiparams" in item and len(item["indiparams"]) > 0:
|
||||||
|
param0 = item["indiparams"][0]
|
||||||
|
if len(param0) == 8: # YYYYMMDD
|
||||||
|
date = param0
|
||||||
|
break
|
||||||
|
|
||||||
|
# Test Case 1: Detect year logic
|
||||||
|
if "20261231" in date:
|
||||||
|
return {"tables": [{"time": [], "table": {}}]} # Empty
|
||||||
|
|
||||||
|
if "20251231" in date:
|
||||||
|
return {
|
||||||
|
"tables": [{
|
||||||
|
"time": ["2025-12-31"],
|
||||||
|
"table": {
|
||||||
|
"revenue_oas": [1000],
|
||||||
|
"roe": [15.5],
|
||||||
|
"total_oi": [5000]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if "20241231" in date:
|
||||||
|
return {
|
||||||
|
"tables": [{
|
||||||
|
"time": ["2024-12-31"],
|
||||||
|
"table": {
|
||||||
|
"revenue_oas": [900],
|
||||||
|
"roe": [14.0],
|
||||||
|
"total_oi": [4000]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"tables": []}
|
||||||
|
|
||||||
|
def test_hk_fetcher_year_detection():
|
||||||
|
print("Testing HK Fetcher Year Detection Logic...")
|
||||||
|
|
||||||
|
# Mock time.strftime to return 2026
|
||||||
|
import time
|
||||||
|
original_strftime = time.strftime
|
||||||
|
time.strftime = MagicMock(return_value="2026")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fetcher = HkFetcher("fake_token")
|
||||||
|
# Replace the client with our mock
|
||||||
|
fetcher.cli = MockIFindClient("fake_token")
|
||||||
|
|
||||||
|
# 1. Test get_income_statement logic
|
||||||
|
print("\nTesting _fetch_financial_data_annual (via income statement)...")
|
||||||
|
# We expect it to try 2026 (fail), then 2025 (succeed), then fetch 2025-2021
|
||||||
|
df_income = fetcher.get_income_statement("0700.HK")
|
||||||
|
|
||||||
|
if not df_income.empty:
|
||||||
|
dates = df_income['end_date'].tolist()
|
||||||
|
print(f"Fetched Income Statement Dates: {dates}")
|
||||||
|
if "20251231" in dates and "20261231" not in dates:
|
||||||
|
print("PASS: Correctly anchored to 2025 instead of 2026.")
|
||||||
|
else:
|
||||||
|
print(f"FAIL: Logic incorrect. Dates found: {dates}")
|
||||||
|
else:
|
||||||
|
print("FAIL: No data returned.")
|
||||||
|
|
||||||
|
# 2. Test get_financial_ratios logic
|
||||||
|
print("\nTesting get_financial_ratios...")
|
||||||
|
df_ratios = fetcher.get_financial_ratios("0700.HK")
|
||||||
|
|
||||||
|
if not df_ratios.empty:
|
||||||
|
dates = df_ratios['end_date'].tolist()
|
||||||
|
print(f"Fetched Ratios Dates: {dates}")
|
||||||
|
if "20251231" in dates and "20261231" not in dates:
|
||||||
|
print("PASS: Correctly anchored to 2025 instead of 2026.")
|
||||||
|
else:
|
||||||
|
print(f"FAIL: Logic incorrect. Dates found: {dates}")
|
||||||
|
else:
|
||||||
|
print("FAIL: No data returned.")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore time
|
||||||
|
time.strftime = original_strftime
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_hk_fetcher_year_detection()
|
||||||
Loading…
Reference in New Issue
Block a user