commit;
This commit is contained in:
parent
cce06d8710
commit
c4c4e8622f
45
src/app.py
45
src/app.py
|
@ -2999,7 +2999,7 @@ def run_batch_stock_price_collection():
|
||||||
|
|
||||||
@app.route('/scheduler/batch_hk_stock_price/collection', methods=['GET'])
|
@app.route('/scheduler/batch_hk_stock_price/collection', methods=['GET'])
|
||||||
def run_batch_hk_stock_price_collection():
|
def run_batch_hk_stock_price_collection():
|
||||||
"""批量采集A股行情并保存到数据库"""
|
"""批量采集港股行情并保存到数据库"""
|
||||||
try:
|
try:
|
||||||
fetch_and_store_hk_stock_data()
|
fetch_and_store_hk_stock_data()
|
||||||
return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"})
|
return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"})
|
||||||
|
@ -3039,23 +3039,38 @@ def get_portfolio_industry_allocation():
|
||||||
def get_notice_list():
|
def get_notice_list():
|
||||||
"""获取重要提醒列表"""
|
"""获取重要提醒列表"""
|
||||||
try:
|
try:
|
||||||
# 模拟数据 - 实际项目中应该从数据库或外部API获取
|
# 导入提醒服务
|
||||||
mock_notices = [
|
from src.valuation_analysis.notice_service import NoticeService
|
||||||
"上证指数突破3200点,市场情绪回暖",
|
|
||||||
"北向资金今日净流入85.6亿元",
|
|
||||||
"科技板块PE估值处于历史低位",
|
|
||||||
"新能源概念股集体上涨,涨幅超3%",
|
|
||||||
"医药板块回调,建议关注低吸机会",
|
|
||||||
"融资融券余额连续三日增长",
|
|
||||||
"消费板块资金流入明显",
|
|
||||||
"市场恐贪指数回升至65",
|
|
||||||
"机器人概念板块技术面突破",
|
|
||||||
"先进封装概念获政策支持"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
# 创建提醒服务实例
|
||||||
|
notice_service = NoticeService()
|
||||||
|
|
||||||
|
# 获取动态提醒数据
|
||||||
|
result = notice_service.get_dynamic_notices()
|
||||||
|
|
||||||
|
if result.get("success"):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": mock_notices
|
"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:
|
except Exception as e:
|
||||||
logger.error(f"获取提醒列表失败: {str(e)}")
|
logger.error(f"获取提醒列表失败: {str(e)}")
|
||||||
|
|
|
@ -99,7 +99,7 @@ class FundamentalAnalyzer:
|
||||||
self.chat_bot = ChatBot(model_type="online_bot")
|
self.chat_bot = ChatBot(model_type="online_bot")
|
||||||
# 使用离线模型进行其他分析
|
# 使用离线模型进行其他分析
|
||||||
self.offline_bot = OfflineChatBot(platform="volc", model_type="offline_model")
|
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="qwq")
|
||||||
self.offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="GLM")
|
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-Encoding': 'gzip, deflate, br, zstd',
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
'Client-Version': 'v2.44.75',
|
'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',
|
'Referer': 'https://weibo.com/u/7735765253',
|
||||||
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
'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/echarts.min.js"></script>
|
||||||
<script src="/static/js/jquery.min.js"></script>
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
<script src="/static/js/bigscreen.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>
|
</body>
|
||||||
</html>
|
</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:
|
def batch_calculate_industry_crowding(self, industries: List[str], concepts: List[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
批量计算多个行业和概念板块的拥挤度指标
|
批量计算多个行业和概念板块的拥挤度指标,并生成个股关联表
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
industries: 行业列表
|
industries: 行业列表
|
||||||
|
@ -878,7 +878,9 @@ class IndustryAnalyzer:
|
||||||
if stocks:
|
if stocks:
|
||||||
concept_stocks[concept] = stocks
|
concept_stocks[concept] = stocks
|
||||||
|
|
||||||
# 5. 批量计算行业拥挤度
|
# 5. 批量计算行业拥挤度并生成个股关联数据
|
||||||
|
industry_crowding_data = []
|
||||||
|
|
||||||
for industry, stocks in industry_stocks.items():
|
for industry, stocks in industry_stocks.items():
|
||||||
try:
|
try:
|
||||||
# 计算行业成交额
|
# 计算行业成交额
|
||||||
|
@ -911,6 +913,23 @@ class IndustryAnalyzer:
|
||||||
ex=86400
|
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)} 条记录")
|
logger.info(f"成功计算行业 {industry} 的拥挤度指标,共 {len(df)} 条记录")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"计算行业 {industry} 的拥挤度指标时出错: {str(e)}")
|
logger.error(f"计算行业 {industry} 的拥挤度指标时出错: {str(e)}")
|
||||||
|
@ -955,9 +974,105 @@ class IndustryAnalyzer:
|
||||||
logger.error(f"计算概念板块 {concept} 的拥挤度指标时出错: {str(e)}")
|
logger.error(f"计算概念板块 {concept} 的拥挤度指标时出错: {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 7. 更新行业拥挤度个股关联表
|
||||||
|
if industry_crowding_data:
|
||||||
|
self._update_industry_crowding_stocks_table(industry_crowding_data)
|
||||||
|
|
||||||
except Exception as e:
|
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:
|
def filter_crowding_by_percentile(self, min_percentile: float, max_percentile: float) -> dict:
|
||||||
"""
|
"""
|
||||||
查询所有缓存中的行业和概念板块拥挤度,筛选最后一个交易日拥挤度百分位在[min, max]区间的行业/概念。
|
查询所有缓存中的行业和概念板块拥挤度,筛选最后一个交易日拥挤度百分位在[min, max]区间的行业/概念。
|
||||||
|
|
|
@ -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
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
Loading…
Reference in New Issue