一名程序员的投资记账方案:Beancount + Fava
你可以在这里找到本文的 Beancount 例子
关于记账
这里说的财务自由是
$$ 被动收入 \ge 生活支出 + 风险缓冲 $$
在我看来,记账对实现财富自由是有很大益处的,包括
- 明确信息:对自己的财务状况有一个明确的估计,回答钱从哪里来,钱到哪里去的问题
- 复盘消费:复盘自己的消费结构分布,复盘哪一些是该花的,哪些是不应该花的
- 支撑决策:复盘不同的资产占比(如股票、债券、货币基金等),计算收益率,评估自己的安全边际等
具体到记账而言,我认为有 2 类
- 消费账:主要用于消费的复盘,也就是服务于财富自由公式里的 $生活支出$
- 投资账:记录自己的投资收益,也就是服务于财富自由公式里的 $被动收入$
只要把这两个账弄清楚了,就知道自己距离财富自由还有多远的路要走 :)
那么问题来了,要用什么软件记账?在挑选软件的时候,我认为下面几个特性是值得关注的
- 通用特性
- 隐私性:个人财务数据是很敏感的,理想情况下它不应该与个人身份标志(如手机号、身份证号等)进行关联。因此那些需要登录才能使用的软件一律被我排除了
- 可视化:不管是消费账还是投资账,我们都需要可视化图表(如饼图、曲线图等)。人类对数字还是没那么敏感的,但是图表一眼就能看出来
- 多端协同:记账的时候手头很有可能只有手机,但复盘的时候对着电脑操作会更好一点。因此多端协同在我看来也很重要
- 导入导出方便:软件是存在生命周期的,你所使用的软件可能在未来的某一天无人维护。因此支持历史记账记录的导出在我看来是很重要的事情
- 消费账软件的特性
- 消费类别可以多级分类:支持对消费类别进行分类,整体呈现一个树状一样的结构,比如
住 -> 电费, 住 -> 水费等 - 消费记录支持打标签:有时候树状结构的消费类别分类无法满足使用需求。比如出去旅游的时候你会有各种消费,他们属于不同的类别,你需要一种方式将他们关联起来。此时如果给他们打上
#旅游就是一个不错的思路
- 消费类别可以多级分类:支持对消费类别进行分类,整体呈现一个树状一样的结构,比如
- 投资账软件的特性
- 资产负债表:提供清晰的资产视图、负债视图
- 投资收益相关指标的计算:支持按月/按季度/按年等时间维度计算回报率、波动率、利润、亏损等
- 资产之间可自由组合进行指标的计算:很多时候你的资产配置里可能有股票、债券,而股票下面可能又有 A 股,又有美股。理想的投资账软件应该支持你随意组合这些标的和相关指标的计算
消费账我用的是 iCost,它基本满足了我对消费账的核心诉求,也满足上面提到的几个核心特性。关于投资账我之前用的是有知有行,美中不足的是
- 它需要用手机登录,我有些担心个人隐私问题
- 不区分份额和金额。如果你有投资的话应该清楚,对于净值型产品,买入的时候会将买入金额转化为对应的份额,份额的净值会每天波动。但有知有行 App 里没有办法看到每天的涨跌(除非你每天不厌其烦自己更新)
- 年化收益率只有持有 $\ge$ 1年的时候才可以查看。我理解它这样设计是为了避免用户因为短期涨跌去做交易操作,但我投资的话不会这样。它也没有提供一个开关选择关闭这个特性
经过一番搜索之后我发现,使用 Beancount + Cocono 记账(iOS 端)+ iCloud 同步应该可以满足上面我提到的几个核心需求。因此下面展开讲讲我是怎么做的,抛砖引玉一下~
目标与非目标
目标:本篇文章的目标是讲解如何使用 Beancount 做投资账,涵盖了投资账的几个经典场景
非目标:本篇文章不涉及如何用 Beancont 做消费账,因此不涉及预算等功能的讲解
如何用 Beancount 做投资记账
基本 Beancount 语法
Beancount 规定账户必须以下面这几个类型开头
Assets:资产(股票、债券等)Expenses:支出,注意这个是正数Income:收入,注意这个是负数Equity:权益,大部分场景不涉及,可以直接忽略Liabilities:负债,比如房贷、车贷
Beancount 支持用 : 建立树状分类结构
Beancount 的账本是一个文本文件,遵循 Beancount 的语法。这一套语法很简单
首先使用 open 进行开户,比如我们开一个用于追踪纳斯达克的股票账户,那么可能涉及
- 一个
Assets类型账户记录持仓 - 一个
Assets类型记录金额来源,这里我们统一用Assets:Cash,也就是假设投资的金额都来自于现金 - 一个
Expenses类型的账户记录买入的手续费 - 两个
Income类型的账户- 一个用于记录可能的分红
- 一个用于记录卖出时的资本利得
这里的 option "operating_currency" "CNY" 指定了账本的主要货币为人民币
option "operating_currency" "CNY"
2025-01-01 open Assets:Cash
2025-01-01 open Assets:Stock:NASDAQ:基金A
2025-01-01 open Expenses:Stock:NASDAQ:基金A
2025-01-01 open Income:Stock:NASDAQ:基金A:Dividend
2025-01-01 open Income:Stock:NASDAQ:基金A:CapitalGain
接下来你就可以正式记账了,最简单的记账模板长这样子
<日期> * "记账备注"
账户1 金额1
账户2 金额2
...
账户n 金额n
Beancount 采用复式记账,因此你需要确保上面的 金额1 + 金额2 + ... + 金额 n 加起来为 0
也因为这一条性质,你可以省略其中某个账户的金额,Beancount 会帮你做计算
净值型产品买入
净值型产品的记录利用了 Beancount 的商品(Commodities)系统。想像一下,你买入净值型产品的时候买入的是份额,那么这些份额就像商品一样的,有数量和买入价。那么卖出的时候也会有卖出价。只要你在记账的时候告诉 Beancount 买入价、卖出价是多少,那么它就会自动帮你算收益
首先,我们先告诉 Beancount 有这么一个商品存在。既然例子是纳斯达克,对应基金A,那么商品名字可以考虑叫做 NASDAQA
2025-01-01 commodity NASDAQA
2025-01-01 commodity CNY
这里将 CNY 也视作一种商品是为了后面用 fava-portfolio-returns 计算回报率
假设 2025 年 9 月 1 日买入基金A 100 元,买入净值是 1.2345,手续费按 0.03% 折算
2025-09-01 * "定投"
Assets:Stock:NASDAQ:基金A 81.00 NASDAQA {1.2345 CNY}
Expenses:Stock:NASDAQ:基金A 0.03 CNY
Assets:Cash
简单算一下就可以知道大概是 81.00 的份额,手续费大概是 0.03 元。你可以注意到上面的 { ... } 记号,这个记号就是用来告诉 Beancount 你买入的每一个 NASDAQA 的成本价是多少。所以上面这条记录翻译过来就是:2025 年 9 月 1 日买入基金 A 一共 81 的份额,买入成本是每份额 1.2345 元,手续费花了 0.03 元
这里我们用到了前面提到的一个特性:因为复式记账,你可以不用写 Assets:Cash 涉及多少金额
总结一下,净值型产品买入的记账模板
<日期> * "记账备注"
账户1 商品代码 {买入时净值}
账户2 金额2
...
账户n 金额n
净值型产品卖出
同理,卖出的时候你可以指定成本价。除此之外,你还得解决一个问题:卖出的时候卖出的是哪些份额?举个例子,采用上面的记账方式,你持仓可能是
- 2025 年 9 月 1 日买入的份额,买入净值是 1.2345 元
- 2025 年 9 月 2 日买入的份额,买入净值是 1.2335 元
- 2025 年 9 月 3 日买入的份额,买入净值是 1.2215 元
2025-09-02,2025-09-03 对应的记账记录这里就不放了,节省篇幅
假设你在 2025 年 9 月 4 日决定卖出 50 个份额,那么你卖出的是哪个?默认情况你需要明确指定,指定方式有
- 指定净值:Beancount 会自己去找历史记录里对应的份额
- 指定日期:Beancount 会卖出对应日期的份额
整体来看模板是
<日期> * "记账备注"
账户1 商品代码 {指定份额} @ {卖出时净值}
账户2 金额2
...
账户n 金额n
那么现在假设你卖出的是 9 月 2 日的份额,并且 9 月 4 号净值是 1.3355 元。假设手续费为 0 ,那么你卖出的时候拿到了约 $1.3355\times 50\approx 66.78$ 元,卖出时的写法就是
2025-09-04 * "卖出"
Assets:Stock:NASDAQ:基金A -50 NASDAQA {2025-09-02} @ 1.3355 CNY
Assets:Cash 66.78 CNY
Income:Stock:NASDAQ:基金A:CapitalGain
或者指定 9 月 2 日对应的净值:
2025-09-04 * "卖出"
Assets:Stock:NASDAQ:基金A -50 NASDAQA {1.2335 CNY} @ 1.3355 CNY
Assets:Cash 66.78 CNY
Income:Stock:NASDAQ:基金A:CapitalGain
这里我们再次利用了 Beancount 的复式记账的特点——将 Income:Stock:NASDAQ:基金A:CapitalGain 的金额省略不写,Beancount 会自动帮我们基于买入时的净值计算收益
但其实,大多数情况下你不需要选择卖出什么时候的份额,因为很多软件默认就是先赎回最早申购的,那么就是一个 FIFO 队列,只需要在记账的时候告诉 Beancount 就行——在 open 开户的时候最后加上 "FIFO"
2025-01-01 open Assets:Stock:NASDAQ:基金A "FIFO"
此时你卖出的时候 {} 里置空就行,其他的照常
净值型产品分红
分红考虑 2 种场景
第一种,红利再投资,此时你的份额会增加。假设基金 A 在 9 月 5 日发生了分红,你可以在对应 App 里面找到具体分红转化成的份额和单份份额价格,然后记录的时候像这样
2025-09-05 * "红利再投资"
Assets:Stock:NASDAQ:基金A 20 NASDAQA {1.2335 CNY}
Income:Stock:NASDAQ:基金A:Dividend
可以看到,跟买入的逻辑是差不多的
第二种,现金分红,此时直接打款到现金里,比上面还简单
2025-09-06 * "现金分红"
Income:Stock:NASDAQ:基金A:Dividend 23.00 CNY
Assets:Cash
非净值型产品记录
并不是所有的投资标的都会有变化的净值(比如货币基金),或者是净值不披露(如投顾产品),或者说净值获取不方便(很多银行理财都是),此时比较简单:直接按照人民币进行结算。只需要偶尔计提一下利息就可以
你就按正常交易思路去做就好。下面的几条记录涵盖来买入、卖出场景
2025-01-01 open Assets:Pocket:基金B "FIFO"
2025-01-01 open Expenses:Pocket:基金B
2025-01-01 open Income:Pocket:基金B:Dividend
2025-01-01 open Income:Pocket:基金B:CapitalGain
2025-09-06 * "买入基金 B 1000 元"
Assets:Cash -1000.00 CNY
Expenses:Pocket:基金B 1.20 CNY
Assets:Pocket:基金B
2025-09-07 * "从基金里提取 10 元"
Assets:Cash 10.00 CNY
Assets:Pocket:基金B
2025-09-07 * "从基金里提取 10 元并计提 2.00 元的利息"
Assets:Cash 10.00 CNY
Income:Pocket:基金B:CapitalGain -2.00 CNY
Assets:Pocket:基金B
基本可视化(Fava)
Fava 是用于 Beancount 记账可视化的一个软件。我们可以使用 uv 进行安装
$ mkdir example && cd example
$ uv init .
$ uv add fava
接下来启动 Fava(假设前面提到的 Beancount 记录放在了 example.beancount 里)
$ uv run fava example.beancount
你可以看到损益表(Income Statement)、试算平衡表(Trial Balance)等图表,我这里放 2 张截图让你感受一下
投资组合的回报率计算(fava-portfolio-returns)
现在你已经可以在 Fava 里面看到各种图表了,但你会发现你没有办法查看投资组合的回报率、波动率等指标,你只能看到绝对值的变化。但从投资的角度出发我们会更关注回报率等指标,因为这个才能衡量你的投资盈利能力如何
幸运的是,社区上早就有人有这种诉求。经过一番工具对比,我发现比较靠谱的是 fava-portfolio-returns,它是基于 beangrow 的一个 Fava 插件
首先添加相关的依赖
$ uv add fava-portfolio-returns
接下来我们需要在你的账本里面加上这么一行,用于表示你启用了这个 Fava 插件
2010-01-01 custom "fava-extension" "fava_portfolio_returns" "{
'beangrow_config': 'beangrow.pbtxt',
'pnl_color_scheme': 'red-green',
'language': 'zh-cn'
}"
这里的 red-green 表示红涨绿跌。beangrow.pbtxt 则是配置文件。如果你自己去看 beangrow 的配置教程的话很容易迷失,因为它写得实在是太长了,包括实现的原理啥的
我这里简单总结一下,beangrow.pbtxt 里需要配置 investments 和 groups。你需要为每一个需要跟踪的投资标的,增加对应的 investment 声明,包括
asset_account:investment 对应的账户dividend_accounts:对应的分红账户cash_accounts:用于计算现金流的账户
然后你可以在 groups 里任意添加 group(使用 investment 指定,允许使用 * 这种通配符),每一个 group 都会计算相关的指标。这也是前面提到的随意将投资标的做组合。沿用本文的例子,配置方法是
investments {
investment {
currency: "CNY"
asset_account: "Assets:Stock:NASDAQ:基金A"
dividend_accounts: "Income:Stock:NASDAQ:基金A:Dividend",
cash_accounts: "Assets:Cash",
}
investment {
currency: "CNY"
asset_account: "Assets:Pocket:基金B"
dividend_accounts: "Income:Pocket:基金B:Dividend"
cash_accounts: "Assets:Cash",
}
}
groups {
group {
currency: "CNY"
name: "个人资产配置"
investment: "Assets:Stock:*",
investment: "Assets:Pocket:*",
}
}
说实话,我也没有摸清 beangrow 具体需要什么日期的净值。一开始我是看它提示缺失什么日期就补充什么日期。后面我发现了 akshare 提供了一个 API 用于查询开放式基金的历史净值,只需要提供基金代码就可以。自己根据这个 API 生成如下的 price 语句对一名程序员来说不算什么难事 :)
另外你还需要指定某些特殊日期的净值,beangrow 计算回报率的时候需要。在 Beancount 里,指定商品的净值使用 price。比如本文的例子里,9 月 1 日基金 A 单份额是 1.2345 元,那么就这么写
2025-09-01 price NASDAQA 1.2345 CNY
建议这里使用 akshare 提供的 API 获取历史净值,并自动生成上面这样的 price 语句
一切就绪之后,重新启动 Fava 你就可以看到
总结
上面谈到的几个功能应该覆盖了 90% 投资记账的场景,心动的话就来试试看吧🥳