补充了一些需要计算的指标
This commit is contained in:
parent
282cec080b
commit
63916f9b24
@ -2535,3 +2535,93 @@ sqlalchemy.exc.ProgrammingError: (sqlalchemy.dialects.postgresql.asyncpg.Program
|
||||
2026-01-12 19:08:25,394 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 00631 HK Equity
|
||||
2026-01-12 19:08:25,394 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=81, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 19:08:26,834 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=81, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-12 20:35:50,685 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=正在初始化数据获取..., Progress=0%
|
||||
2026-01-12 20:35:53,030 - app.clients.bloomberg_client - INFO - Connecting to Jupyter at http://192.168.3.161:8888...
|
||||
2026-01-12 20:35:53,308 - app.clients.bloomberg_client - INFO - ✅ Authentication successful.
|
||||
2026-01-12 20:35:53,350 - app.clients.bloomberg_client - INFO - ✅ Found existing kernel: bc27f3b1-b028-434a-99fa-c1cad4495a87 (remote_env)
|
||||
2026-01-12 20:35:53,415 - app.clients.bloomberg_client - INFO - ✅ WebSocket connected.
|
||||
2026-01-12 20:35:53,415 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=数据源连接成功, Progress=10%
|
||||
2026-01-12 20:35:55,408 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=正在连接 Bloomberg 终端..., Progress=20%
|
||||
2026-01-12 20:35:55,854 - app.clients.bloomberg_client - INFO - 🚀 Starting fetch for: SAB VN Equity
|
||||
2026-01-12 20:35:55,854 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=Starting Bloomberg session..., Progress=20%
|
||||
2026-01-12 20:35:56,946 - app.clients.bloomberg_client - INFO - Using forced currency: CNY
|
||||
2026-01-12 20:35:56,947 - app.clients.bloomberg_client - INFO - Fetching Basic Data...
|
||||
2026-01-12 20:35:56,947 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=Fetching Company Basic Info..., Progress=27%
|
||||
2026-01-12 20:35:58,618 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:35:59", "currency": "CNY", "indicator": "company_name", "value": "SAIGON BEER ALCOHOL BEVERAGE", "value_date": "2026-01-12 20:35:59"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:35:59", "currency": "CNY", "indicator": "pe_ratio", "value": "14.256432059589235", "value_date": "2026-01-12 20:35:59"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:35:59", "currency": "CNY", "indicator": "pb_ratio", "value": "2.6564776404306514", "value_date": "2026-01-12 20:35:59"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:35:59", "currency": "CNY", "indicator": "Rev_Abroad", "value": "0.0", "value_date": "2026-01-12 20:35:59"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:35:59", "currency": "CNY", "indicator": "dividend_yield", "value": "10.638297872340425", "value_date": "2026-01-12 20:35:59"}, {"Company_code": "SAB VN Equity", "update_date": "2026-0
|
||||
2026-01-12 20:35:58,619 - app.clients.bloomberg_client - INFO - ✅ Parsed 7 items from remote.
|
||||
2026-01-12 20:35:58,619 - app.clients.bloomberg_client - INFO - DEBUG: basic_data before save: [{'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'company_name', 'value': 'SAIGON BEER ALCOHOL BEVERAGE', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'pe_ratio', 'value': '14.256432059589235', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'pb_ratio', 'value': '2.6564776404306514', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'Rev_Abroad', 'value': '0.0', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'dividend_yield', 'value': '10.638297872340425', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'IPO_date', 'value': '2008-01-28', 'value_date': '2026-01-12 20:35:59'}, {'Company_code': 'SAB VN Equity', 'update_date': '2026-01-12 20:35:59', 'currency': 'CNY', 'indicator': 'market_cap', 'value': '16002.6454', 'value_date': '2026-01-12 20:35:59'}]
|
||||
2026-01-12 20:36:02,334 - app.clients.bloomberg_client - INFO - ✅ Saved 7 records to database.
|
||||
2026-01-12 20:36:02,336 - app.clients.bloomberg_client - INFO - Fetching Currency Data...
|
||||
2026-01-12 20:36:02,336 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=正在获取货币指标 (CNY)..., Progress=41%
|
||||
2026-01-12 20:36:07,524 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Revenue", "value": "9048.6698", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Net_Income", "value": "1178.9778", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Cash_From_Operating", "value": "446.3911", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Capital_Expenditure", "value": "-46.4354", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Free_Cash_Flow", "value": "399.9557", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:03", "currency": "CNY", "indicator": "Dividends_Paid", "valu
|
||||
2026-01-12 20:36:07,524 - app.clients.bloomberg_client - INFO - ✅ Parsed 225 items from remote.
|
||||
2026-01-12 20:36:28,461 - app.clients.bloomberg_client - INFO - ✅ Saved 225 records to database.
|
||||
2026-01-12 20:36:28,463 - app.clients.bloomberg_client - INFO - Fetching Non-Currency Data...
|
||||
2026-01-12 20:36:28,463 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=正在获取非货币指标..., Progress=55%
|
||||
2026-01-12 20:36:30,451 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "ROE", "value": "35.535", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "ROA", "value": "21.0594", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "ROCE", "value": "49.2784", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "Gross_Margin", "value": "26.8065", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "EBITDA_margin", "value": "17.5493", "value_date": "2016-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:29", "currency": "CNY", "indicator": "Net_Profit_Margin", "value": "14.241", "value_date": "2016-12-31
|
||||
2026-01-12 20:36:30,453 - app.clients.bloomberg_client - INFO - ✅ Parsed 171 items from remote.
|
||||
2026-01-12 20:36:45,144 - app.clients.bloomberg_client - INFO - ✅ Saved 171 records to database.
|
||||
2026-01-12 20:36:45,146 - app.clients.bloomberg_client - INFO - Fetching Price Data (Aligned)...
|
||||
2026-01-12 20:36:45,146 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=正在获取价格指标..., Progress=69%
|
||||
2026-01-12 20:36:46,560 - app.clients.bloomberg_client - INFO - Found 9 revenue reporting dates. Fetching aligned price data...
|
||||
2026-01-12 20:36:59,803 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Last_Price", "value": "15.89628", "value_date": "2024-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Market_Cap", "value": "20387.9708", "value_date": "2024-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Dividend_Yield", "value": "9.9099", "value_date": "2024-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Last_Price", "value": "18.43404", "value_date": "2023-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Market_Cap", "value": "23642.7999", "value_date": "2023-12-31"}, {"Company_code": "SAB VN Equity", "update_date": "2026-01-12 20:36:46", "currency": "CNY", "indicator": "Dividend_Yield", "value": "1.9841", "
|
||||
2026-01-12 20:36:59,803 - app.clients.bloomberg_client - INFO - ✅ Parsed 27 items from remote.
|
||||
2026-01-12 20:37:03,093 - app.clients.bloomberg_client - INFO - ✅ Saved 27 records to database.
|
||||
2026-01-12 20:37:03,094 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=Finalizing data..., Progress=82%
|
||||
2026-01-12 20:37:04,674 - app.clients.bloomberg_client - INFO - ✅ Cleanup and View Refresh completed.
|
||||
2026-01-12 20:37:04,675 - app.clients.bloomberg_client - INFO - ✅ Completed processing for SAB VN Equity
|
||||
2026-01-12 20:37:04,675 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 20:37:05,750 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=82, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
2026-01-12 20:38:40,646 - app.main - INFO - 🔍 [搜索] 开始搜索股票: 青岛啤酒
|
||||
2026-01-12 20:38:40,897 - app.main - INFO - 🤖 [搜索-LLM] 调用 gemini-2.5-flash 进行股票搜索
|
||||
2026-01-12 20:38:40,906 - google_genai.models - INFO - AFC is enabled with max remote calls: 10.
|
||||
2026-01-12 20:38:47,130 - httpx - INFO - HTTP Request: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent "HTTP/1.1 200 OK"
|
||||
2026-01-12 20:38:47,151 - app.main - INFO - ✅ [搜索-LLM] 模型响应完成, 耗时: 6.25秒, Tokens: prompt=166, completion=133, total=299
|
||||
2026-01-12 20:38:47,152 - app.main - INFO - ✅ [搜索] 搜索完成, 找到 2 个结果, 总耗时: 6.51秒
|
||||
2026-01-12 20:38:53,790 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=正在初始化数据获取..., Progress=0%
|
||||
2026-01-12 20:38:54,547 - app.clients.bloomberg_client - INFO - Connecting to Jupyter at http://192.168.3.161:8888...
|
||||
2026-01-12 20:38:55,207 - app.clients.bloomberg_client - INFO - ✅ Authentication successful.
|
||||
2026-01-12 20:38:55,237 - app.clients.bloomberg_client - INFO - ✅ Found existing kernel: bc27f3b1-b028-434a-99fa-c1cad4495a87 (remote_env)
|
||||
2026-01-12 20:38:55,307 - app.clients.bloomberg_client - INFO - ✅ WebSocket connected.
|
||||
2026-01-12 20:38:55,307 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=数据源连接成功, Progress=10%
|
||||
2026-01-12 20:38:56,092 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=正在连接 Bloomberg 终端..., Progress=20%
|
||||
2026-01-12 20:38:57,039 - app.clients.bloomberg_client - INFO - 🚀 Starting fetch for: 600600 CH Equity
|
||||
2026-01-12 20:38:57,039 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Starting Bloomberg session..., Progress=20%
|
||||
2026-01-12 20:38:58,457 - app.clients.bloomberg_client - INFO - Using forced currency: CNY
|
||||
2026-01-12 20:38:58,457 - app.clients.bloomberg_client - INFO - Fetching Basic Data...
|
||||
2026-01-12 20:38:58,457 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Fetching Company Basic Info..., Progress=27%
|
||||
2026-01-12 20:39:00,295 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "company_name", "value": "TSINGTAO BREWERY CO LTD-A", "value_date": "2026-01-12 20:39:01"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "pe_ratio", "value": "18.590088160034433", "value_date": "2026-01-12 20:39:01"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "pb_ratio", "value": "2.746838902322582", "value_date": "2026-01-12 20:39:01"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "dividend_yield", "value": "3.487082022006206", "value_date": "2026-01-12 20:39:01"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "market_cap", "value": "74326.0163", "value_date": "2026-01-12 20:39:01"}]
|
||||
JSON_END
|
||||
|
||||
2026-01-12 20:39:00,296 - app.clients.bloomberg_client - INFO - ✅ Parsed 5 items from remote.
|
||||
2026-01-12 20:39:00,296 - app.clients.bloomberg_client - INFO - DEBUG: basic_data before save: [{'Company_code': '600600 CH Equity', 'update_date': '2026-01-12 20:39:01', 'currency': 'CNY', 'indicator': 'company_name', 'value': 'TSINGTAO BREWERY CO LTD-A', 'value_date': '2026-01-12 20:39:01'}, {'Company_code': '600600 CH Equity', 'update_date': '2026-01-12 20:39:01', 'currency': 'CNY', 'indicator': 'pe_ratio', 'value': '18.590088160034433', 'value_date': '2026-01-12 20:39:01'}, {'Company_code': '600600 CH Equity', 'update_date': '2026-01-12 20:39:01', 'currency': 'CNY', 'indicator': 'pb_ratio', 'value': '2.746838902322582', 'value_date': '2026-01-12 20:39:01'}, {'Company_code': '600600 CH Equity', 'update_date': '2026-01-12 20:39:01', 'currency': 'CNY', 'indicator': 'dividend_yield', 'value': '3.487082022006206', 'value_date': '2026-01-12 20:39:01'}, {'Company_code': '600600 CH Equity', 'update_date': '2026-01-12 20:39:01', 'currency': 'CNY', 'indicator': 'market_cap', 'value': '74326.0163', 'value_date': '2026-01-12 20:39:01'}]
|
||||
2026-01-12 20:39:00,799 - app.clients.bloomberg_client - INFO - ✅ Saved 5 records to database.
|
||||
2026-01-12 20:39:00,799 - app.clients.bloomberg_client - INFO - Fetching Currency Data...
|
||||
2026-01-12 20:39:00,800 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=正在获取货币指标 (CNY)..., Progress=41%
|
||||
2026-01-12 20:39:02,150 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "Revenue", "value": "26106.3437", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "Net_Income", "value": "1043.4864", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "Cash_From_Operating", "value": "3002.1384", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "Capital_Expenditure", "value": "-855.8721", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "Free_Cash_Flow", "value": "2146.2663", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:01", "currency": "CNY", "indicator": "
|
||||
2026-01-12 20:39:02,150 - app.clients.bloomberg_client - INFO - ✅ Parsed 225 items from remote.
|
||||
2026-01-12 20:39:23,000 - app.clients.bloomberg_client - INFO - ✅ Saved 225 records to database.
|
||||
2026-01-12 20:39:23,005 - app.clients.bloomberg_client - INFO - Fetching Non-Currency Data...
|
||||
2026-01-12 20:39:23,005 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=正在获取非货币指标..., Progress=55%
|
||||
2026-01-12 20:39:24,357 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "ROE", "value": "6.3682", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "ROA", "value": "3.5627", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "ROCE", "value": "7.3766", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "Gross_Margin", "value": "41.5266", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "EBITDA_margin", "value": "8.1977", "value_date": "2016-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:23", "currency": "CNY", "indicator": "Net_Profit_Margin", "value": "3.9971", "value_dat
|
||||
2026-01-12 20:39:24,358 - app.clients.bloomberg_client - INFO - ✅ Parsed 180 items from remote.
|
||||
2026-01-12 20:39:38,190 - app.clients.bloomberg_client - INFO - ✅ Saved 180 records to database.
|
||||
2026-01-12 20:39:38,191 - app.clients.bloomberg_client - INFO - Fetching Price Data (Aligned)...
|
||||
2026-01-12 20:39:38,192 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=正在获取价格指标..., Progress=69%
|
||||
2026-01-12 20:39:38,974 - app.clients.bloomberg_client - INFO - Found 9 revenue reporting dates. Fetching aligned price data...
|
||||
2026-01-12 20:39:49,468 - app.clients.bloomberg_client - INFO - REMOTE RAW OUTPUT: JSON_START
|
||||
[{"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Last_Price", "value": "80.92", "value_date": "2024-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Market_Cap", "value": "92347.3395", "value_date": "2024-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Dividend_Yield", "value": "2.4716", "value_date": "2024-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Last_Price", "value": "74.75", "value_date": "2023-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Market_Cap", "value": "84217.9701", "value_date": "2023-12-31"}, {"Company_code": "600600 CH Equity", "update_date": "2026-01-12 20:39:38", "currency": "CNY", "indicator": "Dividend_Yield", "value":
|
||||
2026-01-12 20:39:49,469 - app.clients.bloomberg_client - INFO - ✅ Parsed 27 items from remote.
|
||||
2026-01-12 20:39:52,545 - app.clients.bloomberg_client - INFO - ✅ Saved 27 records to database.
|
||||
2026-01-12 20:39:52,545 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Finalizing data..., Progress=82%
|
||||
2026-01-12 20:39:54,959 - app.clients.bloomberg_client - INFO - ✅ Cleanup and View Refresh completed.
|
||||
2026-01-12 20:39:54,960 - app.clients.bloomberg_client - INFO - ✅ Completed processing for 600600 CH Equity
|
||||
2026-01-12 20:39:54,960 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Bloomberg data sync complete, Progress=90%
|
||||
2026-01-12 20:39:56,366 - app.services.data_fetcher_service - INFO - 🔄 [进度更新] ID=83, Msg=Bloomberg 数据同步完成, Progress=100%
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Loader2, DollarSign, RefreshCw } from "lucide-react"
|
||||
import { Loader2, DollarSign, RefreshCw, ChevronRight, ChevronDown } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { getFinancialData } from "@/lib/api"
|
||||
import { formatNumber, formatLargeNumber, formatDate } from "@/lib/formatters"
|
||||
@ -294,6 +294,103 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
return true
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1.5 Enrich Data with Ratios (Expense / Revenue)
|
||||
// ---------------------------------------------------------------------------
|
||||
filteredRows.forEach(row => {
|
||||
const revenue = typeof row.revenue === 'string' ? parseFloat(row.revenue) : row.revenue
|
||||
|
||||
if (revenue && revenue !== 0) {
|
||||
// Helper to calculate and assign if value exists
|
||||
const calcRatio = (key: string, targetKey: string) => {
|
||||
const val = typeof row[key] === 'string' ? parseFloat(row[key]) : row[key]
|
||||
if (val !== null && val !== undefined && !isNaN(val)) {
|
||||
row[targetKey] = (val / revenue) * 100
|
||||
}
|
||||
}
|
||||
|
||||
calcRatio('selling&marketing', 'selling&marketing_to_revenue')
|
||||
calcRatio('general&admin', 'general&admin_to_revenue')
|
||||
calcRatio('sg&a', 'sg&a_to_revenue')
|
||||
calcRatio('r&d', 'r&d_to_revenue')
|
||||
calcRatio('depreciation', 'depreciation_to_revenue')
|
||||
// Asset Ratios (Denominator: Total Assets)
|
||||
const calcAssetRatio = (key: string, targetKey: string) => {
|
||||
const totalAssets = typeof row['total_assets'] === 'string' ? parseFloat(row['total_assets']) : row['total_assets']
|
||||
const val = typeof row[key] === 'string' ? parseFloat(row[key]) : row[key]
|
||||
if (totalAssets && totalAssets !== 0 && val !== null && val !== undefined && !isNaN(val)) {
|
||||
row[targetKey] = (val / totalAssets) * 100
|
||||
}
|
||||
}
|
||||
calcAssetRatio('cash', 'cash_to_assets')
|
||||
calcAssetRatio('inventory', 'inventory_to_assets')
|
||||
calcAssetRatio('accounts¬es_receivable', 'receivables_to_assets')
|
||||
calcAssetRatio('prepaid', 'prepaid_to_assets')
|
||||
calcAssetRatio('property_plant&equipment', 'ppe_to_assets')
|
||||
calcAssetRatio('lt_investment', 'lt_investment_to_assets')
|
||||
calcAssetRatio('accounts_payable', 'payables_to_assets')
|
||||
calcAssetRatio('st_defer_rev', 'deferred_revenue_to_assets')
|
||||
calcAssetRatio('st_debt', 'st_debt_to_assets')
|
||||
calcAssetRatio('lt_debt', 'lt_debt_to_assets')
|
||||
|
||||
// Calculate Other Assets Ratio
|
||||
// Formula: 100 - Cash% - Inventory% - Receivables% - Prepaid% - PP&E% - LT Investment%
|
||||
if (row['total_assets']) {
|
||||
const getR = (k: string) => (typeof row[k] === 'number' ? row[k] : 0)
|
||||
const sumKnown = getR('cash_to_assets') + getR('inventory_to_assets') +
|
||||
getR('receivables_to_assets') + getR('prepaid_to_assets') +
|
||||
getR('ppe_to_assets') + getR('lt_investment_to_assets')
|
||||
row['other_assets_ratio'] = 100 - sumKnown
|
||||
}
|
||||
|
||||
// Calculate Other Expense Ratio
|
||||
// Formula: Gross Margin - Net Margin - SG&A% - R&D%
|
||||
// Note: All values are expected to be in Percentage (0-100) scale
|
||||
const getVal = (k: string) => {
|
||||
const v = row[k]
|
||||
if (v === null || v === undefined) return 0
|
||||
const num = typeof v === 'string' ? parseFloat(v) : v
|
||||
return (typeof num === 'number' && !isNaN(num)) ? num : 0
|
||||
}
|
||||
|
||||
// Check if we have the necessary components to make a meaningful calculation
|
||||
// We need at least Gross Margin and Net Profit Margin to be present
|
||||
// Note: We check against null/undefined, but getVal handles parsing so we can be more lenient in check or rely on getVal result (though 0 might be valid)
|
||||
// It's safer to try calculating if keys exist
|
||||
if (row['gross_margin'] !== undefined && row['net_profit_margin'] !== undefined) {
|
||||
const gross = getVal('gross_margin')
|
||||
const net = getVal('net_profit_margin')
|
||||
const sga = getVal('sg&a_to_revenue')
|
||||
const rd = getVal('r&d_to_revenue')
|
||||
|
||||
row['other_expense_ratio'] = gross - net - sga - rd
|
||||
|
||||
// Calculate Breakdown Rows
|
||||
row['other_breakdown_gross'] = gross
|
||||
row['other_breakdown_net'] = -1 * net
|
||||
row['other_breakdown_sga'] = -1 * sga
|
||||
row['other_breakdown_rd'] = -1 * rd
|
||||
}
|
||||
|
||||
// Calculate Per Employee Metrics (Unit: Wan)
|
||||
// Revenue input is in Millions (based on formatMoney logic / 100 -> Yi)
|
||||
// Metric = (Val * 1,000,000) / Employee / 10,000
|
||||
// Metric = (Val * 100) / Employee
|
||||
const emp = getVal('num_of_employees') || getVal('employee') // handle both keys if present
|
||||
if (emp && emp > 0) {
|
||||
const rev = typeof row['revenue'] === 'string' ? parseFloat(row['revenue']) : row['revenue']
|
||||
const net = typeof row['net_income'] === 'string' ? parseFloat(row['net_income']) : row['net_income']
|
||||
|
||||
if (rev !== undefined && !isNaN(rev)) {
|
||||
row['revenue_per_employee'] = (rev * 100) / emp
|
||||
}
|
||||
if (net !== undefined && !isNaN(net)) {
|
||||
row['profit_per_employee'] = (net * 100) / emp
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Extract Indicators (From Filtered Data ONLY)
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -322,18 +419,33 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
'dividends_paid', 'dividend_payout_ratio', 'repurchase',
|
||||
'total_assets', 'equity', 'goodwill',
|
||||
'__SECTION_EXPENSE__',
|
||||
'selling&marketing', 'general&admin', 'sg&a', 'r&d',
|
||||
'depreciation', 'tax_rate',
|
||||
'selling&marketing_to_revenue', 'selling&marketing',
|
||||
'general&admin_to_revenue', 'general&admin',
|
||||
'sg&a_to_revenue', 'sg&a',
|
||||
'r&d_to_revenue', 'r&d',
|
||||
'other_expense_ratio',
|
||||
'other_breakdown_gross', 'other_breakdown_net', 'other_breakdown_sga', 'other_breakdown_rd',
|
||||
'depreciation_to_revenue', 'depreciation',
|
||||
'tax_rate',
|
||||
'__SECTION_ASSET__',
|
||||
'cash', 'inventory', 'accounts¬es_receivable', 'prepaid',
|
||||
'property_plant&equipment', 'lt_investment',
|
||||
'accounts_payable', 'st_defer_rev',
|
||||
'st_debt', 'lt_debt', 'total_debt_ratio',
|
||||
'cash_to_assets', 'cash',
|
||||
'inventory_to_assets', 'inventory',
|
||||
'receivables_to_assets', 'accounts¬es_receivable',
|
||||
'prepaid_to_assets', 'prepaid',
|
||||
'ppe_to_assets', 'property_plant&equipment',
|
||||
'lt_investment_to_assets', 'lt_investment',
|
||||
'other_assets_ratio',
|
||||
'payables_to_assets', 'accounts_payable',
|
||||
'deferred_revenue_to_assets', 'st_defer_rev',
|
||||
'st_debt_to_assets', 'st_debt',
|
||||
'lt_debt_to_assets', 'lt_debt',
|
||||
'total_debt_ratio',
|
||||
'__SECTION_TURNOVER__',
|
||||
'inventory_days', 'days_sales_outstanding', 'payables_days',
|
||||
'net_fixed_asset_turnover', 'asset_turnover',
|
||||
'__SECTION_EFFICIENCY__',
|
||||
'employee', 'num_of_employees',
|
||||
'revenue_per_employee', 'profit_per_employee',
|
||||
'__SECTION_MARKET__',
|
||||
'last_price', 'market_cap', 'pe', 'pb',
|
||||
'dividend_yield', 'shareholders'
|
||||
@ -342,7 +454,8 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
// Ensure section header is in the list to be sorted
|
||||
const sectionKeys = [
|
||||
'__SECTION_MAIN__', '__SECTION_EXPENSE__', '__SECTION_ASSET__',
|
||||
'__SECTION_TURNOVER__', '__SECTION_EFFICIENCY__', '__SECTION_MARKET__'
|
||||
'__SECTION_TURNOVER__', '__SECTION_EFFICIENCY__', '__SECTION_MARKET__',
|
||||
'other_breakdown_gross', 'other_breakdown_net', 'other_breakdown_sga', 'other_breakdown_rd',
|
||||
]
|
||||
sectionKeys.forEach(key => {
|
||||
if (!allIndicators.has(key)) {
|
||||
@ -396,6 +509,64 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
show: false, x: 0, y: 0, text: ''
|
||||
})
|
||||
|
||||
// Interactive Rows State
|
||||
const [expandedMetrics, setExpandedMetrics] = useState<Set<string>>(new Set())
|
||||
|
||||
const ratioToAbsMap: Record<string, string | string[]> = {
|
||||
'selling&marketing_to_revenue': 'selling&marketing',
|
||||
'general&admin_to_revenue': 'general&admin',
|
||||
'sg&a_to_revenue': 'sg&a',
|
||||
'r&d_to_revenue': 'r&d',
|
||||
'depreciation_to_revenue': 'depreciation',
|
||||
'other_expense_ratio': ['other_breakdown_gross', 'other_breakdown_net', 'other_breakdown_sga', 'other_breakdown_rd'],
|
||||
// Asset Ratios
|
||||
'cash_to_assets': 'cash',
|
||||
'inventory_to_assets': 'inventory',
|
||||
'receivables_to_assets': 'accounts¬es_receivable',
|
||||
'prepaid_to_assets': 'prepaid',
|
||||
'ppe_to_assets': 'property_plant&equipment',
|
||||
'lt_investment_to_assets': 'lt_investment',
|
||||
'payables_to_assets': 'accounts_payable',
|
||||
'deferred_revenue_to_assets': 'st_defer_rev',
|
||||
'st_debt_to_assets': 'st_debt',
|
||||
'lt_debt_to_assets': 'lt_debt',
|
||||
}
|
||||
const hiddenByDefault = new Set<string>()
|
||||
Object.values(ratioToAbsMap).forEach(val => {
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach(v => hiddenByDefault.add(v))
|
||||
} else {
|
||||
hiddenByDefault.add(val)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure virtual keys are included in sortedIndicators
|
||||
// (Handled via sectionKeys logic above)
|
||||
|
||||
const handleRowClick = (indicator: string) => {
|
||||
const target = ratioToAbsMap[indicator]
|
||||
if (!target) return
|
||||
|
||||
setExpandedMetrics(prev => {
|
||||
const next = new Set(prev)
|
||||
|
||||
const targets = Array.isArray(target) ? target : [target]
|
||||
|
||||
// Toggle logic: If first target is present, remove all. Else add all.
|
||||
// This ensures consistent state even if desynced.
|
||||
const isExpanded = next.has(targets[0])
|
||||
|
||||
targets.forEach(t => {
|
||||
if (isExpanded) {
|
||||
next.delete(t)
|
||||
} else {
|
||||
next.add(t)
|
||||
}
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent, text: string) => {
|
||||
setTooltip({
|
||||
show: true,
|
||||
@ -523,15 +694,44 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
// Check visibility
|
||||
const isHidden = hiddenByDefault.has(indicator) && !expandedMetrics.has(indicator)
|
||||
if (isHidden) return null
|
||||
|
||||
const isTrigger = indicator in ratioToAbsMap
|
||||
|
||||
// Check expansion state for array targets
|
||||
let isExpanded = false
|
||||
if (isTrigger) {
|
||||
const target = ratioToAbsMap[indicator]
|
||||
const firstTarget = Array.isArray(target) ? target[0] : target
|
||||
isExpanded = expandedMetrics.has(firstTarget)
|
||||
}
|
||||
|
||||
const isSubRow = hiddenByDefault.has(indicator)
|
||||
const subRowClass = isSubRow ? "text-xs text-muted-foreground" : ""
|
||||
|
||||
return (
|
||||
<TableRow key={indicator} className="hover:bg-purple-100 dark:hover:bg-purple-900/40">
|
||||
<TableRow
|
||||
key={indicator}
|
||||
className={`hover:bg-purple-100 dark:hover:bg-purple-900/40 ${isHidden ? 'hidden' : ''} ${subRowClass}`}
|
||||
>
|
||||
<TableCell
|
||||
className={`font-medium sticky left-0 bg-background z-10 border-r shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] cursor-help transition-colors hover:bg-muted ${['revenue_growth', 'netincome_growth'].includes(indicator) ? 'text-blue-600 italic dark:text-blue-400' : ''
|
||||
}`}
|
||||
onMouseMove={(e) => handleMouseMove(e, indicator)}
|
||||
className={`font-medium sticky left-0 bg-background z-10 border-r shadow-[2px_0_5px_-2px_rgba(0,0,0,0.1)] transition-colors hover:bg-muted ${['revenue_growth', 'netincome_growth'].includes(indicator) ? 'text-blue-600 italic dark:text-blue-400' : ''
|
||||
} ${isTrigger ? 'cursor-pointer' : 'cursor-help'}`}
|
||||
onMouseMove={(e) => !isTrigger && handleMouseMove(e, indicator)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => isTrigger && handleRowClick(indicator)}
|
||||
>
|
||||
<span>{formatColumnName(indicator)}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{isTrigger && (
|
||||
isExpanded ? <ChevronDown className="w-3 h-3 text-muted-foreground" /> : <ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
<span className={isTrigger ? "" : (hiddenByDefault.has(indicator) ? "pl-8" : "")}>
|
||||
{formatColumnName(indicator)}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
{dates.map(date => {
|
||||
const row = dataMap.get(date)
|
||||
@ -543,7 +743,18 @@ function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: {
|
||||
if (value !== null && value !== undefined) {
|
||||
const numVal = typeof value === 'string' ? parseFloat(value) : value
|
||||
if (typeof numVal === 'number' && !isNaN(numVal)) {
|
||||
if (indicator === 'roe' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40'
|
||||
const assetRatios = [
|
||||
'cash_to_assets', 'inventory_to_assets', 'receivables_to_assets',
|
||||
'prepaid_to_assets', 'ppe_to_assets', 'lt_investment_to_assets',
|
||||
'payables_to_assets', 'deferred_revenue_to_assets',
|
||||
'st_debt_to_assets', 'lt_debt_to_assets', 'other_assets_ratio'
|
||||
]
|
||||
|
||||
if (assetRatios.includes(indicator) && numVal > 30) {
|
||||
highlightClass = 'bg-red-100 dark:bg-red-900/40 font-bold'
|
||||
}
|
||||
else if (indicator === 'other_assets_ratio') highlightClass = 'bg-yellow-100 dark:bg-yellow-900/40'
|
||||
else if (indicator === 'roe' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40'
|
||||
else if (indicator === 'gross_margin' && numVal > 40) highlightClass = 'bg-green-100 dark:bg-green-900/40'
|
||||
else if (indicator === 'net_profit_margin' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40'
|
||||
else if (indicator === 'roce' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40'
|
||||
@ -600,12 +811,22 @@ function formatColumnName(column: string): string {
|
||||
'net_profit_margin': '净利率',
|
||||
'ebitda_margin': 'EBITDA利润率',
|
||||
'sg&a': '销售管理费用(SG&A)',
|
||||
'sg&a_to_revenue': '销售管理费用率',
|
||||
'selling&marketing': '销售费用',
|
||||
'selling&marketing_to_revenue': '销售费用率',
|
||||
'general&admin': '管理费用',
|
||||
'general&admin_to_revenue': '管理费用率',
|
||||
'ga_exp': '管理费用', // Alias
|
||||
'rd_exp': '研发费用',
|
||||
'r&d': '研发费用',
|
||||
'r&d_to_revenue': '研发费用率',
|
||||
'other_expense_ratio': '其他费用率',
|
||||
'other_breakdown_gross': '毛利率',
|
||||
'other_breakdown_net': '(-) 净利率',
|
||||
'other_breakdown_sga': '(-) 销售管理费用率(SG&A)',
|
||||
'other_breakdown_rd': '(-) 研发费用率',
|
||||
'depreciation': '折旧',
|
||||
'depreciation_to_revenue': '折旧率',
|
||||
'tax_rate': '有效税率',
|
||||
'dividends': '分红',
|
||||
'dividend_payout_ratio': '分红支付率',
|
||||
@ -631,12 +852,23 @@ function formatColumnName(column: string): string {
|
||||
'lt_investment': '长期投资',
|
||||
'short_term_debt': '短期借款',
|
||||
'st_debt': '短期借款',
|
||||
'st_debt_to_assets': '短期借款占比',
|
||||
'long_term_debt': '长期借款',
|
||||
'lt_debt': '长期借款',
|
||||
'lt_debt_to_assets': '长期借款占比',
|
||||
'deferred_revenue': '递延收入',
|
||||
'st_defer_rev': '短期递延收入',
|
||||
'deferred_revenue_to_assets': '预收占比',
|
||||
'goodwill': '商誉',
|
||||
'total_debt_ratio': '总债务比率',
|
||||
'cash_to_assets': '现金占比',
|
||||
'inventory_to_assets': '存货占比',
|
||||
'receivables_to_assets': '应收占比',
|
||||
'prepaid_to_assets': '预付占比',
|
||||
'ppe_to_assets': '固定资产占比',
|
||||
'lt_investment_to_assets': '长期投资占比',
|
||||
'payables_to_assets': '应付占比',
|
||||
'other_assets_ratio': '其他资产占比',
|
||||
|
||||
// --- 现金流量表 ---
|
||||
'cash_from_operating': '经营现金流',
|
||||
@ -658,6 +890,8 @@ function formatColumnName(column: string): string {
|
||||
'payables_days': '应付账款周转天数',
|
||||
'employee': '员工人数',
|
||||
'num_of_employees': '员工人数',
|
||||
'revenue_per_employee': '人均创收(万)',
|
||||
'profit_per_employee': '人均创利(万)',
|
||||
|
||||
// --- 估值与市场 ---
|
||||
'pe': '市盈率(PE)',
|
||||
@ -700,7 +934,23 @@ function formatCellValue(column: string, value: any): string {
|
||||
numVal = numVal * 100
|
||||
}
|
||||
|
||||
const isRatio = ['roe', 'roce', 'roa', 'gross_margin', 'net_profit_margin', 'ebitda_margin', 'dividend_yield', 'rev_abroad', 'tax_rate', 'revenue_growth', 'netincome_growth'].includes(lowerCol)
|
||||
const isRatio = ['roe', 'roce', 'roa', 'gross_margin', 'net_profit_margin', 'ebitda_margin', 'dividend_yield', 'rev_abroad', 'tax_rate', 'revenue_growth', 'netincome_growth',
|
||||
'selling&marketing_to_revenue', 'general&admin_to_revenue', 'sg&a_to_revenue', 'r&d_to_revenue', 'depreciation_to_revenue',
|
||||
'other_expense_ratio',
|
||||
'other_breakdown_gross', 'other_breakdown_net', 'other_breakdown_sga', 'other_breakdown_rd',
|
||||
'cash_to_assets', 'inventory_to_assets', 'receivables_to_assets', 'prepaid_to_assets', 'ppe_to_assets', 'lt_investment_to_assets',
|
||||
'payables_to_assets', 'deferred_revenue_to_assets', 'st_debt_to_assets', 'lt_debt_to_assets',
|
||||
'other_assets_ratio'
|
||||
].includes(lowerCol)
|
||||
|
||||
// Special handling for Per Employee (Just number formatting, 2 decimals)
|
||||
if (['revenue_per_employee', 'profit_per_employee'].includes(lowerCol)) {
|
||||
if (numVal === undefined || numVal === null || isNaN(numVal)) return '-'
|
||||
return numVal.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// Scale huge monetary values to "Yi" (100 Million)
|
||||
// Bloomberg raw data is in Millions.
|
||||
|
||||
@ -57,6 +57,8 @@ export function StockChart({ symbol, market }: StockChartProps) {
|
||||
"autosize": true,
|
||||
"symbol": fullSymbol,
|
||||
"interval": "D",
|
||||
"range": "12m",
|
||||
"scale": "log",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"theme": "light",
|
||||
"style": "1",
|
||||
@ -73,9 +75,12 @@ export function StockChart({ symbol, market }: StockChartProps) {
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)] w-full bg-background rounded-lg border overflow-hidden p-1">
|
||||
<div className="tradingview-widget-container h-full w-full" ref={containerRef}>
|
||||
<div className="tradingview-widget-copyright text-xs text-center text-muted-foreground p-2">
|
||||
TradingView Chart by <a href="https://cn.tradingview.com/" rel="noopener nofollow" target="_blank">TradingView</a>
|
||||
<div className="tradingview-widget-container h-full w-full flex flex-col" ref={containerRef}>
|
||||
<div className="tradingview-widget-copyright text-xs text-center text-muted-foreground p-2 mt-auto border-t bg-muted/20">
|
||||
<a href={`https://cn.tradingview.com/chart/?symbol=${symbol}`} rel="noopener nofollow" target="_blank" className="hover:text-primary transition-colors flex items-center justify-center gap-1">
|
||||
<span className="font-medium">在 TradingView 上查看完整图表</span>
|
||||
<span className="text-[10px] opacity-70">({symbol})</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user