Aurelio 储蓄目标所需回报率:从公式、二分求解到 17.5% 的实战解读
【免费下载链接】aurelio-finance
Open-source personal finance app with an AI financial advisor: track your net worth, investments, ETFs, cash and debts on your own computer.
本文围绕 Aurelio(一款开源的个人财务应用,可自托管并内置 AI 财务顾问)中「储蓄目标需要多少年化回报率」这一功能展开:先给出目标的终值公式与数值求解方式,再用 demo 家庭的「三年买 18,000 欧元汽车」算例逐档验证,最后说明backend/app/planning.py的源码实现、REST 接口与前端实时提示的对应关系。读完后你应能独立推演任意目标所需回报率,并准确理解 Aurelio 给出 0%、4%、8%、12% 这些读数的含义与边界。
摘要:储蓄目标所需回报率,就是把「现有本金 + 每月追加」在到期日推到目标金额所需的年化增长率。4,000 欧元起步、每月 250 欧元、三年要凑到 18,000 欧元,需要17.5%/年——Aurelio 会把这条判读为「不现实」。若只靠储蓄就能达成,答案是 0%;若只有不切实际的回报才能达成,正确的动作是改计划(每月多存、推后日期、下调目标),而不是期待那个回报。
问题定义:目标的终值公式
设C为当前已有金额,M为每月追加金额,i为月回报率,n为月数,则到期价值为:
FV = C × (1 + i)^n + M × ((1 + i)^n − 1) / i这一公式同时出现在文档与源码的模块级文档字符串中,见 backend/app/planning.py。
- 当
M = 0(不再追加)时方程可直接解出:i = (target / C)^(1/n) − 1,这是闭式解; - 当
M > 0时没有闭式解,只能数值求解。Aurelio 用**二分法(bisection)**求月利率,再换算成年利率:(1 + i)^12 − 1。
注意这里的年化不是简单乘 12,而是按月复利折算,因此「月利率 0.5%」对应的年化是 6.17% 而不是 6%。这一点在复现计算时最容易出错。
算例:demo 家庭三年买一辆车
Aurelio 的 demo 数据里有一个具体目标(见 backend/app/demo.py):目标 18,000 欧元、三年后到期、现有 4,000 欧元、每月追加 250 欧元。你可以在 自托管快速上手 里用./start.sh --demo直接把这套数据跑起来。
不作任何投资增长时:4,000 + 36 × 250 = 13,000,距目标差 5,000 欧元。填补这个缺口的回报率是17.5%/年,Aurelio 的判读是「Unrealistic: consider extending the horizon, raising contributions, or lowering the target.」
单改某一项会怎样:
| 方案 | 所需回报率 | Aurelio 的判读 |
|---|---|---|
| 按原记录:每月 250,3 年 | 17.5% | Unrealistic(不现实) |
| 每月 300 | 10.7% | Aggressive(激进) |
| 每月 350 | 4.5% | Moderate(温和) |
| 每月 389 | 0% | Already on track without investment growth |
| 改为 4 年 | 4.8% | Moderate(温和) |
| 改为 5 年 | 0% | Already on track without investment growth |
| 目标降为 16,000 的汽车 | 11.0% | Aggressive(激进) |
表里的数字可以用仓库里的函数直接复核(下面的命令在backend/目录、已安装项目依赖的环境下执行):
python3 -c " from app.planning import required_annual_return as r, assess for monthly in (250, 300, 350, 389): rate = r(4000, monthly, 18000, 3) print(monthly, round(rate * 100, 2), assess(rate)) print('4y', round(r(4000, 250, 18000, 4) * 100, 2)) print('16000', round(r(4000, 250, 16000, 3) * 100, 2)) "输出为17.54 / 10.7 / 4.47 / 0.0 / 4.8 / 11.02。注意「每月 389」恰好是 0%:因为4,000 + 36 × 389 = 18,004 ≥ 18,000,在required_annual_return里直接命中if fv(0.0) >= target: return 0.0这个前置判断(backend/app/planning.py),根本不会进入二分循环。也就是说,0% 不是一个算出来的小数字,而是一个「无需增长即可达成」的语义结论。
对三年期的目标来说,月供金额和到期日对结果的影响,远大于任何你可以合理期待的回报率。三年时间太短,一个长期看应当获得温和回报的组合,也可能正好在你需要用钱的时候处于浮亏。
更长的目标:增长承担更多工作
同样的算术放到更长的期限上,复利的分量就显现出来了:
| 目标 | 所需回报率 |
|---|---|
| 25 年后 300,000,从零开始,每月 500 | 5.2% |
| 同上,但从 10,000 起步 | 4.4% |
| 5 年后 50,000,从 10,000 起步,每月 600 | 2.8% |
| 10 年后 60,000,从 50,000 起步,不再追加 | 1.8% |
| 5 年后 200,000,从 20,000 起步,不再追加 | 58.5% |
最后一行的「无追加」情形走的是闭式解分支,5 年即n = 60,(200000/20000)^(1/60) − 1得到月利率约 3.9%,年化即 58.5%。这也是为什么短期限 + 高倍数的目标天然无解,而不是「风险高一点就能做到」。
二分法在源码里是怎么实现的
required_annual_return(current, monthly, target, years)的返回语义有三种:0.0表示无需增长;None表示在这些投入下任何利率都到不了;其余为年化小数(0.175即 17.5%)。核心逻辑见 backend/app/planning.py,可以拆成四步:
- 期限校验:
years <= 0直接返回None,月数n = years * 12.0。 - 零利率快检:
fv(0)用current + monthly * n计算(避免了除零),若已达标返回0.0。 - 可达性判定:
current <= 0 and monthly <= 0时返回None——「无中生有」在任何利率下都不成立;再取上界hi = 1.0(每月 100%,一个夸张的上界),若fv(1.0)仍小于目标,也返回None。 - 二分收敛:
lo = 0.0、hi = 1.0,固定迭代100 次取中点,最后(lo + hi) / 2作为月利率,并换算为(1 + monthly_rate) ** 12 - 1。
两个容易被忽略的实现细节:
- 溢出保护。
(1 + 1.0) ** n在n ≥ 1024个月(约 85 年)时超出浮点上限。源码在 backend/app/planning.py 里捕获OverflowError并返回float("inf"),因为只要current或monthly有一个大于 0,fv就随i单调递增,inf在读作「已超过目标」时是正确的方向。这一处修的是真实缺陷:把溢出当达标,会为「本金为零、月供为零」的目标找出一个利率;因此第 3 步的current <= 0 and monthly <= 0判断必须放在上界检查之前。对应回归测试见 backend/tests/test_planning.py 与 backend/tests/test_a_goal_a_century_away.py。 - 精度自证。测试不是断言「约等于 7.2%」,而是把解出的年利率反解回月利率、重算终值,要求相对误差小于 0.1%,见 backend/tests/test_planning.py。这是验证数值解法最稳妥的方式。
判读档位:0%、4%、8%、12% 从哪来
assess(r)把结果映射成一句结论(backend/app/planning.py)。这些是读计划的经验法则,不是对任何市场回报的预测:
| 所需回报率 | 判读 | 源码条件 |
|---|---|---|
| 0% 或更低 | Already on track without investment growth | r <= 0 |
| 不超过 4% | Conservative: feasible with a low-risk allocation | r <= 0.04 |
| 不超过 8% | Moderate: in line with a diversified long-term portfolio | r <= 0.08 |
| 不超过 12% | Aggressive: high risk, not guaranteed | r <= 0.12 |
| 高于 12% | Unrealistic: consider extending the horizon, raising contributions, or lowering the target | 其余 |
| 任何利率都到不了 | Target effectively unreachable with these contributions. Raise contributions or extend the horizon. | r is None |
由于是<=逐级判断,边界值归入较宽松的一档(正好 8% 是 Moderate)。注意None与「高于 12%」是两种不同结论:前者表示这批投入在数学上无解,后者表示有解但代价荒谬。
计算中有两样东西不包含在内:
- 通货膨胀。整个计算是名义金额。以今天的价格写下的目标,十年后达成时购买力更低——要么用未来货币写目标,要么把所需回报读作实际回报并在其上叠加通胀。
- 税费与成本。基金费率与收益税从回报里扣,所以你需要的税前、费前回报要略高于这个数。
接口、前端与 AI 的三种读法
直接调用计算接口
POST /api/planning/required-return是不依赖数据库的纯计算端点(backend/app/routers/goals.py),请求体由RequiredReturnRequest约束(backend/app/schemas.py:current_amount与monthly_contribution默认 0 且须>= 0,target_amount必填且须> 0,target_date为日期):
curl -s -X POST http://127.0.0.1:8000/api/planning/required-return \ -H 'Content-Type: application/json' \ -d '{"current_amount": 4000, "monthly_contribution": 250, "target_amount": 18000, "target_date": "2029-10-12"}'响应是{ "years": 2.95, "required_annual_return": 0.1754..., "assessment": "Unrealistic: ..." }。years由(target_date - today).days / 365.25得出并四舍五入到两位(backend/app/planning.py):用 365.25 是为了让闰年不改变「年」的含义。若日期已过,返回required_annual_return: null且判读为The target date is in the past.。
表单里即时出现的彩色提示
在 Profile → Goals 里选「Target amount by a date」,填上目标、现有金额、每月追加与日期,页面立刻给出结论,例如「Over ~3 years this requires a return of17.5%/yr.」frontend/src/components/Goals.tsx 的实现要点:
useEffect监听type / targetAmount / targetDate / currentAmount / monthly五个字段,任一变化就以空值兜底(Number(x) || 0)重新请求;- 带
cancelled标志,避免快速输入时旧响应覆盖新结果;请求失败则把提示置空,不让表单因提示报错而中断; - 颜色阈值在
RequiredReturnNote中:null或> 0.12为红色(bg-down-tint),> 0.08为琥珀色(bg-warn-tint),其余为绿色(bg-up-tint); - 展示时用
(r * 100).toFixed(1)保留一位小数,null显示为unreachable。相应请求封装在 frontend/src/api/goals.ts 的computeRequiredReturn。
因为「年数」是服务端按当天算的,同一份表单过几天再看,years与回报率会有轻微变化——这是预期的,目标日期在逼近。
聊天与 AI 顾问看到什么
聊天上下文(以及 MCP 服务器暴露的同一份「财务全貌」)会为每个带金额和日期的目标输出一行,含所需回报与判读,构建逻辑见 backend/app/advisor.py:target 18000 EUR by ... (current 4000, +250/mo, ~3.0y) -> requires 17.5%/yr (Unrealistic: ...)。这让模型不必自己重算,避免同一批记录算出两个答案。
更重要的是,sizing.py会把「一笔钱能站在什么基础上」一次性算清楚(backend/app/sizing.py):每个目标的months_left、gap、needs_a_month_with_no_growth(缺口除以月数,零增长)、lacks_a_month(月需减去已记录的月供,不小于 0)以及required_return。demo 的汽车对应:缺口 14,000、14,000 / 36 ≈ €388.80的月度需求、已记录€250,于是还缺€138.80——正是文档给出的数字。这里的关键设计是:已记录的月供同时被计入「每月承诺」,在计算每月还能投多少时先被扣掉,而「目标还需要多少」只并列展示、绝不从可用额度里再减一次,从而避免重复扣减。
常见问题
多少回报率算现实?没人知道市场未来会给出什么回报。Aurelio 的档位把年化 4% 以内读作保守、8% 以内读作与分散化的长期组合相符、12% 以上读作不现实;它们是读计划的尺子,不是承诺。
怎么降低目标所需的回报?每月多存、把日期推后、或下调目标。对 demo 的汽车,每月多 100 欧元就能把 17.5% 降到 4.5%;多给一年则降到 4.8%。
这跟储蓄计算器是一回事吗?方向相反:储蓄计算器问「给定回报,你将有多少」;这里问「你的计划需要什么回报」。
零回报和「完全无解」怎么区分?前者是0.0(现有本金加月供已够),后者是null(本金与月供同时为 0,或连每月 100% 的利率都到不了目标)。前端把null显示为unreachable,与红色但仍有具体数字的「不现实」是两种不同处境。
相关源码与延伸阅读
- 公式、二分求解与档位判读:backend/app/planning.py
- 每个目标的月需、缺口与所需回报:backend/app/sizing.py
- 计算接口与请求/响应模型:backend/app/routers/goals.py、backend/app/schemas.py
- 前端即时提示与配色:frontend/src/components/Goals.tsx、frontend/src/api/goals.ts
- 数学测试与超长期限回归:backend/tests/test_planning.py、backend/tests/test_a_goal_a_century_away.py
- 同一批「金额站在什么基础上」的测试:backend/tests/test_how_much_stands_on_the_records.py
- 目标与规划的系列导读:wiki/goals-and-planning.md
- 应急基金需要几个月:wiki/how-many-months-of-emergency-fund-do-you-need.md
教育与信息性质内容,不构成受监管的财务建议。
【免费下载链接】aurelio-finance
Open-source personal finance app with an AI financial advisor: track your net worth, investments, ETFs, cash and debts on your own computer.
相关推荐
Aurelio 目标与规划实战:应急储备金月数与储蓄目标所需收益率的计算原理
Aurelio 目标与规划实战:应急储备金月数与储蓄目标所需收益率的计算原理 Aurelio 的「目标与规划」并不停留在心愿清单层面:它把每个规划问题都还原为可
终极Luckysheet数据导出实战指南:从办公需求到专业报表的完美转换
终极Luckysheet数据导出实战指南:从办公需求到专业报表的完美转换 Luckysheet作为一款强大的开源电子表格工具,提供了直观的界面和丰富的功能,帮助
前端UI组件基于 gopsutil 的 Go 系统指标采集实战:从监控上报需求到源码级原理解析(Go 夜读讨论)
基于 gopsutil 的 Go 系统指标采集实战:从监控上报需求到源码级原理解析(Go 夜读讨论) 本篇源自『Go 夜读』微信群的一次技术讨论(记录见 con
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考