Files
aiagents-stock/main_force_ui.py
T
Eikwang 91d32c6ffa 增加更多的历史记录,修正部份API数据获取错误,增加备用API (#5)
* 增加更多的历史记录,修正部份数据获取错误

* 增加更多的历史记录,修正部份API数据获取错误,增加备用API

---------

Co-authored-by: bathfire <>
2025-10-29 16:22:18 +08:00

997 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
)
# 模型选择
model = st.selectbox(
"选择AI模型",
["deepseek-chat", "deepseek-reasoner"],
help="deepseek-chat速度快,deepseek-reasoner推理能力强"
)
st.markdown("---")
# 开始分析按钮
if st.button("🚀 开始主力选股", type="primary", width='content'):
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
# 创建分析器
analyzer = MainForceAnalyzer(model=model)
# 运行分析
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 # 禁用以提升速度
}
selected_model = 'deepseek-chat'
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')