stock-portfolio/SPEC.md
kennethcheng 0051f92b2b v1.0.5 (2026-04-13)
- 📛 证券名称增强:修复 INTC 等证券名称显示为空的问题 (BUG-101)
- 🔒 SELL超量校验:修复卖出数量超过持仓导致持仓变负的Bug (BUG-002)
- 🔄 撤销BUY成本还原:修复撤销买入时平均成本计算公式错误 (BUG-003)
- 💹 Decimal精度计算:持仓分析改用Prisma.Decimal进行金融计算,防止浮点精度丢失 (BUG-004)
- 📛 证券名称显示:在持仓分析卡片、资产分布、盈亏排行等位置同时显示股票代码和名称
- 📋 证券数据库扩展:新增 Intel Corp. (INTC) 证券记录
- 🔍 回退逻辑增强:确保证券名称为空时显示代码而非空白
- 📈 腾讯行情解析升级:精准解析股票名称(索引1),支持港/A/美股及 ETF 名称自动获取
- 🔀 多市场涨跌解析修复:重构腾讯行情多市场适配层,美股(索引4/5)与港A股(索引31/32)使用差异化索引解析涨跌数据
- 🇨🇳 GBK中文解码修复:改用 `arrayBuffer() + TextDecoder('gbk')` 替代 `text()`,彻底解决A股/港股中文股票名称乱码问题
- 💱 JisuAPI实时汇率:接入 JisuAPI 获取实时汇率,缓存1小时,支持 CNY/HKD/USD 转换
- 📊 资产配置动态图:环形图改由后端实时聚合持仓数据驱动,支持 Tooltip 和百分比显示
- 🎨 资产配置图表优化:精美毛玻璃 Tooltip、颜色图标、去除生硬描边、useMemo 性能优化
- 💱 全局货币联动:资产配置图表数值随 CNY/USD/HKD 切换实时转换
- 📝 交易流水增强:新增证券名称列,显示"名称+代码"双行格式
- 💹 全局汇率展示:在导航栏实时显示 USD/CNY/HKD 汇率信息
- 🔧 BUG-202 修复:修正 `convertCurrency` 汇率换算逻辑(原逻辑除法/乘法颠倒,导致 USD→CNY 换算失效)
- 🔧 BUG-201 修复:腾讯行情 API 获取失败时,`priceAvailable` 标记配合前端显示 "N/A" 替代虚假 0%
- 🔧 BUG-203 增强:持仓分析 `name` 字段确保回退到 `pos.symbol`,名称永不空
- 💹 Decimal 精度保障:所有盈亏/汇率计算均使用 Prisma.Decimal,防止浮点精度丢失
2026-04-13 18:41:37 +08:00

21 KiB
Raw Blame History

个人投资持仓管理系统 - 需求规格说明书

文档版本v1.0.0
日期2026-04-12
状态:已完成


1. 产品概述

1.1 产品名称

个人投资持仓管理系统Stock Portfolio Manager

1.2 产品简介

现代化、全面化的个人投资组合管理平台支持多市场美股、A股、港股、加密货币统一管理实时获取行情数据自动计算持仓成本与盈亏。

1.3 目标用户

  • 拥有多个市场投资账户的个人投资者
  • 需要统一管理分散在不同平台的投资组合
  • 希望实时了解总体资产配置和盈亏状况

2. 技术架构

2.1 技术栈

层级 技术 版本
前端框架 Next.js 16
UI 框架 React 19
类型系统 TypeScript 5
样式方案 Tailwind CSS 4
组件库 shadcn/ui -
图表库 Recharts -
后端框架 Next.js API Routes -
ORM Prisma -
数据库 PostgreSQL 16

2.2 系统架构

┌─────────────────────────────────────────────────────┐
│                    前端 (Next.js)                     │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ Dashboard │  │ 持仓明细 │  │ 交易流水 │  │  分析  │ │
│  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘ │
└───────┼────────────┼────────────┼────────────┼───────┘
        │            │            │            │
        └────────────┴─────┬──────┴────────────┘
                           │ REST API
        ┌──────────────────┼──────────────────┐
        │                  │                  │
   ┌────▼────┐     ┌──────▼──────┐   ┌──────▼──────┐
   │ Accounts │     │ Transactions │   │  Positions  │
   │   API    │     │     API      │   │     API     │
   └────┬────┘     └──────┬──────┘   └──────┬──────┘
        │                  │                  │
   ┌────▼──────────────────▼──────────────────▼────┐
   │              Prisma ORM                        │
   └────────────────────┬─────────────────────────┘
                        │
   ┌────────────────────▼─────────────────────────┐
   │              PostgreSQL Database               │
   │  User │ Account │ Security │ Transaction │   │
   │  Position │ ExchangeRate                        │
   └──────────────────────────────────────────────┘

2.3 目录结构

stock-portfolio/
├── prisma/
│   ├── schema.prisma      # 数据模型定义
│   └── seed.ts            # 初始化数据
├── src/
│   ├── app/
│   │   ├── page.tsx       # 主页面Dashboard
│   │   └── api/           # API 路由
│   │       ├── accounts/
│   │       ├── transactions/
│   │       ├── positions/
│   │       ├── securities/
│   │       ├── exchange-rates/
│   │       ├── dashboard/
│   │       │   ├── stats/
│   │       │   └── analytics/
│   │       └── import/
│   ├── components/        # UI 组件
│   ├── lib/
│   │   ├── api.ts         # API 调用封装
│   │   ├── prisma.ts      # Prisma 客户端
│   │   └── import-export.ts
│   └── types/             # TypeScript 类型定义
├── .env                   # 环境变量
└── README.md

3. 功能规格

3.1 市场与账户

3.1.1 支持的市场

市场代码 市场名称 结算货币 示例证券
US 美股 USD AAPL, GOOGL, MSFT
CN A股 CNY 600690, 159235
HK 港股 HKD 00700, 09868
CRYPTO 加密货币 USDT BTC, ETH

3.1.2 账户管理

  • 每个市场对应一个默认账户
  • 账户信息包含:名称、市场类型、基准货币、余额
  • 创建账户时自动设置对应市场的基准货币

3.2 证券管理

3.2.1 证券数据结构

字段 类型 说明
symbol String 证券代码(唯一)
name String 证券名称
market MarketType 市场类型
currency String 结算货币
lotSize Int 每手股数(港股=100A股=100美股=1
priceDecimals Int 价格精度
qtyDecimals Int 数量精度(股票=0数字货币=8
isCrypto Boolean 是否为加密货币

3.2.2 预置证券

代码 名称 市场 货币
00700 腾讯控股 HK HKD
09868 小鹏汽车 HK HKD
09988 阿里巴巴 HK HKD
AAPL Apple Inc. US USD
MSFT Microsoft Corp. US USD
NVDA NVIDIA Corp. US USD
GOOGL Alphabet Inc. US USD
INTC Intel Corp. US USD
600690 海尔智家 CN CNY
159235 中证现金流ETF CN CNY
BTC Bitcoin CRYPTO USDT
ETH Ethereum CRYPTO USDT

3.3 交易类型

类型代码 中文名称 说明 账户影响 持仓影响
BUY 买入 购买证券 扣减余额 增加持仓
SELL 卖出 卖出证券 增加余额 减少持仓
DEPOSIT 入金 资金转入 增加余额
WITHDRAW 出金 资金转出 扣减余额
DIVIDEND 分红 现金分红 增加余额
INTEREST 利息 利息收入 增加余额
FEE 费用 手续费支出 扣减余额

3.4 行情数据

3.4.1 数据源

使用腾讯行情接口获取实时价格:

  • 接口地址:https://qt.gtimg.cn/q=
  • 支持批量查询,用逗号分隔

3.4.2 代码格式转换

用户输入证券代码时,系统自动转换为腾讯接口格式:

市场 用户输入 腾讯接口格式
港股 09868 r_hk09868
A股上海 sh600690 或 600690 sh600690
A股深圳 sz159235 或 159235 sz159235
美股 GOOGL s_usGOOGL

3.4.3 解析字段(多市场差异化索引)

腾讯行情各市场返回字段索引不同,必须根据市场前缀分别解析:

市场 前缀 名称索引 当前价索引 涨跌额索引 涨跌幅索引
美股 s_us 1 3 4 5
港股 r_hk 1 3 31 32
A股(上海) sh 1 3 31 32
A股(深圳) sz 1 3 31 32

返回数据示例:

# 美股 (s_us) - 涨跌额在索引4涨跌幅在索引5
v_s_usGOOG="200~Alphabet-C~GOOG.OQ~315.72~-0.65~-0.21~..."

# 港股 (r_hk) - 涨跌额在索引31涨跌幅在索引32
v_r_hk09868="100~小鹏集团-W~09868~67.600~...~2026/04/13 16:08:56~0.600~0.90~..."

# A股 (sh) - 涨跌额在索引31涨跌幅在索引32
v_sh600690="1~海尔智家~600690~...~20260413161426~-0.14~-0.67~..."

异常处理策略:

  1. 当 Security 表中存在证券记录时,优先使用 Security 表中的名称
  2. 当 Security 表中无记录时使用腾讯行情返回的名称索引1
  3. 当前价获取失败时降级使用持仓的平均成本价avgCost
  4. 涨跌数据获取失败时,降级通过 当前价 - 昨收价 计算
  5. GBK 中文解码:腾讯 API 返回 GBK 编码数据,必须使用 arrayBuffer() + TextDecoder('gbk') 解码,禁止直接使用 text()

3.5 持仓计算

3.5.1 成本计算 - 平均成本法

平均成本 = (Σ买入金额) / 总数量
        = (Q1×P1 + Q2×P2 + ...) / (Q1 + Q2 + ...)

3.5.2 市值计算

市值 = 持仓数量 × 当前价格

3.5.3 盈亏计算

浮动盈亏 = 市值 - 成本基数
        = (数量 × 当前价) - (数量 × 平均成本)

浮动盈亏率 = (浮动盈亏 / 成本基数) × 100%

3.5.4 货币转换

系统接入 JisuAPI (https://api.jisuapi.com/exchange/convert) 获取实时汇率。

汇率获取策略:

  1. 优先使用 JisuAPI 实时汇率
  2. 缓存周期1 小时(防止 API 调用频率限制)
  3. 降级机制JisuAPI 失败时使用默认固定汇率

默认固定汇率(降级用):

从货币 到 USD 汇率
USD 1
CNY 0.137
HKD 0.129
USDT 1

汇率转换公式:

目标货币金额 = 源货币金额 × 汇率
示例1000 CNY × 0.137 = 137 USD

3.6 货币显示

3.6.1 显示货币选择

顶部导航栏提供显示货币切换器:

  • CNY人民币
  • USD美元
  • HKD港币

3.6.2 持仓显示规则

持仓明细和分析页面根据市场显示对应货币:

  • 港股持仓 → HKD
  • A股持仓 → CNY
  • 美股持仓 → USD
  • 加密货币持仓 → USDT

3.6.3 总资产显示

总资产、总盈亏等汇总数据根据用户选择的显示货币进行转换:

显示金额 = USD金额 × 显示货币汇率

资产配置环形图联动:

  • 环形图的市值数值、Tooltip 提示金额、图例金额均随全局显示货币CNY/USD/HKD实时联动转换
  • 使用 useMemo 缓存计算结果,避免重复渲染

3.7 数据导入导出

3.7.1 CSV 导出

支持导出:

  • 交易记录transactions
  • 持仓明细positions

导出字段:

  • 交易记录:时间、类型、证券代码、数量、价格、金额、手续费、备注
  • 持仓明细:证券代码、名称、市场、数量、成本价、当前价、市值、盈亏

3.7.2 CSV 导入

导入交易记录:

  • 支持批量导入
  • 自动验证数据格式
  • 按账户批量创建交易

导入模板包含字段:

  • type, symbol, quantity, price, amount, fee, currency, notes, executedAt

4. 页面与界面

4.1 顶部导航栏

┌─────────────────────────────────────────────────────────────────┐
│ [💰] 投资持仓管理          [CNY▼] [账户▼] [导入] [导出] [+记录交易] │
└─────────────────────────────────────────────────────────────────┘

功能:

  • Logo 和标题
  • 显示货币选择(下拉)
  • 账户选择(下拉)
  • 导入按钮
  • 导出按钮
  • 记录交易按钮

4.2 资产概览卡片

四个卡片并排显示:

  1. 总资产 - 渐变蓝背景,显示总市值和成本
  2. 浮动盈亏 - 渐变绿背景,显示盈亏金额和百分比
  3. 持仓市值 - 渐变紫背景,显示持仓数量和总市值
  4. 账户数量 - 渐变橙背景,显示市场数量

4.3 持仓分析卡片

展示前4个持仓的概览

  • 证券代码、图标和证券名称
  • 市值(使用对应市场货币)
  • 盈亏金额和百分比

4.3.1 证券名称显示

在以下位置均显示证券代码和对应名称:

位置 显示内容
持仓分析卡片 代码 + 名称truncate
资产分布条形图 代码 + 名称truncate
盈亏排行榜 代码 + 名称truncate最大宽度120px

证券名称从数据库 Security 表中获取,与腾讯行情接口解析的实时价格配合使用。

4.4 标签页

4.4.1 持仓明细标签页

表格列:证券 | 市场 | 数量 | 成本价 | 当前价 | 市值 | 盈亏 | 操作

功能:

  • 按市场货币显示价格和市值
  • 显示涨跌百分比
  • 支持删除持仓(卖出全部)

4.4.2 交易流水标签页

表格列:时间 | 类型 | 证券(名称+代码) | 数量 | 价格 | 金额 | 账户 | 操作

证券列显示:

  • 上行:证券名称(加粗)
  • 下行:证券代码(灰色小字)

功能:

  • 支持编辑交易记录
  • 支持删除交易记录
  • 支持导出 CSV
  • 后端通过关联查询附加证券名称

4.4.3 分析标签页

包含两个卡片:

  1. 资产配置环形图 - 由后端实时聚合持仓市值数据,按市场(美股/港股/A股/加密)分组显示占比
  2. 盈亏排行 - 列表显示按盈亏排序的持仓

资产配置数据来源:

  • 后端 API /api/dashboard/analytics 返回 marketDistribution 字段
  • 数据结构:{ name: string, value: number, percent: number }
  • 使用 Recharts <PieChart> 渲染环形图

4.5 对话框

4.5.1 记录交易对话框

字段:

  • 账户选择
  • 交易类型(买入/卖出/入金/出金/分红)
  • 证券代码搜索(自动补全)
  • 数量(买入/卖出时显示)
  • 价格(买入/卖出时显示)
  • 成交总额(自动计算)
  • 手续费
  • 交易时间
  • 备注
  • 确认按钮

4.5.2 交易确认对话框

显示交易摘要:

  • 账户信息
  • 交易类型和证券
  • 成交总额
  • 手续费
  • 交易时间

4.5.3 编辑交易对话框

与记录交易类似,但预填充现有数据。

4.5.4 删除确认对话框

显示待删除项目详情,确认后执行删除。


5. API 规格

5.1 账户 API

GET /api/accounts

获取所有账户列表

Response:

{
  "id": "string",
  "name": "string",
  "marketType": "US|CN|HK|CRYPTO",
  "baseCurrency": "string",
  "balance": "number"
}

5.2 持仓 API

GET /api/positions

获取持仓列表

Query: ?accountId=xxx

Response:

{
  "symbol": "string",
  "quantity": "string",
  "averageCost": "string",
  "currency": "string",
  "accountName": "string",
  "marketType": "string"
}

5.3 证券 API

GET /api/securities

获取证券列表

Query: ?market=US&search=AAPL

POST /api/securities

创建证券

Body:

{
  "symbol": "string",
  "name": "string",
  "market": "US|CN|HK|CRYPTO",
  "currency": "string",
  "lotSize": 100,
  "priceDecimals": 2,
  "qtyDecimals": 0,
  "isCrypto": false
}

5.4 交易 API

GET /api/transactions

获取交易流水

Query: ?accountId=xxx&page=1&limit=20

Response:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

POST /api/transactions

创建交易

Body:

{
  "accountId": "string",
  "type": "BUY|SELL|DEPOSIT|WITHDRAW|DIVIDEND",
  "symbol": "string|null",
  "quantity": "number|null",
  "price": "number|null",
  "amount": "number",
  "fee": "number",
  "currency": "string",
  "notes": "string|null",
  "executedAt": "ISO8601"
}

PATCH /api/transactions

更新交易

Body:

{
  "id": "string",
  "type": "string",
  "symbol": "string|null",
  "quantity": "number|null",
  "price": "number|null",
  "amount": "number|null",
  "fee": "number|null",
  "currency": "string|null",
  "notes": "string|null",
  "executedAt": "string|null"
}

DELETE /api/transactions?id=xxx

删除交易

5.5 行情 API

GET /api/dashboard/analytics

获取持仓分析和汇总

Response:

{
  "prices": {
    "AAPL": { "price": 150.00, "change": 2.50, "changePercent": 1.69 }
  },
  "positions": [
    {
      "symbol": "AAPL",
      "name": "Apple Inc.",
      "marketType": "US",
      "quantity": 10,
      "avgCost": 145.00,
      "currentPrice": 150.00,
      "change": 2.50,
      "changePercent": 1.69,
      "costBasis": 1450.00,
      "costBasisUSD": 1450.00,
      "marketValue": 1500.00,
      "marketValueUSD": 1500.00,
      "pnl": 50.00,
      "pnlPercent": 3.45,
      "pnlUSD": 50.00,
      "currency": "USD"
    }
  ],
  "summary": {
    "totalCostBasis": 10000.00,
    "totalMarketValue": 10500.00,
    "totalPnL": 500.00,
    "totalPnLPercent": 5.00,
    "positionCount": 3
  },
  "byMarket": {
    "US": { "totalCost": 5000, "totalValue": 5500, "totalPnL": 500 },
    "CN": { "totalCost": 3000, "totalValue": 3000, "totalPnL": 0 }
  }
}

6. 数据库模型

6.1 User用户

model User {
  id        String    @id @default(cuid())
  email     String    @unique
  name      String?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  accounts  Account[]
}

6.2 Account账户

model Account {
  id           String        @id @default(cuid())
  userId       String
  user         User          @relation(...)
  name         String
  marketType   MarketType
  baseCurrency String
  balance      Decimal       @default(0)
  createdAt    DateTime      @default(now())
  updatedAt    DateTime      @updatedAt
  transactions Transaction[]
  positions    Position[]
}

enum MarketType {
  US      // 美股
  CN      // A股
  HK      // 港股
  CRYPTO  // 加密货币
}

6.3 Security证券

model Security {
  id             String     @id @default(cuid())
  symbol         String     @unique
  name           String
  market         MarketType
  currency       String
  lotSize        Int        @default(1)
  priceDecimals  Int        @default(2)
  qtyDecimals    Int        @default(0)
  isCrypto       Boolean    @default(false)
}

6.4 Transaction交易流水

model Transaction {
  id            String          @id @default(cuid())
  accountId     String
  account       Account          @relation(...)
  type          TransactionType
  symbol        String?
  quantity      Decimal?
  price         Decimal?
  amount        Decimal
  fee           Decimal         @default(0)
  networkFee    Decimal?
  currency      String
  exchangeRate  Decimal?
  notes         String?
  executedAt    DateTime
}

enum TransactionType {
  DEPOSIT   // 入金
  WITHDRAW  // 出金
  BUY       // 买入
  SELL      // 卖出
  DIVIDEND  // 分红
  INTEREST  // 利息
  FEE       // 费用
}

6.5 Position持仓

model Position {
  id          String   @id @default(cuid())
  accountId   String
  account     Account  @relation(...)
  symbol      String
  quantity    Decimal
  averageCost Decimal
  currency    String
  updatedAt   DateTime @updatedAt

  @@unique([accountId, symbol])
}

6.6 ExchangeRate汇率

model ExchangeRate {
  id            String   @id @default(cuid())
  fromCurrency  String
  toCurrency    String
  rate          Decimal
  effectiveDate DateTime

  @@unique([fromCurrency, toCurrency, effectiveDate])
}

7. 汇率配置

7.1 初始汇率(以 USD 为基准)

从货币 到 USD 汇率
USD 1.000
CNY 0.137
HKD 0.129
USDT 1.000

7.2 货币转换公式

amountInTargetCurrency = amountInUSD × targetCurrencyRate

示例:
100 USD → CNY = 100 × 7.24 = 724 CNY
100 HKD → USD = 100 × 0.129 = 12.9 USD

8. 非功能性需求

8.1 性能

  • 页面加载时间 < 2秒
  • API 响应时间 < 500ms
  • 股价缓存时间 60 秒

8.2 兼容性

  • 支持 Chrome、Firefox、Safari、Edge 最新版本
  • 响应式设计,支持桌面和移动端

8.3 数据安全

  • 所有敏感配置通过环境变量管理
  • 数据库连接使用密码认证

9. 版本历史

版本 日期 说明
v1.0.0 2026-04-12 初始版本,完成核心功能

10. 附录

10.1 环境变量

DATABASE_URL="postgresql://user:password@host:5432/database"
ALPHA_VANTAGE_API_KEY="your_api_key"

10.2 腾讯行情接口示例

港股(小鹏汽车):

GET https://qt.gtimg.cn/q=r_hk09868
Response: v_r_hk09868="100~小鹏汽车-W~09868~67.000~66.950~..."

A股海尔智家

GET https://qt.gtimg.cn/q=sh600690
Response: v_sh600690="1~海尔智家~600690~20.88~20.75~20.79~..."

美股Google

GET https://qt.gtimg.cn/q=s_usGOOG
Response: v_s_usGOOG="200~Alphabet-C~GOOG.OQ~315.72~-0.65~-0.21~..."

10.3 批量查询示例

GET https://qt.gtimg.cn/q=r_hk09868,sh600690,sz159235,s_usGOOG