Skip to content

Latest commit

 

History

History
555 lines (398 loc) · 36.7 KB

File metadata and controls

555 lines (398 loc) · 36.7 KB

家庭账房 PRD — v0.5

v0.4 封板(tag v0.4 ~ v0.4.23)后:报表大整顿 + 摸清第 5 问(跑赢通胀/市场)+ 调优决策 + 配置沉淀管理页 + 股票 cron 估值修复 全部上线。 v0.5 把 v0.4 草草开了个头的「第 5 问」做到严格、做到深:v0.4 的 CPI 对照线是硬编码 2% 的占位实现;v0.5 换成真实拉取、严格推导的 CPI + M2 双基准「财富水位」

本版本主题:

  • 📈 财富水位(主线) — 家庭净资产 vs 两条国家级货币基准线:CPI 保命线(还买得起同样的生活吗)+ M2 地位线(社会财富排位是升还是降)· 严格自算自推、不展示拍脑袋数字
  • 💵 股票账户现金联动(副线) — 录股票时可选「从账户现金划转买入」· 买入扣现金 / 卖出加回 · 全对称 · 现金可负
  • 🎯 FIRE 目标支出自适应(副线) — 财务自由目标的月支出口径可选「自动适配月结支出」· 不再只能填死值 · 周期关闭按近 N 月真实支出滚动重算

配套文档:

  • 技术设计:tech-design/v0.5.md(架构选型 + 决策追溯 · PRD 评审通过后写)
  • UED 预览:preview/v0.5/index.html(财富水位页 + 推导面板 + 股票录入改版 · PRD 评审通过后做)
  • QA 用例:docs/qa-cases.md v05-* 段

状态(2026-05-29):📝 规划中 · PRD 评审 gate · 未写 tech-design / 未写代码


1. 版本目标

1.1 「摸清第 5 问」从占位到严格

v0.4 第 5 问(跑赢通胀和市场了吗?)只做了半个:净资产趋势上画了一条硬编码 2% 的 CPI 对照线。问题:

  • 2% 是拍的,不是真实 CPI(中国近年 CPI 其实 ~0.2-2%,且年际波动巨大)
  • 只有 CPI 一条线 —— 只回答了「能否保命」,没回答「社会排位是否下滑」
  • 没有任何推导透明度 —— 用户看不到这个数怎么来的

v0.5 把它做成两条严格推导的水位线:

基准线 衡量本质 回答用户的问题 中国当前量级
CPI 保命线 购买力 我的钱还买得起同样的生活吗? 近年 ~2%
M2 地位线 相对社会财富份额 我在社会财富里的排位是升还是降? 近年 ~8-9%

核心洞察:两条线差距巨大(近 10 年 M2≈9% / CPI≈2%,每年 7pp 鸿沟)。一个家庭可以「轻松跑赢 CPI(生活质量保住)」却「大幅跑输 M2(社会排位持续下滑)」——只看 CPI 永远发现不了。双线并列才是这一版的灵魂。

1.2 严格推导原则(本版硬约束)

用户明确要求:这两个值不能是简单数字,要严格自算自推。落实为:

  1. 真实拉取,不写死常量(吸取 v0.4 CPI 硬编码 2% 的教训)
  2. 几何平均而非算术平均(通胀复利,几何才是等效年率)
  3. 三法并算 + 全部展示:全历史几何均值 / 剔除极端值几何均值(默认对比线) / 近 10 年几何均值
  4. 推导透明面板:把每个数怎么算的摊给用户看,不藏
  5. 工程算、LLM 不算(承 [[feedback_llm_no_math]]):所有数字工程算好,AI 只做文字解读

1.3 v0.5 两条主线

主线 A · 财富水位(FR-70 ~ FR-77)
   ├─ FR-70 宏观基准数据底座(CPI/M2 表 + 1990-2025 历史 seed + 年度拉取)
   ├─ FR-71 基准推导引擎(几何 / 剔极端 / 近10年 三法并算 · 纯函数)
   ├─ FR-72 财富水位三线图(实际净资产 / CPI 保命线 / M2 地位线)
   ├─ FR-73 真实收益 & 相对社会收益 KPI + 人赚/钱赚分解诊断
   ├─ FR-74 推导透明面板(把 CPI/M2 怎么算的摊开)
   ├─ FR-75 /dashboard CPI 线升级(真实 CPI + 加 M2 线)
   ├─ FR-76 /admin/integrations 加「宏观基准」段(数据源 + 手刷 + 历史表 + 方法选择)
   ├─ FR-77 /checkup 收益维度喂入双基准 + AI 解读
   └─ FR-85 储蓄能力收支趋势图(人赚引擎可视化 · 收入线/支出线/储蓄填充 · 依赖 FR-84)

主线 B · 股票账户现金联动(FR-78 ~ FR-80)
   ├─ FR-78 买入扣现金(录 AUTO 持仓可选「从账户现金划转」· 强制成本 · FX · 可负)
   ├─ FR-79 卖出/减仓/归档加回现金(全对称 · 按市价 · FX)
   └─ FR-80 账户内买卖在 ledger 显示为「再分配」(净值中性 · 不计入收支)

主线 C · FIRE 目标支出自适应(FR-81 ~ FR-83)
   ├─ FR-81 FIRE 支出口径开关(固定值 / 自动适配月结支出)+ 窗口 + 平滑方式
   ├─ FR-82 月结支出自动派生 + 周期关闭触发重算 + 回写存储 + 不足/空期回退
   └─ FR-83 下游透明使用 + 移动靶沟通 + AI 偏离预警适配

缺陷修复(FR-84)
   └─ FR-84 净流入(人赚的)双源 bug · PMC 优先统一口径 · FR-73 前置依赖

1.4 锁定决策(2026-05-29 用户 TUI 拍板)

# 决策点 选定
D1 CPI 口径 整体 CPI(headline · 含食品) —— 真实反映"吃穿"日常成本 + 有 ~40 年历史(核心 CPI 剔食品 + 仅 2013 起,不适用)
D2 净资产比较基准 总净资产(含工资储蓄)+ 人赚/钱赚分解诊断
D3 历史均值默认高亮线 剔除极端值的几何均值(三个值都展示,这条做默认对比)
D4 财富水位入口 并入 /reports(报表)tab 作新 section · 不新起 tab(2026-05-29 用户纠正)—— Reports 定位本就是"钱怎么涨/跌的,跑赢通胀和市场没",且已有 人赚/钱赚 KPI + vs基准 pill 可直接复用
D5 股票扣款币种 账户币种(account.currency)+ 自动 FX 换算
D6 股票买卖对称 全对称:买入扣现金、卖出/减仓/归档按市价加回
D7 FIRE 支出口径 新增「自动适配月结支出」开关(默认仍 FIXED 保兼容)· 窗口默认 12 期 · 平滑默认 剔极端值 · 触发点 周期关闭 · 派生值回写 monthlyExpense 同字段使下游零改动

2. 主线 A · 财富水位

FR-70 · 宏观基准数据底座

用户故事:作为家庭成员,我希望系统用的是真实的、可追溯的 CPI / M2 数据,而不是某个写死的猜测值。

需求:

  • 新建宏观基准数据存储,按年度记录:整体 CPI、M2 增速、数据来源、抓取时间
  • 历史数据 seed:内置 1990-2025 年的年度 CPI / M2 历史值(公开且不变的历史事实,随版本烤入)
  • 年度增量拉取:每年自动拉取最新完整年份(如 2026 年拉 2025 全年值)· 接 v0.4.18 已有的定时调度框架
  • 数据来源:CPI = 国家统计局 · M2 = 中国人民银行 · 失败回退到 seed/手动录入

验收:

  • 历史表有 1990-2025 连续年度数据,无断档
  • 任意年份可查到 (CPI, M2, 来源, 抓取时间)
  • 拉取失败不影响展示(用已有数据)· 错误有日志

FR-71 · 基准推导引擎

用户故事:我担心单个年份波动太大(比如 1994 年 CPI 高达 24%),希望系统用稳健的方法算长期平均,而不是被极端年份带偏。

需求:对 CPI 和 M2 各算三个值并全部展示:

  • 全历史几何均值:(∏(1+rᵢ))^(1/n) − 1(复利等效年率 · 比算术平均更严谨)
  • 剔除极端值几何均值(默认对比线 D3):掐掉头尾各 10% 离群年份(如 1994 恶性通胀、1998-2002 通缩年)后再算几何均值
  • 近 10 年几何均值:贴当前低通胀 + 高 M2 新常态

验收:

  • 三个值同时算出、同时展示
  • 几何均值实现正确(可单测:已知序列手算对照)
  • 剔除极端值的阈值(头尾 10%)可配置
  • 中国 CPI 历史极端年份(1988-89、1993-95、1998-2002)确实被剔除版本排除

FR-72 · 财富水位三线图

用户故事:我想一眼看出我家的净资产,相对"保命线"和"社会地位线",是浮在水面上还是在下沉。

落点:/reports(报表)新增「财富水位」section(D4)· 不新起 tab · 接在现有 XIRR/基准/人赚钱赚 KPI 之后,是它们的自然延伸。

需求:一张时序图,三条线:

  1. 实际净资产(用户真实曲线 · 实线)
  2. CPI 保命线 = 期初净资产 × 累计 CPI 因子(虚线)
  3. M2 地位线 = 期初净资产 × 累计 M2 因子(虚线)
  • 数字直接浮在线上 / 数据点上(承 [[feedback_chart_datalabels]] · 不靠 hover)
  • 水位隐喻文案:净资产在 CPI 线上方 = 生活质量保住 · 在 M2 线上方 = 社会排位上升 · 卡两线之间 = 活得下去但在掉队
  • UI 一律 inline SVG,不用 emoji(承 [[feedback_no_emoji]])

验收:

  • 三线正确渲染 · 累计因子 = 区间内逐年 ∏(1+rᵢ)
  • 期初锚点 = 用户选定区间起点净资产
  • 三种相对位置(全在上 / 卡中间 / 全在下)文案都正确触发

FR-73 · 真实收益 & 相对社会收益 KPI + 人赚/钱赚分解

用户故事:我想知道我"实际"赚了多少(剔除通胀后),以及我是靠工资攒的还是靠投资赚的——因为如果全靠工资,一旦收入断了水位会暴跌。

需求:

  • 真实收益 KPI = (1+名义增长)/(1+累计CPI) − 1 · 标注"剔除通胀后的真实购买力变化"
  • 相对社会收益 KPI = (1+名义增长)/(1+累计M2) − 1 · 标注"相对社会财富份额的变化"
  • 人赚/钱赚分解诊断 —— 直接复用 Reports 已有的 KPI(_region.html 当前已渲染「人赚的·净流入 / 钱赚的·投资 PnL / 资产年化 TWR / vs基准 pill」),不新建数据:
    • 把"跑赢/跑输 M2"归因到已有的「人赚(净流入)」和「钱赚(投资 PnL)」两部分
    • 典型诊断文案:"你跑赢了 M2 地位线,但全靠人赚(工资储蓄 +12.3%);钱赚(投资 +1.8%)其实跑输了 CPI。水位靠收入硬撑,一旦收入中断会迅速下沉。"

验收:

  • 两个收益率口径与水位图一致
  • 分解归因数字与 Reports 现有 净流入/投资PnL KPI 一致(同源 · 不重算)
  • 至少 3 种诊断情景(双赢 / 靠人赚撑 / 双输)文案正确

FR-74 · 推导透明面板

用户故事:作为一个想搞懂的人,我希望看到这些 CPI/M2 平均值到底怎么算出来的,而不是被告知一个数字就完事。

需求:可展开的推导面板,展示:

  • CPI:2025 实际值(来源) / 全历史几何均值 / 剔极端值几何均值(标"默认对比") / 近 10 年几何均值
  • M2:同上四项
  • 当前对比区间的累计因子计算式
  • 每个数标注数据来源 + 抓取日期戳(过期数据 UI 淡化,承数据时效纪律)

验收:面板四项齐全 · 数字与图/KPI 一致 · 来源日期戳可见

FR-75 · dashboard CPI 线升级

用户故事:我平时主要看 dashboard,希望那条已有的 CPI 对照线变成真的,并且也能看到 M2 线。

需求:

  • 把 v0.4 dashboard 净资产趋势上硬编码 2% 的 CPI 对照线,换成 FR-70 拉取的真实 CPI
  • 加一条 M2 线
  • 保留"想看详细水位分析"→ 跳 /reports 财富水位 section 的入口(dashboard 做速览,Reports 做深看)

验收:dashboard 不再出现硬编码 2% · CPI/M2 两线与 /reports 财富水位 section 一致 · 向后兼容(无历史数据时优雅降级)

FR-76 · /admin/integrations 加「宏观基准」段

用户故事:作为管理者,我想能看到当前用的 CPI/M2 值、手动刷新、并选择用哪种平均方法。

需求:在「数据源接入」页(v0.4.23 改名)新增第④段「宏观基准 CPI/M2」:

  • 当前年度 CPI/M2 值 + 来源 + 抓取时间
  • 一键手动刷新(承 [[feedback_admin_runtime_config]] · 外部接入配套手动触发)
  • 历史数据表(只读 · 分页)
  • 默认对比方法选择(剔极端/全历史/近10年 · 默认剔极端)· 存 family_runtime_config · 实时生效

验收:页面 200 · 手刷可用且限频 · 方法切换实时改变 /wealth-level 默认线 · 不重启

FR-77 · /checkup 收益维度喂入双基准

用户故事:体检的 AI 诊断应该知道我跑赢/输了通胀和 M2,给出对应建议。

需求:

  • 把双基准(真实收益 / 相对社会收益 / 人赚钱赚分解)的算好的数字喂进 checkup 收益维度的 prompt
  • AI 输出文字解读(数字工程算好,LLM 不算 · 承 [[feedback_llm_no_math]])
  • 典型:"跑赢 CPI 跑输 M2 → 购买力保住但社会相对财富下滑,建议提高权益/不动产配置比例"

验收:prompt 含算好的双基准数字(read 验证)· AI 不做数学 · 误杀/胡话防回归

FR-85 · 储蓄能力收支趋势图(人赚引擎可视化)

用户故事:我想看到我家收入和支出随时间的走势对比,一眼看出储蓄能力是在变强还是变弱。

现状:/reports 储蓄能力 section 已有「月度收支双柱图」(PMC 源 · 正确)做逐月对比;缺的是趋势视角。(顺带印证 FR-84:同一页 储蓄区双柱用 PMC 显示正确,净流入 KPI 用 cash_flow 显示 0 —— 同页两个源,正是 bug 现场。)

需求:

  • 在储蓄能力 section 现有双柱图旁/下,新增收支趋势图:
    • 收入线 + 支出线(按周期时序)
    • 两线之间填充 = 储蓄额(income > expense 染 forest 绿 · 负染 rust 红)
    • 可选叠加储蓄率走势(次轴或独立 sparkline)
  • 数据源 = PMC(findFamilyAggregateRecent只取真实填报的周期(空期不参与 · 同 FR-84/FR-82 红线)
  • 数字浮在数据点上(承 [[feedback_chart_datalabels]])· Chart.js + SVG 图标 · 不 emoji
  • 定位自洽:框为「储蓄能力走势 / 人赚引擎」(摸清攒钱能力的趋势),不是 v0.4 砍掉的逐笔「流水视角」—— 不回归瀑布/桑基

依赖:FR-84(统一 PMC 净流入口径)· 与 FR-82(FIRE 自动支出读同一月支出)同源

验收:

  • 趋势图收入/支出线与储蓄区 KPI、双柱图同源一致(都 PMC)
  • 空期被正确排除 · 储蓄填充正负染色正确
  • 双柱(对比)与趋势(走势)并存,各司其读

3. 主线 B · 股票账户现金联动

核心认知:「买入股票」在买入当刻是净值中性的 —— 把 X 元现金换成 X 元股票,账户总价值不变;盈亏是之后价格波动才产生。所以这是账户内部两个 holding 之间的再分配,不计入收入/支出 cash_flow(否则会凭空改变净资产)。

FR-78 · 买入扣现金

用户故事:我录入"买了 100 股 PDD @ $80"时,希望系统自动从账户现金里扣掉买入花的钱,而不是让股票凭空多出来、现金还原封不动。

需求:

  • /accounts/{id}/holdings/new-auto 表单新增开关「从账户现金划转买入」
  • 勾选 → 买入成本 costBasis 变必填(前后端都校验)
  • 后端创建 AUTO 持仓的同时:股数 × 成本 经 FX 换成账户币种(D5),从账户币种的 CASH 现金行扣减
    • 账户无该币种现金行 → 新建一条负值现金行
    • 现金允许为负(卖空 / 融资 / 保证金 · 不做非负校验)
  • 触发 refreshAllForFamily(HOLDING_CHANGE) 重算

验收:

  • 勾选未填成本 → 报错(前后端)
  • 买入后:AUTO 持仓 +、账户币种现金 − (FX 换算正确)
  • 买入当刻账户总值中性(市价=成本时)
  • 现金可负 · 无该币种现金行时自动建负值行

FR-79 · 卖出/减仓/归档加回现金

用户故事:既然买入扣现金,那我减仓或归档持仓时,卖出的钱也应该自动回到账户现金里。

需求(D6 全对称):

  • 减仓 / 归档 AUTO 持仓 → 减少股数 × 当前市价 经 FX 加回账户币种现金行
  • 市价加回(卖出当刻净值中性 · 浮盈早已体现在卖前价值)
  • MANUAL 持仓归档 → 按 manualValue 加回 · CASH 行本身不涉及
  • 仅对"曾用现金联动买入"或用户显式勾选的持仓生效(避免误伤纯手工录入的老持仓 · 向后兼容)

验收:

  • 减仓/归档后现金按市价加回(FX 正确)
  • 部分减仓 vs 全归档都正确
  • AUTO / MANUAL 两种持仓加回口径正确
  • 老持仓(未联动)归档不动现金(backward compat)

FR-80 · ledger 显示「再分配」

用户故事:我在账本里要能看到"用 ¥X 现金买了 100 股 PDD"这条记录,知道钱去哪了,但它不该被算成一笔支出。

需求:

  • 账户内买入/卖出在 ledger 显示一条再分配记录(类似 v0.4.1 估值事件的展示形式 · inline SVG 图标)
  • 文案如「买入 100 股 PDD · 现金 −¥56,400」/「卖出 50 股 · 现金 +¥30,000」
  • 明确不计入收入/支出统计(净值中性 · 与 cash_flow 区分开)

验收:ledger 显示再分配行 · 不进收支汇总 · 不污染储蓄能力指标 · 图标用 SVG 非 emoji


4. 主线 C · FIRE 目标支出自适应

现状:/goals 的退休/FIRE 目标,月支出(Goal.paramsJson.monthlyExpense)是用户填死的固定值annualSpend ×12 → 通胀 PV → / 4% 提取率 → targetValue(缓存在 Goal.targetValue)。问题:用户填的支出值往往拍脑袋,且生活水平变化后不更新,FIRE 目标失真。

核心设计点:月支出派生值回写进 paramsJson.monthlyExpense 同一字段 + targetValue 仍是 Goal 缓存字段 → 所有下游消费方(进度 / 三情景预测 / AI报告 / dashboard 目标条)透明工作、零改动。工作量集中在「开关 UI + 周期关闭重算钩子 + 移动靶沟通」三处。

FR-81 · FIRE 支出口径开关

用户故事:我填的"退休后月支出"其实是猜的。我希望能让系统按我实际每月的月结支出来定 FIRE 目标,而不是一个我拍脑袋的固定数。

需求:

  • FIRE 目标设定/编辑表单,月支出口径新增二选一:
    • 固定值(现状 · 默认 · 用户手填)
    • 自动适配月结支出(按近 N 期真实家庭月支出滚动算)
  • 选自动 → 可配 窗口期数(默认 12,平掉季节性)+ 平滑方式(均值 / 剔极端值 / 中位数 · 默认剔极端,承财富水位同源思路防一次性大额带偏)
  • GoalParamsexpense_mode(FIXED / AUTO_MONTHLY · 默认 FIXED 保向后兼容)+ expense_window_months + expense_smoothing

验收:

  • 开关默认 FIXED · 老目标不受影响(向后兼容)
  • 选 AUTO 时窗口/平滑可配 · 选 FIXED 时退回手填
  • 切 AUTO→FIXED 时,以最近一次派生值作为新的手填种子(不丢)

FR-82 · 月结支出自动派生 + 触发 + 存储

用户故事:我每个月记完账,FIRE 目标应该自动跟着我的真实支出走,不用我手动改。

需求:

  • 数据源:PeriodMemberCashflowMapper.findFamilyAggregateRecent(家庭月支出 = SUM 成员 total_expense_input)
  • 触发时机:周期关闭时(承现有指标重算钩子)对 AUTO 模式 FIRE 目标重算月支出基准 → 重算 targetValue → 落库;另加目标页访问时的惰性兜底
  • 存储:派生月支出回写 paramsJson.monthlyExpense + 记 computed_at(供 UI 显示"基于近12月 · 更新于 YYYY-MM")
  • 正确性红线:
    • 跳过未填报的空期(findFamilyAggregateRecent 用 IFNULL 0,空期会错误拉低均值 → 必须只统计真实填了支出的周期)
    • 数据不足(< 窗口期数):回退可用期数均值;一期都没有 → 回退用户填的种子值 + 提示"数据不足,暂用手填值"

验收:

  • 周期关闭后 AUTO 模式目标的 monthlyExpense + targetValue 自动更新
  • 空期被正确排除(构造含空期数据验证均值不被拉低)
  • 数据不足时优雅回退 + 提示
  • 派生值在今天币值口径,通胀 PV 层(×(1+通胀)^years)仍叠加在上面

FR-83 · 下游透明使用 + 移动靶沟通 + AI 预警适配

用户故事:目标会随我的支出变动,我希望界面讲清楚"为什么我的目标数变了",而不是莫名其妙进度倒退。

需求:

  • 进度 / 三情景预测 / dashboard 目标条 透明读派生后的 targetValue(因回写同字段,无需逐个改)
  • 移动靶沟通:AUTO 模式目标卡显示"目标随实际支出动态调整 · 当前基于近 N 月均值 ¥X · 更新于 YYYY-MM"
  • AI 偏离预警(90天节流)适配:targetValue 因支出上升而上调时,预警文案区分"目标因近期支出上升而上调、达成日推迟",而非误判为用户退步

验收:

  • AUTO 模式下进度/预测/目标条数字与新 targetValue 一致
  • 移动靶说明文案正确(含基准值 + 更新时间)
  • AI 预警在 target 上移时文案正确(不误判退步)· 数字工程算好 LLM 不算

5. 缺陷修复(随 v0.5 合并)

FR-84 · 净流入(人赚的)双源 bug · PMC 优先统一口径

现象(prod 报告):reports「人赚的·净流入」显示 0,但用户明明收入 > 支出。

根因:净流入来自 principalVsReturnDecompositionperiodIncome/periodExpenseAccountPeriodFact.incomeBase/expenseBase,只读账户级 cash_flow。但用户工资填在 period_member_cashflow(PMC · /entry 成员月度收支两框),不是逐笔 account cash_flow → incomeBase ≈ 0 → 净流入 = 0。

  • v0.4.3 B2 已为「月均支出」确立 "PMC 优先 · cash_flow 回退"(averageExpense),但净流入分解漏改,仍只读 cash_flow。
  • 连带 bug:钱赚的(PnL = ΔNW − 净流入)被高估 —— 把工资攒的钱误算成投资赚的。
  • 不是 for i=1 跳第 0 期所致(那是 PnL 恒等式锚定需要,人赚+钱赚=ΔNW 要求净流入与 ΔNW 同窗口)。

影响面(3 处同源):

  • principalVsReturnDecomposition(reports 净流入 KPI + 本金vs收益分解图)
  • netInflowForPeriod(v0.4.2 · dashboard 人赚/钱赚)
  • /checkup 收益质量卡(若复用上述)

修复:

  • 统一净流入口径 = PMC 优先 · 逐期回退 cash_flow(承 v0.4.3 B2 同纪律)· 每期:有 PMC 用 PMC 的 (income − expense),该期 PMC 空则回退该期 account cash_flow
  • 同步改 reports 分解 + dashboard netInflowForPeriod + checkup,三处口径一致
  • 必须守 人赚的 + 钱赚的 = ΔNetWorth 恒等式(改完构造数据验证两者加和)
  • FR-73 前置依赖:此源不修,财富水位的人赚/钱赚分解诊断就建在错数据上

验收:

  • 构造"只填 PMC、无 account cash_flow"数据 → 净流入正确(非 0)· 旧"只填 cash_flow"数据仍正确(回退路径)
  • 人赚 + 钱赚 = ΔNetWorth(单测断言)
  • reports / dashboard / checkup 三处净流入数字一致
  • 向后兼容:两种历史数据(纯 PMC / 纯 cash_flow / 混合)都正确

:实现时顺带排除次因 —— 若个别账户 incomeBase 因缺 FX 汇率为 null 被静默吞掉,也要补 fallback(实现期 read 一次 prod 数据形态确认主因 = PMC 双源,非 FX null)。


6. 不做(YAGNI · 守住 10min/月 + 自托管定位)

  • ✗ 个股逐笔买卖流水 / 持仓成本 FIFO/LIFO 核算(到"账户月末快照"为止,不到逐笔)
  • ✗ 实时盯盘 / 分钟级行情
  • ✗ CPI/M2 之外的指数(沪深300、房价指数等留待后续评估)
  • ✗ 公积金/房贷利率工具(B 系列)、CRS/个税(O/P)、双角色记账(A)—— 推 v0.6+
  • ✗ 券商 API 直连下单 · 银行账单 OCR

7. 验收基线(版本完成时)

  • mvn test 全绿(在 v0.4 的 190 基础上 + 新增 FR-70~80 单测)
  • qa-run.sh v05-* 段全绿
  • qa-e2e.sh 端到端(含财富水位真值校验 + 股票现金联动真值校验 + FIRE 自动支出派生真值校验)
  • beta(http://43.106.119.1/)部署 + 用户浏览器验收
  • 向后兼容:宏观表 ADD TABLE · 股票联动仅对显式勾选持仓生效 · 0 破坏现有数据

8. 决策记录

6.1 调研追溯(2026-05 · 三轮)

  • 第 1 轮 · app 横评:扫国内外理财/记账 app 功能 → 偏表层,被用户批"没考虑国情"
  • 第 2 轮 · 国情对齐:转去知乎/雪球/律所专栏挖真实痛点 → 锁定国情独有空白(公积金/CRS/双角色)
  • 第 3 轮 · 底层逻辑:用户批"LPR 时效性 + 底层逻辑没拎清" → 学到要先拎因果链、核数据时效
  • 收敛:用户最终把 v0.5 收窄为单一主题「财富水位 · 资产 vs CPI/M2」—— 比铺开的 Tier 清单聚焦得多

6.2 为什么是 CPI + M2 双基准

  • CPI 答"绝对购买力/生活质量",M2 答"相对社会财富份额/阶层地位"——两个根本不同的问题
  • 中国 M2(~9%)远高于 CPI(~2%),"跑赢 CPI 只是保命,跑赢 M2 才是真增值"是中文理财语境的共识
  • 双线差距能让"看着在涨、其实在掉队"这件隐形的事变得可见

6.3 关键口径决策(见 §1.4 D1-D6)

  • D1 选 headline CPI 而非核心 CPI:核心 CPI 剔食品(不含"吃")+ 仅 2013 起无长历史
  • D3 默认剔极端值几何均值:中国 CPI 史有 1994=24.1% 这类极端年,简单平均会严重失真
  • D2 总净资产 + 人赚/钱赚分解:既答"家庭是否跑赢",又诊断"靠收入还是靠投资"(可持续性)
  • D5/D6 股票联动选完整模型(本位币 FX + 全对称):用户主动选了更完整而非最小实现

6.4 待 tech-design 细化

  • 宏观数据拉取:NBS/PBOC 官方接口 vs 第三方聚合 vs seed+增量(倾向 seed+增量,历史不变只拉新年)
  • 几何均值/剔极端值的数值实现 + 边界(通缩负值年的处理)
  • 股票"账户币种"= account.currency 的确认 + 无该币种现金行时建负值行的细节
  • 财富水位区间选择器(YTD / 近1年 / 近3年 / 自起始)与累计因子对齐

9. v0.5.3 迭代 · 计算指标透明化(2026-06-03)

用户诉求:我们早先给计算型 KPI 加了 ⓘ 小叹号,但它只讲「口径公式」(如「流动资产 ÷ 月均支出」)。用户希望点开后看到真实的计算数值(如「流动资产 ¥18.5万 ÷ 月均支出 ¥1.8万 = 3.2 月」),让非技术家庭成员能验证、能信任每个数字怎么来的。

FR-90 · ⓘ tooltip 显示真实计算数值

  • 全站梳理出 28 个计算型 KPI(净资产 / 总资产 / 总负债 / 紧急储备 / 本月资产收益 / 流动资产 / 本年累计损益 / 人赚净流入 / 钱赚 PnL / 月均收入 / 月均支出 / 储蓄率 / 月储蓄能力 / 已填月份 / vs 基准 / 家庭 XIRR / 资产年化 TWR),分布在 dashboard / reports / reports 储蓄区 / checkup 四个页面。
  • 每个 ⓘ 面板在原口径文字下方,多加一条真实实算行(虚线分隔 + 等宽字体):把当前账期/区间的真实中间数值代入公式展示。
  • 币种一致:dashboard、reports KPI 区按当前展示币种(viewCurrency);checkup 与 reports 储蓄区按家庭本位币(模板原本就如此)。数值与页面上 KPI 同源同币种,绝不出现「面板里一个数、KPI 卡上另一个数」。
  • 诚实口径:XIRR / TWR 是迭代/几何解,无法写成单条四则算式 → 只展示真实输入端点 + 解得值(如「期初净资产 −¥B → 期末 +¥E · N 期求解年化 = x%」),不编造算术步骤。
  • 纯定义指标(账户级 XIRR 为何为 0% 的成因说明、基准列定义)无可代入数值 → 保留口径文字,不强加数字。

非目标

  • 不改任何指标的计算口径(只把已算出的中间量暴露出来展示;计算逻辑零改动)。
  • 不在前端做算术(沿用 [[feedback_thymeleaf_diagnosis]]:数值一律服务端算好成串再传模板,避免 SpEL 沙箱问题)。

10. v0.5.4 迭代 · 目标 AI 月报三处修复(2026-06-03)

用户在目标详情「AI 综合月报」发现三个问题:

FR-91 · 月报脱敏回写(bug)

月报里出现「建议成员A与成员B共同核查」—— 给 LLM 的真名脱敏代号(成员A/成员B)未在展示前还原回真名(v0.3 起 latent)。修:校验仍在代号原文上做(防真名误判泄露),通过后 reverseMapping(raw, 代号→真名) 还原,与资产体检 AI 诊断完全同口径。

FR-92 · 月报缓存语义对齐 AI 诊断

确认:月报本就持久化在 goal_ai_report(UNIQUE(goal_id,period_id,report_type) upsert),详情页加载直接复用已存月报(= 缓存命中,不重算)。补齐 UI:有月报时显「本期复用 · 渲染于…」+「重新生成」按钮(对齐 checkup ↻ 强制刷新语义),原先仅"尚未生成"态才有触发入口。缓存是周期级(下个周期关闭或手动重新生成才更新),非时间 TTL —— 月报本就按账期定义,符合语义。

FR-93 · 仪表盘目标条带「AI 阅读总结」入口

仪表盘目标进度条带每个目标卡右侧加一个小入口(inline-SVG · book-open + AI 字样),直达 /goals/{id}#ai-report 本期月报锚点,用户从仪表盘可一键进入当期 AI 总结。

非目标

  • 不改月报生成时机(仍:周期关闭自动 + 手动触发)· 不引入时间 TTL(周期级缓存即可)。

11. v0.5.5 迭代 · 报表收益指标锚定"已关账快照"(2026-06-03)

背景与问题

报表页四个核心 banner —— ① 家庭 XIRR(含收入)② 资产年化 TWR(剔收入)③ 人赚的·净流入 ④ 钱赚的·投资 PnL —— 当前共用一个数据切片,锚定期取"当前 OPEN(进行中)账期"(ReportsController.anchorPeriod 自 v0.5.1 起 findCurrentOpen() 优先)。

这违背了产品最初的分工:报表 = 已关账账期的稳定快照 · 仪表盘 = 实时(此分工在 v0.1 存在,后被 2026-05-10「dashboard 改实时」与 v0.5.1「绕开 2032 测试期」两次改动抹平,reports 与 dashboard 现都锚 OPEN 期)。

由此产生用户实感的怪象——#3 人赚常年显示 0。根因三件事叠加(FactViewServiceImpl.principalVsReturnDecomposition):

  1. 锚定在 OPEN 期,而月中该期通常还没填,作为窗口终点贡献 ≈0;
  2. 逐期分解永远把窗口最老一期当基准、排除其净流入;
  3. 家庭账期少时(如仅"上月已关账 + 本月 OPEN 空"两期),唯一填了数的已关账月恰好成了被排除的基准,真正计入的只剩那个空 OPEN 期 → #3 = 0。

用户"月末几分钟填一次"的使用节奏下,只要账期不够多,#3 就长期为 0,且 XIRR/TWR 还会用"月中半填的净值"做终点,失真。

FR-94 · 报表锚定"最近已关账账期"(快照语义回归)

  • 报表四指标的数据切片终点改为"最近一个已关账(status=CLOSED)且 period_start ≤ 今天的账期";dashboard 不动,保持实时(锚最新一期、含 OPEN)。两个 tab 分工重新清晰。
  • 终点永远是填满的已关账月:XIRR/TWR 不再用月中半填净值;#3/#4 不再被空 OPEN 期拖成 0。
  • 用户月末填完并关账后,报表立即纳入该月(符合"关账即入快照"心智);未关账前,报表稳定停留在上一已关账月。
  • period_start ≤ 今天的约束顺带干净地解决了当初 v0.5.1 要绕的"测试/误建未来账期(如 2032)被 findLatest 锚到"的问题——不必再靠 OPEN 期兜底。

FR-95 · 已关账账期不足时的诚实空态

  • 逐期差额/回报本就需要 ≥2 个已关账账期才能计算(要有上一期做起点)。当不足时:
    • 仅 0 个已关账期(全新家庭):报表顶部显引导「尚无已关账账期 · 完成本月填报并关账后,这里出现收益快照」,四 banner 显「—」。
    • 仅 1 个已关账期:四 banner 显「—」+ 小字「需 ≥2 个已关账账期」,不再显误导性的 0
  • 其余报表区块(账户级收益表、风险分布、储蓄能力、财富水位)按各自既有口径照常显示,不受影响。

FR-96 · #3 口径文案澄清

  • #3「人赚的·净流入」ⓘ 口径补一句:这是所选区间内逐期(收入−支出)累计(与下方"本金 vs 投资损益"分解柱状图同口径、且满足"人赚+钱赚 = 区间净资产变化"恒等式),非单月

FR-97 · 页面级"已关账快照"透出(朱印章 + 说明行)

光靠角落小字不足以让用户意识到"报表只含已关账账期"。在报表页头醒目透出,呼应晚清账册 / 朱印美学:

  • 朱印红「已关账」印章:标题(数 · 字 · 所至)右侧盖一枚朱印风格徽记(朱红 --rust 系 · 印章感边框 · 可微旋转/做旧),作为本页身份标识常驻,一眼区分于 dashboard。
  • 说明行(替换原 anchor 提示):

    本页为已关账账期的稳定快照 · 数据截至 2026 年 5 月。进行中的本月请看 仪表盘 →(链接 /dashboard)

    • 仪表盘 为链接,引导想看实时的用户去 dashboard,强化两 tab 分工。
  • 与空态联动(承 FR-95):有已关账锚定期 → 显印章 + "数据截至 X";0 个已关账期 → 不显印章,改显 FR-95 的引导空态卡(避免盖一个"截至无"的空印章)。
  • 排版示意(用户已选定):
    月 · 度 · 大 · 账
    数 · 字 · 所至            ┌────────┐
                             │ 已关账 │ ← 朱印红印章
                             └────────┘
    本页为已关账账期的稳定快照 · 数据截至 2026 年 5 月
    进行中的本月请看  仪表盘 →
  • 全站 emoji 红线照旧:印章用 SVG/CSS 实现,不用 emoji([[feedback_no_emoji]])。

口径决策(写入本次,避免歧义)

  • #3/#4 保持"窗口累计"口径(不改成"最近关账月单月净流入")。理由:① 与下方分解柱状图一致;② 维持"人赚 + 钱赚 = 区间 ΔNetWorth"恒等式;③ 与同panel的 XIRR/TWR(本就是区间/年化)口径协调。改单月会破坏这三点。
  • 窗口仍由现有 range 选择器(1M/3M/6M/YTD/1Y/ALL)控制,只是窗口终点从 OPEN 期改为最近已关账期。

非目标

  • 不改 dashboard(继续实时锚最新一期)。
  • 不改任何指标的数学口径(XIRR/TWR/人赚/钱赚算法不动),只改"锚定到哪个账期"。
  • 不引入"报表含进行中账期"开关(会再次模糊 reports/dashboard 分工 · 明确不做)。
  • 0 schema 改动(仅新增一个只读查询 + controller 锚定逻辑)。

验收要点(供 QA 展开)

  • prod 真实数据:报表锚定到最近已关账月,四指标非 0(在有 ≥2 已关账期时);关账新月后报表立即纳入。
  • 2 个已关账期家庭:#3 = 第 2 个已关账月净流入(非 0);1 个 → 显空态而非 0。
  • 存在未来账期(2032 测试期)时不被锚定。
  • dashboard 行为不变(仍含 OPEN 实时)。
  • 报表页头显朱印红「已关账」印章 + "数据截至 <最近关账月>" + 指向仪表盘的链接;0 已关账期时显引导空态而非空印章。印章为 SVG/CSS(无 emoji)。

12. v0.5.6 迭代 · 报表长文目录(2026-06-03)

背景

报表页很长(收益概览 / 本金vs投资 / 财富水位 / 负债风险 / 账户级收益 / 资产配置 / 储蓄能力)。v0.5.1 加过一个右下角低调小 FAB 目录,但易被忽略、无"读到哪一节"高亮,远不到长文档应有的导航体验。调研业界(飞书 / Notion / MDN / NN/g / Wikipedia 2025 移动实验)后升级。

FR-98 · 报表长文目录(PC 常驻树状大纲 + 手机底部 sheet)

  • PC(≥lg):报表右侧常驻 sticky 目录栏,树状大纲(竖向引导线 + 横向树枝 · 支持多层嵌套),scrollspy 高亮当前所在节(滚动联动 · aria-current 作样式钩子 · 当前节朱铜高亮、父级淡显)· 点击平滑跳转。
  • 手机(<lg):右栏收起,左上角出现「目录」唤醒钮 → 底部 sheet 滑出(拖拽手柄 + 关闭 × + Esc/返回键 · 触控行 ≥44px · 点击跳转后自动收起,不丢阅读位)。
  • 章节锚点:#reports-region(收益概览)/ #sec-decompose / #sec-risk / #allocation-diff(条件)/ #sec-accounts / #sec-wealth / #sec-savings,均设 scroll-margin-top 处理 sticky-nav 偏移。
  • 嵌套就绪:当前各节为平铺;CSS/结构按树设计,未来任意加子节(缩进 + 引导线 + 树枝)自动体现层级。

非目标

  • dashboard 不加目录(它短)· 不做纯 CSS scrollspy(浏览器支持不足,用 JS)· 不自动展开全部章节(Wikipedia 2025 实验证明反降留存)· 0 后端 / 0 schema(纯前端)。

13. v0.5.7 迭代 · 长文目录推广到所有长 tab 页(2026-06-04)

背景

v0.5.6 报表长文目录验证 OK。推广到其它长 tab 页,并把目录沉淀为可复用件,避免每页重写。

FR-99 · 目录共用件 + 全长页适配

  • 共用件:fragments/_toc.html(rail(items) / mobile(items) 两片段)+ static/js/toc.js(页面无关 scrollspy + 底部 sheet · 自动接 [data-toc-nav])+ style.css 布局类 .toc-cols/.toc-cols-main(泛化自 .reports-cols)。页面只需:<main th:with="tocItems=${...}">.toc-cols(内容列 + rail)+ mobile + 引 toc.js
  • 接入页:
    • dashboard:概览 / 净资产趋势 / 按成员分布 / 按账户分布 / 账户列表
    • checkup:概览 / 资产配置 / 风险敞口 / 流动性 / 收益质量 / 智能建议 / AI 综合诊断 / 单账户体检
    • reports:改用共用件(行为不变)
  • 不做目录的页(明确非目标):填报(/entry · 任务表单)/ 账户(/accounts · 列表)/ 目标列表(/goals · 卡片列表)/ 管理(/admin · 管理页)—— 非长文阅读,不需目录。
  • 迭代纪律:任何改动某 tab 页 section 结构,须同步该页目录锚点/条目(memory feedback_toc_sync)。

非目标

  • 不动任何指标/数据/后端;0 schema;纯前端。
  • 条件 section(advice 互斥两态)用「同 id」保锚点恒在;allocation-diff 缺失时容忍 dead-anchor(无害 no-op)。

验收

  • dashboard/checkup/reports 三页均有目录(PC 左侧常驻 + scrollspy · 手机左上钮→sheet)· 锚点全部可解析 · HTMX 换 region 后 scrollspy 仍准。