Skill: hitjcl/stock-analyzer-v2 (version 1.0.0)

--- name: stock-analyzer description: A股/港股实时行情查询、基本面分析、深度报告生成与邮件发送一体化工具。触发场景：(1) 用户询问股票价格、市值、PE/PB等数据；(2) 用户要求分析某只或多只股票；(3) 用户要求生成股票分析报告；(4) 用户要求通过邮件发送股票报告。支持AkShare实时行情、聚宽基本面数据、QQ邮箱/Gmail发送。 --- # Stock Analyzer - 股票深度分析工具 > ⚠️ **安全提示**：本 skill 不在任何文件中硬编码密码、授权码或私人账号信息。所有凭证均通过环境变量或运行时配置传入，绝不写入代码或文档。 ## 快速开始 ### 1. 安装依赖 ```bash pip install -r scripts/requirements.txt ``` ### 2. 配置聚宽账号（可选，用于基本面数据） **通过环境变量配置**（推荐，安全）： ```bash export JQDATA_PHONE="你的手机号" export JQDATA_PASSWORD="你的密码" ``` 或使用 `jq_login.py` 交互式配置： ```bash python scripts/jq_login.py ``` ### 3. 查询实时行情 ```python python scripts/stock_query.py --codes 03690.HK,300413.SZ,300251.SZ ``` 返回：股价、涨跌幅、成交量、成交额 ### 4. 获取基本面数据（需配置聚宽） ```python python scripts/stock_fundamentals.py --codes 300413.SZ,300251.SZ ``` 返回：PE、PB、市值、ROE、营收增长率等 ### 5. 生成分析报告 ```python python scripts/generate_report.py --codes 03690.HK,300413.SZ --output report.md ``` ### 6. 发送邮件（需配置邮箱授权码） **通过环境变量配置**： ```bash export QQ_EMAIL_AUTH_CODE="your_auth_code" export GMAIL_APP_PASSWORD="your_app_password" ``` 发送： ```python python scripts/send_email.py --to 收件人邮箱 --file report.md --auth-code "$QQ_EMAIL_AUTH_CODE" ``` ## 工作流程 ``` 用户请求 → 解析股票代码 → 获取实时行情 → 获取基本面 → 深度分析 → 生成报告 → 发送邮件 ``` ## 凭证管理规范 | 凭证 | 推荐配置方式 | 说明 | |------|------------|------| | 聚宽账号 | `JQDATA_PHONE` + `JQDATA_PASSWORD` 环境变量 | 免费注册 joinquant.com | | QQ邮箱授权码 | `QQ_EMAIL_AUTH_CODE` 环境变量 | QQ邮箱设置 → 账户 → POP3/IMAP → 生成授权码 | | Gmail应用密码 | `GMAIL_APP_PASSWORD` 环境变量 | Google账户 → 安全 → 应用密码 | > ⚠️ **禁止**：将任何真实密码、授权码、手机号写入代码、文档或示例中。 ## 股票代码格式 | 市场 | 代码格式 | 示例 | |------|---------|------| | 港股 | `XXXXX.HK` | `03690.HK`（美团） | | A股深交所 | `XXXXXX.SZ` | `300413.SZ`（芒果超媒） | | A股上交所 | `XXXXXX.SH` | `600519.SH`（贵州茅台） | > 💡 常见股票代码见 `references/stock_codes.md` ## 依赖说明 ### AkShare（实时行情，无需账号） - A股实时：`ak.stock_zh_a_spot_em()` - 港股实时：`ak.stock_hk_spot_em()` - 完全免费，但需注意并发限制 ### 聚宽 JQData（基本面数据，需注册） - 注册地址：https://www.joinquant.com - 免费版有数据范围和日期权限限制 - 使用前请先阅读聚宽积分说明 ## 文件说明 ### scripts/ - `stock_query.py` - 实时行情查询（AkShare，无需账号） - `stock_fundamentals.py` - 基本面数据获取（聚宽，需配置） - `generate_report.py` - 报告生成 - `send_email.py` - 邮件发送 - `jq_login.py` - 聚宽账号配置（交互式，安全存储） - `requirements.txt` - 依赖清单 ### references/ - `report_template.md` - 报告模板 - `stock_codes.md` - 常用股票代码对照表 ## 使用示例 **示例1：查询美团股价** ``` 用户：帮我查一下美团的股价 → python scripts/stock_query.py --codes 03690.HK → 返回：美团(03690.HK) HK$80.70 (-1.41%) ``` **示例2：分析多只股票** ``` 用户：分析一下美团、芒果超媒、光线传媒 → python scripts/stock_query.py 获取行情 → python scripts/stock_fundamentals.py 获取基本面 → python scripts/generate_report.py 生成报告 ``` **示例3：发送报告到邮箱** ``` 用户：把报告发到我邮箱 → python scripts/send_email.py --to 收件人邮箱 --file report.md --auth-code "$QQ_EMAIL_AUTH_CODE" ``` ## 常见问题 ### Q: AkShare 获取数据失败？ - 网络问题：检查代理设置 - 并发限制：逐个查询而非批量 - 数据源维护：稍后重试 ### Q: 聚宽登录失败？ - 检查手机号/密码是否正确 - 账号是否过期（免费版1年有效期） - 是否有港股数据权限 ### Q: 邮件发送失败？ - `Login denied`：授权码错误，请重新生成 - `Connection closed`：尝试 465 SSL 端口 - `SMTPAuthenticationError`：确认已开启 SMTP 服务

path: references/report_template.md
content: # 股票分析报告模板

## 报告结构

```markdown
# 五大标的买入分析报告 | YYYY-MM-DD

## 📊 今日行情速览

| 标的 | 代码 | 现价 | 涨跌幅 | 市值 |
|------|------|------|--------|------|
| 美团 | 03690.HK | HK$80.70 | -1.41% | ~500亿HKD |
| ... | ... | ... | ... | ... |

## 🔍 深度分析

### 1. 美团 (03690.HK)

#### 基本面
- 市值：XXX 亿
- PE(TTM)：XXx
- PB：X.Xx
- ROE：XX%

#### 近期动态
- [新闻/公告摘要]

#### 竞争态势
- [竞争对手分析]

#### 技术面
- 支撑位：XXX
- 压力位：XXX
- 趋势：[上涨/下跌/震荡]

#### 操作建议
- **评级**：[买入/持有/减仓/观望]
- **理由**：[一句话理由]
- **止损位**：XXX

---

### 2. [下一只股票]

...

---

## 📋 综合建议

| 标的 | 评级 | 止损位 | 备注 |
|------|------|--------|------|
| 美团 | 持有 | HK$78 | 关注Q1业绩 |
| ... | ... | ... | ... |

## ⚠️ 风险提示

- [风险因素1]
- [风险因素2]

---
*报告生成时间：YYYY-MM-DD HH:MM*
*数据来源：AkShare, JoinQuant*
```

## 分析维度清单

### 必选维度
- [x] 实时价格、涨跌幅
- [x] 市值
- [x] PE/PB 估值
- [x] 操作建议

### 可选维度
- [ ] 营收/利润增速
- [ ] ROE/ROA
- [ ] 行业对比
- [ ] 技术指标（MA、MACD、RSI）
- [ ] 新闻/公告
- [ ] 竞争对手分析
- [ ] 历史估值分位

## 输出格式建议

### Markdown 格式
- 适合：邮件正文、GitHub、文档
- 优点：可读性好、支持表格

### HTML 格式
- 适合：网页展示、邮件富文本
- 优点：排版灵活、支持图表

### PDF 格式
- 适合：正式报告、存档
- 优点：格式固定、可打印


path: references/stock_codes.md
content: # 常用股票代码对照表

## 港股

| 名称 | 代码 | 行业 |
|------|------|------|
| 美团 | 03690.HK | 本地生活 |
| 腾讯控股 | 00700.HK | 互联网 |
| 阅文集团 | 00772.HK | 数字阅读 |
| 快手 | 01024.HK | 短视频 |
| 小米集团 | 01810.HK | 消费电子 |
| 阿里巴巴 | 09988.HK | 电商 |
| 京东集团 | 09618.HK | 电商 |
| 网易 | 09999.HK | 游戏/互联网 |
| 百度 | 09888.HK | 搜索/AI |

## A股 - 深交所

| 名称 | 代码 | 行业 |
|------|------|------|
| 芒果超媒 | 300413.SZ | 传媒 |
| 光线传媒 | 300251.SZ | 传媒 |
| 立高食品 | 300973.SZ | 食品 |
| 宁德时代 | 300750.SZ | 新能源 |
| 比亚迪 | 002594.SZ | 新能源汽车 |
| 迈瑞医疗 | 300760.SZ | 医疗器械 |
| 爱尔眼科 | 300015.SZ | 医疗 |
| 东方财富 | 300059.SZ | 金融科技 |

## A股 - 上交所

| 名称 | 代码 | 行业 |
|------|------|------|
| 贵州茅台 | 600519.SH | 白酒 |
| 中国平安 | 601318.SH | 保险 |
| 招商银行 | 600036.SH | 银行 |
| 海天味业 | 603288.SH | 调味品 |
| 恒瑞医药 | 600276.SH | 医药 |
| 中国中免 | 601888.SH | 免税 |
| 隆基绿能 | 601012.SH | 光伏 |

## 代码格式说明

### AkShare 格式
- A股：纯数字代码，如 `300413`
- 港股：纯数字代码，如 `03690`

### 聚宽格式
- A股：`000001.XSHE`（深交所）或 `600000.XSHG`（上交所）
- 港股：需要港股通权限

### 通达信格式
- A股：纯数字
- 港股：代码前加 `HK`，如 `HK03690`


path: scripts/generate_report.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
股票分析报告生成脚本

整合实时行情和基本面数据，生成 Markdown 格式分析报告。

使用方法:
    python scripts/generate_report.py --codes 03690.HK,300413.SZ --output report.md
    python scripts/generate_report.py --codes 600519.SH --output茅台分析.md --title "贵州茅台深度分析"
"""

import argparse
import os
import sys
import time
from datetime import datetime

def check_akshare():
    """检查并返回 akshare 模块"""
    try:
        import akshare as ak
        return ak
    except ImportError:
        print("❌ akshare 未安装，请运行: pip install akshare")
        sys.exit(1)

def check_jqdata():
    """检查聚宽配置，返回 jq 模块或 None"""
    phone = os.environ.get('JQDATA_PHONE')
    [REDACTED_SECRET]'JQDATA_PASSWORD')
    if not phone or not [REDACTED_SECRET] None
    try:
        import jqdatasdk as jq
        jq.auth(phone, password)
        return jq
    except:
        return None

def parse_code(code):
    """解析股票代码"""
    if '.' in code:
        parts = code.split('.')
        return parts[1].upper(), parts[0]  # (市场, 纯代码)
    return 'SZ', code

def get_a_stock_price(ak, code):
    """获取 A股 实时行情"""
    try:
        df = ak.stock_zh_a_spot_em()
        row = df[df['代码'] == code]
        if row.empty:
            return None
        r = row.iloc[0]
        return {
            'code': code,
            'name': r['名称'],
            'price': float(r['最新价']),
            'change_pct': float(r['涨跌幅']),
            'volume': float(r['成交量']),
            'amount': float(r['成交额']),
            'high': float(r['最高']),
            'low': float(r['最低']),
            'open': float(r['今开']),
            'prev_close': float(r['昨收']),
        }
    except Exception as e:
        return {'code': code, 'error': str(e)}

def get_hk_stock_price(ak, code):
    """获取港股实时行情"""
    try:
        df = ak.stock_hk_spot_em()
        row = df[df['代码'] == code]
        if row.empty:
            return None
        r = row.iloc[0]
        return {
            'code': code,
            'name': r['名称'],
            'price': float(r['最新价']),
            'change_pct': float(r['涨跌幅']),
            'volume': float(r.get('成交量', 0)),
            'amount': float(r.get('成交额', 0)),
            'high': float(r.get('最高', 0)),
            'low': float(r.get('最低', 0)),
        }
    except Exception as e:
        return {'code': code, 'error': str(e)}

def get_fundamentals(jq, code):
    """获取基本面数据"""
    if jq is None:
        return None
    try:
        # 标准化代码
        market, pure = parse_code(code)
        if '.' not in code:
            if pure == 'HK':
                jq_code = f'{pure}{code}'  # 港股格式
            elif pure in ('SZ', 'XSHE'):
                jq_code = f'{code}.XSHE'
            elif pure in ('SH', 'XSHG'):
                jq_code = f'{code}.XSHG'
            else:
                jq_code = code
        else:
            jq_code = code
        
        val = jq.get_valuation(jq_code)
        if val is not None and len(val) > 0:
            row = val.iloc[-1]
            return {
                'market_cap': row.get('market_cap'),
                'pe_ratio': row.get('pe_ratio'),
                'pb_ratio': row.get('pb_ratio'),
                'roe': row.get('roe'),
                'date': str(val.index[-1].date()) if hasattr(val.index[-1], 'date') else str(val.index[-1]),
            }
    except:
        pass
    return None

def format_change_pct(pct):
    """格式化涨跌幅"""
    if pct is None:
        return "N/A"
    symbol = "🔴" if pct < 0 else "🟢" if pct > 0 else "⚪"
    return f"{symbol}{pct:+.2f}%"

def generate_report(codes, title=None, output_path='report.md'):
    """生成分析报告"""
    ak = check_akshare()
    jq = check_jqdata()
    
    now = datetime.now().strftime('%Y-%m-%d %H:%M')
    
    print(f"\n📊 正在生成股票分析报告...")
    print(f"股票数量: {len(codes)}")
    print()
    
    # 获取行情数据
    prices = {}
    for code in codes:
        market, pure = parse_code(code)
        print(f"获取 {code} 行情...")
        if market == 'HK':
            prices[code] = get_hk_stock_price(ak, pure)
        else:
            prices[code] = get_a_stock_price(ak, pure)
        time.sleep(0.3)
    
    # 获取基本面数据
    fundamentals = {}
    if jq:
        print("\n获取基本面数据（需聚宽配置）...")
        for code in codes:
            print(f"获取 {code} 基本面...")
            fundamentals[code] = get_fundamentals(jq, code)
            time.sleep(0.3)
    else:
        print("\n⚠️ 未配置聚宽，跳过基本面数据（运行 jq_login.py 可启用）")
    
    # 构建 Markdown 报告
    lines = []
    
    # 标题
    if title:
        lines.append(f"# {title}\n")
    else:
        stock_names = [prices.get(c, {}).get('name', c) for c in codes]
        lines.append(f"# 股票分析报告 | {', '.join(stock_names)}\n")
    
    lines.append(f"**生成时间**: {now}\n")
    lines.append(f"**数据来源**: AkShare（实时行情）" + 
                 ("、聚宽 JQData（基本面）" if jq else "（基本面数据未配置）") + "\n")
    lines.append("---\n")
    
    # 行情速览
    lines.append("## 📊 行情速览\n")
    lines.append("| 代码 | 名称 | 最新价 | 涨跌幅 | 成交额 |")
    lines.append("|------|------|--------|--------|--------|")
    
    for code in codes:
        p = prices.get(code, {})
        if p and 'error' not in p:
            amt = p.get('amount', 0)
            amt_str = f"{amt/1e8:.2f}亿" if amt > 0 else "N/A"
            lines.append(f"| {code} | {p.get('name', 'N/A')} | {p.get('price', 'N/A')} | "
                        f"{format_change_pct(p.get('change_pct'))} | {amt_str} |")
        else:
            lines.append(f"| {code} | ❌ 获取失败 | - | - | - |")
    
    lines.append("\n")
    
    # 基本面数据
    if fundamentals and any(fundamentals.values()):
        lines.append("## 📈 基本面数据\n")
        lines.append("| 代码 | 总市值(亿) | PE | PB | ROE | 数据日期 |")
        lines.append("|------|-----------|----|----|-----|----------|")
        
        for code in codes:
            f = fundamentals.get(code)
            if f:
                mc = f"{f['market_cap']:.2f}" if f.get('market_cap') else "N/A"
                pe = f"{f['pe_ratio']:.2f}" if f.get('pe_ratio') else "N/A"
                pb = f"{f['pb_ratio']:.2f}" if f.get('pb_ratio') else "N/A"
                roe = f"{f['roe']:.2f}%" if f.get('roe') else "N/A"
                lines.append(f"| {code} | {mc} | {pe} | {pb} | {roe} | {f.get('date', 'N/A')} |")
            else:
                lines.append(f"| {code} | N/A | N/A | N/A | N/A | - |")
        lines.append("\n")
    
    # 操作建议（基于涨跌幅的简单建议）
    lines.append("## 💡 操作建议\n")
    for code in codes:
        p = prices.get(code, {})
        if p and 'error' not in p:
            pct = p.get('change_pct', 0)
            name = p.get('name', code)
            
            if pct < -3:
                rating = "**关注**"
                note = "跌幅较大，谨慎观望"
            elif pct < 0:
                rating = "**持有**"
                note = "小幅回调，趋势未破"
            elif pct < 3:
                rating = "**持有**"
                note = "稳步上涨，趋势良好"
            else:
                rating = "**谨慎**"
                note = "涨幅过大，注意风险"
            
            lines.append(f"- **{name}**({code}): {rating} — {note}\n")
    
    lines.append("---\n")
    lines.append(f"*本报告由 Stock Analyzer 自动生成 | {now}*\n")
    
    # 写入文件
    content = '\n'.join(lines)
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(content)
    
    print(f"\n✅ 报告已生成: {output_path}")
    return output_path

def main():
    parser = argparse.ArgumentParser(description='生成股票分析报告')
    parser.add_argument('--codes', type=str, required=True, 
                       help='股票代码，逗号分隔，如 03690.HK,300413.SZ')
    parser.add_argument('--output', type=str, default='report.md',
                       help='输出文件路径（默认: report.md）')
    parser.add_argument('--title', type=str,
                       help='报告标题（可选）')
    args = parser.parse_args()
    
    codes = [c.strip() for c in args.codes.split(',')]
    generate_report(codes, title=args.title, output_path=args.output)

if __name__ == '__main__':
    main()


path: scripts/jq_login.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
聚宽账号配置脚本（交互式安全存储）

使用方法:
    python jq_login.py           # 交互式配置
    python jq_login.py --show     # 查看已配置的账号（不显示密码）
    python jq_login.py --clear    # 清除已配置的账号
    python jq_login.py --test     # 测试账号是否有效
"""

import argparse
import getpass
import json
import os
import sys

CONFIG_FILE = os.path.join(os.path.dirname(__file__), '.jq_config.json')

def load_config():
    """加载已保存的配置"""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, 'r') as f:
            return json.load(f)
    return {}

def save_config(phone, password):
    """安全保存配置（文件权限设为600）"""
    config = {'phone': phone}
    with open(CONFIG_FILE, 'w') as f:
        json.dump(config, f)
    # 保存密码到环境变量（不落盘）
    os.environ['JQDATA_PHONE'] = phone
    os.environ['JQDATA_PASSWORD'] = password
    # 设置文件权限（仅本人可读写）
    os.chmod(CONFIG_FILE, 0o600)
    print(f"✅ 账号 {phone} 已配置")

def clear_config():
    """清除配置"""
    if os.path.exists(CONFIG_FILE):
        os.remove(CONFIG_FILE)
    print("✅ 配置已清除")

def show_config():
    """显示当前配置"""
    config = load_config()
    if config.get('phone'):
        print(f"已配置账号: {config['phone']}")
        print("密码: [已通过环境变量设置，不显示]")
    else:
        print("未配置聚宽账号")

def test_login():
    """测试登录"""
    phone = os.environ.get('JQDATA_PHONE')
    [REDACTED_SECRET]'JQDATA_PASSWORD')
    
    if not phone or not password:
        # 尝试从配置文件加载
        config = load_config()
        if not config.get('phone'):
            print("❌ 未配置账号，请先运行 jq_login.py 进行配置")
            sys.exit(1)
        print("❌ 环境变量未设置，请重新配置: python jq_login.py")
        sys.exit(1)
    
    try:
        import jqdatasdk as jq
        jq.auth(phone, password)
        print(f"✅ 账号 {phone} 登录成功！")
        
        # 显示积分信息
        count = jq.get_query_count()
        print(f"   总积分: {count.get('total', 'N/A')}")
        print(f"   剩余:   {count.get('spare', 'N/A')}")
        
        return True
    except Exception as e:
        print(f"❌ 登录失败: {e}")
        return False

def main():
    parser = argparse.ArgumentParser(description='聚宽账号配置')
    parser.add_argument('--show', action='store_true', help='显示已配置的账号')
    parser.add_argument('--clear', action='store_true', help='清除已配置的账号')
    parser.add_argument('--test', action='store_true', help='测试账号登录')
    args = parser.parse_args()
    
    if args.show:
        show_config()
        return
    
    if args.clear:
        clear_config()
        return
    
    if args.test:
        test_login()
        return
    
    # 交互式配置
    print("\n" + "="*50)
    print("   聚宽 JQData 账号配置")
    print("="*50)
    print()
    print("请输入聚宽账号信息（手机号和密码）")
    print("凭证仅保存在当前会话环境变量中，不落盘存储")
    print()
    
    phone = input("手机号: ").strip()
    if not phone:
        print("❌ 手机号不能为空")
        sys.exit(1)
    
    [REDACTED_SECRET]"密码: ")
    if not [REDACTED_SECRET]"❌ 密码不能为空")
        sys.exit(1)
    
    # 保存配置
    save_config(phone, password)
    
    # 询问是否测试登录
    print()
    test = input("是否测试登录? (y/n): ").strip().lower()
    if test == 'y':
        test_login()

if __name__ == '__main__':
    main()


path: scripts/requirements.txt
content: akshare>=1.10.0
jqdatasdk>=1.8.0
pandas>=1.5.0
requests>=2.28.0


path: scripts/send_email.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
邮件发送脚本

支持 QQ邮箱 和 Gmail，通过环境变量注入凭证（安全）。

使用方法:
    # 先配置环境变量
    export QQ_EMAIL_AUTH_CODE="your_auth_code"
    
    python scripts/send_email.py --to 收件人@example.com --file report.md
    
    # Gmail
    export GMAIL_APP_PASSWORD="your_app_password"
    python scripts/send_email.py --to 收件人@gmail.com --smtp gmail --app-password "$GMAIL_APP_PASSWORD"
"""

import argparse
import os
import smtplib
import ssl
import sys
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr

def get_auth_code():
    """从环境变量获取 QQ 邮箱授权码"""
    code = os.environ.get('QQ_EMAIL_AUTH_CODE')
    if not code:
        print("❌ 未设置 QQ_EMAIL_AUTH_CODE 环境变量")
        print('请运行: export QQ_EMAIL_AUTH_CODE="your_auth_code"')
        sys.exit(1)
    return code

def get_gmail_password():
    """从环境变量获取 Gmail 应用密码"""
    pw = os.environ.get('GMAIL_APP_PASSWORD')
    if not pw:
        print("❌ 未设置 GMAIL_APP_PASSWORD 环境变量")
        print('请运行: export GMAIL_APP_PASSWORD="your_app_password"')
        sys.exit(1)
    return pw

def send_via_qq(to_email, auth_code, file_path, subject=None):
    """通过 QQ 邮箱发送邮件"""
    SMTP_SERVER = 'smtp.qq.com'
    SMTP_PORT = 465

    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    msg = MIMEMultipart()
    msg['From'] = formataddr(('Stock Analyzer', to_email))
    msg['To'] = formataddr(('Recipient', to_email))

    if subject is None:
        import os as _os
        subject = f'📊 股票分析报告 | {_os.path.basename(file_path)}'
    msg['Subject'] = Header(subject, 'utf-8')

    msg.attach(MIMEText(content, 'plain', 'utf-8'))

    print(f"正在连接 QQ 邮箱 SMTP 服务器...")
    context = ssl.create_default_context()
    with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=30, context=context) as smtp:
        smtp.login(to_email, auth_code)
        print("✅ 登录成功")
        smtp.sendmail(to_email, to_email, msg.as_string())
        print("✅ 邮件发送成功")

def send_via_gmail(to_email, app_password, file_path, subject=None):
    """通过 Gmail 发送邮件"""
    SMTP_SERVER = 'smtp.gmail.com'
    SMTP_PORT = 587

    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()

    msg = MIMEMultipart()
    msg['From'] = to_email
    msg['To'] = to_email

    if subject is None:
        import os as _os
        subject = f'📊 股票分析报告 | {_os.path.basename(file_path)}'
    msg['Subject'] = Header(subject, 'utf-8')

    msg.attach(MIMEText(content, 'plain', 'utf-8'))

    print(f"正在连接 Gmail SMTP 服务器...")
    with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30) as smtp:
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()
        smtp.login(to_email, app_password)
        print("✅ 登录成功")
        smtp.sendmail(to_email, to_email, msg.as_string())
        print("✅ 邮件发送成功")

def main():
    parser = argparse.ArgumentParser(description='发送邮件（支持 QQ邮箱 / Gmail）')
    parser.add_argument('--to', type=str, required=True, help='收件人邮箱')
    parser.add_argument('--file', type=str, required=True, help='要发送的文件路径')
    parser.add_argument('--subject', type=str, help='邮件主题（可选）')
    parser.add_argument('--smtp', type=str, choices=['qq', 'gmail'], default='qq',
                       help='SMTP 服务类型（默认 qq）')
    parser.add_argument('--auth-code', type=str,
                       help='QQ 邮箱授权码（建议使用 QQ_EMAIL_AUTH_CODE 环境变量）')
    parser.add_argument('--app-password', type=str,
                       help='Gmail 应用密码（建议使用 GMAIL_APP_PASSWORD 环境变量）')
    args = parser.parse_args()

    if not os.path.exists(args.file):
        print(f"❌ 文件不存在: {args.file}")
        sys.exit(1)

    try:
        if args.smtp == 'qq':
            # 优先使用参数，其次使用环境变量
            auth = args.auth_code or get_auth_code()
            send_via_qq(args.to, auth, args.file, args.subject)
        else:
            pw = args.app_password or get_gmail_password()
            send_via_gmail(args.to, pw, args.file, args.subject)

        print(f"\n📧 已发送到: {args.to}")

    except smtplib.SMTPAuthenticationError:
        print("❌ 认证失败：授权码/应用密码错误或邮箱地址错误")
        sys.exit(1)
    except smtplib.SMTPConnectError:
        print("❌ 连接失败：请检查网络或 SMTP 服务器地址")
        sys.exit(1)
    except Exception as e:
        print(f"❌ 发送失败: {e}")
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/stock_fundamentals.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
股票基本面数据获取脚本

使用聚宽 JQData API 获取基本面数据（PE、PB、市值等）

使用方法:
    # 先配置聚宽账号（环境变量）
    export JQDATA_PHONE="你的手机号"
    export JQDATA_PASSWORD="你的密码"
    
    python scripts/stock_fundamentals.py --codes 300413.SZ,300251.SZ
    python scripts/stock_fundamentals.py --codes 600519.SH --date 2025-01-20
"""

import argparse
import os
import sys

def check_jqdata():
    """检查聚宽是否可用"""
    phone = os.environ.get('JQDATA_PHONE')
    [REDACTED_SECRET]'JQDATA_PASSWORD')
    
    if not phone or not [REDACTED_SECRET]"❌ 未配置聚宽账号")
        print("请先设置环境变量:")
        print('  export JQDATA_PHONE="你的手机号"')
        print('  export JQDATA_PASSWORD="你的密码"')
        print()
        print("或使用交互式配置:")
        print("  python scripts/jq_login.py")
        sys.exit(1)
    
    try:
        import jqdatasdk as jq
        jq.auth(phone, password)
        return jq
    except ImportError:
        print("❌ jqdatasdk 未安装，请运行: pip install jqdatasdk")
        sys.exit(1)
    except Exception as e:
        print(f"❌ 聚宽登录失败: {e}")
        sys.exit(1)

def normalize_code(code):
    """标准化股票代码为聚宽格式"""
    if '.' in code:
        return code  # 已经是聚宽格式
    # 纯数字格式，尝试判断交易所
    if code.startswith('6'):
        return f'{code}.XSHG'
    else:
        return f'{code}.XSHE'

def get_fundamentals(jq, codes, date=None):
    """获取基本面数据"""
    results = []
    
    for code in codes:
        jq_code = normalize_code(code)
        print(f"正在查询 {code} ({jq_code}) 的基本面数据...")
        
        try:
            # 获取估值数据（PE、PB、市值）
            val = jq.get_valuation(jq_code, start_date=date, end_date=date)
            
            if val is not None and len(val) > 0:
                row = val.iloc[0]
                result = {
                    'code': code,
                    'market_cap': row.get('market_cap', None),  # 总市值（亿元）
                    'pe_ratio': row.get('pe_ratio', None),
                    'pb_ratio': row.get('pb_ratio', None),
                    'roe': row.get('roe', None),
                    'revenue': row.get('revenue', None),
                    'net_profit': row.get('net_profit', None),
                }
                results.append(result)
                
                # 打印结果
                print(f"  ✅ 总市值: {result['market_cap']:.2f}亿" if result['market_cap'] else "  ✅ 总市值: N/A")
                print(f"     PE: {result['pe_ratio']:.2f}" if result['pe_ratio'] else "     PE: N/A")
                print(f"     PB: {result['pb_ratio']:.2f}" if result['pb_ratio'] else "     PB: N/A")
            else:
                print(f"  ⚠️  无数据（可能日期超出权限范围）")
                # 尝试不用日期过滤
                val_all = jq.get_valuation(jq_code)
                if val_all is not None and len(val_all) > 0:
                    row = val_all.iloc[-1]
                    print(f"  最近日期 {val_all.index[-1].date() if hasattr(val_all.index[-1], 'date') else val_all.index[-1]}:")
                    print(f"    总市值: {row.get('market_cap', 'N/A')}")
                    print(f"    PE: {row.get('pe_ratio', 'N/A')}")
                    print(f"    PB: {row.get('pb_ratio', 'N/A')}")
                    
        except Exception as e:
            print(f"  ❌ 查询失败: {e}")
    
    return results

def get_financial_statements(jq, code, report_date='2024-12-31'):
    """获取财务报表（利润表、资产负债表、现金流量表）"""
    jq_code = normalize_code(code)
    
    print(f"\n财务报表 ({report_date}):")
    
    try:
        from jqdatasdk import finance
        
        # 利润表
        q_income = jq.query(finance.STK_INCOME_STATEMENT).filter(
            finance.STK_INCOME_STATEMENT.code == jq_code
        )
        df_income = finance.run_query(q_income)
        
        if df_income is not None and len(df_income) > 0:
            # 找到最近的报告期
            latest = df_income[df_income['report_date'] == report_date]
            if len(latest) > 0:
                r = latest.iloc[0]
                print(f"  营业收入: {r.get('operating_revenue', 'N/A'):.2f}" if r.get('operating_revenue') else "  营业收入: N/A")
                print(f"  净利润: {r.get('net_profit', 'N/A'):.2f}" if r.get('net_profit') else "  净利润: N/A")
            else:
                print(f"  ⚠️  无 {report_date} 报告期数据")
        else:
            print("  ⚠️  无利润表数据")
            
    except Exception as e:
        print(f"  ⚠️  财务数据查询失败: {e}")

def main():
    parser = argparse.ArgumentParser(description='获取股票基本面数据')
    parser.add_argument('--codes', type=str, required=True, help='股票代码，逗号分隔，如 300413.SZ,300251.SZ')
    parser.add_argument('--date', type=str, help='查询日期，如 2025-01-20')
    parser.add_argument('--financials', action='store_true', help='同时获取财务报表')
    args = parser.parse_args()
    
    codes = [c.strip() for c in args.codes.split(',')]
    
    print(f"\n📊 正在查询 {len(codes)} 只股票的基本面数据...")
    print(f"查询日期: {args.date or '最新可用日期'}")
    print()
    
    jq = check_jqdata()
    
    # 显示积分信息
    try:
        count = jq.get_query_count()
        print(f"积分余额: {count.get('spare', 'N/A')} / {count.get('total', 'N/A')}")
        print()
    except:
        pass
    
    results = get_fundamentals(jq, codes, args.date)
    
    if args.financials:
        for code in codes:
            get_financial_statements(jq, code)
    
    print(f"\n✅ 完成，共获取 {len(results)}/{len(codes)} 只股票数据")

if __name__ == '__main__':
    main()


path: scripts/stock_query.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
股票实时行情查询脚本
支持 A股 和 港股 的实时行情获取

使用方法:
    python stock_query.py --codes 03690.HK,300413.SZ,300251.SZ
    python stock_query.py --names 美团,芒果超媒,光线传媒
"""

import argparse
import sys
import time

def check_akshare():
    """检查 akshare 是否安装"""
    try:
        import akshare as ak
        return ak
    except ImportError:
        print("❌ akshare 未安装，请运行: pip install akshare")
        sys.exit(1)

def parse_code(code):
    """解析股票代码，返回 (市场, 纯代码)"""
    if '.' in code:
        parts = code.split('.')
        return parts[1], parts[0]  # (市场, 代码)
    else:
        # 默认 A股
        return 'SZ', code

def get_a_stock_price(ak, code):
    """获取 A股 实时行情"""
    try:
        # 获取 A股 实时行情
        df = ak.stock_zh_a_spot_em()
        
        # 查找目标股票
        row = df[df['代码'] == code]
        if row.empty:
            return None
        
        row = row.iloc[0]
        return {
            'code': code,
            'market': 'SZ' if code.startswith('0') or code.startswith('3') else 'SH',
            'name': row['名称'],
            'price': float(row['最新价']),
            'change_pct': float(row['涨跌幅']),
            'volume': float(row['成交量']),
            'amount': float(row['成交额']),
            'high': float(row['最高']),
            'low': float(row['最低']),
            'open': float(row['今开']),
            'prev_close': float(row['昨收']),
        }
    except Exception as e:
        print(f"❌ 获取 A股 {code} 失败: {e}")
        return None

def get_hk_stock_price(ak, code):
    """获取港股实时行情"""
    try:
        # 获取港股实时行情
        df = ak.stock_hk_spot_em()
        
        # 查找目标股票（港股代码格式：03690）
        row = df[df['代码'] == code]
        if row.empty:
            return None
        
        row = row.iloc[0]
        return {
            'code': code,
            'market': 'HK',
            'name': row['名称'],
            'price': float(row['最新价']),
            'change_pct': float(row['涨跌幅']),
            'volume': float(row['成交量']) if '成交量' in row else 0,
            'amount': float(row['成交额']) if '成交额' in row else 0,
            'high': float(row['最高']) if '最高' in row else 0,
            'low': float(row['最低']) if '最低' in row else 0,
        }
    except Exception as e:
        print(f"❌ 获取港股 {code} 失败: {e}")
        return None

def format_price(data):
    """格式化价格显示"""
    if data is None:
        return "❌ 数据获取失败"
    
    market_symbol = {
        'SZ': '¥',
        'SH': '¥',
        'HK': 'HK$',
    }.get(data['market'], '$')
    
    change_symbol = '🔴' if data['change_pct'] < 0 else '🟢' if data['change_pct'] > 0 else '⚪'
    
    return f"{data['name']}({data['code']}.{data['market']}): {market_symbol}{data['price']:.2f} ({change_symbol}{data['change_pct']:+.2f}%)"

def main():
    parser = argparse.ArgumentParser(description='股票实时行情查询')
    parser.add_argument('--codes', type=str, help='股票代码，逗号分隔，如 03690.HK,300413.SZ')
    parser.add_argument('--names', type=str, help='股票名称，逗号分隔（暂不支持）')
    args = parser.parse_args()
    
    if not args.codes and not args.names:
        parser.print_help()
        return
    
    ak = check_akshare()
    
    codes = []
    if args.codes:
        codes = [c.strip() for c in args.codes.split(',')]
    
    print(f"\n📊 正在查询 {len(codes)} 只股票...\n")
    
    results = []
    for code in codes:
        market, pure_code = parse_code(code)
        
        if market == 'HK':
            data = get_hk_stock_price(ak, pure_code)
        else:
            data = get_a_stock_price(ak, pure_code)
        
        if data:
            results.append(data)
            print(format_price(data))
        
        # 避免请求过快
        time.sleep(0.5)
    
    print(f"\n✅ 成功获取 {len(results)}/{len(codes)} 只股票数据")
    
    return results

if __name__ == '__main__':
    main()


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