commit;
This commit is contained in:
		
							parent
							
								
									cce06d8710
								
							
						
					
					
						commit
						c4c4e8622f
					
				
							
								
								
									
										51
									
								
								src/app.py
								
								
								
								
							
							
						
						
									
										51
									
								
								src/app.py
								
								
								
								
							|  | @ -2999,7 +2999,7 @@ def run_batch_stock_price_collection(): | |||
| 
 | ||||
| @app.route('/scheduler/batch_hk_stock_price/collection', methods=['GET']) | ||||
| def run_batch_hk_stock_price_collection(): | ||||
|     """批量采集A股行情并保存到数据库""" | ||||
|     """批量采集港股行情并保存到数据库""" | ||||
|     try: | ||||
|         fetch_and_store_hk_stock_data() | ||||
|         return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) | ||||
|  | @ -3039,24 +3039,39 @@ def get_portfolio_industry_allocation(): | |||
| def get_notice_list(): | ||||
|     """获取重要提醒列表""" | ||||
|     try: | ||||
|         # 模拟数据 - 实际项目中应该从数据库或外部API获取 | ||||
|         mock_notices = [ | ||||
|             "上证指数突破3200点,市场情绪回暖", | ||||
|             "北向资金今日净流入85.6亿元", | ||||
|             "科技板块PE估值处于历史低位", | ||||
|             "新能源概念股集体上涨,涨幅超3%", | ||||
|             "医药板块回调,建议关注低吸机会", | ||||
|             "融资融券余额连续三日增长", | ||||
|             "消费板块资金流入明显", | ||||
|             "市场恐贪指数回升至65", | ||||
|             "机器人概念板块技术面突破", | ||||
|             "先进封装概念获政策支持" | ||||
|         ] | ||||
|         # 导入提醒服务 | ||||
|         from src.valuation_analysis.notice_service import NoticeService | ||||
|          | ||||
|         return jsonify({ | ||||
|             "status": "success", | ||||
|             "data": mock_notices | ||||
|         }) | ||||
|         # 创建提醒服务实例 | ||||
|         notice_service = NoticeService() | ||||
|          | ||||
|         # 获取动态提醒数据 | ||||
|         result = notice_service.get_dynamic_notices() | ||||
|          | ||||
|         if result.get("success"): | ||||
|             return jsonify({ | ||||
|                 "status": "success", | ||||
|                 "data": result["data"] | ||||
|             }) | ||||
|         else: | ||||
|             # 如果动态提醒失败,返回默认提醒 | ||||
|             logger.warning(f"动态提醒获取失败: {result.get('message')},使用默认提醒") | ||||
|             default_notices = [ | ||||
|                 "📈 上证指数突破3200点,市场情绪回暖", | ||||
|                 "💰 北向资金今日净流入85.6亿元", | ||||
|                 "📊 科技板块PE估值处于历史低位", | ||||
|                 "🔥 新能源概念股集体上涨,涨幅超3%", | ||||
|                 "⚠️ 医药板块回调,建议关注低吸机会", | ||||
|                 "📈 融资融券余额连续三日增长", | ||||
|                 "💰 消费板块资金流入明显", | ||||
|                 "📊 市场恐贪指数回升至65", | ||||
|                 "🤖 机器人概念板块技术面突破", | ||||
|                 "📦 先进封装概念获政策支持" | ||||
|             ] | ||||
|             return jsonify({ | ||||
|                 "status": "success", | ||||
|                 "data": default_notices | ||||
|             }) | ||||
|     except Exception as e: | ||||
|         logger.error(f"获取提醒列表失败: {str(e)}") | ||||
|         return jsonify({'status': 'error', 'message': str(e)}) | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ class FundamentalAnalyzer: | |||
|         self.chat_bot = ChatBot(model_type="online_bot") | ||||
|         # 使用离线模型进行其他分析 | ||||
|         self.offline_bot = OfflineChatBot(platform="volc", model_type="offline_model") | ||||
|         # 千问打杂 | ||||
|         # GLM打杂 | ||||
|         # self.offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="qwq") | ||||
|         self.offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="GLM") | ||||
|          | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ XUEQIU_HEADERS = { | |||
|     'Accept-Encoding': 'gzip, deflate, br, zstd', | ||||
|     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||
|     'Client-Version': 'v2.44.75', | ||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; Hm_lvt_1db88642e346389874251b5a1eded6e3=1751936369; xq_a_token=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xqat=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzU2MDAyNjgyLCJjdG0iOjE3NTM0MTA2ODI0MTQsImNpZCI6ImQ5ZDBuNEFadXAifQ.AlnzQSY7oGKGABfaQcFLg0lAJsDdvBMiwUbgpCMCBlbx6VZPKhzERxWiylQb4dFIyyECvRRJ73SbO9cD46fAqgzOgTxArNHtTKD4lQapTnyb11diDADnpb_nzzaRr4k_BYQRKXWtcJxdUMzde2WLy-eAkSf76QkXmKrwS3kvRm5gfqhdye44whw5XMEGoZ_lXHzGLWGz_PludHZp6W3v-wwZc_0wLU6cTb_KdrwWUWT_8jw5JHXnJEmuZmQI8QWf60DtiHIYCYXarxv8XtyHK7lLKhIAa3C2QmGWw5wv2HGz4I5DPqm2uMPKumgkQxycfAk56-RWviLZ8LAPF-XcbA; xq_r_token=92527e51353f90ba14d5fd16581e5a7a2780baa2; acw_tc=0a27aa0f17542694317833912e006564153fcd1bb89f49a865e382d9953601; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1754269439; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=HS+RscPvXRUz1ypZekks1pgGkAHHlHsHVuftTbDQCbUUaFqtm9BV4h7ghR2d5Nh+YD29otSyz2svRiKWvOJqgQ%3D%3D; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuq4wQWiYMrK4N4hGRtDl=YoDZDGFdDqx0Ei6Fi7HKzYhtBoqzWKjw_wv5YlCZMPO8//1P9PQCNzkOQ4hviDB3DbqDy/dePxYYjDBYD74G_DDeDixdDj4GmDGYtOeDFfCuNq6R5dxDwDB=DmMIbfeDEDG3D0fbeCLRYwDDBDGUFxtaDG4Gf0mDD0wDAo0jooDGWfnu4s6mkeFKN57G3x0tWDBL5QvG3x/lnoGWNVtlfkS2FkPGuDG6Ogl0kDqQO3i2AfP4KGGIm0iBPKY_5leOQDqQe4YwQGDpl0xliO7Gm0DOGDz0G4ixqYw1n0aSpwhixgPXieD1NZcX3ZXDK4rm0IlvYRGImxqnmmlG4eK40w4Am1BqGYeeGn5ixXWa3m2b/DDgi3YD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuq4wQWiYMrK4N4hGbYDiPbY44h7ie03dz7=3xDlouSdLRKl=Q_2YStYQ7OzOy_RBQ1oeziI2pkPsD8RSfPnSw5L7G4xcSPKKMxxoCD6zTiVCud28rNOm2tL7qASSMTjB2GcYPxzSRi94n0Kgjd6C6jKOMh5rMtOfkR2l8TGOPL277=81u9MRkBgIwRxDwx6iYEE4omE9FE1lonhzib3BUC6PD', | ||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; xq_a_token=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xqat=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzU2MDAyNjgyLCJjdG0iOjE3NTM0MTA2ODI0MTQsImNpZCI6ImQ5ZDBuNEFadXAifQ.AlnzQSY7oGKGABfaQcFLg0lAJsDdvBMiwUbgpCMCBlbx6VZPKhzERxWiylQb4dFIyyECvRRJ73SbO9cD46fAqgzOgTxArNHtTKD4lQapTnyb11diDADnpb_nzzaRr4k_BYQRKXWtcJxdUMzde2WLy-eAkSf76QkXmKrwS3kvRm5gfqhdye44whw5XMEGoZ_lXHzGLWGz_PludHZp6W3v-wwZc_0wLU6cTb_KdrwWUWT_8jw5JHXnJEmuZmQI8QWf60DtiHIYCYXarxv8XtyHK7lLKhIAa3C2QmGWw5wv2HGz4I5DPqm2uMPKumgkQxycfAk56-RWviLZ8LAPF-XcbA; xq_r_token=92527e51353f90ba14d5fd16581e5a7a2780baa2; acw_tc=1a0c655917546366986673411e68d25d3c69c1719d6d1d6283c7271cc1529f; is_overseas=0; Hm_lvt_1db88642e346389874251b5a1eded6e3=1754636834; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1754636837; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=Hvg6Ac+qmPnDgzOvFuCePWwm7reK8TPoE9ayL8cyLnFg+Jhg1RJO2WnkeH2T8Q18+iV9bDh+UAq222GxdelHBg%3D%3D; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuqbKOOQYMxPsMKjqDsqze4GzDiLPGhDBWAFdYjdqN4NCtAoqzWWF2ruqe8bOZqKKFS96SM6sXUGQKhexGLDY=DCuXiieGGU4GwDGoD34DiDDpLD03Db4D_nWrD7ORQMluokjeDQ4GyDiUk3ObDm4DfDDLorA6osQ4DGqDSFcyTxD3DfRb4DDN4CIDu_mDDbObt5jcbUx7OBCGxIeDMixGXzGC4InyRNvDrgjMXvzEKH1aDtqD9_au4XxKdr3NEAEP4KGGpC0inpge_5neOQDqix1oeee4eQvxQ5O7Gv0DOGDz0G4ix_jwP_RUWjiihW9PeGAShXZ=E/ZND6q3mi40weUmXjmvYIzSQzWDW9wsemhYedCrwihQYbKYvWRD3YD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuqbKOOQYMxPsMKe4DWhzmxhTKRDjR_xWs_DDs6KmhfHjRKnZkBxNA3TIO4Arip5wU2kO0SwUfkEzryfSk6Rzud3ARD49fiKFd344obYvCv1lxYhY3qdzQe3vWD', | ||||
|     'Referer': 'https://weibo.com/u/7735765253', | ||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||
|     'Sec-Ch-Ua-Mobile': '?0', | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -216,22 +216,5 @@ | |||
| <script src="/static/js/echarts.min.js"></script> | ||||
| <script src="/static/js/jquery.min.js"></script> | ||||
| <script src="/static/js/bigscreen.js"></script> | ||||
| <script> | ||||
| // document.getElementById('fullscreen-btn').onclick = function() { | ||||
| //     function launchFullScreen(element) { | ||||
| //         if(element.requestFullscreen) { | ||||
| //             element.requestFullscreen(); | ||||
| //         } else if(element.mozRequestFullScreen) { | ||||
| //             element.mozRequestFullScreen(); | ||||
| //         } else if(element.webkitRequestFullscreen) { | ||||
| //             element.webkitRequestFullscreen(); | ||||
| //         } else if(element.msRequestFullscreen) { | ||||
| //             element.msRequestFullscreen(); | ||||
| //         } | ||||
| //     } | ||||
| //     launchFullScreen(document.documentElement); | ||||
| //     this.style.display = 'none'; // 全屏后隐藏按钮 | ||||
| // }; | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -0,0 +1,781 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="zh-CN"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>资金与行业估值大屏</title> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | ||||
|     <link rel="stylesheet" href="/static/css/bootstrap.min.css"> | ||||
|     <style> | ||||
|         html, body {  | ||||
|             height: 100%;  | ||||
|             padding-left: 5px; | ||||
|             padding-right: 5px; | ||||
|             padding-top: 5px; | ||||
|         } | ||||
|         body {  | ||||
|             background: #f7f7fa;  | ||||
|             color: #222;  | ||||
|             min-height: 100vh;  | ||||
|         } | ||||
|         .container-fluid {  | ||||
|             min-height: 100vh;  | ||||
|             padding: 0;  | ||||
|         } | ||||
|         .row.d-flex {  | ||||
|             height: 28vh;  | ||||
|             margin-left: 0;  | ||||
|             margin-right: 0;  | ||||
|         } | ||||
|         .row.d-flex2 {  | ||||
|             height: 69vh;  | ||||
|             margin-left: 0;  | ||||
|             margin-right: 0;  | ||||
|         } | ||||
|         .col-3.d-flex { | ||||
|             padding-left: 2px;  | ||||
|             padding-right: 2px; | ||||
|             border: 1.5px solid #c7c6c6; | ||||
|             border-radius: 8px; | ||||
|             box-sizing: border-box; | ||||
|         } | ||||
|         .chart-box { | ||||
|             background: #fff; | ||||
|             border-radius: 8px; | ||||
|             padding: 4px 4px 2px 4px; | ||||
|             box-shadow: 0 2px 8px #e0e0e0; | ||||
|             height: 100%; | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             justify-content: flex-start; | ||||
|         } | ||||
|         .tight-box {  | ||||
|             margin: 0;  | ||||
|             padding: 2px 2px 1px 2px;  | ||||
|         } | ||||
|         .chart-title {  | ||||
|             font-size: 0.95rem;  | ||||
|             color: #333;  | ||||
|             margin-bottom: 2px;  | ||||
|             text-align: center;  | ||||
|         } | ||||
|         .small-title {  | ||||
|             font-size: 0.85rem;  | ||||
|             margin-bottom: 2px;  | ||||
|             margin-top: 2px;  | ||||
|             text-align: center;  | ||||
|             color: #666; | ||||
|         } | ||||
|         .chart-container {  | ||||
|             width: 100%;  | ||||
|             flex: 1 1 0;  | ||||
|             min-height: 0;  | ||||
|             background: #f9f9fb;  | ||||
|         } | ||||
|          | ||||
|         /* 点击后的左侧容器样式 */ | ||||
|         .col-3.d-flex .chart-box.tight-box { | ||||
|             height: 100%; | ||||
|         } | ||||
|          | ||||
|         .col-3.d-flex .chart-container { | ||||
|             height: 45%; | ||||
|             min-height: 200px; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-container { | ||||
|             width: 100%; | ||||
|             height: 75px; | ||||
|             background: #f9f9fb; | ||||
|             padding: 4px; | ||||
|             overflow-y: auto; | ||||
|             font-size: 11px; | ||||
|             line-height: 1.3; | ||||
|         } | ||||
| 
 | ||||
|         .holding-item { | ||||
|             display: flex; | ||||
|             justify-content: space-between; | ||||
|             align-items: center; | ||||
|             padding: 3px 4px; | ||||
|             margin-bottom: 2px; | ||||
|             background: #fff; | ||||
|             border-radius: 3px; | ||||
|             border-left: 3px solid #5470c6; | ||||
|         } | ||||
| 
 | ||||
|         .holding-name { | ||||
|             font-weight: bold; | ||||
|             color: #333; | ||||
|             flex: 1; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|             white-space: nowrap; | ||||
|             font-size: 11px; | ||||
|         } | ||||
| 
 | ||||
|         .holding-amount { | ||||
|             color: #666; | ||||
|             font-size: 10px; | ||||
|             margin-left: 4px; | ||||
|         } | ||||
| 
 | ||||
|         .notice-container { | ||||
|             width: 100%; | ||||
|             flex: 1 1 0; | ||||
|             min-height: 0; | ||||
|             background: #f9f9fb; | ||||
|             overflow-y: auto; /* 改为垂直滚动 */ | ||||
|             position: relative; | ||||
|             scrollbar-width: thin; /* Firefox */ | ||||
|             scrollbar-color: #c1c1c1 #f1f1f1; /* Firefox */ | ||||
|         } | ||||
|          | ||||
|         /* Webkit浏览器的滚动条样式 */ | ||||
|         .notice-container::-webkit-scrollbar { | ||||
|             width: 6px; | ||||
|         } | ||||
|          | ||||
|         .notice-container::-webkit-scrollbar-track { | ||||
|             background: #f1f1f1; | ||||
|             border-radius: 3px; | ||||
|         } | ||||
|          | ||||
|         .notice-container::-webkit-scrollbar-thumb { | ||||
|             background: #c1c1c1; | ||||
|             border-radius: 3px; | ||||
|         } | ||||
|          | ||||
|         .notice-container::-webkit-scrollbar-thumb:hover { | ||||
|             background: #a8a8a8; | ||||
|         } | ||||
| 
 | ||||
|         .notice-content { | ||||
|             padding: 10px; | ||||
|             /* 移除自动滚动动画 */ | ||||
|         } | ||||
| 
 | ||||
|         .notice-item { | ||||
|             padding: 8px 0; | ||||
|             border-bottom: 1px solid #eee; | ||||
|             font-size: 12px; | ||||
|             color: #333; | ||||
|             line-height: 1.4; | ||||
|         } | ||||
| 
 | ||||
|         .notice-item:last-child { | ||||
|             border-bottom: none; | ||||
|         } | ||||
| 
 | ||||
|         /* 移除自动滚动动画 */ | ||||
|         /* @keyframes scrollNotice { | ||||
|             0% { | ||||
|                 transform: translateY(0); | ||||
|             } | ||||
|             100% { | ||||
|                 transform: translateY(-100%); | ||||
|             } | ||||
|         } */ | ||||
| 
 | ||||
|         /* 弹窗样式 */ | ||||
|         .modal-overlay { | ||||
|             position: fixed; | ||||
|             top: 0; | ||||
|             left: 0; | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             background-color: rgba(0, 0, 0, 0.7); | ||||
|             z-index: 1000; | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .modal-content { | ||||
|             background: white; | ||||
|             border-radius: 8px; | ||||
|             width: 80%; | ||||
|             max-width: 800px; | ||||
|             max-height: 80%; | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | ||||
|         } | ||||
| 
 | ||||
|         .modal-header { | ||||
|             display: flex; | ||||
|             justify-content: space-between; | ||||
|             align-items: center; | ||||
|             padding: 20px; | ||||
|             border-bottom: 1px solid #eee; | ||||
|         } | ||||
| 
 | ||||
|         .modal-header h3 { | ||||
|             margin: 0; | ||||
|             color: #333; | ||||
|             font-size: 18px; | ||||
|         } | ||||
| 
 | ||||
|         .modal-close { | ||||
|             background: none; | ||||
|             border: none; | ||||
|             font-size: 24px; | ||||
|             cursor: pointer; | ||||
|             color: #666; | ||||
|             padding: 0; | ||||
|             width: 30px; | ||||
|             height: 30px; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|         } | ||||
| 
 | ||||
|         .modal-close:hover { | ||||
|             color: #333; | ||||
|         } | ||||
| 
 | ||||
|         .modal-body { | ||||
|             flex: 1; | ||||
|             overflow: hidden; | ||||
|             padding: 0; | ||||
|         } | ||||
| 
 | ||||
|         .modal-notice-content { | ||||
|             height: 100%; | ||||
|             overflow-y: auto; | ||||
|             padding: 20px; | ||||
|         } | ||||
| 
 | ||||
|         .modal-notice-item { | ||||
|             padding: 12px 0; | ||||
|             border-bottom: 1px solid #f0f0f0; | ||||
|             font-size: 14px; | ||||
|             line-height: 1.6; | ||||
|             color: #333; | ||||
|         } | ||||
| 
 | ||||
|         .modal-notice-item:last-child { | ||||
|             border-bottom: none; | ||||
|         } | ||||
| 
 | ||||
|         .modal-holdings-content { | ||||
|             height: 100%; | ||||
|             overflow-y: auto; | ||||
|             padding: 20px; | ||||
|             max-height: 70vh; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-table { | ||||
|             width: 100%; | ||||
|             border-collapse: collapse; | ||||
|             font-size: 14px; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-table th, | ||||
|         .holdings-table td { | ||||
|             padding: 8px 12px; | ||||
|             text-align: left; | ||||
|             border-bottom: 1px solid #eee; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-table th { | ||||
|             background-color: #f8f9fa; | ||||
|             font-weight: bold; | ||||
|             color: #333; | ||||
|             position: sticky; | ||||
|             top: 0; | ||||
|             z-index: 10; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-table tr:hover { | ||||
|             background-color: #f5f5f5; | ||||
|         } | ||||
| 
 | ||||
|         .stock-row { | ||||
|             cursor: pointer; | ||||
|             background-color: #f8f9fa; | ||||
|         } | ||||
| 
 | ||||
|         .stock-row:hover { | ||||
|             background-color: #e9ecef !important; | ||||
|         } | ||||
| 
 | ||||
|         .detail-row { | ||||
|             background-color: #fafafa; | ||||
|             display: none; | ||||
|         } | ||||
| 
 | ||||
|         .detail-row.show { | ||||
|             display: table-row; | ||||
|         } | ||||
| 
 | ||||
|         .detail-content { | ||||
|             padding: 10px; | ||||
|             background-color: #fff; | ||||
|             border-left: 3px solid #5470c6; | ||||
|             margin: 5px 0; | ||||
|         } | ||||
| 
 | ||||
|         .expand-icon { | ||||
|             margin-right: 5px; | ||||
|             transition: transform 0.3s; | ||||
|         } | ||||
| 
 | ||||
|         .expand-icon.expanded { | ||||
|             transform: rotate(90deg); | ||||
|         } | ||||
| 
 | ||||
|         .holdings-summary { | ||||
|             margin-bottom: 20px; | ||||
|             padding: 15px; | ||||
|             background-color: #f8f9fa; | ||||
|             border-radius: 5px; | ||||
|             border-left: 4px solid #5470c6; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-summary h4 { | ||||
|             margin: 0 0 10px 0; | ||||
|             color: #333; | ||||
|         } | ||||
| 
 | ||||
|         .holdings-summary p { | ||||
|             margin: 5px 0; | ||||
|             color: #666; | ||||
|         } | ||||
| 
 | ||||
|         /* 平板响应式 */ | ||||
|         @media (max-width: 1200px) { | ||||
|             .row.d-flex { height: auto; } | ||||
|             .chart-box { height: auto; min-height: 180px; } | ||||
|         } | ||||
| 
 | ||||
|         /* 移动端响应式 */ | ||||
|         @media (max-width: 768px) { | ||||
|             html, body { | ||||
|                 padding: 2px; | ||||
|             } | ||||
|             .container-fluid { | ||||
|                 padding: 0; | ||||
|             } | ||||
|             .row.d-flex, .row.d-flex2 { | ||||
|                 flex-direction: column !important; | ||||
|                 height: auto !important; | ||||
|                 margin: 0; | ||||
|             } | ||||
|             .col-3.d-flex { | ||||
|                 width: 100% !important; | ||||
|                 max-width: 100% !important; | ||||
|                 min-width: 0 !important; | ||||
|                 margin-bottom: 8px; | ||||
|                 padding: 1px 0; | ||||
|             } | ||||
|             .chart-box, .tight-box { | ||||
|                 min-height: 200px; | ||||
|                 height: auto !important; | ||||
|                 padding: 2px; | ||||
|             } | ||||
|             .chart-title { | ||||
|                 font-size: 0.9rem; | ||||
|                 margin-bottom: 1px; | ||||
|             } | ||||
|             .small-title { | ||||
|                 font-size: 0.8rem; | ||||
|                 margin-bottom: 1px; | ||||
|                 margin-top: 1px; | ||||
|             } | ||||
|             .chart-container { | ||||
|                 min-height: 180px; | ||||
|             } | ||||
|             /* 调整概念卡片的布局 */ | ||||
|             .tight-box .chart-container { | ||||
|                 min-height: 150px; | ||||
|                 min-width: 0; | ||||
|                 width: 100%; | ||||
|                 box-sizing: border-box; | ||||
|             } | ||||
|             /* 优化图表间距 */ | ||||
|             .tight-box .chart-title { | ||||
|                 padding: 1px 0; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /* 小屏手机响应式 */ | ||||
|         @media (max-width: 480px) { | ||||
|             .chart-box, .tight-box { | ||||
|                 min-height: 180px; | ||||
|             } | ||||
|             .chart-container { | ||||
|                 min-height: 160px; | ||||
|             } | ||||
|             .tight-box .chart-container { | ||||
|                 min-height: 130px; | ||||
|             } | ||||
|             .chart-title { | ||||
|                 font-size: 0.85rem; | ||||
|             } | ||||
|             .small-title { | ||||
|                 font-size: 0.75rem; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /* 标的详情容器样式 */ | ||||
|         .stock-detail-container { | ||||
|             height: 100%; | ||||
|             overflow-y: auto; | ||||
|             padding: 8px; | ||||
|             background: #f9f9fb; | ||||
|             border-radius: 5px; | ||||
|         } | ||||
|          | ||||
|         /* 合并后的标的详情容器样式 */ | ||||
|         .stock-detail-container-merged { | ||||
|             height: 600px; | ||||
|             overflow-y: auto; | ||||
|             padding: 15px; | ||||
|             background: #f9f9fb; | ||||
|             border-radius: 5px; | ||||
|         } | ||||
|          | ||||
|         .stock-details-list { | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             gap: 10px; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-container-merged .stock-details-list { | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             gap: 15px; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-item { | ||||
|             background: #fff; | ||||
|             border-radius: 6px; | ||||
|             padding: 12px; | ||||
|             box-shadow: 0 1px 4px rgba(0,0,0,0.1); | ||||
|             border-left: 3px solid #5470c6; | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-item h4 { | ||||
|             margin: 0 0 10px 0; | ||||
|             color: #333; | ||||
|             font-size: 14px; | ||||
|             font-weight: bold; | ||||
|             border-bottom: 1px solid #f0f0f0; | ||||
|             padding-bottom: 5px; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-container-merged .stock-detail-item h4 { | ||||
|             font-size: 16px; | ||||
|             margin-bottom: 15px; | ||||
|             padding-bottom: 8px; | ||||
|             color: #2c3e50; | ||||
|             border-bottom: 2px solid #3498db; | ||||
|         } | ||||
|          | ||||
|         /* 新增:持仓和因子的左右布局容器 */ | ||||
|         .factor-holding-row { | ||||
|             display: flex; | ||||
|             gap: 15px; | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .holding-section, .factor-section { | ||||
|             flex: 1; | ||||
|             min-width: 0; /* 防止内容溢出 */ | ||||
|         } | ||||
|          | ||||
|         .correction-section { | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .correction-section:last-child { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|          | ||||
|         /* 标题样式 */ | ||||
|         .factor-section h5, .holding-section h5, .correction-section h5 { | ||||
|             margin: 0 0 5px 0; | ||||
|             color: #555; | ||||
|             font-size: 12px; | ||||
|             font-weight: bold; | ||||
|             background: #f8f9fa; | ||||
|             padding: 3px 6px; | ||||
|             border-radius: 3px; | ||||
|             border-left: 2px solid #007bff; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-container-merged .factor-section h5, | ||||
|         .stock-detail-container-merged .holding-section h5, | ||||
|         .stock-detail-container-merged .correction-section h5 { | ||||
|             font-size: 14px; | ||||
|             padding: 6px 10px; | ||||
|             margin-bottom: 10px; | ||||
|             background: #ecf0f1; | ||||
|             border-radius: 4px; | ||||
|         } | ||||
|          | ||||
|         /* 保留原有的detail-section样式用于其他地方 */ | ||||
|         .detail-section { | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|          | ||||
|         .detail-section:last-child { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|          | ||||
|         .detail-section h5 { | ||||
|             margin: 0 0 5px 0; | ||||
|             color: #555; | ||||
|             font-size: 12px; | ||||
|             font-weight: bold; | ||||
|             background: #f8f9fa; | ||||
|             padding: 3px 6px; | ||||
|             border-radius: 3px; | ||||
|             border-left: 2px solid #007bff; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-container-merged .detail-section h5 { | ||||
|             font-size: 14px; | ||||
|             padding: 6px 10px; | ||||
|             margin-bottom: 10px; | ||||
|             background: #ecf0f1; | ||||
|             border-radius: 4px; | ||||
|         } | ||||
|          | ||||
|         .factor-details, .holding-details, .correction-details { | ||||
|             padding: 5px; | ||||
|             background: #fafafa; | ||||
|             border-radius: 3px; | ||||
|             margin-top: 3px; | ||||
|         } | ||||
|          | ||||
|         .factor-details p, .holding-details p, .correction-details p { | ||||
|             margin: 2px 0; | ||||
|             font-size: 11px; | ||||
|             color: #666; | ||||
|             line-height: 1.3; | ||||
|         } | ||||
|          | ||||
|         .stock-detail-container-merged .factor-details p, | ||||
|         .stock-detail-container-merged .holding-details p, | ||||
|         .stock-detail-container-merged .correction-details p { | ||||
|             font-size: 13px; | ||||
|             margin: 4px 0; | ||||
|             line-height: 1.5; | ||||
|             color: #34495e; | ||||
|         } | ||||
|          | ||||
|         .holding-detail-item { | ||||
|             margin-bottom: 3px; | ||||
|             padding: 3px; | ||||
|             background: #f0f8ff; | ||||
|             border-radius: 2px; | ||||
|             border-left: 2px solid #17a2b8; | ||||
|         } | ||||
|          | ||||
|         .correction-detail-item { | ||||
|             margin-bottom: 5px; | ||||
|             padding: 5px; | ||||
|             background: #fff3cd; | ||||
|             border-radius: 2px; | ||||
|             border-left: 2px solid #ffc107; | ||||
|         } | ||||
|          | ||||
|         .correction-detail-item p { | ||||
|             margin: 1px 0; | ||||
|             font-size: 10px; | ||||
|             color: #856404; | ||||
|         } | ||||
|          | ||||
|         /* 响应式调整 */ | ||||
|         @media (max-width: 768px) { | ||||
|             .stock-detail-container-merged { | ||||
|                 height: 500px; | ||||
|                 padding: 10px; | ||||
|             } | ||||
|              | ||||
|             .col-3.d-flex .chart-container { | ||||
|                 height: 40%; | ||||
|                 min-height: 150px; | ||||
|             } | ||||
|              | ||||
|             .stock-detail-item { | ||||
|                 padding: 10px; | ||||
|             } | ||||
|              | ||||
|             .stock-detail-item h4 { | ||||
|                 font-size: 14px; | ||||
|             } | ||||
|              | ||||
|             .detail-section h5 { | ||||
|                 font-size: 12px; | ||||
|             } | ||||
|              | ||||
|             .factor-details, .holding-details, .correction-details { | ||||
|                 padding: 5px; | ||||
|             } | ||||
|              | ||||
|             .factor-details p, .holding-details p, .correction-details p { | ||||
|                 font-size: 11px; | ||||
|             } | ||||
|              | ||||
|             /* 移动端:因子和持仓改为上下布局 */ | ||||
|             .factor-holding-row { | ||||
|                 flex-direction: column; | ||||
|                 gap: 10px; | ||||
|             } | ||||
|              | ||||
|             .factor-section, .holding-section { | ||||
|                 flex: none; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /* 重置按钮样式 */ | ||||
|         #resetViewBtn { | ||||
|             background: rgba(255, 255, 255, 0.9); | ||||
|             border: 1px solid #007bff; | ||||
|             color: #007bff; | ||||
|             font-size: 12px; | ||||
|             padding: 5px 10px; | ||||
|             border-radius: 15px; | ||||
|             box-shadow: 0 2px 5px rgba(0,0,0,0.2); | ||||
|             transition: all 0.3s ease; | ||||
|         } | ||||
|          | ||||
|         #resetViewBtn:hover { | ||||
|             background: #007bff; | ||||
|             color: white; | ||||
|             transform: translateY(-1px); | ||||
|             box-shadow: 0 3px 8px rgba(0,0,0,0.3); | ||||
|         } | ||||
|          | ||||
|         /* 强制第一个容器占据全宽 */ | ||||
|         .col-3.d-flex.expanded { | ||||
|             width: 100% !important; | ||||
|             max-width: 100% !important; | ||||
|             flex: 1 !important; | ||||
|             flex-grow: 1 !important; | ||||
|             flex-shrink: 0 !important; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
| <div class="container-fluid"> | ||||
| 
 | ||||
| <!-- 添加重置按钮 --> | ||||
| <div style="position: fixed; top: 10px; right: 10px; z-index: 1000;"> | ||||
|     <button id="resetViewBtn" class="btn btn-sm btn-outline-primary" style="display: none;"> | ||||
|         返回默认视图 | ||||
|     </button> | ||||
| </div> | ||||
| 
 | ||||
| <div class="row d-flex"> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box w-100"> | ||||
|                 <div class="chart-title">行业持仓占比</div> | ||||
|                 <div id="portfolioChart" class="chart-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box w-100"> | ||||
|                 <div class="chart-title">重要提醒</div> | ||||
|                 <div id="noticeBox" class="notice-container" style="cursor: pointer;"> | ||||
|                     <div class="notice-content"> | ||||
|                         <div class="notice-item">加载数据中...</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box w-100"> | ||||
|                 <div class="chart-title">融资融券数据监控</div> | ||||
|                 <div id="rzrqChart" class="chart-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box w-100"> | ||||
|                 <div class="chart-title">市场恐贪指数</div> | ||||
|                 <div id="fearGreedChart" class="chart-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="row d-flex2"> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box tight-box w-100"> | ||||
|                 <div class="chart-title small-title">行业1-历史PE分析</div> | ||||
|                 <div id="peChart_xjfz" class="chart-container"></div> | ||||
|                 <div class="chart-title small-title" style="margin-top:2px;">行业1-拥挤度</div> | ||||
|                 <div id="crowdChart_xjfz" class="chart-container"></div> | ||||
|                 <!-- <div class="chart-title small-title" style="margin-top:2px;">行业1-持仓标的</div> --> | ||||
|                 <div id="holdings_xjfz" class="holdings-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box tight-box w-100"> | ||||
|                 <div class="chart-title small-title">行业2-历史PE分析</div> | ||||
|                 <div id="peChart_xp" class="chart-container"></div> | ||||
|                 <div class="chart-title small-title" style="margin-top:2px;">行业2-拥挤度</div> | ||||
|                 <div id="crowdChart_xp" class="chart-container"></div> | ||||
|                 <!-- <div class="chart-title small-title" style="margin-top:2px;">行业2-持仓标的</div> --> | ||||
|                 <div id="holdings_xp" class="holdings-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box tight-box w-100"> | ||||
|                 <div class="chart-title small-title">行业3-历史PE分析</div> | ||||
|                 <div id="peChart_xfdz" class="chart-container"></div> | ||||
|                 <div class="chart-title small-title" style="margin-top:2px;">行业3-拥挤度</div> | ||||
|                 <div id="crowdChart_xfdz" class="chart-container"></div> | ||||
|                 <!-- <div class="chart-title small-title" style="margin-top:2px;">行业3-持仓标的</div> --> | ||||
|                 <div id="holdings_xfdz" class="holdings-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-3 d-flex"> | ||||
|             <div class="chart-box tight-box w-100"> | ||||
|                 <div class="chart-title small-title">行业4-历史PE分析</div> | ||||
|                 <div id="peChart_jqr" class="chart-container"></div> | ||||
|                 <div class="chart-title small-title" style="margin-top:2px;">行业4-拥挤度</div> | ||||
|                 <div id="crowdChart_jqr" class="chart-container"></div> | ||||
|                 <!-- <div class="chart-title small-title" style="margin-top:2px;">行业4-持仓标的</div> --> | ||||
|                 <div id="holdings_jqr" class="holdings-container"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| <!-- 弹窗结构 --> | ||||
| <div id="noticeModal" class="modal-overlay" style="display: none;"> | ||||
|     <div class="modal-content"> | ||||
|         <div class="modal-header"> | ||||
|             <h3>重要提醒详情</h3> | ||||
|             <button class="modal-close" onclick="closeNoticeModal()">×</button> | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div id="modalNoticeContent" class="modal-notice-content"> | ||||
|                 <!-- 动态内容 --> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| <!-- 行业持仓详情弹窗 --> | ||||
| <div id="industryHoldingsModal" class="modal-overlay" style="display: none;"> | ||||
|     <div class="modal-content"> | ||||
|         <div class="modal-header"> | ||||
|             <h3 id="industryHoldingsTitle">行业持仓详情</h3> | ||||
|             <button class="modal-close" onclick="closeIndustryHoldingsModal()">×</button> | ||||
|         </div> | ||||
|         <div class="modal-body"> | ||||
|             <div id="industryHoldingsContent" class="modal-holdings-content"> | ||||
|                 <!-- 动态内容 --> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| <script src="/static/js/echarts.min.js"></script> | ||||
| <script src="/static/js/jquery.min.js"></script> | ||||
| <script src="/static/js/bigscreen_v2.js"></script> | ||||
| </body> | ||||
| </html> | ||||
|  | @ -834,7 +834,7 @@ class IndustryAnalyzer: | |||
|      | ||||
|     def batch_calculate_industry_crowding(self, industries: List[str], concepts: List[str] = None) -> None: | ||||
|         """ | ||||
|         批量计算多个行业和概念板块的拥挤度指标 | ||||
|         批量计算多个行业和概念板块的拥挤度指标,并生成个股关联表 | ||||
|          | ||||
|         Args: | ||||
|             industries: 行业列表 | ||||
|  | @ -878,7 +878,9 @@ class IndustryAnalyzer: | |||
|                     if stocks: | ||||
|                         concept_stocks[concept] = stocks | ||||
|              | ||||
|             # 5. 批量计算行业拥挤度 | ||||
|             # 5. 批量计算行业拥挤度并生成个股关联数据 | ||||
|             industry_crowding_data = [] | ||||
|              | ||||
|             for industry, stocks in industry_stocks.items(): | ||||
|                 try: | ||||
|                     # 计算行业成交额 | ||||
|  | @ -911,6 +913,23 @@ class IndustryAnalyzer: | |||
|                         ex=86400 | ||||
|                     ) | ||||
|                      | ||||
|                     # 获取最新的拥挤度数据 | ||||
|                     latest_data = df.iloc[-1] if len(df) > 0 else None | ||||
|                     if latest_data is not None: | ||||
|                         # 生成信号 | ||||
|                         crowding_value = latest_data['percentile'] | ||||
|                         signal = self._generate_crowding_signal(crowding_value) | ||||
|                          | ||||
|                         # 为每个股票生成关联记录 | ||||
|                         for stock_code in stocks: | ||||
|                             industry_crowding_data.append({ | ||||
|                                 'stock_code': stock_code, | ||||
|                                 'industry_name': industry, | ||||
|                                 'crowding_value': crowding_value, | ||||
|                                 'trade_signal': signal, | ||||
|                                 'last_trade_date': latest_data['trade_date'] | ||||
|                             }) | ||||
|                      | ||||
|                     logger.info(f"成功计算行业 {industry} 的拥挤度指标,共 {len(df)} 条记录") | ||||
|                 except Exception as e: | ||||
|                     logger.error(f"计算行业 {industry} 的拥挤度指标时出错: {str(e)}") | ||||
|  | @ -954,9 +973,105 @@ class IndustryAnalyzer: | |||
|                     except Exception as e: | ||||
|                         logger.error(f"计算概念板块 {concept} 的拥挤度指标时出错: {str(e)}") | ||||
|                         continue | ||||
|              | ||||
|             # 7. 更新行业拥挤度个股关联表 | ||||
|             if industry_crowding_data: | ||||
|                 self._update_industry_crowding_stocks_table(industry_crowding_data) | ||||
|                          | ||||
|         except Exception as e: | ||||
|             logger.error(f"批量计算行业拥挤度指标失败: {str(e)}")  | ||||
|             logger.error(f"批量计算行业拥挤度指标失败: {str(e)}") | ||||
|      | ||||
|     def _generate_crowding_signal(self, crowding_value: float) -> str: | ||||
|         """ | ||||
|         根据拥挤度数值生成信号 | ||||
|          | ||||
|         Args: | ||||
|             crowding_value: 拥挤度数值(百分比) | ||||
|              | ||||
|         Returns: | ||||
|             信号字符串 | ||||
|         """ | ||||
|         if crowding_value < 10: | ||||
|             return "强烈买入" | ||||
|         elif crowding_value < 20: | ||||
|             return "买入" | ||||
|         elif crowding_value > 90: | ||||
|             return "强烈卖出" | ||||
|         elif crowding_value > 80: | ||||
|             return "卖出" | ||||
|         else: | ||||
|             return "中性" | ||||
|      | ||||
| 
 | ||||
|      | ||||
|     def _update_industry_crowding_stocks_table(self, industry_crowding_data: List[Dict]) -> None: | ||||
|         """ | ||||
|         更新行业拥挤度个股关联表 | ||||
|          | ||||
|         Args: | ||||
|             industry_crowding_data: 行业拥挤度数据列表 | ||||
|         """ | ||||
|         try: | ||||
|             if not industry_crowding_data: | ||||
|                 logger.warning("没有行业拥挤度数据需要更新") | ||||
|                 return | ||||
|              | ||||
|             # 清空现有数据 | ||||
|             delete_query = "DELETE FROM industry_crowding_stocks" | ||||
|              | ||||
|             # 构建插入语句 | ||||
|             insert_query = """ | ||||
|                 INSERT INTO industry_crowding_stocks  | ||||
|                 (stock_code, industry_name, crowding_value, trade_signal, last_trade_date)  | ||||
|                 VALUES (:stock_code, :industry_name, :crowding_value, :trade_signal, :last_trade_date) | ||||
|             """ | ||||
|              | ||||
|             inserted_count = 0 | ||||
|              | ||||
|             with self.engine.connect() as conn: | ||||
|                 # 开始事务 | ||||
|                 trans = conn.begin() | ||||
|                 try: | ||||
|                     # 清空表 | ||||
|                     conn.execute(text(delete_query)) | ||||
|                      | ||||
|                     # 逐条插入数据 | ||||
|                     for item in industry_crowding_data: | ||||
|                         # 检查该股票和行业的组合是否已存在 | ||||
|                         check_query = text(""" | ||||
|                         SELECT COUNT(*) FROM industry_crowding_stocks  | ||||
|                         WHERE stock_code = :stock_code AND industry_name = :industry_name | ||||
|                         """) | ||||
|                         result = conn.execute(check_query, { | ||||
|                             "stock_code": item['stock_code'],  | ||||
|                             "industry_name": item['industry_name'] | ||||
|                         }).scalar() | ||||
|                          | ||||
|                         if result > 0:  # 数据已存在,执行更新 | ||||
|                             update_query = text(""" | ||||
|                             UPDATE industry_crowding_stocks SET | ||||
|                                 crowding_value = :crowding_value, | ||||
|                                 trade_signal = :trade_signal, | ||||
|                                 last_trade_date = :last_trade_date | ||||
|                             WHERE stock_code = :stock_code AND industry_name = :industry_name | ||||
|                             """) | ||||
|                             conn.execute(update_query, item) | ||||
|                         else:  # 数据不存在,执行插入 | ||||
|                             conn.execute(text(insert_query), item) | ||||
|                          | ||||
|                         inserted_count += 1 | ||||
|                      | ||||
|                     # 提交事务 | ||||
|                     trans.commit() | ||||
|                      | ||||
|                     logger.info(f"成功更新行业拥挤度个股关联表,共 {inserted_count} 条记录") | ||||
|                 except Exception as e: | ||||
|                     # 回滚事务 | ||||
|                     trans.rollback() | ||||
|                     raise e | ||||
|                      | ||||
|         except Exception as e: | ||||
|             logger.error(f"更新行业拥挤度个股关联表失败: {str(e)}")  | ||||
|      | ||||
|     def filter_crowding_by_percentile(self, min_percentile: float, max_percentile: float) -> dict: | ||||
|         """ | ||||
|  |  | |||
|  | @ -0,0 +1,278 @@ | |||
| """ | ||||
| 重要提醒服务模块 | ||||
| 
 | ||||
| 提供动态的重要提醒信息生成功能,包括: | ||||
| 1. 行业拥挤度风险提醒 | ||||
| 2. 行业持仓占比风险提醒 | ||||
| 3. 个股持仓风险提醒 | ||||
| 4. 融资融券风险提醒 | ||||
| 5. 市场恐贪指数风险提醒 | ||||
| """ | ||||
| 
 | ||||
| import logging | ||||
| import requests | ||||
| from typing import Dict, List, Optional | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from .portfolio_analyzer import PortfolioAnalyzer | ||||
| from .industry_analysis import IndustryAnalyzer | ||||
| from .eastmoney_rzrq_collector import EastmoneyRzrqCollector | ||||
| from .fear_greed_index import FearGreedIndexManager | ||||
| 
 | ||||
| # 配置日志 | ||||
| logger = logging.getLogger("notice_service") | ||||
| 
 | ||||
| 
 | ||||
| class NoticeService: | ||||
|     """重要提醒服务类""" | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         """初始化提醒服务""" | ||||
|         self.portfolio_analyzer = PortfolioAnalyzer() | ||||
|         self.industry_analyzer = IndustryAnalyzer() | ||||
|         self.rzrq_collector = EastmoneyRzrqCollector() | ||||
|         self.fear_greed_manager = FearGreedIndexManager() | ||||
|         logger.info("重要提醒服务初始化完成") | ||||
| 
 | ||||
|     def get_dynamic_notices(self) -> Dict: | ||||
|         """ | ||||
|         获取动态的重要提醒列表 | ||||
|          | ||||
|         Returns: | ||||
|             包含提醒信息的字典 | ||||
|         """ | ||||
|         try: | ||||
|             notices = [] | ||||
|              | ||||
|             # 1. 检查行业拥挤度风险 | ||||
|             crowding_notices = self._check_industry_crowding_risk() | ||||
|             notices.extend(crowding_notices) | ||||
|              | ||||
|             # 2. 检查行业持仓占比风险 | ||||
|             allocation_notices = self._check_industry_allocation_risk() | ||||
|             notices.extend(allocation_notices) | ||||
|              | ||||
|             # 3. 检查个股持仓风险 | ||||
|             stock_notices = self._check_stock_holding_risk() | ||||
|             notices.extend(stock_notices) | ||||
|              | ||||
|             # 4. 检查融资融券风险 | ||||
|             rzrq_notices = self._check_rzrq_risk() | ||||
|             notices.extend(rzrq_notices) | ||||
|              | ||||
|             # 5. 检查市场恐贪指数风险 | ||||
|             fear_greed_notices = self._check_fear_greed_risk() | ||||
|             notices.extend(fear_greed_notices) | ||||
|              | ||||
|             # 如果没有风险提醒,添加一些市场信息 | ||||
|             if not notices: | ||||
|                 notices = self._get_market_info_notices() | ||||
|              | ||||
|             return { | ||||
|                 "success": True, | ||||
|                 "data": notices | ||||
|             } | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"获取动态提醒失败: {e}") | ||||
|             return { | ||||
|                 "success": False, | ||||
|                 "message": f"获取动态提醒失败: {str(e)}", | ||||
|                 "data": [] | ||||
|             } | ||||
| 
 | ||||
|     def _check_industry_crowding_risk(self) -> List[str]: | ||||
|         """ | ||||
|         检查行业拥挤度风险 | ||||
|          | ||||
|         Returns: | ||||
|             拥挤度风险提醒列表 | ||||
|         """ | ||||
|         notices = [] | ||||
|          | ||||
|         try: | ||||
|             # 获取持仓行业数据 | ||||
|             portfolio_result = self.portfolio_analyzer.analyze_portfolio_allocation() | ||||
|             if not portfolio_result.get("success"): | ||||
|                 return notices | ||||
|              | ||||
|             industries = portfolio_result["data"]["industries"] | ||||
|              | ||||
|             # 检查每个持仓行业的拥挤度 | ||||
|             for industry_data in industries: | ||||
|                 industry_name = industry_data["industry"] | ||||
|                  | ||||
|                 # 获取行业拥挤度数据 | ||||
|                 crowding_result = self.industry_analyzer.get_industry_analysis( | ||||
|                     industry_name, "pe", None | ||||
|                 ) | ||||
|                  | ||||
|                 if crowding_result.get("success") and "crowding" in crowding_result: | ||||
|                     crowding_data = crowding_result["crowding"] | ||||
|                     current_percentile = crowding_data["current"]["percentile"] | ||||
|                      | ||||
|                     if current_percentile >= 80: | ||||
|                         notices.append(f"⚠️ {industry_name}拥挤度过高({current_percentile:.1f}%),建议减仓") | ||||
|                     elif current_percentile <= 20: | ||||
|                         notices.append(f"💰 {industry_name}拥挤度较低({current_percentile:.1f}%),可考虑加仓") | ||||
|                          | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查行业拥挤度风险失败: {e}") | ||||
|          | ||||
|         return notices | ||||
| 
 | ||||
|     def _check_industry_allocation_risk(self) -> List[str]: | ||||
|         """ | ||||
|         检查行业持仓占比风险 | ||||
|          | ||||
|         Returns: | ||||
|             行业持仓占比风险提醒列表 | ||||
|         """ | ||||
|         notices = [] | ||||
|          | ||||
|         try: | ||||
|             # 获取持仓行业数据 | ||||
|             portfolio_result = self.portfolio_analyzer.analyze_portfolio_allocation() | ||||
|             if not portfolio_result.get("success"): | ||||
|                 return notices | ||||
|              | ||||
|             industries = portfolio_result["data"]["industries"] | ||||
|             total_amount = portfolio_result["data"]["total_amount"] | ||||
|              | ||||
|             # 检查行业持仓占比 | ||||
|             for industry_data in industries: | ||||
|                 industry_name = industry_data["industry"] | ||||
|                 industry_amount = industry_data["amount"] | ||||
|                 industry_ratio = (industry_amount / total_amount) * 100 | ||||
|                  | ||||
|                 if industry_ratio > 50: | ||||
|                     notices.append(f"⚠️ {industry_name}持仓占比过高({industry_ratio:.1f}%),建议分散风险") | ||||
|                      | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查行业持仓占比风险失败: {e}") | ||||
|          | ||||
|         return notices | ||||
| 
 | ||||
|     def _check_stock_holding_risk(self) -> List[str]: | ||||
|         """ | ||||
|         检查个股持仓风险 | ||||
|          | ||||
|         Returns: | ||||
|             个股持仓风险提醒列表 | ||||
|         """ | ||||
|         notices = [] | ||||
|          | ||||
|         try: | ||||
|             # 获取持仓摘要数据 | ||||
|             summary_result = self.portfolio_analyzer.get_portfolio_summary() | ||||
|             if not summary_result.get("success"): | ||||
|                 return notices | ||||
|              | ||||
|             project_details = summary_result["data"]["project_details"] | ||||
|              | ||||
|             # 检查个股保证金金额 | ||||
|             for project in project_details: | ||||
|                 project_name = project["project_name"] | ||||
|                 margin_amount = project["margin_amount"] | ||||
|                  | ||||
|                 if margin_amount > 2000000:  # 200万 | ||||
|                     notices.append(f"⚠️ {project_name}保证金过高({margin_amount/10000:.1f}万),建议控制仓位") | ||||
|                      | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查个股持仓风险失败: {e}") | ||||
|          | ||||
|         return notices | ||||
| 
 | ||||
|     def _check_rzrq_risk(self) -> List[str]: | ||||
|         """ | ||||
|         检查融资融券风险 | ||||
|          | ||||
|         Returns: | ||||
|             融资融券风险提醒列表 | ||||
|         """ | ||||
|         notices = [] | ||||
|          | ||||
|         try: | ||||
|             # 获取融资融券数据 | ||||
|             rzrq_result = self.rzrq_collector.get_chart_data(limit_days=90) | ||||
|             if not rzrq_result.get("success"): | ||||
|                 return notices | ||||
|              | ||||
|             # 计算当前值在历史数据中的百分位 | ||||
|             series_data = rzrq_result["series"] | ||||
|             if not series_data: | ||||
|                 return notices | ||||
|              | ||||
|             main_series = series_data[0]  # 融资融券余额合计 | ||||
|             data_values = main_series["data"] | ||||
|              | ||||
|             # 过滤有效数据 | ||||
|             valid_data = [v for v in data_values if v is not None and v != 0] | ||||
|             if not valid_data: | ||||
|                 return notices | ||||
|              | ||||
|             current_value = data_values[-1] | ||||
|             sorted_data = sorted(valid_data) | ||||
|             current_index = next((i for i, v in enumerate(sorted_data) if v >= current_value), len(sorted_data)) | ||||
|             percentile = (current_index / len(sorted_data)) * 100 | ||||
|              | ||||
|             if percentile >= 80: | ||||
|                 notices.append(f"⚠️ 融资融券余额处于高位({percentile:.1f}%),市场情绪过热") | ||||
|             elif percentile <= 20: | ||||
|                 notices.append(f"💰 融资融券余额处于低位({percentile:.1f}%),市场情绪低迷") | ||||
|                  | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查融资融券风险失败: {e}") | ||||
|          | ||||
|         return notices | ||||
| 
 | ||||
|     def _check_fear_greed_risk(self) -> List[str]: | ||||
|         """ | ||||
|         检查市场恐贪指数风险 | ||||
|          | ||||
|         Returns: | ||||
|             市场恐贪指数风险提醒列表 | ||||
|         """ | ||||
|         notices = [] | ||||
|          | ||||
|         try: | ||||
|             # 获取恐贪指数数据 | ||||
|             fear_greed_result = self.fear_greed_manager.get_index_data(None, None, 180) | ||||
|             if not fear_greed_result.get("success"): | ||||
|                 return notices | ||||
|              | ||||
|             values = fear_greed_result["values"] | ||||
|             if not values: | ||||
|                 return notices | ||||
|              | ||||
|             current_value = values[-1] | ||||
|              | ||||
|             if current_value >= 80: | ||||
|                 notices.append(f"⚠️ 市场恐贪指数过高({current_value:.1f}),市场情绪贪婪") | ||||
|             elif current_value <= 20: | ||||
|                 notices.append(f"💰 市场恐贪指数过低({current_value:.1f}),市场情绪恐惧") | ||||
|                  | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查市场恐贪指数风险失败: {e}") | ||||
|          | ||||
|         return notices | ||||
| 
 | ||||
|     def _get_market_info_notices(self) -> List[str]: | ||||
|         """ | ||||
|         获取市场信息提醒(当没有风险提醒时使用) | ||||
|          | ||||
|         Returns: | ||||
|             市场信息提醒列表 | ||||
|         """ | ||||
|         return [ | ||||
|             "📈 上证指数突破3200点,市场情绪回暖", | ||||
|             "💰 北向资金今日净流入85.6亿元", | ||||
|             "📊 科技板块PE估值处于历史低位", | ||||
|             "🔥 新能源概念股集体上涨,涨幅超3%", | ||||
|             "⚠️ 医药板块回调,建议关注低吸机会", | ||||
|             "📈 融资融券余额连续三日增长", | ||||
|             "💰 消费板块资金流入明显", | ||||
|             "📊 市场恐贪指数回升至65", | ||||
|             "🤖 机器人概念板块技术面突破", | ||||
|             "📦 先进封装概念获政策支持" | ||||
|         ]  | ||||
|  | @ -224,7 +224,7 @@ class PortfolioAnalyzer: | |||
|                 } | ||||
|             } | ||||
|              | ||||
|             logger.info(f"成功分析持仓行业分配,总金额: {total_amount:.2f}万元,共{len(industries_data)}个行业") | ||||
|             logger.info(f"成功分析持仓行业分配,总金额: {total_amount:.2f}元,共{len(industries_data)}个行业") | ||||
|             return result | ||||
|              | ||||
|         except Exception as e: | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue