991 lines
36 KiB
Python
991 lines
36 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
主力选股UI模块
|
||
"""
|
||
|
||
import streamlit as st
|
||
from datetime import datetime, timedelta
|
||
from main_force_analysis import MainForceAnalyzer
|
||
from main_force_pdf_generator import display_report_download_section
|
||
from main_force_history_ui import display_batch_history
|
||
import pandas as pd
|
||
|
||
def display_main_force_selector():
|
||
"""显示主力选股界面"""
|
||
|
||
# 检查是否触发批量分析(不立即删除标志)
|
||
if st.session_state.get('main_force_batch_trigger'):
|
||
run_main_force_batch_analysis()
|
||
return
|
||
|
||
# 检查是否查看历史记录
|
||
if st.session_state.get('main_force_view_history'):
|
||
display_batch_history()
|
||
return
|
||
|
||
# 页面标题和历史记录按钮
|
||
col_title, col_history = st.columns([4, 1])
|
||
with col_title:
|
||
st.markdown("## 🎯 主力选股 - 智能筛选优质标的")
|
||
with col_history:
|
||
st.write("") # 占位
|
||
if st.button("📚 批量分析历史", width='content'):
|
||
st.session_state.main_force_view_history = True
|
||
st.rerun()
|
||
|
||
st.markdown("---")
|
||
|
||
st.markdown("""
|
||
### 功能说明
|
||
|
||
本功能通过以下步骤筛选优质股票:
|
||
|
||
1. **数据获取**: 使用问财获取指定日期以来主力资金净流入前100名股票
|
||
2. **智能筛选**: 过滤掉涨幅过高、市值不符的股票
|
||
3. **AI分析**: 调用资金流向、行业板块、财务基本面三大分析师团队
|
||
4. **综合决策**: 资深研究员综合评估,精选3-5只优质标的
|
||
|
||
**筛选标准**:
|
||
- ✅ 主力资金净流入较多
|
||
- ✅ 区间涨跌幅适中(避免追高)
|
||
- ✅ 财务基本面良好
|
||
- ✅ 行业前景明朗
|
||
- ✅ 综合素质优秀
|
||
""")
|
||
|
||
st.markdown("---")
|
||
|
||
# 参数设置
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
date_option = st.selectbox(
|
||
"选择时间区间",
|
||
["最近3个月", "最近6个月", "最近1年", "自定义日期"]
|
||
)
|
||
|
||
if date_option == "最近3个月":
|
||
days_ago = 90
|
||
start_date = None
|
||
elif date_option == "最近6个月":
|
||
days_ago = 180
|
||
start_date = None
|
||
elif date_option == "最近1年":
|
||
days_ago = 365
|
||
start_date = None
|
||
else:
|
||
custom_date = st.date_input(
|
||
"选择开始日期",
|
||
value=datetime.now() - timedelta(days=90)
|
||
)
|
||
start_date = f"{custom_date.year}年{custom_date.month}月{custom_date.day}日"
|
||
days_ago = None
|
||
|
||
with col2:
|
||
final_n = st.slider(
|
||
"最终精选数量",
|
||
min_value=3,
|
||
max_value=10,
|
||
value=5,
|
||
step=1,
|
||
help="最终推荐的股票数量"
|
||
)
|
||
|
||
with col3:
|
||
st.info("💡 系统将获取前100名股票,进行整体分析后精选优质标的")
|
||
|
||
# 高级选项
|
||
with st.expander("⚙️ 高级筛选参数"):
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
max_change = st.number_input(
|
||
"最大涨跌幅(%)",
|
||
min_value=5.0,
|
||
max_value=200.0,
|
||
value=30.0,
|
||
step=5.0,
|
||
help="过滤掉涨幅过高的股票,避免追高"
|
||
)
|
||
|
||
with col2:
|
||
min_cap = st.number_input(
|
||
"最小市值(亿)",
|
||
min_value=10.0,
|
||
max_value=500.0,
|
||
value=50.0,
|
||
step=10.0
|
||
)
|
||
|
||
with col3:
|
||
max_cap = st.number_input(
|
||
"最大市值(亿)",
|
||
min_value=50.0,
|
||
max_value=50000.0,
|
||
value=5000.0,
|
||
step=100.0
|
||
)
|
||
|
||
st.markdown("---")
|
||
|
||
# 开始分析按钮(使用.env中配置的默认模型)
|
||
if st.button("🚀 开始主力选股", type="primary", width='content'):
|
||
|
||
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
|
||
|
||
# 创建分析器(使用默认模型)
|
||
analyzer = MainForceAnalyzer()
|
||
|
||
# 运行分析
|
||
result = analyzer.run_full_analysis(
|
||
start_date=start_date,
|
||
days_ago=days_ago,
|
||
final_n=final_n,
|
||
max_range_change=max_change,
|
||
min_market_cap=min_cap,
|
||
max_market_cap=max_cap
|
||
)
|
||
|
||
# 保存结果到session_state
|
||
st.session_state.main_force_result = result
|
||
st.session_state.main_force_analyzer = analyzer
|
||
|
||
# 显示结果
|
||
if result['success']:
|
||
st.success(f"✅ 分析完成!共筛选出 {len(result['final_recommendations'])} 只优质标的")
|
||
st.rerun()
|
||
else:
|
||
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
|
||
|
||
# 显示分析结果
|
||
if 'main_force_result' in st.session_state:
|
||
result = st.session_state.main_force_result
|
||
|
||
if result['success']:
|
||
display_analysis_results(result, st.session_state.get('main_force_analyzer'))
|
||
|
||
def display_analysis_results(result: dict, analyzer):
|
||
"""显示分析结果"""
|
||
|
||
st.markdown("---")
|
||
st.markdown("## 📊 分析结果")
|
||
|
||
# 统计信息
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
st.metric("获取股票数", result['total_stocks'])
|
||
|
||
with col2:
|
||
st.metric("筛选后", result['filtered_stocks'])
|
||
|
||
with col3:
|
||
st.metric("最终推荐", len(result['final_recommendations']))
|
||
|
||
st.markdown("---")
|
||
|
||
# 显示AI分析师完整报告
|
||
if analyzer and hasattr(analyzer, 'fund_flow_analysis'):
|
||
display_analyst_reports(analyzer)
|
||
|
||
st.markdown("---")
|
||
|
||
# 显示推荐股票
|
||
if result['final_recommendations']:
|
||
st.markdown("### ⭐ 精选推荐")
|
||
|
||
for rec in result['final_recommendations']:
|
||
with st.expander(
|
||
f"【第{rec['rank']}名】{rec['symbol']} - {rec['name']}",
|
||
expanded=(rec['rank'] <= 3)
|
||
):
|
||
display_recommendation_detail(rec)
|
||
|
||
# 显示候选股票列表
|
||
if analyzer and analyzer.raw_stocks is not None and not analyzer.raw_stocks.empty:
|
||
st.markdown("---")
|
||
st.markdown("### 📋 候选股票列表(筛选后)")
|
||
|
||
# 选择关键列显示
|
||
display_cols = ['股票代码', '股票简称']
|
||
|
||
# 添加行业列
|
||
industry_cols = [col for col in analyzer.raw_stocks.columns if '行业' in col]
|
||
if industry_cols:
|
||
display_cols.append(industry_cols[0])
|
||
|
||
# 添加区间主力资金净流入(智能匹配)
|
||
main_fund_col = None
|
||
main_fund_patterns = [
|
||
'区间主力资金流向', # 实际列名
|
||
'区间主力资金净流入',
|
||
'主力资金流向',
|
||
'主力资金净流入',
|
||
'主力净流入',
|
||
'主力资金'
|
||
]
|
||
for pattern in main_fund_patterns:
|
||
matching = [col for col in analyzer.raw_stocks.columns if pattern in col]
|
||
if matching:
|
||
main_fund_col = matching[0]
|
||
break
|
||
if main_fund_col:
|
||
display_cols.append(main_fund_col)
|
||
|
||
# 添加区间涨跌幅(前复权)(智能匹配)
|
||
interval_pct_col = None
|
||
interval_pct_patterns = [
|
||
'区间涨跌幅:前复权', '区间涨跌幅:前复权(%)', '区间涨跌幅(%)',
|
||
'区间涨跌幅', '涨跌幅:前复权', '涨跌幅:前复权(%)', '涨跌幅(%)', '涨跌幅'
|
||
]
|
||
for pattern in interval_pct_patterns:
|
||
matching = [col for col in analyzer.raw_stocks.columns if pattern in col]
|
||
if matching:
|
||
interval_pct_col = matching[0]
|
||
break
|
||
if interval_pct_col:
|
||
display_cols.append(interval_pct_col)
|
||
|
||
# 添加市值、市盈率、市净率
|
||
for col_name in ['总市值', '市盈率', '市净率']:
|
||
matching_cols = [col for col in analyzer.raw_stocks.columns if col_name in col]
|
||
if matching_cols:
|
||
display_cols.append(matching_cols[0])
|
||
|
||
# 选择存在的列
|
||
final_cols = [col for col in display_cols if col in analyzer.raw_stocks.columns]
|
||
|
||
# 调试信息:显示找到的列名
|
||
with st.expander("🔍 调试信息 - 查看数据列", expanded=False):
|
||
st.caption("所有可用列:")
|
||
cols_list = list(analyzer.raw_stocks.columns)
|
||
st.write(cols_list)
|
||
st.caption(f"\n已选择显示的列: {final_cols}")
|
||
if main_fund_col:
|
||
st.success(f"✅ 找到主力资金列: {main_fund_col}")
|
||
else:
|
||
st.warning("⚠️ 未找到主力资金列")
|
||
if interval_pct_col:
|
||
st.success(f"✅ 找到涨跌幅列: {interval_pct_col}")
|
||
else:
|
||
st.warning("⚠️ 未找到涨跌幅列")
|
||
|
||
# 显示DataFrame
|
||
display_df = analyzer.raw_stocks[final_cols].copy()
|
||
st.dataframe(display_df, width='content', height=400)
|
||
|
||
# 显示统计
|
||
st.caption(f"共 {len(display_df)} 只候选股票,显示 {len(final_cols)} 个字段")
|
||
|
||
# 下载按钮
|
||
csv = display_df.to_csv(index=False, encoding='utf-8-sig')
|
||
st.download_button(
|
||
label="📥 下载候选列表CSV",
|
||
data=csv,
|
||
file_name=f"main_force_stocks_{datetime.now().strftime('%Y%m%d')}.csv",
|
||
mime="text/csv"
|
||
)
|
||
|
||
# 批量分析功能区
|
||
st.markdown("---")
|
||
|
||
col_batch1, col_batch2, col_batch3 = st.columns([2, 1, 1])
|
||
with col_batch1:
|
||
st.markdown("#### 🚀 批量深度分析")
|
||
st.caption("对主力资金净流入TOP股票进行完整的AI团队分析,获取投资评级和关键价位")
|
||
|
||
with col_batch2:
|
||
batch_count = st.selectbox(
|
||
"分析数量",
|
||
options=[10, 20, 30, 50],
|
||
index=1, # 默认20只
|
||
help="选择分析主力资金净流入前N只股票"
|
||
)
|
||
|
||
with col_batch3:
|
||
st.write("") # 占位
|
||
if st.button("🚀 开始批量分析", type="primary", width='content'):
|
||
# 准备数据:按主力资金净流入排序
|
||
df_sorted = analyzer.raw_stocks.copy()
|
||
|
||
# 确保主力资金列是数值类型并排序
|
||
if main_fund_col:
|
||
df_sorted[main_fund_col] = pd.to_numeric(df_sorted[main_fund_col], errors='coerce')
|
||
df_sorted = df_sorted.sort_values(by=main_fund_col, ascending=False)
|
||
|
||
# 提取股票代码并去掉市场后缀(.SH, .SZ等)
|
||
raw_codes = df_sorted.head(batch_count)['股票代码'].tolist()
|
||
stock_codes = []
|
||
for code in raw_codes:
|
||
# 去掉后缀(如果有的话)
|
||
if isinstance(code, str):
|
||
# 去掉 .SH, .SZ, .BJ 等后缀
|
||
clean_code = code.split('.')[0] if '.' in code else code
|
||
stock_codes.append(clean_code)
|
||
else:
|
||
stock_codes.append(str(code))
|
||
|
||
# 存储到session_state,触发批量分析
|
||
st.session_state.main_force_batch_codes = stock_codes
|
||
st.session_state.main_force_batch_trigger = True
|
||
st.rerun()
|
||
|
||
# 显示PDF报告下载区域
|
||
if analyzer and result:
|
||
display_report_download_section(analyzer, result)
|
||
|
||
def display_recommendation_detail(rec: dict):
|
||
"""显示单个推荐股票的详细信息"""
|
||
|
||
col1, col2 = st.columns([1, 1])
|
||
|
||
with col1:
|
||
st.markdown("#### 📌 推荐理由")
|
||
for reason in rec.get('reasons', []):
|
||
st.markdown(f"- {reason}")
|
||
|
||
st.markdown("#### 💡 投资亮点")
|
||
st.info(rec.get('highlights', 'N/A'))
|
||
|
||
with col2:
|
||
st.markdown("#### 📊 投资建议")
|
||
st.markdown(f"**建议仓位**: {rec.get('position', 'N/A')}")
|
||
st.markdown(f"**投资周期**: {rec.get('investment_period', 'N/A')}")
|
||
|
||
st.markdown("#### ⚠️ 风险提示")
|
||
st.warning(rec.get('risks', 'N/A'))
|
||
|
||
# 显示股票详细数据
|
||
if 'stock_data' in rec:
|
||
st.markdown("---")
|
||
st.markdown("#### 📊 股票详细数据")
|
||
|
||
stock_data = rec['stock_data']
|
||
|
||
# 创建数据展示
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
st.metric("股票代码", stock_data.get('股票代码', 'N/A'))
|
||
|
||
# 显示行业
|
||
industry_keys = [k for k in stock_data.keys() if '行业' in k]
|
||
if industry_keys:
|
||
st.metric("所属行业", stock_data.get(industry_keys[0], 'N/A'))
|
||
|
||
with col2:
|
||
# 显示主力资金
|
||
fund_keys = [k for k in stock_data.keys() if '主力' in k and '净流入' in k]
|
||
if fund_keys:
|
||
fund_value = stock_data.get(fund_keys[0], 'N/A')
|
||
if isinstance(fund_value, (int, float)):
|
||
st.metric("主力资金净流入", f"{fund_value/100000000:.2f}亿")
|
||
else:
|
||
st.metric("主力资金净流入", str(fund_value))
|
||
|
||
with col3:
|
||
# 显示涨跌幅
|
||
change_keys = [k for k in stock_data.keys() if '涨跌幅' in k]
|
||
if change_keys:
|
||
change_value = stock_data.get(change_keys[0], 'N/A')
|
||
if isinstance(change_value, (int, float)):
|
||
st.metric("区间涨跌幅", f"{change_value:.2f}%")
|
||
else:
|
||
st.metric("区间涨跌幅", str(change_value))
|
||
|
||
# 显示其他关键指标
|
||
st.markdown("**其他关键指标:**")
|
||
metrics_col1, metrics_col2, metrics_col3 = st.columns(3)
|
||
|
||
with metrics_col1:
|
||
if '市盈率' in stock_data or any('市盈率' in k for k in stock_data.keys()):
|
||
pe_keys = [k for k in stock_data.keys() if '市盈率' in k]
|
||
if pe_keys:
|
||
st.caption(f"市盈率: {stock_data.get(pe_keys[0], 'N/A')}")
|
||
|
||
with metrics_col2:
|
||
if '市净率' in stock_data or any('市净率' in k for k in stock_data.keys()):
|
||
pb_keys = [k for k in stock_data.keys() if '市净率' in k]
|
||
if pb_keys:
|
||
st.caption(f"市净率: {stock_data.get(pb_keys[0], 'N/A')}")
|
||
|
||
with metrics_col3:
|
||
if '总市值' in stock_data or any('总市值' in k for k in stock_data.keys()):
|
||
cap_keys = [k for k in stock_data.keys() if '总市值' in k]
|
||
if cap_keys:
|
||
st.caption(f"总市值: {stock_data.get(cap_keys[0], 'N/A')}")
|
||
|
||
def display_analyst_reports(analyzer):
|
||
"""显示AI分析师完整报告"""
|
||
|
||
st.markdown("### 🤖 AI分析师团队完整报告")
|
||
|
||
# 创建三个标签页
|
||
tab1, tab2, tab3 = st.tabs(["💰 资金流向分析", "📊 行业板块分析", "📈 财务基本面分析"])
|
||
|
||
with tab1:
|
||
st.markdown("#### 💰 资金流向分析师报告")
|
||
st.markdown("---")
|
||
if hasattr(analyzer, 'fund_flow_analysis') and analyzer.fund_flow_analysis:
|
||
st.markdown(analyzer.fund_flow_analysis)
|
||
else:
|
||
st.info("暂无资金流向分析报告")
|
||
|
||
with tab2:
|
||
st.markdown("#### 📊 行业板块及市场热点分析师报告")
|
||
st.markdown("---")
|
||
if hasattr(analyzer, 'industry_analysis') and analyzer.industry_analysis:
|
||
st.markdown(analyzer.industry_analysis)
|
||
else:
|
||
st.info("暂无行业板块分析报告")
|
||
|
||
with tab3:
|
||
st.markdown("#### 📈 财务基本面分析师报告")
|
||
st.markdown("---")
|
||
if hasattr(analyzer, 'fundamental_analysis') and analyzer.fundamental_analysis:
|
||
st.markdown(analyzer.fundamental_analysis)
|
||
else:
|
||
st.info("暂无财务基本面分析报告")
|
||
|
||
def format_number(value, unit='', suffix=''):
|
||
"""格式化数字显示"""
|
||
if value is None or value == 'N/A':
|
||
return 'N/A'
|
||
|
||
try:
|
||
num = float(value)
|
||
|
||
# 如果单位是亿,需要转换
|
||
if unit == '亿':
|
||
if abs(num) >= 100000000: # 大于1亿(以元为单位)
|
||
num = num / 100000000
|
||
elif abs(num) < 100: # 小于100,可能已经是亿
|
||
pass
|
||
else: # 100-100000000之间,可能是万
|
||
num = num / 10000
|
||
|
||
# 格式化显示
|
||
if abs(num) >= 1000:
|
||
formatted = f"{num:,.2f}"
|
||
elif abs(num) >= 1:
|
||
formatted = f"{num:.2f}"
|
||
else:
|
||
formatted = f"{num:.4f}"
|
||
|
||
return f"{formatted}{suffix}"
|
||
except (ValueError, TypeError):
|
||
return str(value)
|
||
|
||
|
||
def run_main_force_batch_analysis():
|
||
"""执行主力选股TOP股票批量分析(遵循统一调用规范)"""
|
||
import time
|
||
import re
|
||
|
||
st.markdown("## 🚀 主力选股TOP股票批量分析")
|
||
st.markdown("---")
|
||
|
||
# 检查是否已有分析结果
|
||
if st.session_state.get('main_force_batch_results'):
|
||
display_main_force_batch_results(st.session_state.main_force_batch_results)
|
||
|
||
# 返回按钮
|
||
col_back, col_clear = st.columns(2)
|
||
with col_back:
|
||
if st.button("🔙 返回主力选股", width='content'):
|
||
# 清除所有批量分析相关状态
|
||
if 'main_force_batch_trigger' in st.session_state:
|
||
del st.session_state.main_force_batch_trigger
|
||
if 'main_force_batch_codes' in st.session_state:
|
||
del st.session_state.main_force_batch_codes
|
||
if 'main_force_batch_results' in st.session_state:
|
||
del st.session_state.main_force_batch_results
|
||
st.rerun()
|
||
|
||
with col_clear:
|
||
if st.button("🔄 重新分析", width='content'):
|
||
# 清除结果,保留触发标志和代码
|
||
if 'main_force_batch_results' in st.session_state:
|
||
del st.session_state.main_force_batch_results
|
||
st.rerun()
|
||
|
||
return
|
||
|
||
# 获取股票代码列表
|
||
stock_codes = st.session_state.get('main_force_batch_codes', [])
|
||
|
||
if not stock_codes:
|
||
st.error("未找到股票代码列表")
|
||
# 清除触发标志
|
||
if 'main_force_batch_trigger' in st.session_state:
|
||
del st.session_state.main_force_batch_trigger
|
||
return
|
||
|
||
st.info(f"即将分析 {len(stock_codes)} 只股票:{', '.join(stock_codes[:10])}{'...' if len(stock_codes) > 10 else ''}")
|
||
|
||
# 返回按钮
|
||
if st.button("🔙 取消返回", type="secondary"):
|
||
# 清除所有批量分析相关状态
|
||
if 'main_force_batch_trigger' in st.session_state:
|
||
del st.session_state.main_force_batch_trigger
|
||
if 'main_force_batch_codes' in st.session_state:
|
||
del st.session_state.main_force_batch_codes
|
||
st.rerun()
|
||
|
||
st.markdown("---")
|
||
|
||
# 分析选项
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
analysis_mode = st.selectbox(
|
||
"分析模式",
|
||
options=["sequential", "parallel"],
|
||
format_func=lambda x: "顺序分析(稳定)" if x == "sequential" else "并行分析(快速)",
|
||
help="顺序分析较慢但稳定,并行分析更快但消耗更多资源"
|
||
)
|
||
|
||
with col2:
|
||
if analysis_mode == "parallel":
|
||
max_workers = st.number_input(
|
||
"并行线程数",
|
||
min_value=2,
|
||
max_value=5,
|
||
value=3,
|
||
help="同时分析的股票数量"
|
||
)
|
||
else:
|
||
max_workers = 1
|
||
|
||
st.markdown("---")
|
||
|
||
# 开始分析按钮
|
||
col_confirm, col_cancel = st.columns(2)
|
||
|
||
start_analysis = False
|
||
with col_confirm:
|
||
if st.button("🚀 确认开始分析", type="primary", width='content'):
|
||
start_analysis = True
|
||
|
||
with col_cancel:
|
||
if st.button("❌ 取消", type="secondary", width='content'):
|
||
# 清除所有批量分析相关状态
|
||
if 'main_force_batch_trigger' in st.session_state:
|
||
del st.session_state.main_force_batch_trigger
|
||
if 'main_force_batch_codes' in st.session_state:
|
||
del st.session_state.main_force_batch_codes
|
||
st.rerun()
|
||
|
||
if start_analysis:
|
||
# 导入统一分析函数(遵循统一规范)
|
||
from app import analyze_single_stock_for_batch
|
||
import concurrent.futures
|
||
import time
|
||
|
||
st.markdown("---")
|
||
st.info("⏳ 正在执行批量分析,请稍候...")
|
||
|
||
# 显示即将分析的股票代码(调试用)
|
||
with st.expander("🔍 调试信息", expanded=True):
|
||
st.write(f"**股票代码数量**: {len(stock_codes)} 只")
|
||
st.write(f"**股票代码列表**: {stock_codes}")
|
||
st.write(f"**代码格式检查**: {'✅ 无后缀,格式正确' if all('.' not in str(c) for c in stock_codes) else '❌ 包含后缀,可能有问题'}")
|
||
st.write(f"**分析模式**: {analysis_mode}")
|
||
st.write(f"**线程数**: {max_workers if analysis_mode == 'parallel' else 1}")
|
||
|
||
# 配置分析师参数
|
||
enabled_analysts_config = {
|
||
'technical': True,
|
||
'fundamental': True,
|
||
'fund_flow': True,
|
||
'risk': True,
|
||
'sentiment': False, # 禁用以提升速度
|
||
'news': False # 禁用以提升速度
|
||
}
|
||
import config
|
||
selected_model = config.DEFAULT_MODEL_NAME
|
||
period = '1y'
|
||
|
||
# 创建进度显示
|
||
progress_bar = st.progress(0)
|
||
status_text = st.empty()
|
||
|
||
# 存储结果
|
||
results = []
|
||
|
||
# 记录开始时间
|
||
start_time = time.time()
|
||
|
||
if analysis_mode == "sequential":
|
||
# 顺序分析
|
||
for i, code in enumerate(stock_codes):
|
||
status_text.text(f"正在分析 {code} ({i+1}/{len(stock_codes)})")
|
||
progress_bar.progress((i + 1) / len(stock_codes))
|
||
|
||
try:
|
||
# 调用统一分析函数
|
||
result = analyze_single_stock_for_batch(
|
||
symbol=code,
|
||
period=period,
|
||
enabled_analysts_config=enabled_analysts_config,
|
||
selected_model=selected_model
|
||
)
|
||
|
||
results.append(result)
|
||
|
||
except Exception as e:
|
||
results.append({
|
||
"symbol": code,
|
||
"success": False,
|
||
"error": str(e)
|
||
})
|
||
|
||
else:
|
||
# 并行分析
|
||
status_text.text(f"并行分析 {len(stock_codes)} 只股票({max_workers}线程)...")
|
||
print(f"\n{'='*60}")
|
||
print(f"🚀 开始并行分析 {len(stock_codes)} 只股票")
|
||
print(f"{'='*60}")
|
||
|
||
def analyze_one(code):
|
||
try:
|
||
print(f" 开始分析: {code}")
|
||
result = analyze_single_stock_for_batch(
|
||
symbol=code,
|
||
period=period,
|
||
enabled_analysts_config=enabled_analysts_config,
|
||
selected_model=selected_model
|
||
)
|
||
print(f" 完成分析: {code}")
|
||
return result
|
||
except Exception as e:
|
||
print(f" 分析失败: {code} - {str(e)}")
|
||
return {"symbol": code, "success": False, "error": str(e)}
|
||
|
||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||
futures = {executor.submit(analyze_one, code): code for code in stock_codes}
|
||
|
||
completed = 0
|
||
for future in concurrent.futures.as_completed(futures):
|
||
code = futures[future] # 获取对应的股票代码
|
||
completed += 1
|
||
progress = completed / len(stock_codes)
|
||
progress_bar.progress(progress)
|
||
status_text.text(f"已完成 {completed}/{len(stock_codes)} ({code})")
|
||
|
||
print(f" 进度更新: {completed}/{len(stock_codes)} ({progress*100:.1f}%) - {code}")
|
||
|
||
try:
|
||
result = future.result()
|
||
results.append(result)
|
||
except Exception as e:
|
||
print(f" 获取结果失败: {code} - {str(e)}")
|
||
results.append({"symbol": code, "success": False, "error": str(e)})
|
||
|
||
print(f"\n✅ 所有并行任务已完成")
|
||
print(f" 完成数: {completed}")
|
||
print(f" 结果数: {len(results)}")
|
||
print(f"{'='*60}\n")
|
||
|
||
# 清除进度
|
||
progress_bar.empty()
|
||
status_text.empty()
|
||
|
||
# 计算统计
|
||
elapsed_time = time.time() - start_time
|
||
success_count = sum(1 for r in results if r.get("success", False))
|
||
failed_count = len(results) - success_count
|
||
|
||
# 显示完成信息
|
||
if success_count > 0:
|
||
st.success(f"✅ 批量分析完成!成功 {success_count} 只,失败 {failed_count} 只,耗时 {elapsed_time/60:.1f} 分钟")
|
||
else:
|
||
st.error(f"❌ 批量分析完成,但所有 {failed_count} 只股票都分析失败!")
|
||
|
||
# 显示失败原因(调试用)
|
||
with st.expander("❌ 查看失败原因", expanded=True):
|
||
for r in results:
|
||
if not r.get("success", False):
|
||
st.error(f"**{r.get('symbol', 'N/A')}**: {r.get('error', '未知错误')}")
|
||
|
||
# 先保存到数据库历史记录(在 rerun 之前完成)
|
||
save_success = False
|
||
save_error = None
|
||
try:
|
||
from main_force_batch_db import batch_db
|
||
|
||
# 调试信息
|
||
print(f"\n{'='*60}")
|
||
print(f"📝 准备保存批量分析结果到历史记录")
|
||
print(f"{'='*60}")
|
||
print(f"股票代码数: {len(stock_codes)}")
|
||
print(f"分析模式: {analysis_mode}")
|
||
print(f"成功数: {success_count}")
|
||
print(f"失败数: {failed_count}")
|
||
print(f"总耗时: {elapsed_time:.2f}秒")
|
||
print(f"结果数: {len(results)}")
|
||
|
||
# 检查结果数据类型
|
||
print(f"\n检查结果数据类型:")
|
||
for i, result in enumerate(results[:3]): # 只检查前3个
|
||
print(f" 结果 {i+1}:")
|
||
for key, value in list(result.items())[:5]: # 只检查前5个字段
|
||
print(f" - {key}: {type(value).__name__}")
|
||
|
||
print(f"\n开始保存到数据库...")
|
||
save_start = time.time()
|
||
|
||
# 保存到数据库
|
||
record_id = batch_db.save_batch_analysis(
|
||
batch_count=len(stock_codes),
|
||
analysis_mode=analysis_mode,
|
||
success_count=success_count,
|
||
failed_count=failed_count,
|
||
total_time=elapsed_time,
|
||
results=results
|
||
)
|
||
|
||
save_elapsed = time.time() - save_start
|
||
print(f"✅ 批量分析结果已保存到历史记录")
|
||
print(f" 记录ID: {record_id}")
|
||
print(f" 保存耗时: {save_elapsed:.2f}秒")
|
||
print(f"{'='*60}\n")
|
||
save_success = True
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
save_error = str(e)
|
||
print(f"\n{'='*60}")
|
||
print(f"⚠️ 保存历史记录失败")
|
||
print(f"{'='*60}")
|
||
print(f"错误信息: {str(e)}")
|
||
print(f"详细错误:")
|
||
print(traceback.format_exc())
|
||
print(f"{'='*60}\n")
|
||
|
||
# 保存结果到session_state
|
||
st.session_state.main_force_batch_results = {
|
||
"results": results,
|
||
"total": len(results),
|
||
"success": success_count,
|
||
"failed": failed_count,
|
||
"elapsed_time": elapsed_time,
|
||
"analysis_mode": analysis_mode,
|
||
"saved_to_history": save_success,
|
||
"save_error": save_error
|
||
}
|
||
|
||
time.sleep(0.5)
|
||
|
||
# 重新渲染以显示结果
|
||
st.rerun()
|
||
|
||
|
||
def display_main_force_batch_results(batch_results):
|
||
"""显示主力选股批量分析结果"""
|
||
import re
|
||
|
||
results = batch_results['results']
|
||
total = batch_results['total']
|
||
success = batch_results['success']
|
||
failed = batch_results['failed']
|
||
elapsed_time = batch_results['elapsed_time']
|
||
saved_to_history = batch_results.get('saved_to_history', False)
|
||
save_error = batch_results.get('save_error')
|
||
|
||
st.markdown("## 📊 批量分析结果")
|
||
|
||
# 显示保存状态
|
||
if saved_to_history:
|
||
st.success("✅ 分析结果已自动保存到历史记录,可点击右上角'📚 批量分析历史'查看")
|
||
elif save_error:
|
||
st.warning(f"⚠️ 历史记录保存失败: {save_error},但结果仍可查看")
|
||
|
||
st.markdown("---")
|
||
|
||
# 统计信息
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
|
||
with col1:
|
||
st.metric("总计分析", f"{total} 只")
|
||
|
||
with col2:
|
||
st.metric("成功分析", f"{success} 只", delta=f"{success/total*100:.1f}%")
|
||
|
||
with col3:
|
||
st.metric("失败分析", f"{failed} 只")
|
||
|
||
with col4:
|
||
st.metric("总耗时", f"{elapsed_time/60:.1f} 分钟")
|
||
|
||
st.markdown("---")
|
||
|
||
# 成功分析的股票
|
||
successful_results = [r for r in results if r['success']]
|
||
|
||
if successful_results:
|
||
st.markdown(f"### ✅ 成功分析的股票 ({len(successful_results)}只)")
|
||
|
||
# 创建DataFrame展示
|
||
display_data = []
|
||
for result in successful_results:
|
||
stock_info = result.get('stock_info', {})
|
||
final_decision = result.get('final_decision', {})
|
||
|
||
# 提取评级emoji
|
||
rating = final_decision.get('rating', '未知')
|
||
rating_emoji = {
|
||
'强烈买入': '🔥',
|
||
'买入': '✅',
|
||
'持有': '⏸️',
|
||
'卖出': '⚠️',
|
||
'强烈卖出': '🚫'
|
||
}.get(rating, '❓')
|
||
|
||
display_data.append({
|
||
'股票代码': stock_info.get('symbol', ''),
|
||
'股票名称': stock_info.get('name', ''),
|
||
'评级': f"{rating_emoji} {rating}",
|
||
'信心度': final_decision.get('confidence_level', 'N/A'),
|
||
'进场区间': final_decision.get('entry_range', 'N/A'),
|
||
'止盈位': final_decision.get('take_profit', 'N/A'),
|
||
'止损位': final_decision.get('stop_loss', 'N/A'),
|
||
'目标价': final_decision.get('target_price', 'N/A')
|
||
})
|
||
|
||
df_display = pd.DataFrame(display_data)
|
||
|
||
# 类型统一,避免Arrow序列化错误
|
||
numeric_cols = ['信心度', '止盈位', '止损位', '目标价']
|
||
for col in numeric_cols:
|
||
if col in df_display.columns:
|
||
df_display[col] = pd.to_numeric(df_display[col], errors='coerce')
|
||
|
||
text_cols = ['股票代码', '股票名称', '评级', '进场区间']
|
||
for col in text_cols:
|
||
if col in df_display.columns:
|
||
df_display[col] = df_display[col].astype(str)
|
||
|
||
st.dataframe(df_display, width='content', height=400)
|
||
|
||
# 详细分析结果(可展开)
|
||
st.markdown("---")
|
||
st.markdown("### 📋 详细分析报告")
|
||
|
||
for result in successful_results:
|
||
stock_info = result.get('stock_info', {})
|
||
final_decision = result.get('final_decision', {})
|
||
|
||
symbol = stock_info.get('symbol', '')
|
||
name = stock_info.get('name', '')
|
||
rating = final_decision.get('rating', '未知')
|
||
rating_emoji = {
|
||
'强烈买入': '🔥',
|
||
'买入': '✅',
|
||
'持有': '⏸️',
|
||
'卖出': '⚠️',
|
||
'强烈卖出': '🚫'
|
||
}.get(rating, '❓')
|
||
|
||
with st.expander(f"{rating_emoji} {symbol} - {name} | {rating}"):
|
||
# 关键信息
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
st.metric("信心度", final_decision.get('confidence_level', 'N/A'))
|
||
|
||
with col2:
|
||
st.metric("进场区间", final_decision.get('entry_range', 'N/A'))
|
||
|
||
with col3:
|
||
st.metric("目标价", final_decision.get('target_price', 'N/A'))
|
||
|
||
# 止盈止损
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
st.metric("止盈位", final_decision.get('take_profit', 'N/A'))
|
||
|
||
with col2:
|
||
st.metric("止损位", final_decision.get('stop_loss', 'N/A'))
|
||
|
||
# 投资建议
|
||
st.markdown("#### 💡 投资建议")
|
||
advice = final_decision.get('operation_advice', final_decision.get('advice', '暂无建议'))
|
||
st.info(advice)
|
||
|
||
# 加入监测按钮
|
||
if st.button(f"➕ 加入监测列表", key=f"monitor_{symbol}"):
|
||
# 解析进场区间
|
||
entry_range = final_decision.get('entry_range', '')
|
||
entry_min, entry_max = None, None
|
||
if entry_range and isinstance(entry_range, str) and "-" in entry_range:
|
||
try:
|
||
parts = entry_range.split("-")
|
||
entry_min = float(parts[0].strip())
|
||
entry_max = float(parts[1].strip())
|
||
except:
|
||
pass
|
||
|
||
# 解析止盈止损
|
||
take_profit_str = final_decision.get('take_profit', '')
|
||
take_profit = None
|
||
if take_profit_str:
|
||
try:
|
||
numbers = re.findall(r'\d+\.?\d*', str(take_profit_str))
|
||
if numbers:
|
||
take_profit = float(numbers[0])
|
||
except:
|
||
pass
|
||
|
||
stop_loss_str = final_decision.get('stop_loss', '')
|
||
stop_loss = None
|
||
if stop_loss_str:
|
||
try:
|
||
numbers = re.findall(r'\d+\.?\d*', str(stop_loss_str))
|
||
if numbers:
|
||
stop_loss = float(numbers[0])
|
||
except:
|
||
pass
|
||
|
||
# 调用监测管理器添加
|
||
from monitor_db import monitor_db
|
||
|
||
try:
|
||
# 准备进场区间数据
|
||
entry_range_dict = {}
|
||
if entry_min and entry_max:
|
||
entry_range_dict = {"min": entry_min, "max": entry_max}
|
||
|
||
# 添加到监测列表
|
||
monitor_db.add_monitored_stock(
|
||
symbol=symbol,
|
||
name=name,
|
||
rating=rating,
|
||
entry_range=entry_range_dict if entry_range_dict else None,
|
||
take_profit=take_profit,
|
||
stop_loss=stop_loss
|
||
)
|
||
st.success(f"✅ {symbol} - {name} 已加入监测列表")
|
||
except Exception as e:
|
||
st.error(f"❌ 添加失败: {str(e)}")
|
||
|
||
# 失败的股票
|
||
failed_results = [r for r in results if not r['success']]
|
||
|
||
if failed_results:
|
||
st.markdown("---")
|
||
st.markdown(f"### ❌ 分析失败的股票 ({len(failed_results)}只)")
|
||
|
||
failed_data = []
|
||
for result in failed_results:
|
||
failed_data.append({
|
||
'股票代码': result.get('symbol', ''),
|
||
'失败原因': result.get('error', '未知错误')
|
||
})
|
||
|
||
df_failed = pd.DataFrame(failed_data)
|
||
st.dataframe(df_failed, width='content')
|
||
|