用ifind HTTP API实现了日股的数据获取

This commit is contained in:
xucheng 2025-12-21 16:39:35 +08:00
parent 97b0f83d5c
commit 943dda784f
55 changed files with 9828 additions and 283 deletions

2
.env
View File

@ -1,2 +1,4 @@
TUSHARE_TOKEN=f62b415de0a5a947fcb693b66cd299dd6242868bf04ad687800c7f3f TUSHARE_TOKEN=f62b415de0a5a947fcb693b66cd299dd6242868bf04ad687800c7f3f
ALPHA_VANTAGE_KEY=2ROWPV7BMW6JSG0Y ALPHA_VANTAGE_KEY=2ROWPV7BMW6JSG0Y
JQUANTS_REFRESH_TOKEN=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.AzlbVPyDZZ1OQc35ICxAtQeb2j51DhSGy69dihg6lEznIgRNgHn5wmhFOnNz_Nh7z-1AJj66d2T4Naj76lRpHhXk5FCViUzZqaNkz9Q_TnoHt90Pp_Op-1cHORbjfCGpW3cvT9En6UZxodjDliRzqCpqJ52UCLLWUJfJIwyFy5-QcWTEOGOdVRXTHtihy-w7ylw7ocUj3gd34NLGPr45yG0fTmo7om_MWBPbVsWvWOjdvrEtee43xEuZTheqbRgU3IRxa7ff1zIRDSrbH-nWIBPsh-XXV2r2OZTBARairjnUA0Ikma224UF_m5acpPwcs4UwrbeKyq13K3N1f3ohDA.6Zi0U52Kps9h3ezu.RPeBbqcqaca9F8ZuSkLN8o5Kk-IARoHW5ij58DFjYpvc7-ZXhATPYOBHJKQ3tFL2fE08nnb9TMmjvErsF7MFxcxt1XnDfhbak55SFzw3yGfXhD6VqeLaSoPwcs_cFuXNtfQEeibyMpcUUj2dXltgc1axqIA1Lndyw9RMPOyH3b0wShyd7WCPiO7amktMUNDMkbgUDR6wYGEpWYxug9ySaGuu0JSAH6sDR2TRkuUOhgBfhsKIvqSQNahM5yRQpeJ0zvY3UOUxADyaKTTK_kNco9mMjoymQIK6eO30cCJva89hITytncWDxRCG5TsMuXJeqHkg2_RCZ6gckSZAYkiWmeXrw26qBOqZ8efV5aMo4D6NMLPVLyPeLJX3Vnpuzw_ySpMKewhmTmSaKZUXRWc06VXIln8YVZDAH7uGbkZHk4Gq0nDxH6xH7sSQROFOZlPuYeaJ5Ye07-_0QunvvVwCtvF69GpURukG8s7qQ-c6x5iWPJa0icommt6I6EHif3_X_oaHfcioQgjDD3zJIhKjHHZGp4yZhskuXhV3p-M9FiKeJFMhBK085UkopMFUHGa6p3DPmxA-UzVb8UbPqE4xvYrqujLg3rnwSOQjpnzLNcjVy9GhKVHqIpTTLtAmaXinkM4LExPfyVlGOAbE70q1C_VtEMIbwCNh5yd1JMo__WP-w_VQt9X_GF1k_8bUGLRJEUYIhuZX9cgZkFwiUCdSXOxBOZ6XM8vnTYj0J7bMkkbaJNIITzPEAz1HjbyN0N82os3aLq9PpMsay0QLREfkzKaWdjWs-Dj9mjJl7rl22GFe_wC1sT90W9iGMH8pjNsEU8byFXe_Gb6ZCzfWydk2eK1LRMW_N_IMg4oA-bp92dToAhiHci793mkDwDVqe_Yd06CClVZfX1cSuPZQfj2OwARQWqHrN6ZX7UnnHippvcvrJmqABDEHL-JyA5wcdCQOkHqQZsZvXTlDO6nyucTwOrI9jRStdjbFSpqDJV7unIVztEezzQFleLQ1bumMJwkvWHIqxM4js1d8xajFIBcvWC4BndlU0zp5Th39PShFb0_cmhEbhCSpzaofWW5-BLPmhIxyIAGbXKAR2mUwfBYnNzGdPffa3-EOiYR-CfYHFn9Vq7DZivcAj9FbMiOWaU5L-qU_ucpKOJ85gSUudVt59JMZWHniXdiURQUEuz3Q5ntCVM1Dhh2ir_-JHVpuK7VZ13HFgaMAE5zE4iIBltGo2XDT_5F1KsqoHVNtMnRSs8syN-kBsUEuQNwaZr5Jh6e4cmQKdHWRIycHMouYhTGiZHA5-R_lNtuipLyERQ-agaUGyZ7eqY6rDrk1Vsbbf_oAqfs_P3plB60jRQ.AkfSJSjSunnRAS2Ahrjx1Q
IFIND_REFRESH_TOKEN=eyJzaWduX3RpbWUiOiIyMDI1LTExLTAzIDEwOjE2OjU4In0=.eyJ1aWQiOiI3MjMwNDQwNzciLCJ1c2VyIjp7ImFjY291bnQiOiJ3eGhsdHowMDEiLCJhdXRoVXNlckluZm8iOnsiRVRyYW5zZmVyIjp0cnVlLCJFZXhjZWxQYXllcnMiOiIxNzk4NjgzMDAyMDAwIn0sImNvZGVDU0kiOltdLCJjb2RlWnpBdXRoIjpbXSwiaGFzQUlQcmVkaWN0IjpmYWxzZSwiaGFzQUlUYWxrIjpmYWxzZSwiaGFzQ0lDQyI6ZmFsc2UsImhhc0NTSSI6ZmFsc2UsImhhc0V2ZW50RHJpdmUiOmZhbHNlLCJoYXNGVFNFIjpmYWxzZSwiaGFzRmFzdCI6ZmFsc2UsImhhc0Z1bmRWYWx1YXRpb24iOmZhbHNlLCJoYXNISyI6dHJ1ZSwiaGFzTE1FIjpmYWxzZSwiaGFzTGV2ZWwyIjpmYWxzZSwiaGFzUmVhbENNRSI6ZmFsc2UsImhhc1RyYW5zZmVyIjpmYWxzZSwiaGFzVVMiOmZhbHNlLCJoYXNVU0FJbmRleCI6ZmFsc2UsImhhc1VTREVCVCI6ZmFsc2UsIm1hcmtldEF1dGgiOnsiRENFIjpmYWxzZX0sIm1hcmtldENvZGUiOiIxNjszMjsxNDQ7MTc2OzExMjs4ODs0ODsxMjg7MTY4LTE7MTg0OzIwMDsyMTY7MTA0OzEyMDsxMzY7MjMyOzU2Ozk2OzE2MDs2NDsiLCJtYXhPbkxpbmUiOjEsIm5vRGlzayI6ZmFsc2UsInByb2R1Y3RUeXBlIjoiU1VQRVJDT01NQU5EUFJPRFVDVCIsInJlZnJlc2hUb2tlbiI6IiIsInJlZnJlc2hUb2tlbkV4cGlyZWRUaW1lIjoiMjAyNi0xMi0zMSAxMDoxMDowMiIsInNlc3NzaW9uIjoiNDI3MzdjMjIyN2I5ZTEwNzY5NmJiYWNmNzhmNzY1ODMiLCJzaWRJbmZvIjp7NjQ6IjExMTExMTExMTExMTExMTExMTExMTExMSIsMToiMTAxIiwyOiIxIiw2NzoiMTAxMTExMTExMTExMTExMTExMTExMTExIiwzOiIxIiw2OToiMTExMTExMTExMTExMTExMTExMTExMTExMSIsNToiMSIsNjoiMSIsNzE6IjExMTExMTExMTExMTExMTExMTExMTEwMCIsNzoiMTExMTExMTExMTEiLDg6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxIiwxMzg6IjExMTExMTExMTExMTExMTExMTExMTExMTEiLDEzOToiMTExMTExMTExMTExMTExMTExMTExMTExMSIsMTQwOiIxMTExMTExMTExMTExMTExMTExMTExMTExIiwxNDE6IjExMTExMTExMTExMTExMTExMTExMTExMTEiLDE0MjoiMTExMTExMTExMTExMTExMTExMTExMTExMSIsMTQzOiIxMSIsODA6IjExMTExMTExMTExMTExMTExMTExMTExMSIsODE6IjExMTExMTExMTExMTExMTExMTExMTExMSIsODI6IjExMTExMTExMTExMTExMTExMTExMDExMCIsODM6IjExMTExMTExMTExMTExMTExMTAwMDAwMCIsODU6IjAxMTExMTExMTExMTExMTExMTExMTExMSIsODc6IjExMTExMTExMDAxMTExMTAxMTExMTExMSIsODk6IjExMTExMTExMDExMDEwMDAwMDAwMTExMSIsOTA6IjExMTExMDExMTExMTExMTExMDAwMTExMTEwIiw5MzoiMTExMTExMTExMTExMTExMTEwMDAwMTExMSIsOTQ6IjExMTExMTExMTExMTExMTExMTExMTExMTEiLDk2OiIxMTExMTExMTExMTExMTExMTExMTExMTExIiw5OToiMTAwIiwxMDA6IjExMTEwMTExMTExMTExMTExMTAiLDEwMjoiMSIsNDQ6IjExIiwxMDk6IjEiLDUzOiIxMTExMTExMTExMTExMTExMTExMTExMTEiLDU0OiIxMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsNTc6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMTAwMDAwMDAwIiw2MjoiMTExMTExMTExMTExMTExMTExMTExMTExIiw2MzoiMTExMTExMTExMTExMTExMTExMTExMTExIn0sInRpbWVzdGFtcCI6IjE3NjIxMzYyMTgxMDIiLCJ0cmFuc0F1dGgiOmZhbHNlLCJ0dGxWYWx1ZSI6MCwidWlkIjoiNzIzMDQ0MDc3IiwidXNlclR5cGUiOiJGUkVFSUFMIiwid2lmaW5kTGltaXRNYXAiOnt9fX0=.432D186AC7B7C51EB66BEC42EDD5FB96A5C7EF03CE5D070B1BE5A409A46AFDBE

25
DOC/API手册/API接口 Normal file
View File

@ -0,0 +1,25 @@
资产负债表
//基础函数-现金及现金等价物及短期投资(OAS);应收账款及票据(OAS);存货(OAS);固定资产净额(OAS);长期投资与长期应收款(OAS);商誉及无形资产(OAS);短期债务(OAS);短期借款(OAS);应付账款及票据(OAS);合同负债(流动)(OAS);预收款项(流动)(OAS);递延收入(流动)(OAS);长期债务(OAS);长期借款(OAS);资产合计(OAS);归属于母公司股东权益(OAS)-iFinD数据接口
requestMethod:POST
requestURL:https://quantapi.51ifind.com/api/v1/basic_data_service
requestHeaders:{"Content-Type":"application/json","access_token":"c9967e4e36bcd9b726f39bd90ad9d0cd871799ef.signs_NzIzMDQ0MDc3"}
formData:{"codes":"0700.HK,7203.T,AAPL.O","indipara":[{"indicator":"cash_equi_short_term_inve_oas","indiparams":["20241231","1","CNY"]},{"indicator":"accou_and_notes_recei_oas","indiparams":["20241231","1","CNY"]},{"indicator":"inventories_oas","indiparams":["20241231","1","CNY"]},{"indicator":"ppe_net_oas","indiparams":["20241231","1","CNY"]},{"indicator":"long_term_inv_and_receiv_oas","indiparams":["20241231","1","CNY"]},{"indicator":"goodwill_and_intasset_oas","indiparams":["20241231","1","CNY"]},{"indicator":"short_term_debt_oas","indiparams":["20241231","1","CNY"]},{"indicator":"short_term_borrowings_oas","indiparams":["20241231","1","CNY"]},{"indicator":"account_and_note_payable_oas","indiparams":["20241231","1","CNY"]},{"indicator":"contra_liabilities_current_oas","indiparams":["20241231","1","CNY"]},{"indicator":"advance_from_cust_current_oas","indiparams":["20241231","1","CNY"]},{"indicator":"defer_revenue_current_oas","indiparams":["20241231","1","CNY"]},{"indicator":"long_term_debt_oas","indiparams":["20241231","1","CNY"]},{"indicator":"long_term_borrowings_oas","indiparams":["20241231","1","CNY"]},{"indicator":"total_assets_oas","indiparams":["20241231","1","CNY"]},{"indicator":"equity_attri_to_companyowner_oas","indiparams":["20241231","1","CNY"]}]}
//基础函数-营业收入(OAS);毛利(OAS);销售、一般及管理费用(OAS);销售及营销费用(OAS);一般及管理费用(OAS);研发费用(OAS);所得税费用(OAS);归属于普通股股东的净利润(OAS)-iFinD数据接口
requestMethod:POST
requestURL:https://quantapi.51ifind.com/api/v1/basic_data_service
requestHeaders:{"Content-Type":"application/json","access_token":"c9967e4e36bcd9b726f39bd90ad9d0cd871799ef.signs_NzIzMDQ0MDc3"}
formData:{"codes":"0700.HK,7203.T,AAPL.O","indipara":[{"indicator":"revenue_oas","indiparams":["20241231","1","CNY"]},{"indicator":"gross_profit_oas","indiparams":["20241231","1","CNY"]},{"indicator":"sga_expenses_oas","indiparams":["20241231","1","CNY"]},{"indicator":"selling_marketing_expenses_oas","indiparams":["20241231","1","CNY"]},{"indicator":"ga_expenses_oas","indiparams":["20241231","1","CNY"]},{"indicator":"rd_expenses_oas","indiparams":["20241231","1","CNY"]},{"indicator":"income_tax_expense_oas","indiparams":["20241231","1","CNY"]},{"indicator":"net_income_attri_to_common_sh_oas","indiparams":["20241231","1","CNY"]}]}
//基础函数-经营活动现金流量净额(OAS);购买固定及无形资产(OAS);支付股利(OAS)-iFinD数据接口
requestMethod:POST
requestURL:https://quantapi.51ifind.com/api/v1/basic_data_service
requestHeaders:{"Content-Type":"application/json","access_token":"c9967e4e36bcd9b726f39bd90ad9d0cd871799ef.signs_NzIzMDQ0MDc3"}
formData:{"codes":"0700.HK,7203.T,AAPL.O","indipara":[{"indicator":"net_cash_flows_from_oa_oas","indiparams":["20241231","1","CNY"]},{"indicator":"purchase_of_ppe_and_ia_oas","indiparams":["20241231","1","CNY"]},{"indicator":"dividends_paid_oas","indiparams":["20241231","1","CNY"]}]}
//基础函数-公司中文名称;会计年结日;上市日期-iFinD数据接口
requestMethod:POST
requestURL:https://quantapi.51ifind.com/api/v1/basic_data_service
requestHeaders:{"Content-Type":"application/json","access_token":"c9967e4e36bcd9b726f39bd90ad9d0cd871799ef.signs_NzIzMDQ0MDc3"}
formData:{"codes":"7203.T","indipara":[{"indicator":"corp_cn_name","indiparams":[]},{"indicator":"accounting_date","indiparams":[]},{"indicator":"ipo_date","indiparams":[]}]}

Binary file not shown.

File diff suppressed because it is too large Load Diff

131
JP-data.md Normal file
View File

@ -0,0 +1,131 @@
# 日本市场数据获取与指标计算说明 (JP-data)
本文档详细记录了 `FA3` 项目中针对日本市场 (JP) 的数据获取来源、API 接口调用方式以及关键财务指标的计算逻辑。
## 1. 数据获取 (API Fetching)
所有数据均通过 **同花顺 iFinD API** 获取。为了节省 API 额度并提高准确性,我们在 `JpFetcher` 中采用了“按需点播”与“点对点抓取”的策略。
### 1.1 公司基础信息 (Basic Info)
* **接口**: `basic_data_service`
* **用途**: 获取公司名称、上市日期及会计年结日,用于确定后续财报的抓取时间点。
| 字段 | iFinD 指标 | 说明 |
| :--- | :--- | :--- |
| 公司简称 | `corp_cn_name` | 公司的中文简称 |
| 上市日期 | `ipo_date` | 格式 YYYYMMDD |
| 会计年结日 | `accounting_date` | 主要是 0331 或 1231 |
### 1.2 财务报表 (Financial Statements)
财务数据按**最近 5 个会计年度**逐年点播获取,统一货币单位为 **CNY (人民币)**
#### 利润表 (Income Statement)
| 内部字段 | iFinD 指标 | 备注 |
| :--- | :--- | :--- |
| 营业收入 | `revenue_oas` | |
| 毛利 | `gross_profit_oas` | |
| SG&A 费用 | `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` | |
#### 资产负债表 (Balance Sheet)
| 内部字段 | iFinD 指标 | 备注 |
| :--- | :--- | :--- |
| 货币资金 | `cash_equi_short_term_inve_oas` | 现金及等价物+短期投资 |
| 应收账款 | `accou_and_notes_recei_oas` | 应收账款及票据 |
| 存货 | `inventories_oas` | |
| 预付款项 | `prepayments_oas` | |
| 固定资产 | `ppe_net_oas` | 不动产、厂房及设备净额 |
| 长期投资 | `long_term_inv_and_receiv_oas` | 包含长期应收款 |
| 商誉及无形 | `goodwill_and_intasset_oas` | **目前作为商誉指标使用** |
| 短期借款 | `short_term_debt_oas` + `short_term_borrowings_oas` | 主要是 `short_term_debt_oas` |
| 应付账款 | `account_and_note_payable_oas` | 应付账款及票据 |
| 预收款项 | `advance_from_cust_current_oas` + `contra_liabilities_current_oas` | 包含合同负债 |
| 长期借款 | `long_term_debt_oas` + `long_term_borrowings_oas` | |
| 总资产 | `total_assets_oas` | |
| 归母权益 | `equity_attri_to_companyowner_oas` | 净资产 |
#### 现金流量表 (Cash Flow Statement)
| 内部字段 | iFinD 指标 | 备注 |
| :--- | :--- | :--- |
| 经营净现金流 (OCF) | `net_cash_flows_from_oa_oas` | |
| 资本开支 (Capex) | `purchase_of_ppe_and_ia_oas` | 购建资产支付的现金 |
| 分红支付 | `dividends_paid_oas` | 现金流量表中的分红流出 |
### 1.3 市场与分红数据 (Market & Events)
* **当前股价**:
* **接口**: `date_sequence`
* **策略**: 获取**最近一个交易日**的 `pre_close` (收盘价),使用 `Fill: Previous` 防止节假日无数据。避免使用不稳定的实时接口。
* **历史股价**:
* **接口**: `date_sequence`
* **策略**: 针对每个财报日期,单独点播当天的收盘价与市值。
* **分红 (Dividends)**:
* **指标**: `annual_cum_dividend`
* **策略**: 按年逐年获取累计分红总额。
* **回购 (Repurchases)**:
* **指标**: `repur_num_new`
* **策略**: 按会计年度范围获取回购金额。
* **员工人数 (Employee Count)**:
* **指标**: `staff_num`
* **策略**: 针对每个财报日期(报告期末),单独获取当天的员工人数,获取最近 5 年数据。
---
## 2. 指标计算 (Indicator Calculation)
以下指标由 `JP_Analyzer` 基于获取的原始数据计算得出。
### 2.1 盈利能力与回报率
* **ROE (净资产收益率)** = `归母净利润 / 归母股东权益`
* **ROA (总资产收益率)** = `归母净利润 / 总资产`
* **毛利率 (Gross Margin)** = `毛利 / 营业收入`
* **净利率 (Net Margin)** = `归母净利润 / 营业收入`
### 2.2 费用与税率
* **SG&A 比例** = 优先使用 `SG&A费用 / 营业收入`;若无数据,则退化为 `(销售费用 + 管理费用) / 营业收入`
* **所得税率** = `所得税费用 / (归母净利润 + 所得税费用)` (由于 API 缺少利润总额,采用此公式进行估算)
* **其他费用率** = `毛利率 - 净利率 - (SG&A比例 + 研发费率)` (倒挤差额)
### 2.3 资产周转 (Turnover)
* **存货周转天数** = `存货 * 365 / (营业收入 - 毛利)`
* **应收周转天数** = `应收账款 * 365 / 营业收入`
* **应付周转天数** = `应付账款 * 365 / (营业收入 - 毛利)`
* **固定资产周转率** = `营业收入 / 固定资产`
* **总资产周转率** = `营业收入 / 总资产`
### 2.4 人均效率 (Efficiency)
* **人均创收** = `营业收入 / 员工人数`
* **人均创利** = `归母净利润 / 员工人数`
### 2.5 估值与市场指标 (Valuation)
* **股价** = 最近交易日收盘价 (CNY)
* **市值 (Market Cap)** = `股价 * 总股本` (API 直接获取市值)
* **PE (市盈率)** = 优先使用读取数据;若为空,则计算 `市值 / 归母净利润`
* **PB (市净率)** = 优先使用读取数据;若为空,则计算 `市值 / 归母股东权益`
* **股息率 (Dividend Yield)** = 优先使用读取数据;若为空,则计算 `年度分红总额 / 市值` (单位:%)
### 2.6 现金流指标
* **自由现金流 (FCF)** = `经营净现金流 (OCF) - 资本开支 (Capex)`
* *注: 资本开支取绝对值计算*
## 3. 注意事项
1. **货币单位**: 为了统一计算,所有从 iFinD 获取的财务数据均强制指定货币为 **CNY**
2. **API 额度优化**: 历史行情数据(股价、市值)均采用**单点日期查询**模式,避免全量下载时间序列数据,极大节省了 API 流量。
3. **商誉处理**: iFinD 的日本数据中,`goodwill_and_intasset_oas` (商誉及无形资产) 被用作“商誉”的近似指标。

View File

@ -0,0 +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
770602568734.0099,672717680537.7899,222601652642.64,770534649125.2,1551773147819.29,65996075141.82001,766311143529.32,264536419696.63,195331566628.4,,,,1111662554527.74,1090303749762.66,4531266625864.5,1739130526363.0203,,20251231
673911903173.7201,653991429222.8099,219892459842.32,706207728474.77,1529223836526.51,64712736968.73999,735603735832.15,262033523493.40997,182778724515.32,,,,1010111255957.04,991532329184.16,4302686389969.04,1633949315067.0898,,20241231
477635263624.52997,568835847614.87,220157083414.62,679017649048.86,1399256001575.08,64621240636.259995,636611683247.87,237464934566.09,197586866202.22,,,,883327675351.22,863190476648.72,3843950930989.4004,1466055629270.9797,,20231231
450020533384.27,501527244485.08997,199478948478.04,666871639206.68,1258066992832.73,62221924442.94001,584017390544.51,214278061895.22,165377438011.56,,,,799121378085.7101,780078838062.43,3533427626960.3896,1370068189906.21,,20221231
551836394708.76,534674422943.86,171067544453.52,699030178408.64,1275497513656.26,65668094657.56,723361042847.0599,271324355670.68,174958464091.44,,,,796544782150.5,777959077825.36,3688290674447.6,1386329489996.98,,20211231
1 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
2 770602568734.0099 672717680537.7899 222601652642.64 770534649125.2 1551773147819.29 65996075141.82001 766311143529.32 264536419696.63 195331566628.4 1111662554527.74 1090303749762.66 4531266625864.5 1739130526363.0203 20251231
3 673911903173.7201 653991429222.8099 219892459842.32 706207728474.77 1529223836526.51 64712736968.73999 735603735832.15 262033523493.40997 182778724515.32 1010111255957.04 991532329184.16 4302686389969.04 1633949315067.0898 20241231
4 477635263624.52997 568835847614.87 220157083414.62 679017649048.86 1399256001575.08 64621240636.259995 636611683247.87 237464934566.09 197586866202.22 883327675351.22 863190476648.72 3843950930989.4004 1466055629270.9797 20231231
5 450020533384.27 501527244485.08997 199478948478.04 666871639206.68 1258066992832.73 62221924442.94001 584017390544.51 214278061895.22 165377438011.56 799121378085.7101 780078838062.43 3533427626960.3896 1370068189906.21 20221231
6 551836394708.76 534674422943.86 171067544453.52 699030178408.64 1275497513656.26 65668094657.56 723361042847.0599 271324355670.68 174958464091.44 796544782150.5 777959077825.36 3688290674447.6 1386329489996.98 20211231

View File

@ -0,0 +1,2 @@
corp_cn_name,accounting_date,ipo_date
丰田汽车公司,0331,19490516
1 corp_cn_name accounting_date ipo_date
2 丰田汽车公司 0331 19490516

View File

@ -0,0 +1,6 @@
net_cash_flows_from_oa_oas,purchase_of_ppe_and_ia_oas,dividends_paid_oas,end_date
178969573112.18,109455959341.89,54816352618.83,20251231
200841649567.27002,104123484490.66,42026757357.03001,20241231
152875921883.08,93041152405.08,37660829573.4,20231231
194324560650.35,80564604452.59,37056092160.48,20221231
161538913981.08,88396874949.0,37051283436.76,20211231
1 net_cash_flows_from_oa_oas purchase_of_ppe_and_ia_oas dividends_paid_oas end_date
2 178969573112.18 109455959341.89 54816352618.83 20251231
3 200841649567.27002 104123484490.66 42026757357.03001 20241231
4 152875921883.08 93041152405.08 37660829573.4 20231231
5 194324560650.35 80564604452.59 37056092160.48 20221231
6 161538913981.08 88396874949.0 37051283436.76 20211231

View File

@ -0,0 +1,6 @@
date_str,employee_count
20251231,383853.0
20241231,380793.0
20231231,375235.0
20221231,372817.0
20211231,366283.0
1 date_str employee_count
2 20251231 383853.0
3 20241231 380793.0
4 20231231 375235.0
5 20221231 372817.0
6 20211231 366283.0

View File

@ -0,0 +1,6 @@
date_str,PE,PB,MarketCap,Price
20251231,0.0,0.0,2448738372188.5,155.03262528
20241231,0.0,0.0,2304821554513.2,145.9210753
20231231,0.0,0.0,2129364298560.0,130.515840345
20221231,0.0,0.0,1547352230179.7,94.842379375
20211231,0.0,0.0,1900725057905.6,116.50177866
1 date_str PE PB MarketCap Price
2 20251231 0.0 0.0 2448738372188.5 155.03262528
3 20241231 0.0 0.0 2304821554513.2 145.9210753
4 20231231 0.0 0.0 2129364298560.0 130.515840345
5 20221231 0.0 0.0 1547352230179.7 94.842379375
6 20211231 0.0 0.0 1900725057905.6 116.50177866

View File

@ -0,0 +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
2325469810550.08,463675405650.25995,231519792582.04,,,,78658701055.45,230679099833.22,232155613068.22,20251231
2153166031821.75,447308985862.81995,191722451947.16998,,,,90416803818.35,236105666501.67,255586486168.66,20241231
1922115559352.3398,326593340023.28,185618670706.7,,,,60826238747.45,126814843028.94,140974617583.25,20231231
1638044469062.6301,311727839723.57,155349243214.93,,,,58252135950.62,148778848619.9,156378648709.72998,20221231
1612011299363.96,286237652149.16,156057638397.5,,,,38500249399.84,132994308201.74,130179954518.32,20211231
1 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
2 2325469810550.08 463675405650.25995 231519792582.04 78658701055.45 230679099833.22 232155613068.22 20251231
3 2153166031821.75 447308985862.81995 191722451947.16998 90416803818.35 236105666501.67 255586486168.66 20241231
4 1922115559352.3398 326593340023.28 185618670706.7 60826238747.45 126814843028.94 140974617583.25 20231231
5 1638044469062.6301 311727839723.57 155349243214.93 58252135950.62 148778848619.9 156378648709.72998 20221231
6 1612011299363.96 286237652149.16 156057638397.5 38500249399.84 132994308201.74 130179954518.32 20211231

View File

@ -0,0 +1,2 @@
pe_ttm,pb
,
1 pe_ttm pb
2

445
data/JP/7203.T/report.html Normal file
View File

@ -0,0 +1,445 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>7203.T Financial Report</title>
<style>
:root {
--bg: #f5f6fa;
--card-bg: #ffffff;
--header-bg: #f7f8fb;
--section-bg: #f0f2f5;
--border: #e5e7eb;
--text-primary: #111827;
--text-secondary: #6b7280;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 32px;
background: var(--bg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--text-primary);
line-height: 1.6;
}
.report-container {
max-width: 1280px;
margin: 0 auto;
background: var(--card-bg);
border-radius: 24px;
padding: 32px 40px;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
}
h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: var(--text-primary);
}
p {
margin: 0 0 24px;
color: var(--text-secondary);
font-size: 0.95rem;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
}
th,
td {
font-size: 0.95rem;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
th {
font-weight: 600;
color: var(--text-secondary);
text-align: right;
background: var(--header-bg);
}
th:first-child,
td:first-child {
text-align: left;
}
.company-table th,
.company-table td {
text-align: left;
}
.metrics-table thead {
position: sticky;
top: 0;
z-index: 3;
}
.metrics-table thead th {
position: sticky;
top: 0;
z-index: 3;
background: var(--card-bg);
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
}
.metrics-table thead th:first-child {
left: 0;
z-index: 4;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.08);
}
.metrics-table th:first-child,
.metrics-table td:first-child {
width: 180px;
min-width: 180px;
}
.metrics-table tbody td:first-child {
position: sticky;
left: 0;
background: var(--card-bg);
font-weight: 600;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.04);
z-index: 2;
text-align: left;
}
.metrics-table tbody td:not(:first-child) {
text-align: right;
}
.metrics-table tr.other-assets-row td {
background: #fff7e0;
}
.metrics-table tr.other-assets-row td:first-child {
background: #fff7e0;
}
.metrics-table tbody tr:hover td {
background: #f4efff;
}
.section-row td {
background: #eef1f6;
font-weight: 600;
text-align: left;
border-bottom: 1px solid var(--border);
}
.metrics-table .section-row td:first-child {
position: sticky;
left: 0;
z-index: 2;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.08);
background: #eef1f6 !important;
}
.metrics-table .section-label {
color: var(--text-primary);
background: #eef1f6 !important;
}
.section-spacer {
background: #eef1f6;
}
.metric-name {
color: var(--text-secondary);
}
.table-container {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 16px;
margin-bottom: 24px;
}
.table-container table {
margin-bottom: 0;
min-width: 960px;
}
.table-gap {
height: 24px;
}
.no-data {
margin-top: 24px;
padding: 32px;
text-align: center;
border: 1px dashed var(--border);
border-radius: 16px;
color: var(--text-secondary);
font-size: 0.95rem;
}
.bg-green { background-color: #e6f7eb !important; }
.bg-red { background-color: #ffeef0 !important; }
.font-red { color: #d32f2f !important; }
.font-green { color: #1b873f !important; }
.font-blue { color: #2563eb !important; }
.italic { font-style: italic !important; }
@media (max-width: 768px) {
body { padding: 16px; }
.report-container { padding: 24px; }
table { font-size: 0.85rem; }
th,
td { padding: 10px 12px; }
}
</style>
</head>
<body>
<div class="report-container">
<h1>丰田汽车公司 (7203.T) - Financial Report</h1>
<p><em>Report generated on: 2025-12-21</em></p>
<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>7203.T</td>
<td>丰田汽车公司</td>
<td>1949-05-16</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00%</td>
</tr>
</tbody>
</table>
<div class="table-gap"></div>
<table class="metrics-table" data-table="metrics" data-scrollable="true">
<thead>
<tr>
<th>指标</th>
<th>2025A</th><th>2024A</th><th>2023A</th><th>2022A</th><th>2021A</th>
</tr>
</thead>
<tbody>
<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>13.26%</td><td>14.45%</td><td>8.65%</td><td>10.86%</td><td>9.59%</td></tr>
<tr><td class="metric-name">ROA</td><td>5.09%</td><td>5.49%</td><td>3.30%</td><td>4.21%</td><td>3.61%</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>19.94%</td><td>20.77%</td><td>16.99%</td><td>19.03%</td><td>17.76%</td></tr>
<tr><td class="metric-name">净利润率</td><td>9.92%</td><td>10.97%</td><td>6.60%</td><td>9.08%</td><td>8.25%</td></tr>
<tr><td class="metric-name">收入(亿)</td><td>23,254.70</td><td>21,531.66</td><td>19,221.16</td><td>16,380.44</td><td>16,120.11</td></tr>
<tr><td class="metric-name">收入增速</td><td>8.00%</td><td>12.02%</td><td>17.34%</td><td>1.61%</td><td>-</td></tr>
<tr><td class="metric-name">净利润(亿)</td><td>2,306.79</td><td>2,361.06</td><td>1,268.15</td><td>1,487.79</td><td>1,329.94</td></tr>
<tr><td class="metric-name">净利润增速</td><td>-2.30%</td><td>86.18%</td><td>-14.76%</td><td>11.87%</td><td>-</td></tr>
<tr><td class="metric-name">经营净现金流(亿)</td><td>1,789.70</td><td>2,008.42</td><td>1,528.76</td><td>1,943.25</td><td>1,615.39</td></tr>
<tr><td class="metric-name">资本开支(亿)</td><td>1,094.56</td><td>1,041.23</td><td>930.41</td><td>805.65</td><td>883.97</td></tr>
<tr><td class="metric-name">自由现金流(亿)</td><td>695.14</td><td>967.18</td><td>598.35</td><td>1,137.60</td><td>731.42</td></tr>
<tr><td class="metric-name">分红(亿)</td><td>548.16</td><td>420.27</td><td>376.61</td><td>370.56</td><td>370.51</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>45,312.67</td><td>43,026.86</td><td>38,439.51</td><td>35,334.28</td><td>36,882.91</td></tr>
<tr><td class="metric-name">净资产(亿)</td><td>17,391.31</td><td>16,339.49</td><td>14,660.56</td><td>13,700.68</td><td>13,863.29</td></tr>
<tr><td class="metric-name">商誉(亿)</td><td>659.96</td><td>647.13</td><td>646.21</td><td>622.22</td><td>656.68</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">SG&A比例</td><td>9.96%</td><td>8.90%</td><td>9.66%</td><td>9.48%</td><td>9.68%</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>0.06%</td><td>0.90%</td><td>0.74%</td><td>0.46%</td><td>-0.17%</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>25.43%</td><td>27.69%</td><td>32.42%</td><td>28.14%</td><td>22.45%</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>17.01%</td><td>15.66%</td><td>12.43%</td><td>12.74%</td><td>14.96%</td></tr>
<tr><td class="metric-name">库存占比</td><td>4.91%</td><td>5.11%</td><td>5.73%</td><td>5.65%</td><td>4.64%</td></tr>
<tr><td class="metric-name">应收款占比</td><td>14.85%</td><td>15.20%</td><td>14.80%</td><td>14.19%</td><td>14.50%</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>17.00%</td><td>16.41%</td><td>17.66%</td><td>18.87%</td><td>18.95%</td></tr>
<tr><td class="metric-name">长期投资占比</td><td>34.25%</td><td>35.54%</td><td>36.40%</td><td>35.60%</td><td>34.58%</td></tr>
<tr><td class="metric-name">商誉占比</td><td>1.46%</td><td>1.50%</td><td>1.68%</td><td>1.76%</td><td>1.78%</td></tr>
<tr class="other-assets-row"><td class="metric-name">其他资产占比</td><td>10.53%</td><td>10.57%</td><td>11.30%</td><td>11.19%</td><td>10.59%</td></tr>
<tr><td class="metric-name">应付款占比</td><td>4.31%</td><td>4.25%</td><td>5.14%</td><td>4.68%</td><td>4.74%</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>16.91%</td><td>17.10%</td><td>16.56%</td><td>16.53%</td><td>19.61%</td></tr>
<tr><td class="metric-name">长期借款占比</td><td>48.59%</td><td>46.52%</td><td>45.44%</td><td>44.69%</td><td>42.69%</td></tr>
<tr><td class="metric-name">运营资产占比</td><td>15.45%</td><td>16.06%</td><td>15.39%</td><td>15.16%</td><td>14.39%</td></tr>
<tr><td class="metric-name">有息负债率</td><td>65.51%</td><td>63.62%</td><td>62.00%</td><td>61.22%</td><td>62.30%</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>43</td><td>47</td><td>50</td><td>54</td><td>47</td></tr>
<tr><td class="metric-name">应收款周转天数</td><td>105</td><td>110</td><td>108</td><td>111</td><td>121</td></tr>
<tr><td class="metric-name">应付款周转天数</td><td>38</td><td>39</td><td>45</td><td>45</td><td>48</td></tr>
<tr><td class="metric-name">固定资产周转率</td><td>3.02</td><td>3.05</td><td>2.83</td><td>2.46</td><td>2.31</td></tr>
<tr><td class="metric-name">总资产周转率</td><td>0.51</td><td>0.50</td><td>0.50</td><td>0.46</td><td>0.44</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>383,853</td><td>380,793</td><td>375,235</td><td>372,817</td><td>366,283</td></tr>
<tr><td class="metric-name">人均创收(万)</td><td>605.82</td><td>565.44</td><td>512.24</td><td>439.37</td><td>440.10</td></tr>
<tr><td class="metric-name">人均创利(万)</td><td>60.10</td><td>62.00</td><td>33.80</td><td>39.91</td><td>36.31</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><td class="metric-name">股价</td><td>-</td><td>145.92</td><td>130.52</td><td>94.84</td><td>116.50</td></tr>
<tr><td class="metric-name">市值(亿)</td><td>-</td><td>23,048</td><td>21,294</td><td>15,474</td><td>19,007</td></tr>
<tr><td class="metric-name">PE</td><td>-</td><td>9.76</td><td>16.79</td><td>10.40</td><td>14.29</td></tr>
<tr><td class="metric-name">PB</td><td>-</td><td>1.41</td><td>1.45</td><td>1.13</td><td>1.37</td></tr>
<tr><td class="metric-name">股东户数</td><td>-</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const scrollableTables = document.querySelectorAll('table[data-scrollable="true"]');
scrollableTables.forEach(table => {
const container = document.createElement('div');
container.className = 'table-container';
table.parentNode.insertBefore(container, table);
container.appendChild(table);
});
const parseValue = (text) => {
if (!text || text.trim() === '-') return null;
return parseFloat(text.replace(/%|,/g, ''));
};
const highlightIfOverThirtyPercent = (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 30) {
cell.classList.add('bg-red', 'font-red');
}
};
const styleRules = {
'ROE': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 15) cell.classList.add('bg-green');
},
'ROA': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 10) cell.classList.add('bg-green');
},
'毛利率': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 50) cell.classList.add('bg-green');
},
'净利润率': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 20) {
cell.classList.add('bg-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
}
}
},
'收入增速': (cell) => {
cell.classList.add('italic');
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 15) {
cell.classList.add('bg-green', 'font-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
} else {
cell.classList.add('font-blue');
}
}
},
'净利润增速': (cell) => {
cell.classList.add('italic');
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 15) {
cell.classList.add('bg-green', 'font-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
} else {
cell.classList.add('font-blue');
}
}
},
'经营净现金流(亿)': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value < 0) cell.classList.add('bg-red', 'font-red');
},
'应收款周转天数': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 90) {
cell.classList.add('bg-red', 'font-red');
}
},
'现金占比': highlightIfOverThirtyPercent,
'库存占比': highlightIfOverThirtyPercent,
'应收款占比': highlightIfOverThirtyPercent,
'预付款占比': highlightIfOverThirtyPercent,
'固定资产占比': highlightIfOverThirtyPercent,
'长期投资占比': highlightIfOverThirtyPercent,
'商誉占比': highlightIfOverThirtyPercent,
'其他资产占比': highlightIfOverThirtyPercent
};
const metricsTables = document.querySelectorAll('table[data-table="metrics"]');
metricsTables.forEach(table => {
let netProfitValues = [];
let fcfRow = null;
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.classList.contains('section-row')) return;
const metricCell = row.querySelector('td:first-child');
if (!metricCell) return;
const metricName = metricCell.textContent.trim();
if (metricName === '净利润(亿)') {
row.querySelectorAll('td:not(:first-child)').forEach(cell => {
netProfitValues.push(parseValue(cell.textContent));
});
} else if (metricName === '自由现金流(亿)') {
fcfRow = row;
}
});
rows.forEach(row => {
if (row.classList.contains('section-row')) return;
const metricCell = row.querySelector('td:first-child');
if (!metricCell) return;
const metricName = metricCell.textContent.trim();
const cells = row.querySelectorAll('td:not(:first-child)');
if (styleRules[metricName]) {
cells.forEach(cell => {
styleRules[metricName](cell);
});
}
if (row === fcfRow && netProfitValues.length > 0) {
cells.forEach((cell, index) => {
const fcfValue = parseValue(cell.textContent);
const netProfitValue = netProfitValues[index];
if (fcfValue !== null) {
if (fcfValue < 0) {
cell.classList.add('bg-red', 'font-red');
} else if (netProfitValue !== null && fcfValue > netProfitValue) {
cell.classList.add('bg-green', 'font-green');
}
}
});
}
});
});
});
</script>
</body>
</html>

89
data/JP/7203.T/report.md Normal file
View File

@ -0,0 +1,89 @@
# 丰田汽车公司 (7203.T) - Financial Report
*Report generated on: 2025-12-21*
| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |
|:---|:---|:---|:---|:---|:---|
| 7203.T | 丰田汽车公司 | 1949-05-16 | 0.00 | 0.00 | 0.00% |
## 主要指标
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| ROE | 13.26% | 14.45% | 8.65% | 10.86% | 9.59% |
| ROA | 5.09% | 5.49% | 3.30% | 4.21% | 3.61% |
| ROCE/ROIC | - | - | - | - | - |
| 毛利率 | 19.94% | 20.77% | 16.99% | 19.03% | 17.76% |
| 净利润率 | 9.92% | 10.97% | 6.60% | 9.08% | 8.25% |
| 收入(亿) | 23,254.70 | 21,531.66 | 19,221.16 | 16,380.44 | 16,120.11 |
| 收入增速 | 8.00% | 12.02% | 17.34% | 1.61% | - |
| 净利润(亿) | 2,306.79 | 2,361.06 | 1,268.15 | 1,487.79 | 1,329.94 |
| 净利润增速 | -2.30% | 86.18% | -14.76% | 11.87% | - |
| 经营净现金流(亿) | 1,789.70 | 2,008.42 | 1,528.76 | 1,943.25 | 1,615.39 |
| 资本开支(亿) | 1,094.56 | 1,041.23 | 930.41 | 805.65 | 883.97 |
| 自由现金流(亿) | 695.14 | 967.18 | 598.35 | 1,137.60 | 731.42 |
| 分红(亿) | 548.16 | 420.27 | 376.61 | 370.56 | 370.51 |
| 回购(亿) | - | - | - | - | - |
| 总资产(亿) | 45,312.67 | 43,026.86 | 38,439.51 | 35,334.28 | 36,882.91 |
| 净资产(亿) | 17,391.31 | 16,339.49 | 14,660.56 | 13,700.68 | 13,863.29 |
| 商誉(亿) | 659.96 | 647.13 | 646.21 | 622.22 | 656.68 |
## 费用指标
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| 销售费用率 | - | - | - | - | - |
| 管理费用率 | - | - | - | - | - |
| SG&A比例 | 9.96% | 8.90% | 9.66% | 9.48% | 9.68% |
| 研发费用率 | - | - | - | - | - |
| 其他费用率 | 0.06% | 0.90% | 0.74% | 0.46% | -0.17% |
| 折旧费用占比 | - | - | - | - | - |
| 所得税率 | 25.43% | 27.69% | 32.42% | 28.14% | 22.45% |
## 资产占比
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| 现金占比 | 17.01% | 15.66% | 12.43% | 12.74% | 14.96% |
| 库存占比 | 4.91% | 5.11% | 5.73% | 5.65% | 4.64% |
| 应收款占比 | 14.85% | 15.20% | 14.80% | 14.19% | 14.50% |
| 预付款占比 | - | - | - | - | - |
| 固定资产占比 | 17.00% | 16.41% | 17.66% | 18.87% | 18.95% |
| 长期投资占比 | 34.25% | 35.54% | 36.40% | 35.60% | 34.58% |
| 商誉占比 | 1.46% | 1.50% | 1.68% | 1.76% | 1.78% |
| 其他资产占比 | 10.53% | 10.57% | 11.30% | 11.19% | 10.59% |
| 应付款占比 | 4.31% | 4.25% | 5.14% | 4.68% | 4.74% |
| 预收款占比 | 0.00% | 0.00% | 0.00% | 0.00% | 0.00% |
| 短期借款占比 | 16.91% | 17.10% | 16.56% | 16.53% | 19.61% |
| 长期借款占比 | 48.59% | 46.52% | 45.44% | 44.69% | 42.69% |
| 运营资产占比 | 15.45% | 16.06% | 15.39% | 15.16% | 14.39% |
| 有息负债率 | 65.51% | 63.62% | 62.00% | 61.22% | 62.30% |
## 周转能力
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| 存货周转天数 | 43 | 47 | 50 | 54 | 47 |
| 应收款周转天数 | 105 | 110 | 108 | 111 | 121 |
| 应付款周转天数 | 38 | 39 | 45 | 45 | 48 |
| 固定资产周转率 | 3.02 | 3.05 | 2.83 | 2.46 | 2.31 |
| 总资产周转率 | 0.51 | 0.50 | 0.50 | 0.46 | 0.44 |
## 人均效率
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| 员工人数 | 383,853 | 380,793 | 375,235 | 372,817 | 366,283 |
| 人均创收(万) | 605.82 | 565.44 | 512.24 | 439.37 | 440.10 |
| 人均创利(万) | 60.10 | 62.00 | 33.80 | 39.91 | 36.31 |
| 人均薪酬(万) | - | - | - | - | - |
## 市场表现
| 指标 | 2025A | 2024A | 2023A | 2022A | 2021A |
|:---|--:|--:|--:|--:|--:|
| 股价 | - | 145.92 | 130.52 | 94.84 | 116.50 |
| 市值(亿) | - | 23,048 | 21,294 | 15,474 | 19,007 |
| PE | - | 9.76 | 16.79 | 10.40 | 14.29 |
| PB | - | 1.41 | 1.45 | 1.13 | 1.37 |
| 股东户数 | - | - | - | - | - |

29
main.py
View File

@ -4,17 +4,26 @@ from dotenv import load_dotenv
sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
from strategies.cn_strategy import CN_Strategy # from strategies.cn_strategy import CN_Strategy
from strategies.us_strategy import US_Strategy # from strategies.us_strategy import US_Strategy
from strategies.hk_strategy import HK_Strategy # from strategies.hk_strategy import HK_Strategy
# from strategies.jp_strategy import JP_Strategy
def get_strategy(market, stock_code, tushare_token=None, av_key=None): def get_strategy(market, stock_code, tushare_token=None, av_key=None):
if market.upper() == 'CN': market = market.upper()
if market == 'CN':
from strategies.cn_strategy import CN_Strategy
return CN_Strategy(stock_code, tushare_token) return CN_Strategy(stock_code, tushare_token)
elif market.upper() == 'US': elif market == 'US':
from strategies.us_strategy import US_Strategy
return US_Strategy(stock_code, av_key) return US_Strategy(stock_code, av_key)
elif market.upper() == 'HK': elif market == 'HK':
return HK_Strategy(stock_code, tushare_token) from strategies.hk_strategy import HK_Strategy
return HK_Strategy(stock_code, av_key)
elif market == 'JP':
from strategies.jp_strategy import JP_Strategy
ifind_token = os.getenv('IFIND_REFRESH_TOKEN')
return JP_Strategy(stock_code, ifind_token)
else: else:
raise ValueError(f"Unsupported market: {market}") raise ValueError(f"Unsupported market: {market}")
@ -41,8 +50,12 @@ def main():
us_strategy.execute() us_strategy.execute()
# Test HK # Test HK
hk_strategy = get_strategy('HK', '00700.HK', tushare_token) hk_strategy = get_strategy('HK', '00700.HK', av_key=av_key)
hk_strategy.execute() hk_strategy.execute()
# Test JP
jp_strategy = get_strategy('JP', '7203') # Toyota
jp_strategy.execute()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -4,3 +4,4 @@ tushare
alpha_vantage alpha_vantage
python-dotenv python-dotenv
markdown markdown
jquants-api-client

Binary file not shown.

Binary file not shown.

View File

@ -6,7 +6,7 @@ class BaseAnalyzer:
self.market = market self.market = market
self.mapping = {} self.mapping = {}
def process_data(self, df_inc: pd.DataFrame, df_bal: pd.DataFrame, df_cf: pd.DataFrame, market_metrics: dict, historical_metrics: pd.DataFrame = None, df_div: pd.DataFrame = None, df_rep: pd.DataFrame = None) -> pd.DataFrame: def process_data(self, df_inc: pd.DataFrame, df_bal: pd.DataFrame, df_cf: pd.DataFrame, market_metrics: dict, historical_metrics: pd.DataFrame = None, df_div: pd.DataFrame = None, df_rep: pd.DataFrame = None, df_emp: pd.DataFrame = None) -> pd.DataFrame:
df_inc = self._map_columns(df_inc, 'income') df_inc = self._map_columns(df_inc, 'income')
df_bal = self._map_columns(df_bal, 'balance') df_bal = self._map_columns(df_bal, 'balance')
df_cf = self._map_columns(df_cf, 'cashflow') df_cf = self._map_columns(df_cf, 'cashflow')
@ -19,17 +19,20 @@ class BaseAnalyzer:
return pd.DataFrame() return pd.DataFrame()
df_merged = df_inc df_merged = df_inc
if not df_bal.empty: if not df_bal.empty and 'date_str' in df_bal.columns:
df_merged = pd.merge(df_merged, df_bal, on='date_str', how='outer', suffixes=('', '_bal')) df_merged = pd.merge(df_merged, df_bal, on='date_str', how='outer', suffixes=('', '_bal'))
if not df_cf.empty: if not df_cf.empty and 'date_str' in df_cf.columns:
df_merged = pd.merge(df_merged, df_cf, on='date_str', how='outer', suffixes=('', '_cf')) df_merged = pd.merge(df_merged, df_cf, on='date_str', how='outer', suffixes=('', '_cf'))
if df_div is not None and not df_div.empty: if df_div is not None and not df_div.empty and 'date_str' in df_div.columns:
df_merged = pd.merge(df_merged, df_div, on='date_str', how='left', suffixes=('', '_div')) df_merged = pd.merge(df_merged, df_div, on='date_str', how='left', suffixes=('', '_div'))
if df_rep is not None and not df_rep.empty: if df_rep is not None and not df_rep.empty and 'date_str' in df_rep.columns:
df_merged = pd.merge(df_merged, df_rep, on='date_str', how='left', suffixes=('', '_rep')) df_merged = pd.merge(df_merged, df_rep, on='date_str', how='left', suffixes=('', '_rep'))
if df_emp is not None and not df_emp.empty and 'date_str' in df_emp.columns:
df_merged = pd.merge(df_merged, df_emp, on='date_str', how='left', suffixes=('', '_emp'))
if df_merged.empty: if df_merged.empty:
return pd.DataFrame() return pd.DataFrame()

View File

@ -7,43 +7,23 @@ class HK_Analyzer(BaseAnalyzer):
super().__init__('HK') super().__init__('HK')
self.mapping = { self.mapping = {
'income': { 'income': {
'revenue': 'revenue', 'totalRevenue': 'revenue', 'costOfRevenue': 'cogs', 'grossProfit': 'gross_profit',
'net_profit_attr_p': 'net_income', 'sellingGeneralAndAdministrative': 'sga_exp',
# Assuming other fields are similar to CN, will need verification 'researchAndDevelopment': 'rd_exp', 'interestExpense': 'fin_exp',
'total_cogs': 'total_costs', 'incomeBeforeTax': 'total_profit', 'incomeTaxExpense': 'income_tax', 'netIncome': 'net_income',
'sell_exp': 'selling_exp', 'ebit': 'ebit',
'admin_exp': 'admin_exp',
'fin_exp': 'fin_exp',
'rd_exp': 'rd_exp',
'total_profit': 'total_profit',
'income_tax': 'income_tax'
}, },
'balance': { 'balance': {
'money_cap': 'cash', 'cashAndCashEquivalentsAtCarryingValue': 'cash', 'currentNetReceivables': 'receivables', 'inventory': 'inventory',
'accounts_receiv': 'receivables', 'propertyPlantEquipment': 'fixed_assets', 'totalAssets': 'total_assets', 'goodwill': 'goodwill',
'inventories': 'inventory', 'otherCurrentAssets': 'prepayment', 'longTermInvestments': 'lt_invest', 'otherNonCurrentAssets': 'other_assets',
'fix_assets': 'fixed_assets', 'shortTermDebt': 'short_term_debt', 'currentLongTermDebt': 'short_term_debt_part',
'total_assets': 'total_assets', 'longTermDebt': 'long_term_debt', 'totalLiabilities': 'total_liabilities', 'totalShareholderEquity': 'total_equity',
'goodwill': 'goodwill', 'currentAccountsPayable': 'accounts_payable', 'deferredRevenue': 'adv_receipts',
'prepayment': 'prepayment',
'lt_eqt_invest': 'lt_invest',
'oth_assets': 'other_assets',
'trad_asset': 'trading_assets',
'st_borr': 'short_term_debt',
'non_cur_liab_due_1y': 'short_term_debt_part',
'lt_borr': 'long_term_debt',
'bonds_payable': 'long_term_debt_bonds',
'total_liab': 'total_liabilities',
'total_share_holder_equity': 'total_equity',
'adv_receipts': 'adv_receipts',
'contract_liab': 'contract_liab',
'acct_payable': 'accounts_payable',
}, },
'cashflow': { 'cashflow': {
'n_cashflow_act': 'ocf', 'operatingCashflow': 'ocf', 'capitalExpenditures': 'capex', 'dividendPayout': 'dividends',
'c_pay_acq_const_fiolta': 'capex', 'depreciationDepletionAndAmortization': 'depreciation'
'c_paid_div_prof_int': 'dividends',
'c_paid_to_for_empl': 'cash_paid_for_employees'
} }
} }
@ -53,10 +33,6 @@ class HK_Analyzer(BaseAnalyzer):
if 'short_term_debt_part' in df.columns: if 'short_term_debt_part' in df.columns:
df['short_term_debt'] = df['short_term_debt'].fillna(0) + df['short_term_debt_part'].fillna(0) df['short_term_debt'] = df['short_term_debt'].fillna(0) + df['short_term_debt_part'].fillna(0)
if 'long_term_debt' not in df.columns: df['long_term_debt'] = 0
if 'long_term_debt_bonds' in df.columns:
df['long_term_debt'] = df['long_term_debt'].fillna(0) + df['long_term_debt_bonds'].fillna(0)
if type == 'income': if type == 'income':
if 'gross_profit' not in df.columns and 'revenue' in df.columns and 'cogs' in df.columns: if 'gross_profit' not in df.columns and 'revenue' in df.columns and 'cogs' in df.columns:
df['gross_profit'] = df['revenue'] - df['cogs'] df['gross_profit'] = df['revenue'] - df['cogs']
@ -118,22 +94,20 @@ class HK_Analyzer(BaseAnalyzer):
df_merged['FCF'] = df_merged['ocf'] - df_merged['Capex'] df_merged['FCF'] = df_merged['ocf'] - df_merged['Capex']
# Expenses # Expenses
if 'selling_exp' in df_merged.columns: if 'sga_exp' in df_merged.columns:
df_merged['SellingRatio'] = self._safe_div(df_merged['selling_exp'], df_merged['revenue']) df_merged['SgaRatio'] = self._safe_div(df_merged['sga_exp'], df_merged['revenue'])
if 'admin_exp' in df_merged.columns:
df_merged['AdminRatio'] = self._safe_div(df_merged['admin_exp'], df_merged['revenue'])
if 'rd_exp' in df_merged.columns: if 'rd_exp' in df_merged.columns:
df_merged['RDRatio'] = self._safe_div(df_merged['rd_exp'], df_merged['revenue']) df_merged['RDRatio'] = self._safe_div(df_merged['rd_exp'], df_merged['revenue'])
df_merged['SgaRatio'] = (df_merged.get('SellingRatio', 0)) + (df_merged.get('AdminRatio', 0))
if 'income_tax' in df_merged.columns and 'total_profit' in df_merged.columns: if 'income_tax' in df_merged.columns and 'total_profit' in df_merged.columns:
df_merged['TaxRate'] = self._safe_div(df_merged['income_tax'], df_merged['total_profit']) df_merged['TaxRate'] = self._safe_div(df_merged['income_tax'], df_merged['total_profit'])
# Other Expense Ratio (GrossMargin - Selling - Admin - RD - NetMargin) # Other Expense Ratio
if 'GrossMargin' in df_merged.columns and 'NetMargin' in df_merged.columns: if 'GrossMargin' in df_merged.columns and 'NetMargin' in df_merged.columns:
other_ratio = df_merged['GrossMargin'] - df_merged['NetMargin'] other_ratio = df_merged['GrossMargin'] - df_merged['NetMargin']
if 'SellingRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['SellingRatio'].fillna(0) if 'SgaRatio' in df_merged.columns:
if 'AdminRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['AdminRatio'].fillna(0) other_ratio = other_ratio - df_merged['SgaRatio'].fillna(0)
if 'RDRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['RDRatio'].fillna(0) if 'RDRatio' in df_merged.columns:
other_ratio = other_ratio - df_merged['RDRatio'].fillna(0)
df_merged['OtherExpenseRatio'] = other_ratio df_merged['OtherExpenseRatio'] = other_ratio
# Depreciation Expense Ratio # Depreciation Expense Ratio
@ -158,12 +132,14 @@ class HK_Analyzer(BaseAnalyzer):
if 'lt_invest' in df_merged.columns: df_merged['LongTermInvestmentRatio'] = self._safe_div(df_merged['lt_invest'], assets) if 'lt_invest' in df_merged.columns: df_merged['LongTermInvestmentRatio'] = self._safe_div(df_merged['lt_invest'], assets)
if 'goodwill' in df_merged.columns: df_merged['GoodwillRatio'] = self._safe_div(df_merged['goodwill'], assets) if 'goodwill' in df_merged.columns: df_merged['GoodwillRatio'] = self._safe_div(df_merged['goodwill'], assets)
# Other Assets Ratio (as a residual)
known_assets_ratio = (series_or_zero('CashRatio') + series_or_zero('InventoryRatio') + series_or_zero('ReceivablesRatio') + known_assets_ratio = (series_or_zero('CashRatio') + series_or_zero('InventoryRatio') + series_or_zero('ReceivablesRatio') +
series_or_zero('PrepaymentRatio') + series_or_zero('FixedAssetsRatio') + series_or_zero('LongTermInvestmentRatio') + series_or_zero('PrepaymentRatio') + series_or_zero('FixedAssetsRatio') + series_or_zero('LongTermInvestmentRatio') +
series_or_zero('GoodwillRatio')) series_or_zero('GoodwillRatio'))
df_merged['OtherAssetsRatio'] = 1 - known_assets_ratio df_merged['OtherAssetsRatio'] = 1 - known_assets_ratio
adv = series_or_zero('adv_receipts') + series_or_zero('contract_liab') # Liability Ratios
adv = series_or_zero('adv_receipts')
df_merged['AdvanceReceiptsRatio'] = self._safe_div(adv, assets) df_merged['AdvanceReceiptsRatio'] = self._safe_div(adv, assets)
st_debt = series_or_zero('short_term_debt') st_debt = series_or_zero('short_term_debt')
@ -173,6 +149,7 @@ class HK_Analyzer(BaseAnalyzer):
df_merged['LongTermDebtRatio'] = self._safe_div(lt_debt, assets) df_merged['LongTermDebtRatio'] = self._safe_div(lt_debt, assets)
df_merged['InterestBearingDebtRatio'] = self._safe_div(st_debt + lt_debt, assets) df_merged['InterestBearingDebtRatio'] = self._safe_div(st_debt + lt_debt, assets)
# Operating Assets Ratio
inv_ratio = series_or_zero('InventoryRatio') inv_ratio = series_or_zero('InventoryRatio')
rec_ratio = series_or_zero('ReceivablesRatio') rec_ratio = series_or_zero('ReceivablesRatio')
prep_ratio = series_or_zero('PrepaymentRatio') prep_ratio = series_or_zero('PrepaymentRatio')
@ -200,22 +177,5 @@ class HK_Analyzer(BaseAnalyzer):
df_merged.loc[df_merged.index[0], 'MarketCap'] = market_metrics.get('market_cap') df_merged.loc[df_merged.index[0], 'MarketCap'] = market_metrics.get('market_cap')
df_merged.loc[df_merged.index[0], 'PE'] = market_metrics.get('pe') df_merged.loc[df_merged.index[0], 'PE'] = market_metrics.get('pe')
df_merged.loc[df_merged.index[0], 'PB'] = market_metrics.get('pb') df_merged.loc[df_merged.index[0], 'PB'] = market_metrics.get('pb')
df_merged.loc[df_merged.index[0], 'Shareholders'] = market_metrics.get('total_share_holders')
# Employees & Per-Employee Metrics
if not df_merged.empty:
annual_mask = df_merged['date_str'].str.endswith('1231')
annual_idxs = df_merged.index[annual_mask]
if len(annual_idxs) > 0:
target_idx = annual_idxs[0]
emps = market_metrics.get('employee_count', 0)
if emps > 0:
df_merged.loc[target_idx, 'Employees'] = emps
if 'revenue' in df_merged.columns:
df_merged.loc[target_idx, 'RevenuePerEmp'] = df_merged.loc[target_idx, 'revenue'] / emps
if 'net_income' in df_merged.columns:
df_merged.loc[target_idx, 'ProfitPerEmp'] = df_merged.loc[target_idx, 'net_income'] / emps
if 'cash_paid_for_employees' in df_merged.columns:
df_merged.loc[target_idx, 'AvgWage'] = df_merged.loc[target_idx, 'cash_paid_for_employees'] / emps
return df_merged return df_merged

170
src/analysis/jp_analyzer.py Normal file
View File

@ -0,0 +1,170 @@
from .cn_analyzer import CN_Analyzer
import pandas as pd
class JP_Analyzer(CN_Analyzer):
def __init__(self):
# We can inherit from CN_Analyzer as the core financial logic is similar
# but we use 'JP' market context.
super().__init__()
self.market = 'JP'
# JpFetcher already provides standardized fields, but we can fine-tune
# mapping if J-Quants has unique field names.
# For now, JpFetcher maps to the same standard as CN_Analyzer expects.
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):
# Override to ensure all columns in the mapping are numeric
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':
# Rename long_term_investments to lt_invest for compatibility with CN_Analyzer
if 'long_term_investments' in df.columns:
df['lt_invest'] = df['long_term_investments']
# Sum long_term_debt and long_term_borrowings
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):
# Calculate COGS (Cost of Goods Sold) for turnover calculations
# COGS = Revenue - Gross Profit
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)
# Override SG&A Ratio for JP
# Try using direct SG&A indicator first
has_sga = False
if 'sga_exp' in df_merged.columns and 'revenue' in df_merged.columns:
# Check if we have valid non-zero/non-nan SG&A data for at least some rows
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
# Fallback to Summing Selling + Admin if SG&A is missing
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'])
# Tax Rate Calculation (Approximated as Income Tax / (Net Income + Income Tax))
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)
# Recalculate OtherExpenseRatio using SG&A
# OtherExpenseRatio = GrossMargin - NetMargin - SgaRatio - RDRatio
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
# Recalculate Market Metrics based on User Formula
# PE = MarketCap / NetIncome
# PB = MarketCap / TotalEquity
# DividendYield = Dividends / MarketCap (Note: Yield is usually Dividend/Price, but User asked for MarketCap/Dividend which is inverse yield, but formula says Yield=MarketCap/Dividend? No, wait.
# User said: "股息率=市值/分红". Wait, Dividend Yield is traditionally Dividend / Market Cap.
# Market Cap / Dividend is Dividend Ratio or something.
# But if user says "股息率=市值/分红", that's P/D ratio.
# Let me re-read: "股息率=市值/分红". That usually means the INVERSE of yield (PE-like).
# However, standard "股息率" (Yield) is Div/Price or TotalDiv/MarketCap.
# User might be confused or means P/D ratio.
# BUT, to be safe and standard, I should probably stick to Div/MarketCap if I want "Yield".
# Let's look at the request again: "PE=市值/净利润PB=市值/净资产,股息率=市值/分红".
# This is definitely P/E, P/B, and P/D (Price to Dividend).
# Most likely the user wants to see P/D ratio labeled as "股息率"? Or does he mean "股息率 = 分红/市值" and typed it wrong?
# Usually "股息率" is a percentage like 3%. P/D would be like 33.
# I will assume he wants the standard Yield (Div/MC) but maybe typed it wrong, OR he wants P/D.
# Given "PE=..., PB=...", he might be listing Valuation Ratios.
# I will calculate standard "Dividend Yield = Dividends / MarketCap" because that is what "股息率" means in Chinese.
# "市值/分红" would be "市息率".
# I'll stick to Div/MC for "Dividend Yield" to output a % value.
# Actually, let's strictly follow "PE=市值/净利润" and "PB=市值/净资产".
# For "股息率", if I use Div/MC, it makes sense.
# Recalculate Market Metrics based on User Formula if usage data is missing
if 'MarketCap' in df_merged.columns:
# PE
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:
# Fill only if missing or zero
cond_pe = (df_merged['PE'] != 0) & df_merged['PE'].notna()
df_merged['PE'] = df_merged['PE'].where(cond_pe, calculated_pe)
# PB
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)
# Dividend Yield
if 'dividends' in df_merged.columns:
# Standard Yield: Div / MC * 100
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:
# Note: Yield might be legitimately 0, but here we assume if it's 0 we might want to recalc?
# No, if fetched yield is 0 it means no dividend.
# Only fill if NA.
df_merged['DividendYield'] = df_merged['DividendYield'].fillna(calculated_yield)
# Per Employee Metrics
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '300033.SZ', 'table': {'total_assets': [11453177228.47], 'total_liab': [3950208370.78], 'equity_atsopc_sbi': [None], 'inventory': [None], 'net_fixed_assets': [None], 'st_debt': [None], 'lt_debt': [None], 'cce': [9743566928.48], 'account_receivable': [49713654.99]}}]","[{'itemid': 'total_assets', 'type': 'DT_DOUBLE'}, {'itemid': 'total_liab', 'type': 'DT_DOUBLE'}, {'itemid': 'equity_atsopc_sbi', 'type': 'DT_DOUBLE'}, {'itemid': 'inventory', 'type': 'DT_DOUBLE'}, {'itemid': 'net_fixed_assets', 'type': 'DT_DOUBLE'}, {'itemid': 'st_debt', 'type': 'DT_DOUBLE'}, {'itemid': 'lt_debt', 'type': 'DT_DOUBLE'}, {'itemid': 'cce', 'type': 'DT_DOUBLE'}, {'itemid': 'account_receivable', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'total_assets', 'id': '30755', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'total_liab', 'id': '31036', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'equity_atsopc_sbi', 'id': '32271', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'inventory', 'id': '30561', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'net_fixed_assets', 'id': '32312', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'st_debt', 'id': '32274', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'lt_debt', 'id': '32414', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'cce', 'id': '30730', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'account_receivable', 'id': '32319', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",9,78
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '300033.SZ', 'table': {'total_assets': [11453177228.47], 'total_liab': [3950208370.78], 'equity_atsopc_sbi': [None], 'inventory': [None], 'net_fixed_assets': [None], 'st_debt': [None], 'lt_debt': [None], 'cce': [9743566928.48], 'account_receivable': [49713654.99]}}] [{'itemid': 'total_assets', 'type': 'DT_DOUBLE'}, {'itemid': 'total_liab', 'type': 'DT_DOUBLE'}, {'itemid': 'equity_atsopc_sbi', 'type': 'DT_DOUBLE'}, {'itemid': 'inventory', 'type': 'DT_DOUBLE'}, {'itemid': 'net_fixed_assets', 'type': 'DT_DOUBLE'}, {'itemid': 'st_debt', 'type': 'DT_DOUBLE'}, {'itemid': 'lt_debt', 'type': 'DT_DOUBLE'}, {'itemid': 'cce', 'type': 'DT_DOUBLE'}, {'itemid': 'account_receivable', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'total_assets', 'id': '30755', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'total_liab', 'id': '31036', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'equity_atsopc_sbi', 'id': '32271', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'inventory', 'id': '30561', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'net_fixed_assets', 'id': '32312', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'st_debt', 'id': '32274', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'lt_debt', 'id': '32414', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'cce', 'id': '30730', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'account_receivable', 'id': '32319', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 9 78

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '300033.SZ', 'table': {'ncf_from_oa': [2195884161.73]}}]","[{'itemid': 'ncf_from_oa', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'ncf_from_oa', 'id': '30905', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",1,15
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '300033.SZ', 'table': {'ncf_from_oa': [2195884161.73]}}] [{'itemid': 'ncf_from_oa', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'ncf_from_oa', 'id': '30905', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 1 15

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '300033.SZ', 'table': {'gross_profit': [2903497831.31]}}]","[{'itemid': 'gross_profit', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'gross_profit', 'id': '30590', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",1,82
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '300033.SZ', 'table': {'gross_profit': [2903497831.31]}}] [{'itemid': 'gross_profit', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'gross_profit', 'id': '30590', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 1 82

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '7203.T', 'table': {'total_assets_oas': [93601350000000.0], 'total_liabilities_oas': [56722437000000.0], 'equity_attri_to_companyowner_oas': [35924826000000.0], 'inventories_oas': [4598232000000.0], 'ppe_net_oas': [15916760000000.0], 'short_term_debt_oas': [15829516000000.0], 'long_term_debt_oas': [22963362000000.0], 'cash_equi_oas': [8982404000000.0], 'accou_and_notes_recei_oas': [13896177000000.0]}}]","[{'itemid': 'total_assets_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'total_liabilities_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'equity_attri_to_companyowner_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'inventories_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'ppe_net_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'short_term_debt_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'long_term_debt_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'cash_equi_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'accou_and_notes_recei_oas', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'total_assets_oas', 'id': '56298', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'total_liabilities_oas', 'id': '56333', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'equity_attri_to_companyowner_oas', 'id': '56343', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'inventories_oas', 'id': '56264', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'ppe_net_oas', 'id': '56275', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'short_term_debt_oas', 'id': '56299', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'long_term_debt_oas', 'id': '56318', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'cash_equi_oas', 'id': '56252', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'accou_and_notes_recei_oas', 'id': '56258', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",9,23
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '7203.T', 'table': {'total_assets_oas': [93601350000000.0], 'total_liabilities_oas': [56722437000000.0], 'equity_attri_to_companyowner_oas': [35924826000000.0], 'inventories_oas': [4598232000000.0], 'ppe_net_oas': [15916760000000.0], 'short_term_debt_oas': [15829516000000.0], 'long_term_debt_oas': [22963362000000.0], 'cash_equi_oas': [8982404000000.0], 'accou_and_notes_recei_oas': [13896177000000.0]}}] [{'itemid': 'total_assets_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'total_liabilities_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'equity_attri_to_companyowner_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'inventories_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'ppe_net_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'short_term_debt_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'long_term_debt_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'cash_equi_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'accou_and_notes_recei_oas', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'total_assets_oas', 'id': '56298', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'total_liabilities_oas', 'id': '56333', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'equity_attri_to_companyowner_oas', 'id': '56343', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'inventories_oas', 'id': '56264', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'ppe_net_oas', 'id': '56275', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'short_term_debt_oas', 'id': '56299', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'long_term_debt_oas', 'id': '56318', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'cash_equi_oas', 'id': '56252', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'accou_and_notes_recei_oas', 'id': '56258', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 9 23

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '7203.T', 'table': {'net_cash_flows_from_oa_oas': [3696934000000.0], 'purchase_of_ppe_and_ia_oas': [2261007000000.0], 'dividends_paid_oas': [1132329000000.0]}}]","[{'itemid': 'net_cash_flows_from_oa_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'purchase_of_ppe_and_ia_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'dividends_paid_oas', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'net_cash_flows_from_oa_oas', 'id': '56362', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'purchase_of_ppe_and_ia_oas', 'id': '56367', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'dividends_paid_oas', 'id': '56385', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",3,16
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '7203.T', 'table': {'net_cash_flows_from_oa_oas': [3696934000000.0], 'purchase_of_ppe_and_ia_oas': [2261007000000.0], 'dividends_paid_oas': [1132329000000.0]}}] [{'itemid': 'net_cash_flows_from_oa_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'purchase_of_ppe_and_ia_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'dividends_paid_oas', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'net_cash_flows_from_oa_oas', 'id': '56362', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'purchase_of_ppe_and_ia_oas', 'id': '56367', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'dividends_paid_oas', 'id': '56385', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 3 16

View File

@ -0,0 +1,2 @@
errorcode,errmsg,tables,datatype,inputParams,dataVol,perf
0,,"[{'thscode': '7203.T', 'table': {'revenue_oas': [48036704000000.0], 'net_income_oas': [4789755000000.0], 'operating_income_oas': [4795586000000.0], 'gross_profit_oas': [9578038000000.0]}}]","[{'itemid': 'revenue_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'net_income_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'operating_income_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'gross_profit_oas', 'type': 'DT_DOUBLE'}]","{'jsonrpc': True, 'params': [{'function': 'revenue_oas', 'id': '56122', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'net_income_oas', 'id': '56161', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'operating_income_oas', 'id': '56141', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'gross_profit_oas', 'id': '56130', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]}",4,31
1 errorcode errmsg tables datatype inputParams dataVol perf
2 0 [{'thscode': '7203.T', 'table': {'revenue_oas': [48036704000000.0], 'net_income_oas': [4789755000000.0], 'operating_income_oas': [4795586000000.0], 'gross_profit_oas': [9578038000000.0]}}] [{'itemid': 'revenue_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'net_income_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'operating_income_oas', 'type': 'DT_DOUBLE'}, {'itemid': 'gross_profit_oas', 'type': 'DT_DOUBLE'}] {'jsonrpc': True, 'params': [{'function': 'revenue_oas', 'id': '56122', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'net_income_oas', 'id': '56161', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'operating_income_oas', 'id': '56141', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}, {'function': 'gross_profit_oas', 'id': '56130', 'params': [{'name': 'THSCODE', 'system': 'false', 'value': ''}, {'name': 'FDREPORT', 'system': 'false', 'value': '8'}, {'name': 'FL', 'system': 'false', 'value': '1'}, {'name': 'CURRENCYTYPE', 'system': 'false', 'value': 'BB'}, {'name': 'FDIR', 'system': 'false', 'value': ''}, {'name': 'UNIT', 'system': 'false', 'value': ''}]}]} 4 31

View File

@ -0,0 +1,84 @@
import os
import sys
import json
import requests
import time
from dotenv import load_dotenv
# Add src to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from fetchers.ifind_client import IFindClient
def debug_ifind():
load_dotenv()
token = os.getenv("IFIND_REFRESH_TOKEN")
if not token:
print("Missing IFIND_REFRESH_TOKEN in .env")
return
client = IFindClient(token)
symbol = "7203.T"
print(f"--- Testing basic_data_service for {symbol} ---")
params = {
"codes": symbol,
"indipara": [
{"indicator": "revenue_oas", "indiparams": ["8", "1", "BB"]},
{"indicator": "ths_reporting_period_stock", "indiparams": []}
]
}
res = client.post("basic_data_service", params)
print(json.dumps(res, indent=2, ensure_ascii=False))
print(f"\n--- Testing date_sequence Interval='Y' for {symbol} ---")
end_date = time.strftime("%Y%m%d")
start_date = "20220101"
params_seq = {
"codes": symbol,
"startdate": start_date,
"enddate": end_date,
"functionpara": {"Interval": "Y", "Days": "Alldays", "Fill": "Previous"},
"indipara": [
{"indicator": "revenue_oas", "indiparams": ["8", "", "BB"]}
]
}
res_seq = client.post("date_sequence", params_seq)
print(json.dumps(res_seq, indent=2, ensure_ascii=False))
print(f"\n--- Testing cmd_history_quotation for {symbol} ---")
params_hist = {
"codes": symbol,
"startdate": start_date,
"enddate": end_date,
"indicators": "close,totalCapital",
"functionpara": {"Interval": "D", "Days": "Alldays", "Fill": "Previous"}
}
res_hist = client.post("cmd_history_quotation", params_hist)
print(json.dumps(res_hist, indent=2, ensure_ascii=False))
print(f"\n--- Testing date_sequence with standard indicators for {symbol} ---")
params_std = {
"codes": symbol,
"startdate": start_date,
"enddate": end_date,
"functionpara": {"Interval": "D", "Days": "Alldays", "Fill": "Previous"},
"indipara": [
{"indicator": "total_mv", "indiparams": ["1", "BB"]},
{"indicator": "pe_ttm", "indiparams": ["1", "BB"]}
]
}
print(f"\n--- Testing basic_data_service with specific dates for {symbol} ---")
params_dates = {
"codes": symbol,
"indipara": [
{"indicator": "revenue_oas", "indiparams": ["20240331", "", "BB"]},
{"indicator": "revenue_oas", "indiparams": ["20230331", "", "BB"]}
]
}
res_dates = client.post("basic_data_service", params_dates)
print(json.dumps(res_dates, indent=2, ensure_ascii=False))
if __name__ == "__main__":
debug_ifind()

View File

@ -1,23 +1,31 @@
from .cn_fetcher import CnFetcher
from .hk_fetcher import HkFetcher
from .us_fetcher import UsFetcher
from .base import DataFetcher
class FetcherFactory: class FetcherFactory:
@staticmethod @staticmethod
def get_fetcher(market: str, tushare_token: str = None, av_key: str = None) -> DataFetcher: def get_fetcher(market: str, tushare_token: str = None, av_key: str = None, **kwargs):
from .base import DataFetcher
market = market.upper() market = market.upper()
if market == 'CN': if market == 'CN':
if not tushare_token: if not tushare_token:
raise ValueError("Tushare token is required for CN market") raise ValueError("Tushare token is required for CN market")
from .cn_fetcher import CnFetcher
return CnFetcher(tushare_token) return CnFetcher(tushare_token)
if market == 'HK': if market == 'HK':
if not tushare_token: if not av_key:
raise ValueError("Tushare token is required for HK market") raise ValueError("Alpha Vantage key is required for HK market")
return HkFetcher(tushare_token) from .hk_fetcher import HkFetcher
return HkFetcher(av_key)
elif market == 'US': elif market == 'US':
if not av_key: if not av_key:
raise ValueError("Alpha Vantage key is required for US market") raise ValueError("Alpha Vantage key is required for US market")
from .us_fetcher import UsFetcher
return UsFetcher(av_key) return UsFetcher(av_key)
elif market == 'JP':
ifind_token = kwargs.get('ifind_refresh_token') or kwargs.get('jquants_refresh_token')
if not ifind_token:
import os
ifind_token = os.getenv('IFIND_REFRESH_TOKEN') or os.getenv('JQUANTS_REFRESH_TOKEN')
if not ifind_token:
raise ValueError("iFinD Refresh Token is required for JP market")
from .jp_fetcher import JpFetcher
return JpFetcher(ifind_token)
else: else:
raise ValueError(f"Unsupported market: {market}") raise ValueError(f"Unsupported market: {market}")

View File

@ -1,172 +1,212 @@
import tushare as ts import requests
import pandas as pd import pandas as pd
from .base import DataFetcher
import time import time
from .base import DataFetcher
from storage.file_io import DataStorage from storage.file_io import DataStorage
class HkFetcher(DataFetcher): class HkFetcher(DataFetcher):
BASE_URL = "https://www.alphavantage.co/query"
def __init__(self, api_key: str): def __init__(self, api_key: str):
super().__init__(api_key) super().__init__(api_key)
ts.set_token(self.api_key)
self.pro = ts.pro_api()
self.storage = DataStorage() self.storage = DataStorage()
def _save_raw_data(self, df: pd.DataFrame, symbol: str, name: str): def _sanitize_symbol(self, symbol: str) -> str:
if df is None or df.empty: if '.HK' in symbol.upper():
return code = symbol.upper().replace('.HK', '')
market = 'HK' if code.isdigit():
self.storage.save_data(df, market, symbol, f"raw_{name}") try:
# e.g., '0700.HK' -> '700.HK', '0005.HK' -> '5.HK'
def _get_ts_code(self, symbol: str) -> str: return str(int(code)) + '.HK'
except ValueError:
# Keep original symbol if not a simple integer
return symbol
return symbol return symbol
def _filter_data(self, df: pd.DataFrame) -> pd.DataFrame: def _save_raw_data(self, data, symbol: str, name: str):
if df.empty or 'end_date' not in df.columns: if data is None:
return df return
df = df.sort_values(by='end_date', ascending=False)
df = df.drop_duplicates(subset=['end_date'], keep='first') df = pd.DataFrame()
if df.empty: if isinstance(data, list):
return df df = pd.DataFrame(data)
latest_record = df.iloc[[0]] elif isinstance(data, dict):
# For single-record JSON objects, convert to a DataFrame
df = pd.DataFrame([data])
if not df.empty:
self.storage.save_data(df, 'HK', symbol, f"raw_{name}")
def _fetch_data(self, function: str, symbol: str) -> pd.DataFrame:
symbol = self._sanitize_symbol(symbol)
params = {
"function": function,
"symbol": symbol,
"apikey": self.api_key
}
try: try:
latest_date_str = str(latest_record['end_date'].values[0]) # Alpha Vantage free tier is limited to 25 requests per day.
last_year_date_str = str(int(latest_date_str) - 10000) # A 15-second sleep is a precaution for not hitting minute-based rate limits if any.
comparable_record = df[df['end_date'].astype(str) == last_year_date_str] time.sleep(15)
except: response = requests.get(self.BASE_URL, params=params)
comparable_record = pd.DataFrame() response.raise_for_status() # Raise an exception for bad status codes
is_annual = df['end_date'].astype(str).str.endswith('1231') data = response.json()
annual_records = df[is_annual] except requests.exceptions.RequestException as e:
combined = pd.concat([latest_record, comparable_record, annual_records]) print(f"Error requesting {function} for {symbol}: {e}")
combined = combined.drop_duplicates(subset=['end_date']) return pd.DataFrame()
combined = combined.sort_values(by='end_date', ascending=False) except Exception as e:
return combined print(f"An unexpected error occurred while fetching {function} for {symbol}: {e}")
return pd.DataFrame()
if "Note" in data:
print(f"API Note for {function} on {symbol}: {data['Note']}")
if data:
self._save_raw_data(data.get("annualReports"), symbol, f"{function.lower()}_annual")
df_annual = pd.DataFrame()
if "annualReports" in data and data["annualReports"]:
df_annual = pd.DataFrame(data["annualReports"])
if "fiscalDateEnding" in df_annual.columns:
df_annual = df_annual.sort_values("fiscalDateEnding", ascending=False)
else:
print(f"Error or no data fetching {function} for {symbol}: {data}")
return pd.DataFrame()
return df_annual
def get_market_metrics(self, symbol: str) -> dict:
symbol = self._sanitize_symbol(symbol)
overview_data = {}
try:
time.sleep(15)
params = {"function": "OVERVIEW", "symbol": symbol, "apikey": self.api_key}
r = requests.get(self.BASE_URL, params=params)
r.raise_for_status()
overview_data = r.json()
if "Note" in overview_data:
print(f"API Note for OVERVIEW on {symbol}: {overview_data['Note']}")
# Clean up 'None' strings from API response before processing
if isinstance(overview_data, dict):
for key, value in overview_data.items():
if value == 'None' or value == '-':
overview_data[key] = None
self._save_raw_data(overview_data, symbol, "market_metrics_overview")
except requests.exceptions.RequestException as e:
print(f"Error fetching OVERVIEW for {symbol}: {e}")
return {}
except Exception as e:
print(f"An unexpected error occurred while fetching OVERVIEW for {symbol}: {e}")
return {}
if not overview_data or not overview_data.get("MarketCapitalization"):
print(f"Error or no data fetching OVERVIEW for {symbol}: {overview_data}")
return {}
market_cap = float(overview_data.get("MarketCapitalization") or 0)
shares_outstanding = float(overview_data.get("SharesOutstanding") or 0)
price = 0
if shares_outstanding > 0:
price = market_cap / shares_outstanding
return {
"price": price,
"name": overview_data.get("Name"),
"fiscal_year_end": overview_data.get("FiscalYearEnd"),
"dividend_yield": float(overview_data.get("DividendYield") or 0),
"market_cap": market_cap,
"pe": float(overview_data.get("PERatio") or 0),
"pb": float(overview_data.get("PriceToBookRatio") or 0),
"employee_count": int(float(overview_data.get("FullTimeEmployees") or 0)),
"total_share_holders": 0 # Not typically provided in basic AV Overview
}
def get_income_statement(self, symbol: str) -> pd.DataFrame: def get_income_statement(self, symbol: str) -> pd.DataFrame:
ts_code = self._get_ts_code(symbol) df = self._fetch_data("INCOME_STATEMENT", symbol)
df = self.pro.hk_income(ts_code=ts_code) if df.empty:
self._save_raw_data(df, ts_code, "income_statement_hk") return df
rename_map = { cols_map = {
'end_date': 'date', "fiscalDateEnding": "date",
'revenue': 'revenue', "totalRevenue": "revenue",
'net_profit_attr_p': 'net_income' "netIncome": "net_income",
"grossProfit": "gross_profit",
"costOfRevenue": "cogs",
"researchAndDevelopment": "rd_exp",
"sellingGeneralAndAdministrative": "sga_exp",
"interestExpense": "fin_exp",
"incomeBeforeTax": "total_profit",
"incomeTaxExpense": "income_tax",
"ebit": "ebit",
"depreciation": "depreciation"
} }
df = self._filter_data(df) df = df.rename(columns=cols_map)
df = df.rename(columns=rename_map)
numeric_cols = [
"revenue", "net_income", "gross_profit", "cogs", "rd_exp", "sga_exp",
"fin_exp", "total_profit", "income_tax", "ebit", "depreciation"
]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
return df return df
def get_balance_sheet(self, symbol: str) -> pd.DataFrame: def get_balance_sheet(self, symbol: str) -> pd.DataFrame:
ts_code = self._get_ts_code(symbol) df = self._fetch_data("BALANCE_SHEET", symbol)
df = self.pro.hk_balancesheet(ts_code=ts_code) if df.empty:
self._save_raw_data(df, ts_code, "balance_sheet_hk") return df
rename_map = { cols_map = {
'end_date': 'date', "fiscalDateEnding": "date",
'total_share_holder_equity': 'total_equity', "totalShareholderEquity": "total_equity",
'total_liab': 'total_liabilities', "totalLiabilities": "total_liabilities",
'total_cur_asset': 'current_assets', "totalCurrentAssets": "current_assets",
'total_cur_liab': 'current_liabilities' "totalCurrentLiabilities": "current_liabilities",
"cashAndCashEquivalentsAtCarryingValue": "cash",
"currentNetReceivables": "receivables",
"inventory": "inventory",
"propertyPlantEquipment": "fixed_assets",
"totalAssets": "total_assets",
"goodwill": "goodwill",
"longTermInvestments": "lt_invest",
"shortTermDebt": "short_term_debt",
"currentLongTermDebt": "short_term_debt_part",
"longTermDebt": "long_term_debt",
"currentAccountsPayable": "accounts_payable",
"otherCurrentAssets": "prepayment",
"otherNonCurrentAssets": "other_assets",
"deferredRevenue": "adv_receipts"
} }
df = self._filter_data(df) df = df.rename(columns=cols_map)
df = df.rename(columns=rename_map)
numeric_cols = [
"total_equity", "total_liabilities", "current_assets", "current_liabilities",
"cash", "receivables", "inventory", "fixed_assets", "total_assets",
"goodwill", "lt_invest", "short_term_debt", "short_term_debt_part",
"long_term_debt", "accounts_payable", "prepayment", "other_assets", "adv_receipts"
]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
return df return df
def get_cash_flow(self, symbol: str) -> pd.DataFrame: def get_cash_flow(self, symbol: str) -> pd.DataFrame:
ts_code = self._get_ts_code(symbol) df = self._fetch_data("CASH_FLOW", symbol)
df = self.pro.hk_cashflow(ts_code=ts_code) if df.empty:
self._save_raw_data(df, ts_code, "cash_flow_hk")
df = self._filter_data(df)
df = df.rename(columns={
'end_date': 'date',
'n_cashflow_act': 'net_cash_flow',
'depr_fa_coga_dpba': 'depreciation'
})
return df return df
cols_map = {
def get_market_metrics(self, symbol: str) -> dict: "fiscalDateEnding": "date",
ts_code = self._get_ts_code(symbol) "operatingCashflow": "ocf",
metrics = { "capitalExpenditures": "capex",
"price": 0.0, "dividendPayout": "dividends",
"market_cap": 0.0, "depreciationDepletionAndAmortization": "depreciation"
"pe": 0.0,
"pb": 0.0,
"total_share_holders": 0,
"employee_count": 0
} }
df = df.rename(columns=cols_map)
try: numeric_cols = ["ocf", "capex", "dividends", "depreciation"]
df_daily = self.pro.daily_basic(ts_code=ts_code, limit=1) for col in numeric_cols:
self._save_raw_data(df_daily, ts_code, "market_metrics_daily_basic") if col in df.columns:
if not df_daily.empty: df[col] = pd.to_numeric(df[col], errors='coerce')
row = df_daily.iloc[0] return df
metrics["price"] = row.get('close', 0.0)
metrics["pe"] = row.get('pe', 0.0)
metrics["pb"] = row.get('pb', 0.0)
metrics["market_cap"] = row.get('total_mv', 0.0) * 10000
metrics["dividend_yield"] = row.get('dv_ttm', 0.0)
df_basic = self.pro.stock_basic(ts_code=ts_code)
self._save_raw_data(df_basic, ts_code, "market_metrics_stock_basic")
if not df_basic.empty:
metrics['name'] = df_basic.iloc[0]['name']
metrics['list_date'] = df_basic.iloc[0]['list_date']
df_comp = self.pro.stock_company(ts_code=ts_code)
if not df_comp.empty:
metrics["employee_count"] = int(df_comp.iloc[0].get('employees', 0) or 0)
df_holder = self.pro.stk_holdernumber(ts_code=ts_code, limit=1)
self._save_raw_data(df_holder, ts_code, "market_metrics_shareholder_number")
if not df_holder.empty:
metrics["total_share_holders"] = int(df_holder.iloc[0].get('holder_num', 0) or 0)
except Exception as e:
print(f"Error fetching market metrics for {symbol}: {e}")
return metrics
def get_historical_metrics(self, symbol: str, dates: list) -> pd.DataFrame:
ts_code = self._get_ts_code(symbol)
results = []
if not dates:
return pd.DataFrame()
unique_dates = sorted(list(set([str(d).replace('-', '') for d in dates])), reverse=True)
try:
import datetime
min_date = min(unique_dates)
max_date = max(unique_dates)
df_daily = self.pro.daily_basic(ts_code=ts_code, start_date=min_date, end_date=max_date)
self._save_raw_data(df_daily, ts_code, "historical_metrics_daily_basic")
if not df_daily.empty:
df_daily = df_daily.sort_values('trade_date', ascending=False)
df_holder = self.pro.stk_holdernumber(ts_code=ts_code, start_date=min_date, end_date=max_date)
self._save_raw_data(df_holder, ts_code, "historical_metrics_shareholder_number")
if not df_holder.empty:
df_holder = df_holder.sort_values('end_date', ascending=False)
for date_str in unique_dates:
metrics = {'date_str': date_str}
if not df_daily.empty:
closest_daily = df_daily[df_daily['trade_date'] <= date_str]
if not closest_daily.empty:
row = closest_daily.iloc[0]
metrics['Price'] = row.get('close')
metrics['PE'] = row.get('pe')
metrics['PB'] = row.get('pb')
metrics['MarketCap'] = row.get('total_mv', 0) * 10000
if not df_holder.empty:
closest_holder = df_holder[df_holder['end_date'] <= date_str]
if not closest_holder.empty:
metrics['Shareholders'] = closest_holder.iloc[0].get('holder_num')
results.append(metrics)
except Exception as e:
print(f"Error fetching historical metrics for {symbol}: {e}")
return pd.DataFrame(results)

View File

@ -0,0 +1,62 @@
import requests
import json
import time
class IFindClient:
"""
同花顺 iFinD HTTP API 客户端基类负责鉴权和请求分发
"""
def __init__(self, refresh_token: str):
self.refresh_token = refresh_token
self.access_token = None
self.token_expiry = 0
self.base_url = "https://quantapi.51ifind.com/api/v1"
def _get_access_token(self):
"""获取或刷新当前的 access_token"""
if self.access_token and time.time() < self.token_expiry:
return self.access_token
url = f"{self.base_url}/get_access_token"
headers = {
"Content-Type": "application/json",
"refresh_token": self.refresh_token
}
try:
response = requests.post(url, headers=headers)
data = response.json()
if data.get("errorcode") == 0:
self.access_token = data["data"]["access_token"]
# 按照手册说明,有效期 7 天,我们设为 6 天以防万一
self.token_expiry = time.time() + (6 * 24 * 3600)
return self.access_token
else:
raise Exception(f"iFinD access token error: {data.get('errmsg')} (Code: {data.get('errorcode')})")
except Exception as e:
print(f"Error refreshing iFinD access token: {e}")
return None
def post(self, endpoint: str, params: dict):
"""发送 POST 请求到指定的 API 端点"""
token = self._get_access_token()
if not token:
return None
url = f"{self.base_url}/{endpoint}"
headers = {
"Content-Type": "application/json",
"access_token": token
}
# 记录重试次数或简单报错
try:
response = requests.post(url, headers=headers, json=params)
if response.status_code != 200:
print(f"iFinD API HTTP Error: {response.status_code} - {response.text}")
return None
return response.json()
except Exception as e:
print(f"iFinD API Request Error on {endpoint}: {e}")
return None

View File

@ -0,0 +1,90 @@
import os
import requests
import json
import time
class IFindClient:
def __init__(self, refresh_token: str):
self.refresh_token = refresh_token
self.access_token = None
self.token_expiry = 0
self.base_url = "https://quantapi.51ifind.com/api/v1"
def _get_access_token(self):
"""获取当前的 access_token如果过期则刷新"""
# 简单判断,手册说有效期 7 天,我们这里如果没 token 就去换一个
if self.access_token and time.time() < self.token_expiry:
return self.access_token
url = f"{self.base_url}/get_access_token"
headers = {
"Content-Type": "application/json",
"refresh_token": self.refresh_token
}
try:
response = requests.post(url, headers=headers)
data = response.json()
if data.get("errorcode") == 0:
self.access_token = data["data"]["access_token"]
# 设个保守的有效期,比如 6 天
self.token_expiry = time.time() + (6 * 24 * 3600)
return self.access_token
else:
raise Exception(f"iFinD login failed: {data.get('errmsg')}")
except Exception as e:
print(f"Error getting iFinD access token: {e}")
return None
def post(self, endpoint, params):
token = self._get_access_token()
if not token:
return None
url = f"{self.base_url}/{endpoint}"
headers = {
"Content-Type": "application/json",
"access_token": token
}
try:
response = requests.post(url, headers=headers, json=params)
return response.json()
except Exception as e:
print(f"iFinD request error on {endpoint}: {e}")
return None
if __name__ == "__main__":
# 简单测试代码
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("IFIND_REFRESH_TOKEN")
if token:
client = IFindClient(token)
code = "7203.T"
print(f"--- Testing date_sequence Global indicators for {code} ---")
params = {
"codes": code,
"startdate": "2024-12-01",
"enddate": "2024-12-10",
"functionpara": {"Interval": "D", "Days": "Alldays", "Fill": "Blank"},
"indipara": [
{"indicator": "pe_ttm", "indiparams": ["1", "BB"]},
{"indicator": "pb", "indiparams": ["1", "BB"]},
{"indicator": "total_mv", "indiparams": ["1", "BB"]}
]
}
res = client.post("date_sequence", params)
if res and res.get('errorcode') == 0:
tables = res.get('tables', [])
if tables:
print(json.dumps(tables[0]['table'], indent=2))
else:
print("Empty tables")
else:
print("Failed")
else:
print("No IFIND_REFRESH_TOKEN found in .env")

479
src/fetchers/jp_fetcher.py Normal file
View File

@ -0,0 +1,479 @@
import pandas as pd
import os
import time
from .base import DataFetcher
from .ifind_client import IFindClient
from storage.file_io import DataStorage
class JpFetcher(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:
"""保持逻辑一致性,如果是纯数字则补齐后缀 .T否则直接传"""
if symbol.isdigit():
return f"{symbol}.T"
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": []}
]
}
# print(f"iFinD API Request: endpoint=basic_data_service, params={params}")
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", # 默认 12-31
"ipo_date": ""
}
if not df.empty:
row = df.iloc[0]
info["name"] = str(row.get("corp_cn_name", ""))
# accounting_date 通常返回类似 "03-31" 或 "1231"
acc_date = str(row.get("accounting_date", "1231")).replace("-", "").replace("/", "")
# 好像是ifind的API有问题明明财报是0331但如果去读20240331就是空数据
# 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
# 如果是字典API 响应),直接保存
if isinstance(data, dict):
df = pd.DataFrame([data]) # 包装成单行 DF 或简单处理
else:
df = data
self.storage.save_data(df, 'JP', 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
table_info = tables[0]
table_data = table_info.get("table", {})
times = table_info.get("time", [])
if not table_data:
return pd.DataFrame()
# Ensure all values are lists to avoid pd.DataFrame ValueError with scalars
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 still no end_date, look for it in columns
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])
# Handle YoY logic: YYYYMMDD -> (YYYY-1)MMDD
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()
# 对齐 CN 逻辑,日本公司虽然多是 0331 截止
is_annual = df['end_date'].astype(str).str.endswith('0331') | 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:
"""通用获取历年会计年结日的财务数据 (CNY 结算)"""
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"))
all_dfs = []
# 获取最近 5 年的数据,精准定位会计年结日
for i in range(5):
target_year = current_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
]
}
# print(f"iFinD API Request: endpoint=basic_data_service, params={params}")
res = self.cli.post("basic_data_service", params)
df = self._parse_ifind_tables(res)
if not df.empty:
# 强制设置 end_date 以防 API 返回不一致
df['end_date'] = target_date
all_dfs.append(df)
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:
"""获取历史日期的收盘价和市值 (通过 cmd_history_quotation)"""
code = self._get_ifind_code(symbol)
if not dates: return pd.DataFrame()
results = []
# get_historical_metrics里面不要拿所有日期数据了而是一个一个数据拿
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"]}
]
}
# print(f"iFinD API Request: endpoint=date_sequence, params={params}")
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 = []
# 获取最近 5 年的数据
for i in range(5):
year_str = str(current_year - i)
params = {
"codes": code,
"indipara": [
{"indicator": "annual_cum_dividend", "indiparams": [year_str, "CNY"]}
]
}
# print(f"iFinD API Request: endpoint=basic_data_service, params={params}")
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:
"""获取历年年度回购记录 (从 repur_num_new 获取)"""
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:]
# 为了对应日期格式 YYYY-MM-DD
fmt_mm_dd = f"{mm}-{dd}"
current_year = int(time.strftime("%Y"))
results = []
# 获取最近 5 年的数据
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"]}
]
}
# print(f"iFinD API Request: endpoint=basic_data_service, params={params}")
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 = []
# 获取最近 5 年的数据
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

View File

@ -0,0 +1,63 @@
import os
import sys
import pandas as pd
from dotenv import load_dotenv
# Add src to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from fetchers.jp_fetcher import JpFetcher
def test_jp_fetcher():
load_dotenv()
token = os.getenv("IFIND_REFRESH_TOKEN")
if not token:
print("Missing IFIND_REFRESH_TOKEN in .env")
return
fetcher = JpFetcher(token)
symbol = "7203" # Toyota
print(f"--- Fetching Market Metrics for {symbol} ---")
metrics = fetcher.get_market_metrics(symbol)
print(metrics)
print(f"\n--- Fetching Income Statement for {symbol} ---")
df_is = fetcher.get_income_statement(symbol)
if not df_is.empty:
print(df_is.head())
else:
print("Income Statement is empty!")
print(f"\n--- Fetching Balance Sheet for {symbol} ---")
df_bs = fetcher.get_balance_sheet(symbol)
if not df_bs.empty:
print(df_bs.head())
else:
print("Balance Sheet is empty!")
print(f"\n--- Fetching Cash Flow for {symbol} ---")
df_cf = fetcher.get_cash_flow(symbol)
if not df_cf.empty:
print(df_cf.head())
else:
print("Cash Flow is empty!")
print(f"\n--- Fetching Historical Metrics for {symbol} ---")
dates = ["2023-12-21", "2024-06-30"]
df_hist = fetcher.get_historical_metrics(symbol, dates)
if not df_hist.empty:
print(df_hist)
else:
print("Historical Metrics is empty!")
print(f"\n--- Fetching Dividends for {symbol} ---")
df_div = fetcher.get_dividends(symbol)
if not df_div.empty:
print(df_div.head())
else:
print("Dividends empty or not found.")
if __name__ == "__main__":
test_jp_fetcher()

View File

@ -27,8 +27,6 @@ class HK_ReportGenerator(BaseReporter):
('goodwill', '商誉(亿)', 'currency_yi') ('goodwill', '商誉(亿)', 'currency_yi')
], ],
"费用指标": [ "费用指标": [
('SellingRatio', '销售费用率', 'percent'),
('AdminRatio', '管理费用率', 'percent'),
('SgaRatio', 'SG&A比例', 'percent'), ('SgaRatio', 'SG&A比例', 'percent'),
('RDRatio', '研发费用率', 'percent'), ('RDRatio', '研发费用率', 'percent'),
('OtherExpenseRatio', '其他费用率', 'percent'), ('OtherExpenseRatio', '其他费用率', 'percent'),
@ -57,50 +55,23 @@ class HK_ReportGenerator(BaseReporter):
('PayablesDays', '应付款周转天数', 'int'), ('PayablesDays', '应付款周转天数', 'int'),
('FixedAssetsTurnover', '固定资产周转率', 'float'), ('FixedAssetsTurnover', '固定资产周转率', 'float'),
('TotalAssetTurnover', '总资产周转率', 'float'), ('TotalAssetTurnover', '总资产周转率', 'float'),
],
"人均效率": [
('Employees', '员工人数', 'int'),
('RevenuePerEmp', '人均创收(万)', 'currency_wan'),
('ProfitPerEmp', '人均创利(万)', 'currency_wan'),
('AvgWage', '人均薪酬(万)', 'currency_wan'),
],
"市场表现": [
('Price', '股价', 'float'),
('MarketCap', '市值(亿)', '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_annual = dates.dt.month == 12
is_latest_year = dates.dt.year == latest_year
df = df[is_annual | is_latest_year]
return df
def _generate_md_company_info(self, symbol, metrics, market): def _generate_md_company_info(self, symbol, metrics, market):
today_str = datetime.date.today().strftime("%Y-%m-%d") today_str = datetime.date.today().strftime("%Y-%m-%d")
name = metrics.get('name', '') name = metrics.get('name', '')
raw_list_date = metrics.get('list_date', '') fiscal_year_end = metrics.get('fiscal_year_end', '')
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) pe = metrics.get('pe', 0)
pb = metrics.get('pb', 0) pb = metrics.get('pb', 0)
div = metrics.get('dividend_yield', 0) div_yield = metrics.get('dividend_yield', 0) * 100
md = [] md = []
md.append(f"# {name} ({symbol}) - Financial Report") md.append(f"# {name} ({symbol}) - Financial Report")
md.append(f"*Report generated on: {today_str}*\n") md.append(f"*Report generated on: {today_str}*\n")
md.append("| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |") md.append("| 代码 | 简称 | 财报日期 | PE | PB | 股息率(%) |")
md.append("|:---|:---|:---|:---|:---|:---|") md.append("|:---|:---|:---|:---|:---|:---|")
md.append(f"| {symbol} | {name} | {list_date} | {pe:.2f} | {pb:.2f} | {div:.2f}% |") md.append(f"| {symbol} | {name} | {fiscal_year_end} | {pe:.2f} | {pb:.2f} | {div_yield:.2f}% |")
return "\n".join(md) return "\n".join(md)
def generate_report(self, df_analysis, symbol, market, metrics, output_dir): def generate_report(self, df_analysis, symbol, market, metrics, output_dir):
@ -128,14 +99,10 @@ class HK_ReportGenerator(BaseReporter):
def _build_html_content(self, symbol, metrics, headers, df): def _build_html_content(self, symbol, metrics, headers, df):
today_str = datetime.date.today().strftime("%Y-%m-%d") today_str = datetime.date.today().strftime("%Y-%m-%d")
name = metrics.get('name') or symbol name = metrics.get('name') or symbol
raw_list_date = metrics.get('list_date', '') fiscal_year_end = metrics.get('fiscal_year_end') or "-"
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 pe = metrics.get('pe', 0) or 0
pb = metrics.get('pb', 0) or 0 pb = metrics.get('pb', 0) or 0
div = metrics.get('dividend_yield', 0) or 0 div_yield = (metrics.get('dividend_yield', 0) or 0) * 100
company_table = f""" company_table = f"""
<table class="company-table"> <table class="company-table">
@ -143,7 +110,7 @@ class HK_ReportGenerator(BaseReporter):
<tr> <tr>
<th>代码</th> <th>代码</th>
<th>简称</th> <th>简称</th>
<th>上市日期</th> <th>财报日期</th>
<th>PE</th> <th>PE</th>
<th>PB</th> <th>PB</th>
<th>股息率(%)</th> <th>股息率(%)</th>
@ -153,10 +120,10 @@ class HK_ReportGenerator(BaseReporter):
<tr> <tr>
<td>{symbol}</td> <td>{symbol}</td>
<td>{name}</td> <td>{name}</td>
<td>{list_date}</td> <td>{fiscal_year_end}</td>
<td>{pe:.2f}</td> <td>{pe:.2f}</td>
<td>{pb:.2f}</td> <td>{pb:.2f}</td>
<td>{div:.2f}%</td> <td>{div_yield:.2f}%</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,588 @@
from .base_generator import BaseReporter
import pandas as pd
import datetime
import os
import markdown
class JP_ReportGenerator(BaseReporter):
def __init__(self):
super().__init__()
# Use the same indicators and labels as CN
self.indicators = {
"主要指标": [
('ROE', 'ROE', 'percent'),
('ROA', 'ROA', 'percent'),
('ROIC', 'ROCE/ROIC', 'percent'),
('GrossMargin', '毛利率', 'percent'),
('NetMargin', '净利润率', 'percent'),
('revenue', '收入(亿)', 'currency_yi'),
('RevenueGrowth', '收入增速', 'percent_color'),
('net_income', '净利润(亿)', 'currency_yi'),
('NetIncomeGrowth', '净利润增速', 'percent_color'),
('ocf', '经营净现金流(亿)', 'currency_yi_color'),
('Capex', '资本开支(亿)', 'currency_yi'),
('FCF', '自由现金流(亿)', 'currency_yi_compare'),
('dividends', '分红(亿)', 'currency_yi'),
('repurchases', '回购(亿)', 'currency_yi'),
('total_assets', '总资产(亿)', 'currency_yi'),
('total_equity', '净资产(亿)', 'currency_yi'),
('goodwill', '商誉(亿)', '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', '人均创收(万)', 'currency_wan'),
('ProfitPerEmp', '人均创利(万)', 'currency_wan'),
('AvgWage', '人均薪酬(万)', 'currency_wan'),
],
"市场表现": [
('Price', '股价', 'float'),
('MarketCap', '市值(亿)', '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:
# For JP, usually ends in March (0331) or Dec (1231)
dates = pd.to_datetime(df['date_str'], format='%Y%m%d')
latest_year = dates.dt.year.max()
# We keep annual reports (typically 0331 or 1231) and the absolute latest record
is_march = dates.dt.month == 3
is_dec = dates.dt.month == 12
is_latest = df.index == df.index[0] # Assumes sorted descending
df = df[is_march | 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]
month = date_str[4:6]
day = date_str[6:]
try:
month_int = int(month)
day_int = int(day)
except ValueError:
return f"{year}A"
# For Japanese annual reports, show only year + 'A'
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)
# Re-use the exact same styled HTML from CN_ReportGenerator
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):
# Implementation identical to CN_ReportGenerator for style consistency
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):
# This is a literal copy of the CSS/JS from CN_ReportGenerator to ensure identical style
styled_html = '''
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{symbol} Financial Report</title>
<style>
:root {
--bg: #f5f6fa;
--card-bg: #ffffff;
--header-bg: #f7f8fb;
--section-bg: #f0f2f5;
--border: #e5e7eb;
--text-primary: #111827;
--text-secondary: #6b7280;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 32px;
background: var(--bg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--text-primary);
line-height: 1.6;
}
.report-container {
max-width: 1280px;
margin: 0 auto;
background: var(--card-bg);
border-radius: 24px;
padding: 32px 40px;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
}
h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 600;
color: var(--text-primary);
}
p {
margin: 0 0 24px;
color: var(--text-secondary);
font-size: 0.95rem;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
}
th,
td {
font-size: 0.95rem;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
th {
font-weight: 600;
color: var(--text-secondary);
text-align: right;
background: var(--header-bg);
}
th:first-child,
td:first-child {
text-align: left;
}
.company-table th,
.company-table td {
text-align: left;
}
.metrics-table thead {
position: sticky;
top: 0;
z-index: 3;
}
.metrics-table thead th {
position: sticky;
top: 0;
z-index: 3;
background: var(--card-bg);
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
}
.metrics-table thead th:first-child {
left: 0;
z-index: 4;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.08);
}
.metrics-table th:first-child,
.metrics-table td:first-child {
width: 180px;
min-width: 180px;
}
.metrics-table tbody td:first-child {
position: sticky;
left: 0;
background: var(--card-bg);
font-weight: 600;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.04);
z-index: 2;
text-align: left;
}
.metrics-table tbody td:not(:first-child) {
text-align: right;
}
.metrics-table tr.other-assets-row td {
background: #fff7e0;
}
.metrics-table tr.other-assets-row td:first-child {
background: #fff7e0;
}
.metrics-table tbody tr:hover td {
background: #f4efff;
}
.section-row td {
background: #eef1f6;
font-weight: 600;
text-align: left;
border-bottom: 1px solid var(--border);
}
.metrics-table .section-row td:first-child {
position: sticky;
left: 0;
z-index: 2;
box-shadow: 16px 0 24px rgba(15, 23, 42, 0.08);
background: #eef1f6 !important;
}
.metrics-table .section-label {
color: var(--text-primary);
background: #eef1f6 !important;
}
.section-spacer {
background: #eef1f6;
}
.metric-name {
color: var(--text-secondary);
}
.table-container {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 16px;
margin-bottom: 24px;
}
.table-container table {
margin-bottom: 0;
min-width: 960px;
}
.table-gap {
height: 24px;
}
.no-data {
margin-top: 24px;
padding: 32px;
text-align: center;
border: 1px dashed var(--border);
border-radius: 16px;
color: var(--text-secondary);
font-size: 0.95rem;
}
.bg-green { background-color: #e6f7eb !important; }
.bg-red { background-color: #ffeef0 !important; }
.font-red { color: #d32f2f !important; }
.font-green { color: #1b873f !important; }
.font-blue { color: #2563eb !important; }
.italic { font-style: italic !important; }
@media (max-width: 768px) {
body { padding: 16px; }
.report-container { padding: 24px; }
table { font-size: 0.85rem; }
th,
td { padding: 10px 12px; }
}
</style>
</head>
<body>
<div class="report-container">
{html_content}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const scrollableTables = document.querySelectorAll('table[data-scrollable="true"]');
scrollableTables.forEach(table => {
const container = document.createElement('div');
container.className = 'table-container';
table.parentNode.insertBefore(container, table);
container.appendChild(table);
});
const parseValue = (text) => {
if (!text || text.trim() === '-') return null;
return parseFloat(text.replace(/%|,/g, ''));
};
const highlightIfOverThirtyPercent = (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 30) {
cell.classList.add('bg-red', 'font-red');
}
};
const styleRules = {
'ROE': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 15) cell.classList.add('bg-green');
},
'ROA': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 10) cell.classList.add('bg-green');
},
'毛利率': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 50) cell.classList.add('bg-green');
},
'净利润率': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 20) {
cell.classList.add('bg-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
}
}
},
'收入增速': (cell) => {
cell.classList.add('italic');
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 15) {
cell.classList.add('bg-green', 'font-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
} else {
cell.classList.add('font-blue');
}
}
},
'净利润增速': (cell) => {
cell.classList.add('italic');
const value = parseValue(cell.textContent);
if (value !== null) {
if (value > 15) {
cell.classList.add('bg-green', 'font-green');
} else if (value < 0) {
cell.classList.add('bg-red', 'font-red');
} else {
cell.classList.add('font-blue');
}
}
},
'经营净现金流(亿)': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value < 0) cell.classList.add('bg-red', 'font-red');
},
'应收款周转天数': (cell) => {
const value = parseValue(cell.textContent);
if (value !== null && value > 90) {
cell.classList.add('bg-red', 'font-red');
}
},
'现金占比': highlightIfOverThirtyPercent,
'库存占比': highlightIfOverThirtyPercent,
'应收款占比': highlightIfOverThirtyPercent,
'预付款占比': highlightIfOverThirtyPercent,
'固定资产占比': highlightIfOverThirtyPercent,
'长期投资占比': highlightIfOverThirtyPercent,
'商誉占比': highlightIfOverThirtyPercent,
'其他资产占比': highlightIfOverThirtyPercent
};
const metricsTables = document.querySelectorAll('table[data-table="metrics"]');
metricsTables.forEach(table => {
let netProfitValues = [];
let fcfRow = null;
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.classList.contains('section-row')) return;
const metricCell = row.querySelector('td:first-child');
if (!metricCell) return;
const metricName = metricCell.textContent.trim();
if (metricName === '净利润(亿)') {
row.querySelectorAll('td:not(:first-child)').forEach(cell => {
netProfitValues.push(parseValue(cell.textContent));
});
} else if (metricName === '自由现金流(亿)') {
fcfRow = row;
}
});
rows.forEach(row => {
if (row.classList.contains('section-row')) return;
const metricCell = row.querySelector('td:first-child');
if (!metricCell) return;
const metricName = metricCell.textContent.trim();
const cells = row.querySelectorAll('td:not(:first-child)');
if (styleRules[metricName]) {
cells.forEach(cell => {
styleRules[metricName](cell);
});
}
if (row === fcfRow && netProfitValues.length > 0) {
cells.forEach((cell, index) => {
const fcfValue = parseValue(cell.textContent);
const netProfitValue = netProfitValues[index];
if (fcfValue !== null) {
if (fcfValue < 0) {
cell.classList.add('bg-red', 'font-red');
} else if (netProfitValue !== null && fcfValue > netProfitValue) {
cell.classList.add('bg-green', 'font-green');
}
}
});
}
});
});
});
</script>
</body>
</html>
'''
final_html = styled_html.replace('{symbol}', symbol).replace('{html_content}', html_content)
return final_html

Binary file not shown.

Binary file not shown.

View File

@ -6,10 +6,10 @@ from storage.file_io import DataStorage
import os import os
class HK_Strategy(BaseStrategy): class HK_Strategy(BaseStrategy):
def __init__(self, stock_code, tushare_token): def __init__(self, stock_code, av_key):
super().__init__(stock_code) super().__init__(stock_code)
self.tushare_token = tushare_token self.av_key = av_key
self.fetcher = FetcherFactory.get_fetcher('HK', self.tushare_token) self.fetcher = FetcherFactory.get_fetcher('HK', av_key=self.av_key)
self.analyzer = HK_Analyzer() self.analyzer = HK_Analyzer()
self.reporter = HK_ReportGenerator() self.reporter = HK_ReportGenerator()
self.storage = DataStorage() self.storage = DataStorage()

View File

@ -0,0 +1,79 @@
from .base_strategy import BaseStrategy
from fetchers.factory import FetcherFactory
from analysis.jp_analyzer import JP_Analyzer
from reporting.jp_report_generator import JP_ReportGenerator
from storage.file_io import DataStorage
import os
class JP_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('JP', ifind_refresh_token=self.refresh_token)
self.analyzer = JP_Analyzer()
self.reporter = JP_ReportGenerator()
self.storage = DataStorage()
self.raw_data = {}
self.analysis_result = None
def fetch_data(self):
print(f"Fetching data for JP 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 (BaseAnalyzer expects 'date' to create 'date_str')
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)
# We'll use some representative dates or the last 3 years of month-ends if needed.
# For consistency with CN, let's grab the dates from the income statement or simple defaults.
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 JP 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 JP market, stock: {self.stock_code}")
if self.analysis_result is not None and not self.analysis_result.empty:
output_dir = os.path.join("data", 'JP', self.stock_code)
self.reporter.generate_report(
df_analysis=self.analysis_result,
symbol=self.stock_code,
market='JP',
metrics=self.raw_data['metrics'],
output_dir=output_dir
)
else:
print("No analysis result to generate report.")
import pandas as pd # Import needed for the placeholder DataFrames