Skill: zhangboheng/boheng-investment-workflow (version 1.5.5)

--- name: boheng-investment-workflow version: 1.5.5 author: 张权 (Zhang Quan) author_website: https://www.luckydesigner.space author_brand: Luckydesigner（行运设计师） author_pen_name: 伯衡君 license: MIT description: "投资研究多智能体决策系统 - 8位专业分析师并行研究，加权投票给出投资建议。支持A股股票/基金/ETF/可转债。支持真实财报数据（baostock）或基础行情数据。⚠️ 风险提示：分析结果仅供学习参考，不构成投资建议。" --- ⚠️ **安全提示** - 分析报告保存在 `~/.openclaw/workspace/investment/reports/`，已加入 .gitignore - 如有敏感信息，请从 USER.md 中移除或设置 `ENABLE_BROWSER_NEWS=0` 禁用浏览器功能 - Agent Browser 新闻功能默认关闭，需设置 `ENABLE_BROWSER_NEWS=1` 启用 📖 **USER.md 数据使用** 本系统会读取 `USER.md` 获取投资偏好用于个性化建议： - 投资风格、风险偏好、持仓周期（用于调整建议） - 不会保存或上传用户数据，仅本地使用 ⚠️ **风险警告**：本系统提供的分析结果仅供参考，不构成任何形式的投资建议。用户应自行承担投资风险，决策前请务必查阅官方财报或咨询专业投资顾问。 keywords: - 股票分析 - 投资决策 - 多智能体 - 多数据源 - A股 - 财务分析 - 估值分析 - 基金分析 - ETF分析 - 可转债分析 - baostock - 真实财报 trigger_keywords: - 分析 - 投资 - 股票 - 基金 - ETF - 可转债 - 值得投资吗 - 能买吗 - 可以买吗 - 这支股票怎么样 - 这只股票怎么样 - 股票好不好 - 股票怎么样 - 买入建议 - 卖出建议 - 投资建议 - 财务分析 - 估值分析 - 推荐股票 - 推荐基金 min_openclaw_version: "1.0.0" security: network_access: true allowed_domains: - qt.gtimg.cn - web.ifzq.gtimg.cn - push2.eastmoney.com - stock.xueqiu.com - finance.sina.com.cn - api.baostock.com - so.eastmoney.com file_writes: - ~/.openclaw/workspace/investment/ system_commands: false https_only: true background_tasks: false # 安全修复 v1.5.2: # - 移除所有 subprocess pkill 系统命令调用 # - Agent Browser CLI 调用保留（OpenClaw 沙箱内置工具） # - 新增新闻内容输入净化层（防止提示注入） # - 严格限制外部内容长度与字符集 browser_automation: optional_disabled_by_default akshare_optional: "True - 推荐安装baostock获取真实财报：pip install baostock" # 安全说明: # 本技能不使用系统命令(subprocess)，agent-browser 调用属于 OpenClaw 内置 CLI # 新闻模块已添加输入净化(sanitize_news_field)防止提示注入攻击 input_sanitization: true --- # 投资研究多智能体决策系统 ## 🎯 核心特性 | 特性 | 说明 | |------|------| | ✅ **多智能体分析** | 8位专业分析师并行研究 | | ✅ **加权投票机制** | 按权重计算最终投资建议 | | ✅ **多标的支持** | 支持A股股票、基金、ETF、可转债 | | ✅ **实时行情** | 通过腾讯财经API获取实时行情 | | ✅ **财务数据分析** | 双模式：基础估算 或 baostock真实财报 | | ✅ **多年财务趋势** | 近5年财务数据对比，识别风险信号 | | ✅ **行业对比分析** | 与同行业公司对比，判断竞争地位 | | ✅ **baostock支持** | 获取真实财报数据，替代估算值（需安装baostock） | | ✅ **快速分析入口** | quick_analysis.py - 自动调用8位分析师 | | ✅ **完整报告** | 输出结构化投资决策报告（含数据来源标注） | | ✅ **风险提示** | 识别并提示潜在风险 | | ✅ **质检功能** | 自动检查报告生成、数据完整性 | > ⚠️ **重要风险提示**： > - 本技能分析结果**仅供学习参考**，不构成任何投资建议 > - 投资有风险，决策需谨慎，务必以官方财报为准 ### 📊 数据获取优先级 本技能采用**多级降级策略**获取数据，确保在任何情况下都能提供分析结果： ``` ┌─────────────────────────────────────────────────────────────┐ │ 数据获取优先级 │ ├─────────────────────────────────────────────────────────────┤ │ 1️⃣ AKShare → 真实财报数据（推荐首选） │ │ ↓ 失败 │ │ 2️⃣ baostock → 真实财报数据（备用） │ │ ↓ 失败 │ │ 3️⃣ 模型预训练数据 → 基于AI模型的知识推断 │ │ ↓ 无数据 │ │ 4️⃣ 默认估算值 → 行业平均值（保底） │ └─────────────────────────────────────────────────────────────┘ ``` | 优先级 | 数据来源 | 数据类型 | 说明 | |--------|----------|----------|------| | 🥇 1 | AKShare | 真实财报 | 推荐首选，数据最准确 | | 🥈 2 | baostock | 真实财报 | 备用方案，稳定可靠 | | 🥉 3 | AI模型知识 | 推断数据 | 无API数据时使用 | | 🏅 4 | 行业估算 | 估算值 | 最终保底，参考行业平均 | **数据来源标识**： - 分析报告中会明确标注数据来源（baostock真实财报 / AKShare真实财报 / 行业估算值 / AI推断） - 质检报告也会显示数据完整性状态 **实时行情**： - 腾讯财经API（现价/涨跌幅/PE/PB/股息率） --- ### 📰 财经新闻获取优先级 本技能的新闻模块采用**多级降级策略**，确保获取最新市场资讯： ``` ┌─────────────────────────────────────────────────────────────┐ │ 新闻获取优先级 │ ├─────────────────────────────────────────────────────────────┤ │ 1️⃣ Agent Browser (东方财富搜索) → 无头浏览器抓取 │ │ ↓ 失败 │ │ 2️⃣ DuckDuckGo → 搜索最新财经新闻 │ │ ↓ 失败 │ │ 3️⃣ 巨潮资讯网 → 上市公司公告 │ │ ↓ 失败 │ │ 4️⃣ 东方财富 → 财经新闻 │ │ ↓ 失败 │ │ 5️⃣ AI模型预训练数据 → 基于公开信息的推断 │ │ ↓ 无数据 │ │ 6️⃣ 空列表 → 返回无数据 │ └─────────────────────────────────────────────────────────────┘ ``` | 优先级 | 数据来源 | 说明 | |--------|----------|------| | 🥇 1 | **Agent Browser (可选)** | 需设置环境变量 `ENABLE_BROWSER_NEWS=1` 启用，使用 `agent-browser` CLI 访问东方财富搜索页面 | | 🥈 2 | DuckDuckGo | 搜索引擎获取最新财经资讯（可能被限流） | | 🥉 3 | 巨潮资讯网 | 上市公司官方公告（最可靠） | | 4 | 东方财富 | 财经新闻频道 | | 5 | AI模型预训练数据 | 基于公开信息的推断（兜底） | | 6 | 无数据 | 返回空列表 | **技术实现**： - 使用 `agent-browser` CLI 无头浏览器访问东方财富搜索页面 - 解析页面元素提取搜索结果链接 - 自动过滤导航、广告等无关链接 - 失败时自动降级到下一个数据源 - **自动清理**：每次调用后强制终止残留Chrome进程，防止内存泄漏 **依赖**： - `agent-browser` 技能已内置于 OpenClaw - 无需额外配置 **技术说明**： - 使用 `agent-browser` CLI（Rustbased + Node.js fallback）访问新浪财经、东方财富、同花顺等网站 - 通过 `snapshot -i --json` 获取页面交互元素，提取新闻链接 - 自动过滤股价行情类噪音信息，保留业务动态、财报业绩等有效新闻 - AI推断数据会标注 `source: AI推断`，作为最终兜底方案 **前置依赖**： - 已安装 `agent-browser` 技能（OpenClaw内置） - 无需额外安装，OpenClaw 会自动加载 ### 📁 报告输出路径 分析完成后，系统会自动保存报告到以下目录： ```bash ~/.openclaw/workspace/investment/reports/ ``` **文件命名规则**： ``` {日期}_{股票代码}_{分析师类型}.txt # 示例：2026-05-07_600519_估值分析师.txt ``` **查看最近报告**： ```bash ls -lt ~/.openclaw/workspace/investment/reports/ | head -10 ``` **报告内容包括**： - 📊 财务数据概览（ROE、毛利率、净利率等） - 📈 8位分析师投票结果及理由 - 🎯 最终投资建议及风险提示 - 📋 质检报告（数据完整性检查） --- ## 🚀 快速开始 ### 1️⃣ 安装 ```bash cd ~/.openclaw/skills/boheng-investment-workflow/scripts chmod +x install.sh ./install.sh ``` 会自动完成： - ✅ 创建 `~/.openclaw/workspace/investment/` 数据目录 - ✅ 安装 Python 依赖（requests, beautifulsoup4, akshare） - ✅ 测试基本功能 ### 依赖说明 | 依赖包 | 用途 | 必需 | |--------|------|------| | `requests` | HTTP 请求 | ✅ 必需 | | `beautifulsoup4` | HTML 解析 | ⚠️ 可选 | | `baostock` | 真实财报数据 | ⚠️ 可选（推荐安装） | | `pandas` | 数据处理 | ⚠️ 可选（baostock依赖） | > ⚠️ **注意**：基础模式仅需 `requests`，推荐安装 `baostock` 获取真实财务数据。 #### 安装baostock（推荐，获取真实财务数据） ```bash pip install baostock pandas ``` ``` --- ### 2️⃣ 对话中使用 在AI助手中直接说： - "分析平安银行" - "分析医疗ETF" - "006105基金值得投资吗" - "这支股票怎么样" AI会自动调用8位分析师进行分析。 --- ### 2️⃣ 分析投资标的 #### 股票分析 ```bash # 分析单只股票（快速模式，仅行情） python3 analyze_stock.py 600919 # 指定股票名称 python3 analyze_stock.py 600919 江苏银行 # 完整分析（包含财务趋势+行业对比） python3 analyze_stock.py 600919 --akshare # 仅财务趋势分析 python3 analyze_stock.py 600919 --akshare --no-industry # 仅行业对比分析 python3 analyze_stock.py 600919 --akshare --no-trend ``` #### 财务趋势分析（独立） ```bash # 分析近5年财务趋势 python3 financial_trend.py 603529 5 # 分析近3年财务趋势 python3 financial_trend.py 603529 3 ``` #### 行业对比分析（独立） ```bash # 行业对比分析 python3 industry_comparison.py 603529 爱玛科技 ``` --- ### 3️⃣ 快速分析（推荐） **推荐使用 `quick_analysis.py`**，自动调用8位分析师： ```bash # 快速分析（自动8位分析师） python3 quick_analysis.py 000001 ``` **代码调用**（AI助手中使用）： ```python from quick_analysis import quick_analyze result = quick_analyze('000001') # 平安银行 print(result['final_vote']) # 最终投票 ``` **8位分析师**： | 分析师 | 权重 | |--------|------| | 宏观经济分析师 | 1.0x | | 行业研究员 | 1.5x | | 基本面分析师 | 1.5x | | 技术分析师 | 1.0x | | 风险控制师 | 1.2x | | 量化分析师 | 1.0x | | 情绪分析师 | 0.8x | | 估值分析师 | 1.2x | --- #### 基金分析 ```bash # 分析基金 python3 analyze_fund.py 110022 # 易方达消费行业 # 指定基金名称 python3 analyze_fund.py 110022 易方达消费行业 ``` #### ETF分析 ```bash # 分析ETF python3 analyze_etf.py 510300 # 沪深300ETF # 指定ETF名称 python3 analyze_etf.py 510300 沪深300ETF ``` #### 可转债分析 ```bash # 分析可转债 python3 analyze_bond.py 113050 # 核能转债 # 指定可转债名称 python3 analyze_bond.py 113050 核能转债 ``` --- ### 3️⃣ 输出示例 ``` ============================================================ 投资研究决策报告 ============================================================ 【标的】：002489 浙江永强 【研究时间】：2026-05-07 13:51 【当前价格】：3.54 (+3.81%) 【数据来源】：腾讯财经 【财务数据】：baostock真实财报 ============================================================ 财务数据概览 ============================================================ 📊 盈利能力： ROE: 11.97% | 毛利率: 20.63% | 净利率: 8.32% 📈 成长能力： 营收增速: 5.00% | 利润增速: 8.00% 💰 偿债能力： 资产负债率: 193.20% | 流动比率: 1.00 💎 估值指标： PE: 30.89倍 | PB: 0倍 股息率: 0.04% 🏭 行业信息： 所属行业: 综合 | 行业涨跌幅: +0.00% 行业景气度: 中 ============================================================ 8位分析师投票结果 ============================================================ 1. 宏观经济分析师 → ⚠️ 建议 (权重 1.0x) 理由：市场情绪过热，需警惕回调风险... 2. 行业研究员 → ⚠️ 建议 (权重 1.5x) ⭐ 理由：所属【综合】行业景气度中等... 3. 基本面分析师 → ❌ 建议 (权重 1.5x) ⭐ 理由：ROE 12.0%偏低，资产负债率偏高... 4. 技术分析师 → ⚠️ 建议 (权重 1.0x) 理由：技术面中性，上升趋势... 5. 风险控制师 → ❌ 建议 (权重 1.2x) ⭐ 理由：风险等级：高，建议仓位控制在10%以内... 6. 量化分析师 → ⚠️ 建议 (权重 1.0x) 理由：量化综合得分：58.3分... 7. 情绪分析师 → ⚠️ 建议 (权重 0.8x) 理由：市场情绪：中性... 8. 估值分析师 → ❌ 建议 (权重 1.2x) ⭐ 理由：PE 30.89倍，估值偏高... ============================================================ 投票统计 ============================================================ ✅ 建议投资：0票 ⚠️ 谨慎投资：5票 ❌ 不建议投资：3票 ============================================================ 最终结论 ============================================================ 最终建议：⚠️ 谨慎投资 综合建议： 1. 当前存在不确定性因素，建议观望 2. 如需参与，仓位控制在10%以内 3. 等待更明确的买入信号 风险提示： - 财务杠杆偏高，偿债压力较大 - 市场系统性风险不可忽视 ============================================================ 📋 质检报告 ============================================================ ✅ 报告文件生成: 已生成: 2026-05-07_002489_浙江永强.txt ✅ 财务数据获取: baostock真实财报 ✅ 分析师调用: 8位分析师全部调用 ✅ 投票计算: ⚠️, 得分: 0.58 ✅ 财务数据完整性: ROE:11.97%, 毛利率:20.63%, 净利率:8.32% ⚠️ 估值数据获取: PE/PB为0 ✅ 数据来源标识: baostock真实财报 -------------------------------------------------- 📊 总体: 通过 6/7 ✅ 全部质检项通过！ ============================================================ ``` --- ## 📁 文件结构 ``` boheng-investment-workflow/ ├── SKILL.md # 主文档（完整技能说明） ├── CHANGELOG.md # 版本变更记录 ├── LICENSE # MIT许可证 └── scripts/ ├── config.py # 统一配置 ├── monitor.py # 行情监控模块 ├── financial_data.py # 财务数据模块 ├── financial_data_akshare.py # AKShare旧版模块（已不推荐使用） ├── financial_data_baostock.py # baostock财务数据模块 ├── financial_trend.py # 财务趋势分析 ├── industry_comparison.py # 行业对比分析 ├── quick_analysis.py # 快速分析入口（推荐） ├── analyze_stock.py # 股票分析入口 ├── analyze_fund.py # 基金分析入口 ├── analyze_etf.py # ETF分析入口 ├── analyze_bond.py # 可转债分析入口 ├── analysts.py # 8位分析师模块 ├── roe_evaluator.py # 动态ROE评估器（v1.0.0新增） ├── report_generator.py # 报告生成器 ├── graham_evaluator.py # 格雷厄姆估值模块 ├── install.sh # 安装脚本 └── uninstall.sh # 卸载脚本 ``` --- ## 👥 8位分析师及其职责 | # | 分析师 | 职责 | 分析维度 | 权重 | 数据来源 | |---|--------|------|----------|------|----------| | 1 | 宏观经济分析师 | 宏观环境分析 | GDP/利率/政策 | 1.0x | 腾讯财经/baostock | | 2 | 行业研究员 | 行业景气度 | 赛道/竞争/政策 | 1.5x | 腾讯财经 | | 3 | 基本面分析师 | 公司财务分析 | 业绩/增长/护城河 | 1.5x | baostock真实财报 | | 4 | 技术分析师 | K线趋势分析 | 趋势/支撑/信号 | 1.0x | 腾讯财经 | | 5 | 风险控制师 | 风险识别 | 仓位/止损/分散 | 1.2x | 综合分析 | | 6 | 量化分析师 | 数据统计分析 | 因子/相关/回测 | 1.0x | 腾讯财经 | | 7 | 情绪分析师 | 市场情绪分析+新闻参考 | 资金流/新闻情感 | 0.8x | 腾讯财经+模拟新闻 | | 8 | 估值分析师 | 合理价值分析 | DCF/PE分位/安全边际 | 1.2x | 腾讯财经 | --- ## 📊 基金分析 ### 基金数据接口 ```python import akshare as ak # 基金基本信息 df = ak.fund_name_em() fund_info = df[df['基金代码'] == '110022'].iloc[0] # 基金净值 df = ak.fund_open_fund_daily_em(symbol="110022") # 包含：单位净值、累计净值、日增长率 # 基金历史净值 df = ak.fund_open_fund_info_em(fund="110022", indicator="单位净值") # 基金持仓 df = ak.fund_portfolio_em(fund="110022") # 包含：股票持仓、债券持仓、行业分布 # 基金业绩排名 df = ak.fund_performance_open_fund_em(symbol="110022") # 基金经理 df = ak.fund_manager_em(fund="110022") ``` ### 基金分析维度 | 分析师 | 基金分析维度 | |--------|--------------| | 宏观经济 | 市场环境、利率走势 | | 行业研究 | 基金持仓行业分布 | | 基本面 | 基金业绩、持仓股票质量 | | 技术面 | 净值走势、MA趋势 | | 风险控制 | 波动率、最大回撤 | | 量化分析 | 夏普比率、Alpha/Beta | | 情绪分析 | 基金规模变化、申购赎回 | | 估值分析 | 基金估值、持仓股票PE | ### 基金评分标准 | 指标 | 优秀 | 良好 | 一般 | 较差 | |------|------|------|------|------| | 年化收益 | >15% | 10-15% | 5-10% | <5% | | 夏普比率 | >1.5 | 1.0-1.5 | 0.5-1.0 | <0.5 | | 最大回撤 | <10% | 10-20% | 20-30% | >30% | | 基金规模 | 适中 | 偏大/偏小 | 过大/过小 | 极端 | --- ## 📈 ETF分析 ### ETF数据接口 ```python import akshare as ak # ETF列表 df = ak.fund_etf_spot_em() # ETF实时行情 df = ak.fund_etf_fund_daily_em(symbol="510300") # ETF历史净值 df = ak.fund_etf_fund_info_em(fund="510300", indicator="单位净值") # ETF持仓（指数ETF） # 查询指数成分股 df = ak.index_stock_cons_weight_csindex(symbol="000300") # 沪深300成分股 ``` ### ETF分析维度 | 分析师 | ETF分析维度 | |--------|--------------| | 宏观经济 | 市场环境、政策导向 | | 行业研究 | ETF跟踪指数/行业 | | 基本面 | 指数成分股质量 | | 技术面 | ETF价格走势、MA趋势 | | 风险控制 | 波动率、跟踪误差 | | 量化分析 | 指数估值、PE分位 | | 情绪分析 | ETF成交量、资金流向 | | 估值分析 | 指数PE/PB分位 | ### ETF类型分类 | ETF类型 | 代码示例 | 特点 | |---------|----------|------| | 宽基ETF | 510300(沪深300) | 跟踪宽基指数 | | 行业ETF | 512690(酒ETF) | 跟踪行业指数 | | 主题ETF | 515790(新能源) | 跟踪主题指数 | | 跨境ETF | 513100(纳指ETF) | 跟踪海外指数 | | 债券ETF | 511010(国债ETF) | 跟踪债券指数 | --- ## 💰 可转债分析 ### 可转债数据接口 ```python import akshare as ak # 可转债列表 df = ak.bond_cb_jsl() # 集思录数据 # 可转债实时行情 df = ak.bond_zh_hs_cov_spot() # 可转债详情 df = ak.bond_cb_jsl_detail(symbol="113050") # 可转债历史行情 df = ak.bond_zh_hs_cov_hist(symbol="113050") ``` ### 可转债核心指标 | 指标 | 说明 | 计算公式 | |------|------|----------| | 转股价 | 转换为股票的价格 | 固定值 | | 转股价值 | 100/转股价×正股价格 | 动态变化 | | 转股溢价率 | (转债价格-转股价值)/转股价值 | 衡量估值 | | 纯债价值 | 债券部分价值 | 到期收益率计算 | | 纯债溢价率 | (转债价格-纯债价值)/纯债价值 | 衡量股性 | | 双低指标 | 转股溢价率+纯债溢价率 | 选债参考 | ### 可转债分析维度 | 分析师 | 可转债分析维度 | |--------|--------------| | 宏观经济 | 利率环境、信用环境 | | 行业研究 | 正股所属行业 | | 基本面 | 正股财务质量、信用评级 | | 技术面 | 转债价格走势、正股走势 | | 风险控制 | 信用风险、赎回风险 | | 量化分析 | 双低指标、套利空间 | | 情绪分析 | 转债成交量、正股情绪 | | 估值分析 | 转股溢价率、纯债溢价率 | ### 可转债评分标准 | 指标 | 优秀 | 良好 | 一般 | 较差 | |------|------|------|------|------| | 转股溢价率 | <5% | 5-15% | 15-30% | >30% | | 纯债溢价率 | <20% | 20-40% | 40-60% | >60% | | 双低指标 | <120 | 120-150 | 150-180 | >180 | | 信用评级 | AAA | AA+ | AA | AA-及以下 | | 正股ROE | >15% | 10-15% | 5-10% | <5% | --- ## 🔍 质检功能 本技能内置**自动质检系统**，在每次分析完成后自动检查数据完整性和流程正确性，确保分析报告的可靠性。 ### 质检项目清单 | # | 质检项目 | 检查内容 | 重要性 | |---|----------|----------|--------| | 1 | 报告文件生成 | 检查分析报告是否成功生成 | ⭐⭐⭐ | | 2 | 财务数据获取 | 验证数据来源（真实财报/估算值） | ⭐⭐⭐ | | 3 | 分析师调用 | 确认8位分析师是否全部成功调用 | ⭐⭐⭐ | | 4 | 投票计算 | 验证加权投票和最终得分计算 | ⭐⭐⭐ | | 5 | 财务数据完整性 | 检查ROE、毛利率、净利率等核心指标 | ⭐⭐ | | 6 | 估值数据获取 | 验证PE、PB是否获取成功 | ⭐⭐ | | 7 | 数据来源标识 | 明确标注数据来源（baostock/估算） | ⭐ | ### 质检输出示例 ``` ================================================== 📋 质检报告 ================================================== ✅ 报告文件生成: 已生成: 2026-05-07_600519_估值分析师.txt ✅ 财务数据获取: baostock真实财报 ✅ 分析师调用: 8位分析师全部调用 ✅ 投票计算: ✅, 得分: 0.75 ✅ 财务数据完整性: ROE:23.5%, 毛利率:91%, 净利率:52% ⚠️ 估值数据获取: PE/PB为0 ✅ 数据来源标识: baostock真实财报, 估算:False -------------------------------------------------- 📊 总体: 通过 6/7 ⚠️ 部分质检项未通过，请检查 ================================================== ``` ### 质检状态说明 | 符号 | 含义 | |------|------| | ✅ | 通过 | | ⚠️ | 警告（可接受但不理想） | | ❌ | 失败（需检查） | ### 查看质检报告 分析完成后，质检报告会自动显示在终端输出中。所有分析都会经过7项质检检查，确保数据完整性。 --- ## 💡 投资建议（基于用户画像） 本技能支持根据用户画像提供个性化投资建议。若用户配置了 `USER.md`，系统会自动读取其投资偏好进行适配；若未配置，则使用以下默认模板： ### 默认用户画像（示例：张权） | 特征 | 默认值 | |------|--------| | **投资风格** | 价值投资（格雷厄姆模型） | | **投资目标** | 财富增值、复利增长 | | **分析偏好** | 量化选股、系统化决策 | | **时间视角** | 长期主义 | ### 评分体系解读（通用） | 得分区间 | 建议 | 解读 | |----------|------|------| | **≥0.80** | ✅ 强烈建议 | 符合价值投资标准，具备安全边际 | | **0.65-0.79** | ⚠️ 谨慎观望 | 需要进一步观察，等待更好的买入时机 | | **<0.65** | ❌ 不建议 | 风险较高或估值不合理 | ### 关键指标优先级（通用） 1. **ROE（净资产收益率）** - 采用动态评估：行业基准×财报季度系数（Q1=25%, Q2=50%, Q3=75%, Q4=100%） 2. **PE（市盈率）** 低于行业平均更安全 3. **PB（市净率）** < 2 倍为合理 4. **股息率** > 3% 提供现金流保障 5. **资产负债率** < 50% 财务健康 ### 动态ROE评估说明 本系统采用**动态ROE评估**机制，考虑以下因素： | 因素 | 说明 | |------|------| | **行业基准** | 不同行业ROE基准不同（食品饮料18%、银行10%、家电12%、电气机械11%等） | | **财报季度** | Q1=25%、Q2=50%、Q3=75%、Q4=100%（Q1数据少，阈值更低） | | **历史趋势** | 与公司历史ROE对比，判断改善或恶化 | **计算公式**：动态阈值 = 行业基准 × 季度系数 #### 季度对比表示例 ``` ====================================================================== 行业: 环保 行业基准ROE: 8.0% ====================================================================== 季度 | 系数 | 行业平均 | 动态阈值 | 公司ROE | 差距 ---------------------------------------------------------------------- Q1 | 25% | 8.0 | 2.00 | 2.71% | +0.71% Q2 | 50% | 8.0 | 4.00 | - | - Q3 | 75% | 8.0 | 6.00 | - | - Q4 | 100% | 8.0 | 8.00 | - | - ====================================================================== 当前所在: Q1 (动态阈值 2.00%) 评级: 良好 (得分: 67) vs行业平均: -66.1% ``` - **行业平均**：该行业所有公司的平均ROE（固定值） - **动态阈值**：行业平均 × 季度系数（会随季度变化） - **差距**：公司ROE - 动态阈值（正数表示高于阈值） ### 使用建议（通用） 1. **组合投资**：不建议单一股票仓位超过20% 2. **分批建仓**：价值股可以考虑定投策略 3. **止损纪律**：设置8%止损线保护本金 4. **持续跟踪**：定期复查持仓股票的基本面变化 ### 自定义用户画像 如需自定义投资建议，请在 `USER.md` 中配置以下字段： ```markdown ## 投资偏好 - 投资风格：[价值投资/成长投资/趋势投资] - 风险偏好：[保守/稳健/激进] - 持仓周期：[短线/中线/长线] - 期望收益率：XX% ```4. **持续跟踪**：定期复查持仓股票的基本面变化 --- ## 📚 参考资源 - [AKShare 官方文档](https://akshare.akfamily.xyz/) - [AKShare GitHub](https://github.com/akfamily/akshare) --- ## 📜 版本变更历史 | 版本 | 日期 | 更新内容 | |------|------|----------| | **v1.5.1** | **2026-05-11** | 优化：根据查询日期自动匹配对应财报期间，支持 -d 参数指定日期 | | **v1.5.0** | **2026-05-09** | 新增：目标价指导功能（方案B扩展估值分析师）- 悲观/中性/乐观目标价、安全边际分析 | | **v1.4.9** | **2026-05-08** | 修复：添加安全警告和USER.md数据使用说明，添加.gitignore | | **v1.4.8** | **2026-05-08** | 新增：个性化投资建议、数据三级fallback、行业涨幅 | | **v1.4.7** | **2026-05-08** | Agent Browser新闻功能默认关闭 | | **v1.4.6** | **2026-05-08** | 精确清理Chrome进程 | | **v1.4.4** | **2026-05-07** | Agent Browser改为东方财富搜索 | | **v1.4.3** | **2026-05-07** | 修复行业映射、PE/PB显示，新增财务数据获取 | | v1.3.9 | 2026-05-04 | 新增财经新闻备用源 | | v1.3.5 | 2026-05-04 | 股息率改为真实数据获取 | | v1.3.4 | 2026-05-04 | 新增新闻数据（模拟）| | v1.3.0 | 2026-05-03 | baostock优先调用 | | v1.2.9 | 2026-05-03 | 明确数据源，添加风险提示 | | v1.2.0 | **2026-04-29** | 新增财务趋势和行业对比分析 | | v1.1.0 | 2026-04-29 | 新增基金、ETF、可转债分析 | | v1.0.0 | 2026-04-29 | 首发：8位分析师并行研究 | --- <div align="center"> <strong>Boheng Investment Workflow</strong><br> 让投资决策更理性、更全面 📊🤖 </div>

path: CHANGELOG.md
content: # 更新日志

## v1.5.5 (2026-05-19)

### ✨ 新增功能
- **动态ROE评估系统** (`roe_evaluator.py`)
  - 根据行业基准动态调整ROE评估标准（不再固定15%）
  - 考虑财报季度因素：Q1=25%, Q2=50%, Q3=75%, Q4=100%
  - 支持历史趋势对比
  - **新增季度对比表**：显示每个季度的行业基准、动态阈值、公司ROE、差距

### 🔧 优化
- `analysts.py` 集成动态ROE评估
- `financial_data_baostock.py` 财报期间改为结构化数据
- `quick_analysis.py` 传递财报季度信息给分析师

### 📚 文档更新
- SKILL.md 新增"动态ROE评估说明"章节（含季度对比表示例）
- 文件结构新增 `roe_evaluator.py`

---

## v1.5.2 (2026-05-11)

### 🔧 优化
- 根据查询日期自动匹配对应财报期间
- 支持 -d 参数指定日期

---

## v1.5.1 (2026-05-xx)

### 早期版本
- 基础8位分析师系统
- baostock/AKShare双数据源
- 财务趋势分析
- 行业对比分析

path: _meta.json
content: ownerId: kn7bj8mtcw6j7s73kyz9zhck6985kv85
slug: boheng-investment-workflow
version: 1.4.7
publishedAt: 1778204000000

path: requirements.txt
content: requests>=2.28.0
beautifulsoup4>=4.11.0
lxml>=4.9.0
duckduckgo-search>=7.0

path: scripts/analysts.py
content: #!/usr/bin/env python3
"""
投资研究系统 - 8位分析师模块
每位分析师独立分析并给出投资建议（使用真实财务数据）
"""
from typing import Dict, List, Tuple
from datetime import datetime
import sys
import os

try:
    from config import ANALYST_WEIGHTS, VOTE_BUY, VOTE_CAUTION, VOTE_SELL
except ImportError:
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from config import ANALYST_WEIGHTS, VOTE_BUY, VOTE_CAUTION, VOTE_SELL

# 导入动态ROE评估器
try:
    from roe_evaluator import evaluate_roe_dynamic, get_industry_roe_benchmark
except ImportError:
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    try:
        from roe_evaluator import evaluate_roe_dynamic, get_industry_roe_benchmark
    except ImportError:
        # 降级方案：使用简单的固定阈值
        evaluate_roe_dynamic = None
        get_industry_roe_benchmark = lambda x: 10.0


class BaseAnalyst:
    """分析师基类"""
    
    def __init__(self, name: str, weight: float = 1.0):
        self.name = name
        self.weight = weight
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        """
        分析股票并给出建议
        
        Args:
            quote: 行情数据
            financial: 财务数据（来自 financial_data.py）
            
        Returns:
            (投票结果, 分析理由)
        """
        raise NotImplementedError


class MacroAnalyst(BaseAnalyst):
    """宏观经济分析师"""
    
    def __init__(self):
        super().__init__("宏观经济分析师", ANALYST_WEIGHTS.get("宏观经济分析师", 1.0))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 获取关键数据
        change_pct = quote.get('涨跌幅', 0)
        name = quote.get('名称', '该股票')
        code = quote.get('代码', '')
        price = quote.get('现价', 0)
        
        # 确保是数值类型
        if isinstance(change_pct, str):
            change_pct = float(change_pct.replace('%', '').replace('+', ''))
        elif change_pct is None:
            change_pct = 0
        
        # 获取财务和估值数据
        fi = financial.get('财务指标', {}) if financial else {}
        ev = financial.get('估值', {}) if financial else {}
        industry_data = financial.get('行业', {}) if financial else {}
        
        # 提取关键指标
        roe = fi.get('ROE', 0)
        pe = ev.get('PE_TTM', 0)
        pb = ev.get('PB', 0)
        industry = industry_data.get('行业', '未知')
        
        # 生成个性化分析
        # 1. 市场情绪判断
        if change_pct > 3:
            sentiment = "市场情绪过热"
        elif change_pct < -3:
            sentiment = "市场情绪低迷"
        else:
            sentiment = "市场情绪平稳"
        
        # 2. 基本面评估 - 使用动态ROE评估
        if roe and roe > 0:
            # 获取财报季度（从financial数据中获取）
            quarter = financial.get('report_period', {}).get('quarter', 4) if financial else 4
            industry = financial.get('行业', {}).get('行业', '综合') if financial else '综合'
            
            # 使用动态ROE评估
            if evaluate_roe_dynamic:
                roe_result = evaluate_roe_dynamic(roe, industry, quarter)
                roe_comment = roe_result.get('reason', f"ROE {roe:.1f}%")
            else:
                # 降级方案
                industry_benchmark = get_industry_roe_benchmark(industry)
                if roe >= industry_benchmark * 1.2:
                    roe_comment = f"ROE {roe:.1f}%，高于行业平均({industry_benchmark}%)"
                elif roe >= industry_benchmark:
                    roe_comment = f"ROE {roe:.1f}%，接近行业平均({industry_benchmark}%)"
                else:
                    roe_comment = f"ROE {roe:.1f}%，低于行业平均({industry_benchmark}%)"
        else:
            roe_comment = "ROE数据异常"
        
        # 3. 估值评估
        if pe and pe > 0:
            if pe < 15:
                pe_comment = f"PE={pe:.1f}倍，估值较低"
            elif pe < 30:
                pe_comment = f"PE={pe:.1f}倍，估值合理"
            else:
                pe_comment = f"PE={pe:.1f}倍，估值偏高"
        else:
            pe_comment = "估值数据获取失败"
        
        # 4. 行业特性
        industry_comments = {
            "银行": "银行业发展受货币政策和利率环境影响较大，当前利率环境相对稳定",
            "房地产": "房地产行业受宏观调控和信贷政策影响明显，需关注政策变化",
            "医药": "医药行业具备防御性，受医保政策和人口老龄化支撑",
            "消费": "消费行业与居民收入和消费意愿密切相关，关注消费复苏进度",
            "科技": "科技行业创新驱动强，但估值波动较大",
            "新能源": "新能源行业政策支持力度大，但竞争激烈",
            "电力": "电力行业稳定但受电价政策影响",
            "钢铁": "钢铁行业受宏观经济和基建投资影响较大",
            "汽车": "汽车行业受消费政策和市场竞争影响",
        }
        industry_comment = industry_comments.get(industry, f"{industry}行业需关注其特定的市场环境和政策因素")
        
        # 5. 综合投票逻辑
        vote = VOTE_CAUTION
        score = 0
        
        # 基本面加分 - 使用动态ROE评估
        if roe and roe > 0:
            quarter = financial.get('report_period', {}).get('quarter', 4) if financial else 4
            industry = financial.get('行业', {}).get('行业', '综合') if financial else '综合'
            
            if evaluate_roe_dynamic:
                roe_result = evaluate_roe_dynamic(roe, industry, quarter)
                roe_score = roe_result.get('score', 0)
                if roe_score >= 80:
                    score += 2
                elif roe_score >= 60:
                    score += 1
            else:
                # 降级方案：使用行业基准
                industry_benchmark = get_industry_roe_benchmark(industry)
                if roe >= industry_benchmark * 1.2:
                    score += 2
                elif roe >= industry_benchmark:
                    score += 1
        
        # 估值加分
        if pe and 0 < pe < 20:
            score += 2
        elif pe and 20 <= pe < 40:
            score += 1
        elif pe and pe >= 60:
            score -= 2
        
        # 市场情绪调整
        if abs(change_pct) < 3:
            score += 1
        elif abs(change_pct) > 5:
            score -= 1
        
        if score >= 3:
            vote = VOTE_BUY
        elif score <= -1:
            vote = VOTE_SELL
        
        # 生成个性化理由
        reason = f"{sentiment}，{name}（{code}）当前价格¥{price}。{roe_comment}，{pe_comment}。{industry_comment}。综合评分{score}分，{('建议买入' if vote == VOTE_BUY else '建议观望' if vote == VOTE_CAUTION else '建议回避')}。"
        
        return vote, reason


class IndustryAnalyst(BaseAnalyst):
    """行业研究员"""
    
    def __init__(self):
        super().__init__("行业研究员", ANALYST_WEIGHTS.get("行业研究员", 1.5))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 使用真实行业数据
        industry_data = financial.get('行业', {}) if financial else {}
        
        # 检查是否有有效的行业数据
        if 'error' in industry_data or not industry_data.get('行业') or industry_data.get('行业') == '未知':
            # 无行业数据时的默认判断
            name = quote.get('名称', '')
            return self._fallback_analyze(name)
        
        industry = industry_data.get('行业', '未知')
        industry_change = industry_data.get('行业涨跌幅', 0)
        industry_rank = industry_data.get('行业排名', 0)
        total_industries = industry_data.get('行业总数', 0)
        sentiment = industry_data.get('行业景气度', '中')
        
        if sentiment == '高':
            vote = VOTE_BUY
            reason = f"所属【{industry}】行业景气度高，行业涨幅{industry_change:+.2f}%，排名第{industry_rank}/{total_industries}。行业处于成长期，市场空间广阔，政策支持力度大。"
        elif sentiment == '低':
            vote = VOTE_SELL
            reason = f"所属【{industry}】行业景气度低，行业涨幅{industry_change:+.2f}%，排名靠后。行业处于调整期，建议规避或等待行业拐点。"
        else:
            vote = VOTE_CAUTION
            reason = f"所属【{industry}】行业景气度中等，行业涨幅{industry_change:+.2f}%。行业处于成熟期，增长空间有限，需关注行业竞争格局变化。"
        
        return vote, reason
    
    def _fallback_analyze(self, name: str) -> Tuple[str, str]:
        """无行业数据时的备用判断"""
        if '银行' in name:
            return VOTE_CAUTION, "所属银行业景气度中等，息差收窄但资产质量改善，需关注经济周期影响。"
        elif '核电' in name or '电力' in name:
            return VOTE_BUY, "所属电力行业景气度稳定，新能源转型带来增长机遇，政策支持力度大。"
        elif '家电' in name:
            return VOTE_CAUTION, "所属家电行业景气度中等，内需疲软但出口增长，需关注消费复苏节奏。"
        elif '永强' in name:
            return VOTE_CAUTION, "所属【家用轻工】行业景气度中等，户外休闲家具行业增长平稳，需关注海外市场需求。"
        else:
            return VOTE_CAUTION, "行业景气度需进一步分析，建议关注行业政策变化。"


class FundamentalAnalyst(BaseAnalyst):
    """基本面分析师"""
    
    def __init__(self):
        super().__init__("基本面分析师", ANALYST_WEIGHTS.get("基本面分析师", 1.5))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 使用真实财务数据
        fi = financial.get('财务指标', {}) if financial else {}
        
        if 'error' in fi:
            return self._fallback_analyze(quote.get('名称', ''))
        
        # 提取关键指标
        roe = fi.get('ROE', 0)
        gross_margin = fi.get('毛利率', 0)
        net_margin = fi.get('净利率', 0)
        revenue_growth = fi.get('营收增速', 0)
        profit_growth = fi.get('利润增速', 0)
        debt_ratio = fi.get('资产负债率', 0)
        current_ratio = fi.get('流动比率', 0)
        ocf_ratio = fi.get('经营现金流/营收', 0)
        
        # 获取行业和财报季度信息
        industry_data = financial.get('行业', {}) if financial else {}
        industry = industry_data.get('行业', '综合') if industry_data else '综合'
        report_period = financial.get('report_period', {}) if financial else {}
        quarter = report_period.get('quarter', 4) if report_period else 4
        
        # 计算基本面评分 - 传入行业和季度用于动态ROE评估
        score = self._calculate_score(
            roe, gross_margin, revenue_growth, profit_growth, 
            debt_ratio, current_ratio, ocf_ratio,
            industry=industry, quarter=quarter
        )
        
        if score >= 70:
            vote = VOTE_BUY
            reason = f"基本面优秀：ROE {roe:.1f}%，毛利率{gross_margin:.1f}%，净利率{net_margin:.1f}%。营收增速{revenue_growth:.1f}%，利润增速{profit_growth:.1f}%。资产负债率{debt_ratio:.1f}%，流动比率{current_ratio:.2f}。经营现金流/营收{ocf_ratio:.1f}%，现金流健康。护城河稳固，具备长期投资价值。"
        elif score >= 50:
            vote = VOTE_CAUTION
            reason = f"基本面一般：ROE {roe:.1f}%，毛利率{gross_margin:.1f}%。营收增速{revenue_growth:.1f}%，利润增速{profit_growth:.1f}%。资产负债率{debt_ratio:.1f}%。需关注业绩增长持续性，建议等待业绩拐点确认。"
        else:
            vote = VOTE_SELL
            reason = f"基本面较差：ROE {roe:.1f}%偏低，毛利率{gross_margin:.1f}%下滑。营收增速{revenue_growth:.1f}%，利润增速{profit_growth:.1f}%。资产负债率{debt_ratio:.1f}%偏高。盈利能力下滑，现金流承压，建议规避。"
        
        return vote, reason
    
    def _calculate_score(self, roe, gross_margin, revenue_growth, profit_growth, 
                         debt_ratio, current_ratio, ocf_ratio, industry="综合", quarter=4) -> float:
        """
        计算基本面评分 - 使用动态ROE评估
        
        Args:
            industry: 行业名称
            quarter: 财报季度 (1/2/3/4)
        """
        score = 0
        
        # ROE评分（权重30%）- 使用动态评估
        if evaluate_roe_dynamic and roe and roe > 0:
            roe_result = evaluate_roe_dynamic(roe, industry, quarter)
            roe_score = roe_result.get('score', 0)
            score += roe_score * 0.3  # 30%权重对应30分
        else:
            # 降级方案：使用行业基准
            industry_benchmark = get_industry_roe_benchmark(industry)
            if roe >= industry_benchmark * 1.3:
                score += 30
            elif roe >= industry_benchmark:
                score += 20
            elif roe >= industry_benchmark * 0.7:
                score += 10
        
        # 毛利率评分（权重20%）
        if gross_margin >= 40:
            score += 20
        elif gross_margin >= 25:
            score += 15
        elif gross_margin >= 15:
            score += 10
        
        # 增长评分（权重20%）
        avg_growth = (revenue_growth + profit_growth) / 2
        if avg_growth >= 20:
            score += 20
        elif avg_growth >= 10:
            score += 15
        elif avg_growth >= 0:
            score += 10
        
        # 偿债能力评分（权重15%）
        if debt_ratio < 40 and current_ratio >= 1.5:
            score += 15
        elif debt_ratio < 60 and current_ratio >= 1:
            score += 10
        elif debt_ratio < 75:
            score += 5
        
        # 现金流评分（权重15%）
        if ocf_ratio >= 20:
            score += 15
        elif ocf_ratio >= 10:
            score += 10
        elif ocf_ratio >= 0:
            score += 5
        
        return score
    
    def _fallback_analyze(self, name: str) -> Tuple[str, str]:
        """无财务数据时的备用判断"""
        return VOTE_CAUTION, "财务数据获取失败，无法进行基本面分析，建议谨慎观望。"


class TechnicalAnalyst(BaseAnalyst):
    """技术分析师"""
    
    def __init__(self):
        super().__init__("技术分析师", ANALYST_WEIGHTS.get("技术分析师", 1.0))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 使用真实技术指标数据
        tech = financial.get('技术指标', {}) if financial else {}
        
        if 'error' in tech:
            return self._fallback_analyze(quote)
        
        # 确保价格是数值
        def safe_float(val):
            if isinstance(val, str):
                try:
                    return float(val.replace('¥', '').replace(',', ''))
                except:
                    return 0
            return float(val) if val is not None else 0
        
        price = safe_float(tech.get('收盘价', quote.get('现价', 0)))
        ma5 = tech.get('MA5', 0)
        ma10 = tech.get('MA10', 0)
        ma20 = tech.get('MA20', 0)
        macd = tech.get('MACD', 0)
        rsi = tech.get('RSI', 50)
        ma_cross = tech.get('MA金叉', False)
        macd_cross = tech.get('MACD金叉', False)
        rsi_overbuy = tech.get('RSI超买', False)
        rsi_oversell = tech.get('RSI超卖', False)
        
        # 技术信号判断
        signals = []
        
        if ma_cross:
            signals.append("MA金叉")
        if macd_cross:
            signals.append("MACD金叉")
        if rsi_oversell:
            signals.append("RSI超卖")
        if rsi_overbuy:
            signals.append("RSI超买")
        
        # 趋势判断
        if price > ma5 and price > ma20:
            trend = "上升趋势"
        elif price < ma5 and price < ma20:
            trend = "下降趋势"
        else:
            trend = "震荡整理"
        
        # 综合判断
        positive_signals = ma_cross or macd_cross or rsi_oversell
        negative_signals = rsi_overbuy
        
        if positive_signals and not negative_signals:
            vote = VOTE_BUY
            reason = f"技术面偏强：{trend}，当前价格¥{price:.2f}。MA5={ma5:.2f}，MA20={ma20:.2f}。RSI={rsi:.1f}。出现买入信号：{', '.join(signals) if signals else '趋势向上'}。建议顺势参与。"
        elif negative_signals:
            vote = VOTE_CAUTION
            reason = f"技术面偏弱：{trend}，当前价格¥{price:.2f}。RSI={rsi:.1f}超买。出现卖出信号：{', '.join(signals)}。建议等待回调。"
        else:
            vote = VOTE_CAUTION
            reason = f"技术面中性：{trend}，当前价格¥{price:.2f}。MA5={ma5:.2f}，MA20={ma20:.2f}。RSI={rsi:.1f}。方向不明，建议等待突破信号。"
        
        return vote, reason
    
    def _fallback_analyze(self, quote: Dict) -> Tuple[str, str]:
        """无技术数据时的备用判断"""
        change_pct = quote.get('涨跌幅', 0)
        if isinstance(change_pct, str):
            try:
                change_pct = float(change_pct.replace('%', '').replace('+', ''))
            except:
                change_pct = 0
        
        price = quote.get('现价', 0)
        if isinstance(price, str):
            try:
                price = float(price.replace('¥', '').replace(',', ''))
            except:
                price = 0
        
        if change_pct > 5:
            return VOTE_CAUTION, f"短期涨幅过大（+{change_pct}%），技术指标可能超买，存在回调压力。"
        elif change_pct > 2:
            return VOTE_BUY, f"股价走势强劲（+{change_pct}%），短期趋势向上，可适当参与。"
        elif change_pct < -3:
            return VOTE_CAUTION, f"股价下跌（{change_pct}%），技术形态走弱，建议观望。"
        else:
            return VOTE_CAUTION, f"股价震荡整理，当前¥{price:.2f}，方向不明。"


class RiskAnalyst(BaseAnalyst):
    """风险控制师"""
    
    def __init__(self):
        super().__init__("风险控制师", ANALYST_WEIGHTS.get("风险控制师", 1.2))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 综合风险评估
        risks = []
        risk_level = "低"
        
        # 波动风险
        change_pct = quote.get('涨跌幅', 0)
        if isinstance(change_pct, str):
            try:
                change_pct = float(change_pct.replace('%', '').replace('+', ''))
            except:
                change_pct = 0
        
        if abs(change_pct) > 5:
            risks.append("波动风险较高")
            risk_level = "中"
        
        # 财务风险
        fi = financial.get('财务指标', {}) if financial else {}
        if 'error' not in fi:
            debt_ratio = fi.get('资产负债率', 0)
            if debt_ratio > 70:
                risks.append("财务杠杆风险")
                risk_level = "高"
            
            ocf_ratio = fi.get('经营现金流/营收', 0)
            if ocf_ratio < 0:
                risks.append("现金流风险")
                risk_level = "高"
        
        # 估值风险
        ev = financial.get('估值分位', {}) if financial else {}
        if 'error' not in ev:
            pe_percentile = ev.get('PE分位', 50)
            if pe_percentile > 80:
                risks.append("估值偏高风险")
                risk_level = "高"
        
        # 系统性风险
        risks.append("市场系统性风险")
        
        # 给出建议
        if risk_level == "高":
            vote = VOTE_SELL
            reason = f"风险等级：高。主要风险：{', '.join(risks)}。建议仓位控制在10%以内，设置8%止损线。谨慎参与。"
        elif risk_level == "中":
            vote = VOTE_CAUTION
            reason = f"风险等级：中。主要风险：{', '.join(risks)}。建议仓位控制在20%以内，设置10%止损线。"
        else:
            vote = VOTE_BUY
            reason = f"风险等级：低。主要风险：{', '.join(risks)}。建议仓位可放宽至30%，设置12%止损线。"
        
        return vote, reason


class QuantAnalyst(BaseAnalyst):
    """量化分析师"""
    
    def __init__(self):
        super().__init__("量化分析师", ANALYST_WEIGHTS.get("量化分析师", 1.0))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 量化因子分析
        momentum_score = 50
        value_score = 50
        quality_score = 50
        
        # 动量因子
        change_pct = quote.get('涨跌幅', 0)
        if isinstance(change_pct, str):
            try:
                change_pct = float(change_pct.replace('%', '').replace('+', ''))
            except:
                change_pct = 0
        momentum_score = 50 + change_pct * 5
        
        # 价值因子
        ev = financial.get('估值分位', {}) if financial else {}
        if 'error' not in ev:
            pe_percentile = ev.get('PE分位', 50)
            value_score = 100 - pe_percentile  # PE分位越低越好
        
        # 质量因子
        fi = financial.get('财务指标', {}) if financial else {}
        if 'error' not in fi:
            roe = fi.get('ROE', 0)
            gross_margin = fi.get('毛利率', 0)
            quality_score = min(100, roe * 3 + gross_margin)
        
        # 综合得分
        total_score = momentum_score * 0.3 + value_score * 0.3 + quality_score * 0.4
        
        if total_score >= 65:
            vote = VOTE_BUY
            reason = f"量化综合得分：{total_score:.1f}分。动量因子{momentum_score:.1f}，价值因子{value_score:.1f}，质量因子{quality_score:.1f}。多因子模型显示正向信号，建议参与。"
        elif total_score >= 50:
            vote = VOTE_CAUTION
            reason = f"量化综合得分：{total_score:.1f}分。动量因子{momentum_score:.1f}，价值因子{value_score:.1f}，质量因子{quality_score:.1f}。因子信号中性，建议观望。"
        else:
            vote = VOTE_SELL
            reason = f"量化综合得分：{total_score:.1f}分。动量因子{momentum_score:.1f}，价值因子{value_score:.1f}，质量因子{quality_score:.1f}。因子信号偏负面，建议规避。"
        
        return vote, reason


class SentimentAnalyst(BaseAnalyst):
    """情绪分析师"""
    
    def __init__(self):
        super().__init__("情绪分析师", ANALYST_WEIGHTS.get("情绪分析师", 0.8))
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 1. 首先分析资金流向情绪
        flow = financial.get('资金流向', {}) if financial else {}
        
        if 'error' in flow:
            # 无资金数据时，使用新闻数据分析
            return self._news_only_analyze(quote, news_data)
        
        main_flow = flow.get('主力净流入', 0) or 0
        five_day_flow = flow.get('5日主力净流入', 0) or 0
        
        # 2. 分析新闻情绪
        news_sentiment = ""
        news_info = ""
        if news_data and len(news_data) > 0:
            positive = sum(1 for n in news_data if n.get('sentiment') == 'positive')
            negative = sum(1 for n in news_data if n.get('sentiment') == 'negative')
            neutral = sum(1 for n in news_data if n.get('sentiment') == 'neutral')
            total = len(news_data)
            
            if positive > negative and positive >= total * 0.4:
                news_sentiment = "偏正面"
            elif negative > positive and negative >= total * 0.4:
                news_sentiment = "偏负面"
            elif neutral >= total * 0.6:
                news_sentiment = "中性"
            else:
                news_sentiment = "分化"
            
            # 获取最新新闻
            latest = news_data[0].get('title', '')[:35] if news_data else ''
            news_info = f"。新闻情绪{news_sentiment}（最新：「{latest}...」）"
        
        # 3. 综合判断
        if main_flow > 0 and five_day_flow > 0:
            sentiment = "乐观"
            vote = VOTE_BUY
            reason = f"市场情绪：{sentiment}。主力资金今日净流入{main_flow/10000:.1f}万，5日累计净流入{five_day_flow/10000:.1f}万。资金流入积极{news_info}可适当参与。"
        elif main_flow < 0 and five_day_flow < 0:
            sentiment = "恐惧"
            vote = VOTE_CAUTION
            reason = f"市场情绪：{sentiment}。主力资金今日净流出{abs(main_flow)/10000:.1f}万，5日累计净流出{abs(five_day_flow)/10000:.1f}万。恐慌情绪蔓延{news_info}建议分批建仓。"
        else:
            sentiment = "中性"
            vote = VOTE_CAUTION
            reason = f"市场情绪：{sentiment}。主力资金今日净流入{main_flow/10000:.1f}万。观望情绪浓厚{news_info}建议等待方向明确。"
        
        return vote, reason
    
    def _news_only_analyze(self, quote: Dict, news_data: List[Dict] = None) -> Tuple[str, str]:
        """无资金数据时，仅根据新闻分析"""
        if not news_data or len(news_data) == 0:
            return self._fallback_analyze(quote)
        
        positive = sum(1 for n in news_data if n.get('sentiment') == 'positive')
        negative = sum(1 for n in news_data if n.get('sentiment') == 'negative')
        total = len(news_data)
        
        latest = news_data[0].get('title', '')[:40] if news_data else ''
        
        if positive > negative and positive >= total * 0.4:
            return VOTE_BUY, f"市场情绪：乐观（基于新闻分析）。正面{positive}条，负面{negative}条。最新：「{latest}...」新闻面偏多，可适当关注。"
        elif negative > positive and negative >= total * 0.4:
            return VOTE_CAUTION, f"市场情绪：谨慎（基于新闻分析）。正面{positive}条，负面{negative}条。最新：「{latest}...」新闻面偏空，建议观望。"
        else:
            return VOTE_CAUTION, f"市场情绪：中性（基于新闻分析）。正面{positive}条，负面{negative}条。最新：「{latest}...」新闻面无明显方向，建议等待。"
    
    def _fallback_analyze(self, quote: Dict) -> Tuple[str, str]:
        """无资金数据时的备用判断"""
        change_pct = quote.get('涨跌幅', 0)
        if isinstance(change_pct, str):
            try:
                change_pct = float(change_pct.replace('%', '').replace('+', ''))
            except:
                change_pct = 0
        
        if change_pct > 5:
            return VOTE_SELL, f"市场情绪：极度贪婪。短期涨幅过大，投资者情绪亢奋，需警惕获利回吐。"
        elif change_pct > 2:
            return VOTE_BUY, f"市场情绪：乐观。股价上涨，市场信心增强。"
        elif change_pct < -3:
            return VOTE_CAUTION, f"市场情绪：恐惧。恐慌情绪蔓延，可能存在逆向机会。"
        else:
            return VOTE_CAUTION, f"市场情绪：中性。观望情绪浓厚。"




class ValuationAnalyst(BaseAnalyst):
    """估值分析师"""
    
    def __init__(self):
        super().__init__("估值分析师", ANALYST_WEIGHTS.get("估值分析师", 1.2))
    
    def calculate_target_prices(self, quote: Dict, financial: Dict = None) -> Dict:
        """
        计算目标价（方案B扩展）
        
        Returns:
            dict: {'pessimistic', 'neutral', 'optimistic', 'safety_margin', 'method'}
        """
        # 获取当前价格
        price = quote.get('现价', 0)
        if isinstance(price, str):
            try:
                price = float(price.replace('¥', '').replace(',', ''))
            except:
                price = 0
        
        if price <= 0:
            return None
        
        # 获取财务数据
        fi = financial.get('财务指标', {}) if financial else {}
        ev = financial.get('估值分位', {}) if financial else {}
        
        # 方法1：PE分位法
        pe_percentile = ev.get('PE分位', 50) if ev else 50
        pe_target = price * (pe_percentile / 50)  # 基于分位调整
        
        # 方法2：简化DCF法（基于ROE）
        roe = fi.get('ROE', 0) if fi else 0
        growth_rate = fi.get('利润增速', 5) / 100 if fi else 0.05
        if roe > 0 and growth_rate > 0:
            # 简化Gordon模型：价值 = ROE * (1+g) / (r-g)
            # 假设折现率r=10%，永续增长率g=3%
            dcf_value = (roe / 100) * (1 + growth_rate) / (0.10 - 0.03)
            dcf_target = price * dcf_value * 10  # 调整为相对价格
            # 限制波动范围
            dcf_target = max(price * 0.5, min(price * 2.0, dcf_target))
        else:
            dcf_target = price
        
        # 方法3：PEG法
        pe_current = ev.get('PE当前', 0) if ev else 0
        if pe_current > 0 and growth_rate > 0:
            peg = pe_current / (growth_rate * 100)  # 转为百分比
            # PEG=1为合理，<1为低估，>1为高估
            peg_target = price / peg if peg > 0 else price
            peg_target = max(price * 0.5, min(price * 2.0, peg_target))
        else:
            peg_target = price
        
        # 综合目标价（三种方法平均）
        neutral_target = (pe_target + dcf_target + peg_target) / 3
        
        # 计算三个目标价
        pessimistic = round(neutral_target * 0.7, 2)   # -30%
        optimistic = round(neutral_target * 1.3, 2)    # +30%
        neutral = round(neutral_target, 2)
        
        # 安全边际
        safety_margin = round((price - neutral) / neutral * 100, 2)
        
        return {
            'pessimistic': pessimistic,
            'neutral': neutral,
            'optimistic': optimistic,
            'safety_margin': safety_margin,
            'method': 'PE分位 + DCF + PEG 综合'
        }
    
    def analyze(self, quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> Tuple[str, str]:
        # 使用真实估值数据
        ev = financial.get('估值分位', {}) if financial else {}
        valuation = financial.get('估值', {}) if financial else {}
        
        if not ev or 'error' in ev:
            return self._fallback_analyze(quote, valuation)
        
        pe_current = ev.get('PE当前', 0)
        pe_percentile = ev.get('PE分位', 50)
        pb_current = ev.get('PB当前', 0)
        pb_percentile = ev.get('PB分位', 50)
        rating = ev.get('估值评级', '合理')
        
        # 股息率
        dv_ratio = valuation.get('股息率', 0) if 'error' not in valuation else 0
        
        # 计算目标价
        target_info = self.calculate_target_prices(quote, financial)
        
        if pe_percentile <= 30:
            vote = VOTE_BUY
            reason = f"估值水平：{pe_percentile}%分位，属于【{rating}】。PE {pe_current}倍，PB {pb_current}倍。股息率{dv_ratio:.1f}%。估值具备安全边际，适合长期持有。"
        elif pe_percentile <= 60:
            vote = VOTE_CAUTION
            reason = f"估值水平：{pe_percentile}%分位，属于【{rating}】。PE {pe_current}倍，PB {pb_current}倍。股息率{dv_ratio:.1f}%。估值合理但无显著优势，需关注业绩增长。"
        else:
            vote = VOTE_SELL
            reason = f"估值水平：{pe_percentile}%分位，属于【{rating}】。PE {pe_current}倍，PB {pb_current}倍。估值偏高，风险大于机会，建议等待估值回归。"
        
        # 添加目标价信息到理由中
        if target_info:
            target_line = f"\n\n🎯 目标价指导：悲观¥{target_info['pessimistic']}（-30%）| 中性¥{target_info['neutral']}（合理）| 乐观¥{target_info['optimistic']}（+30%）\n📊 当前安全边际：{target_info['safety_margin']}%{'（价格低于目标价，可买入）' if target_info['safety_margin'] > 0 else '（价格高于目标价，建议持有）'}"
            reason += target_line
        
        return vote, reason
    
    def _fallback_analyze(self, quote: Dict, valuation: Dict) -> Tuple[str, str]:
        """无估值分位数据时的备用判断"""
        if 'error' not in valuation:
            pe = valuation.get('PE_TTM', 0)
            pb = valuation.get('PB', 0)
            dv = valuation.get('股息率', 0)
            
            if pe < 15 and pb < 1.5:
                return VOTE_BUY, f"PE {pe}倍，PB {pb}倍，股息率{dv:.1f}%。估值偏低，具备安全边际。"
            elif pe < 30:
                return VOTE_CAUTION, f"PE {pe}倍，PB {pb}倍。估值合理，需关注业绩增长。"
            else:
                return VOTE_SELL, f"PE {pe}倍，估值偏高，建议谨慎。"
        
        return VOTE_CAUTION, "估值数据获取失败，无法进行估值分析。"


# 分析师注册表
ANALYSTS = [
    MacroAnalyst(),
    IndustryAnalyst(),
    FundamentalAnalyst(),
    TechnicalAnalyst(),
    RiskAnalyst(),
    QuantAnalyst(),
    SentimentAnalyst(),
    ValuationAnalyst(),
]


def run_all_analysts(quote: Dict, financial: Dict = None, news_data: List[Dict] = None) -> List[Dict]:
    """
    运行所有分析师并返回结果
    
    Args:
        quote: 股票行情数据
        financial: 财务数据（来自 financial_data.py）
        news_data: 新闻数据（传给情绪分析师）
        
    Returns:
        分析结果列表
    """
    results = []
    
    for analyst in ANALYSTS:
        # 所有分析师都需要接收news_data参数
        vote, reason = analyst.analyze(quote, financial, news_data)
        results.append({
            'name': analyst.name,
            'weight': analyst.weight,
            'vote': vote,
            'reason': reason
        })
    
    return results


def calculate_weighted_vote(results: List[Dict]) -> Tuple[str, float]:
    """
    计算加权投票结果
    
    Returns:
        (最终建议, 加权得分)
    """
    scores = {
        VOTE_BUY: 0.0,
        VOTE_CAUTION: 0.0,
        VOTE_SELL: 0.0
    }
    
    total_weight = 0.0
    
    for r in results:
        scores[r['vote']] += r['weight']
        total_weight += r['weight']
    
    # 归一化
    for k in scores:
        scores[k] = scores[k] / total_weight if total_weight > 0 else 0
    
    # 选择得分最高的
    final_vote = max(scores, key=scores.get)
    
    return final_vote, scores[final_vote]


if __name__ == "__main__":
    # 测试
    test_quote = {
        '代码': '600919',
        '名称': '江苏银行',
        '现价': 11.40,
        '涨跌幅': 0.35
    }
    
    test_financial = {
        '财务指标': {
            'ROE': 12.5,
            '毛利率': 45,
            '净利率': 28,
            '营收增速': 8,
            '利润增速': 12,
            '资产负债率': 92,
            '流动比率': 0.8,
            '经营现金流/营收': 25
        },
        '估值分位': {
            'PE当前': 5.8,
            'PE分位': 25,
            'PB当前': 0.65,
            '估值评级': '偏低'
        }
    }
    
    results = run_all_analysts(test_quote, test_financial)
    
    print("=" * 60)
    print("8位分析师投票结果")
    print("=" * 60)
    
    for r in results:
        print(f"{r['name']} → {r['vote']} (权重 {r['weight']}x)")
        print(f"   理由：{r['reason']}")
        print()
    
    final_vote, score = calculate_weighted_vote(results)
    print(f"最终建议：{final_vote} (得分：{score:.2%})")

path: scripts/analyze_bond.py
content: #!/usr/bin/env python3
"""
可转债分析入口脚本
支持可转债转股价值、溢价率分析

数据来源：腾讯财经API (qt.gtimg.cn)
"""
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from datetime import datetime
from config import REPORTS_DIR
import requests


def get_bond_quote(bond_code: str) -> dict:
    """
    获取可转债实时行情（腾讯财经）
    
    白名单域名：qt.gtimg.cn
    """
    try:
        # 可转债市场判断
        market = 'sh' if bond_code.startswith('1') else 'sz'
        url = f"https://qt.gtimg.cn/q={market}{bond_code}"
        
        resp = requests.get(url, timeout=10)
        text = resp.content.decode('gbk')
        
        if '=' in text:
            data_str = text.split('=')[1].strip().strip('"')
            fields = data_str.split('~')
            
            if len(fields) >= 40:
                current = float(fields[3])
                prev_close = float(fields[4])
                
                return {
                    'code': bond_code,
                    'name': fields[1],
                    'price': current,
                    'change': current - prev_close,
                    'change_pct': ((current / prev_close) - 1) * 100,
                }
    except Exception as e:
        return {'error': f'获取可转债行情失败: {str(e)}'}
    
    return {'error': '无法解析可转债数据'}


def get_stock_quote(stock_code: str) -> dict:
    """获取正股行情（腾讯财经）"""
    try:
        market = 'sh' if stock_code.startswith('6') else 'sz'
        url = f"https://qt.gtimg.cn/q={market}{stock_code}"
        
        resp = requests.get(url, timeout=10)
        text = resp.content.decode('gbk')
        
        if '=' in text:
            data_str = text.split('=')[1].strip().strip('"')
            fields = data_str.split('~')
            
            if len(fields) >= 40:
                return {
                    'code': stock_code,
                    'name': fields[1],
                    'price': float(fields[3]),
                    'pe': float(fields[39]) if fields[39] else 0,
                    'pb': float(fields[46]) if fields[46] else 0,
                }
    except:
        pass
    
    return {'error': '无法获取正股数据'}


def calculate_conversion_value(bond_price: float, stock_price: float, conversion_price: float) -> dict:
    """
    计算转股价值和溢价率
    
    Args:
        bond_price: 可转债价格
        stock_price: 正股价格
        conversion_price: 转股价
    
    Returns:
        转股价值、溢价率等指标
    """
    # 转股价值 = 100 / 转股价 × 正股价格
    conversion_value = (100 / conversion_price) * stock_price
    
    # 转股溢价率 = (转债价格 - 转股价值) / 转股价值
    conversion_premium = ((bond_price - conversion_value) / conversion_value) * 100
    
    # 纯债价值估算（简化）
    bond_value = 95  # 假设纯债价值约95元
    
    # 纯债溢价率
    bond_premium = ((bond_price - bond_value) / bond_value) * 100
    
    # 双低指标
    double_low = conversion_premium + bond_premium
    
    return {
        'conversion_value': conversion_value,
        'conversion_premium': conversion_premium,
        'bond_value': bond_value,
        'bond_premium': bond_premium,
        'double_low': double_low,
    }


def analyze_bond(bond_code: str, bond_name: str = '', conversion_price: float = 0) -> dict:
    """
    分析可转债
    
    Args:
        bond_code: 可转债代码
        bond_name: 可转债名称
        conversion_price: 转股价（可选，默认估算）
    """
    print(f"📊 开始分析可转债 {bond_code}")
    print("=" * 60)
    print("⚠️ 注意：转股溢价率为估算值，仅供参考")
    print()
    
    # 获取可转债行情
    print("📈 正在获取可转债行情...")
    bond_quote = get_bond_quote(bond_code)
    
    if 'error' in bond_quote:
        print(f"❌ {bond_quote['error']}")
        return bond_quote
    
    bond_name = bond_name or bond_quote.get('name', '未知')
    print(f"✅ {bond_code} {bond_name}: ¥{bond_quote['price']:.2f} ({bond_quote['change_pct']:+.2f}%)")
    
    # 获取正股代码（可转债代码通常对应正股）
    # 常见可转债-正股映射
    bond_stock_map = {
        '113050': '601985',  # 核能转债 -> 中国核电
        '110048': '600019',  # 宝钢转债 -> 宝钢股份
        '113041': '600900',  # 长电转债 -> 长江电力
        '123107': '000921',  # 海信转债 -> 海信家电
    }
    
    stock_code = bond_stock_map.get(bond_code, '')
    
    # 获取正股行情
    stock_quote = {}
    if stock_code:
        print(f"📊 正在获取正股行情 ({stock_code})...")
        stock_quote = get_stock_quote(stock_code)
        
        if 'error' not in stock_quote:
            print(f"✅ 正股 {stock_quote['name']}: ¥{stock_quote['price']:.2f}")
    
    # 转股价估算（默认）
    if not conversion_price:
        # 基于正股价格估算转股价（通常略高于正股价格）
        if 'error' not in stock_quote:
            conversion_price = stock_quote['price'] * 1.05
        else:
            conversion_price = 10.0  # 默认值
    
    # 计算转股价值
    print(f"\n📊 计算转股价值...")
    conversion_metrics = calculate_conversion_value(
        bond_quote['price'],
        stock_quote.get('price', 10),
        conversion_price
    )
    
    # 分析结果
    result = {
        'bond_code': bond_code,
        'bond_name': bond_name,
        'bond_price': bond_quote['price'],
        'change_pct': bond_quote['change_pct'],
        'stock_code': stock_code,
        'stock_name': stock_quote.get('name', '未知'),
        'stock_price': stock_quote.get('price', 0),
        'stock_pe': stock_quote.get('pe', 0),
        'conversion_price': conversion_price,
        'conversion_value': conversion_metrics['conversion_value'],
        'conversion_premium': conversion_metrics['conversion_premium'],
        'bond_value': conversion_metrics['bond_value'],
        'bond_premium': conversion_metrics['bond_premium'],
        'double_low': conversion_metrics['double_low'],
        'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    }
    
    # 生成报告
    print("\n" + "=" * 60)
    print("        可转债分析报告")
    print("=" * 60)
    print(f"\n【转债代码】：{bond_code}")
    print(f"【转债名称】：{bond_name}")
    print(f"【转债价格】：¥{bond_quote['price']:.2f}")
    print(f"【涨跌幅】：{bond_quote['change_pct']:+.2f}%")
    
    if stock_code and 'error' not in stock_quote:
        print(f"\n📊 正股信息：")
        print(f"   正股代码：{stock_code}")
        print(f"   正股名称：{stock_quote['name']}")
        print(f"   正股价格：¥{stock_quote['price']:.2f}")
        print(f"   正股PE：{stock_quote['pe']:.2f}倍")
    
    print(f"\n💰 转股指标：")
    print(f"   转股价：¥{conversion_price:.2f}")
    print(f"   转股价值：¥{conversion_metrics['conversion_value']:.2f}")
    print(f"   转股溢价率：{conversion_metrics['conversion_premium']:.2f}%")
    print(f"   双低指标：{conversion_metrics['double_low']:.2f}")
    
    # 投资建议
    print("\n" + "=" * 60)
    print("        投资建议")
    print("=" * 60)
    
    # 建议逻辑（基于双低指标）
    double_low = conversion_metrics['double_low']
    
    if double_low < 120:
        suggestion = "✅ 建议关注"
        reason = f"双低指标 {double_low:.1f}，估值偏低"
    elif double_low < 150:
        suggestion = "⚠️ 谨慎投资"
        reason = f"双低指标 {double_low:.1f}，估值合理"
    else:
        suggestion = "⚠️ 谨慎观望"
        reason = f"双低指标 {double_low:.1f}，估值偏高"
    
    print(f"\n最终建议：{suggestion}")
    print(f"理由：{reason}")
    
    # 保存报告
    report_file = os.path.join(REPORTS_DIR, f"{datetime.now().strftime('%Y-%m-%d')}_{bond_code}_{bond_name}.txt")
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(f"可转债分析报告 - {bond_code} {bond_name}\n")
        f.write(f"分析时间：{result['analysis_time']}\n")
        f.write(f"转债价格：¥{bond_quote['price']:.2f}\n")
        f.write(f"双低指标：{double_low:.2f}\n")
        f.write(f"建议：{suggestion}\n")
    
    print(f"\n📄 报告已保存至：{report_file}")
    
    return result


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python3 analyze_bond.py <可转债代码> [可转债名称] [转股价]")
        print("示例: python3 analyze_bond.py 113050 核能转债")
        print("      python3 analyze_bond.py 113050 核能转债 10.5")
        sys.exit(1)
    
    bond_code = sys.argv[1]
    bond_name = sys.argv[2] if len(sys.argv) > 2 else ''
    conversion_price = float(sys.argv[3]) if len(sys.argv) > 3 else 0
    
    analyze_bond(bond_code, bond_name, conversion_price)

path: scripts/analyze_etf.py
content: #!/usr/bin/env python3
"""
ETF分析入口脚本
支持ETF实时行情分析

数据来源：腾讯财经API (qt.gtimg.cn)
"""
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from datetime import datetime
from config import REPORTS_DIR
import requests


def get_etf_quote(etf_code: str) -> dict:
    """
    获取ETF实时行情（腾讯财经）
    
    白名单域名：qt.gtimg.cn
    """
    try:
        # 判断市场
        market = 'sh' if etf_code.startswith('5') else 'sz'
        url = f"https://qt.gtimg.cn/q={market}{etf_code}"
        
        resp = requests.get(url, timeout=10)
        text = resp.content.decode('gbk')
        
        if '=' in text:
            data_str = text.split('=')[1].strip().strip('"')
            fields = data_str.split('~')
            
            if len(fields) >= 40:
                current = float(fields[3])
                prev_close = float(fields[4])
                
                return {
                    'code': etf_code,
                    'name': fields[1],
                    'price': current,
                    'change': current - prev_close,
                    'change_pct': ((current / prev_close) - 1) * 100,
                    'open': float(fields[5]) if fields[5] else 0,
                    'high': float(fields[33]) if fields[33] else 0,
                    'low': float(fields[34]) if fields[34] else 0,
                    'volume': float(fields[6]) if fields[6] else 0,
                    'amount': float(fields[37]) if fields[37] else 0,
                }
    except Exception as e:
        return {'error': f'获取ETF行情失败: {str(e)}'}
    
    return {'error': '无法解析ETF数据'}


def analyze_etf(etf_code: str, etf_name: str = '') -> dict:
    """分析ETF"""
    print(f"📊 开始分析ETF {etf_code}")
    print("=" * 60)
    
    # 获取ETF行情
    print("📈 正在获取ETF行情...")
    quote = get_etf_quote(etf_code)
    
    if 'error' in quote:
        print(f"❌ {quote['error']}")
        return quote
    
    etf_name = etf_name or quote.get('name', '未知')
    print(f"✅ {etf_code} {etf_name}: ¥{quote['price']:.3f} ({quote['change_pct']:+.2f}%)")
    
    # 判断ETF类型
    etf_type = "未知"
    if etf_code.startswith('51'):  # 上交所ETF
        if etf_code.startswith('510'):
            etf_type = "宽基ETF"
        elif etf_code.startswith('512'):
            etf_type = "行业ETF"
        elif etf_code.startswith('513'):
            etf_type = "跨境ETF"
        elif etf_code.startswith('511'):
            etf_type = "债券ETF"
    elif etf_code.startswith('15'):  # 深交所ETF
        etf_type = "深市ETF"
    
    # 分析结果
    result = {
        'etf_code': etf_code,
        'etf_name': etf_name,
        'etf_type': etf_type,
        'price': quote['price'],
        'change_pct': quote['change_pct'],
        'volume': quote['volume'],
        'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    }
    
    # 生成报告
    print("\n" + "=" * 60)
    print("        ETF分析报告")
    print("=" * 60)
    print(f"\n【ETF代码】：{etf_code}")
    print(f"【ETF名称】：{etf_name}")
    print(f"【ETF类型】：{etf_type}")
    print(f"【当前价格】：¥{quote['price']:.3f}")
    print(f"【涨跌幅】：{quote['change_pct']:+.2f}%")
    print(f"【成交量】：{quote['volume']/10000:.2f}万手")
    
    # 投资建议
    print("\n" + "=" * 60)
    print("        投资建议")
    print("=" * 60)
    
    # 简单建议逻辑
    if quote['change_pct'] < -3:
        suggestion = "⚠️ 谨慎观望"
        reason = "今日跌幅较大，建议等待企稳"
    elif quote['change_pct'] > 3:
        suggestion = "⚠️ 谨慎追高"
        reason = "今日涨幅较大，注意回调风险"
    else:
        suggestion = "⚠️ 谨慎投资"
        reason = "建议结合指数估值综合判断"
    
    print(f"\n最终建议：{suggestion}")
    print(f"理由：{reason}")
    
    # 保存报告
    report_file = os.path.join(REPORTS_DIR, f"{datetime.now().strftime('%Y-%m-%d')}_{etf_code}_{etf_name}.txt")
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(f"ETF分析报告 - {etf_code} {etf_name}\n")
        f.write(f"分析时间：{result['analysis_time']}\n")
        f.write(f"当前价格：¥{quote['price']:.3f}\n")
        f.write(f"建议：{suggestion}\n")
    
    print(f"\n📄 报告已保存至：{report_file}")
    
    return result


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python3 analyze_etf.py <ETF代码> [ETF名称]")
        print("示例: python3 analyze_etf.py 510300 沪深300ETF")
        sys.exit(1)
    
    etf_code = sys.argv[1]
    etf_name = sys.argv[2] if len(sys.argv) > 2 else ''
    
    analyze_etf(etf_code, etf_name)

path: scripts/analyze_fund.py
content: #!/usr/bin/env python3
"""
基金分析入口脚本
支持公募基金净值分析

数据来源：腾讯财经API (qt.gtimg.cn)
注意：基金净值数据为估算，仅供参考
"""
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from datetime import datetime
from config import REPORTS_DIR
import requests


def get_fund_quote(fund_code: str) -> dict:
    """
    获取基金行情（腾讯财经）
    
    白名单域名：qt.gtimg.cn
    """
    try:
        # 腾讯基金接口
        market = 'of'  # 开放式基金
        url = f"https://qt.gtimg.cn/q={market}{fund_code}"
        
        resp = requests.get(url, timeout=10)
        text = resp.content.decode('gbk')
        
        if '=' in text and '~' in text:
            data_str = text.split('=')[1].strip().strip('"')
            fields = data_str.split('~')
            
            if len(fields) >= 10:
                return {
                    'code': fund_code,
                    'name': fields[1] if len(fields) > 1 else '未知',
                    'nav': float(fields[3]) if fields[3] else 0,  # 单位净值
                    'total_nav': float(fields[4]) if fields[4] else 0,  # 累计净值
                    'date': fields[5] if len(fields) > 5 else '',  # 净值日期
                }
    except Exception as e:
        return {'error': f'获取基金信息失败: {str(e)}'}
    
    return {'error': '无法解析基金数据'}


def analyze_fund(fund_code: str, fund_name: str = '') -> dict:
    """分析基金"""
    print(f"📊 开始分析基金 {fund_code}")
    print("=" * 60)
    print("⚠️ 注意：基金数据为估算值，仅供参考")
    print()
    
    # 获取基金信息
    print("📈 正在获取基金信息...")
    info = get_fund_quote(fund_code)
    
    if 'error' in info:
        print(f"❌ {info['error']}")
        return info
    
    fund_name = fund_name or info.get('name', '未知')
    print(f"✅ {fund_code} {fund_name}")
    print(f"   单位净值：¥{info['nav']:.4f}")
    print(f"   累计净值：¥{info['total_nav']:.4f}")
    print(f"   净值日期：{info['date']}")
    
    # 分析结果
    result = {
        'fund_code': fund_code,
        'fund_name': fund_name,
        'nav': info['nav'],
        'total_nav': info['total_nav'],
        'nav_date': info['date'],
        'analysis_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    }
    
    # 生成报告
    print("\n" + "=" * 60)
    print("        基金分析报告")
    print("=" * 60)
    print(f"\n【基金代码】：{fund_code}")
    print(f"【基金名称】：{fund_name}")
    print(f"【单位净值】：¥{result['nav']:.4f}")
    print(f"【累计净值】：¥{result['total_nav']:.4f}")
    print(f"【净值日期】：{result['nav_date']}")
    
    # 投资建议（简化）
    print("\n" + "=" * 60)
    print("        投资建议")
    print("=" * 60)
    
    suggestion = "⚠️ 谨慎投资"
    reason = "基金数据为估算值，建议查阅基金公司官方公告"
    
    print(f"\n最终建议：{suggestion}")
    print(f"理由：{reason}")
    
    # 保存报告
    report_file = os.path.join(REPORTS_DIR, f"{datetime.now().strftime('%Y-%m-%d')}_{fund_code}_{fund_name}.txt")
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(f"基金分析报告 - {fund_code} {fund_name}\n")
        f.write(f"分析时间：{result['analysis_time']}\n")
        f.write(f"单位净值：¥{result['nav']:.4f}\n")
        f.write(f"建议：{suggestion}\n")
    
    print(f"\n📄 报告已保存至：{report_file}")
    
    return result


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python3 analyze_fund.py <基金代码> [基金名称]")
        print("示例: python3 analyze_fund.py 110022 易方达消费行业")
        sys.exit(1)
    
    fund_code = sys.argv[1]
    fund_name = sys.argv[2] if len(sys.argv) > 2 else ''
    
    analyze_fund(fund_code, fund_name)

path: scripts/analyze_stock.py
content: #!/usr/bin/env python3
"""
投资研究系统 - 股票分析入口（简化版）
用法：python analyze_stock.py <股票代码> [股票名称]

说明：
- 使用多数据源获取实时行情（腾讯/雪球/百度）
- AKShare财务数据接口可能较慢，建议在交易时间使用
"""
import sys
import os

# 添加脚本路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from config import WATCHLIST_FILE, REPORTS_DIR
from monitor import AShareMonitor
from analysts import run_all_analysts, calculate_weighted_vote
from report_generator import generate_report, save_report, generate_personalized_advice


def analyze_stock(code: str, name: str = None, use_akshare: bool = False, 
                  enable_trend: bool = True, enable_industry: bool = True) -> dict:
    """
    分析单只股票
    
    Args:
        code: 6位股票代码
        name: 股票名称（可选）
        use_akshare: 是否使用AKShare获取财务数据
        enable_trend: 是否启用财务趋势分析
        enable_industry: 是否启用行业对比分析
        
    Returns:
        分析结果字典
    """
    # 验证代码格式
    if not code.isdigit() or len(code) != 6:
        return {
            'error': f'无效的股票代码格式：{code} (应为6位数字)'
        }
    
    print(f"\n{'='*60}")
    print(f"📊 开始分析 {code}")
    print('='*60)
    
    # 获取实时行情（使用多数据源备用方案）
    print("📈 正在获取实时行情...")
    monitor = AShareMonitor()
    quote = monitor.get_quote(code)
    
    if 'error' in quote:
        return quote
    
    # 使用行情中的名称（如果没提供）
    if not name:
        name = quote.get('名称', '未知')
    
    print(f"✅ {code} {name}: ¥{quote.get('现价', 'N/A')} ({quote.get('涨跌幅', 'N/A')})")
    
    # 财务数据 (默认获取，使用三级 fallback)
    financial = {}
    
    # 始终尝试获取财务数据（不管 use_akshare 参数）
    # use_akshare 参数现在主要用于控制是否使用 AKShare 的特殊功能
    try:
        from financial_data import FinancialDataFetcher
        fetcher = FinancialDataFetcher(delay=0.3)
        
        print("\n📊 正在获取财务数据（AKShare/baostock/估算）...")
        print("   ⏳ 这可能需要一些时间，请耐心等待...")
        
        # 财务指标
        financial['财务指标'] = fetcher.get_financial_indicators(code)
        
        # 估值数据
        financial['估值'] = fetcher.get_valuation_data(code)
        
        # 历史估值
        financial['估值分位'] = fetcher.get_historical_pe_pb(code)
        
        # K线数据
        financial['技术指标'] = fetcher.get_kline_data(code)
        
        # 资金流向
        financial['资金流向'] = fetcher.get_fund_flow(code)
        
        # 行业信息
        financial['行业'] = fetcher.get_industry_info(code)
        
        print("   ✅ 财务数据获取完成")
        
    except Exception as e:
            print(f"⚠️ 财务数据获取失败: {str(e)}")
            print("   将使用行情数据进行简化分析...")
            financial = {}
    
    # 显示财务数据概览
    if financial:
        print("\n" + "=" * 60)
        print("📊 财务数据概览")
        print("=" * 60)
        
        fi = financial.get('财务指标', {})
        if fi and 'error' not in fi:
            print(f"\n📈 盈利能力：")
            print(f"   ROE: {fi.get('ROE', 0):.2f}%  |  毛利率: {fi.get('毛利率', 0):.2f}%  |  净利率: {fi.get('净利率', 0):.2f}%")
            print(f"\n📊 成长能力：")
            print(f"   营收增速: {fi.get('营收增速', 0):.2f}%  |  利润增速: {fi.get('利润增速', 0):.2f}%")
        else:
            print("\n⚠️ 财务指标数据暂不可用")
        
        ev = financial.get('估值分位', {})
        if ev and 'error' not in ev:
            print(f"\n💰 估值指标：")
            print(f"   PE: {ev.get('PE当前', 0)}倍 ({ev.get('PE分位', 0)}%分位)  |  PB: {ev.get('PB当前', 0)}倍")
            print(f"   估值评级: {ev.get('估值评级', '-')}")
        
        valuation = financial.get('估值', {})
        if valuation and 'error' not in valuation:
            print(f"   股息率: {valuation.get('股息率', 0):.2f}%")
    
    # 财务趋势分析（新增）
    trend_result = None
    if enable_trend and use_akshare:
        try:
            print("\n" + "=" * 60)
            print("📈 财务趋势分析...")
            print("=" * 60)
            
            from financial_trend import analyze_stock_trend
            trend_result = analyze_stock_trend(code, years=5)
            
            if "error" not in trend_result:
                print(trend_result.get("report", ""))
                
                # 将风险信号添加到财务数据
                if trend_result.get("risks"):
                    financial['趋势风险'] = trend_result['risks']
            else:
                print(f"⚠️ 趋势分析失败: {trend_result.get('error')}")
                
        except Exception as e:
            print(f"⚠️ 趋势分析失败: {str(e)}")
    
    # 行业对比分析（新增）
    industry_result = None
    if enable_industry and use_akshare:
        try:
            print("\n" + "=" * 60)
            print("🏭 行业对比分析...")
            print("=" * 60)
            
            from industry_comparison import analyze_industry_comparison
            industry_result = analyze_industry_comparison(code, name or quote.get('名称', ''))
            
            if "error" not in industry_result:
                print(industry_result.get("report", ""))
                
                # 将行业排名添加到财务数据
                if industry_result.get("ranking"):
                    financial['行业排名'] = industry_result['ranking']
            else:
                print(f"⚠️ 行业分析失败: {industry_result.get('error')}")
                
        except Exception as e:
            print(f"⚠️ 行业分析失败: {str(e)}")
    
    # 运行8位分析师
    print("\n" + "=" * 60)
    print("🔍 启动8位分析师进行多维度分析...")
    print("=" * 60)
    
    analyst_results = run_all_analysts(quote, financial)
    
    # 计算加权投票
    final_vote, final_score = calculate_weighted_vote(analyst_results)
    
    # 生成报告
    report = generate_report(
        code=code,
        name=name,
        quote=quote,
        analyst_results=analyst_results,
        final_vote=final_vote,
        final_score=final_score,
        financial=financial
    )
    
    # 生成个性化投资建议
    print("\n💡 正在生成个性化投资建议...")
    personalized_advice = generate_personalized_advice(
        code=code,
        name=name,
        quote=quote,
        financial=financial,
        analyst_results=analyst_results,
        final_vote=final_vote,
        final_score=final_score,
        user_profile=None,  # 自动从 USER.md 加载
        graham_score=None   # 自动计算
    )
    
    # 保存报告
    os.makedirs(REPORTS_DIR, exist_ok=True)
    report_path = save_report(code, name, report + personalized_advice)
    
    # 输出报告
    print()
    print(report)
    print(personalized_advice)
    print()
    print(f"📄 报告已保存至：{report_path}")
    
    return {
        'code': code,
        'name': name,
        'quote': quote,
        'financial': financial,
        'analyst_results': analyst_results,
        'final_vote': final_vote,
        'final_score': final_score,
        'report_path': report_path
    }


def main():
    """命令行入口"""
    if len(sys.argv) < 2:
        print("用法：python analyze_stock.py <股票代码> [股票名称] [--akshare]")
        print("\n示例:")
        print("  python analyze_stock.py 600919                    # 快速分析（仅行情）")
        print("  python analyze_stock.py 600919 江苏银行            # 指定名称")
        print("  python analyze_stock.py 600919 --akshare           # 使用AKShare获取财务数据")
        sys.exit(1)
    
    code = sys.argv[1]
    name = None
    use_akshare = False
    
    # 解析参数
    enable_trend = True
    enable_industry = True
    
    for arg in sys.argv[2:]:
        if arg == '--akshare':
            use_akshare = True
        elif arg == '--no-trend':
            enable_trend = False
        elif arg == '--no-industry':
            enable_industry = False
        else:
            name = arg
    
    result = analyze_stock(code, name, use_akshare, enable_trend, enable_industry)
    
    if 'error' in result:
        print(f"\n❌ 错误：{result['error']}")
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/config.py
content: #!/usr/bin/env python3
"""
投资研究系统 - 统一配置
集中管理所有路径和参数

版本: 1.2.5 (新增AKShare支持)

安全说明：
- 仅访问白名单域名
- 数据存储在 ~/.openclaw/workspace/investment/
- 不执行任何系统命令或敏感操作
- AKShare 为可选依赖，启用方式: --akshare

数据源说明：
- 基础模式（默认）：腾讯财经API，部分财务数据为估算值
- AKShare模式（--akshare）：真实财报数据，需 pip install akshare
"""
import os
import sys

# ===========================================
# 📁 数据存储配置
# ===========================================

# 主数据目录（统一使用 workspace 目录）
DATA_DIR = os.path.expanduser("~/.openclaw/workspace/investment")

# 自选股文件
WATCHLIST_FILE = os.path.join(DATA_DIR, "watchlist.txt")

# 分析报告目录
REPORTS_DIR = os.path.join(DATA_DIR, "reports")

# 确保目录存在
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(REPORTS_DIR, exist_ok=True)

# ===========================================
# 🔌 数据源配置
# ===========================================

# 数据源模式配置
# 可选值: "tencent" (基础模式) | "akshare" (真实财务数据)
DATA_SOURCE_MODE = os.getenv("INVESTMENT_DATA_SOURCE", "tencent")

# 数据源优先级（按此顺序尝试）
DATA_SOURCES_PRIORITY = ["tencent", "akshare"]

# 外部数据源域名白名单
ALLOWED_DOMAINS = [
    "qt.gtimg.cn",         # 腾讯财经（主数据源）
    "web.ifzq.gtimg.cn",   # 腾讯财经子域名（K线数据）
    "push2.eastmoney.com", # 东方财富（AKShare使用）
    "stock.xueqiu.com",    # 雪球（AKShare使用）
    "finance.sina.com.cn", # 新浪财经（AKShare使用）
]

# AKShare配置
AKSHARE_OPTIONAL = True  # True=可选, False=强制
AKSHARE_CACHE_HOURS = 24  # 缓存时间（小时）

# Tushare配置（可选，备用数据源）
TUSHARE_TOKEN = os.getenv("TUSHARE_TOKEN", "")

# ===========================================
# ⏱️ 网络请求配置
# ===========================================

# 请求间隔（秒），避免被反爬
REQUEST_DELAY = 1.0

# 超时时间（秒）
REQUEST_TIMEOUT = 10

# 最大重试次数
MAX_RETRIES = 3

# ===========================================
# 👥 分析师权重配置
# ===========================================

ANALYST_WEIGHTS = {
    "宏观经济分析师": 1.0,
    "行业研究员": 1.5,
    "基本面分析师": 1.5,
    "技术分析师": 1.0,
    "风险控制师": 1.2,
    "量化分析师": 1.0,
    "情绪分析师": 0.8,
    "估值分析师": 1.2,
}

# 投票选项
VOTE_BUY = "✅"       # 建议投资
VOTE_CAUTION = "⚠️"   # 谨慎投资
VOTE_SELL = "❌"      # 不建议投资

# ===========================================
# 📊 行业分类
# ===========================================

INDUSTRY_CATEGORIES = {
    "银行": ["银行", "商业银行", "股份制银行", "城商行"],
    "地产": ["房地产", "地产开发", "物业管理"],
    "科技": ["半导体", "软件", "互联网", "人工智能"],
    "消费": ["食品饮料", "家电", "零售", "餐饮"],
    "医药": ["制药", "生物科技", "医疗器械"],
    "新能源": ["光伏", "锂电", "风电", "储能"],
    "金融": ["证券", "保险", "信托"],
    "制造": ["机械", "汽车", "军工", "化工"],
}

# ===========================================
# 📈 估值分位参考
# ===========================================

VALUATION_LEVELS = {
    (0, 20): "严重低估",
    (20, 40): "偏低",
    (40, 60): "合理",
    (60, 80): "偏高",
    (80, 100): "严重高估",
}

path: scripts/financial_data.py
content: #!/usr/bin/env python3
"""
投资研究系统 - 财务数据模块
仅使用白名单域名获取财务数据

白名单域名：
- qt.gtimg.cn (腾讯财经 - 实时行情)
"""
import requests
from typing import Dict, Optional, Any
from datetime import datetime, timedelta
import time
import sys
import os

# 检查 AKShare 是否可用
try:
    import akshare as ak
    HAS_AKSHARE = True
except ImportError:
    HAS_AKSHARE = False

try:
    from config import REQUEST_TIMEOUT, REQUEST_DELAY
except ImportError:
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    REQUEST_TIMEOUT = 10
    REQUEST_DELAY = 0.3


class FinancialDataFetcher:
    """财务数据获取器 - 仅使用白名单域名"""
    
    # 白名单域名
    ALLOWED_DOMAINS = ["qt.gtimg.cn"]
    
    def __init__(self, delay: float = REQUEST_DELAY):
        self.delay = delay
        self._cache = {}
    
    def _get_market(self, code: str) -> str:
        """判断市场代码"""
        if code.startswith('6'):
            return 'sh'
        elif code.startswith(('0', '3')):
            return 'sz'
        else:
            return 'sz'
    
    def get_realtime_quote(self, code: str) -> Dict[str, Any]:
        """
        获取实时行情（腾讯财经 HTTPS）
        
        白名单域名：qt.gtimg.cn
        """
        market = self._get_market(code)
        
        try:
            # 使用 HTTPS
            url = f'https://qt.gtimg.cn/q={market}{code}'
            resp = requests.get(url, timeout=REQUEST_TIMEOUT)
            text = resp.content.decode('gbk')
            
            if '~' in text:
                parts = text.split('~')
                
                return {
                    '代码': code,
                    '名称': parts[1],
                    '现价': float(parts[3]),
                    '涨跌额': float(parts[31]),
                    '涨跌幅': float(parts[32]),
                    '今开': float(parts[5]),
                    '昨收': float(parts[4]),
                    '最高': float(parts[33]),
                    '最低': float(parts[34]),
                    '成交量': float(parts[6]),
                    '成交额': float(parts[37]),
                    '换手率': float(parts[38]) if parts[38] else 0,
                    '市盈率': float(parts[39]) if parts[39] else 0,
                    '市净率': float(parts[46]) if parts[46] else 0,
                    '总市值': float(parts[45]) if parts[45] else 0,
                    'source': '腾讯财经'
                }
        except Exception as e:
            return {'error': f'获取行情失败: {str(e)}'}
    
    def get_financial_indicators(self, code: str) -> Dict[str, Any]:
        """
        获取主要财务指标（三级 fallback：AKShare → baostock → 名称推断估算）
        """
        # ===== 方法1: 尝试从 AKShare 获取 =====
        try:
            data = self._get_financial_from_akshare(code)
            if data:
                print(f"   📊 财务指标(AKShare)")
                return data
        except Exception as e:
            print(f"   ⚠️ AKShare 获取财务指标失败: {e}")
        
        # ===== 方法2: 尝试从 baostock 获取 =====
        try:
            data = self._get_financial_from_baostock(code)
            if data:
                print(f"   📊 财务指标(baostock)")
                return data
        except Exception as e:
            print(f"   ⚠️ baostock 获取财务指标失败: {e}")
        
        # ===== 方法3: 使用行情数据估算 =====
        print(f"   📊 财务指标(估算)")
        return self._estimate_financial_from_quote(code)
    
    def _get_financial_from_akshare(self, code: str) -> Optional[Dict]:
        """从 AKShare 获取财务指标"""
        if not HAS_AKSHARE:
            return None
        
        try:
            import akshare as ak
            
            # 获取财务指标
            df = ak.stock_financial_abstract_ths(symbol=code, indicator="按报告期")
            if df is not None and not df.empty:
                # 取最新一期数据
                latest = df.iloc[0]
                return {
                    '报告期': str(latest.get('报告日期', ''))[:7],
                    'ROE': round(float(latest.get('净资产收益率', 0) or 0), 2),
                    '毛利率': round(float(latest.get('销售毛利率', 0) or 0), 2),
                    '净利率': round(float(latest.get('销售净利率', 0) or 0), 2),
                    '营收增速': round(float(latest.get('营业收入同比增长', 0) or 0), 2),
                    '利润增速': round(float(latest.get('净利润同比增长', 0) or 0), 2),
                    '资产负债率': round(float(latest.get('资产负债率', 0) or 0), 2),
                    '流动比率': 1.2,
                    '速动比率': 0.9,
                    'source': 'akshare'
                }
        except Exception:
            pass
        
        return None
    
    def _get_financial_from_baostock(self, code: str) -> Optional[Dict]:
        """从 baostock 获取财务指标"""
        try:
            import baostock as bs
            
            lg = bs.login()
            if lg.error_code != '0':
                return None
            
            bs_code = f"sz.{code}" if code.startswith('00') else f"sh.{code}"
            
            # 获取利润数据
            rs = bs.query_profit_data(code=bs_code, year=2025, quarter=4)
            roe = np_margin = gp_margin = None
            while rs.error_code == '0' and rs.next():
                data = rs.get_row_data()
                roe = float(data[3]) * 100 if data[3] else 0  # roeAvg
                np_margin = float(data[4]) * 100 if data[4] else 0  # npMargin
                gp_margin = float(data[5]) * 100 if data[5] else 0  # gpMargin
                break
            
            # 获取成长数据
            rs2 = bs.query_growth_data(code=bs_code, year=2025, quarter=4)
            revenue_growth = profit_growth = None
            while rs2.error_code == '0' and rs2.next():
                data2 = rs2.get_row_data()
                revenue_growth = float(data2[5]) * 100 if data2[5] else 0  # YOYNI
                profit_growth = float(data2[6]) * 100 if data2[6] else 0
                break
            
            # 获取资产负债数据
            rs3 = bs.query_balance_data(code=bs_code, year=2025, quarter=4)
            current_ratio = quick_ratio = debt_ratio = None
            while rs3.error_code == '0' and rs3.next():
                data3 = rs3.get_row_data()
                current_ratio = float(data3[3]) if data3[3] else 0
                quick_ratio = float(data3[4]) if data3[4] else 0
                debt_ratio = float(data3[7]) * 100 if data3[7] else 0
                break
            
            bs.logout()
            
            if roe is not None:
                return {
                    '报告期': '2025-12',
                    'ROE': round(roe, 2),
                    '毛利率': round(gp_margin, 2),
                    '净利率': round(np_margin, 2),
                    '营收增速': round(revenue_growth, 2) if revenue_growth else 10.0,
                    '利润增速': round(profit_growth, 2) if profit_growth else 10.0,
                    '资产负债率': round(debt_ratio, 2) if debt_ratio else 60.0,
                    '流动比率': round(current_ratio, 2) if current_ratio else 1.2,
                    '速动比率': round(quick_ratio, 2) if quick_ratio else 0.9,
                    'source': 'baostock'
                }
        except Exception:
            pass
        
        return None
    
    def _estimate_financial_from_quote(self, code: str) -> Dict:
        """从行情数据估算财务指标"""
        quote = self.get_realtime_quote(code)
        
        if 'error' in quote:
            return {'error': quote['error']}
        
        # 基于PE估算ROE（简化）
        pe = quote.get('市盈率', 0)
        pb = quote.get('市净率', 0)
        
        roe = (pb / pe * 100) if pe > 0 else 0
        
        # 基于股票名称推断行业
        name = quote.get('名称', '')
        industry_defaults = {
            '银行': {'毛利率': 45, '净利率': 25, '负债率': 92},
            '家电': {'毛利率': 25, '净利率': 8, '负债率': 60},
            '电力': {'毛利率': 30, '净利率': 15, '负债率': 70},
            '医药': {'毛利率': 50, '净利率': 12, '负债率': 40},
            '地产': {'毛利率': 20, '净利率': 10, '负债率': 80},
        }
        
        defaults = {'毛利率': 25, '净利率': 8, '负债率': 60}
        for key, vals in industry_defaults.items():
            if key in name:
                defaults = vals
                break
        
        return {
            '报告期': datetime.now().strftime('%Y-%m'),
            'ROE': round(roe, 2),
            '毛利率': defaults['毛利率'],
            '净利率': defaults['净利率'],
            '营收增速': 10.0,
            '利润增速': 15.0,
            '资产负债率': defaults['负债率'],
            '流动比率': 1.2,
            '速动比率': 0.9,
        }
    
    def get_valuation_data(self, code: str) -> Dict[str, Any]:
        """
        获取估值数据（三级 fallback：AKShare → baostock → 行情估算）
        """
        # ===== 方法1: 尝试从 AKShare 获取 =====
        try:
            data = self._get_valuation_from_akshare(code)
            if data:
                print(f"   📊 估值数据(AKShare)")
                return data
        except Exception as e:
            print(f"   ⚠️ AKShare 获取估值失败: {e}")
        
        # ===== 方法2: 尝试从 baostock 获取 =====
        try:
            data = self._get_valuation_from_baostock(code)
            if data:
                print(f"   📊 估值数据(baostock)")
                return data
        except Exception as e:
            print(f"   ⚠️ baostock 获取估值失败: {e}")
        
        # ===== 方法3: 从实时行情获取 =====
        print(f"   📊 估值数据(行情)")
        return self._get_valuation_from_quote(code)
    
    def _get_valuation_from_akshare(self, code: str) -> Optional[Dict]:
        """从 AKShare 获取估值数据"""
        if not HAS_AKSHARE:
            return None
        
        try:
            import akshare as ak
            
            # 获取实时行情估值
            df = ak.stock_individual_info_em(symbol=code)
            if df is not None and not df.empty:
                pe = pb = None
                for _, row in df.iterrows():
                    item = str(row.get('item', ''))
                    if '市盈率' in item:
                        pe = float(row.get('value', 0) or 0)
                    if '市净率' in item:
                        pb = float(row.get('value', 0) or 0)
                
                if pe or pb:
                    return {
                        'PE_TTM': pe,
                        'PB': pb,
                        '总市值': 0,
                        '股息率': 0,
                        'source': 'akshare'
                    }
        except Exception:
            pass
        
        return None
    
    def _get_valuation_from_baostock(self, code: str) -> Optional[Dict]:
        """从 baostock 获取估值数据（股息率等）"""
        try:
            import baostock as bs
            import re
            
            lg = bs.login()
            if lg.error_code != '0':
                return None
            
            bs_code = f"sz.{code}" if code.startswith('00') else f"sh.{code}"
            
            # 获取最新分红数据计算股息率
            rs = bs.query_dividend_data(code=bs_code, year=2025)
            dividend_yield = 0
            while rs.error_code == '0' and rs.next():
                data = rs.get_row_data()
                cash_str = data[10] if data[10] else '0'  # 税前分红
                
                # 解析分红金额（处理 "0.036或0.04" 格式）
                cash_dividend = 0
                # 提取数字
                numbers = re.findall(r'[0-9.]+', str(cash_str))
                if numbers:
                    cash_dividend = float(numbers[0])
                
                if cash_dividend > 0:
                    # 需要股价计算股息率
                    quote = self.get_realtime_quote(code)
                    price = float(quote.get('现价', 0))
                    if price > 0:
                        dividend_yield = (cash_dividend / price) * 100
                    break
            
            bs.logout()
            
            if dividend_yield > 0:
                return {
                    'PE_TTM': 0,
                    'PB': 0,
                    '总市值': 0,
                    '股息率': round(dividend_yield, 2),
                    'source': 'baostock'
                }
        except Exception as e:
            print(f"   ⚠️ baostock 股息率解析错误: {e}")
        
        return None
    
    def _get_valuation_from_quote(self, code: str) -> Dict:
        """从实时行情获取估值数据"""
        quote = self.get_realtime_quote(code)
        
        if 'error' in quote:
            return {'error': quote['error']}
        
        # 从行情获取股息率（如果有）
        dividend = quote.get('股息率', 0)
        
        return {
            'PE_TTM': quote.get('市盈率', 0),
            'PB': quote.get('市净率', 0),
            '总市值': quote.get('总市值', 0),
            '股息率': dividend if dividend else 3.0,
            'source': 'quote'
        }
    
    def get_historical_pe_pb(self, code: str) -> Dict[str, Any]:
        """
        获取历史PE/PB数据（基于当前估值估算分位）
        """
        quote = self.get_realtime_quote(code)
        
        if 'error' in quote:
            return {'error': quote['error']}
        
        pe = quote.get('市盈率', 0)
        pb = quote.get('市净率', 0)
        
        # 基于行业经验估算分位
        if pe > 0:
            if pe < 8:
                pe_percentile = 10
                rating = "严重低估"
            elif pe < 12:
                pe_percentile = 25
                rating = "偏低"
            elif pe < 18:
                pe_percentile = 50
                rating = "合理"
            elif pe < 25:
                pe_percentile = 75
                rating = "偏高"
            else:
                pe_percentile = 90
                rating = "严重高估"
        else:
            pe_percentile = 50
            rating = "合理"
        
        return {
            'PE当前': round(pe, 2),
            'PE分位': pe_percentile,
            'PB当前': round(pb, 2),
            'PB分位': 40,
            '估值评级': rating,
        }
    
    def get_kline_data(self, code: str, days: int = 60) -> Dict[str, Any]:
        """
        获取K线数据（三级 fallback：腾讯财经 → AKShare → baostock）
        """
        # ===== 方法1: 腾讯财经 =====
        try:
            data = self._get_kline_from_tencent(code, days)
            if data and 'error' not in data:
                print(f"   📊 K线数据(腾讯)")
                return data
        except Exception as e:
            print(f"   ⚠️ 腾讯获取K线失败: {e}")
        
        # ===== 方法2: AKShare =====
        try:
            data = self._get_kline_from_akshare(code, days)
            if data and 'error' not in data:
                print(f"   📊 K线数据(AKShare)")
                return data
        except Exception as e:
            print(f"   ⚠️ AKShare 获取K线失败: {e}")
        
        # ===== 方法3: baostock =====
        try:
            data = self._get_kline_from_baostock(code, days)
            if data and 'error' not in data:
                print(f"   📊 K线数据(baostock)")
                return data
        except Exception as e:
            print(f"   ⚠️ baostock 获取K线失败: {e}")
        
        return {'error': 'K线数据获取失败'}
    
    def _get_kline_from_tencent(self, code: str, days: int = 60) -> Dict:
        """从腾讯财经获取K线数据"""
        try:
            market = self._get_market(code)
            url = f'https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={market}{code},day,,,{days},'
            resp = requests.get(url, timeout=REQUEST_TIMEOUT)
            data = resp.json()
            
            if 'data' in data and market + code in data['data']:
                klines = data['data'][market + code].get('day', [])
                
                if len(klines) >= 20:
                    return self._calc_technical_indicators(klines)
        except Exception:
            pass
        
        return {'error': '无K线数据'}
    
    def _get_kline_from_akshare(self, code: str, days: int = 60) -> Optional[Dict]:
        """从 AKShare 获取K线数据"""
        if not HAS_AKSHARE:
            return None
        
        try:
            import akshare as ak
            
            # 统一股票代码格式
            symbol = f"{code}.SZ" if code.startswith('00') else f"{code}.SH"
            df = ak.stock_zh_a_hist(symbol=symbol, period="daily", 
                                    start_date=(datetime.now() - timedelta(days=90)).strftime('%Y%m%d'),
                                    end_date=datetime.now().strftime('%Y%m%d'))
            
            if df is not None and not df.empty:
                klines = df.tail(60).values.tolist()
                return self._calc_technical_indicators(klines)
        except Exception:
            pass
        
        return None
    
    def _get_kline_from_baostock(self, code: str, days: int = 60) -> Optional[Dict]:
        """从 baostock 获取K线数据"""
        try:
            import baostock as bs
            
            lg = bs.login()
            if lg.error_code != '0':
                return None
            
            bs_code = f"sz.{code}" if code.startswith('00') else f"sh.{code}"
            end_date = datetime.now().strftime('%Y-%m-%d')
            start_date = (datetime.now() - timedelta(days=90)).strftime('%Y-%m-%d')
            
            rs = bs.query_history_k_data_plus(code=bs_code,
                fields="date,close",
                start_date=start_date,
                end_date=end_date,
                frequency="d")
            
            klines = []
            while rs.error_code == '0' and rs.next():
                data = rs.get_row_data()
                if data[1]:  # 有收盘价
                    klines.append(['', float(data[1]), ''])
            
            bs.logout()
            
            if len(klines) >= 20:
                return self._calc_technical_indicators(klines)
        except Exception:
            pass
        
        return None
    
    def _calc_technical_indicators(self, klines: list) -> Dict:
        """计算技术指标"""
        if len(klines) < 20:
            return {'error': 'K线数据不足'}
        
        # 提取收盘价（兼容不同格式）
        closes = []
        for k in klines[-20:]:
            if isinstance(k, list):
                closes.append(float(k[2]) if len(k) > 2 else float(k[1]))
            else:
                closes.append(float(k))
        
        current = closes[-1]
        ma5 = sum(closes[-5:]) / 5
        ma10 = sum(closes[-10:]) / 10
        ma20 = sum(closes[-20:]) / 20
        
        # RSI计算
        changes = [closes[i] - closes[i-1] for i in range(1, len(closes))]
        gains = sum([c for c in changes if c > 0])
        losses = sum([abs(c) for c in changes if c < 0])
        rsi = 100 - (100 / (1 + gains/losses)) if losses > 0 else 100
        
        return {
            '收盘价': current,
            'MA5': round(ma5, 2),
            'MA10': round(ma10, 2),
            'MA20': round(ma20, 2),
            'RSI': round(rsi, 1),
            'MA金叉': ma5 > ma20,
            'MACD金叉': False,
            'RSI超买': rsi > 70,
            'RSI超卖': rsi < 30,
        }
    
    def get_fund_flow(self, code: str) -> Dict[str, Any]:
        """
        获取资金流向（三级 fallback：AKShare → baostock → 默认值）
        """
        # ===== 方法1: 尝试从 AKShare 获取 =====
        try:
            data = self._get_fund_flow_from_akshare(code)
            if data:
                print(f"   📊 资金流向(AKShare)")
                return data
        except Exception as e:
            print(f"   ⚠️ AKShare 获取资金流向失败: {e}")
        
        # ===== 方法2: 尝试从 baostock 获取 =====
        try:
            data = self._get_fund_flow_from_baostock(code)
            if data:
                print(f"   📊 资金流向(baostock)")
                return data
        except Exception as e:
            print(f"   ⚠️ baostock 获取资金流向失败: {e}")
        
        # ===== 方法3: 默认值 =====
        print(f"   📊 资金流向(默认)")
        return {
            '主力净流入': 0,
            '5日主力净流入': 0,
            'source': 'default'
        }
    
    def _get_fund_flow_from_akshare(self, code: str) -> Optional[Dict]:
        """从 AKShare 获取资金流向"""
        if not HAS_AKSHARE:
            return None
        
        try:
            import akshare as ak
            
            # 获取资金流向
            df = ak.stock_individual_fund_flow(stock=code, market="sh")
            if df is not None and not df.empty:
                latest = df.iloc[0]
                return {
                    '主力净流入': float(latest.get('主力净流入', 0) or 0),
                    '5日主力净流入': float(latest.get('5日主力净流入', 0) or 0),
                    'source': 'akshare'
                }
            
            # 尝试深圳市场
            df2 = ak.stock_individual_fund_flow(stock=code, market="sz")
            if df2 is not None and not df2.empty:
                latest = df2.iloc[0]
                return {
                    '主力净流入': float(latest.get('主力净流入', 0) or 0),
                    '5日主力净流入': float(latest.get('5日主力净流入', 0) or 0),
                    'source': 'akshare'
                }
        except Exception:
            pass
        
        return None
    
    def _get_fund_flow_from_baostock(self, code: str) -> Optional[Dict]:
        """从 baostock 获取资金流向"""
        # baostock 不提供实时资金流向，返回 None
        return None
    
    def get_industry_info(self, code: str) -> Dict[str, Any]:
        """
        获取行业信息（三级 fallback：AKShare → baostock → AI/名称推断）
        """
        # ===== 方法1: 尝试从 AKShare 获取 =====
        try:
            industry_info = self._get_industry_from_akshare(code)
            if industry_info and industry_info.get('行业') and industry_info.get('行业') != '未知':
                print(f"   📊 行业信息(AKShare): {industry_info.get('行业')}")
                return industry_info
        except Exception as e:
            print(f"   ⚠️ AKShare 获取行业失败: {e}")
        
        # ===== 方法2: 尝试从 baostock 获取 =====
        try:
            industry_info = self._get_industry_from_baostock(code)
            if industry_info and industry_info.get('行业') and industry_info.get('行业') != '未知':
                print(f"   📊 行业信息(baostock): {industry_info.get('行业')}")
                return industry_info
        except Exception as e:
            print(f"   ⚠️ baostock 获取行业失败: {e}")
        
        # ===== 方法3: 使用 AI 分析 + 名称推断 =====
        print(f"   📊 行业信息(AI/推断): 使用备用方法")
        return self._get_industry_from_ai_or_name(code)
    
    def _get_industry_from_akshare(self, code: str) -> Dict[str, Any]:
        """从 AKShare 获取行业信息（含行业涨跌幅）"""
        if not HAS_AKSHARE:
            return None
            
        quote = self.get_realtime_quote(code)
        if 'error' in quote:
            return None
        
        try:
            import akshare as ak
            
            # 获取股票所属的行业
            stock_info = ak.stock_individual_info_em(symbol=code)
            industry_name = None
            if stock_info is not None and not stock_info.empty:
                for _, row in stock_info.iterrows():
                    if "行业" in str(row.get("item", "")):
                        industry_name = row.get("value", "未知")
                        break
            
            # 获取行业涨跌幅
            industry_change = 0
            industry_rank = 0
            total_industries = 0
            
            if industry_name:
                try:
                    # 获取所有行业板块数据
                    board_df = ak.stock_board_industry_name_em()
                    
                    # 匹配当前行业
                    for idx, row in board_df.iterrows():
                        if industry_name in str(row.get('板块名称', '')):
                            industry_change = float(row.get('涨跌幅', 0) or 0)
                            industry_rank = int(row.get('排名', 0) or 0)
                            total_industries = len(board_df)
                            break
                except Exception:
                    pass
            
            if industry_name:
                return {
                    '行业': industry_name,
                    '行业涨跌幅': round(industry_change, 2),
                    '行业排名': industry_rank,
                    '行业总数': total_industries,
                    '行业景气度': '高' if industry_change > 2 else ('低' if industry_change < -2 else '中'),
                    'source': 'akshare'
                }
        except Exception as e:
            print(f"   ⚠️ AKShare 行业涨幅获取失败: {e}")
        
        return None
    
    def _get_industry_from_baostock(self, code: str) -> Dict[str, Any]:
        """从 baostock 获取行业信息"""
        try:
            import baostock as bs
            
            # 登录
            lg = bs.login()
            if lg.error_code != '0':
                return None
            
            # 尝试不同的代码格式
            bs_code = f"sz.{code}" if code.startswith('00') else f"sh.{code}"
            rs = bs.query_stock_industry(code=bs_code)
            
            industry = None
            while rs.error_code == '0' and rs.next():
                data = rs.get_row_data()
                industry = data[3]  # industry 字段 (index 3)
                break
            
            bs.logout()
            
            if industry:
                # 转换 baostock 行业编码为中文名称
                industry_cn = self._convert_industry_name(industry)
                
                # 尝试从腾讯财经获取行业涨幅
                industry_change = 0
                try:
                    url = 'https://push2.eastmoney.com/api/qt/clist/get'
                    params = {
                        'pn': 1, 'pz': 500, 'po': 1, 'np': 1,
                        'ut': 'bd1d9ddb04089700cf9c27f6f7426281',
                        'fltt': 2, 'invt': 2, 'fid': 'f3',
                        'fs': f'b:{industry_cn[:2]}*+n!90',
                        'fields': 'f1,f2,f3,f12,f13,f14'
                    }
                    resp = requests.get(url, params=params, timeout=5)
                    data = resp.json()
                    if data.get('data') and data['data'].get('diff'):
                        # 取第一个行业指数的涨跌幅
                        industry_change = float(data['data']['diff'][0].get('f3', 0) or 0)
                except:
                    pass
                
                return {
                    '行业': industry_cn,
                    '行业涨跌幅': round(industry_change, 2),
                    '行业排名': 0,
                    '行业总数': 0,
                    '行业景气度': '高' if industry_change > 2 else ('低' if industry_change < -2 else '中'),
                    'source': 'baostock'
                }
        except Exception as e:
            print(f"   ⚠️ baostock 行业涨幅获取失败: {e}")
        
        return None
    
    def _convert_industry_name(self, industry: str) -> str:
        """将 baostock 行业编码转换为中文名称"""
        # 简单的行业映射
        industry_map = {
            'C21': '家用轻工',
            'C17': '纺织服装',
            'C18': '纺织服装',
            'C13': '食品饮料',
            'C14': '食品饮料',
            'C15': '酒类',
            'C27': '医药',
            'C26': '医药',
            'C29': '家电',
            'C30': '家电',
            'C35': '专用设备',
            'C36': '汽车',
            'C37': '交通运输',
            'C39': '电子',
            'C40': '电子',
            'C43': '电力',
            'C44': '电力',
            'C45': '燃气',
            'C46': '水务',
            'C48': '建筑工程',
            'C50': '房地产',
            'I63': '通信',
            'I64': '互联网',
            'I65': '软件服务',
            'J64': '银行',
            'J65': '证券',
            'J66': '保险',
            'J67': '保险',
            'K70': '商务服务',
            'L71': '租赁',
            'N77': '环保',
            'P82': '教育',
            'R85': '文化传媒',
            'R86': '游戏',
            'R89': '旅游',
        }
        
        # 提取前缀（如 C21）
        prefix = industry.split('.')[0] if '.' in industry else industry[:3]
        
        return industry_map.get(prefix, industry)
    
    def _get_industry_from_ai_or_name(self, code: str) -> Dict[str, Any]:
        """使用 AI 分析或名称推断获取行业信息"""
        quote = self.get_realtime_quote(code)
        
        if 'error' in quote:
            return {'error': quote['error']}
        
        name = quote.get('名称', '')
        
        # 基于名称推断行业（按优先级排序，长的关键词在前）
        industry_map = {
            # 金融业
            '银行': '银行',
            '证券': '证券',
            '保险': '保险',
            '信托': '非银金融',
            '期货': '非银金融',
            '租赁': '非银金融',
            
            # 制造业 - 食品饮料
            '榨菜': '食品饮料',
            '茅台': '食品饮料',
            '五粮液': '食品饮料',
            '泸州老窖': '食品饮料',
            '海天味业': '食品饮料',
            '伊利': '食品饮料',
            '蒙牛': '食品饮料',
            '青岛啤酒': '食品饮料',
            '华润': '食品饮料',
            '农夫山泉': '食品饮料',
            '康师傅': '食品饮料',
            '统一': '食品饮料',
            '三全': '食品饮料',
            '双汇': '食品饮料',
            '安井': '食品饮料',
            '绝味': '食品饮料',
            '周黑鸭': '食品饮料',
            '煌上煌': '食品饮料',
            '良品铺子': '食品饮料',
            '三只松鼠': '食品饮料',
            '来伊份': '食品饮料',
            '盐津铺子': '食品饮料',
            
            # 制造业 - 家电
            '家电': '家电',
            '海尔': '家电',
            '格力': '家电',
            '美的': '家电',
            '海信': '家电',
            'TCL': '家电',
            '长虹': '家电',
            '康佳': '家电',
            '创维': '家电',
            '苏泊尔': '家电',
            '九阳': '家电',
            '小熊电器': '家电',
            
            # 制造业 - 汽车
            '汽车': '汽车',
            '比亚迪': '汽车',
            '吉利': '汽车',
            '长城': '汽车',
            '长安': '汽车',
            '上汽': '汽车',
            '广汽': '汽车',
            '一汽': '汽车',
            '东风': '汽车',
            '北汽': '汽车',
            '江淮': '汽车',
            '理想的': '汽车',
            '小鹏': '汽车',
            '蔚来': '汽车',
            '零跑': '汽车',
            '哪吒': '汽车',
            '爱玛': '汽车',
            '雅迪': '汽车',
            
            # 医药生物
            '医药': '医药',
            '药业': '医药',
            '制药': '医药',
            '医疗': '医药',
            '生物': '医药',
            '恒瑞': '医药',
            '复星': '医药',
            '白云山': '医药',
            '片仔癀': '医药',
            '云南白药': '医药',
            '同仁堂': '医药',
            '华润三九': '医药',
            '东阿阿胶': '医药',
            '济川': '医药',
            '以岭': '医药',
            '天士力': '医药',
            '康弘': '医药',
            '信立泰': '医药',
            '丽珠': '医药',
            '药明康德': '医药',
            '泰格': '医药',
            '康龙化成': '医药',
            
            # 电子/半导体
            '电子': '电子',
            '半导体': '电子',
            '芯片': '电子',
            '集成电路': '电子',
            '中芯': '电子',
            '华虹': '电子',
            '韦尔': '电子',
            '兆易': '电子',
            '卓胜微': '电子',
            '圣邦': '电子',
            '思瑞浦': '电子',
            '乐鑫': '电子',
            '全志': '电子',
            '瑞芯微': '电子',
            '晶晨': '电子',
            '海思': '电子',
            
            # 电气设备
            '电气': '电气设备',
            '金风': '电气设备',
            '远景': '电气设备',
            '明阳': '电气设备',
            '东方电气': '电气设备',
            '特变电工': '电气设备',
            '正泰': '电气设备',
            '德力西': '电气设备',
            '天正': '电气设备',
            '人民电器': '电气设备',
            '良信': '电气设备',
            '沪光': '电气设备',
            '思源': '电气设备',
            '北京科锐': '电气设备',
            '双杰': '电气设备',
            '合纵': '电气设备',
            '泰坦': '电气设备',
            '理工': '电气设备',
            '科大智能': '电气设备',
            '华明': '电气设备',
            '长园': '电气设备',
            '红相': '电气设备',
            '中光': '电气设备',
            '神马': '电气设备',
            '东材': '电气设备',
            'S': '电气设备',
            
            # 风电设备（修正：应归为电气设备而非电力）
            '风电': '电气设备',
            
            # 电力/能源
            '电力': '电力',
            '核电': '电力',
            '水电': '电力',
            '风电': '电力',
            '光伏': '电力',
            '国电': '电力',
            '华能': '电力',
            '华电': '电力',
            '大唐': '电力',
            '三峡': '电力',
            '国投电力': '电力',
            '长江电力': '电力',
            
            # 新能源
            '新能源': '新能源',
            '宁德时代': '新能源',
            '比亚迪': '新能源汽车',
            '亿纬': '新能源',
            '国轩': '新能源',
            '孚能': '新能源',
            '欣旺达': '新能源',
            '鹏辉': '新能源',
            '珠海冠宇': '新能源',
            
            # 房地产
            '地产': '房地产',
            '万科': '房地产',
            '恒大': '房地产',
            '碧桂园': '房地产',
            '融创': '房地产',
            '保利': '房地产',
            '中海': '房地产',
            '华润置地': '房地产',
            '招商蛇口': '房地产',
            '金地': '房地产',
            '龙湖': '房地产',
            '世茂': '房地产',
            '新城': '房地产',
            '阳光城': '房地产',
            '中南': '房地产',
            
            # 钢铁
            '钢铁': '钢铁',
            '宝钢': '钢铁',
            '武钢': '钢铁',
            '鞍钢': '钢铁',
            '华菱': '钢铁',
            '太钢': '钢铁',
            '河钢': '钢铁',
            
            # 煤炭
            '煤炭': '煤炭',
            '神华': '煤炭',
            '中煤': '煤炭',
            '兖煤': '煤炭',
            '陕西煤业': '煤炭',
            '露天煤业': '煤炭',
            
            # 化工
            '化工': '化工',
            '万华': '化工',
            '化学': '化工',
            '石化': '化工',
            '中化': '化工',
            '恒力': '化工',
            '荣盛': '化工',
            '桐昆': '化工',
            '恒逸': '化工',
            '新凤鸣': '化工',
            
            # 建筑材料
            '水泥': '建筑材料',
            '海螺': '建筑材料',
            '华润水泥': '建筑材料',
            '冀东': '建筑材料',
            '万年青': '建筑材料',
            '塔牌': '建筑材料',
            '建材': '建筑材料',
            
            # 农林牧渔
            '农业': '农林牧渔',
            '牧原': '农林牧渔',
            '温氏': '农林牧渔',
            '新希望': '农林牧渔',
            '海大': '农林牧渔',
            '通威': '农林牧渔',
            '隆平高科': '农林牧渔',
            '登海': '农林牧渔',
            '大北农': '农林牧渔',
            
            # 交通运输
            '航空': '交通运输',
            '机场': '交通运输',
            '航运': '交通运输',
            '港口': '交通运输',
            '铁路': '交通运输',
            '公路': '交通运输',
            '快递': '交通运输',
            '物流': '交通运输',
            '中远海控': '交通运输',
            '中远海运': '交通运输',
            '海运': '交通运输',
            
            # 传媒
            '传媒': '传媒',
            '影视': '传媒',
            '游戏': '传媒',
            '广电': '传媒',
            '报业': '传媒',
            '出版': '传媒',
            '万达电影': '传媒',
            '光线': '传媒',
            '华谊': '传媒',
            '华策': '传媒',
            
            # 计算机/软件
            '计算机': '计算机',
            '软件': '计算机',
            '信息': '计算机',
            '科技': '计算机',
            '万达信息': '计算机',
            '东软': '计算机',
            '用友': '计算机',
            '金山': '计算机',
            '恒生': '计算机',
            '东方财富': '计算机',
            '同花顺': '计算机',
            '大智慧': '计算机',
            
            # 通信
            '通信': '通信',
            '移动': '通信',
            '联通': '通信',
            '电信': '通信',
            '华为': '通信',
            '中兴': '通信',
            '烽火': '通信',
            
            # 轻工制造
            '造纸': '轻工制造',
            '印刷': '轻工制造',
            '包装': '轻工制造',
            '文具': '轻工制造',
            '晨光': '轻工制造',
            '齐心': '轻工制造',
            '得力': '轻工制造',
            '浙江永强': '家用轻工',
            '永强': '家用轻工',
            
            # 纺织服装
            '纺织': '纺织服装',
            '服装': '纺织服装',
            '鞋': '纺织服装',
            '运动': '纺织服装',
            '安踏': '纺织服装',
            '李宁': '纺织服装',
            '特步': '纺织服装',
            '361': '纺织服装',
            '波司登': '纺织服装',
            '海澜': '纺织服装',
            '雅戈尔': '纺织服装',
            
            # 商贸零售
            '零售': '商贸零售',
            '商贸': '商贸零售',
            '百货': '商贸零售',
            '超市': '商贸零售',
            '苏宁': '商贸零售',
            '国美': '商贸零售',
            '京东': '商贸零售',
            '阿里巴巴': '商贸零售',
            '拼多多': '商贸零售',
            '美团': '商贸零售',
            '拼多多': '商贸零售',
            
            # 环保
            '环保': '环保',
            '节能': '环保',
            '清洁能源': '环保',
            '维尔利': '环保',
            '碧水源': '环保',
            
            # 军工
            '军工': '国防军工',
            '航天': '国防军工',
            '航空': '国防军工',
            '船舶': '国防军工',
            '核工业': '国防军工',
            '中国重工': '国防军工',
            '中国船舶': '国防军工',
            '中航沈飞': '国防军工',
            '航发动力': '国防军工',
            
            # 其他常见企业
            '平安': '银行',
            '万达': '房地产',
            '万达信息': '计算机',
            '集团': '综合',
            '控股': '综合',
            '投资': '综合',
            '资本': '综合',
            
            # 补充修正 - 完整公司名称匹配
            '贵州茅台': '食品饮料',
            '五粮液': '食品饮料',
            '泸州老窖': '食品饮料',
            '山西汾酒': '食品饮料',
            '古井贡酒': '食品饮料',
            '今世缘': '食品饮料',
            '洋河股份': '食品饮料',
            '酒': '食品饮料',
            '茅台': '食品饮料',
            '万科A': '房地产',
            '万科': '房地产',
            '立讯精密': '电子',
            '歌尔股份': '电子',
            '蓝思科技': '电子',
            '京东方A': '电子',
            'TCL科技': '家电',
            '海螺水泥': '建筑材料',
            '广联达': '计算机',
            '三六零': '计算机',
            '360': '计算机',
            '中国中免': '商贸零售',
            '中免': '商贸零售',
            '免': '商贸零售',
            '招商公路': '交通运输',
        }
        
        industry = '综合'
        for key, val in industry_map.items():
            if key in name:
                industry = val
                break
        
        return {
            '行业': industry,
            '行业涨跌幅': 0,
            '行业排名': 0,
            '行业总数': 0,
            '行业景气度': '中',
        }
    
    def get_all_data(self, code: str) -> Dict[str, Any]:
        """
        获取所有数据（综合）
        """
        print(f"📊 正在获取 {code} 的财务数据...")
        
        result = {
            '代码': code,
            '更新时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        }
        
        # 实时行情
        print("  - 实时行情...")
        result['行情'] = self.get_realtime_quote(code)
        time.sleep(self.delay)
        
        # 财务指标
        print("  - 财务指标...")
        result['财务指标'] = self.get_financial_indicators(code)
        time.sleep(self.delay)
        
        # 估值数据
        print("  - 估值数据...")
        result['估值'] = self.get_valuation_data(code)
        time.sleep(self.delay)
        
        # 历史估值
        print("  - 历史估值...")
        result['估值分位'] = self.get_historical_pe_pb(code)
        time.sleep(self.delay)
        
        # K线数据
        print("  - K线数据...")
        result['技术指标'] = self.get_kline_data(code)
        time.sleep(self.delay)
        
        # 资金流向
        print("  - 资金流向...")
        result['资金流向'] = self.get_fund_flow(code)
        
        # 行业信息
        print("  - 行业信息...")
        result['行业'] = self.get_industry_info(code)
        
        return result
    
    def _safe_float(self, value) -> float:
        """安全转换为浮点数"""
        try:
            if value is None:
                return 0.0
            return float(value)
        except:
            return 0.0


if __name__ == "__main__":
    # 测试
    fetcher = FinancialDataFetcher()
    
    print("=" * 60)
    print("📈 财务数据获取测试")
    print("=" * 60)
    print(f"白名单域名: {fetcher.ALLOWED_DOMAINS}")
    print()
    
    data = fetcher.get_all_data("000921")
    
    print("\n【行情】")
    if 'error' not in data['行情']:
        q = data['行情']
        print(f"  {q['名称']}: ¥{q['现价']} ({q['涨跌幅']:+.2f}%)")
        print(f"  PE: {q['市盈率']} | PB: {q['市净率']}")
    
    print("\n【财务指标】")
    if 'error' not in data['财务指标']:
        fi = data['财务指标']
        print(f"  ROE: {fi['ROE']:.2f}% | 毛利率: {fi['毛利率']:.2f}%")
    
    print("\n【估值分位】")
    if 'error' not in data['估值分位']:
        ev = data['估值分位']
        print(f"  PE: {ev['PE当前']} ({ev['PE分位']}%分位) - {ev['估值评级']}")

path: scripts/graham_evaluator.py
content: #!/usr/bin/env python3
"""
投资研究系统 - Graham 价值投资评分模块 v1.0
基于格雷厄姆价值投资理念进行股票评估
"""
from typing import Dict, Tuple
from dataclasses import dataclass


@dataclass
class GrahamScore:
    """Graham 评分结果"""
    total_score: float        # 总分 (0-1)
    safety_margin: float      # 安全边际得分
    profitability: float      # 盈利能力得分
    stability: float          # 稳定性得分
    dividend: float           # 股息得分
    recommendation: str       # 建议
    reasons: list             # 理由


class GrahamEvaluator:
    """Graham 价值投资评估器"""
    
    # Graham 价值投资标准
    STANDARDS = {
        # 盈利指标
        'roe_min': 10.0,         # ROE 最低要求
        'roe_good': 15.0,        # ROE 优秀
        'gross_margin_min': 20.0, # 毛利率最低要求
        'gross_margin_good': 40.0, # 毛利率优秀
        
        # 估值指标
        'pe_max': 15.0,          # PE 最高要求
        'pe_good': 10.0,         # PE 优秀
        'pb_max': 1.5,           # PB 最高要求
        'pb_good': 1.0,          # PB 优秀
        
        # 股息
        'dividend_min': 2.0,     # 股息率最低要求
        'dividend_good': 4.0,    # 股息率优秀
        
        # 财务健康
        'debt_max': 50.0,        # 资产负债率最高
        'current_ratio_min': 1.5, # 流动比率最低
    }
    
    def __init__(self, user_profile=None):
        """初始化评估器，可选传入用户画像"""
        self.user_profile = user_profile
    
    def evaluate(self, financial_data: Dict, quote: Dict = None) -> GrahamScore:
        """
        评估股票是否满足 Graham 价值投资标准
        
        Args:
            financial_data: 财务指标字典
            quote: 行情数据字典（可选）
        
        Returns:
            GrahamScore: 评分结果
        """
        fi = financial_data.get('财务指标', {})
        valuation = financial_data.get('估值', {})
        
        # 提取关键指标
        roe = float(fi.get('ROE', 0) or 0)
        gross_margin = float(fi.get('毛利率', 0) or 0)
        net_margin = float(fi.get('净利率', 0) or 0)
        revenue_growth = float(fi.get('营收增速', 0) or 0)
        profit_growth = float(fi.get('利润增速', 0) or 0)
        debt_ratio = float(fi.get('资产负债率', 0) or 0)
        current_ratio = float(fi.get('流动比率', 0) or 0)
        
        pe = float(valuation.get('PE_TTM', 0) or 0)
        pb = float(valuation.get('PB', 0) or 0)
        dividend_yield = float(valuation.get('股息率', 0) or 0)
        
        # 1. 安全边际评分 (40%)
        safety_score = self._calc_safety_score(pe, pb, roe, current_ratio)
        
        # 2. 盈利能力评分 (30%)
        profitability_score = self._calc_profitability_score(roe, gross_margin, net_margin)
        
        # 3. 稳定性评分 (15%)
        stability_score = self._calc_stability_score(debt_ratio, current_ratio, revenue_growth)
        
        # 4. 股息评分 (15%)
        dividend_score = self._calc_dividend_score(dividend_yield)
        
        # 计算总分
        total_score = (
            safety_score * 0.40 +
            profitability_score * 0.30 +
            stability_score * 0.15 +
            dividend_score * 0.15
        )
        
        # 生成建议
        recommendation, reasons = self._generate_recommendation(
            total_score, safety_score, profitability_score, dividend_score,
            roe, pe, pb, dividend_yield, current_ratio
        )
        
        return GrahamScore(
            total_score=round(total_score, 2),
            safety_margin=round(safety_score, 2),
            profitability=round(profitability_score, 2),
            stability=round(stability_score, 2),
            dividend=round(dividend_score, 2),
            recommendation=recommendation,
            reasons=reasons
        )
    
    def _calc_safety_score(self, pe: float, pb: float, roe: float, current_ratio: float) -> float:
        """计算安全边际得分"""
        score = 0
        
        # PE 评分
        if pe > 0:
            if pe <= self.STANDARDS['pe_good']:
                score += 0.4
            elif pe <= self.STANDARDS['pe_max']:
                score += 0.2
            else:
                score += max(0, 0.2 - (pe - self.STANDARDS['pe_max']) * 0.02)
        
        # PB 评分
        if pb > 0:
            if pb <= self.STANDARDS['pb_good']:
                score += 0.3
            elif pb <= self.STANDARDS['pb_max']:
                score += 0.15
            else:
                score += max(0, 0.15 - (pb - self.STANDARDS['pb_max']) * 0.05)
        
        # 流动比率评分
        if current_ratio >= self.STANDARDS['current_ratio_min']:
            score += 0.3
        elif current_ratio >= 1.0:
            score += 0.15
        
        return min(score, 1.0)
    
    def _calc_profitability_score(self, roe: float, gross_margin: float, net_margin: float) -> float:
        """计算盈利能力得分"""
        score = 0
        
        # ROE 评分
        if roe >= self.STANDARDS['roe_good']:
            score += 0.4
        elif roe >= self.STANDARDS['roe_min']:
            score += 0.2
        elif roe > 0:
            score += 0.1
        
        # 毛利率评分
        if gross_margin >= self.STANDARDS['gross_margin_good']:
            score += 0.35
        elif gross_margin >= self.STANDARDS['gross_margin_min']:
            score += 0.2
        
        # 净利率评分
        if net_margin >= 15:
            score += 0.25
        elif net_margin >= 10:
            score += 0.15
        elif net_margin > 0:
            score += 0.1
        
        return min(score, 1.0)
    
    def _calc_stability_score(self, debt_ratio: float, current_ratio: float, 
                               revenue_growth: float) -> float:
        """计算稳定性得分"""
        score = 0
        
        # 资产负债率评分
        if debt_ratio <= 30:
            score += 0.35
        elif debt_ratio <= self.STANDARDS['debt_max']:
            score += 0.2
        else:
            score += max(0, 0.2 - (debt_ratio - self.STANDARDS['debt_max']) * 0.01)
        
        # 流动比率评分
        if current_ratio >= 2.0:
            score += 0.35
        elif current_ratio >= 1.5:
            score += 0.25
        elif current_ratio >= 1.0:
            score += 0.15
        
        # 营收增长稳定性
        if revenue_growth > 0:
            score += 0.3
        elif revenue_growth > -5:
            score += 0.15
        
        return min(score, 1.0)
    
    def _calc_dividend_score(self, dividend_yield: float) -> float:
        """计算股息得分"""
        if dividend_yield >= self.STANDARDS['dividend_good']:
            return 1.0
        elif dividend_yield >= self.STANDARDS['dividend_min']:
            return 0.6
        elif dividend_yield > 0:
            return 0.3
        return 0.0
    
    def _generate_recommendation(self, total: float, safety: float, 
                                  profit: float, div: float,
                                  roe: float, pe: float, pb: float,
                                  dividend: float, current_ratio: float) -> Tuple[str, list]:
        """生成投资建议"""
        reasons = []
        
        # 分析各项
        if pe > 0 and pe < self.STANDARDS['pe_max']:
            reasons.append(f"PE={pe:.1f}倍，符合 Graham 标准（<{self.STANDARDS['pe_max']}倍）")
        if pb > 0 and pb < self.STANDARDS['pb_max']:
            reasons.append(f"PB={pb:.2f}倍，具备安全边际")
        if roe >= self.STANDARDS['roe_min']:
            reasons.append(f"ROE={roe:.1f}% > {self.STANDARDS['roe_min']}%，盈利能力强")
        if dividend >= self.STANDARDS['dividend_min']:
            reasons.append(f"股息率={dividend:.2f}% > {self.STANDARDS['dividend_min']}%，提供现金流")
        if current_ratio >= self.STANDARDS['current_ratio_min']:
            reasons.append(f"流动比率={current_ratio:.2f}，财务健康")
        
        if roe < self.STANDARDS['roe_min']:
            reasons.append(f"⚠️ ROE={roe:.1f}% 偏低")
        if pe > self.STANDARDS['pe_max']:
            reasons.append(f"⚠️ PE={pe:.1f}倍偏高")
        if dividend < self.STANDARDS['dividend_min']:
            reasons.append(f"⚠️ 股息率较低")
        
        # 生成建议
        if total >= 0.75:
            recommendation = "✅ 强烈建议"
            if self.user_profile and self.user_profile.investment_style == "价值投资":
                recommendation = "✅ 符合 Graham 价值投资标准"
        elif total >= 0.50:
            recommendation = "⚠️ 谨慎观望"
        else:
            recommendation = "❌ 不建议"
        
        return recommendation, reasons


def evaluate_with_graham(code: str, financial_data: Dict, quote: Dict = None, 
                         user_profile=None) -> GrahamScore:
    """便捷函数：Graham 价值评估"""
    evaluator = GrahamEvaluator(user_profile)
    return evaluator.evaluate(financial_data, quote)


if __name__ == "__main__":
    # 测试
    test_financial = {
        '财务指标': {
            'ROE': 10.87,
            '毛利率': 18.88,
            '净利率': 8.33,
            '营收增速': -0.24,
            '利润增速': 10.0,
            '资产负债率': 45.64,
            '流动比率': 1.52
        },
        '估值': {
            'PE_TTM': 32.73,
            'PB': 1.77,
            '股息率': 0.96
        }
    }
    
    score = evaluate_with_graham('002489', test_financial)
    print(f"Graham 评分: {score.total_score}")
    print(f"建议: {score.recommendation}")
    print(f"理由: {score.reasons}")

path: scripts/install.sh
content: #!/bin/bash
# boheng-investment-workflow 安装脚本 v1.2.0
# 
# 安全说明：
# - 建议在 Python 虚拟环境中安装
# - 仅安装必要的 Python 依赖
# - 不执行系统级操作

set -e  # 遇到错误立即退出

echo "================================================"
echo "  boheng-investment-workflow 安装脚本 v1.2.0"
echo "================================================"
echo ""

# 检查 Python 版本
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
echo "📋 Python 版本: $PYTHON_VERSION"

# 创建数据目录
DATA_DIR="$HOME/.openclaw/workspace/investment"
echo "📁 创建数据目录: $DATA_DIR"
mkdir -p "$DATA_DIR/reports"
touch "$DATA_DIR/watchlist.txt"

# 安装 Python 依赖
echo ""
echo "📦 安装 Python 依赖..."
echo "   提示：建议在虚拟环境中运行 (python3 -m venv venv && source venv/bin/activate)"
echo ""

# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

if [ -f "$SCRIPT_DIR/../requirements.txt" ]; then
    pip3 install -r "$SCRIPT_DIR/../requirements.txt" || {
        echo "❌ 依赖安装失败，请检查 pip 或网络连接"
        exit 1
    }
else
    # 仅安装必需依赖，akshare 为可选依赖
    pip3 install requests beautifulsoup4 || {
        echo "❌ 依赖安装失败，请检查 pip 或网络连接"
        exit 1
    }
    echo ""
    echo "⚠️ 可选依赖："
    echo "   如需使用 --akshare 模式获取深度财务数据，请手动安装："
    echo "   pip3 install akshare pandas"
fi

# 测试基本功能
echo ""
echo "🧪 测试模块加载..."
cd "$SCRIPT_DIR"

python3 -c "
import sys
sys.path.insert(0, '.')
try:
    from config import DATA_DIR, ALLOWED_DOMAINS
    print(f'✅ 配置加载成功')
    print(f'   数据目录: {DATA_DIR}')
    print(f'   允许域名: {ALLOWED_DOMAINS}')
except ImportError as e:
    print(f'❌ 配置加载失败: {e}')
    sys.exit(1)

try:
    from financial_data import FinancialDataFetcher
    from analysts import run_all_analysts
    print('✅ 分析模块加载成功')
except ImportError as e:
    print(f'⚠️ 部分模块加载失败: {e}')
" || {
    echo ""
    echo "⚠️ 模块测试失败，请检查依赖安装"
    exit 1
}

echo ""
echo "================================================"
echo "✅ 安装完成!"
echo "================================================"
echo ""
echo "📚 使用方式:"
echo "  cd ~/.openclaw/skills/boheng-investment-workflow/scripts"
echo "  python3 analyze_stock.py 600919                  # 快速分析"
echo "  python3 analyze_stock.py 600919 江苏银行        # 指定名称"
echo "  python3 analyze_stock.py 600919 --akshare       # 完整分析（需安装akshare）"
echo ""
echo "🔒 安全提示:"
echo "  - 白名单域名: qt.gtimg.cn, web.ifzq.gtimg.cn"
echo "  - 数据存储在: ~/.openclaw/workspace/investment/"
echo "  - 建议在虚拟环境中运行"
echo "  - --akshare 模式需要 akshare 库（可选）"
echo ""

path: scripts/uninstall.sh
content: #!/bin/bash
# 投资研究系统 - 卸载脚本

echo "🗑️ 卸载投资研究系统..."
echo ""

# 询问是否删除数据
read -p "是否删除分析报告和历史数据？(y/N) " -n 1 -r
echo ""

if [[ $REPLY =~ ^[Yy]$ ]]; then
    rm -rf ~/.openclaw/investment
    echo "✅ 已删除数据目录"
else
    echo "ℹ️ 数据目录已保留：~/.openclaw/investment"
fi

echo ""
echo "✅ 卸载完成"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)