Skill: 54meteor/stock-assistant (version 1.2.0)

--- name: stock-assistant description: | A股交易辅助工具，集行情查询、交易记录管理、盈亏分析于一体。 用于：(1) 查询A股实时行情 (2) 管理交易记录 (3) 计算持仓和盈亏 (4) 导入/导出CSV 代码目录：D:\aicode\stock-assistant（跨平台：代码会自动适配路径） 调用方式：from fetcher import get_quote 或 python main.py quote <股票代码> --- # 炒股助手 A股交易辅助工具，提供行情查询、交易管理、盈亏分析功能。 ## 代码位置 **项目目录**：`D:\aicode\stock-assistant`（Linux/Mac 路径相同） ``` D:\aicode\stock-assistant/ ├── main.py # CLI 入口 ├── fetcher.py # A股行情获取 ├── trader.py # 交易管理 ├── models.py # 数据模型 ├── db.py # SQLite 操作 ├── requirements.txt # 依赖 └── data/ # 数据库目录 ``` ## 跨平台说明 代码已适配 Windows/Linux/Mac： - Windows：`D:\aicode\stock-assistant\data\trades.db` - Linux/Mac：自动使用脚本所在目录的 `data/trades.db` Python import 时路径自动适配，无需手动指定。 ## 快速命令 | 命令 | 说明 | |------|------| | `python main.py quote 600016` | 查询股票行情（完整数据） | | `python main.py positions` | 查看当前持仓 | | `python main.py pnl 600016` | 查看盈亏(自动获取实时价) | | `python main.py pnl 600016 --price 4.15` | 查看盈亏(指定价格) | | `python main.py list` | 列出所有交易记录 | | `python main.py list --code 600016` | 列出指定股票交易记录 | | `python main.py import trades.csv` | 从CSV导入交易记录 | | `python main.py export --output trades.csv` | 导出交易记录到CSV | | `python main.py add --code 600016 --type buy --quantity 1000 --price 3.90` | 添加买入记录 | | `python main.py add --code 600016 --type sell --quantity 500 --price 4.20` | 添加卖出记录 | | `python main.py delete <id>` | 删除指定ID的交易记录 | | `python main.py notify 600016` | 查询并输出行情(飞书卡片格式) | | `python main.py notify --codes 600016,000001` | 查询多只股票 | | `python main.py notify --codes 600016 --webhook https://xxx` | 推送到飞书群机器人 | | `python main.py notify --codes 600016 --app-id xxx --app-secret xxx --receive-id xxx` | 推送到飞书私聊 | ## 行情查询 API ### Python 调用方式 ```python import sys import os # 自动适配路径（Windows/Linux/Mac） script_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_dir) from fetcher import get_quote from trader import StockTrader # 查询行情 - 返回完整字典数据 quote = get_quote('600016') # 交易管理（无需指定路径，自动使用脚本所在目录） trader = StockTrader() # 返回字段说明： # { # '股票代码': '600016', # '股票名称': '民生银行', # '当前价格': '3.97', # '涨跌额': '0.03', # '涨跌幅(%)': '0.76', # '今开价': '3.93', # '昨收价': '3.94', # '最高价': '4.05', # '最低价': '3.90', # '成交量(手)': '1552322', # '成交额(元)': '547876', # '换手率(%)': '2.35', # '市盈率(动态)': '6.52', # '市净率': '0.65', # '总市值(亿元)': '1825.63', # '流通市值(亿元)': '1523.85', # '涨停价': '4.35', # '跌停价': '3.55', # '买一价': '3.97', '买一量(手)': '10000', # '卖一价': '3.98', '卖一量(手)': '20000', # ... (买二~买五, 卖二~卖五), # '日期时间': '2026-03-14 14:30:00', # '_股票代码': 'sh600016', # '_市场': 'A股' # } ``` ### 返回字段完整列表 | 字段名 | 类型 | 说明 | 示例 | |--------|------|------|------| | **基础信息** |||| | 股票代码 | string | A股代码 | 600016 | | 股票名称 | string | 上市公司名称 | 民生银行 | | _市场 | string | 市场标识 | A股 | | 日期时间 | string | 数据时间 | 2026-03-14 14:30:00 | | **价格数据** |||| | 当前价格 | string | 最新成交价 | 3.97 | | 涨跌额 | string | 涨跌金额 | +0.03 | | 涨跌幅(%) | string | 涨跌幅比例 | +0.76% | | 今开价 | string | 今日开盘价 | 3.93 | | 昨收价 | string | 昨日收盘价 | 3.94 | | 最高价 | string | 今日最高价 | 4.05 | | 最低价 | string | 今日最低价 | 3.90 | | **成交数据** |||| | 成交量(手) | string | 成交数量(手) | 1552322 | | 成交额(元) | string | 成交金额 | 547876 | | 换手率(%) | string | 日换手率 | 2.35% | | **财务指标** |||| | 市盈率(动态) | string | PE TTM | 6.52 | | 市净率 | string | PB | 0.65 | | 总市值(亿元) | string | 总市值 | 1825.63 | | 流通市值(亿元) | string | 流通市值 | 1523.85 | | **涨跌停** |||| | 涨停价 | string | 当日涨停价 | 4.35 | | 跌停价 | string | 当日跌停价 | 3.55 | | **盘口数据（5档）** |||| | 买一价~买五价 | string | 买入价位 | 3.97 | | 买一量~买五量(手) | string | 买入挂单量 | 10000 | | 卖一价~卖五价 | string | 卖出价位 | 3.98 | | 卖一量~卖五量(手) | string | 卖出挂单量 | 20000 | ### CLI 输出格式 ```bash $ python main.py quote 600016 【A股行情】 ================================================== 股票: 民生银行 (600016) 市场: A股 【价格】 当前价: 3.97 涨跌额: 0.03 涨跌幅: 0.76% 【开盘收盘】 今开: 3.93 昨收: 3.94 最高: 4.05 最低: 3.90 【成交】 成交量: 1552322 手 成交额: 547876 元 换手率: 2.35% 【财务指标】 市盈率(动态): 6.52 市净率: 0.65 总市值: 1825.63 亿元 流通市值: 1523.85 亿元 【涨跌停】 涨停价: 4.35 跌停价: 3.55 【盘口】 买一: 3.97 (10000) → 卖一: 3.98 (20000) 买二: 3.96 (15000) → 卖二: 3.99 (18000) ... 更新时间: 2026-03-14 14:30:00 ``` ## 交易管理 API ```python import sys sys.path.insert(0, 'D:\\aicode\\stock-assistant') from trader import StockTrader trader = StockTrader('D:\\aicode\\stock-assistant\\data\\trades.db') # 导入CSV trader.import_csv('trades.csv') # 获取持仓 positions = trader.get_positions() # 计算盈亏 pnl = trader.get_pnl_with_live_price('600016') print(f"盈亏: {pnl.profit:.2f} ({pnl.profit_pct:.2f}%)") ``` ## 数据格式 ### CSV 导入/导出格式 ```csv 日期,时间,股票代码,股票名称,买卖方向,成交数量,成交均价,成交金额,佣金,其他费用,印花税,过户费 2026-03-14,10:30:00,600016,民生银行,买入,1000,3.90,3900.00,2.73,0.30,0.00,0.04 2026-03-14,14:30:00,600016,民生银行,卖出,500,4.10,2050.00,1.44,0.20,2.05,0.02 ``` ### 字段说明 | 字段 | 必填 | 说明 | |------|------|------| | 日期 | 是 | 交易日期，格式 YYYY-MM-DD | | 时间 | 否 | 交易时间，格式 HH:MM:SS | | 股票代码 | 是 | A股代码，如 600016 | | 股票名称 | 否 | 股票名称，如 民生银行 | | 买卖方向 | 是 | 买入/卖出 | | 成交数量 | 是 | 股数 | | 成交均价 | 是 | 价格 | | 成交金额 | 是 | 数量 × 价格 | | 佣金 | 否 | 手续费，默认0 | | 其他费用 | 否 | 其他费用，默认0 | | 印花税 | 否 | 印花税（卖出收），默认0 | | 过户费 | 否 | 过户费，默认0 | ## 注意事项 1. **只支持A股** - 不支持港股、美股 2. **数据来源** - 腾讯财经（免费接口） 3. **存储** - SQLite 本地数据库 `data/trades.db` 4. **路径** - 所有路径使用 Windows 格式，如 `D:\aicode\stock-assistant` ## 后续更新 项目代码更新后，同步更新此 skill 文档。 --- ## 更新规则 Skill 文件位置：`D:\aicode\stock-assistant\SKILL.md` 更新时同步修改： - 项目代码更新后 - 新增功能后 - 参数变化后 直接编辑这个文件即可，格式是 Markdown。

path: DESIGN.md
content: # 炒股助手 - 设计方案

## 1. 项目概述

**名称**：炒股助手
**定位**：A股交易辅助工具，集行情查询、交易记录管理、盈亏分析于一体
**代码目录**：`<YOUR_PATH>`

## 2. 技术栈

- **语言**：Python 3.x
- **存储**：SQLite（轻量级，无需配置）
- **接口**：腾讯财经（免费，无需认证）

## 3. 目录结构

```
<YOUR_PATH>/
├── main.py              # CLI 入口
├── fetcher.py           # 行情获取（A股）
├── trader.py            # 交易逻辑
├── db.py                # 数据库操作
├── models.py            # 数据模型
├── exporter.py          # 导出功能
├── requirements.txt     # 依赖
└── data/                # 数据目录
    └── trades.db        # SQLite数据库
```

## 4. 功能设计

### 4.1 行情查询
```bash
# 查询股票行情
python main.py quote 600016
python main.py quote 000001
```

**返回数据**：当前价、涨跌额、涨跌幅、今开、昨收、最高、最低、成交量、成交额、换手率、市盈率、市净率等

### 4.2 交易记录管理
```bash
# 导入CSV
python main.py import trades.csv

# 手动添加交易
python main.py add --code 600016 --name 民生银行 --type buy --quantity 1700 --price 3.90

# 查看持仓
python main.py positions

# 查看指定股票盈亏
python main.py pnl 600016 --current-price 4.15

# 导出记录
python main.py export --format csv
```

### 4.3 费率推算
根据交易记录反推佣金率、印花税率、过户费

## 5. 数据模型

### 交易记录表 (trades)
| 字段 | 类型 | 说明 |
|-----|------|-----|
| id | INTEGER | 主键 |
| trade_date | TEXT | 交易日期 |
| trade_time | TEXT | 交易时间 |
| code | TEXT | 股票代码 |
| name | TEXT | 股票名称 |
| direction | TEXT | 买入/卖出 |
| quantity | INTEGER | 成交数量 |
| price | REAL | 成交均价 |
| amount | REAL | 成交金额 |
| commission | REAL | 佣金 |
| fee | REAL | 其他费用 |
| stamp_tax | REAL | 印花税 |
| transfer_fee | REAL | 过户费 |
| created_at | TEXT | 记录时间 |

### 持仓表 (positions)
| 字段 | 类型 | 说明 |
|-----|------|-----|
| code | TEXT | 股票代码 |
| name | TEXT | 股票名称 |
| quantity | INTEGER | 持仓数量 |
| avg_cost | REAL | 平均成本 |
| total_cost | REAL | 总成本 |

## 6. 开发计划

### Phase 1: 基础功能
- [x] 行情查询（A股）
- [x] CSV导入
- [x] SQLite存储
- [x] 持仓计算
- [x] 盈亏计算

### Phase 2: 增强
- [ ] 费率自动推算
- [ ] 批量查询多只股票
- [ ] 统计报表

### Phase 3: 智能化
- [ ] 股价提醒
- [ ] 持仓预警


path: data/import.csv
content: 成交日期,成交时间,证券代码,证券名称,委托方向,成交数量,成交均价,成交金额,佣金,其他费用,印花税,过户费,资金余额,股份余额,委托编号,成交编号,交易市场,股东账号,币种,
2026-03-13,10:59:54,"= ""600016      """,民生银行,证券卖出,10200,4.000,40800.000,7.990,2.21,20.40,0.41,0.100,10300,1057433,36730265,沪市A股,A580723221,人民币,
2026-02-24,10:17:47,"= ""600016      """,民生银行,证券买入,1700,3.900,6630.000,4.640,0.36,0.00,0.06,0.110,20500,504821,25595339,沪市A股,A580723221,人民币,
2026-02-13,09:44:57,"= ""600016      """,民生银行,证券买入,9400,3.940,37036.000,7.260,2.00,0.00,0.37,6652.170,18800,221668,10561197,沪市A股,A580723221,人民币,
2026-02-11,14:27:33,"= ""600016      """,民生银行,证券卖出,5400,4.010,21654.000,4.240,1.17,10.83,0.22,0.100,9400,1285893,46683114,沪市A股,A580723221,人民币,
2026-02-10,10:27:34,"= ""600016      """,民生银行,证券卖出,5600,3.950,22120.000,4.340,1.19,11.06,0.22,22013.820,14800,650777,24649550,沪市A股,A580723221,人民币,
2025-12-11,09:50:05,"= ""600016      """,民生银行,证券买入,5600,3.930,22008.000,4.310,1.19,0.00,0.22,0.100,20400,165042,11776846,沪市A股,A580723221,人民币,
2025-12-10,09:43:42,"= ""600016      """,民生银行,证券买入,5400,3.970,21438.000,4.200,1.16,0.00,0.22,0.090,14800,263789,9317808,沪市A股,A580723221,人民币,
2025-12-08,09:57:25,"= ""000725      """,京东方Ａ,证券卖出,10300,4.240,43672.000,8.120,2.80,21.84,0.00,0.100,0,472447,0103000018540908,深市A股,"= ""0137474483    """,人民币,
2025-12-02,10:26:46,"= ""000725      """,京东方Ａ,证券买入,10300,3.890,40067.000,7.450,2.57,0.00,0.00,0.100,10300,478138,0103000026974181,深市A股,"= ""0137474483    """,人民币,
2025-12-01,10:16:34,"= ""600016      """,民生银行,证券卖出,9700,4.130,40061.000,7.850,2.17,20.03,0.40,0.110,9400,427667,21085662,沪市A股,A580723221,人民币,
2025-12-01,09:41:27,"= ""600016      """,民生银行,证券买入,9400,4.100,38540.000,7.560,2.08,0.00,0.38,-40030.440,19100,191086,9156124,沪市A股,A580723221,人民币,
2025-11-28,11:05:21,"= ""601000      """,唐山港,证券卖出,10000,3.840,38400.000,7.530,2.07,19.20,0.38,0.100,0,569501,24739498,沪市A股,A580723221,人民币,
2025-11-28,10:17:10,"= ""600016      """,民生银行,证券买入,4600,4.090,18814.000,3.980,1.02,0.00,0.19,-38370.720,9700,480138,17002794,沪市A股,A580723221,人民币,
2025-11-27,11:02:25,"= ""600016      """,民生银行,证券卖出,4600,4.120,18952.000,3.970,1.03,9.48,0.19,18773.290,5100,580270,26378392,沪市A股,A580723221,人民币,
2025-11-26,10:59:06,"= ""600016      """,民生银行,证券买入,5100,4.090,20859.000,4.090,1.12,0.00,0.22,0.090,9700,705985,25671061,沪市A股,A580723221,人民币,
2025-11-26,09:33:13,"= ""601000      """,唐山港,证券买入,5000,3.890,19450.000,3.950,1.05,0.00,0.19,20864.520,10000,78925,3260589,沪市A股,A580723221,人民币,
2025-11-18,10:25:53,"= ""601000      """,唐山港,证券买入,5000,4.000,20000.000,3.900,1.10,0.00,0.18,0.120,5000,651999,24524943,沪市A股,A580723221,人民币,
2025-11-05,11:19:52,"= ""600016      """,民生银行,证券买入,2300,4.030,9269.000,4.490,0.51,0.00,0.09,0.100,4600,259767,34009885,沪市A股,A580723221,人民币,
2025-11-04,11:12:38,"= ""600016      """,民生银行,证券买入,2300,4.040,9292.000,4.490,0.51,0.00,0.09,0.100,2300,760280,34476461,沪市A股,A580723221,人民币,


path: data/import_converted.csv
content: 日期,时间,股票代码,股票名称,买卖方向,成交数量,成交均价,成交金额,佣金,其他费用,印花税,过户费
2026-03-13,10:59:54,600016,民生银行,卖出,10200,4.0,40800.0,7.99,2.21,20.4,0.41
2026-02-24,10:17:47,600016,民生银行,买入,1700,3.9,6630.0,4.64,0.36,0.0,0.06
2026-02-13,09:44:57,600016,民生银行,买入,9400,3.94,37036.0,7.26,2.0,0.0,0.37
2026-02-11,14:27:33,600016,民生银行,卖出,5400,4.01,21654.0,4.24,1.17,10.83,0.22
2026-02-10,10:27:34,600016,民生银行,卖出,5600,3.95,22120.0,4.34,1.19,11.06,0.22
2025-12-11,09:50:05,600016,民生银行,买入,5600,3.93,22008.0,4.31,1.19,0.0,0.22
2025-12-10,09:43:42,600016,民生银行,买入,5400,3.97,21438.0,4.2,1.16,0.0,0.22
2025-12-08,09:57:25,000725,京东方Ａ,卖出,10300,4.24,43672.0,8.12,2.8,21.84,0.0
2025-12-02,10:26:46,000725,京东方Ａ,买入,10300,3.89,40067.0,7.45,2.57,0.0,0.0
2025-12-01,10:16:34,600016,民生银行,卖出,9700,4.13,40061.0,7.85,2.17,20.03,0.4
2025-12-01,09:41:27,600016,民生银行,买入,9400,4.1,38540.0,7.56,2.08,0.0,0.38
2025-11-28,11:05:21,601000,唐山港,卖出,10000,3.84,38400.0,7.53,2.07,19.2,0.38
2025-11-28,10:17:10,600016,民生银行,买入,4600,4.09,18814.0,3.98,1.02,0.0,0.19
2025-11-27,11:02:25,600016,民生银行,卖出,4600,4.12,18952.0,3.97,1.03,9.48,0.19
2025-11-26,10:59:06,600016,民生银行,买入,5100,4.09,20859.0,4.09,1.12,0.0,0.22
2025-11-26,09:33:13,601000,唐山港,买入,5000,3.89,19450.0,3.95,1.05,0.0,0.19
2025-11-18,10:25:53,601000,唐山港,买入,5000,4.0,20000.0,3.9,1.1,0.0,0.18
2025-11-05,11:19:52,600016,民生银行,买入,2300,4.03,9269.0,4.49,0.51,0.0,0.09
2025-11-04,11:12:38,600016,民生银行,买入,2300,4.04,9292.0,4.49,0.51,0.0,0.09


path: db.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据库操作
"""

import sqlite3
import os
import sys
from typing import List, Optional, Dict, Any
from contextlib import contextmanager

# 处理相对导入 - 检查是否作为模块导入
try:
    from .models import Trade, Position
except ImportError:
    from models import Trade, Position


class Database:
    def __init__(self, db_path: str = 'data/trades.db'):
        self.db_path = db_path
        self._ensure_dir()
        self._init_db()
    
    def _ensure_dir(self):
        """确保数据目录存在"""
        dir_path = os.path.dirname(self.db_path)
        if dir_path and not os.path.exists(dir_path):
            os.makedirs(dir_path)
    
    @contextmanager
    def _get_conn(self):
        """获取数据库连接"""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        try:
            yield conn
            conn.commit()
        finally:
            conn.close()
    
    def _init_db(self):
        """初始化数据库表"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            
            # 交易记录表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS trades (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    trade_date TEXT NOT NULL,
                    trade_time TEXT,
                    code TEXT NOT NULL,
                    name TEXT,
                    direction TEXT NOT NULL,
                    quantity INTEGER NOT NULL,
                    price REAL NOT NULL,
                    amount REAL NOT NULL,
                    commission REAL DEFAULT 0,
                    fee REAL DEFAULT 0,
                    stamp_tax REAL DEFAULT 0,
                    transfer_fee REAL DEFAULT 0,
                    created_at TEXT NOT NULL
                )
            ''')
            
            # 创建索引
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_trades_code ON trades(code)
            ''')
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_trades_date ON trades(trade_date)
            ''')
    
    def add_trade(self, trade: Trade) -> int:
        """添加交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO trades (
                    trade_date, trade_time, code, name, direction,
                    quantity, price, amount, commission, fee,
                    stamp_tax, transfer_fee, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (
                trade.trade_date, trade.trade_time, trade.code, trade.name,
                trade.direction, trade.quantity, trade.price, trade.amount,
                trade.commission, trade.fee, trade.stamp_tax, trade.transfer_fee,
                trade.created_at
            ))
            return cursor.lastrowid
    
    def get_trades(self, code: Optional[str] = None) -> List[Trade]:
        """获取交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            
            if code:
                cursor.execute(
                    'SELECT * FROM trades WHERE code = ? ORDER BY trade_date DESC, trade_time DESC',
                    (code,)
                )
            else:
                cursor.execute('SELECT * FROM trades ORDER BY trade_date DESC, trade_time DESC')
            
            rows = cursor.fetchall()
            return [self._row_to_trade(row) for row in rows]
    
    def get_trade_by_id(self, trade_id: int) -> Optional[Trade]:
        """根据ID获取交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            cursor.execute('SELECT * FROM trades WHERE id = ?', (trade_id,))
            row = cursor.fetchone()
            return self._row_to_trade(row) if row else None
    
    def update_trade(self, trade: Trade) -> bool:
        """更新交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            cursor.execute('''
                UPDATE trades SET
                    trade_date = ?, trade_time = ?, code = ?, name = ?,
                    direction = ?, quantity = ?, price = ?, amount = ?,
                    commission = ?, fee = ?, stamp_tax = ?, transfer_fee = ?
                WHERE id = ?
            ''', (
                trade.trade_date, trade.trade_time, trade.code, trade.name,
                trade.direction, trade.quantity, trade.price, trade.amount,
                trade.commission, trade.fee, trade.stamp_tax, trade.transfer_fee,
                trade.id
            ))
            return cursor.rowcount > 0
    
    def delete_trade(self, trade_id: int) -> bool:
        """删除交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            cursor.execute('DELETE FROM trades WHERE id = ?', (trade_id,))
            return cursor.rowcount > 0
    
    def delete_all_trades(self) -> int:
        """删除所有交易记录"""
        with self._get_conn() as conn:
            cursor = conn.cursor()
            cursor.execute('DELETE FROM trades')
            return cursor.rowcount
    
    def get_positions(self) -> List[Position]:
        """计算持仓"""
        trades = self.get_trades()
        
        # 按股票代码分组
        positions: Dict[str, Position] = {}
        
        for trade in trades:
            if trade.code not in positions:
                positions[trade.code] = Position(
                    code=trade.code,
                    name=trade.name,
                    quantity=0,
                    avg_cost=0.0,
                    total_cost=0.0
                )
            
            pos = positions[trade.code]
            
            if trade.direction == '买入':
                # 计算新的平均成本
                total_cost = pos.total_cost + trade.amount + trade.commission + trade.fee
                total_qty = pos.quantity + trade.quantity
                pos.quantity = total_qty
                pos.total_cost = total_cost
                pos.avg_cost = total_cost / total_qty if total_qty > 0 else 0
            elif trade.direction == '卖出':
                pos.quantity -= trade.quantity
                pos.total_cost -= trade.amount - trade.commission - trade.fee
        
        # 过滤掉持仓为0的
        return [p for p in positions.values() if p.quantity > 0]
    
    def _row_to_trade(self, row) -> Trade:
        """将数据库行转换为Trade对象"""
        return Trade(
            id=row['id'],
            trade_date=row['trade_date'],
            trade_time=row['trade_time'],
            code=row['code'],
            name=row['name'],
            direction=row['direction'],
            quantity=row['quantity'],
            price=row['price'],
            amount=row['amount'],
            commission=row['commission'],
            fee=row['fee'],
            stamp_tax=row['stamp_tax'],
            transfer_fee=row['transfer_fee'],
            created_at=row['created_at']
        )


# 全局数据库实例
_db: Optional[Database] = None


def get_db(db_path: str = 'data/trades.db') -> Database:
    """获取数据库实例"""
    global _db
    if _db is None:
        _db = Database(db_path)
    return _db


path: main.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
炒股助手 - CLI 入口
"""

import sys
import argparse
import os
import json

# 确保当前目录在 Python 路径中
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from fetcher import StockFetcher
from trader import StockTrader


def cmd_quote(args):
    """行情查询"""
    fetcher = StockFetcher()
    result = fetcher.get_quote(args.code)
    print(fetcher.format_quote(result))


def cmd_positions(args):
    """查看持仓"""
    trader = StockTrader()
    positions = trader.get_positions()
    
    if not positions:
        print("暂无持仓")
        return
    
    print("\n【当前持仓】")
    print("-" * 60)
    print(f"{'代码':<10} {'名称':<10} {'数量':<10} {'成本价':<12} {'总成本':<15}")
    print("-" * 60)
    
    total_market_value = 0
    total_cost = 0
    
    for pos in positions:
        print(f"{pos.code:<10} {pos.name:<10} {pos.quantity:<10} {pos.avg_cost:<12.4f} {pos.total_cost:<15.2f}")
        total_cost += pos.total_cost
    
    print("-" * 60)
    print(f"总成本: {total_cost:.2f}")


def cmd_pnl(args):
    """盈亏计算"""
    trader = StockTrader()
    
    try:
        if args.price:
            # 使用指定价格
            pnl = trader.calculate_pnl(args.code, args.price)
        else:
            # 获取实时价格
            pnl = trader.get_pnl_with_live_price(args.code)
        
        if not pnl:
            print(f"未找到股票 {args.code} 的持仓")
            return
        
        print(f"\n【{pnl.name} ({pnl.code})】")
        print("-" * 40)
        print(f"持仓数量: {pnl.quantity} 股")
        print(f"成本均价: {pnl.avg_cost:.4f} 元")
        print(f"当前价格: {pnl.current_price:.4f} 元")
        print(f"市值: {pnl.market_value:.2f} 元")
        print(f"盈亏: {pnl.profit:+.2f} 元 ({pnl.profit_pct:+.2f}%)")
        
    except ValueError as e:
        print(f"错误: {e}")


def cmd_list(args):
    """列出交易记录"""
    trader = StockTrader()
    trades = trader.list_trades(args.code)
    
    if not trades:
        print("暂无交易记录")
        return
    
    print(f"\n【交易记录】{' (筛选: ' + args.code + ')' if args.code else ''}")
    print("-" * 80)
    print(f"{'日期':<12} {'代码':<10} {'名称':<10} {'方向':<6} {'数量':<8} {'价格':<10} {'金额':<12}")
    print("-" * 80)
    
    for trade in trades:
        print(f"{trade.trade_date:<12} {trade.code:<10} {trade.name:<10} {trade.direction:<6} "
              f"{trade.quantity:<8} {trade.price:<10.4f} {trade.amount:<12.2f}")
    
    print("-" * 80)
    print(f"共 {len(trades)} 条记录")


def cmd_import(args):
    """导入CSV"""
    if not os.path.exists(args.file):
        print(f"文件不存在: {args.file}")
        return
    
    trader = StockTrader()
    try:
        count = trader.import_csv(args.file)
        print(f"成功导入 {count} 条交易记录")
    except Exception as e:
        print(f"导入失败: {e}")


def cmd_export(args):
    """导出CSV"""
    trader = StockTrader()
    try:
        count = trader.export_csv(args.output, args.code)
        print(f"成功导出 {count} 条记录到 {args.output}")
    except Exception as e:
        print(f"导出失败: {e}")


def cmd_add(args):
    """手动添加交易"""
    trader = StockTrader()
    
    trade_data = {
        'date': args.date,
        'time': args.time or '',
        'code': args.code,
        'name': args.name or '',
        'direction': '买入' if args.type == 'buy' else '卖出',
        'quantity': args.quantity,
        'price': args.price,
        'amount': args.quantity * args.price,
    }
    
    try:
        trade_id = trader.add_trade(trade_data)
        print(f"成功添加交易记录 (ID: {trade_id})")
    except Exception as e:
        print(f"添加失败: {e}")


def cmd_delete(args):
    """删除交易记录"""
    trader = StockTrader()
    
    if trader.delete_trade(args.id):
        print(f"已删除交易记录 ID: {args.id}")
    else:
        print(f"删除失败，记录不存在")


def cmd_notify(args):
    """推送股价到飞书"""
    from fetcher import StockFetcher, send_feishu, send_feishu_private
    from trader import StockTrader
    import json
    
    codes = args.codes.split(',') if args.codes else [args.code]
    fetcher = StockFetcher()
    trader = StockTrader()
    
    messages = []
    
    for code in codes:
        code = code.strip()
        if not code:
            continue
        
        # 获取行情
        result = fetcher.get_quote(code)
        
        # 获取持仓
        position = trader.get_position(code)
        
        # 生成卡片（带持仓信息）
        message = fetcher.format_table(result, position)
        messages.append(message)
    
    # 合并多张卡片
    full_message = messages[0] if len(messages) == 1 else json.dumps({
        'config': {'wide_screen_mode': True},
        'header': {'title': {'tag': 'plain_text', 'content': '📊 多只股票行情'}, 'template': 'blue'},
        'elements': [{'tag': 'div', 'text': {'tag': 'lark_md', 'content': m}} for m in messages]
    })
    
    if args.webhook:
        # 发送到飞书 Webhook (群机器人) - 需要转文本
        text_content = "\n\n---\n\n".join([json.loads(m).get('elements', [])[0].get('text', {}).get('content', '') if m.startswith('{') else m for m in messages])
        success = send_feishu(args.webhook, text_content)
        if success:
            print("✅ 已发送到飞书群")
        else:
            print("❌ 发送失败")
    elif args.app_id and args.app_secret and args.receive_id:
        # 发送到飞书私聊 - 使用卡片格式
        success = send_feishu_private(full_message, args.app_id, args.app_secret, args.receive_id, is_card=True)
        if success:
            print("✅ 已发送到飞书私聊")
        else:
            print("❌ 发送失败")
    else:
        # 只输出
        print(full_message)


def main():
    parser = argparse.ArgumentParser(
        description='炒股助手 - A股交易辅助工具',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  python main.py quote 600016              查询行情
  python main.py positions                 查看持仓
  python main.py pnl 600016                 查看盈亏(实时价格)
  python main.py pnl 600016 --price 4.15    查看盈亏(指定价格)
  python main.py import trades.csv          导入CSV
  python main.py add --code 600016 --type buy --quantity 1000 --price 3.90
  python main.py notify 600016              查询并输出行情(多数据源备用)
  python main.py notify --codes 600016,000001 --webhook https://xxx  推送到飞书
        '''
    )
    
    subparsers = parser.add_subparsers(dest='command', help='子命令')
    
    # 行情查询
    p_quote = subparsers.add_parser('quote', help='查询股票行情')
    p_quote.add_argument('code', help='股票代码')
    p_quote.set_defaults(func=cmd_quote)
    
    # 持仓
    p_pos = subparsers.add_parser('positions', help='查看当前持仓')
    p_pos.set_defaults(func=cmd_positions)
    
    # 盈亏
    p_pnl = subparsers.add_parser('pnl', help='计算盈亏')
    p_pnl.add_argument('code', help='股票代码')
    p_pnl.add_argument('--price', type=float, help='指定价格(不指定则获取实时价格)')
    p_pnl.set_defaults(func=cmd_pnl)
    
    # 交易记录
    p_list = subparsers.add_parser('list', help='列出交易记录')
    p_list.add_argument('--code', help='筛选股票代码')
    p_list.set_defaults(func=cmd_list)
    
    # 导入
    p_import = subparsers.add_parser('import', help='导入CSV文件')
    p_import.add_argument('file', help='CSV文件路径')
    p_import.set_defaults(func=cmd_import)
    
    # 导出
    p_export = subparsers.add_parser('export', help='导出CSV文件')
    p_export.add_argument('--output', default='trades.csv', help='输出文件路径')
    p_export.add_argument('--code', help='筛选股票代码')
    p_export.set_defaults(func=cmd_export)
    
    # 添加
    p_add = subparsers.add_parser('add', help='手动添加交易')
    p_add.add_argument('--code', required=True, help='股票代码')
    p_add.add_argument('--name', help='股票名称')
    p_add.add_argument('--type', choices=['buy', 'sell'], required=True, help='买入/卖出')
    p_add.add_argument('--quantity', type=int, required=True, help='数量')
    p_add.add_argument('--price', type=float, required=True, help='价格')
    p_add.add_argument('--date', default='', help='交易日期(默认今天)')
    p_add.add_argument('--time', help='交易时间')
    p_add.set_defaults(func=cmd_add)
    
    # 删除
    p_del = subparsers.add_parser('delete', help='删除交易记录')
    p_del.add_argument('id', type=int, help='交易记录ID')
    p_del.set_defaults(func=cmd_delete)
    
    # 推送
    p_notify = subparsers.add_parser('notify', help='推送股价到飞书(定时任务用)')
    p_notify.add_argument('code', nargs='?', help='股票代码(可省略，配合 --codes 使用)')
    p_notify.add_argument('--codes', help='多个股票代码，用逗号分隔')
    p_notify.add_argument('--webhook', help='飞书 Webhook URL (群机器人)')
    p_notify.add_argument('--app-id', dest='app_id', help='飞书应用 App ID (私聊)')
    p_notify.add_argument('--app-secret', dest='app_secret', help='飞书应用 App Secret (私聊)')
    p_notify.add_argument('--receive-id', dest='receive_id', help='接收者 ID (私聊)')
    p_notify.set_defaults(func=cmd_notify)
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        return
    
    # 设置默认日期
    if hasattr(args, 'date') and args.date == '':
        from datetime import datetime
        args.date = datetime.now().strftime('%Y-%m-%d')
    
    args.func(args)


if __name__ == '__main__':
    main()


path: models.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据模型
"""

from dataclasses import dataclass
from typing import Optional
from datetime import datetime


@dataclass
class Trade:
    """交易记录"""
    id: Optional[int] = None
    trade_date: str = ''
    trade_time: str = ''
    code: str = ''
    name: str = ''
    direction: str = ''  # 买入/卖出
    quantity: int = 0
    price: float = 0.0
    amount: float = 0.0
    commission: float = 0.0
    fee: float = 0.0
    stamp_tax: float = 0.0
    transfer_fee: float = 0.0
    created_at: str = ''
    
    def __post_init__(self):
        if not self.created_at:
            self.created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')


@dataclass
class Position:
    """持仓"""
    code: str = ''
    name: str = ''
    quantity: int = 0
    avg_cost: float = 0.0
    total_cost: float = 0.0
    
    @property
    def market_value(self) -> float:
        """市值"""
        return self.quantity * self.avg_cost
    
    @property
    def profit(self) -> float:
        """盈亏金额"""
        return 0.0  # 需要传入当前价计算


@dataclass
class PnLResult:
    """盈亏结果"""
    code: str = ''
    name: str = ''
    quantity: int = 0
    avg_cost: float = 0.0
    current_price: float = 0.0
    market_value: float = 0.0
    profit: float = 0.0
    profit_pct: float = 0.0


path: notify_job.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
股票定时推送脚本
用法: python notify_job.py --codes 600016,000725 --app-id xxx --app-secret xxx --receive-id xxx
"""

import sys
import os
import argparse
import json

# 添加当前目录到 Python 路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from fetcher import StockFetcher, send_feishu_private
from trader import StockTrader


def main():
    parser = argparse.ArgumentParser(description='股票定时推送')
    parser.add_argument('--codes', required=True, help='股票代码，用逗号分隔')
    parser.add_argument('--app-id', required=True, help='飞书应用 App ID')
    parser.add_argument('--app-secret', required=True, help='飞书应用 App Secret')
    parser.add_argument('--receive-id', required=True, help='接收者 ID')
    
    args = parser.parse_args()
    
    codes = [c.strip() for c in args.codes.split(',') if c.strip()]
    fetcher = StockFetcher()
    trader = StockTrader()
    
    messages = []
    
    for code in codes:
        # 获取行情
        result = fetcher.get_quote(code)
        
        # 获取持仓
        position = trader.get_position(code)
        
        # 生成卡片
        message = fetcher.format_table(result, position)
        messages.append(message)
    
    # 分别发送每只股票的卡片
    success_count = 0
    for message in messages:
        success = send_feishu_private(message, args.app_id, args.app_secret, args.receive_id, is_card=True)
        if success:
            success_count += 1
    
    if success_count == len(messages):
        print("✅ 已发送")
        return 0
    else:
        print("❌ 发送失败")
        return 1


if __name__ == '__main__':
    sys.exit(main())


path: requirements.txt
content: requests>=2.28.0


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