用ifind HTTP API实现了日股的数据获取
This commit is contained in:
parent
97b0f83d5c
commit
943dda784f
2
.env
2
.env
@ -1,2 +1,4 @@
|
||||
TUSHARE_TOKEN=f62b415de0a5a947fcb693b66cd299dd6242868bf04ad687800c7f3f
|
||||
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
25
DOC/API手册/API接口
Normal 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":[]}]}
|
||||
|
||||
BIN
DOC/API手册/iFinD HTTP API 用户手册.pdf
Normal file
BIN
DOC/API手册/iFinD HTTP API 用户手册.pdf
Normal file
Binary file not shown.
7200
DOC/API手册/ifind_manual.txt
Normal file
7200
DOC/API手册/ifind_manual.txt
Normal file
File diff suppressed because it is too large
Load Diff
131
JP-data.md
Normal file
131
JP-data.md
Normal 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` (商誉及无形资产) 被用作“商誉”的近似指标。
|
||||
6
data/JP/7203.T/raw_balance_sheet_raw.csv
Normal file
6
data/JP/7203.T/raw_balance_sheet_raw.csv
Normal 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
|
||||
|
2
data/JP/7203.T/raw_basic_info_raw.csv
Normal file
2
data/JP/7203.T/raw_basic_info_raw.csv
Normal file
@ -0,0 +1,2 @@
|
||||
corp_cn_name,accounting_date,ipo_date
|
||||
丰田汽车公司,0331,19490516
|
||||
|
6
data/JP/7203.T/raw_cash_flow_raw.csv
Normal file
6
data/JP/7203.T/raw_cash_flow_raw.csv
Normal 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
|
||||
|
6
data/JP/7203.T/raw_employee_count_raw.csv
Normal file
6
data/JP/7203.T/raw_employee_count_raw.csv
Normal file
@ -0,0 +1,6 @@
|
||||
date_str,employee_count
|
||||
20251231,383853.0
|
||||
20241231,380793.0
|
||||
20231231,375235.0
|
||||
20221231,372817.0
|
||||
20211231,366283.0
|
||||
|
6
data/JP/7203.T/raw_historical_metrics_raw.csv
Normal file
6
data/JP/7203.T/raw_historical_metrics_raw.csv
Normal 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
|
||||
|
6
data/JP/7203.T/raw_income_statement_raw.csv
Normal file
6
data/JP/7203.T/raw_income_statement_raw.csv
Normal 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
|
||||
|
2
data/JP/7203.T/raw_market_metrics_raw.csv
Normal file
2
data/JP/7203.T/raw_market_metrics_raw.csv
Normal file
@ -0,0 +1,2 @@
|
||||
pe_ttm,pb
|
||||
,
|
||||
|
445
data/JP/7203.T/report.html
Normal file
445
data/JP/7203.T/report.html
Normal 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
89
data/JP/7203.T/report.md
Normal 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
29
main.py
@ -4,17 +4,26 @@ from dotenv import load_dotenv
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from strategies.cn_strategy import CN_Strategy
|
||||
from strategies.us_strategy import US_Strategy
|
||||
from strategies.hk_strategy import HK_Strategy
|
||||
# from strategies.cn_strategy import CN_Strategy
|
||||
# from strategies.us_strategy import US_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):
|
||||
if market.upper() == 'CN':
|
||||
market = market.upper()
|
||||
if market == 'CN':
|
||||
from strategies.cn_strategy import CN_Strategy
|
||||
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)
|
||||
elif market.upper() == 'HK':
|
||||
return HK_Strategy(stock_code, tushare_token)
|
||||
elif market == 'HK':
|
||||
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:
|
||||
raise ValueError(f"Unsupported market: {market}")
|
||||
|
||||
@ -41,8 +50,12 @@ def main():
|
||||
us_strategy.execute()
|
||||
|
||||
# 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()
|
||||
|
||||
# Test JP
|
||||
jp_strategy = get_strategy('JP', '7203') # Toyota
|
||||
jp_strategy.execute()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -4,3 +4,4 @@ tushare
|
||||
alpha_vantage
|
||||
python-dotenv
|
||||
markdown
|
||||
jquants-api-client
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/analysis/__pycache__/jp_analyzer.cpython-312.pyc
Normal file
BIN
src/analysis/__pycache__/jp_analyzer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/analysis/__pycache__/jp_analyzer.cpython-313.pyc
Normal file
BIN
src/analysis/__pycache__/jp_analyzer.cpython-313.pyc
Normal file
Binary file not shown.
@ -6,7 +6,7 @@ class BaseAnalyzer:
|
||||
self.market = market
|
||||
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_bal = self._map_columns(df_bal, 'balance')
|
||||
df_cf = self._map_columns(df_cf, 'cashflow')
|
||||
@ -19,17 +19,20 @@ class BaseAnalyzer:
|
||||
return pd.DataFrame()
|
||||
|
||||
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'))
|
||||
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'))
|
||||
|
||||
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'))
|
||||
|
||||
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'))
|
||||
|
||||
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:
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
@ -7,43 +7,23 @@ class HK_Analyzer(BaseAnalyzer):
|
||||
super().__init__('HK')
|
||||
self.mapping = {
|
||||
'income': {
|
||||
'revenue': 'revenue',
|
||||
'net_profit_attr_p': 'net_income',
|
||||
# Assuming other fields are similar to CN, will need verification
|
||||
'total_cogs': 'total_costs',
|
||||
'sell_exp': 'selling_exp',
|
||||
'admin_exp': 'admin_exp',
|
||||
'fin_exp': 'fin_exp',
|
||||
'rd_exp': 'rd_exp',
|
||||
'total_profit': 'total_profit',
|
||||
'income_tax': 'income_tax'
|
||||
'totalRevenue': 'revenue', 'costOfRevenue': 'cogs', 'grossProfit': 'gross_profit',
|
||||
'sellingGeneralAndAdministrative': 'sga_exp',
|
||||
'researchAndDevelopment': 'rd_exp', 'interestExpense': 'fin_exp',
|
||||
'incomeBeforeTax': 'total_profit', 'incomeTaxExpense': 'income_tax', 'netIncome': 'net_income',
|
||||
'ebit': 'ebit',
|
||||
},
|
||||
'balance': {
|
||||
'money_cap': 'cash',
|
||||
'accounts_receiv': 'receivables',
|
||||
'inventories': 'inventory',
|
||||
'fix_assets': 'fixed_assets',
|
||||
'total_assets': 'total_assets',
|
||||
'goodwill': 'goodwill',
|
||||
'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',
|
||||
'cashAndCashEquivalentsAtCarryingValue': 'cash', 'currentNetReceivables': 'receivables', 'inventory': 'inventory',
|
||||
'propertyPlantEquipment': 'fixed_assets', 'totalAssets': 'total_assets', 'goodwill': 'goodwill',
|
||||
'otherCurrentAssets': 'prepayment', 'longTermInvestments': 'lt_invest', 'otherNonCurrentAssets': 'other_assets',
|
||||
'shortTermDebt': 'short_term_debt', 'currentLongTermDebt': 'short_term_debt_part',
|
||||
'longTermDebt': 'long_term_debt', 'totalLiabilities': 'total_liabilities', 'totalShareholderEquity': 'total_equity',
|
||||
'currentAccountsPayable': 'accounts_payable', 'deferredRevenue': 'adv_receipts',
|
||||
},
|
||||
'cashflow': {
|
||||
'n_cashflow_act': 'ocf',
|
||||
'c_pay_acq_const_fiolta': 'capex',
|
||||
'c_paid_div_prof_int': 'dividends',
|
||||
'c_paid_to_for_empl': 'cash_paid_for_employees'
|
||||
'operatingCashflow': 'ocf', 'capitalExpenditures': 'capex', 'dividendPayout': 'dividends',
|
||||
'depreciationDepletionAndAmortization': 'depreciation'
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,10 +33,6 @@ class HK_Analyzer(BaseAnalyzer):
|
||||
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)
|
||||
|
||||
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 'gross_profit' not in df.columns and 'revenue' in df.columns and 'cogs' in df.columns:
|
||||
df['gross_profit'] = df['revenue'] - df['cogs']
|
||||
@ -118,22 +94,20 @@ class HK_Analyzer(BaseAnalyzer):
|
||||
df_merged['FCF'] = df_merged['ocf'] - df_merged['Capex']
|
||||
|
||||
# Expenses
|
||||
if 'selling_exp' in df_merged.columns:
|
||||
df_merged['SellingRatio'] = self._safe_div(df_merged['selling_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 'sga_exp' in df_merged.columns:
|
||||
df_merged['SgaRatio'] = self._safe_div(df_merged['sga_exp'], df_merged['revenue'])
|
||||
if 'rd_exp' in df_merged.columns:
|
||||
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:
|
||||
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:
|
||||
other_ratio = df_merged['GrossMargin'] - df_merged['NetMargin']
|
||||
if 'SellingRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['SellingRatio'].fillna(0)
|
||||
if 'AdminRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['AdminRatio'].fillna(0)
|
||||
if 'RDRatio' in df_merged.columns: other_ratio = other_ratio - df_merged['RDRatio'].fillna(0)
|
||||
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
|
||||
|
||||
# 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 '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') +
|
||||
series_or_zero('PrepaymentRatio') + series_or_zero('FixedAssetsRatio') + series_or_zero('LongTermInvestmentRatio') +
|
||||
series_or_zero('GoodwillRatio'))
|
||||
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)
|
||||
|
||||
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['InterestBearingDebtRatio'] = self._safe_div(st_debt + lt_debt, assets)
|
||||
|
||||
# Operating Assets Ratio
|
||||
inv_ratio = series_or_zero('InventoryRatio')
|
||||
rec_ratio = series_or_zero('ReceivablesRatio')
|
||||
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], 'PE'] = market_metrics.get('pe')
|
||||
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
|
||||
|
||||
170
src/analysis/jp_analyzer.py
Normal file
170
src/analysis/jp_analyzer.py
Normal 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.
BIN
src/fetchers/__pycache__/ifind_client.cpython-312.pyc
Normal file
BIN
src/fetchers/__pycache__/ifind_client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/fetchers/__pycache__/ifind_client.cpython-313.pyc
Normal file
BIN
src/fetchers/__pycache__/ifind_client.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/fetchers/__pycache__/jp_fetcher.cpython-312.pyc
Normal file
BIN
src/fetchers/__pycache__/jp_fetcher.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/fetchers/__pycache__/jp_fetcher.cpython-313.pyc
Normal file
BIN
src/fetchers/__pycache__/jp_fetcher.cpython-313.pyc
Normal file
Binary file not shown.
2
src/fetchers/data/JP/300033.SZ/raw_balance_sheet_raw.csv
Normal file
2
src/fetchers/data/JP/300033.SZ/raw_balance_sheet_raw.csv
Normal 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
|
||||
|
2
src/fetchers/data/JP/300033.SZ/raw_cash_flow_raw.csv
Normal file
2
src/fetchers/data/JP/300033.SZ/raw_cash_flow_raw.csv
Normal 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
|
||||
|
@ -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
|
||||
|
2
src/fetchers/data/JP/7203/raw_balance_sheet_raw.csv
Normal file
2
src/fetchers/data/JP/7203/raw_balance_sheet_raw.csv
Normal 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
|
||||
|
2
src/fetchers/data/JP/7203/raw_cash_flow_raw.csv
Normal file
2
src/fetchers/data/JP/7203/raw_cash_flow_raw.csv
Normal 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
|
||||
|
2
src/fetchers/data/JP/7203/raw_income_statement_raw.csv
Normal file
2
src/fetchers/data/JP/7203/raw_income_statement_raw.csv
Normal 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
|
||||
|
84
src/fetchers/debug_ifind.py
Normal file
84
src/fetchers/debug_ifind.py
Normal 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()
|
||||
@ -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:
|
||||
@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()
|
||||
if market == 'CN':
|
||||
if not tushare_token:
|
||||
raise ValueError("Tushare token is required for CN market")
|
||||
from .cn_fetcher import CnFetcher
|
||||
return CnFetcher(tushare_token)
|
||||
if market == 'HK':
|
||||
if not tushare_token:
|
||||
raise ValueError("Tushare token is required for HK market")
|
||||
return HkFetcher(tushare_token)
|
||||
if not av_key:
|
||||
raise ValueError("Alpha Vantage key is required for HK market")
|
||||
from .hk_fetcher import HkFetcher
|
||||
return HkFetcher(av_key)
|
||||
elif market == 'US':
|
||||
if not av_key:
|
||||
raise ValueError("Alpha Vantage key is required for US market")
|
||||
from .us_fetcher import UsFetcher
|
||||
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:
|
||||
raise ValueError(f"Unsupported market: {market}")
|
||||
|
||||
@ -1,172 +1,212 @@
|
||||
import tushare as ts
|
||||
import requests
|
||||
import pandas as pd
|
||||
from .base import DataFetcher
|
||||
import time
|
||||
from .base import DataFetcher
|
||||
from storage.file_io import DataStorage
|
||||
|
||||
class HkFetcher(DataFetcher):
|
||||
BASE_URL = "https://www.alphavantage.co/query"
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
super().__init__(api_key)
|
||||
ts.set_token(self.api_key)
|
||||
self.pro = ts.pro_api()
|
||||
self.storage = DataStorage()
|
||||
|
||||
def _save_raw_data(self, df: pd.DataFrame, symbol: str, name: str):
|
||||
if df is None or df.empty:
|
||||
return
|
||||
market = 'HK'
|
||||
self.storage.save_data(df, market, symbol, f"raw_{name}")
|
||||
|
||||
def _get_ts_code(self, symbol: str) -> str:
|
||||
def _sanitize_symbol(self, symbol: str) -> str:
|
||||
if '.HK' in symbol.upper():
|
||||
code = symbol.upper().replace('.HK', '')
|
||||
if code.isdigit():
|
||||
try:
|
||||
# e.g., '0700.HK' -> '700.HK', '0005.HK' -> '5.HK'
|
||||
return str(int(code)) + '.HK'
|
||||
except ValueError:
|
||||
# Keep original symbol if not a simple integer
|
||||
return symbol
|
||||
return symbol
|
||||
|
||||
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]]
|
||||
def _save_raw_data(self, data, symbol: str, name: str):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
df = pd.DataFrame()
|
||||
if isinstance(data, list):
|
||||
df = pd.DataFrame(data)
|
||||
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:
|
||||
latest_date_str = str(latest_record['end_date'].values[0])
|
||||
last_year_date_str = str(int(latest_date_str) - 10000)
|
||||
comparable_record = df[df['end_date'].astype(str) == last_year_date_str]
|
||||
except:
|
||||
comparable_record = pd.DataFrame()
|
||||
is_annual = df['end_date'].astype(str).str.endswith('1231')
|
||||
annual_records = df[is_annual]
|
||||
combined = pd.concat([latest_record, comparable_record, annual_records])
|
||||
combined = combined.drop_duplicates(subset=['end_date'])
|
||||
combined = combined.sort_values(by='end_date', ascending=False)
|
||||
return combined
|
||||
# Alpha Vantage free tier is limited to 25 requests per day.
|
||||
# A 15-second sleep is a precaution for not hitting minute-based rate limits if any.
|
||||
time.sleep(15)
|
||||
response = requests.get(self.BASE_URL, params=params)
|
||||
response.raise_for_status() # Raise an exception for bad status codes
|
||||
data = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error requesting {function} for {symbol}: {e}")
|
||||
return pd.DataFrame()
|
||||
except Exception as e:
|
||||
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:
|
||||
ts_code = self._get_ts_code(symbol)
|
||||
df = self.pro.hk_income(ts_code=ts_code)
|
||||
self._save_raw_data(df, ts_code, "income_statement_hk")
|
||||
rename_map = {
|
||||
'end_date': 'date',
|
||||
'revenue': 'revenue',
|
||||
'net_profit_attr_p': 'net_income'
|
||||
df = self._fetch_data("INCOME_STATEMENT", symbol)
|
||||
if df.empty:
|
||||
return df
|
||||
cols_map = {
|
||||
"fiscalDateEnding": "date",
|
||||
"totalRevenue": "revenue",
|
||||
"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=rename_map)
|
||||
df = df.rename(columns=cols_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
|
||||
|
||||
def get_balance_sheet(self, symbol: str) -> pd.DataFrame:
|
||||
ts_code = self._get_ts_code(symbol)
|
||||
df = self.pro.hk_balancesheet(ts_code=ts_code)
|
||||
self._save_raw_data(df, ts_code, "balance_sheet_hk")
|
||||
rename_map = {
|
||||
'end_date': 'date',
|
||||
'total_share_holder_equity': 'total_equity',
|
||||
'total_liab': 'total_liabilities',
|
||||
'total_cur_asset': 'current_assets',
|
||||
'total_cur_liab': 'current_liabilities'
|
||||
df = self._fetch_data("BALANCE_SHEET", symbol)
|
||||
if df.empty:
|
||||
return df
|
||||
cols_map = {
|
||||
"fiscalDateEnding": "date",
|
||||
"totalShareholderEquity": "total_equity",
|
||||
"totalLiabilities": "total_liabilities",
|
||||
"totalCurrentAssets": "current_assets",
|
||||
"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=rename_map)
|
||||
df = df.rename(columns=cols_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
|
||||
|
||||
def get_cash_flow(self, symbol: str) -> pd.DataFrame:
|
||||
ts_code = self._get_ts_code(symbol)
|
||||
df = self.pro.hk_cashflow(ts_code=ts_code)
|
||||
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
|
||||
|
||||
def get_market_metrics(self, symbol: str) -> dict:
|
||||
ts_code = self._get_ts_code(symbol)
|
||||
metrics = {
|
||||
"price": 0.0,
|
||||
"market_cap": 0.0,
|
||||
"pe": 0.0,
|
||||
"pb": 0.0,
|
||||
"total_share_holders": 0,
|
||||
"employee_count": 0
|
||||
df = self._fetch_data("CASH_FLOW", symbol)
|
||||
if df.empty:
|
||||
return df
|
||||
cols_map = {
|
||||
"fiscalDateEnding": "date",
|
||||
"operatingCashflow": "ocf",
|
||||
"capitalExpenditures": "capex",
|
||||
"dividendPayout": "dividends",
|
||||
"depreciationDepletionAndAmortization": "depreciation"
|
||||
}
|
||||
df = df.rename(columns=cols_map)
|
||||
|
||||
try:
|
||||
df_daily = self.pro.daily_basic(ts_code=ts_code, limit=1)
|
||||
self._save_raw_data(df_daily, ts_code, "market_metrics_daily_basic")
|
||||
if not df_daily.empty:
|
||||
row = df_daily.iloc[0]
|
||||
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)
|
||||
numeric_cols = ["ocf", "capex", "dividends", "depreciation"]
|
||||
for col in numeric_cols:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
return df
|
||||
|
||||
62
src/fetchers/ifind_client.py
Normal file
62
src/fetchers/ifind_client.py
Normal 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
|
||||
90
src/fetchers/ifind_test.py
Normal file
90
src/fetchers/ifind_test.py
Normal 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
479
src/fetchers/jp_fetcher.py
Normal 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
|
||||
63
src/fetchers/test_ifind_jp.py
Normal file
63
src/fetchers/test_ifind_jp.py
Normal 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()
|
||||
Binary file not shown.
BIN
src/reporting/__pycache__/jp_report_generator.cpython-312.pyc
Normal file
BIN
src/reporting/__pycache__/jp_report_generator.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/reporting/__pycache__/jp_report_generator.cpython-313.pyc
Normal file
BIN
src/reporting/__pycache__/jp_report_generator.cpython-313.pyc
Normal file
Binary file not shown.
@ -27,8 +27,6 @@ class HK_ReportGenerator(BaseReporter):
|
||||
('goodwill', '商誉(亿)', 'currency_yi')
|
||||
],
|
||||
"费用指标": [
|
||||
('SellingRatio', '销售费用率', 'percent'),
|
||||
('AdminRatio', '管理费用率', 'percent'),
|
||||
('SgaRatio', 'SG&A比例', 'percent'),
|
||||
('RDRatio', '研发费用率', 'percent'),
|
||||
('OtherExpenseRatio', '其他费用率', 'percent'),
|
||||
@ -57,50 +55,23 @@ class HK_ReportGenerator(BaseReporter):
|
||||
('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:
|
||||
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):
|
||||
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
|
||||
fiscal_year_end = metrics.get('fiscal_year_end', '')
|
||||
pe = metrics.get('pe', 0)
|
||||
pb = metrics.get('pb', 0)
|
||||
div = metrics.get('dividend_yield', 0)
|
||||
div_yield = metrics.get('dividend_yield', 0) * 100
|
||||
|
||||
md = []
|
||||
md.append(f"# {name} ({symbol}) - Financial Report")
|
||||
md.append(f"*Report generated on: {today_str}*\n")
|
||||
md.append("| 代码 | 简称 | 上市日期 | PE | PB | 股息率(%) |")
|
||||
md.append("| 代码 | 简称 | 财报日期 | PE | PB | 股息率(%) |")
|
||||
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)
|
||||
|
||||
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):
|
||||
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 "-"
|
||||
fiscal_year_end = metrics.get('fiscal_year_end') or "-"
|
||||
pe = metrics.get('pe', 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"""
|
||||
<table class="company-table">
|
||||
@ -143,7 +110,7 @@ class HK_ReportGenerator(BaseReporter):
|
||||
<tr>
|
||||
<th>代码</th>
|
||||
<th>简称</th>
|
||||
<th>上市日期</th>
|
||||
<th>财报日期</th>
|
||||
<th>PE</th>
|
||||
<th>PB</th>
|
||||
<th>股息率(%)</th>
|
||||
@ -153,10 +120,10 @@ class HK_ReportGenerator(BaseReporter):
|
||||
<tr>
|
||||
<td>{symbol}</td>
|
||||
<td>{name}</td>
|
||||
<td>{list_date}</td>
|
||||
<td>{fiscal_year_end}</td>
|
||||
<td>{pe:.2f}</td>
|
||||
<td>{pb:.2f}</td>
|
||||
<td>{div:.2f}%</td>
|
||||
<td>{div_yield:.2f}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
588
src/reporting/jp_report_generator.py
Normal file
588
src/reporting/jp_report_generator.py
Normal 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.
BIN
src/strategies/__pycache__/jp_strategy.cpython-312.pyc
Normal file
BIN
src/strategies/__pycache__/jp_strategy.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/strategies/__pycache__/jp_strategy.cpython-313.pyc
Normal file
BIN
src/strategies/__pycache__/jp_strategy.cpython-313.pyc
Normal file
Binary file not shown.
@ -6,10 +6,10 @@ from storage.file_io import DataStorage
|
||||
import os
|
||||
|
||||
class HK_Strategy(BaseStrategy):
|
||||
def __init__(self, stock_code, tushare_token):
|
||||
def __init__(self, stock_code, av_key):
|
||||
super().__init__(stock_code)
|
||||
self.tushare_token = tushare_token
|
||||
self.fetcher = FetcherFactory.get_fetcher('HK', self.tushare_token)
|
||||
self.av_key = av_key
|
||||
self.fetcher = FetcherFactory.get_fetcher('HK', av_key=self.av_key)
|
||||
self.analyzer = HK_Analyzer()
|
||||
self.reporter = HK_ReportGenerator()
|
||||
self.storage = DataStorage()
|
||||
|
||||
79
src/strategies/jp_strategy.py
Normal file
79
src/strategies/jp_strategy.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user