Files
invest-mind-store/packages/design-document/机生文档/投资收益记录系统设计.md
2026-02-11 16:01:42 +08:00

733 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 投资收益记录系统设计文档
## 一、需求概述
### 1.1 核心目标
开发一个自动化的投资收益记录应用,**用户只需直接修改持仓的成本价和份额**,系统自动更新市场价格、计算收益和收益率,无需用户定期手动更新资产金额。
**设计原则:**
- **被动变更(分红、拆股、送股)**:系统自动完成
- **主动变更(买入、卖出、追加)**:用户直接修改成本价和份数,无需记录每次交易细节
- **适用场景**:多券商用户汇总统计,不是替代券商系统
### 1.2 支持的资产类型
- **股票**支持A股、港股、美股等
- **基金**:支持各类基金产品
- **现金**:证券账户留存现金、分红现金等
### 1.3 核心功能
- 自动更新市场价格(每日收盘后)
- 自动计算总资产、总收益
- 自动计算累计收益率、年化收益率、当年收益率
- 支持多种交易场景处理
- 使用基金净值法和资金加权法计算收益率(取两者低者)
---
## 二、设计分析与优化
### 2.1 用户提出的设计分析
#### ✅ 正确的设计点
1. **拆股处理**
- 自动变更股份数和成本价
- **实现方式**:股份数 = 原股份数 × 拆股比例,成本价 = 原成本价 ÷ 拆股比例
- **示例**10股拆成20股1:2成本价从100元变为50元
2. **分红处理**
- 股票成本价减去每股分红得到最新成本价
- **实现方式**:新成本价 = 原成本价 - 每股分红金额
- **注意**:分红需要同时记录到现金账户
3. **买入/卖出处理**
- 变更持仓份额和成本价
- **实现方式**
- **买入**:新成本价 = (原成本价 × 原份额 + 买入价 × 买入份额) / (原份额 + 买入份额)
- **卖出**:只减少份额,成本价不变(先进先出或加权平均)
4. **提现处理**
- 变更现金账户金额
- **实现方式**:现金余额 = 原余额 - 提现金额
#### ⚠️ 需要补充和优化的点
1. **分红处理需要完善**
- 需要区分现金分红和股票分红(送股)
- 现金分红:成本价降低,现金账户增加
- 股票分红(送股):股份数增加,成本价降低
- **公式**:新成本价 = (原成本价 × 原股份数) / (原股份数 + 送股数)
2. **买入/卖出成本价计算**
- 需要考虑交易费用(佣金、印花税等)
- **买入成本价** = (买入价 × 买入份额 + 交易费用) / 买入份额
- **卖出**:需要计算已实现收益,并更新持仓成本价
3. **基金的特殊处理**
- 基金分红通常有现金分红和红利再投资两种方式
- 需要支持基金份额的自动调整
---
## 三、遗漏场景分析
### 3.1 必须处理的场景
1. **送股(股票分红)**
- 场景公司送股如10送5
- 处理:股份数增加,成本价降低
- 公式:新股份数 = 原股份数 × (1 + 送股比例),新成本价 = 原成本价 / (1 + 送股比例)
2. **配股**
- 场景:公司配股,需要用户决定是否参与
- 处理:如果参与,需要记录配股价格和数量,重新计算成本价
3. **转增股本**
- 场景:资本公积转增股本
- 处理:类似送股,股份数增加,成本价降低
4. **股票合并(并股)**
- 场景如10股合并为1股
- 处理:股份数减少,成本价提高
- 公式:新股份数 = 原股份数 / 合并比例,新成本价 = 原成本价 × 合并比例
5. **现金分红再投资**
- 场景:用户选择将分红现金再次买入股票
- 处理:需要记录一笔买入交易
6. **基金分红再投资**
- 场景:基金分红选择红利再投资
- 处理:基金份额增加,成本价不变
7. **股票退市/摘牌**
- 场景:股票退市,无法获取市场价格
- 处理:需要标记为退市状态,使用最后已知价格或用户手动设置
8. **股票停牌**
- 场景:股票长期停牌
- 处理:使用停牌前最后价格,标记停牌状态
9. **汇率变动(港股、美股)**
- 场景:持有港股、美股,汇率变动影响资产价值
- 处理:需要记录买入时的汇率,计算时使用当前汇率
10. **交易费用**
- 场景:买入/卖出都有交易费用
- 处理:买入时计入成本,卖出时从收益中扣除
11. **现金账户利息**
- 场景:证券账户现金可能产生利息
- 处理:定期(如每月)更新现金余额,增加利息收入
12. **资产转移**
- 场景:从一个账户转移到另一个账户
- 处理:需要支持多账户管理
### 3.2 可选处理的场景
1. **股票期权/权证**
2. **可转债**
3. **分级基金**
4. **ETF套利**
---
## 四、收益率计算方法
### 4.1 基金净值法(时间加权收益率)
**原理**:消除资金流入流出的影响,只反映投资能力。
**计算公式**
```
收益率 = (期末净值 - 期初净值) / 期初净值
```
**每日净值计算**
```
当日净值 = (总资产价值) / (累计投入资金)
```
**累计净值**
```
累计净值 = 1 × (1 + r1) × (1 + r2) × ... × (1 + rn)
```
### 4.2 资金加权法(内部收益率 IRR
**原理**:考虑资金流入流出的时间点,计算真实的投资回报率。
**计算公式**
```
NPV = Σ(CFt / (1 + IRR)^t) = 0
```
其中:
- CFt第t期的现金流正数表示投入负数表示提取
- IRR内部收益率
**实现方式**
- 使用迭代法如牛顿法求解IRR
- 或使用Excel的XIRR函数考虑具体日期
### 4.3 取两者低者的实现
**策略**:保守计算,取两种方法中较低的收益率。
**实现逻辑**
```javascript
const timeWeightedReturn = calculateTimeWeightedReturn();
const moneyWeightedReturn = calculateIRR();
const finalReturn = Math.min(timeWeightedReturn, moneyWeightedReturn);
```
### 4.4 其他收益率指标
1. **累计收益率**
```
累计收益率 = (当前总资产 - 累计投入资金) / 累计投入资金
```
2. **年化收益率**
```
年化收益率 = (1 + 累计收益率)^(365 / 投资天数) - 1
```
3. **当年收益率**
```
当年收益率 = (当前总资产 - 年初总资产) / 年初总资产
```
---
## 五、数据模型设计
### 5.1 资产账户Account
```typescript
interface Account {
id: string; // 账户ID
name: string; // 账户名称
type: 'stock' | 'fund' | 'cash'; // 账户类型
currency: string; // 货币类型CNY/USD/HKD
createdAt: Date; // 创建时间
updatedAt: Date; // 更新时间
}
```
### 5.2 持仓Position
```typescript
interface Position {
id: string; // 持仓ID
accountId: string; // 所属账户ID
symbol: string; // 股票/基金代码
name: string; // 股票/基金名称
shares: number; // 持仓份额
costPrice: number; // 成本价(每股/每份)
currentPrice: number; // 当前价格
market: string; // 市场A股/港股/美股)
currency: string; // 货币类型
status: 'active' | 'suspended' | 'delisted'; // 状态
createdAt: Date;
updatedAt: Date;
}
```
### 5.3 持仓变更记录PositionChange
```typescript
interface PositionChange {
id: string; // 变更ID
positionId: string; // 持仓ID
changeDate: Date; // 变更日期
changeType: 'manual' | 'buy' | 'sell' | 'auto'; // 变更类型
beforeShares: number; // 变更前份数
beforeCostPrice: number; // 变更前成本价
afterShares: number; // 变更后份数
afterCostPrice: number; // 变更后成本价
notes?: string; // 备注/思考
createdAt: Date;
}
```
### 5.3.1 资金变动记录CashFlow- 用于计算 IRR
```typescript
interface CashFlow {
id: string; // 资金流ID
accountId: string; // 账户ID
flowDate: Date; // 资金变动日期
flowType: 'deposit' | 'withdraw' | 'dividend' | 'interest'; // 类型
amount: number; // 金额(正数表示投入,负数表示提取)
currency: string; // 货币类型
notes?: string; // 备注
createdAt: Date;
}
```
**说明:**
- 不再需要详细的交易记录表
- 持仓变更记录只记录成本价和份数的变化,不记录交易细节
- 资金变动记录用于计算 IRR用户只需记录投入/提取的时间和金额
### 5.4 现金账户CashAccount
```typescript
interface CashAccount {
id: string; // 现金账户ID
accountId: string; // 所属账户ID
balance: number; // 余额
currency: string; // 货币类型
interestRate?: number; // 利率(年化)
updatedAt: Date;
}
```
### 5.5 每日资产快照DailySnapshot
```typescript
interface DailySnapshot {
id: string; // 快照ID
date: Date; // 日期
totalAsset: number; // 总资产
totalCost: number; // 总成本(累计投入)
totalProfit: number; // 总收益
netValue: number; // 单位净值(总资产/总成本)
timeWeightedReturn: number; // 时间加权收益率
moneyWeightedReturn: number; // 资金加权收益率(基于资金流计算)
finalReturn: number; // 最终收益率(取两者低者)
annualizedReturn: number; // 年化收益率
yearToDateReturn: number; // 当年收益率
positions: Position[]; // 持仓明细JSON格式
cashAccounts: CashAccount[]; // 现金账户明细JSON格式
}
```
**说明:**
- 系统每日自动生成资产快照
- 用于计算时间加权收益率(基金净值法)
- 不依赖交易记录,只依赖持仓和资产快照
---
## 六、核心算法设计
### 6.1 成本价计算算法
#### 买入后成本价计算
```javascript
function calculateCostAfterBuy(originalShares, originalCost, buyShares, buyPrice, fee) {
const totalCost = (originalShares * originalCost) + (buyShares * buyPrice) + fee;
const totalShares = originalShares + buyShares;
return totalCost / totalShares;
}
```
#### 卖出后成本价计算
```javascript
// 卖出不改变成本价,只减少份额
function calculateCostAfterSell(originalShares, originalCost, sellShares) {
// 成本价不变
return originalCost;
}
```
#### 拆股后成本价计算
```javascript
function calculateCostAfterSplit(originalShares, originalCost, splitRatio) {
// splitRatio: 如 1:2 拆股splitRatio = 2
const newShares = originalShares * splitRatio;
const newCost = originalCost / splitRatio;
return { newShares, newCost };
}
```
#### 分红后成本价计算
```javascript
function calculateCostAfterDividend(originalCost, dividendPerShare) {
// 现金分红:成本价降低
return originalCost - dividendPerShare;
}
```
#### 送股后成本价计算
```javascript
function calculateCostAfterBonus(originalShares, originalCost, bonusRatio) {
// bonusRatio: 如 10送5bonusRatio = 0.5
const newShares = originalShares * (1 + bonusRatio);
const newCost = originalCost / (1 + bonusRatio);
return { newShares, newCost };
}
```
### 6.2 收益率计算算法
#### 时间加权收益率(基金净值法)
```javascript
function calculateTimeWeightedReturn(snapshots) {
let cumulativeReturn = 1;
for (let i = 1; i < snapshots.length; i++) {
const prevSnapshot = snapshots[i - 1];
const currSnapshot = snapshots[i];
// 计算期间收益率
const periodReturn = (currSnapshot.totalAsset - prevSnapshot.totalAsset) / prevSnapshot.totalAsset;
cumulativeReturn *= (1 + periodReturn);
}
return cumulativeReturn - 1;
}
```
#### 资金加权收益率IRR
```javascript
function calculateIRR(cashFlows) {
// cashFlows: 资金变动记录数组
// 正数投入资金deposit
// 负数提取资金withdraw
// 最后一条:当前资产价值(负数,表示"提取"
// 构建现金流数组
const flows = cashFlows.map(cf => ({
date: cf.flowDate,
amount: cf.amount
}));
// 添加当前资产价值(作为最后一笔现金流)
const currentAsset = calculateTotalAsset();
flows.push({
date: new Date(),
amount: -currentAsset // 负数表示"提取"
});
function npv(rate) {
let sum = 0;
const baseDate = flows[0].date;
for (let i = 0; i < flows.length; i++) {
const days = (flows[i].date - baseDate) / (1000 * 60 * 60 * 24);
sum += flows[i].amount / Math.pow(1 + rate, days / 365);
}
return sum;
}
// 使用二分法求解
let low = -0.99;
let high = 10;
let mid;
for (let i = 0; i < 100; i++) {
mid = (low + high) / 2;
const npvValue = npv(mid);
if (Math.abs(npvValue) < 0.0001) {
return mid;
}
if (npvValue > 0) {
low = mid;
} else {
high = mid;
}
}
return mid;
}
```
**说明:**
- 基于资金变动记录CashFlow计算不是基于交易记录
- 用户只需记录:什么时候投入多少钱、什么时候提取多少钱
- 比记录每笔交易简单很多
#### 年化收益率计算
```javascript
function calculateAnnualizedReturn(totalReturn, days) {
return Math.pow(1 + totalReturn, 365 / days) - 1;
}
```
### 6.3 每日资产更新流程
```javascript
async function updateDailyAssets(date) {
// 1. 获取所有持仓
const positions = await getActivePositions();
// 2. 更新每个持仓的市场价格
for (const position of positions) {
const currentPrice = await fetchMarketPrice(position.symbol, position.market, date);
position.currentPrice = currentPrice;
position.updatedAt = date;
}
// 3. 计算总资产
const totalAsset = calculateTotalAsset(positions, cashAccounts);
// 4. 计算总成本
const totalCost = calculateTotalCost(positions, cashAccounts);
// 5. 计算总收益
const totalProfit = totalAsset - totalCost;
// 6. 计算收益率
const timeWeightedReturn = calculateTimeWeightedReturn(snapshots);
const moneyWeightedReturn = calculateIRR(cashFlows, dates);
const finalReturn = Math.min(timeWeightedReturn, moneyWeightedReturn);
// 7. 保存资产快照
await saveAssetSnapshot({
date,
totalAsset,
totalCost,
totalProfit,
timeWeightedReturn,
moneyWeightedReturn,
finalReturn,
positions,
cashAccounts
});
}
```
---
## 七、交易场景处理流程
### 7.1 买入/卖出股票/基金(简化操作)
**新设计:用户直接修改持仓**
```
用户操作:
1. 打开持仓列表,找到对应股票
2. 点击"编辑"或"调整"
3. 直接修改:
- 成本价(如:从 100 改为 95表示加仓后新的加权成本价
- 份数(如:从 100 股改为 150 股)
4. 可选:添加备注/思考(如:"2024年1月加仓"
5. 保存
系统处理:
1. 记录变更前状态(用于变更历史)
2. 更新持仓(成本价、份数)
3. 记录持仓变更记录(简化版,不记录交易细节)
4. 更新总资产和收益率
5. 生成每日资产快照(用于收益率计算)
```
**优势:**
- ✅ 操作简单,只需修改两个数字
- ✅ 适合多券商用户快速汇总
- ✅ 用户可以根据券商系统的成本价直接输入
- ✅ 不需要记录每笔交易的费用、时间等细节
**注意事项:**
- 用户需要自己计算加权平均成本价(或使用系统提供的计算器)
- 系统会记录变更历史,但不会记录详细的交易信息
### 7.3 拆股处理
```
1. 系统检测或用户输入:拆股比例(如 1:2
2. 系统处理:
- 自动更新股份数 = 原股份数 × 拆股比例
- 自动更新成本价 = 原成本价 ÷ 拆股比例
- 记录拆股交易记录
3. 更新总资产(总价值不变)
```
### 7.4 现金分红处理
```
1. 系统检测或用户输入:每股分红金额
2. 系统处理:
- 计算分红总额 = 每股分红 × 持仓份额
- 更新成本价 = 原成本价 - 每股分红
- 增加现金账户余额(分红总额)
- 记录分红交易记录
3. 更新总资产和收益率
```
### 7.5 送股处理
```
1. 系统检测或用户输入:送股比例(如 10送5比例为0.5
2. 系统处理:
- 更新股份数 = 原股份数 × (1 + 送股比例)
- 更新成本价 = 原成本价 ÷ (1 + 送股比例)
- 记录送股交易记录
3. 更新总资产(总价值不变)
```
### 7.6 提现处理
```
1. 用户输入:提现金额
2. 系统处理:
- 检查现金账户余额是否足够
- 减少现金账户余额
- 记录提现交易记录
3. 更新总资产
```
---
## 八、系统架构设计
### 8.1 技术栈建议
**前端**
- React / Vue.js
- TypeScript
- 状态管理Redux / Zustand
- UI框架Ant Design / Material-UI
**后端**
- Node.js / Python
- 数据库PostgreSQL / MySQL
- 缓存Redis
**数据获取**
- 股票价格API腾讯财经、新浪财经、Yahoo Finance
- 基金净值API天天基金、晨星
### 8.2 模块划分
1. **资产管理模块**
- 账户管理
- 持仓管理
- 现金账户管理
2. **交易处理模块**
- 买入/卖出处理
- 分红处理
- 拆股/送股处理
- 交易记录管理
3. **价格更新模块**
- 定时任务(每日收盘后更新)
- 价格数据获取
- 价格数据缓存
4. **收益计算模块**
- 时间加权收益率计算
- 资金加权收益率计算
- 年化收益率计算
- 资产快照生成
5. **数据统计模块**
- 总资产统计
- 收益统计
- 收益率统计
- 持仓分析
### 8.3 数据存储设计
**表结构**
- accounts账户表
- positions持仓表
- transactions交易记录表
- cash_accounts现金账户表
- asset_snapshots资产快照表
- market_prices市场价格表用于缓存
---
## 九、实现建议
### 9.1 开发优先级
**第一阶段MVP**
1. 基本的资产账户管理
2. 持仓管理(买入/卖出)
3. 每日价格更新
4. 基本的收益计算(累计收益率)
**第二阶段**
1. 拆股、分红、送股处理
2. 时间加权收益率和资金加权收益率计算
3. 年化收益率计算
4. 资产快照功能
**第三阶段**
1. 多账户管理
2. 多货币支持
3. 高级统计和分析
4. 数据导出功能
### 9.2 注意事项
1. **数据准确性**
- 价格数据需要可靠的数据源
- 交易记录需要完整保存,不可修改
- 成本价计算需要精确
2. **性能优化**
- 价格数据缓存
- 资产快照定期生成,避免实时计算
- 收益率计算优化IRR计算可能较慢
3. **用户体验**
- 交易记录操作简单
- 自动处理拆股、分红等场景
- 清晰的收益展示
4. **数据备份**
- 定期备份交易记录
- 支持数据导出
---
## 十、总结
### 10.1 设计要点
1. ✅ **用户只需直接修改持仓**:买入/卖出时直接修改成本价和份数,无需记录交易细节
2. ✅ **自动处理特殊事件**:拆股、分红、送股等被动变更由系统自动处理
3. ✅ **自动更新市场价格**:每日收盘后自动更新,无需用户手动输入
4. ✅ **简化操作流程**:适合多券商用户快速汇总统计,不是替代券商系统
5. ✅ **保守的收益率计算**:使用时间加权(基于资产快照)和资金加权(基于资金流)两种方法,取较低者
6. ✅ **完整的收益统计**:累计收益率、年化收益率、当年收益率等
7. ✅ **支持计划和复盘**:通过持仓思考记录实现,关联持仓而非交易
### 10.2 需要补充的场景
1. 送股(股票分红)
2. 配股
3. 转增股本
4. 股票合并
5. 汇率变动(港股、美股)
6. 交易费用处理
7. 现金账户利息
8. 股票停牌/退市处理
### 10.3 实现建议
- 采用模块化设计,便于扩展
- 持仓变更记录不可修改,保证数据可追溯性
- 每日自动生成资产快照,用于收益率计算
- 提供成本价计算器工具,帮助用户计算加权平均成本价
- 支持批量导入功能,可以从券商系统导出后导入
- 提供清晰的数据展示和统计
- 持仓思考记录支持计划和复盘功能
### 10.4 数据模型总结
**核心表:**
- `positions` - 持仓表(用户直接修改成本价和份数)
- `position_changes` - 持仓变更记录(系统自动记录变更历史)
- `cash_flows` - 资金变动记录(用户记录投入/提取,用于 IRR 计算)
- `daily_snapshots` - 每日资产快照(系统自动生成,用于时间加权收益率)
- `position_thoughts` - 持仓思考记录(用户记录思考,用于计划和复盘)
**不再需要:**
- ~~详细的交易记录表~~(改为简化的持仓变更记录)
---
**文档版本**v1.0
**创建日期**2024年
**最后更新**2024年