diff --git a/app.py b/app.py
index 921c3c3..d3eed2e 100644
--- a/app.py
+++ b/app.py
@@ -7,6 +7,8 @@ from datetime import datetime
import time
import base64
import os
+# 从新的配置文件导入model_options
+from model_config import model_options
from stock_data import StockDataFetcher
from ai_agents import StockAnalysisAgents
@@ -34,19 +36,16 @@ def model_selector():
"""模型选择器"""
st.sidebar.markdown("---")
st.sidebar.subheader("🤖 AI模型选择")
-
- model_options = {
- "deepseek-chat": "DeepSeek Chat (默认)",
- "deepseek-reasoner": "DeepSeek Reasoner (推理增强)"
- }
-
+
+
+
selected_model = st.sidebar.selectbox(
"选择AI模型",
options=list(model_options.keys()),
format_func=lambda x: model_options[x],
help="DeepSeek Reasoner提供更强的推理能力,但响应时间可能更长"
)
-
+
return selected_model
# 自定义CSS样式 - 专业版
@@ -285,78 +284,78 @@ def main():
基于DeepSeek的专业量化投资分析平台 | Multi-Agent Stock Analysis System
""", unsafe_allow_html=True)
-
+
# 侧边栏
with st.sidebar:
# 快捷导航 - 移到顶部
st.markdown("### 🔍 功能导航")
-
+
# 🏠 单股分析(首页)
if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"):
# 清除所有功能页面标志
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_portfolio']:
if key in st.session_state:
del st.session_state[key]
-
+
st.markdown("---")
-
+
# 🎯 选股板块
with st.expander("🎯 选股板块", expanded=True):
st.markdown("**根据不同策略筛选优质股票**")
-
+
if st.button("💰 主力选股", width='stretch', key="nav_main_force", help="基于主力资金流向的选股策略"):
st.session_state.show_main_force = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
'show_longhubang', 'show_portfolio']:
if key in st.session_state:
del st.session_state[key]
-
+
# 📊 策略分析
with st.expander("📊 策略分析", expanded=True):
st.markdown("**AI驱动的板块和龙虎榜策略**")
-
+
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"):
st.session_state.show_sector_strategy = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_longhubang', 'show_portfolio', 'show_smart_monitor']:
if key in st.session_state:
del st.session_state[key]
-
+
if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"):
st.session_state.show_longhubang = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_portfolio', 'show_smart_monitor']:
if key in st.session_state:
del st.session_state[key]
-
+
# 💼 投资管理
with st.expander("💼 投资管理", expanded=True):
st.markdown("**持仓跟踪与实时监测**")
-
+
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"):
st.session_state.show_portfolio = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_smart_monitor']:
if key in st.session_state:
del st.session_state[key]
-
+
if st.button("🤖 AI盯盘", width='stretch', key="nav_smart_monitor", help="DeepSeek AI自动盯盘决策交易(支持A股T+1)"):
st.session_state.show_smart_monitor = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
+ for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_portfolio']:
if key in st.session_state:
del st.session_state[key]
-
+
if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"):
st.session_state.show_monitor = True
for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio',
'show_config', 'show_sector_strategy', 'show_smart_monitor']:
if key in st.session_state:
del st.session_state[key]
-
+
st.markdown("---")
-
+
# 📖 历史记录
if st.button("📖 历史记录", width='stretch', key="nav_history", help="查看历史分析记录"):
st.session_state.show_history = True
@@ -364,20 +363,20 @@ def main():
'show_main_force', 'show_sector_strategy']:
if key in st.session_state:
del st.session_state[key]
-
+
# ⚙️ 环境配置
if st.button("⚙️ 环境配置", width='stretch', key="nav_config", help="系统设置与API配置"):
st.session_state.show_config = True
- for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy',
+ for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy',
'show_longhubang', 'show_portfolio']:
if key in st.session_state:
del st.session_state[key]
-
+
st.markdown("---")
-
+
# 系统配置
st.markdown("### ⚙️ 系统配置")
-
+
# API密钥检查
api_key_status = check_api_key()
if api_key_status:
@@ -385,35 +384,35 @@ def main():
else:
st.error("❌ API未配置")
st.caption("请在.env中配置API密钥")
-
+
st.markdown("---")
-
+
# 模型选择器
selected_model = model_selector()
st.session_state.selected_model = selected_model
-
+
st.markdown("---")
-
+
# 系统状态面板
st.markdown("### 📊 系统状态")
-
+
monitor_status = "🟢 运行中" if monitor_service.running else "🔴 已停止"
st.markdown(f"**监测服务**: {monitor_status}")
-
+
try:
from monitor_db import monitor_db
stocks = monitor_db.get_monitored_stocks()
notifications = monitor_db.get_pending_notifications()
record_count = db.get_record_count()
-
+
st.markdown(f"**分析记录**: {record_count}条")
st.markdown(f"**监测股票**: {len(stocks)}只")
st.markdown(f"**待处理**: {len(notifications)}条")
except:
pass
-
+
st.markdown("---")
-
+
# 分析参数设置
st.markdown("### 📊 分析参数")
period = st.selectbox(
@@ -422,9 +421,9 @@ def main():
index=0,
help="选择历史数据的时间范围"
)
-
+
st.markdown("---")
-
+
# 帮助信息
with st.expander("💡 使用帮助"):
st.markdown("""
@@ -446,48 +445,48 @@ def main():
5. 情绪数据(ARBR) → 6. 新闻(qstock)
7. AI团队分析 → 8. 团队讨论 → 9. 决策
""")
-
+
# 检查是否显示历史记录
if 'show_history' in st.session_state and st.session_state.show_history:
display_history_records()
return
-
+
# 检查是否显示监测面板
if 'show_monitor' in st.session_state and st.session_state.show_monitor:
display_monitor_manager()
return
-
+
# 检查是否显示主力选股
if 'show_main_force' in st.session_state and st.session_state.show_main_force:
display_main_force_selector()
return
-
+
# 检查是否显示智策板块
if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy:
display_sector_strategy()
return
-
+
# 检查是否显示智瞰龙虎
if 'show_longhubang' in st.session_state and st.session_state.show_longhubang:
display_longhubang()
return
-
+
# 检查是否显示AI盯盘
if 'show_smart_monitor' in st.session_state and st.session_state.show_smart_monitor:
smart_monitor_ui()
return
-
+
# 检查是否显示持仓分析
if 'show_portfolio' in st.session_state and st.session_state.show_portfolio:
from portfolio_ui import display_portfolio_manager
display_portfolio_manager()
return
-
+
# 检查是否显示环境配置
if 'show_config' in st.session_state and st.session_state.show_config:
display_config_manager()
return
-
+
# 主界面
# 添加单个/批量分析切换
col_mode1, col_mode2 = st.columns([1, 3])
@@ -498,7 +497,7 @@ def main():
horizontal=True,
help="单个分析:分析单只股票;批量分析:同时分析多只股票"
)
-
+
with col_mode2:
if analysis_mode == "批量分析":
batch_mode = st.radio(
@@ -508,37 +507,37 @@ def main():
help="顺序分析:按次序分析,稳定但较慢;多线程并行:同时分析多只,快速但消耗资源"
)
st.session_state.batch_mode = batch_mode
-
+
st.markdown("---")
-
+
if analysis_mode == "单个分析":
# 单个股票分析界面
col1, col2, col3 = st.columns([2, 1, 1])
-
+
with col1:
stock_input = st.text_input(
- "🔍 请输入股票代码或名称",
+ "🔍 请输入股票代码或名称",
placeholder="例如: AAPL, 000001, 00700",
help="支持A股(如000001)、港股(如00700)和美股(如AAPL)"
)
-
+
with col2:
analyze_button = st.button("🚀 开始分析", type="primary", width='stretch')
-
+
with col3:
if st.button("🔄 清除缓存", width='stretch'):
st.cache_data.clear()
st.success("缓存已清除")
-
+
else:
# 批量股票分析界面
stock_input = st.text_area(
- "🔍 请输入多个股票代码(每行一个或用逗号分隔)",
+ "🔍 请输入多个股票代码(每行一个或用逗号分隔)",
placeholder="例如:\n000001\n600036\n00700\n\n或者: 000001, 600036, 00700, AAPL",
height=120,
help="支持多种格式:每行一个代码或用逗号分隔。支持A股、港股、美股"
)
-
+
col1, col2, col3 = st.columns(3)
with col1:
analyze_button = st.button("🚀 开始批量分析", type="primary", width='stretch')
@@ -551,31 +550,31 @@ def main():
if 'batch_analysis_results' in st.session_state:
del st.session_state.batch_analysis_results
st.success("已清除批量分析结果")
-
+
# 分析师团队选择
st.markdown("---")
st.subheader("👥 选择分析师团队")
-
+
col1, col2, col3 = st.columns(3)
-
+
with col1:
- enable_technical = st.checkbox("📊 技术分析师", value=True,
+ enable_technical = st.checkbox("📊 技术分析师", value=True,
help="负责技术指标分析、图表形态识别、趋势判断")
enable_fundamental = st.checkbox("💼 基本面分析师", value=True,
help="负责公司财务分析、行业研究、估值分析")
-
+
with col2:
enable_fund_flow = st.checkbox("💰 资金面分析师", value=True,
help="负责资金流向分析、主力行为研究")
enable_risk = st.checkbox("⚠️ 风险管理师", value=True,
help="负责风险识别、风险评估、风险控制策略制定")
-
+
with col3:
enable_sentiment = st.checkbox("📈 市场情绪分析师", value=True,
help="负责市场情绪研究、ARBR指标分析(仅A股)")
enable_news = st.checkbox("📰 新闻分析师", value=True,
help="负责新闻事件分析、舆情研究(仅A股,qstock数据源)")
-
+
# 显示已选择的分析师
selected_analysts = []
if enable_technical:
@@ -590,12 +589,12 @@ def main():
selected_analysts.append("市场情绪分析师")
if enable_news:
selected_analysts.append("新闻分析师")
-
+
if selected_analysts:
st.info(f"✅ 已选择 {len(selected_analysts)} 位分析师: {', '.join(selected_analysts)}")
else:
st.warning("⚠️ 请至少选择一位分析师")
-
+
# 保存选择到session_state
st.session_state.enable_technical = enable_technical
st.session_state.enable_fundamental = enable_fundamental
@@ -603,19 +602,19 @@ def main():
st.session_state.enable_risk = enable_risk
st.session_state.enable_sentiment = enable_sentiment
st.session_state.enable_news = enable_news
-
+
st.markdown("---")
-
+
if analyze_button and stock_input:
if not api_key_status:
st.error("❌ 请先配置 DeepSeek API Key")
return
-
+
# 检查是否至少选择了一位分析师
if not selected_analysts:
st.error("❌ 请至少选择一位分析师参与分析")
return
-
+
if analysis_mode == "单个分析":
# 单个股票分析
# 清除之前的分析结果
@@ -631,23 +630,23 @@ def main():
del st.session_state.final_decision
if 'just_completed' in st.session_state:
del st.session_state.just_completed
-
+
run_stock_analysis(stock_input, period)
-
+
else:
# 批量股票分析
# 解析股票代码列表
stock_list = parse_stock_list(stock_input)
-
+
if not stock_list:
st.error("❌ 请输入有效的股票代码")
return
-
+
if len(stock_list) > 20:
st.warning(f"⚠️ 检测到 {len(stock_list)} 只股票,建议一次分析不超过20只")
-
+
st.info(f"📊 准备分析 {len(stock_list)} 只股票: {', '.join(stock_list)}")
-
+
# 清除之前的分析结果(包括单个和批量)
if 'batch_analysis_results' in st.session_state:
del st.session_state.batch_analysis_results
@@ -663,17 +662,17 @@ def main():
del st.session_state.final_decision
if 'just_completed' in st.session_state:
del st.session_state.just_completed
-
+
# 获取批量模式
batch_mode = st.session_state.get('batch_mode', '顺序分析')
-
+
# 运行批量分析
run_batch_analysis(stock_list, period, batch_mode)
-
+
# 检查是否有已完成的批量分析结果(优先显示批量结果)
if 'batch_analysis_results' in st.session_state and st.session_state.batch_analysis_results:
display_batch_analysis_results(st.session_state.batch_analysis_results, period)
-
+
# 检查是否有已完成的单个分析结果(但不是刚刚完成的,避免重复显示)
elif 'analysis_completed' in st.session_state and st.session_state.analysis_completed:
# 如果是刚刚完成的分析,清除标志,避免重复显示
@@ -685,26 +684,26 @@ def main():
agents_results = st.session_state.agents_results
discussion_result = st.session_state.discussion_result
final_decision = st.session_state.final_decision
-
+
# 重新获取股票数据用于显示图表
stock_info_current, stock_data, indicators = get_stock_data(stock_info['symbol'], period)
-
+
# 显示股票基本信息
display_stock_info(stock_info, indicators)
-
+
# 显示股票图表
if stock_data is not None:
display_stock_chart(stock_data, stock_info)
-
+
# 显示各分析师报告
display_agents_analysis(agents_results)
-
+
# 显示团队讨论
display_team_discussion(discussion_result)
-
+
# 显示最终决策
display_final_decision(final_decision, stock_info, agents_results, discussion_result)
-
+
# 示例和说明
elif not stock_input:
show_example_interface()
@@ -723,18 +722,18 @@ def get_stock_data(symbol, period):
fetcher = StockDataFetcher()
stock_info = fetcher.get_stock_info(symbol)
stock_data = fetcher.get_stock_data(symbol, period)
-
+
if isinstance(stock_data, dict) and "error" in stock_data:
return stock_info, None, None
-
+
stock_data_with_indicators = fetcher.calculate_technical_indicators(stock_data)
indicators = fetcher.get_latest_indicators(stock_data_with_indicators)
-
+
return stock_info, stock_data_with_indicators, indicators
def parse_stock_list(stock_input):
"""解析股票代码列表
-
+
支持的格式:
- 每行一个代码
- 逗号分隔
@@ -742,17 +741,17 @@ def parse_stock_list(stock_input):
"""
if not stock_input or not stock_input.strip():
return []
-
+
# 先按换行符分割
lines = stock_input.strip().split('\n')
-
+
# 处理每一行
stock_list = []
for line in lines:
line = line.strip()
if not line:
continue
-
+
# 检查是否包含逗号
if ',' in line:
codes = [code.strip() for code in line.split(',')]
@@ -763,7 +762,7 @@ def parse_stock_list(stock_input):
stock_list.extend([code for code in codes if code])
else:
stock_list.append(line)
-
+
# 去重并保持顺序
seen = set()
unique_list = []
@@ -771,18 +770,18 @@ def parse_stock_list(stock_input):
if code not in seen:
seen.add(code)
unique_list.append(code)
-
+
return unique_list
def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None, selected_model='deepseek-chat'):
"""单个股票分析(用于批量分析)
-
+
Args:
symbol: 股票代码
period: 数据周期
enabled_analysts_config: 分析师配置字典
selected_model: 选择的AI模型
-
+
返回分析结果或错误信息
"""
try:
@@ -796,20 +795,20 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
'sentiment': False,
'news': False
}
-
+
# 1. 获取股票数据
stock_info, stock_data, indicators = get_stock_data(symbol, period)
-
+
if "error" in stock_info:
return {"symbol": symbol, "error": stock_info['error'], "success": False}
-
+
if stock_data is None:
return {"symbol": symbol, "error": "无法获取股票历史数据", "success": False}
-
+
# 2. 获取财务数据
fetcher = StockDataFetcher()
financial_data = fetcher.get_financial_data(symbol)
-
+
# 2.5 获取季报数据(仅A股)
quarterly_data = None
enable_fundamental = enabled_analysts_config.get('fundamental', True)
@@ -820,12 +819,12 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
quarterly_data = quarterly_fetcher.get_quarterly_reports(symbol)
except:
pass
-
+
# 获取分析师选择状态(从参数而不是session_state)
enable_fund_flow = enabled_analysts_config.get('fund_flow', True)
enable_sentiment = enabled_analysts_config.get('sentiment', False)
enable_news = enabled_analysts_config.get('news', False)
-
+
# 3. 获取资金流向数据(akshare数据源,可选)
fund_flow_data = None
if enable_fund_flow and fetcher._is_chinese_stock(symbol):
@@ -835,7 +834,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
fund_flow_data = fund_flow_fetcher.get_fund_flow_data(symbol)
except:
pass
-
+
# 4. 获取市场情绪数据(可选)
sentiment_data = None
if enable_sentiment and fetcher._is_chinese_stock(symbol):
@@ -845,7 +844,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
sentiment_data = sentiment_fetcher.get_market_sentiment_data(symbol, stock_data)
except:
pass
-
+
# 5. 获取新闻数据(qstock数据源,可选)
news_data = None
if enable_news and fetcher._is_chinese_stock(symbol):
@@ -855,7 +854,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
news_data = news_fetcher.get_stock_news(symbol)
except:
pass
-
+
# 5.5 获取风险数据(限售解禁、大股东减持、重要事件,可选)
risk_data = None
enable_risk = enabled_analysts_config.get('risk', True)
@@ -864,26 +863,26 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
risk_data = fetcher.get_risk_data(symbol)
except:
pass
-
+
# 6. 初始化AI分析系统
agents = StockAnalysisAgents(model=selected_model)
-
+
# 使用传入的分析师配置
enabled_analysts = enabled_analysts_config
-
+
# 7. 运行多智能体分析
agents_results = agents.run_multi_agent_analysis(
- stock_info, stock_data, indicators, financial_data,
+ stock_info, stock_data, indicators, financial_data,
fund_flow_data, sentiment_data, news_data, quarterly_data, risk_data,
enabled_analysts=enabled_analysts_config
)
-
+
# 8. 团队讨论
discussion_result = agents.conduct_team_discussion(agents_results, stock_info)
-
+
# 9. 最终决策
final_decision = agents.make_final_decision(discussion_result, stock_info, indicators)
-
+
# 保存到数据库
saved_to_db = False
db_error = None
@@ -902,7 +901,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
except Exception as e:
db_error = str(e)
print(f"❌ {symbol} 保存到数据库失败: {db_error}")
-
+
return {
"symbol": symbol,
"success": True,
@@ -914,7 +913,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None,
"saved_to_db": saved_to_db,
"db_error": db_error
}
-
+
except Exception as e:
return {"symbol": symbol, "error": str(e), "success": False}
@@ -922,7 +921,7 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
"""运行批量股票分析"""
import concurrent.futures
import threading
-
+
# 在开始分析前获取配置(从session_state)
enabled_analysts_config = {
'technical': st.session_state.get('enable_technical', True),
@@ -933,26 +932,26 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
'news': st.session_state.get('enable_news', False)
}
selected_model = st.session_state.get('selected_model', 'deepseek-chat')
-
+
# 创建进度显示
st.subheader(f"📊 批量分析进行中 ({batch_mode})")
-
+
progress_bar = st.progress(0)
status_text = st.empty()
-
+
# 存储结果
results = []
total = len(stock_list)
-
+
if batch_mode == "多线程并行":
# 多线程并行分析
status_text.text(f"🚀 使用多线程并行分析 {total} 只股票...")
-
+
# 创建线程锁用于更新进度
lock = threading.Lock()
completed = [0] # 使用列表以便在闭包中修改
progress_status = [{}] # 存储进度状态
-
+
def analyze_with_progress(symbol):
"""包装分析函数,不在线程中访问Streamlit上下文"""
try:
@@ -967,27 +966,27 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
error_result = {"symbol": symbol, "error": str(e), "success": False}
progress_status[0][symbol] = error_result
return error_result
-
+
# 使用线程池执行,限制最大并发数为3以避免API限流
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
- future_to_symbol = {executor.submit(analyze_with_progress, symbol): symbol
+ future_to_symbol = {executor.submit(analyze_with_progress, symbol): symbol
for symbol in stock_list}
-
+
for future in concurrent.futures.as_completed(future_to_symbol):
symbol = future_to_symbol[future]
try:
result = future.result(timeout=300) # 5分钟超时
results.append(result)
-
+
# 在主线程中更新UI
progress = len(results) / total
progress_bar.progress(progress)
-
+
if result['success']:
status_text.text(f"✅ [{len(results)}/{total}] {symbol} 分析完成")
else:
status_text.text(f"❌ [{len(results)}/{total}] {symbol} 分析失败: {result.get('error', '未知错误')}")
-
+
except concurrent.futures.TimeoutError:
results.append({"symbol": symbol, "error": "分析超时(5分钟)", "success": False})
progress_bar.progress(len(results) / total)
@@ -996,95 +995,95 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
results.append({"symbol": symbol, "error": str(e), "success": False})
progress_bar.progress(len(results) / total)
status_text.text(f"❌ [{len(results)}/{total}] {symbol} 出现错误")
-
+
else:
# 顺序分析
status_text.text(f"📝 按顺序分析 {total} 只股票...")
-
+
for i, symbol in enumerate(stock_list, 1):
status_text.text(f"🔍 [{i}/{total}] 正在分析 {symbol}...")
-
+
try:
result = analyze_single_stock_for_batch(symbol, period, enabled_analysts_config, selected_model)
except Exception as e:
result = {"symbol": symbol, "error": str(e), "success": False}
-
+
results.append(result)
-
+
# 更新进度
progress = i / total
progress_bar.progress(progress)
-
+
if result['success']:
status_text.text(f"✅ [{i}/{total}] {symbol} 分析完成")
else:
status_text.text(f"❌ [{i}/{total}] {symbol} 分析失败: {result.get('error', '未知错误')}")
-
+
# 完成
progress_bar.progress(1.0)
-
+
# 统计结果
success_count = sum(1 for r in results if r['success'])
failed_count = total - success_count
saved_count = sum(1 for r in results if r.get('saved_to_db', False))
-
+
# 显示完成信息
if success_count > 0:
status_text.success(f"✅ 批量分析完成!成功 {success_count} 只,失败 {failed_count} 只,已保存 {saved_count} 只到历史记录")
-
+
# 显示保存失败的股票
save_failed = [r['symbol'] for r in results if r.get('success') and not r.get('saved_to_db', False)]
if save_failed:
st.warning(f"⚠️ 以下股票分析成功但保存失败: {', '.join(save_failed)}")
else:
status_text.error(f"❌ 批量分析完成,但所有股票都分析失败")
-
+
# 保存结果到session_state
st.session_state.batch_analysis_results = results
st.session_state.batch_analysis_mode = batch_mode
-
+
time.sleep(1)
progress_bar.empty()
-
+
# 自动显示结果
st.rerun()
def run_stock_analysis(symbol, period):
"""运行股票分析"""
-
+
# 进度条
progress_bar = st.progress(0)
status_text = st.empty()
-
+
try:
# 1. 获取股票数据
status_text.text("📈 正在获取股票数据...")
progress_bar.progress(10)
-
+
stock_info, stock_data, indicators = get_stock_data(symbol, period)
-
+
if "error" in stock_info:
st.error(f"❌ {stock_info['error']}")
return
-
+
if stock_data is None:
st.error("❌ 无法获取股票历史数据")
return
-
+
# 显示股票基本信息
display_stock_info(stock_info, indicators)
progress_bar.progress(20)
-
+
# 显示股票图表
display_stock_chart(stock_data, stock_info)
progress_bar.progress(30)
-
+
# 2. 获取财务数据
status_text.text("📊 正在获取财务数据...")
fetcher = StockDataFetcher() # 创建fetcher实例
financial_data = fetcher.get_financial_data(symbol)
progress_bar.progress(35)
-
+
# 2.5 获取季报数据(仅在选择了基本面分析师且为A股时)
enable_fundamental = st.session_state.get('enable_fundamental', True)
quarterly_data = None
@@ -1107,12 +1106,12 @@ def run_stock_analysis(symbol, period):
elif enable_fundamental and not fetcher._is_chinese_stock(symbol):
st.info("ℹ️ 美股暂不支持季报数据")
progress_bar.progress(37)
-
+
# 获取分析师选择状态
enable_fund_flow = st.session_state.get('enable_fund_flow', True)
enable_sentiment = st.session_state.get('enable_sentiment', False)
enable_news = st.session_state.get('enable_news', False)
-
+
# 3. 获取资金流向数据(仅在选择了资金面分析师时,使用akshare数据源)
fund_flow_data = None
if enable_fund_flow and fetcher._is_chinese_stock(symbol):
@@ -1132,7 +1131,7 @@ def run_stock_analysis(symbol, period):
elif enable_fund_flow and not fetcher._is_chinese_stock(symbol):
st.info("ℹ️ 美股暂不支持资金流向数据")
progress_bar.progress(40)
-
+
# 4. 获取市场情绪数据(仅在选择了市场情绪分析师时)
sentiment_data = None
if enable_sentiment and fetcher._is_chinese_stock(symbol):
@@ -1151,7 +1150,7 @@ def run_stock_analysis(symbol, period):
elif enable_sentiment and not fetcher._is_chinese_stock(symbol):
st.info("ℹ️ 美股暂不支持市场情绪数据(ARBR等指标)")
progress_bar.progress(45)
-
+
# 5. 获取新闻数据(仅在选择了新闻分析师时,使用qstock数据源)
news_data = None
if enable_news and fetcher._is_chinese_stock(symbol):
@@ -1171,7 +1170,7 @@ def run_stock_analysis(symbol, period):
elif enable_news and not fetcher._is_chinese_stock(symbol):
st.info("ℹ️ 美股暂不支持新闻数据")
progress_bar.progress(45)
-
+
# 5.5 获取风险数据(仅在选择了风险管理师时,使用问财数据源)
enable_risk = st.session_state.get('enable_risk', True)
risk_data = None
@@ -1188,7 +1187,7 @@ def run_stock_analysis(symbol, period):
risk_types.append("大股东减持")
if risk_data.get('important_events') and risk_data['important_events'].get('has_data'):
risk_types.append("重要事件")
-
+
if risk_types:
st.info(f"✅ 成功获取风险数据:{', '.join(risk_types)}")
else:
@@ -1201,19 +1200,19 @@ def run_stock_analysis(symbol, period):
elif enable_risk and not fetcher._is_chinese_stock(symbol):
st.info("ℹ️ 美股暂不支持风险数据(限售解禁、大股东减持等)")
progress_bar.progress(50)
-
+
# 6. 初始化AI分析系统
status_text.text("🤖 正在初始化AI分析系统...")
# 使用选择的模型
selected_model = st.session_state.get('selected_model', 'deepseek-chat')
agents = StockAnalysisAgents(model=selected_model)
progress_bar.progress(55)
-
+
# 获取所有分析师选择状态
enable_technical = st.session_state.get('enable_technical', True)
enable_fundamental = st.session_state.get('enable_fundamental', True)
enable_risk = st.session_state.get('enable_risk', True)
-
+
# 创建分析师启用字典
enabled_analysts = {
'technical': enable_technical,
@@ -1223,35 +1222,35 @@ def run_stock_analysis(symbol, period):
'sentiment': enable_sentiment,
'news': enable_news
}
-
+
# 7. 运行多智能体分析(传入所有数据和分析师选择)
status_text.text("🔍 AI分析师团队正在分析,请耐心等待几分钟...")
agents_results = agents.run_multi_agent_analysis(
- stock_info, stock_data, indicators, financial_data,
+ stock_info, stock_data, indicators, financial_data,
fund_flow_data, sentiment_data, news_data, quarterly_data, risk_data,
enabled_analysts=enabled_analysts
)
progress_bar.progress(75)
-
+
# 显示各分析师报告
display_agents_analysis(agents_results)
-
+
# 8. 团队讨论
status_text.text("🤝 分析团队正在讨论...")
discussion_result = agents.conduct_team_discussion(agents_results, stock_info)
progress_bar.progress(88)
-
+
# 显示团队讨论
display_team_discussion(discussion_result)
-
+
# 9. 最终决策
status_text.text("📋 正在制定最终投资决策...")
final_decision = agents.make_final_decision(discussion_result, stock_info, indicators)
progress_bar.progress(100)
-
+
# 显示最终决策
display_final_decision(final_decision, stock_info, agents_results, discussion_result)
-
+
# 保存分析结果到session_state(用于页面刷新后显示)
st.session_state.analysis_completed = True
st.session_state.stock_info = stock_info
@@ -1259,7 +1258,7 @@ def run_stock_analysis(symbol, period):
st.session_state.discussion_result = discussion_result
st.session_state.final_decision = final_decision
st.session_state.just_completed = True # 标记刚刚完成分析
-
+
# 保存到数据库
try:
db.save_analysis(
@@ -1274,12 +1273,12 @@ def run_stock_analysis(symbol, period):
st.success("✅ 分析记录已保存到数据库")
except Exception as e:
st.warning(f"⚠️ 保存到数据库时出现错误: {str(e)}")
-
+
status_text.text("✅ 分析完成!")
time.sleep(1)
status_text.empty()
progress_bar.empty()
-
+
except Exception as e:
st.error(f"❌ 分析过程中出现错误: {str(e)}")
progress_bar.empty()
@@ -1288,29 +1287,29 @@ def run_stock_analysis(symbol, period):
def display_stock_info(stock_info, indicators):
"""显示股票基本信息"""
st.subheader(f"📊 {stock_info.get('name', 'N/A')} ({stock_info.get('symbol', 'N/A')})")
-
+
# 基本信息卡片
col1, col2, col3, col4, col5 = st.columns(5)
-
+
with col1:
current_price = stock_info.get('current_price', 'N/A')
st.metric("当前价格", f"{current_price}")
-
+
with col2:
change_percent = stock_info.get('change_percent', 'N/A')
if isinstance(change_percent, (int, float)):
st.metric("涨跌幅", f"{change_percent:.2f}%", f"{change_percent:.2f}%")
else:
st.metric("涨跌幅", f"{change_percent}")
-
+
with col3:
pe_ratio = stock_info.get('pe_ratio', 'N/A')
st.metric("市盈率", f"{pe_ratio}")
-
+
with col4:
pb_ratio = stock_info.get('pb_ratio', 'N/A')
st.metric("市净率", f"{pb_ratio}")
-
+
with col5:
market_cap = stock_info.get('market_cap', 'N/A')
if isinstance(market_cap, (int, float)):
@@ -1318,13 +1317,13 @@ def display_stock_info(stock_info, indicators):
st.metric("市值", market_cap_str)
else:
st.metric("市值", f"{market_cap}")
-
+
# 技术指标
if indicators and not isinstance(indicators, dict) or "error" not in indicators:
st.subheader("📈 关键技术指标")
-
+
col1, col2, col3, col4 = st.columns(4)
-
+
with col1:
rsi = indicators.get('rsi', 'N/A')
if isinstance(rsi, (int, float)):
@@ -1336,21 +1335,21 @@ def display_stock_info(stock_info, indicators):
st.metric("RSI", f"{rsi:.2f}")
else:
st.metric("RSI", f"{rsi}")
-
+
with col2:
ma20 = indicators.get('ma20', 'N/A')
if isinstance(ma20, (int, float)):
st.metric("MA20", f"{ma20:.2f}")
else:
st.metric("MA20", f"{ma20}")
-
+
with col3:
volume_ratio = indicators.get('volume_ratio', 'N/A')
if isinstance(volume_ratio, (int, float)):
st.metric("量比", f"{volume_ratio:.2f}")
else:
st.metric("量比", f"{volume_ratio}")
-
+
with col4:
macd = indicators.get('macd', 'N/A')
if isinstance(macd, (int, float)):
@@ -1361,10 +1360,10 @@ def display_stock_info(stock_info, indicators):
def display_stock_chart(stock_data, stock_info):
"""显示股票图表"""
st.subheader("📈 股价走势图")
-
+
# 创建蜡烛图
fig = go.Figure()
-
+
# 添加蜡烛图
fig.add_trace(go.Candlestick(
x=stock_data.index,
@@ -1374,7 +1373,7 @@ def display_stock_chart(stock_data, stock_info):
close=stock_data['Close'],
name="K线"
))
-
+
# 添加移动平均线
if 'MA5' in stock_data.columns:
fig.add_trace(go.Scatter(
@@ -1383,7 +1382,7 @@ def display_stock_chart(stock_data, stock_info):
name="MA5",
line=dict(color='orange', width=1)
))
-
+
if 'MA20' in stock_data.columns:
fig.add_trace(go.Scatter(
x=stock_data.index,
@@ -1391,7 +1390,7 @@ def display_stock_chart(stock_data, stock_info):
name="MA20",
line=dict(color='blue', width=1)
))
-
+
if 'MA60' in stock_data.columns:
fig.add_trace(go.Scatter(
x=stock_data.index,
@@ -1399,7 +1398,7 @@ def display_stock_chart(stock_data, stock_info):
name="MA60",
line=dict(color='purple', width=1)
))
-
+
# 布林带
if 'BB_upper' in stock_data.columns and 'BB_lower' in stock_data.columns:
fig.add_trace(go.Scatter(
@@ -1416,7 +1415,7 @@ def display_stock_chart(stock_data, stock_info):
fill='tonexty',
fillcolor='rgba(0,100,80,0.1)'
))
-
+
fig.update_layout(
title=f"{stock_info.get('name', 'N/A')} 股价走势",
xaxis_title="日期",
@@ -1424,11 +1423,11 @@ def display_stock_chart(stock_data, stock_info):
height=500,
showlegend=True
)
-
+
# 生成唯一的key
chart_key = f"main_stock_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
st.plotly_chart(fig, use_container_width=True, config={'responsive': True}, key=chart_key)
-
+
# 成交量图
if 'Volume' in stock_data.columns:
fig_volume = go.Figure()
@@ -1438,14 +1437,14 @@ def display_stock_chart(stock_data, stock_info):
name="成交量",
marker_color='lightblue'
))
-
+
fig_volume.update_layout(
title="成交量",
xaxis_title="日期",
yaxis_title="成交量",
height=200
)
-
+
# 生成唯一的key
volume_key = f"volume_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
st.plotly_chart(fig_volume, use_container_width=True, config={'responsive': True}, key=volume_key)
@@ -1453,22 +1452,22 @@ def display_stock_chart(stock_data, stock_info):
def display_agents_analysis(agents_results):
"""显示各分析师报告"""
st.subheader("🤖 AI分析师团队报告")
-
+
# 创建标签页
tab_names = []
tab_contents = []
-
+
for agent_key, agent_result in agents_results.items():
agent_name = agent_result.get('agent_name', '未知分析师')
tab_names.append(agent_name)
tab_contents.append(agent_result)
-
+
tabs = st.tabs(tab_names)
-
+
for i, tab in enumerate(tabs):
with tab:
agent_result = tab_contents[i]
-
+
# 分析师信息
st.markdown(f"""
@@ -1478,7 +1477,7 @@ def display_agents_analysis(agents_results):
分析时间:{agent_result.get('timestamp', '未知')}
""", unsafe_allow_html=True)
-
+
# 分析报告
st.markdown("**📄 分析报告:**")
st.write(agent_result.get('analysis', '暂无分析'))
@@ -1486,62 +1485,62 @@ def display_agents_analysis(agents_results):
def display_team_discussion(discussion_result):
"""显示团队讨论"""
st.subheader("🤝 分析团队讨论")
-
+
st.markdown("""
💭 团队综合讨论
各位分析师正在就该股票进行深入讨论,整合不同维度的分析观点...
""", unsafe_allow_html=True)
-
+
st.write(discussion_result)
def display_final_decision(final_decision, stock_info, agents_results=None, discussion_result=None):
"""显示最终投资决策"""
st.subheader("📋 最终投资决策")
-
+
if isinstance(final_decision, dict) and "decision_text" not in final_decision:
# JSON格式的决策
col1, col2 = st.columns([1, 2])
-
+
with col1:
# 投资评级
rating = final_decision.get('rating', '未知')
rating_color = {"买入": "🟢", "持有": "🟡", "卖出": "🔴"}.get(rating, "⚪")
-
+
st.markdown(f"""
{rating_color} {rating}
投资评级
""", unsafe_allow_html=True)
-
+
# 关键指标
confidence = final_decision.get('confidence_level', 'N/A')
st.metric("信心度", f"{confidence}/10")
-
+
target_price = final_decision.get('target_price', 'N/A')
st.metric("目标价格", f"{target_price}")
-
+
position_size = final_decision.get('position_size', 'N/A')
st.metric("建议仓位", f"{position_size}")
-
+
with col2:
# 详细建议
st.markdown("**🎯 操作建议:**")
st.write(final_decision.get('operation_advice', '暂无建议'))
-
+
st.markdown("**📍 关键位置:**")
col2_1, col2_2 = st.columns(2)
-
+
with col2_1:
st.write(f"**进场区间:** {final_decision.get('entry_range', 'N/A')}")
st.write(f"**止盈位:** {final_decision.get('take_profit', 'N/A')}")
-
+
with col2_2:
st.write(f"**止损位:** {final_decision.get('stop_loss', 'N/A')}")
st.write(f"**持有周期:** {final_decision.get('holding_period', 'N/A')}")
-
+
# 风险提示
risk_warning = final_decision.get('risk_warning', '')
if risk_warning:
@@ -1551,12 +1550,12 @@ def display_final_decision(final_decision, stock_info, agents_results=None, disc
{risk_warning}
""", unsafe_allow_html=True)
-
+
else:
# 文本格式的决策
decision_text = final_decision.get('decision_text', str(final_decision))
st.write(decision_text)
-
+
# 添加PDF导出功能
st.markdown("---")
if agents_results and discussion_result:
@@ -1567,9 +1566,9 @@ def display_final_decision(final_decision, stock_info, agents_results=None, disc
def show_example_interface():
"""显示示例界面"""
st.subheader("💡 使用说明")
-
+
col1, col2 = st.columns(2)
-
+
with col1:
st.markdown("""
### 🚀 如何使用
@@ -1585,7 +1584,7 @@ def show_example_interface():
- **风险管理**:风险识别与控制
- **市场情绪**:情绪指标、热点分析
""")
-
+
with col2:
st.markdown("""
### 📈 示例股票代码
@@ -1605,9 +1604,9 @@ def show_example_interface():
- MSFT (微软)
- NVDA (英伟达)
""")
-
+
st.info("💡 提示:首次运行需要配置DeepSeek API Key,请在.env中设置DEEPSEEK_API_KEY")
-
+
st.markdown("---")
st.markdown("""
### 🌏 市场支持说明
@@ -1622,16 +1621,16 @@ def show_example_interface():
def display_history_records():
"""显示历史分析记录"""
st.subheader("📚 历史分析记录")
-
+
# 获取所有记录
records = db.get_all_records()
-
+
if not records:
st.info("📭 暂无历史分析记录")
return
-
+
st.write(f"📊 共找到 {len(records)} 条分析记录")
-
+
# 搜索和筛选
col1, col2 = st.columns([3, 1])
with col1:
@@ -1641,53 +1640,53 @@ def display_history_records():
st.write("")
if st.button("🔄 刷新列表"):
st.rerun()
-
+
# 筛选记录
filtered_records = records
if search_term:
filtered_records = [
- record for record in records
- if search_term.lower() in record['symbol'].lower() or
+ record for record in records
+ if search_term.lower() in record['symbol'].lower() or
search_term.lower() in record['stock_name'].lower()
]
-
+
if not filtered_records:
st.warning("🔍 未找到匹配的记录")
return
-
+
# 显示记录列表
for record in filtered_records:
# 根据评级设置颜色和图标
rating = record.get('rating', '未知')
rating_color = {
"买入": "🟢",
- "持有": "🟡",
+ "持有": "🟡",
"卖出": "🔴",
"强烈买入": "🟢",
"强烈卖出": "🔴"
}.get(rating, "⚪")
-
+
with st.expander(f"{rating_color} {record['stock_name']} ({record['symbol']}) - {record['analysis_date']}"):
col1, col2, col3, col4 = st.columns([2, 2, 1, 1])
-
+
with col1:
st.write(f"**股票代码:** {record['symbol']}")
st.write(f"**股票名称:** {record['stock_name']}")
-
+
with col2:
st.write(f"**分析时间:** {record['analysis_date']}")
st.write(f"**数据周期:** {record['period']}")
st.write(f"**投资评级:** **{rating}**")
-
+
with col3:
if st.button("👀 查看详情", key=f"view_{record['id']}"):
st.session_state.viewing_record_id = record['id']
-
+
with col4:
if st.button("➕ 监测", key=f"add_monitor_{record['id']}"):
st.session_state.add_to_monitor_id = record['id']
st.session_state.viewing_record_id = record['id']
-
+
# 删除按钮(新增一行)
col5, _, _, _ = st.columns(4)
with col5:
@@ -1697,7 +1696,7 @@ def display_history_records():
st.rerun()
else:
st.error("❌ 删除失败")
-
+
# 查看详细记录
if 'viewing_record_id' in st.session_state:
display_record_detail(st.session_state.viewing_record_id)
@@ -1706,16 +1705,16 @@ def display_add_to_monitor_dialog(record):
"""显示加入监测的对话框"""
st.markdown("---")
st.subheader("➕ 加入监测")
-
+
final_decision = record['final_decision']
-
+
# 从final_decision中提取关键数据
if isinstance(final_decision, dict):
# 解析进场区间
entry_range_str = final_decision.get('entry_range', 'N/A')
entry_min = 0.0
entry_max = 0.0
-
+
# 尝试解析进场区间字符串,支持多种格式
if entry_range_str and entry_range_str != 'N/A':
try:
@@ -1742,14 +1741,14 @@ def display_add_to_monitor_dialog(record):
break
except:
pass
-
+
# 提取止盈和止损
take_profit_str = final_decision.get('take_profit', 'N/A')
stop_loss_str = final_decision.get('stop_loss', 'N/A')
-
+
take_profit = 0.0
stop_loss = 0.0
-
+
# 解析止盈位
if take_profit_str and take_profit_str != 'N/A':
try:
@@ -1762,7 +1761,7 @@ def display_add_to_monitor_dialog(record):
take_profit = float(numbers[0])
except:
pass
-
+
# 解析止损位
if stop_loss_str and stop_loss_str != 'N/A':
try:
@@ -1775,18 +1774,18 @@ def display_add_to_monitor_dialog(record):
stop_loss = float(numbers[0])
except:
pass
-
+
# 获取评级
rating = final_decision.get('rating', '买入')
-
+
# 检查是否已经在监测列表中
from monitor_db import monitor_db
existing_stocks = monitor_db.get_monitored_stocks()
is_duplicate = any(stock['symbol'] == record['symbol'] for stock in existing_stocks)
-
+
if is_duplicate:
st.warning(f"⚠️ {record['symbol']} 已经在监测列表中。继续添加将创建重复监测项。")
-
+
st.info(f"""
**从分析结果中提取的数据:**
- 进场区间: {entry_min} - {entry_max}
@@ -1794,41 +1793,41 @@ def display_add_to_monitor_dialog(record):
- 止损位: {stop_loss if stop_loss > 0 else '未设置'}
- 投资评级: {rating}
""")
-
+
# 显示表单供用户确认或修改
with st.form(key=f"monitor_form_{record['id']}"):
st.markdown("**请确认或修改监测参数:**")
-
+
col1, col2 = st.columns([1, 1])
-
+
with col1:
st.subheader("🎯 关键位置")
new_entry_min = st.number_input("进场区间最低价", value=float(entry_min), step=0.01, format="%.2f")
new_entry_max = st.number_input("进场区间最高价", value=float(entry_max), step=0.01, format="%.2f")
new_take_profit = st.number_input("止盈价位", value=float(take_profit), step=0.01, format="%.2f")
new_stop_loss = st.number_input("止损价位", value=float(stop_loss), step=0.01, format="%.2f")
-
+
with col2:
st.subheader("⚙️ 监测设置")
check_interval = st.slider("监测间隔(分钟)", 5, 120, 30)
notification_enabled = st.checkbox("启用通知", value=True)
- new_rating = st.selectbox("投资评级", ["买入", "持有", "卖出"],
+ new_rating = st.selectbox("投资评级", ["买入", "持有", "卖出"],
index=["买入", "持有", "卖出"].index(rating) if rating in ["买入", "持有", "卖出"] else 0)
-
+
col_a, col_b, col_c = st.columns(3)
-
+
with col_a:
submit = st.form_submit_button("✅ 确认加入监测", type="primary", width='stretch')
-
+
with col_b:
cancel = st.form_submit_button("❌ 取消", width='stretch')
-
+
if submit:
if new_entry_min > 0 and new_entry_max > 0 and new_entry_max > new_entry_min:
try:
# 添加到监测数据库
entry_range = {"min": new_entry_min, "max": new_entry_max}
-
+
stock_id = monitor_db.add_monitored_stock(
symbol=record['symbol'],
name=record['stock_name'],
@@ -1839,14 +1838,14 @@ def display_add_to_monitor_dialog(record):
check_interval=check_interval,
notification_enabled=notification_enabled
)
-
+
st.success(f"✅ 已成功将 {record['symbol']} 加入监测列表!")
st.balloons()
-
+
# 立即更新一次价格
from monitor_service import monitor_service
monitor_service.manual_update_stock(stock_id)
-
+
# 清理session state并跳转到监测页面
if 'add_to_monitor_id' in st.session_state:
del st.session_state.add_to_monitor_id
@@ -1854,19 +1853,19 @@ def display_add_to_monitor_dialog(record):
del st.session_state.viewing_record_id
if 'show_history' in st.session_state:
del st.session_state.show_history
-
+
# 设置跳转到监测页面
st.session_state.show_monitor = True
st.session_state.monitor_jump_highlight = record['symbol'] # 标记要高亮显示的股票
-
+
time.sleep(1.5)
st.rerun()
-
+
except Exception as e:
st.error(f"❌ 加入监测失败: {str(e)}")
else:
st.error("❌ 请输入有效的进场区间(最低价应小于最高价,且都大于0)")
-
+
if cancel:
if 'add_to_monitor_id' in st.session_state:
del st.session_state.add_to_monitor_id
@@ -1882,12 +1881,12 @@ def display_record_detail(record_id):
"""显示单条记录的详细信息"""
st.markdown("---")
st.subheader("📋 详细分析记录")
-
+
record = db.get_record_by_id(record_id)
if not record:
st.error("❌ 记录不存在")
return
-
+
# 基本信息
col1, col2, col3 = st.columns(3)
with col1:
@@ -1896,32 +1895,32 @@ def display_record_detail(record_id):
st.metric("股票名称", record['stock_name'])
with col3:
st.metric("分析时间", record['analysis_date'])
-
+
# 股票基本信息
st.subheader("📊 股票基本信息")
stock_info = record['stock_info']
if stock_info:
col1, col2, col3, col4, col5 = st.columns(5)
-
+
with col1:
current_price = stock_info.get('current_price', 'N/A')
st.metric("当前价格", f"{current_price}")
-
+
with col2:
change_percent = stock_info.get('change_percent', 'N/A')
if isinstance(change_percent, (int, float)):
st.metric("涨跌幅", f"{change_percent:.2f}%", f"{change_percent:.2f}%")
else:
st.metric("涨跌幅", f"{change_percent}")
-
+
with col3:
pe_ratio = stock_info.get('pe_ratio', 'N/A')
st.metric("市盈率", f"{pe_ratio}")
-
+
with col4:
pb_ratio = stock_info.get('pb_ratio', 'N/A')
st.metric("市净率", f"{pb_ratio}")
-
+
with col5:
market_cap = stock_info.get('market_cap', 'N/A')
if isinstance(market_cap, (int, float)):
@@ -1929,25 +1928,25 @@ def display_record_detail(record_id):
st.metric("市值", market_cap_str)
else:
st.metric("市值", f"{market_cap}")
-
+
# 各分析师报告
st.subheader("🤖 AI分析师团队报告")
agents_results = record['agents_results']
if agents_results:
tab_names = []
tab_contents = []
-
+
for agent_key, agent_result in agents_results.items():
agent_name = agent_result.get('agent_name', '未知分析师')
tab_names.append(agent_name)
tab_contents.append(agent_result)
-
+
tabs = st.tabs(tab_names)
-
+
for i, tab in enumerate(tabs):
with tab:
agent_result = tab_contents[i]
-
+
st.markdown(f"""
👨💼 {agent_result.get('agent_name', '未知')}
@@ -1955,10 +1954,10 @@ def display_record_detail(record_id):
关注领域:{', '.join(agent_result.get('focus_areas', []))}
""", unsafe_allow_html=True)
-
+
st.markdown("**📄 分析报告:**")
st.write(agent_result.get('analysis', '暂无分析'))
-
+
# 团队讨论
st.subheader("🤝 分析团队讨论")
discussion_result = record['discussion_result']
@@ -1969,68 +1968,68 @@ def display_record_detail(record_id):
""", unsafe_allow_html=True)
st.write(discussion_result)
-
+
# 最终决策
st.subheader("📋 最终投资决策")
final_decision = record['final_decision']
if final_decision:
if isinstance(final_decision, dict) and "decision_text" not in final_decision:
col1, col2 = st.columns([1, 2])
-
+
with col1:
rating = final_decision.get('rating', '未知')
rating_color = {"买入": "🟢", "持有": "🟡", "卖出": "🔴"}.get(rating, "⚪")
-
+
st.markdown(f"""
{rating_color} {rating}
投资评级
""", unsafe_allow_html=True)
-
+
confidence = final_decision.get('confidence_level', 'N/A')
st.metric("信心度", f"{confidence}/10")
-
+
target_price = final_decision.get('target_price', 'N/A')
st.metric("目标价格", f"{target_price}")
-
+
position_size = final_decision.get('position_size', 'N/A')
st.metric("建议仓位", f"{position_size}")
-
+
with col2:
st.markdown("**🎯 操作建议:**")
st.write(final_decision.get('operation_advice', '暂无建议'))
-
+
st.markdown("**📍 关键位置:**")
col2_1, col2_2 = st.columns(2)
-
+
with col2_1:
st.write(f"**进场区间:** {final_decision.get('entry_range', 'N/A')}")
st.write(f"**止盈位:** {final_decision.get('take_profit', 'N/A')}")
-
+
with col2_2:
st.write(f"**止损位:** {final_decision.get('stop_loss', 'N/A')}")
st.write(f"**持有周期:** {final_decision.get('holding_period', 'N/A')}")
else:
decision_text = final_decision.get('decision_text', str(final_decision))
st.write(decision_text)
-
+
# 加入监测功能
st.markdown("---")
st.subheader("🎯 操作")
-
+
# 检查是否需要显示加入监测的对话框
if 'add_to_monitor_id' in st.session_state and st.session_state.add_to_monitor_id == record_id:
display_add_to_monitor_dialog(record)
else:
# 只有在不显示对话框时才显示按钮
col1, col2 = st.columns([1, 3])
-
+
with col1:
if st.button("➕ 加入监测", type="primary", width='stretch'):
st.session_state.add_to_monitor_id = record_id
st.rerun()
-
+
# 返回按钮
st.markdown("---")
if st.button("⬅️ 返回历史记录列表"):
@@ -2043,32 +2042,36 @@ def display_record_detail(record_id):
def display_config_manager():
"""显示环境配置管理界面"""
st.subheader("⚙️ 环境配置管理")
-
+
st.markdown("""
在这里可以配置系统的环境变量,包括API密钥、数据源配置、量化交易配置等。
注意:配置修改后需要重启应用才能生效。
""", unsafe_allow_html=True)
-
+
# 获取当前配置
config_info = config_manager.get_config_info()
-
+
# 创建标签页
tab1, tab2, tab3, tab4 = st.tabs(["📝 基本配置", "📊 数据源配置", "🤖 量化交易配置", "📢 通知配置"])
-
+
# 使用session_state保存临时配置
if 'temp_config' not in st.session_state:
st.session_state.temp_config = {key: info["value"] for key, info in config_info.items()}
-
+
with tab1:
st.markdown("### DeepSeek API配置")
st.markdown("DeepSeek是系统的核心AI引擎,必须配置才能使用分析功能。")
-
- # DeepSeek API Key
+ st.markdown("DeepSeek:https://api.deepseek.com/v1")
+ st.markdown("硅基流动:https://api.siliconflow.cn/v1")
+ st.markdown("火山引擎:https://ark.cn-beijing.volces.com/api/v3")
+ st.markdown("阿里:https://dashscope.aliyuncs.com/compatible-mode/v1")
+
+ # DeepSeek API Key
api_key_info = config_info["DEEPSEEK_API_KEY"]
current_api_key = st.session_state.temp_config.get("DEEPSEEK_API_KEY", "")
-
+
new_api_key = st.text_input(
f"🔑 {api_key_info['description']} {'*' if api_key_info['required'] else ''}",
value=current_api_key,
@@ -2077,20 +2080,20 @@ def display_config_manager():
key="input_deepseek_api_key"
)
st.session_state.temp_config["DEEPSEEK_API_KEY"] = new_api_key
-
+
# 显示当前状态
if new_api_key:
masked_key = new_api_key[:8] + "*" * (len(new_api_key) - 12) + new_api_key[-4:] if len(new_api_key) > 12 else "***"
st.success(f"✅ API密钥已设置: {masked_key}")
else:
st.warning("⚠️ 未设置API密钥,系统无法使用AI分析功能")
-
+
st.markdown("---")
-
+
# DeepSeek Base URL
base_url_info = config_info["DEEPSEEK_BASE_URL"]
current_base_url = st.session_state.temp_config.get("DEEPSEEK_BASE_URL", "")
-
+
new_base_url = st.text_input(
f"🌐 {base_url_info['description']}",
value=current_base_url,
@@ -2098,16 +2101,16 @@ def display_config_manager():
key="input_deepseek_base_url"
)
st.session_state.temp_config["DEEPSEEK_BASE_URL"] = new_base_url
-
+
st.info("💡 如何获取DeepSeek API密钥?\n\n1. 访问 https://platform.deepseek.com\n2. 注册/登录账号\n3. 进入API密钥管理页面\n4. 创建新的API密钥\n5. 复制密钥并粘贴到上方输入框")
-
+
with tab2:
st.markdown("### Tushare数据接口(可选)")
st.markdown("Tushare提供更丰富的A股财务数据,配置后可以获取更详细的财务分析。")
-
+
tushare_info = config_info["TUSHARE_TOKEN"]
current_tushare = st.session_state.temp_config.get("TUSHARE_TOKEN", "")
-
+
new_tushare = st.text_input(
f"🎫 {tushare_info['description']}",
value=current_tushare,
@@ -2116,22 +2119,22 @@ def display_config_manager():
key="input_tushare_token"
)
st.session_state.temp_config["TUSHARE_TOKEN"] = new_tushare
-
+
if new_tushare:
st.success("✅ Tushare Token已设置")
else:
st.info("ℹ️ 未设置Tushare Token,系统将使用其他数据源")
-
+
st.info("💡 如何获取Tushare Token?\n\n1. 访问 https://tushare.pro\n2. 注册账号\n3. 进入个人中心\n4. 获取Token\n5. 复制并粘贴到上方输入框")
-
+
with tab3:
st.markdown("### MiniQMT量化交易配置(可选)")
st.markdown("配置后可以使用量化交易功能,自动执行交易策略。")
-
+
# 启用开关
miniqmt_enabled_info = config_info["MINIQMT_ENABLED"]
current_enabled = st.session_state.temp_config.get("MINIQMT_ENABLED", "false") == "true"
-
+
new_enabled = st.checkbox(
"启用MiniQMT量化交易",
value=current_enabled,
@@ -2139,14 +2142,14 @@ def display_config_manager():
key="input_miniqmt_enabled"
)
st.session_state.temp_config["MINIQMT_ENABLED"] = "true" if new_enabled else "false"
-
+
# 其他配置
col1, col2 = st.columns(2)
-
+
with col1:
account_id_info = config_info["MINIQMT_ACCOUNT_ID"]
current_account_id = st.session_state.temp_config.get("MINIQMT_ACCOUNT_ID", "")
-
+
new_account_id = st.text_input(
f"🆔 {account_id_info['description']}",
value=current_account_id,
@@ -2154,10 +2157,10 @@ def display_config_manager():
key="input_miniqmt_account_id"
)
st.session_state.temp_config["MINIQMT_ACCOUNT_ID"] = new_account_id
-
+
host_info = config_info["MINIQMT_HOST"]
current_host = st.session_state.temp_config.get("MINIQMT_HOST", "")
-
+
new_host = st.text_input(
f"🖥️ {host_info['description']}",
value=current_host,
@@ -2165,11 +2168,11 @@ def display_config_manager():
key="input_miniqmt_host"
)
st.session_state.temp_config["MINIQMT_HOST"] = new_host
-
+
with col2:
port_info = config_info["MINIQMT_PORT"]
current_port = st.session_state.temp_config.get("MINIQMT_PORT", "")
-
+
new_port = st.text_input(
f"🔌 {port_info['description']}",
value=current_port,
@@ -2177,28 +2180,28 @@ def display_config_manager():
key="input_miniqmt_port"
)
st.session_state.temp_config["MINIQMT_PORT"] = new_port
-
+
if new_enabled:
st.success("✅ MiniQMT已启用")
else:
st.info("ℹ️ MiniQMT未启用")
-
+
st.warning("⚠️ 警告:量化交易涉及真实资金操作,请谨慎配置和使用!")
-
+
with tab4:
st.markdown("### 通知配置")
st.markdown("配置邮件和Webhook通知,用于实时监测和智策定时分析的提醒。")
-
+
# 创建两列布局
col_email, col_webhook = st.columns(2)
-
+
with col_email:
st.markdown("#### 📧 邮件通知")
-
+
# 邮件启用开关
email_enabled_info = config_info.get("EMAIL_ENABLED", {"value": "false"})
current_email_enabled = st.session_state.temp_config.get("EMAIL_ENABLED", "false") == "true"
-
+
new_email_enabled = st.checkbox(
"启用邮件通知",
value=current_email_enabled,
@@ -2206,11 +2209,11 @@ def display_config_manager():
key="input_email_enabled"
)
st.session_state.temp_config["EMAIL_ENABLED"] = "true" if new_email_enabled else "false"
-
+
# SMTP服务器
smtp_server_info = config_info.get("SMTP_SERVER", {"description": "SMTP服务器地址", "value": ""})
current_smtp_server = st.session_state.temp_config.get("SMTP_SERVER", "")
-
+
new_smtp_server = st.text_input(
f"📮 {smtp_server_info['description']}",
value=current_smtp_server,
@@ -2219,11 +2222,11 @@ def display_config_manager():
key="input_smtp_server"
)
st.session_state.temp_config["SMTP_SERVER"] = new_smtp_server
-
+
# SMTP端口
smtp_port_info = config_info.get("SMTP_PORT", {"description": "SMTP端口", "value": "587"})
current_smtp_port = st.session_state.temp_config.get("SMTP_PORT", "587")
-
+
new_smtp_port = st.text_input(
f"🔌 {smtp_port_info['description']}",
value=current_smtp_port,
@@ -2232,11 +2235,11 @@ def display_config_manager():
key="input_smtp_port"
)
st.session_state.temp_config["SMTP_PORT"] = new_smtp_port
-
+
# 发件人邮箱
email_from_info = config_info.get("EMAIL_FROM", {"description": "发件人邮箱", "value": ""})
current_email_from = st.session_state.temp_config.get("EMAIL_FROM", "")
-
+
new_email_from = st.text_input(
f"📤 {email_from_info['description']}",
value=current_email_from,
@@ -2245,11 +2248,11 @@ def display_config_manager():
key="input_email_from"
)
st.session_state.temp_config["EMAIL_FROM"] = new_email_from
-
+
# 邮箱授权码
email_password_info = config_info.get("EMAIL_PASSWORD", {"description": "邮箱授权码", "value": ""})
current_email_password = st.session_state.temp_config.get("EMAIL_PASSWORD", "")
-
+
new_email_password = st.text_input(
f"🔐 {email_password_info['description']}",
value=current_email_password,
@@ -2259,11 +2262,11 @@ def display_config_manager():
key="input_email_password"
)
st.session_state.temp_config["EMAIL_PASSWORD"] = new_email_password
-
+
# 收件人邮箱
email_to_info = config_info.get("EMAIL_TO", {"description": "收件人邮箱", "value": ""})
current_email_to = st.session_state.temp_config.get("EMAIL_TO", "")
-
+
new_email_to = st.text_input(
f"📥 {email_to_info['description']}",
value=current_email_to,
@@ -2272,23 +2275,23 @@ def display_config_manager():
key="input_email_to"
)
st.session_state.temp_config["EMAIL_TO"] = new_email_to
-
+
if new_email_enabled and all([new_smtp_server, new_email_from, new_email_password, new_email_to]):
st.success("✅ 邮件配置完整")
elif new_email_enabled:
st.warning("⚠️ 邮件配置不完整")
else:
st.info("ℹ️ 邮件通知未启用")
-
+
st.caption("💡 QQ邮箱授权码获取:设置 → 账户 → POP3/IMAP/SMTP → 生成授权码")
-
+
with col_webhook:
st.markdown("#### 📱 Webhook通知")
-
+
# Webhook启用开关
webhook_enabled_info = config_info.get("WEBHOOK_ENABLED", {"value": "false"})
current_webhook_enabled = st.session_state.temp_config.get("WEBHOOK_ENABLED", "false") == "true"
-
+
new_webhook_enabled = st.checkbox(
"启用Webhook通知",
value=current_webhook_enabled,
@@ -2296,11 +2299,11 @@ def display_config_manager():
key="input_webhook_enabled"
)
st.session_state.temp_config["WEBHOOK_ENABLED"] = "true" if new_webhook_enabled else "false"
-
+
# Webhook类型选择
webhook_type_info = config_info.get("WEBHOOK_TYPE", {"description": "Webhook类型", "value": "dingtalk", "options": ["dingtalk", "feishu"]})
current_webhook_type = st.session_state.temp_config.get("WEBHOOK_TYPE", "dingtalk")
-
+
new_webhook_type = st.selectbox(
f"📲 {webhook_type_info['description']}",
options=webhook_type_info.get('options', ["dingtalk", "feishu"]),
@@ -2309,11 +2312,11 @@ def display_config_manager():
key="input_webhook_type"
)
st.session_state.temp_config["WEBHOOK_TYPE"] = new_webhook_type
-
+
# Webhook URL
webhook_url_info = config_info.get("WEBHOOK_URL", {"description": "Webhook地址", "value": ""})
current_webhook_url = st.session_state.temp_config.get("WEBHOOK_URL", "")
-
+
new_webhook_url = st.text_input(
f"🔗 {webhook_url_info['description']}",
value=current_webhook_url,
@@ -2322,11 +2325,11 @@ def display_config_manager():
key="input_webhook_url"
)
st.session_state.temp_config["WEBHOOK_URL"] = new_webhook_url
-
+
# Webhook自定义关键词(钉钉安全验证)
webhook_keyword_info = config_info.get("WEBHOOK_KEYWORD", {"description": "自定义关键词(钉钉安全验证)", "value": "aiagents通知"})
current_webhook_keyword = st.session_state.temp_config.get("WEBHOOK_KEYWORD", "aiagents通知")
-
+
new_webhook_keyword = st.text_input(
f"🔑 {webhook_keyword_info['description']}",
value=current_webhook_keyword,
@@ -2336,7 +2339,7 @@ def display_config_manager():
key="input_webhook_keyword"
)
st.session_state.temp_config["WEBHOOK_KEYWORD"] = new_webhook_keyword
-
+
# 测试连通按钮
if new_webhook_enabled and new_webhook_url:
if st.button("🧪 测试Webhook连通", width='stretch', key="test_webhook_btn"):
@@ -2346,13 +2349,13 @@ def display_config_manager():
for key in ["WEBHOOK_ENABLED", "WEBHOOK_TYPE", "WEBHOOK_URL", "WEBHOOK_KEYWORD"]:
temp_env_backup[key] = os.getenv(key)
os.environ[key] = st.session_state.temp_config.get(key, "")
-
+
try:
# 创建临时通知服务实例
from notification_service import NotificationService
temp_notification_service = NotificationService()
success, message = temp_notification_service.send_test_webhook()
-
+
if success:
st.success(f"✅ {message}")
else:
@@ -2366,59 +2369,59 @@ def display_config_manager():
os.environ[key] = value
elif key in os.environ:
del os.environ[key]
-
+
if new_webhook_enabled and new_webhook_url:
st.success(f"✅ Webhook配置完整 ({new_webhook_type})")
elif new_webhook_enabled:
st.warning("⚠️ 请配置Webhook URL")
else:
st.info("ℹ️ Webhook通知未启用")
-
+
# 显示帮助信息
if new_webhook_type == "dingtalk":
st.caption("💡 钉钉机器人配置:\n1. 进入钉钉群 → 设置 → 智能群助手\n2. 添加机器人 → 自定义\n3. 复制Webhook地址\n4. 安全设置选择【自定义关键词】,填写上方的关键词")
else:
st.caption("💡 飞书机器人配置:\n1. 进入飞书群 → 设置 → 群机器人\n2. 添加机器人 → 自定义机器人\n3. 复制Webhook地址")
-
+
st.markdown("---")
st.info("💡 **使用说明**:\n- 可以同时启用邮件和Webhook通知\n- 实时监测和智策定时分析都会使用配置的通知方式\n- 配置后建议使用各功能中的测试按钮验证通知是否正常")
-
+
# 操作按钮
st.markdown("---")
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
-
+
with col1:
if st.button("💾 保存配置", type="primary", width='stretch'):
# 验证配置
is_valid, message = config_manager.validate_config(st.session_state.temp_config)
-
+
if is_valid:
# 保存配置
if config_manager.write_env(st.session_state.temp_config):
st.success("✅ 配置已保存到 .env 文件")
st.info("ℹ️ 请重启应用使配置生效")
-
+
# 尝试重新加载配置
try:
config_manager.reload_config()
st.success("✅ 配置已重新加载")
except Exception as e:
st.warning(f"⚠️ 配置重新加载失败: {e}")
-
+
time.sleep(2)
st.rerun()
else:
st.error("❌ 保存配置失败")
else:
st.error(f"❌ 配置验证失败: {message}")
-
+
with col2:
if st.button("🔄 重置", width='stretch'):
# 重置为当前文件中的值
st.session_state.temp_config = {key: info["value"] for key, info in config_info.items()}
st.success("✅ 已重置为当前配置")
st.rerun()
-
+
with col3:
if st.button("⬅️ 返回", width='stretch'):
if 'show_config' in st.session_state:
@@ -2426,12 +2429,12 @@ def display_config_manager():
if 'temp_config' in st.session_state:
del st.session_state.temp_config
st.rerun()
-
+
# 显示当前.env文件内容
st.markdown("---")
with st.expander("📄 查看当前 .env 文件内容"):
current_config = config_manager.read_env()
-
+
st.code(f"""# AI股票分析系统环境配置
# 由系统自动生成和管理
@@ -2465,15 +2468,15 @@ WEBHOOK_KEYWORD="{current_config.get('WEBHOOK_KEYWORD', 'aiagents通知')}"
def display_batch_analysis_results(results, period):
"""显示批量分析结果(对比视图)"""
-
+
st.subheader("📊 批量分析结果对比")
-
+
# 统计信息
total = len(results)
success_results = [r for r in results if r['success']]
failed_results = [r for r in results if not r['success']]
saved_count = sum(1 for r in results if r.get('saved_to_db', False))
-
+
# 显示统计
col1, col2, col3, col4 = st.columns(4)
with col1:
@@ -2484,19 +2487,19 @@ def display_batch_analysis_results(results, period):
st.metric("失败", len(failed_results), delta=None, delta_color="inverse")
with col4:
st.metric("已保存", saved_count, delta=None, delta_color="normal")
-
+
# 提示信息
if saved_count > 0:
st.info(f"💾 已有 {saved_count} 只股票的分析结果保存到历史记录,可在侧边栏点击「📖 历史记录」查看")
-
+
st.markdown("---")
-
+
# 失败的股票列表
if failed_results:
with st.expander(f"❌ 查看失败的 {len(failed_results)} 只股票", expanded=False):
for result in failed_results:
st.error(f"**{result['symbol']}**: {result.get('error', '未知错误')}")
-
+
# 保存失败的股票列表
save_failed_results = [r for r in success_results if not r.get('saved_to_db', False)]
if save_failed_results:
@@ -2504,12 +2507,12 @@ def display_batch_analysis_results(results, period):
for result in save_failed_results:
db_error = result.get('db_error', '未知错误')
st.warning(f"**{result['symbol']} - {result['stock_info'].get('name', 'N/A')}**: {db_error}")
-
+
# 成功的股票分析结果
if not success_results:
st.warning("⚠️ 没有成功分析的股票")
return
-
+
# 创建对比视图选项
view_mode = st.radio(
"显示模式",
@@ -2517,7 +2520,7 @@ def display_batch_analysis_results(results, period):
horizontal=True,
help="对比表格:横向对比多只股票;详细卡片:逐个查看详细分析"
)
-
+
if view_mode == "对比表格":
# 表格对比视图
display_comparison_table(success_results)
@@ -2528,16 +2531,16 @@ def display_batch_analysis_results(results, period):
def display_comparison_table(results):
"""显示对比表格"""
import pandas as pd
-
+
st.subheader("📋 股票对比表格")
-
+
# 构建对比数据
comparison_data = []
for result in results:
stock_info = result['stock_info']
indicators = result.get('indicators', {})
final_decision = result['final_decision']
-
+
# 解析评级
if isinstance(final_decision, dict):
rating = final_decision.get('rating', 'N/A')
@@ -2547,11 +2550,11 @@ def display_comparison_table(results):
rating = 'N/A'
confidence = 'N/A'
target_price = 'N/A'
-
+
# 确保信心度为字符串类型,避免类型混合导致的序列化错误
if isinstance(confidence, (int, float)):
confidence = str(confidence)
-
+
row = {
'股票代码': stock_info.get('symbol', 'N/A'),
'股票名称': stock_info.get('name', 'N/A'),
@@ -2566,10 +2569,10 @@ def display_comparison_table(results):
'目标价格': target_price
}
comparison_data.append(row)
-
+
# 创建DataFrame
df = pd.DataFrame(comparison_data)
-
+
# 应用样式
# 显示表格(不使用样式,避免matplotlib导入问题)
st.dataframe(
@@ -2577,14 +2580,14 @@ def display_comparison_table(results):
width='stretch',
height=400
)
-
+
# 添加评级说明
st.caption("💡 投资评级说明:强烈买入 > 买入 > 持有 > 卖出 > 强烈卖出")
-
+
# 添加筛选功能
st.markdown("---")
st.subheader("🔍 快速筛选")
-
+
col1, col2 = st.columns(2)
with col1:
rating_filter = st.multiselect(
@@ -2592,17 +2595,17 @@ def display_comparison_table(results):
options=df['投资评级'].unique().tolist(),
default=df['投资评级'].unique().tolist()
)
-
+
with col2:
# 按涨跌幅排序
sort_by = st.selectbox(
"排序方式",
["默认", "涨跌幅降序", "涨跌幅升序", "信心度降序", "RSI降序"]
)
-
+
# 应用筛选
filtered_df = df[df['投资评级'].isin(rating_filter)]
-
+
# 应用排序
if sort_by == "涨跌幅降序":
filtered_df = filtered_df.sort_values('涨跌幅(%)', ascending=False)
@@ -2612,7 +2615,7 @@ def display_comparison_table(results):
filtered_df = filtered_df.sort_values('信心度', ascending=False)
elif sort_by == "RSI降序":
filtered_df = filtered_df.sort_values('RSI', ascending=False)
-
+
if not filtered_df.empty:
st.dataframe(filtered_df, width='stretch')
else:
@@ -2620,46 +2623,46 @@ def display_comparison_table(results):
def display_detailed_cards(results, period):
"""显示详细卡片视图"""
-
+
st.subheader("📇 详细分析卡片")
-
+
# 选择要查看的股票
stock_options = [f"{r['stock_info']['symbol']} - {r['stock_info']['name']}" for r in results]
selected_stock = st.selectbox("选择股票", options=stock_options)
-
+
# 找到对应的结果
selected_index = stock_options.index(selected_stock)
result = results[selected_index]
-
+
# 显示详细分析
stock_info = result['stock_info']
indicators = result['indicators']
agents_results = result['agents_results']
discussion_result = result['discussion_result']
final_decision = result['final_decision']
-
+
# 获取股票数据用于显示图表
try:
stock_info_current, stock_data, _ = get_stock_data(stock_info['symbol'], period)
-
+
# 显示股票基本信息
display_stock_info(stock_info, indicators)
-
+
# 显示股票图表
if stock_data is not None:
display_stock_chart(stock_data, stock_info)
-
+
# 显示各分析师报告
display_agents_analysis(agents_results)
-
+
# 显示团队讨论
display_team_discussion(discussion_result)
-
+
# 显示最终决策
display_final_decision(final_decision, stock_info, agents_results, discussion_result)
-
+
except Exception as e:
st.error(f"显示详细信息时出错: {str(e)}")
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/docker-compose.yml b/docker-compose.yml
index f0a79fb..cf484ad 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,7 +7,7 @@ services:
dockerfile: Dockerfile
container_name: agentsstock1
ports:
- - "8503:8501"
+ - "8503:8503"
volumes:
# 数据和数据库持久化 - 使用目录挂载而不是文件挂载
- ./data:/app/data
@@ -20,7 +20,7 @@ services:
networks:
- agentsstock-network
healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"]
+ test: ["CMD", "curl", "-f", "http://localhost:8503/_stcore/health"]
interval: 30s
timeout: 10s
retries: 3
diff --git a/longhubang_ui.py b/longhubang_ui.py
index db8ab46..e10dc60 100644
--- a/longhubang_ui.py
+++ b/longhubang_ui.py
@@ -136,9 +136,12 @@ def display_analysis_tab():
)
with col3:
+ # 导入model_config.py中定义的model_options
+ from model_config import model_options as app_model_options
selected_model = st.selectbox(
"AI模型",
- ["deepseek-chat", "deepseek-reasoner"],
+ list(app_model_options.keys()),
+ format_func=lambda x: app_model_options[x],
help="Reasoner模型提供更强的推理能力"
)
@@ -721,12 +724,12 @@ def display_visualizations(result):
def display_pdf_export_section(result):
"""显示PDF导出功能"""
- st.markdown("### 📄 导出PDF报告")
+ st.markdown("### 📄 导出报告")
- col1, col2 = st.columns([3, 1])
+ col1, col2, col3 = st.columns([2, 1, 1])
with col1:
- st.info("💡 点击按钮生成并下载专业的PDF分析报告")
+ st.info("💡 点击按钮生成并下载专业分析报告")
with col2:
if st.button("📥 生成PDF", type="primary", width='stretch'):
@@ -752,6 +755,154 @@ def display_pdf_export_section(result):
except Exception as 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():
diff --git a/main_force_ui.py b/main_force_ui.py
index 3df96fa..5477f48 100644
--- a/main_force_ui.py
+++ b/main_force_ui.py
@@ -13,17 +13,17 @@ 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:
@@ -33,9 +33,9 @@ def display_main_force_selector():
if st.button("📚 批量分析历史", width='content'):
st.session_state.main_force_view_history = True
st.rerun()
-
+
st.markdown("---")
-
+
st.markdown("""
### 功能说明
@@ -53,18 +53,18 @@ def display_main_force_selector():
- ✅ 行业前景明朗
- ✅ 综合素质优秀
""")
-
+
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
@@ -81,7 +81,7 @@ def display_main_force_selector():
)
start_date = f"{custom_date.year}年{custom_date.month}月{custom_date.day}日"
days_ago = None
-
+
with col2:
final_n = st.slider(
"最终精选数量",
@@ -91,14 +91,14 @@ def display_main_force_selector():
step=1,
help="最终推荐的股票数量"
)
-
+
with col3:
st.info("💡 系统将获取前100名股票,进行整体分析后精选优质标的")
-
+
# 高级选项
with st.expander("⚙️ 高级筛选参数"):
col1, col2, col3 = st.columns(3)
-
+
with col1:
max_change = st.number_input(
"最大涨跌幅(%)",
@@ -108,7 +108,7 @@ def display_main_force_selector():
step=5.0,
help="过滤掉涨幅过高的股票,避免追高"
)
-
+
with col2:
min_cap = st.number_input(
"最小市值(亿)",
@@ -117,7 +117,7 @@ def display_main_force_selector():
value=50.0,
step=10.0
)
-
+
with col3:
max_cap = st.number_input(
"最大市值(亿)",
@@ -126,24 +126,27 @@ def display_main_force_selector():
value=5000.0,
step=100.0
)
-
+
# 模型选择
+ # 导入model_config.py中定义的model_options
+ from model_config import model_options as app_model_options
model = st.selectbox(
"选择AI模型",
- ["deepseek-chat", "deepseek-reasoner"],
+ list(app_model_options.keys()),
+ format_func=lambda x: app_model_options[x],
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,
@@ -153,83 +156,83 @@ def display_main_force_selector():
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']}",
+ 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:
@@ -239,11 +242,11 @@ def display_analysis_results(result: dict, analyzer):
break
if main_fund_col:
display_cols.append(main_fund_col)
-
+
# 添加区间涨跌幅(前复权)(智能匹配)
interval_pct_col = None
interval_pct_patterns = [
- '区间涨跌幅:前复权', '区间涨跌幅:前复权(%)', '区间涨跌幅(%)',
+ '区间涨跌幅:前复权', '区间涨跌幅:前复权(%)', '区间涨跌幅(%)',
'区间涨跌幅', '涨跌幅:前复权', '涨跌幅:前复权(%)', '涨跌幅(%)', '涨跌幅'
]
for pattern in interval_pct_patterns:
@@ -253,16 +256,16 @@ def display_analysis_results(result: dict, analyzer):
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("所有可用列:")
@@ -277,14 +280,14 @@ def display_analysis_results(result: dict, analyzer):
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(
@@ -293,15 +296,15 @@ def display_analysis_results(result: dict, analyzer):
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(
"分析数量",
@@ -309,18 +312,18 @@ def display_analysis_results(result: dict, analyzer):
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 = []
@@ -332,55 +335,55 @@ def display_analysis_results(result: dict, analyzer):
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]
@@ -390,7 +393,7 @@ def display_recommendation_detail(rec: dict):
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]
@@ -400,23 +403,23 @@ def display_recommendation_detail(rec: dict):
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]
@@ -425,12 +428,12 @@ def display_recommendation_detail(rec: dict):
def display_analyst_reports(analyzer):
"""显示AI分析师完整报告"""
-
+
st.markdown("### 🤖 AI分析师团队完整报告")
-
+
# 创建三个标签页
tab1, tab2, tab3 = st.tabs(["💰 资金流向分析", "📊 行业板块分析", "📈 财务基本面分析"])
-
+
with tab1:
st.markdown("#### 💰 资金流向分析师报告")
st.markdown("---")
@@ -438,7 +441,7 @@ def display_analyst_reports(analyzer):
st.markdown(analyzer.fund_flow_analysis)
else:
st.info("暂无资金流向分析报告")
-
+
with tab2:
st.markdown("#### 📊 行业板块及市场热点分析师报告")
st.markdown("---")
@@ -446,7 +449,7 @@ def display_analyst_reports(analyzer):
st.markdown(analyzer.industry_analysis)
else:
st.info("暂无行业板块分析报告")
-
+
with tab3:
st.markdown("#### 📈 财务基本面分析师报告")
st.markdown("---")
@@ -459,10 +462,10 @@ 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亿(以元为单位)
@@ -471,7 +474,7 @@ def format_number(value, unit='', suffix=''):
pass
else: # 100-100000000之间,可能是万
num = num / 10000
-
+
# 格式化显示
if abs(num) >= 1000:
formatted = f"{num:,.2f}"
@@ -479,7 +482,7 @@ def format_number(value, unit='', suffix=''):
formatted = f"{num:.2f}"
else:
formatted = f"{num:.4f}"
-
+
return f"{formatted}{suffix}"
except (ValueError, TypeError):
return str(value)
@@ -489,14 +492,14 @@ 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:
@@ -509,28 +512,28 @@ def run_main_force_batch_analysis():
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"):
# 清除所有批量分析相关状态
@@ -539,12 +542,12 @@ def run_main_force_batch_analysis():
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(
"分析模式",
@@ -552,7 +555,7 @@ def run_main_force_batch_analysis():
format_func=lambda x: "顺序分析(稳定)" if x == "sequential" else "并行分析(快速)",
help="顺序分析较慢但稳定,并行分析更快但消耗更多资源"
)
-
+
with col2:
if analysis_mode == "parallel":
max_workers = st.number_input(
@@ -564,17 +567,17 @@ def run_main_force_batch_analysis():
)
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'):
# 清除所有批量分析相关状态
@@ -583,16 +586,16 @@ def run_main_force_batch_analysis():
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)} 只")
@@ -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"**分析模式**: {analysis_mode}")
st.write(f"**线程数**: {max_workers if analysis_mode == 'parallel' else 1}")
-
+
# 配置分析师参数
enabled_analysts_config = {
'technical': True,
@@ -612,23 +615,23 @@ def run_main_force_batch_analysis():
}
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(
@@ -637,23 +640,23 @@ def run_main_force_batch_analysis():
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}")
@@ -668,10 +671,10 @@ def run_main_force_batch_analysis():
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] # 获取对应的股票代码
@@ -679,48 +682,48 @@ def run_main_force_batch_analysis():
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"📝 准备保存批量分析结果到历史记录")
@@ -731,17 +734,17 @@ def run_main_force_batch_analysis():
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),
@@ -751,14 +754,14 @@ def run_main_force_batch_analysis():
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)
@@ -769,7 +772,7 @@ def run_main_force_batch_analysis():
print(f"详细错误:")
print(traceback.format_exc())
print(f"{'='*60}\n")
-
+
# 保存结果到session_state
st.session_state.main_force_batch_results = {
"results": results,
@@ -781,9 +784,9 @@ def run_main_force_batch_analysis():
"saved_to_history": save_success,
"save_error": save_error
}
-
+
time.sleep(0.5)
-
+
# 重新渲染以显示结果
st.rerun()
@@ -791,7 +794,7 @@ def run_main_force_batch_analysis():
def display_main_force_batch_results(batch_results):
"""显示主力选股批量分析结果"""
import re
-
+
results = batch_results['results']
total = batch_results['total']
success = batch_results['success']
@@ -799,46 +802,46 @@ def display_main_force_batch_results(batch_results):
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 = {
@@ -848,7 +851,7 @@ def display_main_force_batch_results(batch_results):
'卖出': '⚠️',
'强烈卖出': '🚫'
}.get(rating, '❓')
-
+
display_data.append({
'股票代码': stock_info.get('symbol', ''),
'股票名称': 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('target_price', 'N/A')
})
-
+
df_display = pd.DataFrame(display_data)
-
+
# 类型统一,避免Arrow序列化错误
numeric_cols = ['信心度', '止盈位', '止损位', '目标价']
for col in numeric_cols:
@@ -872,17 +875,17 @@ def display_main_force_batch_results(batch_results):
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', '未知')
@@ -893,34 +896,34 @@ def display_main_force_batch_results(batch_results):
'卖出': '⚠️',
'强烈卖出': '🚫'
}.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}"):
# 解析进场区间
@@ -933,7 +936,7 @@ def display_main_force_batch_results(batch_results):
entry_max = float(parts[1].strip())
except:
pass
-
+
# 解析止盈止损
take_profit_str = final_decision.get('take_profit', '')
take_profit = None
@@ -944,7 +947,7 @@ def display_main_force_batch_results(batch_results):
take_profit = float(numbers[0])
except:
pass
-
+
stop_loss_str = final_decision.get('stop_loss', '')
stop_loss = None
if stop_loss_str:
@@ -954,16 +957,16 @@ def display_main_force_batch_results(batch_results):
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,
@@ -976,21 +979,21 @@ def display_main_force_batch_results(batch_results):
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')
diff --git a/model_config.py b/model_config.py
new file mode 100644
index 0000000..c28270e
--- /dev/null
+++ b/model_config.py
@@ -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": "阶跃星辰(硅基流动)"
+}
\ No newline at end of file
diff --git a/pdf_generator.py b/pdf_generator.py
index 69b9c47..8f01c1b 100644
--- a/pdf_generator.py
+++ b/pdf_generator.py
@@ -259,6 +259,128 @@ def create_download_link(pdf_content, filename):
href = f'📄 下载PDF报告'
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'📝 下载Markdown报告'
+ return href
+
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
"""显示PDF导出区域"""
@@ -269,8 +391,11 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
with col2:
# 生成PDF报告按钮(使用股票代码作为key的一部分,确保唯一性)
- button_key = f"pdf_btn_{stock_info.get('symbol', 'unknown')}"
- if st.button("📄 生成并下载PDF报告", type="primary", width='content', key=button_key):
+ pdf_button_key = f"pdf_btn_{stock_info.get('symbol', 'unknown')}"
+ 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报告..."):
try:
# 生成PDF内容
@@ -300,3 +425,36 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
st.error(f"❌ 生成PDF报告时出错: {str(e)}")
import traceback
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"""
+
+ {download_link}
+
+ """, 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()}")
+
diff --git a/pdf_generator_fixed.py b/pdf_generator_fixed.py
index 9ea3ae0..ee15bdd 100644
--- a/pdf_generator_fixed.py
+++ b/pdf_generator_fixed.py
@@ -133,151 +133,6 @@ def create_html_download_link(content, filename, link_text):
href = f'{link_text}'
return href
-def generate_html_content(markdown_content):
- """将Markdown转换为HTML"""
- html_content = f"""
-
-
-
-
- AI股票分析报告
-
-
-
-
-"""
-
- # 简单的Markdown到HTML转换
- html_body = markdown_content
- html_body = html_body.replace('\n# ', '\n
').replace('\n## ', '\n').replace('\n### ', '\n')
- html_body = html_body.replace('# ', '').replace('## ', '').replace('### ', '')
- html_body = html_body.replace('\n---\n', '\n
\n')
-
- # 处理粗体文本
- html_body = re.sub(r'\*\*(.*?)\*\*', r'\1', 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('
')
- in_table = True
- cells = [cell.strip() for cell in line.split('|')[1:-1]]
- processed_lines.append('')
- for cell in cells:
- processed_lines.append(f'| {cell} | ')
- processed_lines.append('
')
- elif '|' in line and in_table:
- if '---' not in line:
- cells = [cell.strip() for cell in line.split('|')[1:-1]]
- processed_lines.append('')
- for cell in cells:
- processed_lines.append(f'| {cell} | ')
- processed_lines.append('
')
- elif in_table and '|' not in line:
- processed_lines.append('
')
- processed_lines.append(line)
- in_table = False
- else:
- processed_lines.append(line)
-
- if in_table:
- processed_lines.append('')
-
- 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'
{para}
')
- else:
- processed_paragraphs.append(para)
-
- html_body = '\n'.join(processed_paragraphs)
-
- html_content += html_body + """
-
-
-
-"""
-
- return html_content
-
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
"""显示PDF导出区域 - 修复报告生成问题"""
@@ -290,8 +145,11 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
# 生成报告按钮
import uuid
import time
- button_key = f"generate_report_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
- if st.button("📊 生成并下载报告", type="primary", width='content', key=button_key):
+ pdf_button_key = f"generate_report_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
+ 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("正在生成报告..."):
try:
# 生成Markdown内容
@@ -337,4 +195,44 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
except Exception as e:
st.error(f"❌ 生成报告时出错: {str(e)}")
import traceback
- st.error(f"详细错误信息: {traceback.format_exc()}")
\ No newline at end of file
+ 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"""
+
+ {md_link}
+
+ """, 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()}")
+
diff --git a/pdf_generator_pandoc.py b/pdf_generator_pandoc.py
index fde9066..cdfafec 100644
--- a/pdf_generator_pandoc.py
+++ b/pdf_generator_pandoc.py
@@ -267,12 +267,47 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
col1, col2, col3 = st.columns([1, 2, 1])
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
with st.spinner("正在生成报告..."):
success = generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
if success:
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:
diff --git a/sector_strategy_ui.py b/sector_strategy_ui.py
index 8b03514..dc5f13a 100644
--- a/sector_strategy_ui.py
+++ b/sector_strategy_ui.py
@@ -116,9 +116,12 @@ def display_analysis_tab():
col1, col2, col3 = st.columns([2, 2, 2])
with col1:
+ # 导入model_config.py中定义的model_options
+ from model_config import model_options as app_model_options
selected_model = st.selectbox(
- "选择AI模型",
- ["deepseek-chat", "deepseek-reasoner"],
+ "AI模型",
+ list(app_model_options.keys()),
+ format_func=lambda x: app_model_options[x],
help="Reasoner模型提供更强的推理能力"
)
@@ -774,10 +777,10 @@ def display_pdf_export_section(result):
"""显示PDF导出部分"""
st.subheader("📄 导出报告")
- col1, col2, col3 = st.columns([2, 1, 1])
+ col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
with col1:
- st.write("将分析报告导出为PDF文件,方便保存和分享")
+ st.write("将分析报告导出为PDF或Markdown文件,方便保存和分享")
with col2:
if st.button("📥 生成PDF报告", type="primary", width='content'):
@@ -802,6 +805,23 @@ def display_pdf_export_section(result):
st.error(f"❌ PDF生成失败: {str(e)}")
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,显示下载按钮
if 'sector_pdf_data' in st.session_state:
st.download_button(
@@ -811,6 +831,197 @@ def display_pdf_export_section(result):
mime="application/pdf",
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():