This commit is contained in:
liao 2025-08-08 16:56:02 +08:00
parent cce06d8710
commit c4c4e8622f
9 changed files with 2408 additions and 41 deletions

View File

@ -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)})

View File

@ -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")

View File

@ -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

View File

@ -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>

View File

@ -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()">&times;</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()">&times;</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>

View File

@ -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)}")
@ -955,9 +974,105 @@ class IndustryAnalyzer:
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)}")
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:
"""
查询所有缓存中的行业和概念板块拥挤度筛选最后一个交易日拥挤度百分位在[min, max]区间的行业/概念

View File

@ -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",
"🤖 机器人概念板块技术面突破",
"📦 先进封装概念获政策支持"
]

View File

@ -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: