From dbc6b5b3599a49eca2af978d14cbbbdaa52c8448 Mon Sep 17 00:00:00 2001 From: wsx180808 <53265364+wsx180808@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:10:15 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8Ddocker=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=89=93=E5=BC=80=E9=A1=B5=E9=9D=A2=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.支持下载DM文件 3.支持硅基流动,阿里百炼模型 --- app.py | 817 ++++++++++++++++++++-------------------- docker-compose.yml | 4 +- longhubang_ui.py | 159 +++++++- main_force_ui.py | 349 ++++++++--------- model_config.py | 24 ++ pdf_generator.py | 162 +++++++- pdf_generator_fixed.py | 194 +++------- pdf_generator_pandoc.py | 37 +- sector_strategy_ui.py | 219 ++++++++++- 9 files changed, 1224 insertions(+), 741 deletions(-) create mode 100644 model_config.py 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(): """, 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'') - 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'') - processed_lines.append('') - elif in_table and '|' not in line: - processed_lines.append('
{cell}
{cell}
') - 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():