用户故事:真实收益追踪
创建日期: 2025-01-11
状态: ✅ 已实现
优先级: P0
1. 背景与问题
当前问题
现有的"快照式盘点"模式无法区分"资产增值收益"和"资金流入流出"。
示例场景:
- 期初:沪深300ETF 10,000元
- 期末:沪深300ETF 15,000元
- 账面变化:+5,000元
但这5,000元可能是:
- 用户加仓投入:+3,000元
- 实际投资收益:+2,000元
用户需求:
"我想知道我的每一个具体资产(比如'沪深300ETF')到底赚了多少钱,而不是被新投入的资金'污染'。"
2. 解决方案
核心思路
在盘点时,除了记录资产当前市值,还可以选填"本期资金变动"(净流入/流出)。
真实收益计算公式:
真实收益 = 期末市值 - 期初市值 - 净流入
真实收益率 = 真实收益 / (期初市值 + 净流入/2)
设计原则
- 输入负担最小化:只有资金变动的资产需要额外填写
- 向后兼容:历史数据默认 net_inflow = 0
- 精确到单个资产:每个资产独立追踪
- 不需要交易流水:只在盘点时汇总
3. 数据模型改动
3.1 SnapshotItem 增加字段
#![allow(unused)] fn main() { // src/models/snapshot.rs pub struct SnapshotItem { pub asset_id: Uuid, pub asset_name: String, pub scope: AssetScope, pub category: Category, pub sub_asset_class: String, pub vehicle_type: VehicleType, pub value: Decimal, // 新增:本期净流入(正=投入,负=取出) pub net_inflow: Decimal, // 默认 Decimal::ZERO } }
3.2 数据库表结构
-- snapshot_items 表新增列
ALTER TABLE snapshot_items ADD COLUMN net_inflow TEXT NOT NULL DEFAULT '0';
3.3 Snapshot 新增辅助方法
#![allow(unused)] fn main() { impl Snapshot { /// 计算单个资产的真实收益(相对于上一个快照) pub fn asset_true_return(&self, asset_id: Uuid, prev_snapshot: &Snapshot) -> Option<TrueReturn> { let curr_item = self.items.iter().find(|i| i.asset_id == asset_id)?; let prev_item = prev_snapshot.items.iter().find(|i| i.asset_id == asset_id)?; let book_return = curr_item.value - prev_item.value; let true_return = book_return - curr_item.net_inflow; let base = prev_item.value + curr_item.net_inflow / dec!(2); let true_rate = if base > Decimal::ZERO { true_return / base * dec!(100) } else { Decimal::ZERO }; Some(TrueReturn { asset_id, book_return, net_inflow: curr_item.net_inflow, true_return, true_rate, }) } } pub struct TrueReturn { pub asset_id: Uuid, pub book_return: Decimal, pub net_inflow: Decimal, pub true_return: Decimal, pub true_rate: Decimal, } }
4. UI 改动
4.1 盘点模式
在每个资产条目下方增加"本期投入"输入框(可选,默认收起)。
布局设计:
┌────────────────────────────────────────────────────────────────┐
│ 沪深300ETF 原值: ¥10,000 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 当前市值 [ ¥15,000 ] 变化: +¥5,000 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 📥 本期投入/取出 [ ¥3,000 ] (可选,取出填负数) │
│ │
│ 💡 真实收益: +¥2,000 (+17.4%) │
└────────────────────────────────────────────────────────────────┘
交互设计:
- 默认隐藏"本期投入"行
- 点击某个资产的"+"按钮展开填写
- 填写后实时计算并显示"真实收益"
- 空值或0表示没有资金变动
4.2 收益分析页
增加"真实收益"视角,显示剔除资金进出后的收益。
概览卡片:
┌─────────────────────────────┬─────────────────────────────┐
│ 账面变动 │ 真实收益 │
│ +¥ 100,000 │ +¥ 45,000 │
│ +10.5% │ +4.85% │
│ 含资金流入 ¥55,000 │ 剔除资金进出后 │
└─────────────────────────────┴─────────────────────────────┘
归因表格扩展(按单个资产):
┌──────────────┬────────────┬────────────┬────────────┬──────────┐
│ 资产 │ 账面变化 │ 资金进出 │ 真实收益 │ 收益率 │
├──────────────┼────────────┼────────────┼────────────┼──────────┤
│ 沪深300ETF │ +¥5,000 │ +¥3,000 │ +¥2,000 │ +17.4% │
│ 中证500ETF │ +¥3,000 │ +¥5,000 │ -¥2,000 │ -6.7% │
│ 余额宝 │ +¥100 │ ¥0 │ +¥100 │ +0.2% │
│ 朝朝宝 │ +¥25,700 │ +¥25,700 │ ¥0 │ 0% │
├──────────────┼────────────┼────────────┼────────────┼──────────┤
│ 合计 │ +¥33,800 │ +¥33,700 │ +¥100 │ +0.1% │
└──────────────┴────────────┴────────────┴────────────┴──────────┘
5. 实现计划
Phase 1:数据模型与数据库(预估 0.5 天)
- SnapshotItem 增加 net_inflow 字段
- 数据库迁移脚本
- snapshot_repo.rs 读写支持
Phase 2:盘点模式 UI(预估 1 天)
- InventoryMode 组件增加"本期投入"输入
- 实时计算真实收益预览
- 保存时存储 net_inflow
Phase 3:收益分析页(预估 1 天)
- 收益概览卡片增加"真实收益"
- 归因表格支持按资产展开
- 计算逻辑更新
Phase 4:测试与优化(预估 0.5 天)
- 单元测试
- 边界情况处理
- UI 细节优化
总预估:3 天
6. 验收标准
- 盘点时可以为每个资产填写"本期投入/取出"金额
- 收益分析页显示"账面收益"和"真实收益"双指标
- 支持按单个资产查看真实收益归因
- 历史数据向后兼容(net_inflow 默认为 0)
- 输入体验流畅,大部分资产不需要额外输入
7. 术语定义
| 术语 | 定义 |
|---|---|
| 账面收益 | 期末市值 - 期初市值 |
| 净流入 | 本期投入资金 - 本期取出资金 |
| 真实收益 | 账面收益 - 净流入 |
| 真实收益率 | 真实收益 / (期初市值 + 净流入/2) × 100% |
8. 附录:用户操作示例
场景:用户月初工资入账,定投了两只基金
操作流程:
- 打开资产管理 → 发起盘点
- 更新各资产当前市值
- 对于"朝朝宝",填写本期投入 +25,700(工资入账)
- 对于"沪深300ETF",填写本期投入 +3,000(定投)
- 对于"中证500ETF",填写本期投入 +5,000(定投)
- 完成盘点
结果:收益分析页显示
- 朝朝宝真实收益 ≈ 0(纯粹是工资入账)
- 沪深300ETF真实收益 = 实际涨跌
- 中证500ETF真实收益 = 实际涨跌(可能是负的)
这样用户就能清楚看到:哪些资产在真正赚钱,哪些在亏钱。