+2
-2
@@ -7,7 +7,7 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: agentsstock1
|
container_name: agentsstock1
|
||||||
ports:
|
ports:
|
||||||
- "8503:8501"
|
- "8503:8503"
|
||||||
volumes:
|
volumes:
|
||||||
# 数据和数据库持久化 - 使用目录挂载而不是文件挂载
|
# 数据和数据库持久化 - 使用目录挂载而不是文件挂载
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- agentsstock-network
|
- agentsstock-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8503/_stcore/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
+155
-4
@@ -136,9 +136,12 @@ def display_analysis_tab():
|
|||||||
)
|
)
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
|
# 导入model_config.py中定义的model_options
|
||||||
|
from model_config import model_options as app_model_options
|
||||||
selected_model = st.selectbox(
|
selected_model = st.selectbox(
|
||||||
"AI模型",
|
"AI模型",
|
||||||
["deepseek-chat", "deepseek-reasoner"],
|
list(app_model_options.keys()),
|
||||||
|
format_func=lambda x: app_model_options[x],
|
||||||
help="Reasoner模型提供更强的推理能力"
|
help="Reasoner模型提供更强的推理能力"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -721,12 +724,12 @@ def display_visualizations(result):
|
|||||||
def display_pdf_export_section(result):
|
def display_pdf_export_section(result):
|
||||||
"""显示PDF导出功能"""
|
"""显示PDF导出功能"""
|
||||||
|
|
||||||
st.markdown("### 📄 导出PDF报告")
|
st.markdown("### 📄 导出报告")
|
||||||
|
|
||||||
col1, col2 = st.columns([3, 1])
|
col1, col2, col3 = st.columns([2, 1, 1])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.info("💡 点击按钮生成并下载专业的PDF分析报告")
|
st.info("💡 点击按钮生成并下载专业分析报告")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("📥 生成PDF", type="primary", width='stretch'):
|
if st.button("📥 生成PDF", type="primary", width='stretch'):
|
||||||
@@ -752,6 +755,154 @@ def display_pdf_export_section(result):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"❌ PDF生成失败: {str(e)}")
|
st.error(f"❌ PDF生成失败: {str(e)}")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
if st.button("📝 生成Markdown", type="secondary", width='stretch'):
|
||||||
|
with st.spinner("正在生成Markdown报告..."):
|
||||||
|
try:
|
||||||
|
# 生成Markdown内容
|
||||||
|
markdown_content = generate_markdown_report(result)
|
||||||
|
|
||||||
|
# 提供下载
|
||||||
|
st.download_button(
|
||||||
|
label="📥 下载Markdown报告",
|
||||||
|
data=markdown_content,
|
||||||
|
file_name=f"智瞰龙虎报告_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md",
|
||||||
|
mime="text/markdown",
|
||||||
|
width='stretch'
|
||||||
|
)
|
||||||
|
|
||||||
|
st.success("✅ Markdown报告生成成功!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ Markdown生成失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_markdown_report(result_data: dict) -> str:
|
||||||
|
"""生成龙虎榜分析Markdown报告"""
|
||||||
|
|
||||||
|
# 获取当前时间
|
||||||
|
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||||
|
|
||||||
|
# 标题页
|
||||||
|
markdown_content = f"""# 智瞰龙虎榜分析报告
|
||||||
|
|
||||||
|
**AI驱动的龙虎榜多维度分析系统**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 报告概览
|
||||||
|
|
||||||
|
- **生成时间**: {current_time}
|
||||||
|
- **数据记录**: {result_data.get('data_info', {}).get('total_records', 0)} 条
|
||||||
|
- **涉及股票**: {result_data.get('data_info', {}).get('total_stocks', 0)} 只
|
||||||
|
- **涉及游资**: {result_data.get('data_info', {}).get('total_youzi', 0)} 个
|
||||||
|
- **AI分析师**: 5位专业分析师团队
|
||||||
|
- **分析模型**: DeepSeek AI Multi-Agent System
|
||||||
|
|
||||||
|
> ⚠️ 本报告由AI系统基于龙虎榜公开数据自动生成,仅供参考,不构成投资建议。市场有风险,投资需谨慎。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 数据概况
|
||||||
|
|
||||||
|
本次分析共涵盖 **{result_data.get('data_info', {}).get('total_records', 0)}** 条龙虎榜记录,
|
||||||
|
涉及 **{result_data.get('data_info', {}).get('total_stocks', 0)}** 只股票和
|
||||||
|
**{result_data.get('data_info', {}).get('total_youzi', 0)}** 个游资席位。
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 资金概况
|
||||||
|
summary = result_data.get('data_info', {}).get('summary', {})
|
||||||
|
markdown_content += f"""
|
||||||
|
### 💰 资金概况
|
||||||
|
|
||||||
|
- **总买入金额**: {summary.get('total_buy_amount', 0):,.2f} 元
|
||||||
|
- **总卖出金额**: {summary.get('total_sell_amount', 0):,.2f} 元
|
||||||
|
- **净流入金额**: {summary.get('total_net_inflow', 0):,.2f} 元
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TOP游资
|
||||||
|
if summary.get('top_youzi'):
|
||||||
|
markdown_content += "### 🏆 活跃游资 TOP10\n\n| 排名 | 游资名称 | 净流入金额(元) |\n|------|----------|---------------|\n"
|
||||||
|
for idx, (name, amount) in enumerate(list(summary['top_youzi'].items())[:10], 1):
|
||||||
|
markdown_content += f"| {idx} | {name} | {amount:,.2f} |\n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
# TOP股票
|
||||||
|
if summary.get('top_stocks'):
|
||||||
|
markdown_content += "### 📈 资金净流入 TOP20 股票\n\n| 排名 | 股票代码 | 股票名称 | 净流入金额(元) |\n|------|----------|----------|---------------|\n"
|
||||||
|
for idx, stock in enumerate(summary['top_stocks'][:20], 1):
|
||||||
|
markdown_content += f"| {idx} | {stock['code']} | {stock['name']} | {stock['net_inflow']:,.2f} |\n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
# 热门概念
|
||||||
|
if summary.get('hot_concepts'):
|
||||||
|
markdown_content += "### 🔥 热门概念 TOP15\n\n"
|
||||||
|
for idx, (concept, count) in enumerate(list(summary['hot_concepts'].items())[:15], 1):
|
||||||
|
markdown_content += f"{idx}. {concept} ({count}次) \n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
# 推荐股票
|
||||||
|
recommended = result_data.get('recommended_stocks', [])
|
||||||
|
if recommended:
|
||||||
|
markdown_content += f"""
|
||||||
|
## 🎯 AI推荐股票
|
||||||
|
|
||||||
|
基于5位AI分析师的综合分析,系统识别出以下 **{len(recommended)}** 只潜力股票,
|
||||||
|
这些股票在资金流向、游资关注度、题材热度等多个维度表现突出。
|
||||||
|
|
||||||
|
### 推荐股票清单
|
||||||
|
|
||||||
|
| 排名 | 股票代码 | 股票名称 | 净流入金额 | 确定性 | 持有周期 |
|
||||||
|
|------|----------|----------|------------|--------|----------|
|
||||||
|
"""
|
||||||
|
for stock in recommended[:10]:
|
||||||
|
markdown_content += f"| {stock.get('rank', '-')} | {stock.get('code', '-')} | {stock.get('name', '-')} | {stock.get('net_inflow', 0):,.0f} | {stock.get('confidence', '-')} | {stock.get('hold_period', '-')} |\n"
|
||||||
|
|
||||||
|
markdown_content += "\n### 推荐理由详解\n\n"
|
||||||
|
for stock in recommended[:5]: # 只详细展示前5只
|
||||||
|
markdown_content += f"**{stock.get('rank', '-')}. {stock.get('name', '-')} ({stock.get('code', '-')})**\n\n"
|
||||||
|
markdown_content += f"- 推荐理由: {stock.get('reason', '暂无')}\n"
|
||||||
|
markdown_content += f"- 确定性: {stock.get('confidence', '-')}\n"
|
||||||
|
markdown_content += f"- 持有周期: {stock.get('hold_period', '-')}\n\n"
|
||||||
|
|
||||||
|
# AI分析师报告
|
||||||
|
agents_analysis = result_data.get('agents_analysis', {})
|
||||||
|
if agents_analysis:
|
||||||
|
markdown_content += "## 🤖 AI分析师报告\n\n"
|
||||||
|
markdown_content += "本报告由5位AI专业分析师从不同维度进行分析,综合形成投资建议:\n\n"
|
||||||
|
markdown_content += "- **游资行为分析师** - 分析游资操作特征和意图\n"
|
||||||
|
markdown_content += "- **个股潜力分析师** - 挖掘次日大概率上涨的股票\n"
|
||||||
|
markdown_content += "- **题材追踪分析师** - 识别热点题材和轮动机会\n"
|
||||||
|
markdown_content += "- **风险控制专家** - 识别高风险股票和市场陷阱\n"
|
||||||
|
markdown_content += "- **首席策略师** - 综合研判并给出最终建议\n\n"
|
||||||
|
|
||||||
|
agent_titles = {
|
||||||
|
'youzi': '游资行为分析师',
|
||||||
|
'stock': '个股潜力分析师',
|
||||||
|
'theme': '题材追踪分析师',
|
||||||
|
'risk': '风险控制专家',
|
||||||
|
'chief': '首席策略师综合研判'
|
||||||
|
}
|
||||||
|
|
||||||
|
for agent_key, agent_title in agent_titles.items():
|
||||||
|
agent_data = agents_analysis.get(agent_key, {})
|
||||||
|
if agent_data:
|
||||||
|
markdown_content += f"### {agent_title}\n\n"
|
||||||
|
analysis_text = agent_data.get('analysis', '暂无分析')
|
||||||
|
# 处理文本中的换行
|
||||||
|
analysis_text = analysis_text.replace('\n', '\n\n')
|
||||||
|
markdown_content += f"{analysis_text}\n\n"
|
||||||
|
|
||||||
|
markdown_content += """
|
||||||
|
---
|
||||||
|
|
||||||
|
*报告由智瞰龙虎AI系统自动生成*
|
||||||
|
"""
|
||||||
|
|
||||||
|
return markdown_content
|
||||||
|
|
||||||
|
|
||||||
def display_history_tab():
|
def display_history_tab():
|
||||||
|
|||||||
+176
-173
@@ -13,17 +13,17 @@ import pandas as pd
|
|||||||
|
|
||||||
def display_main_force_selector():
|
def display_main_force_selector():
|
||||||
"""显示主力选股界面"""
|
"""显示主力选股界面"""
|
||||||
|
|
||||||
# 检查是否触发批量分析(不立即删除标志)
|
# 检查是否触发批量分析(不立即删除标志)
|
||||||
if st.session_state.get('main_force_batch_trigger'):
|
if st.session_state.get('main_force_batch_trigger'):
|
||||||
run_main_force_batch_analysis()
|
run_main_force_batch_analysis()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 检查是否查看历史记录
|
# 检查是否查看历史记录
|
||||||
if st.session_state.get('main_force_view_history'):
|
if st.session_state.get('main_force_view_history'):
|
||||||
display_batch_history()
|
display_batch_history()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 页面标题和历史记录按钮
|
# 页面标题和历史记录按钮
|
||||||
col_title, col_history = st.columns([4, 1])
|
col_title, col_history = st.columns([4, 1])
|
||||||
with col_title:
|
with col_title:
|
||||||
@@ -33,9 +33,9 @@ def display_main_force_selector():
|
|||||||
if st.button("📚 批量分析历史", width='content'):
|
if st.button("📚 批量分析历史", width='content'):
|
||||||
st.session_state.main_force_view_history = True
|
st.session_state.main_force_view_history = True
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
### 功能说明
|
### 功能说明
|
||||||
|
|
||||||
@@ -53,18 +53,18 @@ def display_main_force_selector():
|
|||||||
- ✅ 行业前景明朗
|
- ✅ 行业前景明朗
|
||||||
- ✅ 综合素质优秀
|
- ✅ 综合素质优秀
|
||||||
""")
|
""")
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 参数设置
|
# 参数设置
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
date_option = st.selectbox(
|
date_option = st.selectbox(
|
||||||
"选择时间区间",
|
"选择时间区间",
|
||||||
["最近3个月", "最近6个月", "最近1年", "自定义日期"]
|
["最近3个月", "最近6个月", "最近1年", "自定义日期"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if date_option == "最近3个月":
|
if date_option == "最近3个月":
|
||||||
days_ago = 90
|
days_ago = 90
|
||||||
start_date = None
|
start_date = None
|
||||||
@@ -81,7 +81,7 @@ def display_main_force_selector():
|
|||||||
)
|
)
|
||||||
start_date = f"{custom_date.year}年{custom_date.month}月{custom_date.day}日"
|
start_date = f"{custom_date.year}年{custom_date.month}月{custom_date.day}日"
|
||||||
days_ago = None
|
days_ago = None
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
final_n = st.slider(
|
final_n = st.slider(
|
||||||
"最终精选数量",
|
"最终精选数量",
|
||||||
@@ -91,14 +91,14 @@ def display_main_force_selector():
|
|||||||
step=1,
|
step=1,
|
||||||
help="最终推荐的股票数量"
|
help="最终推荐的股票数量"
|
||||||
)
|
)
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
st.info("💡 系统将获取前100名股票,进行整体分析后精选优质标的")
|
st.info("💡 系统将获取前100名股票,进行整体分析后精选优质标的")
|
||||||
|
|
||||||
# 高级选项
|
# 高级选项
|
||||||
with st.expander("⚙️ 高级筛选参数"):
|
with st.expander("⚙️ 高级筛选参数"):
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
max_change = st.number_input(
|
max_change = st.number_input(
|
||||||
"最大涨跌幅(%)",
|
"最大涨跌幅(%)",
|
||||||
@@ -108,7 +108,7 @@ def display_main_force_selector():
|
|||||||
step=5.0,
|
step=5.0,
|
||||||
help="过滤掉涨幅过高的股票,避免追高"
|
help="过滤掉涨幅过高的股票,避免追高"
|
||||||
)
|
)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
min_cap = st.number_input(
|
min_cap = st.number_input(
|
||||||
"最小市值(亿)",
|
"最小市值(亿)",
|
||||||
@@ -117,7 +117,7 @@ def display_main_force_selector():
|
|||||||
value=50.0,
|
value=50.0,
|
||||||
step=10.0
|
step=10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
max_cap = st.number_input(
|
max_cap = st.number_input(
|
||||||
"最大市值(亿)",
|
"最大市值(亿)",
|
||||||
@@ -126,24 +126,27 @@ def display_main_force_selector():
|
|||||||
value=5000.0,
|
value=5000.0,
|
||||||
step=100.0
|
step=100.0
|
||||||
)
|
)
|
||||||
|
|
||||||
# 模型选择
|
# 模型选择
|
||||||
|
# 导入model_config.py中定义的model_options
|
||||||
|
from model_config import model_options as app_model_options
|
||||||
model = st.selectbox(
|
model = st.selectbox(
|
||||||
"选择AI模型",
|
"选择AI模型",
|
||||||
["deepseek-chat", "deepseek-reasoner"],
|
list(app_model_options.keys()),
|
||||||
|
format_func=lambda x: app_model_options[x],
|
||||||
help="deepseek-chat速度快,deepseek-reasoner推理能力强"
|
help="deepseek-chat速度快,deepseek-reasoner推理能力强"
|
||||||
)
|
)
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 开始分析按钮
|
# 开始分析按钮
|
||||||
if st.button("🚀 开始主力选股", type="primary", width='content'):
|
if st.button("🚀 开始主力选股", type="primary", width='content'):
|
||||||
|
|
||||||
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
|
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
|
||||||
|
|
||||||
# 创建分析器
|
# 创建分析器
|
||||||
analyzer = MainForceAnalyzer(model=model)
|
analyzer = MainForceAnalyzer(model=model)
|
||||||
|
|
||||||
# 运行分析
|
# 运行分析
|
||||||
result = analyzer.run_full_analysis(
|
result = analyzer.run_full_analysis(
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
@@ -153,83 +156,83 @@ def display_main_force_selector():
|
|||||||
min_market_cap=min_cap,
|
min_market_cap=min_cap,
|
||||||
max_market_cap=max_cap
|
max_market_cap=max_cap
|
||||||
)
|
)
|
||||||
|
|
||||||
# 保存结果到session_state
|
# 保存结果到session_state
|
||||||
st.session_state.main_force_result = result
|
st.session_state.main_force_result = result
|
||||||
st.session_state.main_force_analyzer = analyzer
|
st.session_state.main_force_analyzer = analyzer
|
||||||
|
|
||||||
# 显示结果
|
# 显示结果
|
||||||
if result['success']:
|
if result['success']:
|
||||||
st.success(f"✅ 分析完成!共筛选出 {len(result['final_recommendations'])} 只优质标的")
|
st.success(f"✅ 分析完成!共筛选出 {len(result['final_recommendations'])} 只优质标的")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
|
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
|
||||||
|
|
||||||
# 显示分析结果
|
# 显示分析结果
|
||||||
if 'main_force_result' in st.session_state:
|
if 'main_force_result' in st.session_state:
|
||||||
result = st.session_state.main_force_result
|
result = st.session_state.main_force_result
|
||||||
|
|
||||||
if result['success']:
|
if result['success']:
|
||||||
display_analysis_results(result, st.session_state.get('main_force_analyzer'))
|
display_analysis_results(result, st.session_state.get('main_force_analyzer'))
|
||||||
|
|
||||||
def display_analysis_results(result: dict, analyzer):
|
def display_analysis_results(result: dict, analyzer):
|
||||||
"""显示分析结果"""
|
"""显示分析结果"""
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("## 📊 分析结果")
|
st.markdown("## 📊 分析结果")
|
||||||
|
|
||||||
# 统计信息
|
# 统计信息
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("获取股票数", result['total_stocks'])
|
st.metric("获取股票数", result['total_stocks'])
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("筛选后", result['filtered_stocks'])
|
st.metric("筛选后", result['filtered_stocks'])
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
st.metric("最终推荐", len(result['final_recommendations']))
|
st.metric("最终推荐", len(result['final_recommendations']))
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 显示AI分析师完整报告
|
# 显示AI分析师完整报告
|
||||||
if analyzer and hasattr(analyzer, 'fund_flow_analysis'):
|
if analyzer and hasattr(analyzer, 'fund_flow_analysis'):
|
||||||
display_analyst_reports(analyzer)
|
display_analyst_reports(analyzer)
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 显示推荐股票
|
# 显示推荐股票
|
||||||
if result['final_recommendations']:
|
if result['final_recommendations']:
|
||||||
st.markdown("### ⭐ 精选推荐")
|
st.markdown("### ⭐ 精选推荐")
|
||||||
|
|
||||||
for rec in result['final_recommendations']:
|
for rec in result['final_recommendations']:
|
||||||
with st.expander(
|
with st.expander(
|
||||||
f"【第{rec['rank']}名】{rec['symbol']} - {rec['name']}",
|
f"【第{rec['rank']}名】{rec['symbol']} - {rec['name']}",
|
||||||
expanded=(rec['rank'] <= 3)
|
expanded=(rec['rank'] <= 3)
|
||||||
):
|
):
|
||||||
display_recommendation_detail(rec)
|
display_recommendation_detail(rec)
|
||||||
|
|
||||||
# 显示候选股票列表
|
# 显示候选股票列表
|
||||||
if analyzer and analyzer.raw_stocks is not None and not analyzer.raw_stocks.empty:
|
if analyzer and analyzer.raw_stocks is not None and not analyzer.raw_stocks.empty:
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📋 候选股票列表(筛选后)")
|
st.markdown("### 📋 候选股票列表(筛选后)")
|
||||||
|
|
||||||
# 选择关键列显示
|
# 选择关键列显示
|
||||||
display_cols = ['股票代码', '股票简称']
|
display_cols = ['股票代码', '股票简称']
|
||||||
|
|
||||||
# 添加行业列
|
# 添加行业列
|
||||||
industry_cols = [col for col in analyzer.raw_stocks.columns if '行业' in col]
|
industry_cols = [col for col in analyzer.raw_stocks.columns if '行业' in col]
|
||||||
if industry_cols:
|
if industry_cols:
|
||||||
display_cols.append(industry_cols[0])
|
display_cols.append(industry_cols[0])
|
||||||
|
|
||||||
# 添加区间主力资金净流入(智能匹配)
|
# 添加区间主力资金净流入(智能匹配)
|
||||||
main_fund_col = None
|
main_fund_col = None
|
||||||
main_fund_patterns = [
|
main_fund_patterns = [
|
||||||
'区间主力资金流向', # 实际列名
|
'区间主力资金流向', # 实际列名
|
||||||
'区间主力资金净流入',
|
'区间主力资金净流入',
|
||||||
'主力资金流向',
|
'主力资金流向',
|
||||||
'主力资金净流入',
|
'主力资金净流入',
|
||||||
'主力净流入',
|
'主力净流入',
|
||||||
'主力资金'
|
'主力资金'
|
||||||
]
|
]
|
||||||
for pattern in main_fund_patterns:
|
for pattern in main_fund_patterns:
|
||||||
@@ -239,11 +242,11 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
break
|
break
|
||||||
if main_fund_col:
|
if main_fund_col:
|
||||||
display_cols.append(main_fund_col)
|
display_cols.append(main_fund_col)
|
||||||
|
|
||||||
# 添加区间涨跌幅(前复权)(智能匹配)
|
# 添加区间涨跌幅(前复权)(智能匹配)
|
||||||
interval_pct_col = None
|
interval_pct_col = None
|
||||||
interval_pct_patterns = [
|
interval_pct_patterns = [
|
||||||
'区间涨跌幅:前复权', '区间涨跌幅:前复权(%)', '区间涨跌幅(%)',
|
'区间涨跌幅:前复权', '区间涨跌幅:前复权(%)', '区间涨跌幅(%)',
|
||||||
'区间涨跌幅', '涨跌幅:前复权', '涨跌幅:前复权(%)', '涨跌幅(%)', '涨跌幅'
|
'区间涨跌幅', '涨跌幅:前复权', '涨跌幅:前复权(%)', '涨跌幅(%)', '涨跌幅'
|
||||||
]
|
]
|
||||||
for pattern in interval_pct_patterns:
|
for pattern in interval_pct_patterns:
|
||||||
@@ -253,16 +256,16 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
break
|
break
|
||||||
if interval_pct_col:
|
if interval_pct_col:
|
||||||
display_cols.append(interval_pct_col)
|
display_cols.append(interval_pct_col)
|
||||||
|
|
||||||
# 添加市值、市盈率、市净率
|
# 添加市值、市盈率、市净率
|
||||||
for col_name in ['总市值', '市盈率', '市净率']:
|
for col_name in ['总市值', '市盈率', '市净率']:
|
||||||
matching_cols = [col for col in analyzer.raw_stocks.columns if col_name in col]
|
matching_cols = [col for col in analyzer.raw_stocks.columns if col_name in col]
|
||||||
if matching_cols:
|
if matching_cols:
|
||||||
display_cols.append(matching_cols[0])
|
display_cols.append(matching_cols[0])
|
||||||
|
|
||||||
# 选择存在的列
|
# 选择存在的列
|
||||||
final_cols = [col for col in display_cols if col in analyzer.raw_stocks.columns]
|
final_cols = [col for col in display_cols if col in analyzer.raw_stocks.columns]
|
||||||
|
|
||||||
# 调试信息:显示找到的列名
|
# 调试信息:显示找到的列名
|
||||||
with st.expander("🔍 调试信息 - 查看数据列", expanded=False):
|
with st.expander("🔍 调试信息 - 查看数据列", expanded=False):
|
||||||
st.caption("所有可用列:")
|
st.caption("所有可用列:")
|
||||||
@@ -277,14 +280,14 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
st.success(f"✅ 找到涨跌幅列: {interval_pct_col}")
|
st.success(f"✅ 找到涨跌幅列: {interval_pct_col}")
|
||||||
else:
|
else:
|
||||||
st.warning("⚠️ 未找到涨跌幅列")
|
st.warning("⚠️ 未找到涨跌幅列")
|
||||||
|
|
||||||
# 显示DataFrame
|
# 显示DataFrame
|
||||||
display_df = analyzer.raw_stocks[final_cols].copy()
|
display_df = analyzer.raw_stocks[final_cols].copy()
|
||||||
st.dataframe(display_df, width='content', height=400)
|
st.dataframe(display_df, width='content', height=400)
|
||||||
|
|
||||||
# 显示统计
|
# 显示统计
|
||||||
st.caption(f"共 {len(display_df)} 只候选股票,显示 {len(final_cols)} 个字段")
|
st.caption(f"共 {len(display_df)} 只候选股票,显示 {len(final_cols)} 个字段")
|
||||||
|
|
||||||
# 下载按钮
|
# 下载按钮
|
||||||
csv = display_df.to_csv(index=False, encoding='utf-8-sig')
|
csv = display_df.to_csv(index=False, encoding='utf-8-sig')
|
||||||
st.download_button(
|
st.download_button(
|
||||||
@@ -293,15 +296,15 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
file_name=f"main_force_stocks_{datetime.now().strftime('%Y%m%d')}.csv",
|
file_name=f"main_force_stocks_{datetime.now().strftime('%Y%m%d')}.csv",
|
||||||
mime="text/csv"
|
mime="text/csv"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 批量分析功能区
|
# 批量分析功能区
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
col_batch1, col_batch2, col_batch3 = st.columns([2, 1, 1])
|
col_batch1, col_batch2, col_batch3 = st.columns([2, 1, 1])
|
||||||
with col_batch1:
|
with col_batch1:
|
||||||
st.markdown("#### 🚀 批量深度分析")
|
st.markdown("#### 🚀 批量深度分析")
|
||||||
st.caption("对主力资金净流入TOP股票进行完整的AI团队分析,获取投资评级和关键价位")
|
st.caption("对主力资金净流入TOP股票进行完整的AI团队分析,获取投资评级和关键价位")
|
||||||
|
|
||||||
with col_batch2:
|
with col_batch2:
|
||||||
batch_count = st.selectbox(
|
batch_count = st.selectbox(
|
||||||
"分析数量",
|
"分析数量",
|
||||||
@@ -309,18 +312,18 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
index=1, # 默认20只
|
index=1, # 默认20只
|
||||||
help="选择分析主力资金净流入前N只股票"
|
help="选择分析主力资金净流入前N只股票"
|
||||||
)
|
)
|
||||||
|
|
||||||
with col_batch3:
|
with col_batch3:
|
||||||
st.write("") # 占位
|
st.write("") # 占位
|
||||||
if st.button("🚀 开始批量分析", type="primary", width='content'):
|
if st.button("🚀 开始批量分析", type="primary", width='content'):
|
||||||
# 准备数据:按主力资金净流入排序
|
# 准备数据:按主力资金净流入排序
|
||||||
df_sorted = analyzer.raw_stocks.copy()
|
df_sorted = analyzer.raw_stocks.copy()
|
||||||
|
|
||||||
# 确保主力资金列是数值类型并排序
|
# 确保主力资金列是数值类型并排序
|
||||||
if main_fund_col:
|
if main_fund_col:
|
||||||
df_sorted[main_fund_col] = pd.to_numeric(df_sorted[main_fund_col], errors='coerce')
|
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)
|
df_sorted = df_sorted.sort_values(by=main_fund_col, ascending=False)
|
||||||
|
|
||||||
# 提取股票代码并去掉市场后缀(.SH, .SZ等)
|
# 提取股票代码并去掉市场后缀(.SH, .SZ等)
|
||||||
raw_codes = df_sorted.head(batch_count)['股票代码'].tolist()
|
raw_codes = df_sorted.head(batch_count)['股票代码'].tolist()
|
||||||
stock_codes = []
|
stock_codes = []
|
||||||
@@ -332,55 +335,55 @@ def display_analysis_results(result: dict, analyzer):
|
|||||||
stock_codes.append(clean_code)
|
stock_codes.append(clean_code)
|
||||||
else:
|
else:
|
||||||
stock_codes.append(str(code))
|
stock_codes.append(str(code))
|
||||||
|
|
||||||
# 存储到session_state,触发批量分析
|
# 存储到session_state,触发批量分析
|
||||||
st.session_state.main_force_batch_codes = stock_codes
|
st.session_state.main_force_batch_codes = stock_codes
|
||||||
st.session_state.main_force_batch_trigger = True
|
st.session_state.main_force_batch_trigger = True
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
# 显示PDF报告下载区域
|
# 显示PDF报告下载区域
|
||||||
if analyzer and result:
|
if analyzer and result:
|
||||||
display_report_download_section(analyzer, result)
|
display_report_download_section(analyzer, result)
|
||||||
|
|
||||||
def display_recommendation_detail(rec: dict):
|
def display_recommendation_detail(rec: dict):
|
||||||
"""显示单个推荐股票的详细信息"""
|
"""显示单个推荐股票的详细信息"""
|
||||||
|
|
||||||
col1, col2 = st.columns([1, 1])
|
col1, col2 = st.columns([1, 1])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.markdown("#### 📌 推荐理由")
|
st.markdown("#### 📌 推荐理由")
|
||||||
for reason in rec.get('reasons', []):
|
for reason in rec.get('reasons', []):
|
||||||
st.markdown(f"- {reason}")
|
st.markdown(f"- {reason}")
|
||||||
|
|
||||||
st.markdown("#### 💡 投资亮点")
|
st.markdown("#### 💡 投资亮点")
|
||||||
st.info(rec.get('highlights', 'N/A'))
|
st.info(rec.get('highlights', 'N/A'))
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.markdown("#### 📊 投资建议")
|
st.markdown("#### 📊 投资建议")
|
||||||
st.markdown(f"**建议仓位**: {rec.get('position', 'N/A')}")
|
st.markdown(f"**建议仓位**: {rec.get('position', 'N/A')}")
|
||||||
st.markdown(f"**投资周期**: {rec.get('investment_period', 'N/A')}")
|
st.markdown(f"**投资周期**: {rec.get('investment_period', 'N/A')}")
|
||||||
|
|
||||||
st.markdown("#### ⚠️ 风险提示")
|
st.markdown("#### ⚠️ 风险提示")
|
||||||
st.warning(rec.get('risks', 'N/A'))
|
st.warning(rec.get('risks', 'N/A'))
|
||||||
|
|
||||||
# 显示股票详细数据
|
# 显示股票详细数据
|
||||||
if 'stock_data' in rec:
|
if 'stock_data' in rec:
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("#### 📊 股票详细数据")
|
st.markdown("#### 📊 股票详细数据")
|
||||||
|
|
||||||
stock_data = rec['stock_data']
|
stock_data = rec['stock_data']
|
||||||
|
|
||||||
# 创建数据展示
|
# 创建数据展示
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("股票代码", stock_data.get('股票代码', 'N/A'))
|
st.metric("股票代码", stock_data.get('股票代码', 'N/A'))
|
||||||
|
|
||||||
# 显示行业
|
# 显示行业
|
||||||
industry_keys = [k for k in stock_data.keys() if '行业' in k]
|
industry_keys = [k for k in stock_data.keys() if '行业' in k]
|
||||||
if industry_keys:
|
if industry_keys:
|
||||||
st.metric("所属行业", stock_data.get(industry_keys[0], 'N/A'))
|
st.metric("所属行业", stock_data.get(industry_keys[0], 'N/A'))
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
# 显示主力资金
|
# 显示主力资金
|
||||||
fund_keys = [k for k in stock_data.keys() if '主力' in k and '净流入' in k]
|
fund_keys = [k for k in stock_data.keys() if '主力' in k and '净流入' in k]
|
||||||
@@ -390,7 +393,7 @@ def display_recommendation_detail(rec: dict):
|
|||||||
st.metric("主力资金净流入", f"{fund_value/100000000:.2f}亿")
|
st.metric("主力资金净流入", f"{fund_value/100000000:.2f}亿")
|
||||||
else:
|
else:
|
||||||
st.metric("主力资金净流入", str(fund_value))
|
st.metric("主力资金净流入", str(fund_value))
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
# 显示涨跌幅
|
# 显示涨跌幅
|
||||||
change_keys = [k for k in stock_data.keys() if '涨跌幅' in k]
|
change_keys = [k for k in stock_data.keys() if '涨跌幅' in k]
|
||||||
@@ -400,23 +403,23 @@ def display_recommendation_detail(rec: dict):
|
|||||||
st.metric("区间涨跌幅", f"{change_value:.2f}%")
|
st.metric("区间涨跌幅", f"{change_value:.2f}%")
|
||||||
else:
|
else:
|
||||||
st.metric("区间涨跌幅", str(change_value))
|
st.metric("区间涨跌幅", str(change_value))
|
||||||
|
|
||||||
# 显示其他关键指标
|
# 显示其他关键指标
|
||||||
st.markdown("**其他关键指标:**")
|
st.markdown("**其他关键指标:**")
|
||||||
metrics_col1, metrics_col2, metrics_col3 = st.columns(3)
|
metrics_col1, metrics_col2, metrics_col3 = st.columns(3)
|
||||||
|
|
||||||
with metrics_col1:
|
with metrics_col1:
|
||||||
if '市盈率' in stock_data or any('市盈率' in k for k in stock_data.keys()):
|
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]
|
pe_keys = [k for k in stock_data.keys() if '市盈率' in k]
|
||||||
if pe_keys:
|
if pe_keys:
|
||||||
st.caption(f"市盈率: {stock_data.get(pe_keys[0], 'N/A')}")
|
st.caption(f"市盈率: {stock_data.get(pe_keys[0], 'N/A')}")
|
||||||
|
|
||||||
with metrics_col2:
|
with metrics_col2:
|
||||||
if '市净率' in stock_data or any('市净率' in k for k in stock_data.keys()):
|
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]
|
pb_keys = [k for k in stock_data.keys() if '市净率' in k]
|
||||||
if pb_keys:
|
if pb_keys:
|
||||||
st.caption(f"市净率: {stock_data.get(pb_keys[0], 'N/A')}")
|
st.caption(f"市净率: {stock_data.get(pb_keys[0], 'N/A')}")
|
||||||
|
|
||||||
with metrics_col3:
|
with metrics_col3:
|
||||||
if '总市值' in stock_data or any('总市值' in k for k in stock_data.keys()):
|
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]
|
cap_keys = [k for k in stock_data.keys() if '总市值' in k]
|
||||||
@@ -425,12 +428,12 @@ def display_recommendation_detail(rec: dict):
|
|||||||
|
|
||||||
def display_analyst_reports(analyzer):
|
def display_analyst_reports(analyzer):
|
||||||
"""显示AI分析师完整报告"""
|
"""显示AI分析师完整报告"""
|
||||||
|
|
||||||
st.markdown("### 🤖 AI分析师团队完整报告")
|
st.markdown("### 🤖 AI分析师团队完整报告")
|
||||||
|
|
||||||
# 创建三个标签页
|
# 创建三个标签页
|
||||||
tab1, tab2, tab3 = st.tabs(["💰 资金流向分析", "📊 行业板块分析", "📈 财务基本面分析"])
|
tab1, tab2, tab3 = st.tabs(["💰 资金流向分析", "📊 行业板块分析", "📈 财务基本面分析"])
|
||||||
|
|
||||||
with tab1:
|
with tab1:
|
||||||
st.markdown("#### 💰 资金流向分析师报告")
|
st.markdown("#### 💰 资金流向分析师报告")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
@@ -438,7 +441,7 @@ def display_analyst_reports(analyzer):
|
|||||||
st.markdown(analyzer.fund_flow_analysis)
|
st.markdown(analyzer.fund_flow_analysis)
|
||||||
else:
|
else:
|
||||||
st.info("暂无资金流向分析报告")
|
st.info("暂无资金流向分析报告")
|
||||||
|
|
||||||
with tab2:
|
with tab2:
|
||||||
st.markdown("#### 📊 行业板块及市场热点分析师报告")
|
st.markdown("#### 📊 行业板块及市场热点分析师报告")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
@@ -446,7 +449,7 @@ def display_analyst_reports(analyzer):
|
|||||||
st.markdown(analyzer.industry_analysis)
|
st.markdown(analyzer.industry_analysis)
|
||||||
else:
|
else:
|
||||||
st.info("暂无行业板块分析报告")
|
st.info("暂无行业板块分析报告")
|
||||||
|
|
||||||
with tab3:
|
with tab3:
|
||||||
st.markdown("#### 📈 财务基本面分析师报告")
|
st.markdown("#### 📈 财务基本面分析师报告")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
@@ -459,10 +462,10 @@ def format_number(value, unit='', suffix=''):
|
|||||||
"""格式化数字显示"""
|
"""格式化数字显示"""
|
||||||
if value is None or value == 'N/A':
|
if value is None or value == 'N/A':
|
||||||
return 'N/A'
|
return 'N/A'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
num = float(value)
|
num = float(value)
|
||||||
|
|
||||||
# 如果单位是亿,需要转换
|
# 如果单位是亿,需要转换
|
||||||
if unit == '亿':
|
if unit == '亿':
|
||||||
if abs(num) >= 100000000: # 大于1亿(以元为单位)
|
if abs(num) >= 100000000: # 大于1亿(以元为单位)
|
||||||
@@ -471,7 +474,7 @@ def format_number(value, unit='', suffix=''):
|
|||||||
pass
|
pass
|
||||||
else: # 100-100000000之间,可能是万
|
else: # 100-100000000之间,可能是万
|
||||||
num = num / 10000
|
num = num / 10000
|
||||||
|
|
||||||
# 格式化显示
|
# 格式化显示
|
||||||
if abs(num) >= 1000:
|
if abs(num) >= 1000:
|
||||||
formatted = f"{num:,.2f}"
|
formatted = f"{num:,.2f}"
|
||||||
@@ -479,7 +482,7 @@ def format_number(value, unit='', suffix=''):
|
|||||||
formatted = f"{num:.2f}"
|
formatted = f"{num:.2f}"
|
||||||
else:
|
else:
|
||||||
formatted = f"{num:.4f}"
|
formatted = f"{num:.4f}"
|
||||||
|
|
||||||
return f"{formatted}{suffix}"
|
return f"{formatted}{suffix}"
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return str(value)
|
return str(value)
|
||||||
@@ -489,14 +492,14 @@ def run_main_force_batch_analysis():
|
|||||||
"""执行主力选股TOP股票批量分析(遵循统一调用规范)"""
|
"""执行主力选股TOP股票批量分析(遵循统一调用规范)"""
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
|
||||||
st.markdown("## 🚀 主力选股TOP股票批量分析")
|
st.markdown("## 🚀 主力选股TOP股票批量分析")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 检查是否已有分析结果
|
# 检查是否已有分析结果
|
||||||
if st.session_state.get('main_force_batch_results'):
|
if st.session_state.get('main_force_batch_results'):
|
||||||
display_main_force_batch_results(st.session_state.main_force_batch_results)
|
display_main_force_batch_results(st.session_state.main_force_batch_results)
|
||||||
|
|
||||||
# 返回按钮
|
# 返回按钮
|
||||||
col_back, col_clear = st.columns(2)
|
col_back, col_clear = st.columns(2)
|
||||||
with col_back:
|
with col_back:
|
||||||
@@ -509,28 +512,28 @@ def run_main_force_batch_analysis():
|
|||||||
if 'main_force_batch_results' in st.session_state:
|
if 'main_force_batch_results' in st.session_state:
|
||||||
del st.session_state.main_force_batch_results
|
del st.session_state.main_force_batch_results
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
with col_clear:
|
with col_clear:
|
||||||
if st.button("🔄 重新分析", width='content'):
|
if st.button("🔄 重新分析", width='content'):
|
||||||
# 清除结果,保留触发标志和代码
|
# 清除结果,保留触发标志和代码
|
||||||
if 'main_force_batch_results' in st.session_state:
|
if 'main_force_batch_results' in st.session_state:
|
||||||
del st.session_state.main_force_batch_results
|
del st.session_state.main_force_batch_results
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 获取股票代码列表
|
# 获取股票代码列表
|
||||||
stock_codes = st.session_state.get('main_force_batch_codes', [])
|
stock_codes = st.session_state.get('main_force_batch_codes', [])
|
||||||
|
|
||||||
if not stock_codes:
|
if not stock_codes:
|
||||||
st.error("未找到股票代码列表")
|
st.error("未找到股票代码列表")
|
||||||
# 清除触发标志
|
# 清除触发标志
|
||||||
if 'main_force_batch_trigger' in st.session_state:
|
if 'main_force_batch_trigger' in st.session_state:
|
||||||
del st.session_state.main_force_batch_trigger
|
del st.session_state.main_force_batch_trigger
|
||||||
return
|
return
|
||||||
|
|
||||||
st.info(f"即将分析 {len(stock_codes)} 只股票:{', '.join(stock_codes[:10])}{'...' if len(stock_codes) > 10 else ''}")
|
st.info(f"即将分析 {len(stock_codes)} 只股票:{', '.join(stock_codes[:10])}{'...' if len(stock_codes) > 10 else ''}")
|
||||||
|
|
||||||
# 返回按钮
|
# 返回按钮
|
||||||
if st.button("🔙 取消返回", type="secondary"):
|
if st.button("🔙 取消返回", type="secondary"):
|
||||||
# 清除所有批量分析相关状态
|
# 清除所有批量分析相关状态
|
||||||
@@ -539,12 +542,12 @@ def run_main_force_batch_analysis():
|
|||||||
if 'main_force_batch_codes' in st.session_state:
|
if 'main_force_batch_codes' in st.session_state:
|
||||||
del st.session_state.main_force_batch_codes
|
del st.session_state.main_force_batch_codes
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 分析选项
|
# 分析选项
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
analysis_mode = st.selectbox(
|
analysis_mode = st.selectbox(
|
||||||
"分析模式",
|
"分析模式",
|
||||||
@@ -552,7 +555,7 @@ def run_main_force_batch_analysis():
|
|||||||
format_func=lambda x: "顺序分析(稳定)" if x == "sequential" else "并行分析(快速)",
|
format_func=lambda x: "顺序分析(稳定)" if x == "sequential" else "并行分析(快速)",
|
||||||
help="顺序分析较慢但稳定,并行分析更快但消耗更多资源"
|
help="顺序分析较慢但稳定,并行分析更快但消耗更多资源"
|
||||||
)
|
)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if analysis_mode == "parallel":
|
if analysis_mode == "parallel":
|
||||||
max_workers = st.number_input(
|
max_workers = st.number_input(
|
||||||
@@ -564,17 +567,17 @@ def run_main_force_batch_analysis():
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
max_workers = 1
|
max_workers = 1
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 开始分析按钮
|
# 开始分析按钮
|
||||||
col_confirm, col_cancel = st.columns(2)
|
col_confirm, col_cancel = st.columns(2)
|
||||||
|
|
||||||
start_analysis = False
|
start_analysis = False
|
||||||
with col_confirm:
|
with col_confirm:
|
||||||
if st.button("🚀 确认开始分析", type="primary", width='content'):
|
if st.button("🚀 确认开始分析", type="primary", width='content'):
|
||||||
start_analysis = True
|
start_analysis = True
|
||||||
|
|
||||||
with col_cancel:
|
with col_cancel:
|
||||||
if st.button("❌ 取消", type="secondary", width='content'):
|
if st.button("❌ 取消", type="secondary", width='content'):
|
||||||
# 清除所有批量分析相关状态
|
# 清除所有批量分析相关状态
|
||||||
@@ -583,16 +586,16 @@ def run_main_force_batch_analysis():
|
|||||||
if 'main_force_batch_codes' in st.session_state:
|
if 'main_force_batch_codes' in st.session_state:
|
||||||
del st.session_state.main_force_batch_codes
|
del st.session_state.main_force_batch_codes
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if start_analysis:
|
if start_analysis:
|
||||||
# 导入统一分析函数(遵循统一规范)
|
# 导入统一分析函数(遵循统一规范)
|
||||||
from app import analyze_single_stock_for_batch
|
from app import analyze_single_stock_for_batch
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import time
|
import time
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.info("⏳ 正在执行批量分析,请稍候...")
|
st.info("⏳ 正在执行批量分析,请稍候...")
|
||||||
|
|
||||||
# 显示即将分析的股票代码(调试用)
|
# 显示即将分析的股票代码(调试用)
|
||||||
with st.expander("🔍 调试信息", expanded=True):
|
with st.expander("🔍 调试信息", expanded=True):
|
||||||
st.write(f"**股票代码数量**: {len(stock_codes)} 只")
|
st.write(f"**股票代码数量**: {len(stock_codes)} 只")
|
||||||
@@ -600,7 +603,7 @@ def run_main_force_batch_analysis():
|
|||||||
st.write(f"**代码格式检查**: {'✅ 无后缀,格式正确' if all('.' not in str(c) for c in stock_codes) else '❌ 包含后缀,可能有问题'}")
|
st.write(f"**代码格式检查**: {'✅ 无后缀,格式正确' if all('.' not in str(c) for c in stock_codes) else '❌ 包含后缀,可能有问题'}")
|
||||||
st.write(f"**分析模式**: {analysis_mode}")
|
st.write(f"**分析模式**: {analysis_mode}")
|
||||||
st.write(f"**线程数**: {max_workers if analysis_mode == 'parallel' else 1}")
|
st.write(f"**线程数**: {max_workers if analysis_mode == 'parallel' else 1}")
|
||||||
|
|
||||||
# 配置分析师参数
|
# 配置分析师参数
|
||||||
enabled_analysts_config = {
|
enabled_analysts_config = {
|
||||||
'technical': True,
|
'technical': True,
|
||||||
@@ -612,23 +615,23 @@ def run_main_force_batch_analysis():
|
|||||||
}
|
}
|
||||||
selected_model = 'deepseek-chat'
|
selected_model = 'deepseek-chat'
|
||||||
period = '1y'
|
period = '1y'
|
||||||
|
|
||||||
# 创建进度显示
|
# 创建进度显示
|
||||||
progress_bar = st.progress(0)
|
progress_bar = st.progress(0)
|
||||||
status_text = st.empty()
|
status_text = st.empty()
|
||||||
|
|
||||||
# 存储结果
|
# 存储结果
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# 记录开始时间
|
# 记录开始时间
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
if analysis_mode == "sequential":
|
if analysis_mode == "sequential":
|
||||||
# 顺序分析
|
# 顺序分析
|
||||||
for i, code in enumerate(stock_codes):
|
for i, code in enumerate(stock_codes):
|
||||||
status_text.text(f"正在分析 {code} ({i+1}/{len(stock_codes)})")
|
status_text.text(f"正在分析 {code} ({i+1}/{len(stock_codes)})")
|
||||||
progress_bar.progress((i + 1) / len(stock_codes))
|
progress_bar.progress((i + 1) / len(stock_codes))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 调用统一分析函数
|
# 调用统一分析函数
|
||||||
result = analyze_single_stock_for_batch(
|
result = analyze_single_stock_for_batch(
|
||||||
@@ -637,23 +640,23 @@ def run_main_force_batch_analysis():
|
|||||||
enabled_analysts_config=enabled_analysts_config,
|
enabled_analysts_config=enabled_analysts_config,
|
||||||
selected_model=selected_model
|
selected_model=selected_model
|
||||||
)
|
)
|
||||||
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append({
|
results.append({
|
||||||
"symbol": code,
|
"symbol": code,
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 并行分析
|
# 并行分析
|
||||||
status_text.text(f"并行分析 {len(stock_codes)} 只股票({max_workers}线程)...")
|
status_text.text(f"并行分析 {len(stock_codes)} 只股票({max_workers}线程)...")
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f"🚀 开始并行分析 {len(stock_codes)} 只股票")
|
print(f"🚀 开始并行分析 {len(stock_codes)} 只股票")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
def analyze_one(code):
|
def analyze_one(code):
|
||||||
try:
|
try:
|
||||||
print(f" 开始分析: {code}")
|
print(f" 开始分析: {code}")
|
||||||
@@ -668,10 +671,10 @@ def run_main_force_batch_analysis():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" 分析失败: {code} - {str(e)}")
|
print(f" 分析失败: {code} - {str(e)}")
|
||||||
return {"symbol": code, "success": False, "error": str(e)}
|
return {"symbol": code, "success": False, "error": str(e)}
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
futures = {executor.submit(analyze_one, code): code for code in stock_codes}
|
futures = {executor.submit(analyze_one, code): code for code in stock_codes}
|
||||||
|
|
||||||
completed = 0
|
completed = 0
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
code = futures[future] # 获取对应的股票代码
|
code = futures[future] # 获取对应的股票代码
|
||||||
@@ -679,48 +682,48 @@ def run_main_force_batch_analysis():
|
|||||||
progress = completed / len(stock_codes)
|
progress = completed / len(stock_codes)
|
||||||
progress_bar.progress(progress)
|
progress_bar.progress(progress)
|
||||||
status_text.text(f"已完成 {completed}/{len(stock_codes)} ({code})")
|
status_text.text(f"已完成 {completed}/{len(stock_codes)} ({code})")
|
||||||
|
|
||||||
print(f" 进度更新: {completed}/{len(stock_codes)} ({progress*100:.1f}%) - {code}")
|
print(f" 进度更新: {completed}/{len(stock_codes)} ({progress*100:.1f}%) - {code}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = future.result()
|
result = future.result()
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" 获取结果失败: {code} - {str(e)}")
|
print(f" 获取结果失败: {code} - {str(e)}")
|
||||||
results.append({"symbol": code, "success": False, "error": str(e)})
|
results.append({"symbol": code, "success": False, "error": str(e)})
|
||||||
|
|
||||||
print(f"\n✅ 所有并行任务已完成")
|
print(f"\n✅ 所有并行任务已完成")
|
||||||
print(f" 完成数: {completed}")
|
print(f" 完成数: {completed}")
|
||||||
print(f" 结果数: {len(results)}")
|
print(f" 结果数: {len(results)}")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
# 清除进度
|
# 清除进度
|
||||||
progress_bar.empty()
|
progress_bar.empty()
|
||||||
status_text.empty()
|
status_text.empty()
|
||||||
|
|
||||||
# 计算统计
|
# 计算统计
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
success_count = sum(1 for r in results if r.get("success", False))
|
success_count = sum(1 for r in results if r.get("success", False))
|
||||||
failed_count = len(results) - success_count
|
failed_count = len(results) - success_count
|
||||||
|
|
||||||
# 显示完成信息
|
# 显示完成信息
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
st.success(f"✅ 批量分析完成!成功 {success_count} 只,失败 {failed_count} 只,耗时 {elapsed_time/60:.1f} 分钟")
|
st.success(f"✅ 批量分析完成!成功 {success_count} 只,失败 {failed_count} 只,耗时 {elapsed_time/60:.1f} 分钟")
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ 批量分析完成,但所有 {failed_count} 只股票都分析失败!")
|
st.error(f"❌ 批量分析完成,但所有 {failed_count} 只股票都分析失败!")
|
||||||
|
|
||||||
# 显示失败原因(调试用)
|
# 显示失败原因(调试用)
|
||||||
with st.expander("❌ 查看失败原因", expanded=True):
|
with st.expander("❌ 查看失败原因", expanded=True):
|
||||||
for r in results:
|
for r in results:
|
||||||
if not r.get("success", False):
|
if not r.get("success", False):
|
||||||
st.error(f"**{r.get('symbol', 'N/A')}**: {r.get('error', '未知错误')}")
|
st.error(f"**{r.get('symbol', 'N/A')}**: {r.get('error', '未知错误')}")
|
||||||
|
|
||||||
# 先保存到数据库历史记录(在 rerun 之前完成)
|
# 先保存到数据库历史记录(在 rerun 之前完成)
|
||||||
save_success = False
|
save_success = False
|
||||||
save_error = None
|
save_error = None
|
||||||
try:
|
try:
|
||||||
from main_force_batch_db import batch_db
|
from main_force_batch_db import batch_db
|
||||||
|
|
||||||
# 调试信息
|
# 调试信息
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f"📝 准备保存批量分析结果到历史记录")
|
print(f"📝 准备保存批量分析结果到历史记录")
|
||||||
@@ -731,17 +734,17 @@ def run_main_force_batch_analysis():
|
|||||||
print(f"失败数: {failed_count}")
|
print(f"失败数: {failed_count}")
|
||||||
print(f"总耗时: {elapsed_time:.2f}秒")
|
print(f"总耗时: {elapsed_time:.2f}秒")
|
||||||
print(f"结果数: {len(results)}")
|
print(f"结果数: {len(results)}")
|
||||||
|
|
||||||
# 检查结果数据类型
|
# 检查结果数据类型
|
||||||
print(f"\n检查结果数据类型:")
|
print(f"\n检查结果数据类型:")
|
||||||
for i, result in enumerate(results[:3]): # 只检查前3个
|
for i, result in enumerate(results[:3]): # 只检查前3个
|
||||||
print(f" 结果 {i+1}:")
|
print(f" 结果 {i+1}:")
|
||||||
for key, value in list(result.items())[:5]: # 只检查前5个字段
|
for key, value in list(result.items())[:5]: # 只检查前5个字段
|
||||||
print(f" - {key}: {type(value).__name__}")
|
print(f" - {key}: {type(value).__name__}")
|
||||||
|
|
||||||
print(f"\n开始保存到数据库...")
|
print(f"\n开始保存到数据库...")
|
||||||
save_start = time.time()
|
save_start = time.time()
|
||||||
|
|
||||||
# 保存到数据库
|
# 保存到数据库
|
||||||
record_id = batch_db.save_batch_analysis(
|
record_id = batch_db.save_batch_analysis(
|
||||||
batch_count=len(stock_codes),
|
batch_count=len(stock_codes),
|
||||||
@@ -751,14 +754,14 @@ def run_main_force_batch_analysis():
|
|||||||
total_time=elapsed_time,
|
total_time=elapsed_time,
|
||||||
results=results
|
results=results
|
||||||
)
|
)
|
||||||
|
|
||||||
save_elapsed = time.time() - save_start
|
save_elapsed = time.time() - save_start
|
||||||
print(f"✅ 批量分析结果已保存到历史记录")
|
print(f"✅ 批量分析结果已保存到历史记录")
|
||||||
print(f" 记录ID: {record_id}")
|
print(f" 记录ID: {record_id}")
|
||||||
print(f" 保存耗时: {save_elapsed:.2f}秒")
|
print(f" 保存耗时: {save_elapsed:.2f}秒")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
save_success = True
|
save_success = True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
save_error = str(e)
|
save_error = str(e)
|
||||||
@@ -769,7 +772,7 @@ def run_main_force_batch_analysis():
|
|||||||
print(f"详细错误:")
|
print(f"详细错误:")
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
# 保存结果到session_state
|
# 保存结果到session_state
|
||||||
st.session_state.main_force_batch_results = {
|
st.session_state.main_force_batch_results = {
|
||||||
"results": results,
|
"results": results,
|
||||||
@@ -781,9 +784,9 @@ def run_main_force_batch_analysis():
|
|||||||
"saved_to_history": save_success,
|
"saved_to_history": save_success,
|
||||||
"save_error": save_error
|
"save_error": save_error
|
||||||
}
|
}
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# 重新渲染以显示结果
|
# 重新渲染以显示结果
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
@@ -791,7 +794,7 @@ def run_main_force_batch_analysis():
|
|||||||
def display_main_force_batch_results(batch_results):
|
def display_main_force_batch_results(batch_results):
|
||||||
"""显示主力选股批量分析结果"""
|
"""显示主力选股批量分析结果"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
results = batch_results['results']
|
results = batch_results['results']
|
||||||
total = batch_results['total']
|
total = batch_results['total']
|
||||||
success = batch_results['success']
|
success = batch_results['success']
|
||||||
@@ -799,46 +802,46 @@ def display_main_force_batch_results(batch_results):
|
|||||||
elapsed_time = batch_results['elapsed_time']
|
elapsed_time = batch_results['elapsed_time']
|
||||||
saved_to_history = batch_results.get('saved_to_history', False)
|
saved_to_history = batch_results.get('saved_to_history', False)
|
||||||
save_error = batch_results.get('save_error')
|
save_error = batch_results.get('save_error')
|
||||||
|
|
||||||
st.markdown("## 📊 批量分析结果")
|
st.markdown("## 📊 批量分析结果")
|
||||||
|
|
||||||
# 显示保存状态
|
# 显示保存状态
|
||||||
if saved_to_history:
|
if saved_to_history:
|
||||||
st.success("✅ 分析结果已自动保存到历史记录,可点击右上角'📚 批量分析历史'查看")
|
st.success("✅ 分析结果已自动保存到历史记录,可点击右上角'📚 批量分析历史'查看")
|
||||||
elif save_error:
|
elif save_error:
|
||||||
st.warning(f"⚠️ 历史记录保存失败: {save_error},但结果仍可查看")
|
st.warning(f"⚠️ 历史记录保存失败: {save_error},但结果仍可查看")
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 统计信息
|
# 统计信息
|
||||||
col1, col2, col3, col4 = st.columns(4)
|
col1, col2, col3, col4 = st.columns(4)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("总计分析", f"{total} 只")
|
st.metric("总计分析", f"{total} 只")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("成功分析", f"{success} 只", delta=f"{success/total*100:.1f}%")
|
st.metric("成功分析", f"{success} 只", delta=f"{success/total*100:.1f}%")
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
st.metric("失败分析", f"{failed} 只")
|
st.metric("失败分析", f"{failed} 只")
|
||||||
|
|
||||||
with col4:
|
with col4:
|
||||||
st.metric("总耗时", f"{elapsed_time/60:.1f} 分钟")
|
st.metric("总耗时", f"{elapsed_time/60:.1f} 分钟")
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# 成功分析的股票
|
# 成功分析的股票
|
||||||
successful_results = [r for r in results if r['success']]
|
successful_results = [r for r in results if r['success']]
|
||||||
|
|
||||||
if successful_results:
|
if successful_results:
|
||||||
st.markdown(f"### ✅ 成功分析的股票 ({len(successful_results)}只)")
|
st.markdown(f"### ✅ 成功分析的股票 ({len(successful_results)}只)")
|
||||||
|
|
||||||
# 创建DataFrame展示
|
# 创建DataFrame展示
|
||||||
display_data = []
|
display_data = []
|
||||||
for result in successful_results:
|
for result in successful_results:
|
||||||
stock_info = result.get('stock_info', {})
|
stock_info = result.get('stock_info', {})
|
||||||
final_decision = result.get('final_decision', {})
|
final_decision = result.get('final_decision', {})
|
||||||
|
|
||||||
# 提取评级emoji
|
# 提取评级emoji
|
||||||
rating = final_decision.get('rating', '未知')
|
rating = final_decision.get('rating', '未知')
|
||||||
rating_emoji = {
|
rating_emoji = {
|
||||||
@@ -848,7 +851,7 @@ def display_main_force_batch_results(batch_results):
|
|||||||
'卖出': '⚠️',
|
'卖出': '⚠️',
|
||||||
'强烈卖出': '🚫'
|
'强烈卖出': '🚫'
|
||||||
}.get(rating, '❓')
|
}.get(rating, '❓')
|
||||||
|
|
||||||
display_data.append({
|
display_data.append({
|
||||||
'股票代码': stock_info.get('symbol', ''),
|
'股票代码': stock_info.get('symbol', ''),
|
||||||
'股票名称': stock_info.get('name', ''),
|
'股票名称': stock_info.get('name', ''),
|
||||||
@@ -859,9 +862,9 @@ def display_main_force_batch_results(batch_results):
|
|||||||
'止损位': final_decision.get('stop_loss', 'N/A'),
|
'止损位': final_decision.get('stop_loss', 'N/A'),
|
||||||
'目标价': final_decision.get('target_price', 'N/A')
|
'目标价': final_decision.get('target_price', 'N/A')
|
||||||
})
|
})
|
||||||
|
|
||||||
df_display = pd.DataFrame(display_data)
|
df_display = pd.DataFrame(display_data)
|
||||||
|
|
||||||
# 类型统一,避免Arrow序列化错误
|
# 类型统一,避免Arrow序列化错误
|
||||||
numeric_cols = ['信心度', '止盈位', '止损位', '目标价']
|
numeric_cols = ['信心度', '止盈位', '止损位', '目标价']
|
||||||
for col in numeric_cols:
|
for col in numeric_cols:
|
||||||
@@ -872,17 +875,17 @@ def display_main_force_batch_results(batch_results):
|
|||||||
for col in text_cols:
|
for col in text_cols:
|
||||||
if col in df_display.columns:
|
if col in df_display.columns:
|
||||||
df_display[col] = df_display[col].astype(str)
|
df_display[col] = df_display[col].astype(str)
|
||||||
|
|
||||||
st.dataframe(df_display, width='content', height=400)
|
st.dataframe(df_display, width='content', height=400)
|
||||||
|
|
||||||
# 详细分析结果(可展开)
|
# 详细分析结果(可展开)
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📋 详细分析报告")
|
st.markdown("### 📋 详细分析报告")
|
||||||
|
|
||||||
for result in successful_results:
|
for result in successful_results:
|
||||||
stock_info = result.get('stock_info', {})
|
stock_info = result.get('stock_info', {})
|
||||||
final_decision = result.get('final_decision', {})
|
final_decision = result.get('final_decision', {})
|
||||||
|
|
||||||
symbol = stock_info.get('symbol', '')
|
symbol = stock_info.get('symbol', '')
|
||||||
name = stock_info.get('name', '')
|
name = stock_info.get('name', '')
|
||||||
rating = final_decision.get('rating', '未知')
|
rating = final_decision.get('rating', '未知')
|
||||||
@@ -893,34 +896,34 @@ def display_main_force_batch_results(batch_results):
|
|||||||
'卖出': '⚠️',
|
'卖出': '⚠️',
|
||||||
'强烈卖出': '🚫'
|
'强烈卖出': '🚫'
|
||||||
}.get(rating, '❓')
|
}.get(rating, '❓')
|
||||||
|
|
||||||
with st.expander(f"{rating_emoji} {symbol} - {name} | {rating}"):
|
with st.expander(f"{rating_emoji} {symbol} - {name} | {rating}"):
|
||||||
# 关键信息
|
# 关键信息
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("信心度", final_decision.get('confidence_level', 'N/A'))
|
st.metric("信心度", final_decision.get('confidence_level', 'N/A'))
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("进场区间", final_decision.get('entry_range', 'N/A'))
|
st.metric("进场区间", final_decision.get('entry_range', 'N/A'))
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
st.metric("目标价", final_decision.get('target_price', 'N/A'))
|
st.metric("目标价", final_decision.get('target_price', 'N/A'))
|
||||||
|
|
||||||
# 止盈止损
|
# 止盈止损
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("止盈位", final_decision.get('take_profit', 'N/A'))
|
st.metric("止盈位", final_decision.get('take_profit', 'N/A'))
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("止损位", final_decision.get('stop_loss', 'N/A'))
|
st.metric("止损位", final_decision.get('stop_loss', 'N/A'))
|
||||||
|
|
||||||
# 投资建议
|
# 投资建议
|
||||||
st.markdown("#### 💡 投资建议")
|
st.markdown("#### 💡 投资建议")
|
||||||
advice = final_decision.get('operation_advice', final_decision.get('advice', '暂无建议'))
|
advice = final_decision.get('operation_advice', final_decision.get('advice', '暂无建议'))
|
||||||
st.info(advice)
|
st.info(advice)
|
||||||
|
|
||||||
# 加入监测按钮
|
# 加入监测按钮
|
||||||
if st.button(f"➕ 加入监测列表", key=f"monitor_{symbol}"):
|
if st.button(f"➕ 加入监测列表", key=f"monitor_{symbol}"):
|
||||||
# 解析进场区间
|
# 解析进场区间
|
||||||
@@ -933,7 +936,7 @@ def display_main_force_batch_results(batch_results):
|
|||||||
entry_max = float(parts[1].strip())
|
entry_max = float(parts[1].strip())
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 解析止盈止损
|
# 解析止盈止损
|
||||||
take_profit_str = final_decision.get('take_profit', '')
|
take_profit_str = final_decision.get('take_profit', '')
|
||||||
take_profit = None
|
take_profit = None
|
||||||
@@ -944,7 +947,7 @@ def display_main_force_batch_results(batch_results):
|
|||||||
take_profit = float(numbers[0])
|
take_profit = float(numbers[0])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
stop_loss_str = final_decision.get('stop_loss', '')
|
stop_loss_str = final_decision.get('stop_loss', '')
|
||||||
stop_loss = None
|
stop_loss = None
|
||||||
if stop_loss_str:
|
if stop_loss_str:
|
||||||
@@ -954,16 +957,16 @@ def display_main_force_batch_results(batch_results):
|
|||||||
stop_loss = float(numbers[0])
|
stop_loss = float(numbers[0])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 调用监测管理器添加
|
# 调用监测管理器添加
|
||||||
from monitor_db import monitor_db
|
from monitor_db import monitor_db
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 准备进场区间数据
|
# 准备进场区间数据
|
||||||
entry_range_dict = {}
|
entry_range_dict = {}
|
||||||
if entry_min and entry_max:
|
if entry_min and entry_max:
|
||||||
entry_range_dict = {"min": entry_min, "max": entry_max}
|
entry_range_dict = {"min": entry_min, "max": entry_max}
|
||||||
|
|
||||||
# 添加到监测列表
|
# 添加到监测列表
|
||||||
monitor_db.add_monitored_stock(
|
monitor_db.add_monitored_stock(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
@@ -976,21 +979,21 @@ def display_main_force_batch_results(batch_results):
|
|||||||
st.success(f"✅ {symbol} - {name} 已加入监测列表")
|
st.success(f"✅ {symbol} - {name} 已加入监测列表")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"❌ 添加失败: {str(e)}")
|
st.error(f"❌ 添加失败: {str(e)}")
|
||||||
|
|
||||||
# 失败的股票
|
# 失败的股票
|
||||||
failed_results = [r for r in results if not r['success']]
|
failed_results = [r for r in results if not r['success']]
|
||||||
|
|
||||||
if failed_results:
|
if failed_results:
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown(f"### ❌ 分析失败的股票 ({len(failed_results)}只)")
|
st.markdown(f"### ❌ 分析失败的股票 ({len(failed_results)}只)")
|
||||||
|
|
||||||
failed_data = []
|
failed_data = []
|
||||||
for result in failed_results:
|
for result in failed_results:
|
||||||
failed_data.append({
|
failed_data.append({
|
||||||
'股票代码': result.get('symbol', ''),
|
'股票代码': result.get('symbol', ''),
|
||||||
'失败原因': result.get('error', '未知错误')
|
'失败原因': result.get('error', '未知错误')
|
||||||
})
|
})
|
||||||
|
|
||||||
df_failed = pd.DataFrame(failed_data)
|
df_failed = pd.DataFrame(failed_data)
|
||||||
st.dataframe(df_failed, width='content')
|
st.dataframe(df_failed, width='content')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
模型配置文件
|
||||||
|
包含所有可用的AI模型选项
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_options = {
|
||||||
|
"deepseek-chat": "DeepSeek Chat (默认)",
|
||||||
|
"deepseek-reasoner": "DeepSeek Reasoner (推理增强)",
|
||||||
|
"qwen-plus": "qwen-plus (阿里百炼)",
|
||||||
|
"qwen-plus-latest": "qwen-plus-latest (阿里百炼)",
|
||||||
|
"qwen-flash": "qwen-flash (阿里百炼)",
|
||||||
|
"qwen-turbo": "qwen-turbo (阿里百炼)",
|
||||||
|
"qwen3-max": "qwen-max (阿里百炼)",
|
||||||
|
"qwen-long": "qwen-long (阿里百炼)",
|
||||||
|
"deepseek-ai/DeepSeek-R1-0528-Qwen3-8B": "DeepSeek-R1 免费(硅基流动)",
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct": "Qwen 免费(硅基流动)",
|
||||||
|
"Pro/deepseek-ai/DeepSeek-V3.1-Terminus": "DeepSeek-V3.1-Terminus (硅基流动)",
|
||||||
|
"deepseek-ai/DeepSeek-R1": "DeepSeek-R1 (硅基流动)",
|
||||||
|
"Qwen/Qwen3-235B-A22B-Thinking-2507": "Qwen3-235B (硅基流动)",
|
||||||
|
"zai-org/GLM-4.6": "智谱(硅基流动)",
|
||||||
|
"moonshotai/Kimi-K2-Instruct-0905": "Kimi (硅基流动)",
|
||||||
|
"Ring-1T": "蚂蚁百灵 (硅基流动)",
|
||||||
|
"step3": "阶跃星辰(硅基流动)"
|
||||||
|
}
|
||||||
+160
-2
@@ -259,6 +259,128 @@ def create_download_link(pdf_content, filename):
|
|||||||
href = f'<a href="data:application/pdf;base64,{b64}" download="{filename}" style="display: inline-block; padding: 15px 30px; background-color: #e74c3c; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; margin: 10px;">📄 下载PDF报告</a>'
|
href = f'<a href="data:application/pdf;base64,{b64}" download="{filename}" style="display: inline-block; padding: 15px 30px; background-color: #e74c3c; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; margin: 10px;">📄 下载PDF报告</a>'
|
||||||
return href
|
return href
|
||||||
|
|
||||||
|
def generate_markdown_report(stock_info, agents_results, discussion_result, final_decision):
|
||||||
|
"""生成Markdown格式的分析报告"""
|
||||||
|
|
||||||
|
# 获取当前时间
|
||||||
|
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||||
|
|
||||||
|
markdown_content = f"""
|
||||||
|
# AI股票分析报告
|
||||||
|
|
||||||
|
**生成时间**: {current_time}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 股票基本信息
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| **股票代码** | {stock_info.get('symbol', 'N/A')} |
|
||||||
|
| **股票名称** | {stock_info.get('name', 'N/A')} |
|
||||||
|
| **当前价格** | {stock_info.get('current_price', 'N/A')} |
|
||||||
|
| **涨跌幅** | {stock_info.get('change_percent', 'N/A')}% |
|
||||||
|
| **市盈率(PE)** | {stock_info.get('pe_ratio', 'N/A')} |
|
||||||
|
| **市净率(PB)** | {stock_info.get('pb_ratio', 'N/A')} |
|
||||||
|
| **市值** | {stock_info.get('market_cap', 'N/A')} |
|
||||||
|
| **市场** | {stock_info.get('market', 'N/A')} |
|
||||||
|
| **交易所** | {stock_info.get('exchange', 'N/A')} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 各分析师详细分析
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 添加各分析师的分析结果
|
||||||
|
agent_names = {
|
||||||
|
'technical': '📈 技术分析师',
|
||||||
|
'fundamental': '📊 基本面分析师',
|
||||||
|
'fund_flow': '💰 资金面分析师',
|
||||||
|
'risk_management': '⚠️ 风险管理师',
|
||||||
|
'market_sentiment': '📈 市场情绪分析师'
|
||||||
|
}
|
||||||
|
|
||||||
|
for agent_key, agent_name in agent_names.items():
|
||||||
|
if agent_key in agents_results:
|
||||||
|
agent_result = agents_results[agent_key]
|
||||||
|
if isinstance(agent_result, dict):
|
||||||
|
analysis_text = agent_result.get('analysis', '暂无分析')
|
||||||
|
else:
|
||||||
|
analysis_text = str(agent_result)
|
||||||
|
|
||||||
|
markdown_content += f"""
|
||||||
|
### {agent_name}
|
||||||
|
|
||||||
|
{analysis_text}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 添加团队讨论结果
|
||||||
|
markdown_content += f"""
|
||||||
|
## 🤝 团队综合讨论
|
||||||
|
|
||||||
|
{discussion_result}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 最终投资决策
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 处理最终决策的显示
|
||||||
|
if isinstance(final_decision, dict) and "decision_text" not in final_decision:
|
||||||
|
# JSON格式的决策
|
||||||
|
markdown_content += f"""
|
||||||
|
**投资评级**: {final_decision.get('rating', '未知')}
|
||||||
|
|
||||||
|
**目标价位**: {final_decision.get('target_price', 'N/A')}
|
||||||
|
|
||||||
|
**操作建议**: {final_decision.get('operation_advice', '暂无建议')}
|
||||||
|
|
||||||
|
**进场区间**: {final_decision.get('entry_range', 'N/A')}
|
||||||
|
|
||||||
|
**止盈位**: {final_decision.get('take_profit', 'N/A')}
|
||||||
|
|
||||||
|
**止损位**: {final_decision.get('stop_loss', 'N/A')}
|
||||||
|
|
||||||
|
**持有周期**: {final_decision.get('holding_period', 'N/A')}
|
||||||
|
|
||||||
|
**仓位建议**: {final_decision.get('position_size', 'N/A')}
|
||||||
|
|
||||||
|
**信心度**: {final_decision.get('confidence_level', 'N/A')}/10
|
||||||
|
|
||||||
|
**风险提示**: {final_decision.get('risk_warning', '无')}
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
# 文本格式的决策
|
||||||
|
decision_text = final_decision.get('decision_text', str(final_decision))
|
||||||
|
markdown_content += decision_text
|
||||||
|
|
||||||
|
markdown_content += """
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 免责声明
|
||||||
|
|
||||||
|
本报告由AI系统生成,仅供参考,不构成投资建议。投资有风险,入市需谨慎。请在做出投资决策前咨询专业的投资顾问。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*报告生成时间: {current_time}*
|
||||||
|
*AI股票分析系统 v1.0*
|
||||||
|
"""
|
||||||
|
|
||||||
|
return markdown_content
|
||||||
|
|
||||||
|
def create_markdown_download_link(markdown_content, filename):
|
||||||
|
"""创建Markdown下载链接"""
|
||||||
|
b64 = base64.b64encode(markdown_content.encode()).decode()
|
||||||
|
href = f'<a href="data:text/markdown;base64,{b64}" download="{filename}" style="display: inline-block; padding: 15px 30px; background-color: #9b59b6; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; margin: 10px;">📝 下载Markdown报告</a>'
|
||||||
|
return href
|
||||||
|
|
||||||
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
||||||
"""显示PDF导出区域"""
|
"""显示PDF导出区域"""
|
||||||
|
|
||||||
@@ -269,8 +391,11 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
|
|||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
# 生成PDF报告按钮(使用股票代码作为key的一部分,确保唯一性)
|
# 生成PDF报告按钮(使用股票代码作为key的一部分,确保唯一性)
|
||||||
button_key = f"pdf_btn_{stock_info.get('symbol', 'unknown')}"
|
pdf_button_key = f"pdf_btn_{stock_info.get('symbol', 'unknown')}"
|
||||||
if st.button("📄 生成并下载PDF报告", type="primary", width='content', key=button_key):
|
markdown_button_key = f"markdown_btn_{stock_info.get('symbol', 'unknown')}"
|
||||||
|
|
||||||
|
# 生成PDF报告按钮
|
||||||
|
if st.button("📄 生成并下载PDF报告", type="primary", width='content', key=pdf_button_key):
|
||||||
with st.spinner("正在生成PDF报告..."):
|
with st.spinner("正在生成PDF报告..."):
|
||||||
try:
|
try:
|
||||||
# 生成PDF内容
|
# 生成PDF内容
|
||||||
@@ -300,3 +425,36 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
|
|||||||
st.error(f"❌ 生成PDF报告时出错: {str(e)}")
|
st.error(f"❌ 生成PDF报告时出错: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
st.error(f"详细错误信息: {traceback.format_exc()}")
|
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
# 生成Markdown报告按钮
|
||||||
|
if st.button("📝 生成并下载Markdown报告", type="secondary", width='content', key=markdown_button_key):
|
||||||
|
with st.spinner("正在生成Markdown报告..."):
|
||||||
|
try:
|
||||||
|
# 生成Markdown内容
|
||||||
|
markdown_content = generate_markdown_report(stock_info, agents_results, discussion_result, final_decision)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"股票分析报告_{stock_symbol}_{timestamp}.md"
|
||||||
|
|
||||||
|
st.success("✅ Markdown报告生成成功!")
|
||||||
|
st.balloons()
|
||||||
|
|
||||||
|
# 显示下载链接
|
||||||
|
st.markdown("### 📄 报告下载")
|
||||||
|
|
||||||
|
download_link = create_markdown_download_link(markdown_content, filename)
|
||||||
|
st.markdown(f"""
|
||||||
|
<div style="text-align: center; margin: 20px 0;">
|
||||||
|
{download_link}
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.info("💡 提示:点击上方按钮即可下载Markdown格式的完整分析报告")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ 生成Markdown报告时出错: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
|||||||
+46
-148
@@ -133,151 +133,6 @@ def create_html_download_link(content, filename, link_text):
|
|||||||
href = f'<a href="data:text/html;base64,{b64}" download="{filename}" style="display: inline-block; padding: 10px 20px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 5px; margin: 5px;">{link_text}</a>'
|
href = f'<a href="data:text/html;base64,{b64}" download="{filename}" style="display: inline-block; padding: 10px 20px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 5px; margin: 5px;">{link_text}</a>'
|
||||||
return href
|
return href
|
||||||
|
|
||||||
def generate_html_content(markdown_content):
|
|
||||||
"""将Markdown转换为HTML"""
|
|
||||||
html_content = f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>AI股票分析报告</title>
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}}
|
|
||||||
.container {{
|
|
||||||
background-color: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}}
|
|
||||||
h1 {{
|
|
||||||
color: #2c3e50;
|
|
||||||
border-bottom: 3px solid #3498db;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}}
|
|
||||||
h2 {{
|
|
||||||
color: #34495e;
|
|
||||||
border-left: 4px solid #3498db;
|
|
||||||
padding-left: 15px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}}
|
|
||||||
h3 {{
|
|
||||||
color: #2980b9;
|
|
||||||
margin-top: 25px;
|
|
||||||
}}
|
|
||||||
table {{
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 20px 0;
|
|
||||||
}}
|
|
||||||
th, td {{
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
}}
|
|
||||||
th {{
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
}}
|
|
||||||
tr:nth-child(even) {{
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}}
|
|
||||||
.disclaimer {{
|
|
||||||
background-color: #fff3cd;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}}
|
|
||||||
.footer {{
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 30px;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-style: italic;
|
|
||||||
}}
|
|
||||||
hr {{
|
|
||||||
border: none;
|
|
||||||
height: 2px;
|
|
||||||
background-color: #ecf0f1;
|
|
||||||
margin: 20px 0;
|
|
||||||
}}
|
|
||||||
strong {{
|
|
||||||
color: #2c3e50;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 简单的Markdown到HTML转换
|
|
||||||
html_body = markdown_content
|
|
||||||
html_body = html_body.replace('\n# ', '\n<h1>').replace('\n## ', '\n<h2>').replace('\n### ', '\n<h3>')
|
|
||||||
html_body = html_body.replace('# ', '<h1>').replace('## ', '<h2>').replace('### ', '<h3>')
|
|
||||||
html_body = html_body.replace('\n---\n', '\n<hr>\n')
|
|
||||||
|
|
||||||
# 处理粗体文本
|
|
||||||
html_body = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_body)
|
|
||||||
|
|
||||||
# 处理表格
|
|
||||||
lines = html_body.split('\n')
|
|
||||||
in_table = False
|
|
||||||
processed_lines = []
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if '|' in line and not in_table and line.strip().startswith('|'):
|
|
||||||
processed_lines.append('<table>')
|
|
||||||
in_table = True
|
|
||||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
|
||||||
processed_lines.append('<tr>')
|
|
||||||
for cell in cells:
|
|
||||||
processed_lines.append(f'<th>{cell}</th>')
|
|
||||||
processed_lines.append('</tr>')
|
|
||||||
elif '|' in line and in_table:
|
|
||||||
if '---' not in line:
|
|
||||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
|
||||||
processed_lines.append('<tr>')
|
|
||||||
for cell in cells:
|
|
||||||
processed_lines.append(f'<td>{cell}</td>')
|
|
||||||
processed_lines.append('</tr>')
|
|
||||||
elif in_table and '|' not in line:
|
|
||||||
processed_lines.append('</table>')
|
|
||||||
processed_lines.append(line)
|
|
||||||
in_table = False
|
|
||||||
else:
|
|
||||||
processed_lines.append(line)
|
|
||||||
|
|
||||||
if in_table:
|
|
||||||
processed_lines.append('</table>')
|
|
||||||
|
|
||||||
html_body = '\n'.join(processed_lines)
|
|
||||||
|
|
||||||
# 处理段落
|
|
||||||
paragraphs = html_body.split('\n\n')
|
|
||||||
processed_paragraphs = []
|
|
||||||
for para in paragraphs:
|
|
||||||
para = para.strip()
|
|
||||||
if para and not para.startswith('<') and not para.startswith('---'):
|
|
||||||
processed_paragraphs.append(f'<p>{para}</p>')
|
|
||||||
else:
|
|
||||||
processed_paragraphs.append(para)
|
|
||||||
|
|
||||||
html_body = '\n'.join(processed_paragraphs)
|
|
||||||
|
|
||||||
html_content += html_body + """
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return html_content
|
|
||||||
|
|
||||||
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
||||||
"""显示PDF导出区域 - 修复报告生成问题"""
|
"""显示PDF导出区域 - 修复报告生成问题"""
|
||||||
|
|
||||||
@@ -290,8 +145,11 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
|
|||||||
# 生成报告按钮
|
# 生成报告按钮
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
button_key = f"generate_report_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
pdf_button_key = f"generate_report_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||||
if st.button("📊 生成并下载报告", type="primary", width='content', key=button_key):
|
markdown_button_key = f"generate_markdown_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# 生成PDF和HTML报告按钮
|
||||||
|
if st.button("📊 生成并下载报告(PDF/HTML)", type="primary", width='content', key=pdf_button_key):
|
||||||
with st.spinner("正在生成报告..."):
|
with st.spinner("正在生成报告..."):
|
||||||
try:
|
try:
|
||||||
# 生成Markdown内容
|
# 生成Markdown内容
|
||||||
@@ -337,4 +195,44 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"❌ 生成报告时出错: {str(e)}")
|
st.error(f"❌ 生成报告时出错: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
st.error(f"详细错误信息: {traceback.format_exc()}")
|
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
# 单独生成Markdown报告按钮
|
||||||
|
if st.button("📝 生成并下载Markdown报告", type="secondary", width='content', key=markdown_button_key):
|
||||||
|
with st.spinner("正在生成Markdown报告..."):
|
||||||
|
try:
|
||||||
|
# 生成Markdown内容
|
||||||
|
markdown_content = generate_markdown_report(stock_info, agents_results, discussion_result, final_decision)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"股票分析报告_{stock_symbol}_{timestamp}.md"
|
||||||
|
|
||||||
|
st.success("✅ Markdown报告生成成功!")
|
||||||
|
st.balloons()
|
||||||
|
|
||||||
|
# 显示下载链接
|
||||||
|
st.markdown("### 📄 报告下载")
|
||||||
|
|
||||||
|
# 创建下载链接
|
||||||
|
md_link = create_download_link(
|
||||||
|
markdown_content,
|
||||||
|
filename,
|
||||||
|
"📝 下载Markdown报告"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 显示下载链接
|
||||||
|
st.markdown(f"""
|
||||||
|
<div style="text-align: center; margin: 20px 0;">
|
||||||
|
{md_link}
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.info("💡 提示:点击上方按钮即可下载Markdown格式的报告文件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ 生成Markdown报告时出错: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
|||||||
+36
-1
@@ -267,12 +267,47 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
|
|||||||
col1, col2, col3 = st.columns([1, 2, 1])
|
col1, col2, col3 = st.columns([1, 2, 1])
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("📊 生成并下载报告", type="primary", width='content', key="generate_report_btn"):
|
pdf_button_key = "generate_report_btn"
|
||||||
|
markdown_button_key = "generate_markdown_btn"
|
||||||
|
|
||||||
|
# 生成PDF报告按钮
|
||||||
|
if st.button("📊 生成并下载报告(PDF/HTML)", type="primary", width='content', key=pdf_button_key):
|
||||||
st.session_state.show_download_links = True
|
st.session_state.show_download_links = True
|
||||||
with st.spinner("正在生成报告..."):
|
with st.spinner("正在生成报告..."):
|
||||||
success = generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
|
success = generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
|
||||||
if success:
|
if success:
|
||||||
st.balloons()
|
st.balloons()
|
||||||
|
|
||||||
|
# 生成Markdown报告按钮
|
||||||
|
if st.button("📝 生成并下载Markdown报告", type="secondary", width='content', key=markdown_button_key):
|
||||||
|
with st.spinner("正在生成Markdown报告..."):
|
||||||
|
try:
|
||||||
|
# 生成Markdown内容
|
||||||
|
markdown_content = generate_markdown_report(stock_info, agents_results, discussion_result, final_decision)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"股票分析报告_{stock_symbol}_{timestamp}.md"
|
||||||
|
|
||||||
|
st.success("✅ Markdown报告生成成功!")
|
||||||
|
st.balloons()
|
||||||
|
|
||||||
|
# 显示下载链接
|
||||||
|
st.markdown("### 📄 报告下载")
|
||||||
|
|
||||||
|
# Markdown下载链接
|
||||||
|
md_download_link = create_download_link(
|
||||||
|
markdown_content,
|
||||||
|
filename,
|
||||||
|
"📝 下载Markdown报告"
|
||||||
|
)
|
||||||
|
st.markdown(md_download_link, unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.info("💡 提示:点击上方按钮即可下载Markdown格式的报告文件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ 生成Markdown报告时出错: {str(e)}")
|
||||||
|
|
||||||
# 如果已经生成了报告,显示下载链接
|
# 如果已经生成了报告,显示下载链接
|
||||||
if st.session_state.show_download_links:
|
if st.session_state.show_download_links:
|
||||||
|
|||||||
+215
-4
@@ -116,9 +116,12 @@ def display_analysis_tab():
|
|||||||
col1, col2, col3 = st.columns([2, 2, 2])
|
col1, col2, col3 = st.columns([2, 2, 2])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
|
# 导入model_config.py中定义的model_options
|
||||||
|
from model_config import model_options as app_model_options
|
||||||
selected_model = st.selectbox(
|
selected_model = st.selectbox(
|
||||||
"选择AI模型",
|
"AI模型",
|
||||||
["deepseek-chat", "deepseek-reasoner"],
|
list(app_model_options.keys()),
|
||||||
|
format_func=lambda x: app_model_options[x],
|
||||||
help="Reasoner模型提供更强的推理能力"
|
help="Reasoner模型提供更强的推理能力"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -774,10 +777,10 @@ def display_pdf_export_section(result):
|
|||||||
"""显示PDF导出部分"""
|
"""显示PDF导出部分"""
|
||||||
st.subheader("📄 导出报告")
|
st.subheader("📄 导出报告")
|
||||||
|
|
||||||
col1, col2, col3 = st.columns([2, 1, 1])
|
col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.write("将分析报告导出为PDF文件,方便保存和分享")
|
st.write("将分析报告导出为PDF或Markdown文件,方便保存和分享")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("📥 生成PDF报告", type="primary", width='content'):
|
if st.button("📥 生成PDF报告", type="primary", width='content'):
|
||||||
@@ -802,6 +805,23 @@ def display_pdf_export_section(result):
|
|||||||
st.error(f"❌ PDF生成失败: {str(e)}")
|
st.error(f"❌ PDF生成失败: {str(e)}")
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
|
if st.button("📝 生成Markdown", type="secondary", width='content'):
|
||||||
|
with st.spinner("正在生成Markdown报告..."):
|
||||||
|
try:
|
||||||
|
# 生成Markdown内容
|
||||||
|
markdown_content = generate_sector_markdown_report(result)
|
||||||
|
|
||||||
|
# 保存到session_state
|
||||||
|
st.session_state.sector_markdown_data = markdown_content
|
||||||
|
st.session_state.sector_markdown_filename = f"智策报告_{result.get('timestamp', datetime.now().strftime('%Y%m%d_%H%M%S')).replace(':', '').replace(' ', '_')}.md"
|
||||||
|
|
||||||
|
st.success("✅ Markdown报告生成成功!")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ Markdown生成失败: {str(e)}")
|
||||||
|
|
||||||
|
with col4:
|
||||||
# 如果已经生成了PDF,显示下载按钮
|
# 如果已经生成了PDF,显示下载按钮
|
||||||
if 'sector_pdf_data' in st.session_state:
|
if 'sector_pdf_data' in st.session_state:
|
||||||
st.download_button(
|
st.download_button(
|
||||||
@@ -811,6 +831,197 @@ def display_pdf_export_section(result):
|
|||||||
mime="application/pdf",
|
mime="application/pdf",
|
||||||
width='content'
|
width='content'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 如果已经生成了Markdown,显示下载按钮
|
||||||
|
if 'sector_markdown_data' in st.session_state:
|
||||||
|
st.download_button(
|
||||||
|
label="💾 下载Markdown",
|
||||||
|
data=st.session_state.sector_markdown_data,
|
||||||
|
file_name=st.session_state.sector_markdown_filename,
|
||||||
|
mime="text/markdown",
|
||||||
|
width='content'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sector_markdown_report(result_data: dict) -> str:
|
||||||
|
"""生成智策分析Markdown报告"""
|
||||||
|
|
||||||
|
# 获取当前时间
|
||||||
|
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||||
|
|
||||||
|
# 标题页
|
||||||
|
markdown_content = f"""# 智策板块策略分析报告
|
||||||
|
|
||||||
|
**AI驱动的多维度板块投资决策支持系统**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 报告信息
|
||||||
|
|
||||||
|
- **生成时间**: {current_time}
|
||||||
|
- **分析周期**: 当日市场数据
|
||||||
|
- **AI模型**: DeepSeek Multi-Agent System
|
||||||
|
- **分析维度**: 宏观·板块·资金·情绪
|
||||||
|
|
||||||
|
> ⚠️ 本报告由AI系统自动生成,仅供参考,不构成投资建议。投资有风险,决策需谨慎。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 市场概况
|
||||||
|
|
||||||
|
本报告基于{result_data.get('timestamp', 'N/A')}的实时市场数据,
|
||||||
|
通过四位AI智能体的多维度分析,为您提供板块投资策略建议。
|
||||||
|
|
||||||
|
### 分析师团队:
|
||||||
|
|
||||||
|
- **宏观策略师** - 分析宏观经济、政策导向、新闻事件
|
||||||
|
- **板块诊断师** - 分析板块走势、估值水平、轮动特征
|
||||||
|
- **资金流向分析师** - 分析主力资金、北向资金流向
|
||||||
|
- **市场情绪解码员** - 分析市场情绪、热度、赚钱效应
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 核心预测
|
||||||
|
predictions = result_data.get('final_predictions', {})
|
||||||
|
|
||||||
|
if predictions.get('prediction_text'):
|
||||||
|
# 文本格式预测
|
||||||
|
markdown_content += f"""
|
||||||
|
## 🎯 核心预测
|
||||||
|
|
||||||
|
{predictions.get('prediction_text', '')}
|
||||||
|
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
# JSON格式预测
|
||||||
|
markdown_content += "## 🎯 核心预测\n\n"
|
||||||
|
|
||||||
|
# 1. 板块多空预测
|
||||||
|
long_short = predictions.get('long_short', {})
|
||||||
|
bullish = long_short.get('bullish', [])
|
||||||
|
bearish = long_short.get('bearish', [])
|
||||||
|
|
||||||
|
markdown_content += "### 📊 板块多空预测\n\n"
|
||||||
|
|
||||||
|
if bullish:
|
||||||
|
markdown_content += "#### 🟢 看多板块\n\n"
|
||||||
|
for idx, item in enumerate(bullish, 1):
|
||||||
|
markdown_content += f"{idx}. **{item.get('sector', 'N/A')}** (信心度: {item.get('confidence', 0)}/10)\n"
|
||||||
|
markdown_content += f" - 理由: {item.get('reason', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 风险: {item.get('risk', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
if bearish:
|
||||||
|
markdown_content += "#### 🔴 看空板块\n\n"
|
||||||
|
for idx, item in enumerate(bearish, 1):
|
||||||
|
markdown_content += f"{idx}. **{item.get('sector', 'N/A')}** (信心度: {item.get('confidence', 0)}/10)\n"
|
||||||
|
markdown_content += f" - 理由: {item.get('reason', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 风险: {item.get('risk', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
# 2. 板块轮动预测
|
||||||
|
rotation = predictions.get('rotation', {})
|
||||||
|
current_strong = rotation.get('current_strong', [])
|
||||||
|
potential = rotation.get('potential', [])
|
||||||
|
declining = rotation.get('declining', [])
|
||||||
|
|
||||||
|
markdown_content += "### 🔄 板块轮动预测\n\n"
|
||||||
|
|
||||||
|
if current_strong:
|
||||||
|
markdown_content += "#### 💪 当前强势板块\n\n"
|
||||||
|
for item in current_strong:
|
||||||
|
markdown_content += f"- **{item.get('sector', 'N/A')}**\n"
|
||||||
|
markdown_content += f" - 轮动逻辑: {item.get('logic', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 时间窗口: {item.get('time_window', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 操作建议: {item.get('advice', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
if potential:
|
||||||
|
markdown_content += "#### 🌱 潜力接力板块\n\n"
|
||||||
|
for item in potential:
|
||||||
|
markdown_content += f"- **{item.get('sector', 'N/A')}**\n"
|
||||||
|
markdown_content += f" - 轮动逻辑: {item.get('logic', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 时间窗口: {item.get('time_window', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 操作建议: {item.get('advice', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
if declining:
|
||||||
|
markdown_content += "#### 📉 衰退板块\n\n"
|
||||||
|
for item in declining:
|
||||||
|
markdown_content += f"- **{item.get('sector', 'N/A')}**\n"
|
||||||
|
markdown_content += f" - 轮动逻辑: {item.get('logic', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 时间窗口: {item.get('time_window', 'N/A')}\n"
|
||||||
|
markdown_content += f" - 操作建议: {item.get('advice', 'N/A')}\n\n"
|
||||||
|
|
||||||
|
# 3. 板块热度排行
|
||||||
|
heat = predictions.get('heat', {})
|
||||||
|
hottest = heat.get('hottest', [])
|
||||||
|
heating = heat.get('heating', [])
|
||||||
|
cooling = heat.get('cooling', [])
|
||||||
|
|
||||||
|
markdown_content += "### 🔥 板块热度排行\n\n"
|
||||||
|
|
||||||
|
if hottest:
|
||||||
|
markdown_content += "#### 最热板块\n\n| 排名 | 板块 | 热度评分 | 趋势 | 持续性 |\n|------|------|----------|------|--------|\n"
|
||||||
|
for idx, item in enumerate(hottest[:10], 1):
|
||||||
|
markdown_content += f"| {idx} | {item.get('sector', 'N/A')} | {item.get('score', 0)} | {item.get('trend', 'N/A')} | {item.get('sustainability', 'N/A')} |\n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
if heating:
|
||||||
|
markdown_content += "#### 升温板块\n\n"
|
||||||
|
for idx, item in enumerate(heating[:5], 1):
|
||||||
|
markdown_content += f"{idx}. {item.get('sector', 'N/A')} (评分: {item.get('score', 0)})\n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
if cooling:
|
||||||
|
markdown_content += "#### 降温板块\n\n"
|
||||||
|
for idx, item in enumerate(cooling[:5], 1):
|
||||||
|
markdown_content += f"{idx}. {item.get('sector', 'N/A')} (评分: {item.get('score', 0)})\n"
|
||||||
|
markdown_content += "\n"
|
||||||
|
|
||||||
|
# 4. 策略总结
|
||||||
|
summary = predictions.get('summary', {})
|
||||||
|
if summary:
|
||||||
|
markdown_content += "### 📝 策略总结\n\n"
|
||||||
|
|
||||||
|
if summary.get('market_view'):
|
||||||
|
markdown_content += f"**市场观点:** {summary.get('market_view', '')}\n\n"
|
||||||
|
|
||||||
|
if summary.get('key_opportunity'):
|
||||||
|
markdown_content += f"**核心机会:** {summary.get('key_opportunity', '')}\n\n"
|
||||||
|
|
||||||
|
if summary.get('major_risk'):
|
||||||
|
markdown_content += f"**主要风险:** {summary.get('major_risk', '')}\n\n"
|
||||||
|
|
||||||
|
if summary.get('strategy'):
|
||||||
|
markdown_content += f"**整体策略:** {summary.get('strategy', '')}\n\n"
|
||||||
|
|
||||||
|
# AI智能体分析
|
||||||
|
agents_analysis = result_data.get('agents_analysis', {})
|
||||||
|
if agents_analysis:
|
||||||
|
markdown_content += "## 🤖 AI智能体分析\n\n"
|
||||||
|
|
||||||
|
for key, agent_data in agents_analysis.items():
|
||||||
|
agent_name = agent_data.get('agent_name', '未知分析师')
|
||||||
|
agent_role = agent_data.get('agent_role', '')
|
||||||
|
focus_areas = ', '.join(agent_data.get('focus_areas', []))
|
||||||
|
analysis = agent_data.get('analysis', '')
|
||||||
|
|
||||||
|
markdown_content += f"### {agent_name}\n\n"
|
||||||
|
markdown_content += f"- **职责**: {agent_role}\n"
|
||||||
|
markdown_content += f"- **关注领域**: {focus_areas}\n\n"
|
||||||
|
markdown_content += f"{analysis}\n\n"
|
||||||
|
markdown_content += "---\n\n"
|
||||||
|
|
||||||
|
# 综合研判
|
||||||
|
comprehensive_report = result_data.get('comprehensive_report', '')
|
||||||
|
if comprehensive_report:
|
||||||
|
markdown_content += "## 📊 综合研判\n\n"
|
||||||
|
markdown_content += f"{comprehensive_report}\n\n"
|
||||||
|
|
||||||
|
markdown_content += """
|
||||||
|
---
|
||||||
|
|
||||||
|
*报告由智策AI系统自动生成*
|
||||||
|
"""
|
||||||
|
|
||||||
|
return markdown_content
|
||||||
|
|
||||||
|
|
||||||
def display_scheduler_settings():
|
def display_scheduler_settings():
|
||||||
|
|||||||
Reference in New Issue
Block a user