From c4c4e8622fc90355f0a9f4b06c77c38e3d2924a0 Mon Sep 17 00:00:00 2001 From: liao Date: Fri, 8 Aug 2025 16:56:02 +0800 Subject: [PATCH] commit; --- src/app.py | 51 +- src/fundamentals_llm/fundamental_analysis.py | 2 +- src/scripts/config.py | 2 +- src/static/js/bigscreen_v2.js | 1195 ++++++++++++++++++ src/templates/bigscreen.html | 17 - src/templates/bigscreen_v2.html | 781 ++++++++++++ src/valuation_analysis/industry_analysis.py | 121 +- src/valuation_analysis/notice_service.py | 278 ++++ src/valuation_analysis/portfolio_analyzer.py | 2 +- 9 files changed, 2408 insertions(+), 41 deletions(-) create mode 100644 src/static/js/bigscreen_v2.js create mode 100644 src/templates/bigscreen_v2.html create mode 100644 src/valuation_analysis/notice_service.py diff --git a/src/app.py b/src/app.py index d10e65a..7690daa 100644 --- a/src/app.py +++ b/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)}) diff --git a/src/fundamentals_llm/fundamental_analysis.py b/src/fundamentals_llm/fundamental_analysis.py index c020b66..00fde66 100644 --- a/src/fundamentals_llm/fundamental_analysis.py +++ b/src/fundamentals_llm/fundamental_analysis.py @@ -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") diff --git a/src/scripts/config.py b/src/scripts/config.py index aaff9b9..74562f7 100644 --- a/src/scripts/config.py +++ b/src/scripts/config.py @@ -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', diff --git a/src/static/js/bigscreen_v2.js b/src/static/js/bigscreen_v2.js new file mode 100644 index 0000000..2f98dd8 --- /dev/null +++ b/src/static/js/bigscreen_v2.js @@ -0,0 +1,1195 @@ +$(function() { + // 1. 行业持仓占比 + $.get('/api/portfolio/industry_allocation', function(res) { + if(res.status === 'success') { + renderPortfolioChart(res.data); + } else { + $('#portfolioChart').html('
暂无持仓数据
'); + } + }).fail(function() { + $('#portfolioChart').html('
暂无持仓数据
'); + }); + + // 2. 重要提醒数据 + $.get('/api/notice/list', function(res) { + if(res.status === 'success' && res.data && res.data.length > 0) { + updateNoticeBox(res.data); + } else { + // 如果没有数据,显示暂无提醒 + updateNoticeBox([]); + } + }).fail(function() { + // 请求失败时,显示暂无提醒 + updateNoticeBox([]); + }); + + // 3. 融资融券/恐贪指数 + $.get('/api/rzrq/chart_data?days=90', function(res) { + if(res.status === 'success') renderRzrqChart(res.data); + }); + $.get('/api/fear_greed/data?limit=180', function(res) { + if(res.status === 'success') renderFearGreedChart(res.data); + }); + + // 4. 行业PE和拥挤度(基于持仓行业) + function loadIndustryAnalysis() { + // 首先获取持仓数据 + $.get('/api/portfolio/industry_allocation', function(res) { + if(res.status === 'success') { + const portfolioIndustries = res.data.industries.map(item => item.industry); + + // 定义补齐的行业列表 + const defaultIndustries = ['半导体', '消费电子', '自动化设备', '光学光电']; + + // 合并持仓行业和默认行业,去重 + let allIndustries = [...new Set([...portfolioIndustries, ...defaultIndustries])]; + + // 取前四个行业 + const selectedIndustries = allIndustries.slice(0, 4); + + // 定义图表ID映射 + const chartIds = [ + {peId: "peChart_xjfz", crowdId: "crowdChart_xjfz", holdingsId: "holdings_xjfz"}, + {peId: "peChart_xp", crowdId: "crowdChart_xp", holdingsId: "holdings_xp"}, + {peId: "peChart_xfdz", crowdId: "crowdChart_xfdz", holdingsId: "holdings_xfdz"}, + {peId: "peChart_jqr", crowdId: "crowdChart_jqr", holdingsId: "holdings_jqr"} + ]; + + // 更新HTML标题 + updateIndustryTitles(selectedIndustries); + + // 加载持仓摘要数据 + $.get('/api/portfolio/summary', function(summaryRes) { + if(summaryRes.status === 'success') { + const projectDetails = summaryRes.data.project_details; + + // 加载每个行业的分析数据 + selectedIndustries.forEach((industry, index) => { + const chartId = chartIds[index]; + + // 加载PE和拥挤度数据 + $.get(`/api/industry/analysis?industry_name=${encodeURIComponent(industry)}&metric=pe`, function(res) { + if(res.status === 'success') { + renderPEChart(chartId.peId, res.data); + renderCrowdChart(chartId.crowdId, res.data.crowding); + } + }); + + // 加载持仓标的数据 + loadIndustryHoldings(industry, chartId.holdingsId, projectDetails); + }); + } + }); + } + }); + } + + function loadIndustryHoldings(industry, holdingsId, projectDetails) { + // 筛选该行业的持仓标的 + const industryHoldings = projectDetails.filter(item => item.industry === industry); + + // 按持仓金额排序,取前三名 + const topHoldings = industryHoldings + .sort((a, b) => b.margin_amount - a.margin_amount) + .slice(0, 3); + + // 渲染持仓标的 + renderHoldings(holdingsId, topHoldings, industry); + } + + function renderHoldings(holdingsId, holdings, industry) { + const container = document.getElementById(holdingsId); + if (!container) return; + + if (holdings.length === 0) { + container.innerHTML = '
暂无持仓
'; + return; + } + + let html = ''; + holdings.forEach(holding => { + const amountInWan = Math.round(holding.margin_amount / 10000); + html += ` +
+
${holding.project_name}
+
${amountInWan}万
+
+ `; + }); + + container.innerHTML = html; + } + + function updateIndustryTitles(industries) { + const titles = [ + {peTitle: "peChart_xjfz", crowdTitle: "crowdChart_xjfz", holdingsTitle: "holdings_xjfz"}, + {peTitle: "peChart_xp", crowdTitle: "crowdChart_xp", holdingsTitle: "holdings_xp"}, + {peTitle: "peChart_xfdz", crowdTitle: "crowdChart_xfdz", holdingsTitle: "holdings_xfdz"}, + {peTitle: "peChart_jqr", crowdTitle: "crowdChart_jqr", holdingsTitle: "holdings_jqr"} + ]; + + industries.forEach((industry, index) => { + const title = titles[index]; + if (title) { + // 更新PE分析标题 + const peTitleElement = document.querySelector(`#${title.peTitle}`).parentElement.querySelector('.chart-title'); + if (peTitleElement) { + peTitleElement.textContent = `${industry}-历史PE分析`; + } + + // 更新拥挤度标题 - 使用更精确的选择器 + const crowdContainer = document.querySelector(`#${title.crowdTitle}`).parentElement; + const allTitles = crowdContainer.querySelectorAll('.chart-title'); + if (allTitles.length >= 2) { + // 第二个.chart-title是拥挤度的标题 + allTitles[1].textContent = `${industry}-拥挤度`; + } + + // 更新持仓标的标题 + const holdingsContainer = document.querySelector(`#${title.holdingsTitle}`).parentElement; + const holdingsTitles = holdingsContainer.querySelectorAll('.chart-title'); + if (holdingsTitles.length >= 3) { + // 第三个.chart-title是持仓标的的标题 + holdingsTitles[2].textContent = `${industry}-持仓标的`; + } + } + }); + } + + // 调用行业分析加载函数 + loadIndustryAnalysis(); + + // 绑定重置按钮事件 + $('#resetViewBtn').on('click', function() { + resetToDefaultView(); + }); + + // 重置到默认视图 + function resetToDefaultView() { + // 清除全局变量 + window.currentSelectedIndustry = null; + + // 隐藏重置按钮 + $('#resetViewBtn').hide(); + + // 恢复第二行的原始布局 + const rowContainer = document.querySelector('.row.d-flex2'); + rowContainer.innerHTML = ` +
+
+
行业1-历史PE分析
+
+
行业1-拥挤度
+
+
+
+
+
+
+
行业2-历史PE分析
+
+
行业2-拥挤度
+
+
+
+
+
+
+
行业3-历史PE分析
+
+
行业3-拥挤度
+
+
+
+
+
+
+
行业4-历史PE分析
+
+
行业4-拥挤度
+
+
+
+
+ `; + + // 重新加载默认的四个行业分析 + loadIndustryAnalysis(); + + // 移除动态创建的容器 + ['stockDetailContainer', 'factorContainer', 'holdingContainer', 'correctionContainer'].forEach(id => { + const element = document.getElementById(id); + if (element) { + element.remove(); + } + }); + } + + // 加载行业详情函数 + function loadIndustryDetail(industryName) { + // 设置全局变量,记录当前选中的行业 + window.currentSelectedIndustry = industryName; + + // 显示重置按钮 + $('#resetViewBtn').show(); + + // 直接更新持仓详情,因为updateSecondContainer会重新构建整个第二行 + updateHoldingsDetails(industryName); + } + + // 这个函数已经不再使用,删除 + + function updateHoldingsDetails(industryName) { + // 获取行业持仓数据 + $.get(`/api/portfolio/industry_holdings?industry_name=${encodeURIComponent(industryName)}`, function(res) { + if(res.status === 'success') { + const holdings = res.data.holdings; + const stockCodes = [...new Set(holdings.map(item => item.stock_code))]; + + // 更新其他三个容器为合并的详细信息 + updateSecondContainer(stockCodes, industryName); + updateThirdContainer(stockCodes); + updateFourthContainer(stockCodes); + } + }); + } + + function updateSecondContainer(stockCodes, industryName) { + // 获取第二行的容器 + const rowContainer = document.querySelector('.row.d-flex2'); + + // 清空第二行,只保留第一个容器 + rowContainer.innerHTML = ` +
+
+
${industryName}-历史PE分析
+
+
${industryName}-拥挤度
+
+
+
+
+
+
标的详情
+
+
+
+ `; + + // 重新加载第一个容器的PE和拥挤度数据 + $.get(`/api/industry/analysis?industry_name=${encodeURIComponent(industryName)}&metric=pe`, function(res) { + if(res.status === 'success') { + renderPEChart('peChart_xjfz', res.data); + renderCrowdChart('crowdChart_xjfz', res.data.crowding); + } + }); + + // 加载所有股票详情 + const detailContainer = document.getElementById('stockDetailContainer'); + loadStockDetails(stockCodes, detailContainer); + } + + function updateThirdContainer(stockCodes) { + // 这个函数不再使用,但保留以避免错误 + } + + function updateFourthContainer(stockCodes) { + // 这个函数不再使用,但保留以避免错误 + } + + function loadStockDetails(stockCodes, container) { + if (stockCodes.length === 0) { + container.innerHTML = '
暂无持仓数据
'; + return; + } + + // 显示加载状态 + container.innerHTML = '
加载中...
'; + + // 并行获取三种数据 + let factorData = null; + let holdingData = null; + let correctionData = {}; + let loadedCount = 0; + const totalRequests = 2 + stockCodes.length; // 因子数据 + 持仓数据 + 每个股票的修正数据 + + // 获取因子数据 + $.get('https://spb.bmbs.tech/api/dify/getStocksTriggerFactor?groupId=default_system_pool', function(res) { + if(res.status === 'success') { + factorData = res.factorMessages || []; + } + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }).fail(function() { + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }); + + // 获取持仓数据 + const currentIndustry = window.currentSelectedIndustry || ''; + $.get(`/api/portfolio/industry_holdings?industry_name=${encodeURIComponent(currentIndustry)}`, function(res) { + if(res.status === 'success') { + holdingData = res.data.holdings || []; + } + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }).fail(function() { + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }); + + // 获取每个股票的修正数据 + stockCodes.forEach(stockCode => { + const convertedCode = convertStockCode(stockCode); + $.get(`http://192.168.16.71:8000/corrections/history/${convertedCode}`, function(res) { + correctionData[stockCode] = res || []; + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }).fail(function() { + correctionData[stockCode] = []; + loadedCount++; + if (loadedCount === totalRequests) { + renderStockDetails(container, stockCodes, factorData, holdingData, correctionData); + } + }); + }); + } + + function renderStockDetails(container, stockCodes, factorData, holdingData, correctionData) { + if (stockCodes.length === 0) { + container.innerHTML = '
暂无持仓数据
'; + return; + } + + let html = '
'; + + stockCodes.forEach(stockCode => { + // 转换股票代码格式用于匹配因子数据 (600584.SH -> SH600584) + const factorCode = convertStockCodeForFactor(stockCode); + // 获取该股票的因子数据 + const factorInfo = factorData ? factorData.find(item => item.code === factorCode) : null; + + // 获取该股票的持仓数据 + const stockHoldings = holdingData ? holdingData.filter(item => item.stock_code === stockCode) : []; + + // 获取该股票的修正数据 + const stockCorrections = correctionData[stockCode] || []; + + // 从持仓数据中获取股票名称,如果没有则使用因子数据中的名称 + let stockName = '未知'; + if (stockHoldings.length > 0 && stockHoldings[0].project_name) { + stockName = stockHoldings[0].project_name; + } else if (factorInfo && factorInfo.stock) { + stockName = factorInfo.stock; + } + + html += ` +
+

${stockName} (${stockCode})

+ + +
+ +
+
持仓情况
+
+ `; + + if (stockHoldings.length > 0) { + stockHoldings.forEach((holding, index) => { + const notionalWan = Math.round(holding.notional_principal / 10000); + const marginWan = Math.round(holding.margin_amount / 10000); + const createTime = holding.create_time ? holding.create_time.split(' ')[0] : '未知'; + + html += ` +
+

交易${index + 1}: 名义本金${notionalWan}万, 保证金${marginWan}万, 建仓时间${createTime}

+
+ `; + }); + } else { + html += '

暂无持仓详情

'; + } + + html += ` +
+
+ + +
+
因子情况
+
+ `; + + if (factorInfo && factorInfo.details) { + Object.entries(factorInfo.details).forEach(([factor, value]) => { + html += `

${factor}: ${value}

`; + }); + } else { + html += '

暂无因子数据

'; + } + + html += ` +
+
+
+ + +
+
修正事件
+
+ `; + + if (stockCorrections.length > 0) { + stockCorrections.forEach((correction, index) => { + const createTime = correction.created_at ? correction.created_at.split('T')[0] : '未知'; + + // 根据target_field判断显示类型 + let fieldType = ''; + if (correction.target_field === 'prediction_peak') { + fieldType = '预测峰值'; + } else if (correction.target_field === 'prediction_trough') { + fieldType = '预测谷值'; + } else { + fieldType = correction.target_field; // 如果是其他字段,直接显示 + } + + html += ` +
+

${fieldType}: 修正数值为${correction.correction_value},修正理由${correction.reason},修正人${correction.corrected_by},修正时间${createTime}。

+
+ `; + }); + } else { + html += '

暂无修正记录

'; + } + + html += ` +
+
+
+ `; + }); + + html += '
'; + container.innerHTML = html; + } + + // 这个函数已经不再使用,删除 + + // 这个函数已经不再使用,删除 + + function convertStockCode(stockCode) { + // 将 600584.SH 格式转换为 SH600584 格式 + const parts = stockCode.split('.'); + if (parts.length === 2) { + return `${parts[1]}${parts[0]}`; + } + return stockCode; + } + + function convertStockCodeForFactor(stockCode) { + // 将 600584.SH 格式转换为 SH600584 格式 (用于匹配因子数据) + const parts = stockCode.split('.'); + if (parts.length === 2) { + return `${parts[1]}${parts[0]}`; + } + return stockCode; + } + + // 行业持仓详情弹窗相关函数 + function showIndustryHoldingsModal(industry) { + const modal = document.getElementById('industryHoldingsModal'); + const modalTitle = document.getElementById('industryHoldingsTitle'); + const modalContent = document.getElementById('industryHoldingsContent'); + + // 更新标题 + modalTitle.textContent = `${industry}持仓详情`; + + // 显示加载状态 + modalContent.innerHTML = '
加载中...
'; + + // 显示弹窗 + modal.style.display = 'flex'; + + // 阻止背景滚动 + document.body.style.overflow = 'hidden'; + + // 获取行业持仓详情 + $.get(`/api/portfolio/industry_holdings?industry_name=${encodeURIComponent(industry)}`, function(res) { + if(res.status === 'success') { + renderIndustryHoldingsDetail(modalContent, res.data); + } else { + modalContent.innerHTML = `
${res.message || '获取数据失败'}
`; + } + }).fail(function() { + modalContent.innerHTML = '
网络错误,请稍后重试
'; + }); + } + + function closeIndustryHoldingsModal() { + const modal = document.getElementById('industryHoldingsModal'); + modal.style.display = 'none'; + + // 恢复背景滚动 + document.body.style.overflow = 'auto'; + } + + function renderIndustryHoldingsDetail(container, data) { + const { industry_name, total_count, total_margin_amount, holdings } = data; + + let html = ` +
+

${industry_name}持仓概览

+

持仓标的数量: ${total_count}个

+

总保证金金额: ${Math.round(total_margin_amount/10000)}万元

+
+ `; + + if (holdings.length > 0) { + // 按股票代码聚合数据 + const stockGroups = {}; + holdings.forEach(holding => { + const stockCode = holding.stock_code; + if (!stockGroups[stockCode]) { + stockGroups[stockCode] = { + project_name: holding.project_name, + stock_code: stockCode, + notional_principal: 0, + margin_amount: 0, + details: [], + last_create_time: '' + }; + } + stockGroups[stockCode].notional_principal += holding.notional_principal; + stockGroups[stockCode].margin_amount += holding.margin_amount; + stockGroups[stockCode].details.push(holding); + + // 更新最后建仓时间 + const createTime = holding.create_time || ''; + if (createTime > stockGroups[stockCode].last_create_time) { + stockGroups[stockCode].last_create_time = createTime; + } + }); + + // 转换为数组并排序 + const stockList = Object.values(stockGroups).sort((a, b) => b.margin_amount - a.margin_amount); + + html += ` + + + + + + + + + + + + + `; + + stockList.forEach((stock, index) => { + const notionalWan = Math.round(stock.notional_principal / 10000); + const marginWan = Math.round(stock.margin_amount / 10000); + const lastCreateTime = stock.last_create_time ? stock.last_create_time.split(' ')[0] : '未知'; + + html += ` + + + + + + + + + + + + `; + }); + + html += ` + +
个股名称股票代码名义本金(万元)保证金率(%)保证金金额(万元)最后建仓时间
${stock.project_name}${stock.stock_code}${notionalWan}-${marginWan}${lastCreateTime}
+
+
${stock.project_name} (${stock.stock_code}) 交易详情
+ + + + + + + + + + + + `; + + // 按建仓时间倒序排列详细交易记录 + const sortedDetails = stock.details.sort((a, b) => { + const timeA = a.create_time || ''; + const timeB = b.create_time || ''; + return timeB.localeCompare(timeA); // 倒序排列 + }); + + sortedDetails.forEach((detail, detailIndex) => { + const detailNotionalWan = Math.round(detail.notional_principal / 10000); + const detailMarginWan = Math.round(detail.margin_amount / 10000); + const createTime = detail.create_time ? detail.create_time.split(' ')[0] : '未知'; + + html += ` + + + + + + + + `; + }); + + html += ` + +
交易序号名义本金(万元)保证金率(%)保证金金额(万元)建仓时间
交易${detailIndex + 1}${detailNotionalWan}${detail.margin_rate}${detailMarginWan}${createTime}
+
+
+ `; + } else { + html += '
暂无持仓数据
'; + } + + container.innerHTML = html; + + // 绑定展开/收起事件 + bindExpandEvents(); + } + + function bindExpandEvents() { + const stockRows = document.querySelectorAll('.stock-row'); + stockRows.forEach(row => { + row.addEventListener('click', function() { + const stockCode = this.getAttribute('data-stock'); + const detailRow = document.getElementById(`detail-${stockCode}`); + const expandIcon = this.querySelector('.expand-icon'); + + if (detailRow.classList.contains('show')) { + // 收起 + detailRow.classList.remove('show'); + expandIcon.classList.remove('expanded'); + } else { + // 展开 + detailRow.classList.add('show'); + expandIcon.classList.add('expanded'); + } + }); + }); + } + + // 绑定行业持仓点击事件 + function bindIndustryHoldingsClick() { + // 为每个持仓容器添加点击事件 + const holdingsContainers = ['holdings_xjfz', 'holdings_xp', 'holdings_xfdz', 'holdings_jqr']; + const industries = ['半导体', '自动化设备', '军工电子', '消费电子']; // 默认行业,实际会根据持仓动态更新 + + holdingsContainers.forEach((containerId, index) => { + const container = document.getElementById(containerId); + if (container) { + container.style.cursor = 'pointer'; + container.addEventListener('click', function() { + // 获取当前显示的行业名称 + const industryTitle = this.parentElement.querySelector('.chart-title'); + if (industryTitle) { + const industryName = industryTitle.textContent.split('-')[0]; + showIndustryHoldingsModal(industryName); + } + }); + } + }); + } + + // 在页面加载完成后绑定点击事件 + setTimeout(bindIndustryHoldingsClick, 2000); // 延迟绑定,确保数据加载完成 + + // --- 渲染函数 --- + const chartInstances = {}; + + function getDefaultNotices() { + return [ + "上证指数突破3200点,市场情绪回暖", + "北向资金今日净流入85.6亿元", + "科技板块PE估值处于历史低位", + "新能源概念股集体上涨,涨幅超3%", + "医药板块回调,建议关注低吸机会", + "融资融券余额连续三日增长", + "消费板块资金流入明显", + "市场恐贪指数回升至65", + "机器人概念板块技术面突破", + "先进封装概念获政策支持" + ]; + } + + function updateNoticeBox(notices) { + const noticeContent = document.querySelector('.notice-content'); + if (!noticeContent) return; + + let html = ''; + + // 检查是否有提醒数据 + if (!notices || notices.length === 0) { + html = '
暂无重要提醒
'; + } else { + // 只显示一次内容,不再重复 + notices.forEach(notice => { + html += `
${notice}
`; + }); + } + + noticeContent.innerHTML = html; + + // 添加鼠标滚轮事件监听 + const noticeContainer = document.getElementById('noticeBox'); + if (noticeContainer) { + noticeContainer.addEventListener('wheel', function(e) { + e.preventDefault(); // 阻止默认滚动行为 + + const scrollAmount = e.deltaY > 0 ? 30 : -30; // 向下滚动30px,向上滚动-30px + noticeContainer.scrollTop += scrollAmount; + }); + } + } + + function renderPortfolioChart(data) { + if(!data || !data.industries || !data.total_amount) return; + const dom = document.getElementById('portfolioChart'); + const chart = echarts.init(dom); + chartInstances['portfolioChart'] = chart; + + // 更新HTML标题 + const titleElement = dom.parentElement.querySelector('.chart-title'); + if (titleElement) { + titleElement.textContent = `行业持仓占比 (总持仓: ${Math.round(data.total_amount/10000)}万元)`; + } + + // 准备饼图数据 + const pieData = data.industries.map(item => ({ + name: item.industry, + value: item.amount, + itemStyle: { + color: item.color + } + })); + + chart.setOption({ + title: { + show: false + }, + tooltip: { + trigger: 'item', + formatter: function(params) { + const percentage = ((params.value / data.total_amount) * 100).toFixed(1); + return `${params.name}
持仓金额: ${Math.round(params.value/10000)}万元
占比: ${percentage}%`; + } + }, + legend: { + show: false + }, + series: [{ + name: '行业持仓', + type: 'pie', + radius: '60%', + center: ['50%', '55%'], + data: pieData, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + }, + label: { + show: true, + position: 'outside', + formatter: function(params) { + const percentage = ((params.value / data.total_amount) * 100).toFixed(1); + return `${params.name}\n${percentage}%`; + }, + fontSize: 12, + color: '#333' + }, + labelLine: { + show: true, + length: 15, + length2: 10, + smooth: true + } + }] + }); + + // 添加饼图点击事件 + chart.on('click', function(params) { + if (params.componentType === 'series') { + const clickedIndustry = params.name; + loadIndustryDetail(clickedIndustry); + + // 静默调用通知接口 + $.get(`https://spb.bmbs.tech/api/dify/webSelectStockIndustry?industry=${encodeURIComponent(clickedIndustry)}`) + .fail(function(xhr, status, error) { + // 静默处理错误,不显示给用户 + console.log('通知接口调用失败:', error); + }); + } + }); + + dom.onclick = function() { + for(const k in chartInstances) { + if(k !== 'portfolioChart' && chartInstances[k]) { + chartInstances[k].dispatchAction({ type: 'hideTip' }); + } + } + }; + } + function renderRzrqChart(data) { + if(!data || !data.dates || !data.series) return; + const dom = document.getElementById('rzrqChart'); + const chart = echarts.init(dom); + chartInstances['rzrqChart'] = chart; + const s = data.series[0]; + let min = Math.min(...s.data.filter(v => v !== null && v !== undefined)); + min = Math.floor(min * 0.98); + + // 计算当前值在历史数据中的百分位 + const validData = s.data.filter(v => v !== null && v !== undefined); + const currentValue = s.data[s.data.length - 1]; + const sortedData = validData.sort((a, b) => a - b); + const currentIndex = sortedData.findIndex(v => v >= currentValue); + const percentile = (currentIndex / sortedData.length) * 100; + + // 准备色带数据 + let markArea = []; + if (percentile >= 80) { + // 当前值在80%以上,显示红色色带标记80%-100% + const maxValue = Math.max(...validData); + const threshold80 = sortedData[Math.floor(sortedData.length * 0.8)]; + markArea.push([{ + yAxis: threshold80, + itemStyle: { + color: 'rgba(255, 0, 0, 0.2)' + } + }, { + yAxis: maxValue, + itemStyle: { + color: 'rgba(255, 0, 0, 0.2)' + } + }]); + } else if (percentile <= 20) { + // 当前值在20%以下,显示绿色色带标记0%-20% + const minValue = Math.min(...validData); + const threshold20 = sortedData[Math.floor(sortedData.length * 0.2)]; + markArea.push([{ + yAxis: minValue, + itemStyle: { + color: 'rgba(0, 255, 0, 0.2)' + } + }, { + yAxis: threshold20, + itemStyle: { + color: 'rgba(0, 255, 0, 0.2)' + } + }]); + } + + chart.setOption({ + title: {text: '', show: false}, + tooltip: {trigger: 'axis'}, + legend: {data: [s.name], top: 5, textStyle: {color:'#333'}}, + grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true}, + xAxis: {type: 'category', data: data.dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}}, + yAxis: {type: 'value', name: s.unit, axisLabel: {color:'#666'}, min: min}, + series: [{ + name: s.name, + type: 'line', + data: s.data, + symbol: 'none', + lineStyle:{width:2}, + markArea: markArea.length > 0 ? { + data: markArea + } : undefined + }] + }); + dom.onclick = function() { + for(const k in chartInstances) { + if(k !== 'rzrqChart' && chartInstances[k]) { + chartInstances[k].dispatchAction({ type: 'hideTip' }); + } + } + }; + } + function renderFearGreedChart(data) { + if(!data || !data.dates || !data.values) return; + const dom = document.getElementById('fearGreedChart'); + const chart = echarts.init(dom); + chartInstances['fearGreedChart'] = chart; + let min = Math.min(...data.values.filter(v => v !== null && v !== undefined)); + min = Math.floor(min * 0.98); + + // 获取当前值 + const currentValue = data.values[data.values.length - 1]; + + // 准备色带数据 - 直接使用固定的0-100范围 + let markArea = []; + if (currentValue >= 80) { + // 当前值在80以上,显示红色色带标记80-100 + markArea.push([{ + yAxis: 80, + itemStyle: { + color: 'rgba(255, 0, 0, 0.2)' + } + }, { + yAxis: 100, + itemStyle: { + color: 'rgba(255, 0, 0, 0.2)' + } + }]); + } else if (currentValue <= 20) { + // 当前值在20以下,显示绿色色带标记0-20 + markArea.push([{ + yAxis: 0, + itemStyle: { + color: 'rgba(0, 255, 0, 0.2)' + } + }, { + yAxis: 20, + itemStyle: { + color: 'rgba(0, 255, 0, 0.2)' + } + }]); + } + + chart.setOption({ + title: {text: '', show: false}, + tooltip: {trigger: 'axis'}, + legend: {data: ['恐贪指数'], top: 5, textStyle: {color:'#333'}}, + grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true}, + xAxis: {type: 'category', data: data.dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}}, + yAxis: {type: 'value', min: min, max: 100, axisLabel: {color:'#666'}}, + series: [{ + name: '恐贪指数', + type: 'line', + data: data.values, + symbol: 'none', + lineStyle:{width:2, color:'#f0ad4e'}, + markArea: markArea.length > 0 ? { + data: markArea + } : undefined + }] + }); + dom.onclick = function() { + for(const k in chartInstances) { + if(k !== 'fearGreedChart' && chartInstances[k]) { + chartInstances[k].dispatchAction({ type: 'hideTip' }); + } + } + }; + } + function renderPEChart(domId, data) { + if(!data || !data.series || !data.xAxis || !data.xAxis[0] || !data.xAxis[0].data) return; + const dom = document.getElementById(domId); + const chart = echarts.init(dom); + chartInstances[domId] = chart; + const mainSeries = data.series.filter(s => s.name.indexOf('平均PE') !== -1 || s.name.indexOf('PE') !== -1); + mainSeries.forEach(s => { s.symbol = 'none'; }); + let allValues = []; + mainSeries.forEach(s => allValues = allValues.concat(s.data.filter(v => v !== null && v !== undefined))); + let min = Math.min(...allValues); + min = Math.floor(min * 0.98); + chart.setOption({ + title: {text: '', show: false}, + tooltip: {trigger: 'axis'}, + legend: {show: false}, + grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true}, + xAxis: {type: 'category', data: data.xAxis[0].data, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}}, + yAxis: {type: 'value', name: data.yAxis[0].name, axisLabel: {color:'#666'}, min: min}, + series: mainSeries, + dataZoom: [ + {type: 'inside', start: 0, end: 100, zoomOnTouch: true, moveOnMouseWheel: true} + ] + }); + dom.onclick = function() { + for(const k in chartInstances) { + if(k !== domId && chartInstances[k]) { + chartInstances[k].dispatchAction({ type: 'hideTip' }); + } + } + }; + } + function renderCrowdChart(domId, crowding) { + if(!crowding || !crowding.dates || !crowding.percentiles) return; + // 展示近一年(240天)数据 + let dates = crowding.dates; + let percentiles = crowding.percentiles; + if(dates.length > 240) { + dates = dates.slice(-240); + percentiles = percentiles.slice(-240); + } + const dom = document.getElementById(domId); + const chart = echarts.init(dom); + chartInstances[domId] = chart; + let min = Math.min(...percentiles.filter(v => v !== null && v !== undefined)); + min = Math.floor(min * 0.98); + // 检查最后一个点是否需要高亮 + let markPoint = undefined; + const lastVal = percentiles[percentiles.length-1]; + if(lastVal !== undefined && (lastVal > 80 || lastVal < 20)) { + markPoint = { + data: [{ + coord: [dates[dates.length-1], lastVal], + symbol: 'circle', + symbolSize: 16, + itemStyle: { + color: lastVal > 80 ? '#ff3333' : '#33cc33', + shadowBlur: 20, + shadowColor: lastVal > 80 ? '#ff3333' : '#33cc33', + opacity: 1 + }, + label: {show: false}, + animation: true, + animationDuration: 500, + animationEasing: 'bounceOut', + animationDurationUpdate: 500, + animationEasingUpdate: 'bounceOut', + effect: { + show: true, + period: 1, + scaleSize: 2, + color: lastVal > 80 ? '#ff3333' : '#33cc33', + shadowBlur: 10 + } + }] + }; + } + chart.setOption({ + title: {text: '', show: false}, + tooltip: {trigger: 'axis'}, + legend: {data: ['拥挤度历史百分位'], top: 5, textStyle: {color:'#333'}}, + grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true}, + xAxis: {type: 'category', data: dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}}, + yAxis: {type: 'value', min: min, max: 100, name: '百分位(%)', axisLabel: {color:'#666'}}, + series: [{ + name: '拥挤度历史百分位', + type: 'line', + data: percentiles, + symbol: 'none', + lineStyle:{width:2, color:'#ff7f50'}, + markPoint: markPoint + }], + dataZoom: [ + {type: 'inside', start: 0, end: 100, zoomOnTouch: true, moveOnMouseWheel: true} + ] + }); + dom.onclick = function() { + for(const k in chartInstances) { + if(k !== domId && chartInstances[k]) { + chartInstances[k].dispatchAction({ type: 'hideTip' }); + } + } + }; + } + + // 统一resize自适应 + $(window).on('resize', function() { + for(const key in chartInstances) { + if(chartInstances[key] && chartInstances[key].resize) { + chartInstances[key].resize(); + } + } + }); + +}); + +// 弹窗相关函数 - 移到全局作用域 +function showNoticeModal() { + const modal = document.getElementById('noticeModal'); + const modalContent = document.getElementById('modalNoticeContent'); + + // 获取当前所有提醒数据 + const notices = getCurrentNotices(); + + // 生成弹窗内容 + let html = ''; + notices.forEach(notice => { + html += ``; + }); + modalContent.innerHTML = html; + + // 显示弹窗 + modal.style.display = 'flex'; + + // 阻止背景滚动 + document.body.style.overflow = 'hidden'; +} + +function closeNoticeModal() { + const modal = document.getElementById('noticeModal'); + modal.style.display = 'none'; + + // 恢复背景滚动 + document.body.style.overflow = 'auto'; +} + +function closeIndustryHoldingsModal() { + const modal = document.getElementById('industryHoldingsModal'); + modal.style.display = 'none'; + + // 恢复背景滚动 + document.body.style.overflow = 'auto'; +} + +function getCurrentNotices() { + // 从当前显示的提醒框中获取数据,或者使用默认数据 + const noticeItems = document.querySelectorAll('.notice-item'); + if (noticeItems.length > 0) { + return Array.from(noticeItems).map(item => item.textContent.trim()); + } + return getDefaultNotices(); +} + +// 页面加载完成后绑定事件 +$(function() { + // 绑定点击事件 + document.getElementById('noticeBox').addEventListener('click', showNoticeModal); + + // 点击弹窗背景关闭 + document.getElementById('noticeModal').addEventListener('click', function(e) { + if (e.target === this) { + closeNoticeModal(); + } + }); + + // 点击行业持仓详情弹窗背景关闭 + document.getElementById('industryHoldingsModal').addEventListener('click', function(e) { + if (e.target === this) { + closeIndustryHoldingsModal(); + } + }); + + // ESC键关闭弹窗 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeNoticeModal(); + closeIndustryHoldingsModal(); + } + }); +}); diff --git a/src/templates/bigscreen.html b/src/templates/bigscreen.html index 0cd500a..96c6e8d 100644 --- a/src/templates/bigscreen.html +++ b/src/templates/bigscreen.html @@ -216,22 +216,5 @@ - diff --git a/src/templates/bigscreen_v2.html b/src/templates/bigscreen_v2.html new file mode 100644 index 0000000..acddcd3 --- /dev/null +++ b/src/templates/bigscreen_v2.html @@ -0,0 +1,781 @@ + + + + + 资金与行业估值大屏 + + + + + +
+ + +
+ +
+ +
+
+
+
行业持仓占比
+
+
+
+
+
+
重要提醒
+
+
+
加载数据中...
+
+
+
+
+
+
+
融资融券数据监控
+
+
+
+
+
+
市场恐贪指数
+
+
+
+
+
+
+
+
行业1-历史PE分析
+
+
行业1-拥挤度
+
+ +
+
+
+
+
+
行业2-历史PE分析
+
+
行业2-拥挤度
+
+ +
+
+
+
+
+
行业3-历史PE分析
+
+
行业3-拥挤度
+
+ +
+
+
+
+
+
行业4-历史PE分析
+
+
行业4-拥挤度
+
+ +
+
+
+
+
+ + + + + + + + + + + + diff --git a/src/valuation_analysis/industry_analysis.py b/src/valuation_analysis/industry_analysis.py index 895242a..797428f 100644 --- a/src/valuation_analysis/industry_analysis.py +++ b/src/valuation_analysis/industry_analysis.py @@ -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: """ diff --git a/src/valuation_analysis/notice_service.py b/src/valuation_analysis/notice_service.py new file mode 100644 index 0000000..a5672b8 --- /dev/null +++ b/src/valuation_analysis/notice_service.py @@ -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", + "🤖 机器人概念板块技术面突破", + "📦 先进封装概念获政策支持" + ] \ No newline at end of file diff --git a/src/valuation_analysis/portfolio_analyzer.py b/src/valuation_analysis/portfolio_analyzer.py index d77a135..6eca7e5 100644 --- a/src/valuation_analysis/portfolio_analyzer.py +++ b/src/valuation_analysis/portfolio_analyzer.py @@ -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: