个人用的永久投资组合配比跟踪工具。手动录入黄金 / 国债 / 股票三类资产的持仓, 自动计算相对目标的偏离,在触碰上下限阈值时高亮提醒并给出可以直接执行的具体金额。
- 理论依据:哈利·布朗的永久投资组合(Harry Browne's Permanent Portfolio)
- 阈值口径:目标 33% / 33% / 34%,下限 20%、上限 46%(触碰即触发)
- 数据来源:手动录入(自动化行情抓取是后续计划,数据结构已预留)
永久投资组合的核心纪律是"再平衡":某一类涨得太多(或跌得太狠)时把它调回目标。 但这件事在实践中有两个坑——什么时候该动手,以及到底要买卖多少钱。
这个工具把这两件事都算出来:
- 每次保存持仓自动记录一条当日快照,折线图能看到配比是怎么一步步漂移的
- 越界时给出「卖出黄金 40,716.50 元 → 买入国债 19,333.50 元 / 买入股票 21,383.00 元」
- 卖出总额与买入总额严格相等(有单元测试硬断言,四舍五入残差会被归并)
- 额外给出书里的「资金变动法」:加钱补欠配最多的一类(不是"占比最低"的一类—— 占比最低 ≠ 最需要补;目标为 0 的类别缺口恒为负,不会被选中), 取钱优先动现金,比卖出再买入省手续费
| 页面 | 内容 |
|---|---|
| 总览 | 状态横幅(绿/黄/红/灰)、环形饼图(内环目标 / 外环实际)、偏离柱状图(含阈值安全带)、再平衡建议表 |
| 持仓录入 | 按账户分组的可编辑表格,含市值与盈亏;保存即记录当日快照 |
| 历史趋势 | 三大类占比随时间的三条曲线,叠加目标线与 20%/46% 阈值带 |
| 设置 | 目标配比、上下限(含 15/35 与 30/20 两套换算预设)、是否计入现金、最小建议金额 |
- 分母默认不含现金。占比 = 该类市值 ÷ (黄金 + 国债 + 股票)。现金类资产仍会展示绝对值, 但不参与比例与阈值判定。页面上常驻显示当前口径,可在设置页切换。 切到"含现金"后,历史曲线的分母也会同步切换,且此时再平衡建议里会出现 "卖出(现金)"——那是正确行为:现金的目标是 0%,它是买入三大类的资金来源。
- 阈值判定是"等于也算触碰":占比 ≥ 46% 或 ≤ 20% 都触发。
- 百分比全链路是数值口径:
33.0就是 33%,不是 0.33。 - 同一天保存多次只留一条快照(幂等覆盖),避免曲线出现重复点。
- 补录历史日期写入的是当前持仓按该日期标记,本版本不支持编辑历史持仓。 历史页的补录按钮旁有同样的说明。
- 录入价格请用后复权等价价格,或把现金分红并入数量。否则长期收益率会被低估约 2 个百分点/年。
- 记价方式在标的创建时选定:"数量 × 单价"适合 ETF/基金,"直接填金额"适合 实物金条、银行金条这类不按份额计价的资产。选"直接填金额"时成本要填累计投入金额, 选"数量 × 单价"时填成本单价——填错会导致盈亏列恒显示"—"。
- 建议金额有一道最小交易额门槛(默认 1000 元)。当某笔建议低于门槛时, 工具会把超出的一侧按比例缩到与另一侧相等,而不是给出"卖 1900 但只买 1000" 这种会让账对不上的建议;如果缩完仍低于门槛,就整体归零并在表里说明原因。
# 后端
cd backend
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt # Windows: .venv\Scripts\python -m pip install -r requirements-dev.txt
.venv/bin/python -m uvicorn app.main:app --reload --port 8000
# 前端(另开一个终端)
cd frontend
pnpm install
pnpm dev # http://127.0.0.1:5173,/api 自动代理到 8000只跑后端也够用:先 pnpm build,然后直接访问 http://127.0.0.1:8000,
FastAPI 会连同前端产物一起托管(含 SPA 深链接回退)。
首次使用顺序:设置页建账户与标的 → 持仓录入填数量与现价 → 保存 → 总览看配比。
| 类别 | 标的 |
|---|---|
| 黄金 | 518880(华安,规模约 862 亿)、159934(易方达) |
| 国债 | 511010(5 年)、511260(10 年)、511090(30 年,年化波动 6.6%,约为 5 年期的 2.6 倍) |
| 股票 | 510300(沪深300)、510500(中证500)、563360(中证A500) |
cd backend && .venv/bin/python -m pytest -q # 90 个测试
cd frontend && pnpm test -- --run # 15 个测试后端测试覆盖的边界:恰好 20%/46% 触发、含/不含现金两种口径、分母为 0 不产生 NaN、 卖出总额严格等于买入总额、建议方向不被残差归并翻转、四舍五入残差归并、 最小交易额归一化到不动点、同日快照幂等、未填写行不写入快照、 非法日期/Infinity 被 422 拦下且不落库、删标的后持仓不会被新标的继承。
其中 test_balance_invariant_holds_across_portfolios 是穷举不变量测试:
6 组阈值配置 × 每种 343 个组合,断言"卖出 == 买入"与"建议方向与真实缺口同号"。
这两条都曾被真实违反过(穷举 417 万组合时出现过 43 万个"有卖出、无买入"的失衡态)。
前端 thresholds.ts 与后端 valuation.py 是同源逻辑,thresholds.test.ts 里
有一组与后端 API 测试用同一份数字的跨端口径一致性断言,防止两端漂移。
见 docs/DEPLOY.md(systemd + Nginx + certbot,含备份与排障)。
浏览器通知需要 HTTPS;页面高亮提醒在 HTTP 下也正常工作。
backend/ FastAPI + SQLAlchemy + SQLite
app/services/valuation.py 配比/占比/阈值(纯函数,无 IO)
app/services/rebalance.py 再平衡与资金变动建议(纯函数)
app/services/snapshots.py 日度快照(同日幂等)
frontend/ React + Vite + TypeScript + ECharts
docs/ 设计文档、实施计划、调研报告、代码审查报告、部署说明
deploy/ systemd unit / nginx 配置 / deploy.sh
- 设计文档:docs/superpowers/specs/2026-09-25-asset-monitor-design.md
- 实施计划:docs/superpowers/plans/2026-09-25-asset-monitor-implementation.md
- 品种与再平衡调研:docs/permanent-portfolio-tool-research.md
- codegraph 代码审查报告:docs/review-findings.md
- 修复的独立验证报告:docs/verification-report.md
阈值设定的出处:《哈利·布朗的永久投资组合》中译本的"调整带"——单一资产 >35% 或 <15% 即回各 25%,作者同时说明 30%/20% 亦可。本工具按三大类等比例换算为 20%/46% (30/20 档对应 26.67%/40%),并做成可配置。
- 手动录入会陈旧:页面显示"数据更新于 N 天前",超过 30 天横幅置灰, 浏览器通知也会在正文里标注陈旧天数。
- 阈值可能数年不触发:这是永久投资组合的正常特征,可在设置页切到 30/20 换算档。
- QDII 是 T+1 确认:录入时请以已确认份额为准,否则会有 1-2 个百分点的口径误差。
- 补录用的是当前持仓:历史页的补录只是把当前持仓标记成你选的那一天, 不会还原那天的真实持仓。
- 改了标的的记价方式不会搬移已有数据:从"数量 × 单价"改成"直接填金额"后, 原来的数量/单价会留在库里但不再参与估值。建议新建标的而不是改已有的。
- 不做多币种、税费计算、交易流水:个人自用范围内的有意简化。
- 前端产物约 1.2 MB(主要是 ECharts),个人内网使用无影响。
- Vite dev server 绑定 127.0.0.1:Vite 5.4 默认只监听
[::1](纯 IPv6), 那样浏览器打开 README 里写的http://127.0.0.1:5173会连接被拒绝。 配置里已显式指定host: '127.0.0.1';如需 IPv6 访问,改用http://[::1]:5173。 - pnpm 10+ 默认拦截依赖构建脚本:
frontend/pnpm-workspace.yaml已白名单 esbuild, 否则 vite/vitest 会直接报ERR_PNPM_IGNORED_BUILDS而无法运行。 - pip 镜像源:若
pip install报"找不到 fastapi 的版本",多半是全局index-url指向了一个不可用的镜像。加-i https://pypi.org/simple绕过。
数据结构已为以下扩展预留(instruments.code / pricing_mode / 快照表):
- 接入行情自动更新(东方财富
push2hiskline 接口,免 token、支持后复权) - 越界时推送到企业微信 / 飞书 / Server 酱
- 情景压力测试面板(输入"黄金跌 50%",直接显示组合总损失)