From aab874d32631ba667dfe27cff66e7171486f3ec1 Mon Sep 17 00:00:00 2001 From: oficcejo Date: Sun, 19 Oct 2025 09:43:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E9=A3=8E=E9=99=A9=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B8=88=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 10 +- ai_agents.py | 132 +++++++++-- app.py | 83 +++++-- risk_data_debug_output.txt | 60 +++++ risk_data_fetcher.py | 469 +++++++++++++++++++++++++++++++++++++ stock_data.py | 28 +++ test_risk_data.py | 173 ++++++++++++++ test_risk_data_debug.py | 146 ++++++++++++ 风险管理师增强功能说明.md | 371 +++++++++++++++++++++++++++++ 9 files changed, 1425 insertions(+), 47 deletions(-) create mode 100644 risk_data_debug_output.txt create mode 100644 risk_data_fetcher.py create mode 100644 test_risk_data.py create mode 100644 test_risk_data_debug.py create mode 100644 风险管理师增强功能说明.md diff --git a/.env.example b/.env.example index f0d2198..19d9873 100644 --- a/.env.example +++ b/.env.example @@ -21,14 +21,6 @@ DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # 获取地址:https://tushare.pro/register TUSHARE_TOKEN= -# ========== AI服务商配置 ========== -AI_PROVIDER="siliconflow" - -# ========== 硅基流动 API配置 ========== -SILICONFLOW_API_KEY="YOUR_API_KEY_FROM_CLOUD_SILICONFLOW_CN" -SILICONFLOW_BASE_URL="https://api.siliconflow.cn/v1" -SILICONFLOW_MODEL="Qwen/Qwen2.5-72B-Instruct" - # ========== MiniQMT量化交易配置(可选)========== # 是否启用MiniQMT量化交易接口 @@ -58,7 +50,7 @@ SMTP_SERVER=smtp.163.com # SMTP服务器端口 # 常用端口: -# - 465:SSL加密连接(推荐) +# - 465:SSL加密连接163邮箱常用(推荐) # - 587:TLS加密连接 SMTP_PORT=465 diff --git a/ai_agents.py b/ai_agents.py index df45d14..6ec1baa 100644 --- a/ai_agents.py +++ b/ai_agents.py @@ -73,13 +73,34 @@ class StockAnalysisAgents: "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") } - def risk_management_agent(self, stock_info: Dict, indicators: Dict) -> Dict[str, Any]: - """风险管理智能体""" + def risk_management_agent(self, stock_info: Dict, indicators: Dict, risk_data: Dict = None) -> Dict[str, Any]: + """风险管理智能体(增强版)""" print("⚠️ 风险管理师正在评估中...") + + # 如果有风险数据,显示数据来源 + if risk_data and risk_data.get('data_success'): + print(" ✓ 已获取问财风险数据(限售解禁、大股东减持、重要事件)") + else: + print(" ⚠ 未获取到风险数据,将基于基本信息分析") + time.sleep(1) + # 构建风险数据文本 + risk_data_text = "" + if risk_data and risk_data.get('data_success'): + # 使用格式化的风险数据 + from risk_data_fetcher import RiskDataFetcher + fetcher = RiskDataFetcher() + risk_data_text = f""" + +【实际风险数据】(来自问财) +{fetcher.format_risk_data_for_ai(risk_data)} + +以上是通过问财(pywencai)获取的实际风险数据,请重点关注这些数据进行深度风险分析。 +""" + risk_prompt = f""" -作为风险管理专家,请基于以下信息进行风险评估: +作为资深风险管理专家,请基于以下信息进行全面深度的风险评估: 股票信息: - 股票代码:{stock_info.get('symbol', 'N/A')} @@ -93,32 +114,105 @@ class StockAnalysisAgents: - RSI:{indicators.get('rsi', 'N/A')} - 布林带位置:当前价格相对于上下轨的位置 - 波动率指标等 +{risk_data_text} -请从以下角度进行风险评估: -1. 市场风险(系统性风险) -2. 个股风险(非系统性风险) -3. 流动性风险 -4. 波动性风险 -5. 估值风险 -6. 行业风险 -7. 风险等级评定(低/中/高) -8. 风险控制建议 +⚠️ 重要提示:以上风险数据是从问财(pywencai)实时查询的完整原始数据,请你: +1. 仔细解析每一条记录的所有字段信息 +2. 识别数据中的关键风险点(时间、规模、频率、股东身份等) +3. 对数据进行深度分析,不要遗漏任何重要信息 +4. 如果数据中有日期字段,要特别关注最近的记录和即将发生的事件 +5. 如果数据中有金额/比例字段,要评估其规模和影响力 +6. 基于实际数据给出量化的风险评估,而不是空泛的描述 -给出专业的风险评估报告。 +请从以下角度进行全面的风险评估: + +1. **限售解禁风险分析** ⭐ 重点 + - 解禁时间和规模评估 + - 解禁对股价的潜在冲击 + - 解禁股东类型分析(创始人/投资机构/其他) + - 历史解禁后股价走势参考 + - 风险等级评定和应对建议 + +2. **股东减持风险分析** ⭐ 重点 + - 减持频率和力度评估 + - 减持股东身份和意图分析 + - 减持对市场信心的影响 + - 是否存在连续减持或集中减持 + - 风险警示和投资建议 + +3. **重要事件风险分析** ⭐ 重点 + - 识别可能影响股价的重大事件 + - 事件性质判断(利好/利空/中性) + - 事件影响的时间维度(短期/中期/长期) + - 事件的确定性和不确定性 + - 风险提示和关注要点 + +4. **市场风险(系统性风险)** + - 宏观经济环境风险 + - 市场整体走势风险 + - Beta系数反映的市场敏感度 + - 系统性风险应对策略 + +5. **个股风险(非系统性风险)** + - 公司基本面风险 + - 经营管理风险 + - 竞争力风险 + - 行业地位风险 + +6. **流动性风险** + - 成交量和换手率分析 + - 买卖盘深度评估 + - 流动性枯竭风险 + - 大额交易影响评估 + +7. **波动性风险** + - 价格波动幅度分析 + - 52周最高最低位分析 + - RSI等技术指标的风险提示 + - 波动率对投资的影响 + +8. **估值风险** + - 当前估值水平评估 + - 市场预期和估值偏差 + - 估值过高风险警示 + +9. **行业风险** + - 行业周期阶段 + - 行业竞争格局 + - 行业政策风险 + - 行业技术变革风险 + +10. **综合风险评定** + - 风险等级评定(低/中/高) + - 主要风险因素排序 + - 风险暴露时间窗口 + - 风险演变趋势判断 + +11. **风险控制建议** ⭐ 核心 + - 仓位控制建议(具体比例) + - 止损位设置建议(具体价位) + - 风险规避策略(什么情况下不建议投资) + - 风险对冲方案(如果适用) + - 持仓时间建议 + - 重点关注指标和信号 + +请基于实际数据进行客观、专业、严谨的风险评估,给出可操作的风险控制建议。 +如果某些风险数据缺失,也要指出数据缺失本身可能带来的风险。 """ messages = [ - {"role": "system", "content": "你是一名专业的风险管理专家,具有丰富的风险识别和控制经验。"}, + {"role": "system", "content": "你是一名资深的风险管理专家,具有20年以上的风险识别和控制经验,擅长全面评估各类投资风险,特别关注限售解禁、股东减持、重要事件等可能影响股价的风险因素。你擅长从海量原始数据中提取关键信息,进行深度解析和量化评估。"}, {"role": "user", "content": risk_prompt} ] - analysis = self.deepseek_client.call_api(messages) + analysis = self.deepseek_client.call_api(messages, max_tokens=6000) return { "agent_name": "风险管理师", "agent_role": "负责风险识别、风险评估、风险控制策略制定", "analysis": analysis, - "focus_areas": ["风险识别", "风险量化", "风险控制", "资产配置"], + "focus_areas": ["限售解禁风险", "股东减持风险", "重要事件风险", "风险识别", "风险量化", "风险控制", "资产配置"], + "risk_data": risk_data, # 保存风险数据以供后续使用 "timestamp": time.strftime("%Y-%m-%d %H:%M:%S") } @@ -313,7 +407,7 @@ class StockAnalysisAgents: def run_multi_agent_analysis(self, stock_info: Dict, stock_data: Any, indicators: Dict, financial_data: Dict = None, fund_flow_data: Dict = None, sentiment_data: Dict = None, news_data: Dict = None, - quarterly_data: Dict = None, + quarterly_data: Dict = None, risk_data: Dict = None, enabled_analysts: Dict = None) -> Dict[str, Any]: """运行多智能体分析 @@ -356,9 +450,9 @@ class StockAnalysisAgents: if enabled_analysts.get('fund_flow', True): agents_results["fund_flow"] = self.fund_flow_analyst_agent(stock_info, indicators, fund_flow_data) - # 风险管理分析 + # 风险管理分析(传入风险数据) if enabled_analysts.get('risk', True): - agents_results["risk_management"] = self.risk_management_agent(stock_info, indicators) + agents_results["risk_management"] = self.risk_management_agent(stock_info, indicators, risk_data) # 市场情绪分析(传入市场情绪数据) if enabled_analysts.get('sentiment', False): diff --git a/app.py b/app.py index 1ba331f..8d8ef19 100644 --- a/app.py +++ b/app.py @@ -636,9 +636,21 @@ def main(): st.info(f"📊 准备分析 {len(stock_list)} 只股票: {', '.join(stock_list)}") - # 清除之前的批量分析结果 + # 清除之前的分析结果(包括单个和批量) if 'batch_analysis_results' in st.session_state: del st.session_state.batch_analysis_results + if 'analysis_completed' in st.session_state: + del st.session_state.analysis_completed + if 'stock_info' in st.session_state: + del st.session_state.stock_info + if 'agents_results' in st.session_state: + del st.session_state.agents_results + if 'discussion_result' in st.session_state: + del st.session_state.discussion_result + if 'final_decision' in st.session_state: + 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', '顺序分析') @@ -646,8 +658,12 @@ def main(): # 运行批量分析 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) + # 检查是否有已完成的单个分析结果(但不是刚刚完成的,避免重复显示) - if 'analysis_completed' in st.session_state and st.session_state.analysis_completed: + elif 'analysis_completed' in st.session_state and st.session_state.analysis_completed: # 如果是刚刚完成的分析,清除标志,避免重复显示 if st.session_state.get('just_completed', False): st.session_state.just_completed = False @@ -677,10 +693,6 @@ def main(): # 显示最终决策 display_final_decision(final_decision, stock_info, agents_results, discussion_result) - # 检查是否有已完成的批量分析结果 - elif '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 not stock_input: show_example_interface() @@ -832,6 +844,15 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None, except: pass + # 5.5 获取风险数据(限售解禁、大股东减持、重要事件,可选) + risk_data = None + enable_risk = enabled_analysts_config.get('risk', True) + if enable_risk and fetcher._is_chinese_stock(symbol): + try: + risk_data = fetcher.get_risk_data(symbol) + except: + pass + # 6. 初始化AI分析系统 agents = StockAnalysisAgents(model=selected_model) @@ -841,7 +862,7 @@ def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None, # 7. 运行多智能体分析 agents_results = agents.run_multi_agent_analysis( stock_info, stock_data, indicators, financial_data, - fund_flow_data, sentiment_data, news_data, quarterly_data, + fund_flow_data, sentiment_data, news_data, quarterly_data, risk_data, enabled_analysts=enabled_analysts_config ) @@ -1137,6 +1158,36 @@ def run_stock_analysis(symbol, period): news_data = None 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 + if enable_risk and fetcher._is_chinese_stock(symbol): + status_text.text("⚠️ 正在获取风险数据(限售解禁、大股东减持、重要事件)...") + try: + risk_data = fetcher.get_risk_data(symbol) + if risk_data and risk_data.get('data_success'): + # 统计获取到的风险数据类型 + risk_types = [] + if risk_data.get('lifting_ban') and risk_data['lifting_ban'].get('has_data'): + risk_types.append("限售解禁") + if risk_data.get('shareholder_reduction') and risk_data['shareholder_reduction'].get('has_data'): + 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: + st.info("ℹ️ 暂无风险相关数据") + else: + st.info("ℹ️ 暂无风险相关数据,将基于基本信息进行风险分析") + except Exception as e: + st.warning(f"⚠️ 获取风险数据时出错: {str(e)}") + risk_data = None + elif enable_risk and not fetcher._is_chinese_stock(symbol): + st.info("ℹ️ 美股暂不支持风险数据(限售解禁、大股东减持等)") progress_bar.progress(50) # 6. 初始化AI分析系统 @@ -1165,7 +1216,7 @@ def run_stock_analysis(symbol, period): status_text.text("🔍 AI分析师团队正在分析,请耐心等待几分钟...") agents_results = agents.run_multi_agent_analysis( stock_info, stock_data, indicators, financial_data, - fund_flow_data, sentiment_data, news_data, quarterly_data, + fund_flow_data, sentiment_data, news_data, quarterly_data, risk_data, enabled_analysts=enabled_analysts ) progress_bar.progress(75) @@ -2504,22 +2555,16 @@ def display_comparison_table(results): df = pd.DataFrame(comparison_data) # 应用样式 - def highlight_rating(val): - if val == '买入' or val == '强烈买入': - return 'background-color: #c8e6c9; color: #2e7d32;' - elif val == '持有': - return 'background-color: #fff9c4; color: #f57f17;' - elif val == '卖出' or val == '强烈卖出': - return 'background-color: #ffcdd2; color: #c62828;' - return '' - - # 显示表格 + # 显示表格(不使用样式,避免matplotlib导入问题) st.dataframe( - df.style.applymap(highlight_rating, subset=['投资评级']), + df, use_container_width=True, height=400 ) + # 添加评级说明 + st.caption("💡 投资评级说明:强烈买入 > 买入 > 持有 > 卖出 > 强烈卖出") + # 添加筛选功能 st.markdown("---") st.subheader("🔍 快速筛选") diff --git a/risk_data_debug_output.txt b/risk_data_debug_output.txt new file mode 100644 index 0000000..d9c4e31 --- /dev/null +++ b/risk_data_debug_output.txt @@ -0,0 +1,60 @@ +================================================================================ +原始数据结构 +================================================================================ + +data_success: True + + +lifting_ban: + has_data: True + query: 300433限售解禁 + 记录数: 1 + 列名: ['table2', '解禁股数_选股表格', '解禁股数_回测点评', '解禁股数_事件K线图', '解禁股数_解禁时间表', '解禁股数_公告', '解禁股数_研报', '解禁股数_新闻'] + +shareholder_reduction: + has_data: True + query: 300433大股东减持公告 + 记录数: 1 + 列名: ['title_content'] + +important_events: + has_data: True + query: 300433近期重要事件 + 记录数: 1 + 列名: ['近期重要事件', '重要事件共用模块_简单表格', '重要事件共用模块_事件k线图'] + +================================================================================ +格式化后传给AI的数据 +================================================================================ + +================================================================================ +【限售解禁数据】 +================================================================================ +查询语句: 300433限售解禁 + +共 1 条记录,显示前50条: + table2 解禁股数_选股表格 解禁股数_回测点评 解禁股数_事件K线图 解禁股数_解禁时间表 解禁股数_公告 解禁股数_研报 解禁股数_新闻 +[{'实际解禁股数': 4557611, '解禁股类型': '股权激励限售股份', '是否公告值': '公布值', '股票简称': '蓝思科技', '解禁成本': 6.118285024553978, '实际解禁金额': 124650660.85000001, '解禁日期': '20251017', '解禁计算参考时间': '20251017', '时间区间': '20251017-20261016', '股票代码': '300433.SZ', '实际解禁比例': 0.09146810710188108}] {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 1}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 7619, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=7619&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 2}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 10667, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=10667&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 3}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 7621, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=7621&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 4}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9377, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9377&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 5}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9379, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9379&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 6}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9381, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9381&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 7}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9383, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9383&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} + +================================================================================ +【大股东减持数据】 +================================================================================ +查询语句: 300433大股东减持公告 + +共 1 条记录,显示前50条: + title_content + uid ... publish_date +0 bf08a24fbdf8d319 ... NaN +1 1798c7717c8b9a90 ... 2025-10-15 08:47:10 +2 499bb08daa40cdf1 ... NaN + +[3 rows x 8 columns] + +================================================================================ +【重要事件数据】 +================================================================================ +查询语句: 300433近期重要事件 + +共 1 条记录,显示前50条: + 近期重要事件 重要事件共用模块_简单表格 重要事件共用模块_事件k线图 +[{'事件': '分红预案', '概要': '拟10派1元(含税)。', '日期': '20250826', '股票简称': '蓝思科技', '股票代码': '300433.SZ'}] {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '300433近期重要事件', 'extra': {'source_key': 1}, 'logid': 'c6b7099cfb37d5ae1c35b22f181e7cb0', 'pid': 13332, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251019","20251019"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251019%22%2C%2220251019%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=c6b7099cfb37d5ae1c35b22f181e7cb0&pid=13332&w=300433%E8%BF%91%E6%9C%9F%E9%87%8D%E8%A6%81%E4%BA%8B%E4%BB%B6'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '300433近期重要事件', 'extra': {'source_key': 2}, 'logid': 'c6b7099cfb37d5ae1c35b22f181e7cb0', 'pid': 13333, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251019","20251019"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251019%22%2C%2220251019%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=c6b7099cfb37d5ae1c35b22f181e7cb0&pid=13333&w=300433%E8%BF%91%E6%9C%9F%E9%87%8D%E8%A6%81%E4%BA%8B%E4%BB%B6'} diff --git a/risk_data_fetcher.py b/risk_data_fetcher.py new file mode 100644 index 0000000..bd6af7e --- /dev/null +++ b/risk_data_fetcher.py @@ -0,0 +1,469 @@ +""" +风险数据获取模块 +使用pywencai获取股票风险相关信息: +1. 限售解禁数据 +2. 大股东减持公告 +3. 近期重要事件 +""" + +import pywencai +import pandas as pd +from typing import Dict, Any +import time +import warnings +import os + +# 屏蔽pywencai的Node.js警告信息(不影响功能) +warnings.filterwarnings('ignore', category=DeprecationWarning) +os.environ['PYTHONWARNINGS'] = 'ignore::DeprecationWarning' +os.environ['NODE_NO_WARNINGS'] = '1' # 屏蔽Node.js警告 + + +class RiskDataFetcher: + """风险数据获取类""" + + def __init__(self): + """初始化""" + pass + + def get_risk_data(self, symbol: str) -> Dict[str, Any]: + """ + 获取股票风险相关数据 + + Args: + symbol: 股票代码(如:600000) + + Returns: + 包含风险数据的字典 + """ + print(f"\n正在获取 {symbol} 的风险数据...") + + risk_data = { + 'symbol': symbol, + 'data_success': False, + 'lifting_ban': None, # 限售解禁数据 + 'shareholder_reduction': None, # 大股东减持数据 + 'important_events': None, # 重要事件数据 + 'error': None + } + + try: + # 1. 获取限售解禁数据 + print(" 查询限售解禁数据...") + lifting_ban = self._get_lifting_ban_data(symbol) + risk_data['lifting_ban'] = lifting_ban + if lifting_ban and lifting_ban.get('has_data'): + print(f" 获取到限售解禁数据") + else: + print(f" 暂无限售解禁数据") + + time.sleep(1) # 避免请求过快 + + # 2. 获取大股东减持公告 + print(" 查询大股东减持公告...") + reduction = self._get_shareholder_reduction_data(symbol) + risk_data['shareholder_reduction'] = reduction + if reduction and reduction.get('has_data'): + print(f" 获取到大股东减持数据") + else: + print(f" 暂无大股东减持数据") + + time.sleep(1) # 避免请求过快 + + # 3. 获取近期重要事件 + print(" 查询近期重要事件...") + events = self._get_important_events_data(symbol) + risk_data['important_events'] = events + if events and events.get('has_data'): + print(f" 获取到重要事件数据") + else: + print(f" 暂无重要事件数据") + + # 如果至少有一个数据源成功,则认为获取成功 + if (lifting_ban and lifting_ban.get('has_data')) or \ + (reduction and reduction.get('has_data')) or \ + (events and events.get('has_data')): + risk_data['data_success'] = True + print(f"风险数据获取完成") + else: + print(f"未获取到风险相关数据") + + except Exception as e: + print(f"风险数据获取失败: {str(e)}") + risk_data['error'] = str(e) + + return risk_data + + def _get_lifting_ban_data(self, symbol: str) -> Dict[str, Any]: + """获取限售解禁数据""" + result = { + 'has_data': False, + 'query': f"{symbol}限售解禁", + 'data': None, + 'summary': None + } + + try: + # 构建问句 + query = f"{symbol}限售解禁" + + # 使用pywencai查询 + response = pywencai.get(query=query, loop=True) + + if response is None: + return result + + # 处理返回结果 + df_result = self._convert_to_dataframe(response) + + if df_result is None or df_result.empty: + return result + + # 提取有用的信息 + result['has_data'] = True + result['data'] = df_result + + # 生成摘要 + summary = [] + + # 尝试提取关键字段 + if '解禁时间' in df_result.columns or '限售解禁日' in df_result.columns: + time_col = '解禁时间' if '解禁时间' in df_result.columns else '限售解禁日' + summary.append(f"发现 {len(df_result)} 条解禁记录") + + # 提取最近的解禁记录 + recent_records = df_result.head(5) + for idx, row in recent_records.iterrows(): + record_info = [] + if time_col in row.index: + record_info.append(f"日期: {row[time_col]}") + if '解禁股数' in row.index: + record_info.append(f"解禁股数: {row['解禁股数']}") + if '解禁市值' in row.index: + record_info.append(f"解禁市值: {row['解禁市值']}") + if '股东名称' in row.index: + record_info.append(f"股东: {row['股东名称']}") + + if record_info: + summary.append(" | ".join(record_info)) + else: + # 如果没有标准字段,只记录有数据 + summary.append(f"获取到 {len(df_result)} 条相关记录") + + result['summary'] = "\n".join(summary) if summary else "有限售解禁数据" + + except Exception as e: + result['error'] = str(e) + + return result + + def _get_shareholder_reduction_data(self, symbol: str) -> Dict[str, Any]: + """获取大股东减持公告数据""" + result = { + 'has_data': False, + 'query': f"{symbol}大股东减持公告", + 'data': None, + 'summary': None + } + + try: + # 构建问句 + query = f"{symbol}大股东减持公告" + + # 使用pywencai查询 + response = pywencai.get(query=query, loop=True) + + if response is None: + return result + + # 处理返回结果 + df_result = self._convert_to_dataframe(response) + + if df_result is None or df_result.empty: + return result + + # 提取有用的信息 + result['has_data'] = True + result['data'] = df_result + + # 生成摘要 + summary = [] + + # 尝试提取关键字段 + if '公告日期' in df_result.columns or '减持日期' in df_result.columns: + date_col = '公告日期' if '公告日期' in df_result.columns else '减持日期' + summary.append(f"发现 {len(df_result)} 条减持公告") + + # 提取最近的减持记录 + recent_records = df_result.head(5) + for idx, row in recent_records.iterrows(): + record_info = [] + if date_col in row.index: + record_info.append(f"日期: {row[date_col]}") + if '股东名称' in row.index: + record_info.append(f"股东: {row['股东名称']}") + if '减持股数' in row.index: + record_info.append(f"减持股数: {row['减持股数']}") + if '减持比例' in row.index: + record_info.append(f"减持比例: {row['减持比例']}") + + if record_info: + summary.append(" | ".join(record_info)) + else: + # 如果没有标准字段,只记录有数据 + summary.append(f"获取到 {len(df_result)} 条相关记录") + + result['summary'] = "\n".join(summary) if summary else "有大股东减持数据" + + except Exception as e: + result['error'] = str(e) + + return result + + def _get_important_events_data(self, symbol: str) -> Dict[str, Any]: + """获取近期重要事件数据""" + result = { + 'has_data': False, + 'query': f"{symbol}近期重要事件", + 'data': None, + 'summary': None + } + + try: + # 构建问句 + query = f"{symbol}近期重要事件" + + # 使用pywencai查询 + response = pywencai.get(query=query, loop=True) + + if response is None: + return result + + # 处理返回结果 + df_result = self._convert_to_dataframe(response) + + if df_result is None or df_result.empty: + return result + + # 提取有用的信息 + result['has_data'] = True + result['data'] = df_result + + # 生成摘要 + summary = [] + + # 尝试提取关键字段 + if '事件时间' in df_result.columns or '公告日期' in df_result.columns: + time_col = '事件时间' if '事件时间' in df_result.columns else '公告日期' + summary.append(f"发现 {len(df_result)} 条重要事件") + + # 提取最近的事件 + recent_events = df_result.head(10) + for idx, row in recent_events.iterrows(): + event_info = [] + if time_col in row.index: + event_info.append(f"时间: {row[time_col]}") + if '事件类型' in row.index: + event_info.append(f"类型: {row['事件类型']}") + if '事件内容' in row.index: + content = str(row['事件内容'])[:100] # 限制长度 + event_info.append(f"内容: {content}") + elif '标题' in row.index: + title = str(row['标题'])[:100] + event_info.append(f"标题: {title}") + + if event_info: + summary.append(" | ".join(event_info)) + else: + # 如果没有标准字段,只记录有数据 + summary.append(f"获取到 {len(df_result)} 条相关记录") + + result['summary'] = "\n".join(summary) if summary else "有重要事件数据" + + except Exception as e: + result['error'] = str(e) + + return result + + def _convert_to_dataframe(self, result) -> pd.DataFrame: + """将pywencai返回结果转换为DataFrame""" + try: + if result is None: + return None + + df_result = None + + if isinstance(result, dict): + try: + df_result = pd.DataFrame([result]) + except Exception: + return None + elif isinstance(result, pd.DataFrame): + df_result = result + else: + return None + + if df_result is None or df_result.empty: + return None + + # 处理嵌套结构(tableV1) + if 'tableV1' in df_result.columns and len(df_result.columns) == 1: + table_v1_data = df_result.iloc[0]['tableV1'] + if isinstance(table_v1_data, pd.DataFrame): + df_result = table_v1_data + elif isinstance(table_v1_data, list) and len(table_v1_data) > 0: + df_result = pd.DataFrame(table_v1_data) + else: + return None + + # 处理嵌套结构(title_content等单列嵌套) + # 如果只有一列,且该列的值是DataFrame,则展开 + if len(df_result.columns) == 1: + col_name = df_result.columns[0] + first_value = df_result.iloc[0][col_name] + if isinstance(first_value, pd.DataFrame): + print(f" 检测到嵌套DataFrame(列名: {col_name}),正在展开...") + df_result = first_value + + return df_result if not df_result.empty else None + + except Exception as e: + print(f" 转换DataFrame时出错: {str(e)}") + return None + + def format_risk_data_for_ai(self, risk_data: Dict[str, Any]) -> str: + """格式化风险数据供AI分析使用 - 直接转换DataFrame为字符串""" + if not risk_data or not risk_data.get('data_success'): + return "未获取到风险数据" + + formatted_text = [] + + try: + # 1. 限售解禁数据 + lifting_ban = risk_data.get('lifting_ban') + if lifting_ban and lifting_ban.get('has_data') and lifting_ban.get('data') is not None: + formatted_text.append("=" * 80) + formatted_text.append("【限售解禁数据】") + formatted_text.append("=" * 80) + formatted_text.append(f"查询语句: {lifting_ban.get('query', '')}") + formatted_text.append("") + + # 直接将DataFrame转换为字符串(最多50行) + df = lifting_ban.get('data') + try: + df_str = df.head(50).to_string(index=False, max_rows=50, max_cols=20) + formatted_text.append(f"共 {len(df)} 条记录,显示前50条:") + formatted_text.append(df_str) + except Exception as e: + formatted_text.append(f"数据转换失败: {str(e)}") + formatted_text.append("") + + # 2. 大股东减持数据 + reduction = risk_data.get('shareholder_reduction') + if reduction and reduction.get('has_data') and reduction.get('data') is not None: + formatted_text.append("=" * 80) + formatted_text.append("【大股东减持数据】") + formatted_text.append("=" * 80) + formatted_text.append(f"查询语句: {reduction.get('query', '')}") + formatted_text.append("") + + # 直接将DataFrame转换为字符串(最多50行) + df = reduction.get('data') + try: + df_str = df.head(50).to_string(index=False, max_rows=50, max_cols=20) + formatted_text.append(f"共 {len(df)} 条记录,显示前50条:") + formatted_text.append(df_str) + except Exception as e: + formatted_text.append(f"数据转换失败: {str(e)}") + formatted_text.append("") + + # 3. 重要事件数据 + events = risk_data.get('important_events') + if events and events.get('has_data') and events.get('data') is not None: + formatted_text.append("=" * 80) + formatted_text.append("【重要事件数据】") + formatted_text.append("=" * 80) + formatted_text.append(f"查询语句: {events.get('query', '')}") + formatted_text.append("") + + # 直接将DataFrame转换为字符串(最多50行) + df = events.get('data') + try: + df_str = df.head(50).to_string(index=False, max_rows=50, max_cols=20) + formatted_text.append(f"共 {len(df)} 条记录,显示前50条:") + formatted_text.append(df_str) + except Exception as e: + formatted_text.append(f"数据转换失败: {str(e)}") + formatted_text.append("") + + return "\n".join(formatted_text) if formatted_text else "暂无风险数据" + + except Exception as e: + print(f"格式化风险数据时出错: {str(e)}") + import traceback + traceback.print_exc() + return f"格式化风险数据时出错: {str(e)}" + + def _format_dataframe_for_ai(self, df: pd.DataFrame, data_type: str) -> str: + """将DataFrame格式化为AI易读的文本格式""" + lines = [] + + # 显示数据总数 + lines.append(f"共 {len(df)} 条{data_type}记录") + lines.append("") + + # 显示列名 + lines.append(f"数据字段:{', '.join(df.columns.tolist())}") + lines.append("") + + # 逐行显示数据(最多显示50条,避免数据过大) + max_rows = min(50, len(df)) + + for idx, row in df.head(max_rows).iterrows(): + lines.append(f"【记录 {idx + 1}】") + + # 显示每个字段的值 + for col in df.columns: + value = row[col] + + # 处理不同类型的值 + if pd.isna(value): + value_str = "无数据" + elif isinstance(value, (int, float)): + value_str = str(value) + else: + value_str = str(value) + # 限制过长的字符串 + if len(value_str) > 200: + value_str = value_str[:200] + "..." + + lines.append(f" {col}: {value_str}") + + lines.append("") + + if len(df) > max_rows: + lines.append(f"... 还有 {len(df) - max_rows} 条记录(已省略)") + lines.append("") + + return "\n".join(lines) + + +# 测试代码 +if __name__ == "__main__": + fetcher = RiskDataFetcher() + + # 测试获取风险数据 + test_symbol = "600000" + print(f"测试获取 {test_symbol} 的风险数据...") + + risk_data = fetcher.get_risk_data(test_symbol) + + print("\n" + "=" * 60) + print("获取结果:") + print("=" * 60) + print(f"数据获取成功: {risk_data['data_success']}") + + if risk_data['data_success']: + print("\n格式化的风险数据:") + print(fetcher.format_risk_data_for_ai(risk_data)) + diff --git a/stock_data.py b/stock_data.py index 05bb496..5c628ec 100644 --- a/stock_data.py +++ b/stock_data.py @@ -816,6 +816,34 @@ class StockDataFetcher: # - 新方案:使用 akshare 的 stock_individual_fund_flow 接口 # - 新方案优势:数据标准化、准确获取最近20个交易日、6类资金详细分类 + def get_risk_data(self, symbol): + """ + 获取股票风险数据(限售解禁、大股东减持、重要事件) + 只支持中国A股 + """ + try: + # 只有中国A股才支持风险数据查询 + if not self._is_chinese_stock(symbol): + return { + 'symbol': symbol, + 'data_success': False, + 'error': '仅支持中国A股风险数据查询' + } + + # 使用风险数据获取器 + from risk_data_fetcher import RiskDataFetcher + fetcher = RiskDataFetcher() + risk_data = fetcher.get_risk_data(symbol) + + return risk_data + + except Exception as e: + return { + 'symbol': symbol, + 'data_success': False, + 'error': f'获取风险数据失败: {str(e)}' + } + def _safe_convert(self, value): """安全地转换数值""" if value is None or value == '' or (isinstance(value, float) and np.isnan(value)): diff --git a/test_risk_data.py b/test_risk_data.py new file mode 100644 index 0000000..7c58362 --- /dev/null +++ b/test_risk_data.py @@ -0,0 +1,173 @@ +""" +风险数据获取功能测试脚本 +测试pywencai获取限售解禁、大股东减持、重要事件数据 +""" + +from risk_data_fetcher import RiskDataFetcher + + +def test_risk_data_fetcher(): + """测试风险数据获取器""" + + print("=" * 80) + print("风险数据获取功能测试") + print("=" * 80) + + # 初始化获取器 + fetcher = RiskDataFetcher() + + # 测试股票列表(建议使用大盘股,数据更全) + test_symbols = [ + "600000", # 浦发银行 + "000001", # 平安银行 + "600519", # 茅台 + ] + + for symbol in test_symbols: + print(f"\n{'=' * 80}") + print(f"测试股票: {symbol}") + print(f"{'=' * 80}") + + # 获取风险数据 + risk_data = fetcher.get_risk_data(symbol) + + # 显示结果 + print(f"\n数据获取成功: {risk_data.get('data_success', False)}") + + if risk_data.get('error'): + print(f"错误信息: {risk_data['error']}") + continue + + # 显示限售解禁数据 + lifting_ban = risk_data.get('lifting_ban') + if lifting_ban and lifting_ban.get('has_data'): + print(f"\n✅ 限售解禁数据:") + print(f" 查询语句: {lifting_ban.get('query')}") + df_data = lifting_ban.get('data') + if df_data is not None and not df_data.empty: + print(f" 记录数: {len(df_data)}") + print(f" 字段: {', '.join(df_data.columns.tolist())}") + print(f" 前3条记录:") + print(df_data.head(3).to_string(index=False)) + else: + print(f"\nℹ️ 暂无限售解禁数据") + + # 显示股东减持数据 + reduction = risk_data.get('shareholder_reduction') + if reduction and reduction.get('has_data'): + print(f"\n✅ 大股东减持数据:") + print(f" 查询语句: {reduction.get('query')}") + df_data = reduction.get('data') + if df_data is not None and not df_data.empty: + print(f" 记录数: {len(df_data)}") + print(f" 字段: {', '.join(df_data.columns.tolist())}") + print(f" 前3条记录:") + print(df_data.head(3).to_string(index=False)) + else: + print(f"\nℹ️ 暂无大股东减持数据") + + # 显示重要事件数据 + events = risk_data.get('important_events') + if events and events.get('has_data'): + print(f"\n✅ 重要事件数据:") + print(f" 查询语句: {events.get('query')}") + df_data = events.get('data') + if df_data is not None and not df_data.empty: + print(f" 记录数: {len(df_data)}") + print(f" 字段: {', '.join(df_data.columns.tolist())}") + print(f" 前3条记录:") + print(df_data.head(3).to_string(index=False)) + else: + print(f"\nℹ️ 暂无重要事件数据") + + # 显示格式化后的数据 + print(f"\n{'=' * 80}") + print("格式化供AI分析的数据:") + print(f"{'=' * 80}") + formatted_data = fetcher.format_risk_data_for_ai(risk_data) + print(formatted_data) + + print(f"\n{'=' * 80}") + print("测试完成!") + print(f"{'=' * 80}") + + +def test_risk_management_agent(): + """测试风险管理师功能""" + + print("\n" + "=" * 80) + print("风险管理师AI分析测试") + print("=" * 80) + + # 需要配置API Key才能测试 + try: + from ai_agents import StockAnalysisAgents + from risk_data_fetcher import RiskDataFetcher + + # 测试股票 + test_symbol = "600000" + + print(f"\n测试股票: {test_symbol}") + + # 获取风险数据 + fetcher = RiskDataFetcher() + risk_data = fetcher.get_risk_data(test_symbol) + + if not risk_data.get('data_success'): + print("未获取到风险数据,跳过AI分析测试") + return + + # 模拟股票信息和指标 + stock_info = { + 'symbol': test_symbol, + 'name': '测试股票', + 'current_price': 10.5, + 'beta': 1.2, + '52_week_high': 12.0, + '52_week_low': 8.5 + } + + indicators = { + 'rsi': 65.5, + 'macd': 0.15 + } + + # 初始化AI分析系统 + print("\n初始化AI分析系统...") + agents = StockAnalysisAgents() + + # 运行风险管理师分析 + print("\n运行风险管理师分析...") + result = agents.risk_management_agent(stock_info, indicators, risk_data) + + # 显示分析结果 + print(f"\n{'=' * 80}") + print(f"分析师: {result['agent_name']}") + print(f"职责: {result['agent_role']}") + print(f"关注领域: {', '.join(result['focus_areas'])}") + print(f"{'=' * 80}") + print("\n分析报告:") + print(result['analysis']) + + except ImportError as e: + print(f"⚠️ 导入模块失败: {e}") + print("提示:请确保已安装所有依赖并配置API Key") + except Exception as e: + print(f"❌ 测试失败: {e}") + + +if __name__ == "__main__": + # 测试1:风险数据获取 + test_risk_data_fetcher() + + # 测试2:AI分析(需要API Key) + print("\n\n是否测试AI分析功能?(需要配置DeepSeek API Key)") + user_input = input("输入 y 继续,其他键跳过: ") + + if user_input.lower() == 'y': + test_risk_management_agent() + else: + print("\n跳过AI分析测试") + + print("\n✅ 所有测试完成!") + diff --git a/test_risk_data_debug.py b/test_risk_data_debug.py new file mode 100644 index 0000000..4a7b1b7 --- /dev/null +++ b/test_risk_data_debug.py @@ -0,0 +1,146 @@ +""" +调试风险数据获取和格式化 +检查数据是否真的传递给AI +""" + +from risk_data_fetcher import RiskDataFetcher +import json + + +def test_full_data_flow(symbol="300433"): + """测试完整数据流程""" + + print("=" * 80) + print(f"测试股票: {symbol}") + print("=" * 80) + + fetcher = RiskDataFetcher() + + # 1. 获取原始数据 + print("\n【步骤1】获取原始风险数据...") + risk_data = fetcher.get_risk_data(symbol) + + print(f"\n数据获取成功: {risk_data.get('data_success', False)}") + + # 2. 查看每类数据的详情 + print("\n【步骤2】查看原始数据详情...") + + # 限售解禁 + lifting_ban = risk_data.get('lifting_ban') + if lifting_ban: + print(f"\n限售解禁数据:") + print(f" has_data: {lifting_ban.get('has_data')}") + df = lifting_ban.get('data') + if df is not None: + print(f" 记录数: {len(df)}") + print(f" 列名: {list(df.columns)}") + print(f"\n 前3条数据:") + print(df.head(3)) + else: + print(f" data字段为None") + + # 大股东减持 + reduction = risk_data.get('shareholder_reduction') + if reduction: + print(f"\n大股东减持数据:") + print(f" has_data: {reduction.get('has_data')}") + df = reduction.get('data') + if df is not None: + print(f" 记录数: {len(df)}") + print(f" 列名: {list(df.columns)}") + print(f"\n 前3条数据:") + print(df.head(3)) + print(f"\n 完整第1条数据:") + if len(df) > 0: + first_row = df.iloc[0] + for col in df.columns: + print(f" {col}: {first_row[col]}") + else: + print(f" data字段为None") + + # 重要事件 + events = risk_data.get('important_events') + if events: + print(f"\n重要事件数据:") + print(f" has_data: {events.get('has_data')}") + df = events.get('data') + if df is not None: + print(f" 记录数: {len(df)}") + print(f" 列名: {list(df.columns)}") + print(f"\n 前3条数据:") + print(df.head(3)) + else: + print(f" data字段为None") + + # 3. 测试格式化后的数据 + print("\n" + "=" * 80) + print("【步骤3】测试格式化后传给AI的数据") + print("=" * 80) + + formatted_data = fetcher.format_risk_data_for_ai(risk_data) + + print("\n格式化后的数据长度:", len(formatted_data), "字符") + print("\n格式化后的数据内容:") + print(formatted_data) + + # 4. 保存到文件供查看 + print("\n" + "=" * 80) + print("【步骤4】保存数据到文件") + print("=" * 80) + + with open("risk_data_debug_output.txt", "w", encoding="utf-8") as f: + f.write("=" * 80 + "\n") + f.write("原始数据结构\n") + f.write("=" * 80 + "\n\n") + + # 保存原始数据(不包含DataFrame,只保存基本信息) + f.write("data_success: " + str(risk_data.get('data_success')) + "\n\n") + + for key in ['lifting_ban', 'shareholder_reduction', 'important_events']: + data = risk_data.get(key) + if data: + f.write(f"\n{key}:\n") + f.write(f" has_data: {data.get('has_data')}\n") + f.write(f" query: {data.get('query')}\n") + df = data.get('data') + if df is not None: + f.write(f" 记录数: {len(df)}\n") + f.write(f" 列名: {list(df.columns)}\n") + + f.write("\n" + "=" * 80 + "\n") + f.write("格式化后传给AI的数据\n") + f.write("=" * 80 + "\n\n") + f.write(formatted_data) + + print("✓ 数据已保存到 risk_data_debug_output.txt") + + # 5. 检查数据是否完整 + print("\n" + "=" * 80) + print("【步骤5】数据完整性检查") + print("=" * 80) + + if "【大股东减持数据】" in formatted_data: + print("✓ 格式化数据中包含大股东减持数据") + + # 检查是否有实际内容 + reduction_section = formatted_data.split("【大股东减持数据】")[1].split("=" * 80)[0] + if len(reduction_section.strip()) > 100: + print(f"✓ 减持数据部分有 {len(reduction_section)} 字符") + else: + print(f"⚠️ 减持数据部分只有 {len(reduction_section)} 字符,可能数据不完整") + else: + print("❌ 格式化数据中未找到大股东减持数据") + + print("\n" + "=" * 80) + print("测试完成!请查看 risk_data_debug_output.txt 文件") + print("=" * 80) + + +if __name__ == "__main__": + import sys + + # 默认测试300433,也可以通过命令行参数指定其他股票 + symbol = sys.argv[1] if len(sys.argv) > 1 else "300433" + + test_full_data_flow(symbol) + diff --git a/风险管理师增强功能说明.md b/风险管理师增强功能说明.md new file mode 100644 index 0000000..b1f654f --- /dev/null +++ b/风险管理师增强功能说明.md @@ -0,0 +1,371 @@ +# 风险管理师增强功能说明 ⚠️ + +## 📋 功能概述 + +风险管理师功能已全面增强,现在使用**问财(pywencai)**实时获取三大类风险数据,为投资决策提供更全面的风险评估。 + +## ✨ 新增功能 + +### 1. 三大风险数据源 🔍 + +风险管理师现在会自动查询以下三类关键风险信息: + +| 风险类型 | 查询问句 | 数据内容 | +|---------|---------|---------| +| **限售解禁** | `股票代码限售解禁` | 解禁时间、解禁股数、解禁市值、股东名称 | +| **大股东减持** | `股票代码大股东减持公告` | 减持日期、股东名称、减持股数、减持比例 | +| **重要事件** | `股票代码近期重要事件` | 事件时间、事件类型、事件内容 | + +### 2. 完整数据传递 📊 + +系统会将pywencai返回的**完整原始数据**喂给AI,包括: + +- **所有字段**:不遗漏任何信息 +- **所有记录**:最多传递50条记录(避免过载) +- **结构化格式**:AI易于解析的格式 +- **详细信息**:时间、金额、比例、股东名称等 + +**数据传递示例:** + +``` +================================================================================ +【限售解禁完整数据】 +================================================================================ +查询语句: 600000限售解禁 + +共 10 条限售解禁记录 + +数据字段:股票代码, 股票简称, 解禁时间, 解禁股数, 解禁市值, 股东名称, ... + +【记录 1】 + 股票代码: 600000 + 股票简称: 浦发银行 + 解禁时间: 2024-03-15 + 解禁股数: 500000000 + 解禁市值: 5000000000 + 股东名称: 某某投资公司 + ... + +【记录 2】 + ... +``` + +### 3. 智能风险分析 🧠 + +风险管理师会**深度解析所有原始数据**,进行**11个维度**的全面风险评估: + +#### ⭐ 三大重点风险分析 + +1. **限售解禁风险分析** + - 解禁时间和规模评估 + - 解禁对股价的潜在冲击 + - 解禁股东类型分析(创始人/投资机构/其他) + - 历史解禁后股价走势参考 + - 风险等级评定和应对建议 + +2. **股东减持风险分析** + - 减持频率和力度评估 + - 减持股东身份和意图分析 + - 减持对市场信心的影响 + - 是否存在连续减持或集中减持 + - 风险警示和投资建议 + +3. **重要事件风险分析** + - 识别可能影响股价的重大事件 + - 事件性质判断(利好/利空/中性) + - 事件影响的时间维度(短期/中期/长期) + - 事件的确定性和不确定性 + - 风险提示和关注要点 + +#### 📊 其他风险维度 + +4. **市场风险**(系统性风险) +5. **个股风险**(非系统性风险) +6. **流动性风险** +7. **波动性风险** +8. **估值风险** +9. **行业风险** +10. **综合风险评定** +11. **风险控制建议** ⭐ + +### 4. AI深度解析能力 🤖 + +AI会对每条原始记录进行深度分析: + +- **时间识别**:自动识别日期字段,关注最近和即将发生的事件 +- **金额量化**:评估解禁市值、减持金额的规模和影响 +- **股东分析**:识别股东类型(创始人/机构/高管) +- **频率统计**:分析减持频率,判断是否连续减持 +- **趋势预判**:基于历史数据预测未来风险 + +### 5. 可操作的风险控制建议 📋 + +风险管理师会提供具体的风险控制建议: + +- **仓位控制建议**:具体的仓位比例建议 +- **止损位设置**:具体的止损价位建议 +- **风险规避策略**:明确什么情况下不建议投资 +- **风险对冲方案**:如果适用,提供对冲策略 +- **持仓时间建议**:建议的持仓周期 +- **重点关注指标**:需要持续监控的关键指标 + +## 🎯 使用方法 + +### 自动获取(推荐)✅ + +风险管理师默认启用,系统会自动获取风险数据: + +1. 在股票分析界面,确保**风险管理师**处于勾选状态 +2. 输入股票代码(仅支持**A股6位代码**) +3. 点击"开始分析" +4. 系统自动获取风险数据并进行分析 + +### 数据获取流程 🔄 + +``` +1. 查询限售解禁数据 → 2秒延迟 +2. 查询大股东减持数据 → 2秒延迟 +3. 查询重要事件数据 → 完成 +4. 综合所有数据进行AI分析 +``` + +## 📊 数据展示 + +### 获取成功提示 + +``` +✅ 成功获取风险数据:限售解禁, 大股东减持, 重要事件 +``` + +### 部分数据提示 + +``` +ℹ️ 成功获取风险数据:大股东减持 +ℹ️ 暂无限售解禁和重要事件数据 +``` + +### 无数据提示 + +``` +ℹ️ 暂无风险相关数据,将基于基本信息进行风险分析 +``` + +### 美股提示 + +``` +ℹ️ 美股暂不支持风险数据(限售解禁、大股东减持等) +``` + +## 💡 使用场景 + +### 场景1:发现解禁风险 🚨 + +**案例**:某股票即将迎来大规模限售解禁 + +**风险管理师分析**: +- 识别解禁时间窗口 +- 评估解禁规模对股价的压力 +- 分析解禁股东类型(创始人vs机构) +- 建议:解禁前减仓,解禁后观察 + +### 场景2:监控股东减持 ⚠️ + +**案例**:大股东连续减持 + +**风险管理师分析**: +- 识别减持频率和强度 +- 分析减持股东身份(实际控制人/财务投资者) +- 评估对市场信心的影响 +- 建议:降低仓位,设置止损位 + +### 场景3:追踪重要事件 📰 + +**案例**:公司面临重大诉讼或政策变动 + +**风险管理师分析**: +- 识别事件性质和影响范围 +- 评估短中长期影响 +- 判断事件的不确定性 +- 建议:谨慎持仓,关注后续进展 + +## 🔧 技术实现 + +### 核心模块 + +``` +risk_data_fetcher.py # 风险数据获取模块 +├── RiskDataFetcher # 风险数据获取类 +│ ├── get_risk_data() # 主函数:获取所有风险数据 +│ ├── _get_lifting_ban_data() # 获取限售解禁 +│ ├── _get_shareholder_reduction_data() # 获取股东减持 +│ ├── _get_important_events_data() # 获取重要事件 +│ ├── format_risk_data_for_ai() # 格式化供AI分析(完整数据) +│ └── _format_dataframe_for_ai() # DataFrame转文本格式 +``` + +### 数据处理流程 + +``` +pywencai查询 → DataFrame原始数据 → 格式化为文本 → 喂给AI + ↓ ↓ ↓ ↓ + 问句查询 保留所有字段 逐行格式化 深度解析 + 实时数据 完整记录 AI可读 量化评估 +``` + +### 数据流程 + +``` +用户输入股票代码 + ↓ +stock_data.py → get_risk_data() + ↓ +risk_data_fetcher.py → 调用pywencai + ↓ +获取三类风险数据 + ↓ +ai_agents.py → risk_management_agent() + ↓ +DeepSeek AI 深度分析 + ↓ +生成风险评估报告 +``` + +## 📈 数据来源 + +- **数据源**:问财(pywencai) +- **更新频率**:实时查询 +- **查询语法**:自然语言问句 +- **数据类型**:结构化表格数据 + +## ⚙️ 配置说明 + +### 默认配置 + +```python +# 风险管理师默认启用 +enable_risk = True + +# 仅支持A股 +supported_markets = ["中国A股(6位代码)"] + +# 查询延迟(避免频繁请求) +query_delay = 1秒 +``` + +### 在批量分析中使用 + +```python +# 批量分析也支持风险管理功能 +enabled_analysts_config = { + 'technical': True, + 'fundamental': True, + 'fund_flow': True, + 'risk': True, # 启用风险管理师 + 'sentiment': False, + 'news': False +} +``` + +## 🎓 最佳实践 + +### 1. 结合其他分析师 👥 + +风险管理师的分析应该与其他分析师综合判断: + +- **技术分析师**:识别技术面风险信号 +- **基本面分析师**:评估基本面恶化风险 +- **资金面分析师**:监控主力资金流出 +- **风险管理师**:识别特定风险事件 + +### 2. 重点关注时间窗口 ⏰ + +特别关注以下时间窗口的风险: + +- 解禁前1个月:提前规避风险 +- 减持公告后:评估市场反应 +- 重大事件发生时:及时调整策略 + +### 3. 设置风险预警 🔔 + +可以结合监测功能,设置风险预警: + +- 解禁预警:提前30天预警 +- 减持预警:出现减持公告时预警 +- 事件预警:重大负面事件预警 + +## 📝 注意事项 + +### 支持范围 + +✅ **支持**:中国A股(6位数字代码) +❌ **不支持**:美股、港股 + +### 数据时效性 + +- 数据来自问财实时查询 +- 可能存在1-2个交易日的延迟 +- 建议定期刷新分析 + +### 网络依赖 + +- 需要稳定的网络连接 +- 查询可能需要5-10秒 +- 失败时会自动降级到基础分析 + +### 数据准确性 + +- 数据来源于公开信息 +- 建议交叉验证重要信息 +- 以官方公告为准 + +## 🚀 未来规划 + +### 计划新增功能 + +- [ ] 风险评分量化模型 +- [ ] 风险历史对比分析 +- [ ] 风险预警阈值设置 +- [ ] 风险事件影响回测 +- [ ] 多股票风险对比分析 + +### 优化方向 + +- 增加更多风险数据源 +- 优化数据查询速度 +- 提升AI分析准确度 +- 增强可视化展示 + +## 📞 问题反馈 + +如果在使用过程中遇到问题: + +1. **数据获取失败**:检查网络连接,稍后重试 +2. **分析结果异常**:确认股票代码正确(6位A股代码) +3. **其他问题**:查看控制台错误信息 + +## 📚 相关文档 + +- `主力选股使用指南.md` - 了解主力资金分析 +- `分析师团队选择功能说明.md` - 了解如何选择分析师 +- `智瞰龙虎功能说明.md` - 了解龙虎榜风险分析 + +## 🎯 总结 + +风险管理师增强功能通过**问财实时数据**和**完整数据传递**,为投资者提供: + +✅ **三大风险数据**:限售解禁、大股东减持、重要事件 +✅ **完整原始数据**:所有字段、所有记录,不遗漏任何信息 +✅ **AI深度解析**:自动识别关键风险点,量化评估 +✅ **11维度分析**:全面评估各类风险 +✅ **可操作建议**:具体的风险控制策略 +✅ **实时更新**:基于最新公开信息 +✅ **智能分析**:DeepSeek AI从海量数据中提取关键信息 + +**核心优势:** +- 不再是摘要或概括,而是**完整原始数据** +- AI自主解析每条记录的**每个字段** +- 基于实际数据给出**量化风险评估** +- 让AI的分析更准确、更深入、更可靠 + +让投资决策更安全、更可靠!⚠️📊💡 +