Contents

『Beancount指南』复式记账

一直都有记账的想法。

我们经常都会有的困惑:感觉没干什么,工资就没了。随着工作年限的增加,愈发觉得对自我财务进行管理非常重要。这既能察觉不必要的花费,又不至于过于紧缩,什么都不敢消费。

随着我逐渐开始折腾各类金融产品,我才意识到清楚了解自身的财务状况开始变得越来越困难,让我对我的财务状况引起了深深的担忧,所以好好规划下记账这件事情。

记账是个人理财的基础,更是通往财务自由的必经之路。

如何实现财务自由

这个章节标题有点吸引人眼球,像是成功学教程。但是我保证我下面说的内容都是严肃,而且有明确定义和方法的。

为什么要记账,我来用一句话总结就是为了提升对自我的认识。记账是个人理财的基础,更是通往财务自由的必经之路。财务自由这件事情固然是根本需要赚钱来实现,但并不是只有一夜暴富才能实现的。财务自由其实并不是一件虚无缥缈的事情,而是每个人都可以努力达到的一种状态,换句话说就是「退休」,只不是有人能在三十多岁退休,有人要六十岁,有人则要到八十岁。

财务自由的一般定义是由资产产生的收入不少于生活开销。如果不知道自己有多少开销,甚至不知道自己有多少资产、收入,即便是一夜暴富,财务自由是一件不可能的事情。

接下来我来解释为什么财务自由是可以实现的,而且应该是每个人的目标。首先需要强调的概念是**资产(Assets)净资产(Net Assets)**的区别,虽然我之后还会详细解释,但是这里需要记住,财务自由并非对个人净资产的要求,这也就是为什么不需要暴富就能实现。像所有投资都有期限一样,个人的寿命也是有期限的。每当我想到我总有一天会死,都会觉得有些忧伤,但这是无法改变的自然规律。只要在预期的寿命之内资产(而不是净资产)产生现金流能够满足生活所需的开销,这就是财务自由。我对资产产生现金流的定义较为宽泛,不仅包括利息、分红、租金、版税这类收入,还包括了直接通过资产折现的收入,即净资产减少。

要想达到财务自由,需要三点要求:对支出的预期、对资产和收入的了解、对寿命的期望。这三点都是说起来容易,做起来难的,但是记账可以帮助你很大程度上解决至少前两个问题。至于第三个问题,才是财务自由的根本困难所在,不过这个问题的解决方案已经超出了本文探讨的内容了。

本文介绍复式簿记的基本概念以及如何使用 Beancount 记账。本文适合的读者:

  • 想要记账的

  • 曾经或正在记账但是目前对记账方式/软件不满意的

  • 控制欲强的

为什么要记账

记账能让自己了解自己的财务状况,用大白话来说就是能回答以下问题:

  • 我的钱从哪来?

  • 我的钱在哪?

  • 我的钱去哪了?

一本维护良好的账本能生成很多有用的财务报表,其中最有用的是「损益表」和「资产负债表」,前者能回答第一个和第三个问题,后者能回答第二个问题。为了维护一本良好的账本,你需要科学的记账方法和科学的记账软件,本文将向你安利一种科学的记账方法「复式簿记」和一套科学的记账软件「Beancount」。

在 2023 年初的时候,我看到了 Beancount 这个软件,便去了解了一下,这才打开了新世界的大门——这才是「double-entry accounting done right」啊!没有复杂的 GUI,只有亲切的 CLI、强大的功能、简明的语法。这才意识到原来复式簿记可以如此简单好用。

用了一年 Beancount 之后,我对它十分满意,因此写了这么一篇博客,希望能将它推广给更多的用户。

阅读以下内容之前,你需要做好以下准备:

  • 有基础的会计知识,至少听说过「会计恒等式

  • 能熟练地在终端里编辑文本文件

  • 对自己的财务状况有基本了解,并愿意对此做出优化

有以下技能会更方便:

  • 基础的 Python 知识,或是其他适合于文本处理的编程语言知识(用于导入银行账单)

  • 熟练使用 Git 等版本管理工具(用于跨设备同步)

为什么要复式记账

记账这件事是那种容易让人因为一时冲动开始,但是很快就放弃的事情。每个记过账人都有不同的原因和契机,但能坚持下来的则凤毛麟角。记账的好处是提升对自己的了解,解决那种不知道自己赚的钱都到哪里去了的问题。但记账的困难也显而易见,那就是麻烦,还容易遗漏、错误。一般来说普通人对记账的理解就是每笔消费都干什么了,所以就是每一笔消费都有一个金额和类别就可以了。这种记账方式一开始简单,但能带来的价值有限,只是开支记录而已,长期看来难以说服自己为了这些价值而忍受麻烦的记账过程。

相比普通的流水账,复式记账的核心理念是账户之间的进出关系,要求所有的记录全部入账,它可以保证账目的完整性和一致性。复式记账可以提供除了开支记录之外的损益表、资产负债表、现金流量表、试算平衡表等报表。复式记账还可以把投资和消费轻易区分,譬如购入电脑、手机,可以作为资产项目入账并定期折旧。同理对各种代金券、点数积分的购入一样要算入资产而不是消费。

虽然有各种各样的手机应用号称可以简化记账,它们把用户交互做得更加简单友善,有的还可以从银行账户、信用卡公司直接抓取数据,降低心理障碍,但与此同时它们却带来了另一个问题,就是数据所有权、安全性和持久性的疑虑。这些工具大多都是把数据存储在云端的,泄漏隐私的可能性不言而喻。更麻烦的是,它们几乎都是自己的专有格式,无法导出保存,或者哪怕可以导出也难以使用。这也意味着你不得不一直用一个产品,直到有一天倒闭或者服务关闭为止。这对我来说是不可接受的,因为在互联网时代能活过十年的产品非常稀少,无论是像 Google 这样随意关闭服务的大公司,还是随时有可能倒闭的小公司。复式记账的价值在于数据的完整性,我对其数据寿命的要求是二十年以上。

复式记账是一种划时代的发明,这个发明被认为是源于中世纪的地中海城邦(意大利或者埃及犹太人)。复式记账技术成为中世纪至大航海时代复杂贸易的支柱性工具,使得复杂的合约、信贷成为可能。后世会计学、金融学的许多概念都来自于复式记账,可以说体系化的资本主义是从复式记账的实践中诞生的。

复式记账

在会计学中,复式簿记(又称为复式记账法)是商业及其他组织上记录金融交易的标准系统。

该系统之所以称为复式簿记,是因为每笔交易都至少记录在两个不同的账户当中。每笔交易的结果至少被记录在一个借方和一个贷方的账户,且该笔交易的借贷双方总额相等,即“有借必有贷,借贷必相等”。

例如,如果A企业向B企业销售商品,B企业用即期支票向A企业支付货款,那么A企业的会计就应该在贷方记为“销售收入”,在借方记为“现金”。相反地,B企业的会计应该在借方记为“进货”,并在贷方记为“银行存款”。

借方项目通常记在左边,贷方则记在右边,空白账簿看起来像个T字,故账户也被称为T字帐。

—- 维基百科-《复式簿记》

有复式记账,那么一定有单式记账,所谓单式记账就是常说的「流水账」,主要用于记录一个账户上的账面流水。比如支付宝里的账单就是典型的单式记账。

大部分普通人所说的「记账」,是指单式簿记(也称「流水账」),每笔交易只涉及一个账户。单式簿记简单易上手,但出错后不易排查,且在面对复杂交易(如一对多、多对多)时力不从心甚至无能为力。

复式记账不同于通常的流水账。

在流水账里,我们把每一笔支出和收入进行一个描述性的记录,例如:

1
2
3
2020年4月15日,抢到红包5.2元,开心。
2020年4月16日,使用支付宝花费34.1元购买水果和酸奶。
2020年4月16日,骑哈罗单车花费1.5元。

为何要采用复式记账?我常听到个人用不上复式记账的说法,然而事实显然并非如此,这一原因在于,每一笔账面数字的减少都是不一样的,每一笔资产都不会是从天上掉下来的(虽然我的账户里的确有“Windfall”这一账户),同时还有的资产可能会通过某种交易后变换一种形式回到自己的资产中(如购买股票),在这种情况下单式记账只能记为如下的两笔:

1
2
2020-08-29 支出 1600 USD ; 购入 1 GOOG
2020-08-29 收入 1 GOOG

你真的支出了1600 USD么?没有,你只是将这 1600 USD 兑换成了1股 GOOG 的股票,而复式记账则会记为如下的一笔

1
2
3
2020-08-29
   A银行现金账户 -1600 USD
   B券商股票账户 +1 GOOG

复式记账将支出与收入连起来了,我们也可以看出,这一次交易没有净资产的变化,变化的只是净资产的类别。单式记账的表达能力较复式记账低了许多。如果说这个例子不够明显的话,不如举另一个例子,即假设你购入了一件商品。

购买操作是完完全全的支出行为,几乎所有人都会记为:

1
2020-08-29 支出 100 CNY ; 购买日用品

由于你是网购,5天后,你发现这个东西收到之后就堆在角落,甚至没有开箱,你决定用“七天无理由”退货,这时候,你会记作:

1
2020-09-04 收入 100 CNY ; 退货

当你统计这一年的总开支的时候,就会凭空多了100元的支出与100元的收入,但实际上你并没有真正花掉这笔钱,你只是将这笔钱暂存在售货方,并在退货时取回了它,对于复式记账,则会记作这么两笔交易:

1
2
3
4
5
6
7
2020-08-29
    A银行现金账户 -100 CNY ; 日用品
    消费账户      +100 CNY

2020-08-29
    A银行现金账户 +100 CNY ; 日用品退货
    消费账户      -100 CNY

当你年末统计自己的消费的时候,你看了一眼消费账户,会发现有一笔增量与一笔减量,但这不要紧,你总是将「消费账户」的值视作自己的消费。

买了又退货,显然算不上消费。

与退货交易类似的,那就是借贷交易了。在借款的过程当中,你的净资产并没有增加,因为随着你的资产增加的,还有你的债务,最常见的就是信用卡了。如果使用单式记账,那么直到信用卡账单到来之前,你都会对着你的银行存款开心,并不知不觉超额消费。而如果你使用复式记账,并追踪自己的「净资产」,则会快速发觉这一点。

但至少……即使你用单式记账,你看到了账单。

  • 讲道理单式记账也可以看总和啊。
  • 但是哪家单式记账软件给你看综总和的?我的银行App都不告诉我净资产的。

复式记账拥有极强的表达能力,因此即使是个人使用,也能从中获益。其实你并不需要懂很多才能开始复式记账,只需要一些最基本的概念就可以了。这个概念就是会计恒等式。

作为会计学的核心,复式记账是一种实践中诞生的技术。除了最基本的会计恒等式之外,复式记账只有规范,没有对错。那么什么是会计恒等式呢?最基本的形式就是:

1
资产 = 负债 + 权益(净资产)

这个等式对于没有接触过会计学的人来说可能不太容易理解,但是记账实践过马上会明白的。理解的难点在于,会计学上的「资产(Assets)」和一般人对一个人富有程度的理解不太一样,因为资产是负债和权益的总和,「负债(Liabilities)」也是资产的一部分。

更容易理解的部分其实是「权益(Equity)」,在个人理财的上下文中,又和「净资产(Net Assets)」是等价的,也就是经常说的「个人净值」。净资产需要由总资产排除负债,一个人到底是否富有,看的是净资产,而不是资产

举例说明,一个人首付 20 万美元,贷款 80 万美元,买了价格为 100 万美元的房产。假设这个人没有别的资产和负债,那么他的资产就是 100 万美元,负债是 80 万美元,而净资产是 20 万美元。在房价没有变化的情况下,买房对一个人的净资产没有什么影响(忽略手续费的话),而他的资产和负债都暴增,也就是所谓的「扩大资产负债表」。

如果你理解了会计恒等式,那么复式记账的一切知识障碍已经扫除了。

要了解复式记账,就要首先理解下面几个概念:

  1. Income 收入
  2. Assets 资产
  3. Expenses 支出
  4. Liabilities 负债

图中显示,我们的每次财务行为,都被抽象为钱在这四个「桶」之间流转。如图中所示一样,我们的钱全部来源于「收入」和「负债」,最终流向「资产」和「消费」,总体上始终满足:

1
收入+资产+负债+支出=0

为了等式成立,「收入」和「负债」使用负数表示。并且每一次记账行为,至少会涉及两个「桶」间的关联。这里用几个例子说明:

2020年4月15日,抢到红包5.2元,开心。 可以记录为:

1
2
2020年4月15日,收入-5.2元。
2020年4月15日,资产+5.2元。

2020年4月16日,使用支付宝花费34.1元购买水果和酸奶。 可以记录为:

1
2
2020年4月16日,资产-34.1元购买水果和酸奶。
2020年4月16日,支出+34.1元购买水果和酸奶。

不难发现,每一笔账单都会有两条记录,并且两条记录的和为0;这也许就是复式记账(Double Entry Bookkeeping)Double Entry 的所在吧。复式记账记录了资金的流动,使其更具有可靠性和可参考性。

作为一个已经著名经久的记账方法,我们远不至于再在记事本上手写这些账目。这里介绍一种简单且高效的复式记账工具。

复式簿记是一种把每笔交易都记录到复数个账户中的簿记方法。举个例子,想像你面前有两个桶,分别是「资产」(Assets)和「费用」(Expenses),左边的桶里装满了豆子,右边的桶是空的。你用 1000 元听了一场演唱会,为了记录这笔费用,你把 1000 粒豆子从 Assets 桶里转移到了 Expenses 桶里,代表你的资产减少了 1000 元,而你花在演唱会上的费用增加了 1000 元,在这个过程中,豆子的总量没有变化(资产减少的豆子与费用增加的豆子数量一致),这便是最简单的复式簿记。

实际上,复式簿记系统中,一般有五个大桶,每个桶里又可以放很多个小桶,这五个大桶分别是:

  • 资产 Assets —— 现金、银行存款、有价证券等;

  • 负债 Liabilities —— 信用卡、房贷、车贷等;

  • 收入 Income —— 工资、奖金等;

  • 费用 Expenses —— 外出就餐、购物、旅行等;

  • 权益 Equity —— 用于「存放」某个时间段开始前已有的豆子,下文详述。

豆子(或是货币)在这五个桶里倒来倒去,出入相抵,这便是会计恒等式。这些桶里剩余的货币数量,则是生成损益表和资产负债表的重要依据。

与传统的复式簿记不同,Beancount 及其前辈们用的复式簿记方法使用了正负号而不是拗口的「借」(debit)和「贷」(credit)来表示五个桶之间的豆子变动,更加容易理解和思考,也不容易出错。

本文所介绍的复式簿记采用 Beancount 的方案。

虽然复式簿记可以用来记任何东西的变动,但主要还是用来记货币的变动,因此桶中的数字是可以为负数的。

具体怎么记呢?再举几个例子:

  • 收入→资产:小明是个无业游民,有天他在路上捡到 100 元,没有交给警察叔叔——收入桶倒出 100 元,资产桶增加 100 元;

  • 负债→资产:小明看中一件新衣服,但是买不起,于是问大明借了 200 元——负债桶倒出 200 元,资产桶增加 200 元;

  • 资产→费用:小明用 300 元买了一件衣服——资产桶倒出 300 元,费用桶增加 300 元;

  • 费用→资产:小明发现衣服不合适,要退货,老板说你穿了好几天了,只能退你 250 元——费用桶倒出 250 元,资产桶增加 250 元;

小明完成了这四笔交易之后,四个桶的状态:

  • 收入:-100 元
  • 负债:-200 元
  • 资产:250 元
  • 费用:50 元

这四个桶里的数字加起来是——0 元。因为这四个桶里的数字之和一开始就是 0,而每笔交易都是在桶之间加减,负数和正数的绝对值相等(和为 0),因此总量并没有变化。

每笔交易在不同账户的数字加起来和为 0 是复式簿记的重要特性和原则,也是用来检验账目正确性的重要依据。

复式簿记这一特性在企业账目管理中有着重要的意义——不同账户的交易内容记在不同的账本上,由不同的财务人员管理,使账目之间互相制约、不容易出错(无论是有意的还是无意的)。

在上面的例子中,「负债 -200 元」和「资产 250 元」挺容易理解的,但是「收入 -100 元」和「费用 50 元」可能不是那么容易一下子想通。如果上面倒豆子的想像没能让你信服的话,以下两个方案有助于理解(但可能并没有倒豆子那么欢乐):

  • 把收入(Income)想像成一个装着你一生(过去和未来)所有劳动成果的桶,每次你的收入都是从桶里取出东西(通常以货币的形式),一直取啊取啊,直到某一天……所以收入桶的数字通常是负数

  • 把费用(Expenses)想像成一个装着你一生(过去和未来)所有消费的桶,每次你的支出都是往桶里放东西(以货币的形式表现),和朋友出去唱歌转换成快乐存进去,看过的电子书转换成精神食粮存进去,吃过的饭转换成……所以费用桶的数字通常是正数

一旦接受了「收入和负债通常为负数」、「资产和费用通常为正数」这两个设定,那你便很容易理解这条等式了:

(Income + Liabilities) + (Assets + Expenses) + Equity = 0

用大白话来说就是:你赚的钱(Income),加上你借来的钱(Liabilities),最终要么变成你自己的钱(Assets),要么就是花掉了(Expenses),最终得到的是个零。这就是人的一辈子……

等下,Equity 是怎么来的?仔细想想小明的例子,他的四个桶要满足这个等式,前提是桶里都是空的。但是小明不是一个刚出生的婴儿,他已经活了二十多年了,之前的 Income、Liabilities、Assets、Expenses 怎么算呢?答案就是放到 Equity 里。当小明决定开始用复式簿记的时候,他从 Equity 里倒一些豆子其他桶里(或从其他桶倒一些豆子到 Equity 里),将其他桶的数字调节成符合当前实际情况即可。实际操作中,人们一般只关心 Income 和 Expenses 桶的数字在某段时间内的变化,并不关心它的总数(除非你想统计你出生到现在一共收入多少、支出多少),只要把 Assets 和 Liabilities 调节准就行了。这便是 Equity 的作用——存放已有的「权益」。

更一般地,Equity 可以用来存放所选取的时间范围之前的「汇总」。比如小明从 2012 年开始用 Beancount,一直用到 2016 年,他想只看 2016 年的财务状况,那 Beancount 便会把他 2016 之前四年的数据「调节」到 Equity 里,来维持 2016 年会计恒等式的平衡。

Beancount 命令行复式簿记

再说一遍,复式记账在于实践,在开始实践之前学很多艰深的会计学概念没有任何意义。要开始实践,就要有工具上手了。在众多工具中我推荐使用Beancount,原因如下:

  1. Beancount是一个开源工具,用Python实现的,可以本地运行。

  2. 账本是一套基于文本的语法,方便存储和管理,个人拥有全部的数据,还可以使用Git管理。

  3. 账本的语法很规范,也具备灵活度,像编程语言一样可以嵌套引入,也有语法高亮和代码检查工具。

  4. 有完整的命令行工具链和可视化工具 Fava,还有基于 SQL 的查询和报表生成。

  5. 没有预先定义的类别、货币等现实世界概念,可以轻松实现多币种记账,包括各种点数、虚拟货币。

除了 Beancount,Plain Text Accounting网站还列出了其他的开源工具比较,类似的具有竞争力的开源工具还有 Ledger 和 hledger,其中 Ledger 是这类工具的开创者。无论使用里面介绍的哪个工具,其基本理念都差不多,即记录账户之间的资金流动。最重要的是,你是账本的所有者,如果不喜欢一个工具了,可以轻易转换到另一个工具。虽然它们语法有些许区别,但写一个脚本来转换不难。

Beancount 是一个基于文本的复式记账软件,于其说是一个复式记账软件,不如说是一个复式账软件,毕竟,它没有提供任何对于记账相关的功能,它提供的是对于某种特定格式的账本的解析功能,实际上的记账人只是你自己,甚至你的账本 Beancount 不会做任何操作。但直到我写这篇文章的时候才发现官方将其定义为「一种复式记账计算机语言」。

简单来讲,它可以让你以纯文本方式记账,并通过一种 类 SQL 的语言 来对交易进行查询。记账文件还可以配合 Git 进行版本控制。

此外,Beancount 官方提供了一个名叫 fava 的图形化管理工具,它基于 Web,能够提供比原生页面更加丰富的内容,一般记账所需要的信息一目了然。想体验的同学可以在官方提供的 Demo 中简单感受下。

Beancount 是一个 Ledger-like 软件。Ledger 是这一类复式簿记软件的开创者。他们共有的特点是:

  • 采用改进的复式簿记方案(使用正负号而不是「借」和「贷」来表示账户之间的变化);

  • 使用纯文本文件作为账本,用户用文本编辑器即可记账;

  • 账本既是用户输入的文件,同时也是软件的「数据库」;

  • 软件读取账本并生成报表,账本本身也可供人类直接阅读。

市面上的复式簿记软件不少(如 GnuCash),但是大部分都是提供一个 GUI,用户在一堆文本框里输入各种数字和文字,软件接受输入然后存储到自己的数据库里(SQLite、MySQL 等)。用户无法直接看到或操作他们的数据,必须通过软件来操作;一旦软件停止更新,用户的数据就危在旦夕:难以导出,难以复用,很难跨平台或跨设备同步。

而 Ledger-like 软件则直接使用文本文件作为账本,用户直接用最喜爱的编辑器打开账本即可记账。软件只是读取你的账本并生成报表,即使软件停止更新,用户依然可以直接阅读账本。你可以方便地在各在平台上记账,甚至跨设备问题也可以用 Dropbox 等同步工具,或是 Git 等版本管理工具轻松解决。

Beancount 是 Ledger-like 软件中优秀的一员,相比用 C++ 写成的 Ledger,用 Python 写成的 Beancount 更轻便,更方便增加插件和二次开发,也增加了很多功能,如灵活强大的多「货币」支持。这里为加上引号是因为,Beancount 其实并不知道什么是「货币」,它记录的只是「通货」(commodity)的变化,所有的 commodity 皆由用户自己定义,因此 Beancount 可以用来记录包括货币在内任何东西的变化,比如年假天数、股票、航空里程、信用卡积分,当然了,还可以用来数豆子,这也是 Beancount 名字的来源。

安装 Beancount 和 Fava

Beancount 的优点很突出:

  1. 开源,使用 Python 实现,可以本地运行。虽然部署需要一些 Python 环境,但身为程序员来说,简直轻车熟路。
  2. 基于文本,使用灵活。也就是可以使用 Git 便捷的进行版本控制。简直为程序员量身打造。

FavaBeancount 的可视化工具。这里简单列出步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. 安装
python -m venv beancount
source beancount/bin/active
pip install beancount fava
# 可以切换为国内镜像源
pip install beancount fava -i https://pypi.tuna.tsinghua.edu.cn/simple

# 2. 从下面地址上下载示例
https://bitbucket.org/blais/beancount/src/default/examples/

# 3. 下载完毕后,在 example.beancount 所在文件目录下执行
fava example.beancount

# 4. 打开浏览器,访问 http://localhost:5000/ 即可

安装可能碰到的问题:

  1. pip 下载速度太慢,请求超时。(可以使用国内的镜像源 https://pypi.tuna.tsinghua.edu.cn/simple
  2. windows 10 缺少类库,导致安装失败。(可以下载 Microsoft Visual C++ Build Tools,自行安装即可)
  3. 部分依赖库版本和本地已有的版本不一致导致安装失败。(更新本地的依赖)

Beancount 本身不需要守护进程,因为记账文件是直接用文本存储在系统中的,beancount 只是用来做查询。不过 fava 服务倒是可以以守护进程的方式部署起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[Unit]
Description=Fava
Documentation=https://beancount.github.io/fava/
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
Restart=on-abnormal
User=pi
Group=pi
Environment=HOME=/home/pi
ExecStart=/home/pi/.local/bin/fava -p 6666 main.beancount
WorkingDirectory=/home/pi/projects/accounting

[Install]
WantedBy=multi-user.target

简单入门

众所周知,这个是著名的会计恒等式:

资产(Assets) = 负债(Liabilities) + 股东权益(Shareholders’ / Owners’ Equity)

这就引入了Beancount的五个基本账户中的三个:Assets、Liabilities、Equity,此外 Beancount 还有两个账户:Income、Expense,分别表示收入和支出的账户。

相比于大多数记账软件,Beancount 推广了「货币」这一概念,一般翻译成「通货」,在实际对于个人而言,不如说是「有价证券」或者「等价物」,其实还是回到了「通货」的含义上。在 Beancount 看来,人民币(CNY)和美元(USD)和一股谷歌C类股票(GOOG)和一辆车都是没有区别的,都是一类等价物,但你可以在交易的时候,标记每个等价物之间的价值,也可以时不时地在账本中写下等价物之间地价值关系。

同时,Beancount 使用纯文本记账,相比于数据库而言,就是不需要任何的 API 知识,自己写一个简单而糙的程序,就可以实现各种导入、导出操作,再也不用配置各种数据库,也不会因为软件的导入导出功能而烦恼。同时,你可以将其存储在Git上做各种维护。

Beancount的一条记录大致长这样:

1
2
3
2020-08-29 * "一笔消费"
    Assets:Bank:CMB       -199.00 CNY
    Expenses:Daily:Misc    199.00 CNY

作为复式记账的基本原则「有借必有贷,借贷必相等」,Beancount 要求每一条记录中的所有数值相加相等(由于通货转换过程中的四舍五入,不可避免地会引入舍入误差,那么在可以接受的误差内)。

既然提到了通货之间的转换问题,那么,对于前文购入谷歌股票的记录,则长这样:

1
2
3
2020-08-29 * "购入1股谷歌-C"
    Assets:Bank:MatsuBank -1600.00 USD
    Assets:Stock              1.00 GOOG { 1600.00 USD }

我们的资产(Assets)并没有减少,只是从一个账户转移到了另一个账户(同时变换了一下形式)。但这里就会说,-1600 USD + 1.00 GOOG 显然不等于0啊。

这就是 Beancount 对于资产的「标签」功能,在这笔交易中,这一股 GOOG 被打上了值 1600 美金的标签,那么实际计算的时候,就是:-1600 USD + 1.00 GOOG ( = 1600 USD ) = 0,这就平了。

Beancount还支持一些别的记录,比如 Balance 记录,表示一条断言,即某一天某个账户上的某个通货的资产应当等于某个数值。比如:

1
2020-08-30 balance Assets:Stock 1.00 GOOG

非常简单的一句话,当 Beancount 发现资产不能对上的时候,就会报出错误,我常常在信用卡的账单日,和一个月结束的时候,通过这一条语句来完成对账操作。

那么,在卖出的时候要怎么记账呢?

1
2
3
2020-08-30 * "卖出1股谷歌-C"
    Assets:Bank:MatsuBank 1650.00 USD
    Assets:Stock 1.00 GOOG { 1650.00 USD }

这样看起来是正确的,但实际上 Beancount 会报错,因为你没有购入价格是 1650 美金的谷歌C股票,实际上,这笔交易可以记成这样:

1
2
3
4
2020-08-30 * "卖出1股谷歌-C"
    Assets:Bank:MatsuBank  1650.00 USD
    Assets:Stock             -1.00 GOOG { 1600.00 USD } @@ 1650.00 USD
    Income:Invesrment       -50.00 USD

表示你以 1650.00 美金卖出了 1 股以 1600.00 买入的谷歌C股票,并获得了 50 美金的收益。在实际操作中,常常会在不同价格零散买入股票、基金,并在某个时间点一口气卖出一定数量的资产。Beancount 支持自动选择一个通货的标签,一般情况下,我用的是**「先进先出」**的策略,这需要在定义账户的时候写明:

1
1970-01-01 open Assets:Stock "FIFO"

即表示此账户上的交易遵循“先进先出”的策略,当没有指明标签的时候,从最早的标签开始。

现实中,仅仅维护 5 个账户(收入、支出、资产、债务、权益)是不够的,Beancount 支持许多个账户,并将其划分到这五类账户当中。

对于我本人而言,我有这么些账户:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
; 银行存款
1970-01-01 open Assets:Bank
; 各类基金及股票
1970-01-01 open Assets:Investment:Fund:R5 "FIFO"
1970-01-01 open Assets:Investment:Fund:R4 "FIFO"
1970-01-01 open Assets:Investment:Fund:R3 "FIFO"
1970-01-01 open Assets:Investment:Fund:R2 "FIFO"
1970-01-01 open Assets:Investment:Stock "FIFO"

; 信用卡
1970-01-01 open Liabilities:CreditCards
; 个人借款
1970-01-01 open Liabilities:Personal

; 工资
1970-01-01 open Income:Salary
; 股权激励
1970-01-01 open Income:RSU
; 奇怪的收入
1970-01-01 open Income:Windfall

; 分红和切割
1970-01-01 open Income:Investment:Fund:R5:Dividend
; 盈亏
1970-01-01 open Income:Investment:Fund:R5:PnL
1970-01-01 open Income:Investment:Fund:R3:Dividend
1970-01-01 open Income:Investment:Fund:R3:PnL

开始记账

也许你已经安装完毕,磨拳搽掌准备行动。

我这里介绍一种初始化账单数据的方案。

一个场景:

小明现在有 工行卡1234(10000元),现金(2000元),欠花呗(1200元);有5000元股票。

然后小明决定今天开始记账,他可以按下面操作开始。

首先创建一个账单文件 xiaoming.beancount

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
* 指定账单使用的货币单位
option "operating_currency" "CNY"

* 开启具有的账户:2020-04-16 指开启时间,之后关于这个账户的资金变动时间不能早于 2020-04-16,否则 beancount 会报错
* CNY 指 货币单位,元
* 工商银行1234
2020-04-16 open Assets:ICBC1234 CNY
* 现金
2020-04-16 open Assets:Cash CNY
* 股票
2020-04-16 open Assets:Stock CNY
* 花呗
2020-04-16 open Liabilities:Huabei CNY
* 可乐消费
2020-04-16 open Expenses:Cola CNY

* 这个账户一般用于初始账户的收支平衡
2020-04-16 open Equity:Opening-Balances CNY

* 初始化已有资产和负债
* 先用 Equity:Opening-Balances 平衡没有数据来源的 Assets:ICBC1234 账户,并且设为 10000 元
2020-04-16 pad Assets:ICBC1234 Equity:Opening-Balances
2020-04-17 balance Assets:ICBC1234     10000.00 CNY

* 同样的方式初始化其他账户
2020-04-16 pad Assets:Cash Equity:Opening-Balances
2020-04-17 balance Assets:Cash          2000.00 CNY
2020-04-16 pad Assets:Stock Equity:Opening-Balances
2020-04-17 balance Assets:Stock         5000.00 CNY

* 债务一般负数表示
2020-04-16 pad Liabilities:Huabei Equity:Opening-Balances
2020-04-17 balance Liabilities:Huabei  -1200.00 CNY

* 然后从 2020-04-17 开始记录自己的收入和支出
* 下班用现金买罐可乐
2020-04-17 * "买罐可乐"
  Assets:Cash                             -3.00 CNY
  Expenses:Cola                            3.00 CNY

xiaoming.beancount 目录下执行 fava xiaoming.beancount。打开浏览器访问 http://localhost:5000/ ,应该就可以看到自己的账目信息。

这里需要注意的是,2020-04-17 balance Assets:ICBC1234 10000.00 CNY 这条语句(包括下面几条同语法的语句)中的 balance 一般表示所记录日期当天的最开始时间 2020-04-17 00:00,所以这里记录了截止 2020-04-16 23:59 的资产,而从 2020-04-17开始记录后续的资金流水。

关于 fava 的使用,这里就不再赘述。

初始化配置

在初始化的时候,需要配置一些属性。

1
2
3
4
5
6
7
8
9
;功能:连接多个 beancount 文件
;语法:include 文件路径
include "accounts/accounts.beancount"
include "events.beancount"

;功能:设置属性
;语法:option 属性名 值
option "title" "我的账本"
option "operating_currency" "CNY"

账户相关

beancount 账户是树形结构的,不支持中文名,在命名的时候可以采用银行的缩写及尾号、法人姓名、用途等等。

1
2
3
4
5
6
7
;开通账户:
;时间 open 账户名
1970-07-01 open Assets:Cards:BOC:1234

;注销账户:
;时间 close 账户名
1999-07-01 close Assets:Cards:BOC:1234

交易记录

beancount的交易记录主要包含时间和相关账户现金流(可以涉及多个账户)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
;功能:交易记录
;语法:时间 [对账标志位] 收款人 备注
;        账户A 变动情况
;        账户B 变动情况

2020-07-01 * "KFC" "10 块钱买了一个吮指原味鸡"  ;*号表示确认无误
  Assets:Cards:BOC:1234 -7.00 CNY
  Equity:Tickets:KFC -3.00 CNY   ;假设有个 3 元的优惠券
  Expenses:Eating 10.00 CNY

;beancount支持单个账户的自动补全,下面的记录和上面的记录是等效的
2020-07-01 * "KFC" "10 块钱买了一个吮指原味鸡"  ;*号表示确认无误
  Assets:Cards:BOC:1234 -7.00 CNY
  Equity:Tickets:KFC -3.00 CNY   ;假设有个3元的优惠券
  Expenses:Eating ;beancount 自动补全,会转化为 Eating 账户 +10.00 CNY

2020-07-01 * "KFC" "10块钱买了一个吮指原味鸡"  ;*号表示确认无误
  Assets:Cards:BOC:1234 -7.00 CNY
  Equity:Tickets:KFC -3.00 CNY   ;假设有个3元的优惠券
  Expenses:Eating ;beancount 自动对齐,会转化为 Eating 账户 +10.00 CNY

证券投资相关的记录会更加完整

1
2
3
4
5
6
7
8
9
;语法:时间 [对账标志位] 收款人 备注
;        账户A 数量 通货名称{成本价} @现价 或者 @@总价 
;        账户B 变动情况
;        账户C 变动情况
2020-07-22 * "A股" "33.29 元加仓 400 股中银证券"
    Assets:Bond:AMS:Cash -13316.00 CNY
    Assets:Bond:AMS:Positions 400.00 A_601696 {} @33.29 CNY
    Expenses:AMS 5.00 CNY
    Assets:Bond:AMS:Cash -5.00 CNY

标签堆栈,堆栈中所有交易记录都会被自动打上标签,便于后期检索

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
;语法:pushtag #标签内容
;       交易记录1
;       交易记录2
;       .........
;      poptag #标签内容

pushtag #2019-07-Europe-Trip

2019-06-01 * "奥地利航空" "东京-维也纳"
    Expenses:Transport:Airline 600 USD
    Liabilities:US:CreditCard:Citi

...

poptag #2019-07-Europe-Trip

价格相关

beancount 中可以使用多种通货,但好比不同货币有汇率,这些通货之间也有对应数量关系,比较直观的理解是某个东西有价格,这就是单位通货和货币的汇率。

1
2
3
;功能:导入某个通货某日的价格
;语法:时间 price 通货 价格
2020-08-01 price GOLD 420.00 CNY 

Beancount 文件格式

装好之后便可以开始写你的第一个账本了。怎么写?Beancount 作者写了非常非常详细的文档:

比如小明如果用 Beancount 的话,他的账本将是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1970-01-01 open Income:Windfall
1970-01-01 open Assets:Cash
1970-01-01 open Liabilities:Da-Ming
1970-01-01 open Expenses:Clothing

2016-01-01 * "捡到钱了"
  Income:Windfall                            -100.00 CNY
  Assets:Cash                                +100.00 CNY

2016-01-01 * "向大明借钱"
  Liabilities:Da-Ming                        -200.00 CNY
  Assets:Cash                                +200.00 CNY

2016-01-01 * "XX 百货商店" "买衣服"
  Assets:Cash                                -300.00 CNY
  Expenses:Clothing                          +300.00 CNY

2016-01-02 * "XX 百货商店" "退衣服"
  Expenses:Clothing                          -250.00 CNY
  Assets:Cash                                +250.00 CNY

首先小明需要设立账户。开户日期可随自己喜好定,只需比最早一笔涉及到该账户的交易更早即可。这里小明都使用了 1970-01-01 作为开户日期,保证今后记录的各种交易都会发生在这个日期之后。

然后便可以真正开始记了,交易的格式如上所示。其中日期后面的星号(*)代表这是一笔已确认的交易,如果换成感叹号(!)的话,则代表这笔交易有疑惑,后期对账时应注意。对账标志后面则是跟着「收款人」(Payee)和「备注」(Narration),需要用引号包起来。Payee 是可选的,只有一个字符串的话,这串字符就是 Narration 了。

小明的账本已经写完了,手工书写,也能肉眼阅读。那么 Beancount 有什么用?当然是生成报表:

1
2
3
4
5
6
(BEANCOUNT) bean-report xiaoming.bean balances
Assets:Cash               250.00 CNY
Equity
Expenses:Clothing          50.00 CNY
Income:Windfall          -100.00 CNY
Liabilities:Da-Ming      -200.00 CNY

于是小明对自己的财务状况一目了然:有 250 元现金,在衣服上花了 50 元,一共收入了 100 元,欠着大明 200 元。bean-report 没有报错,说明账是平的(总和为 0)。bean-report 还能生成很多报表,使用 bean-report -h 查看帮助。

Beancount 自带了一个朴素的 Web UI,能以交互式的方式查看各种财务报表,执行 bean-web xiaoming.beancount 命令,然后在浏览器中打开 http://localhost:8080/ 即可:

小明的资产负债表
小明的损益表

fava 可以看到一个更华丽的 Web UI,执行 fava xiaoming.beancount 命令,然后在浏览器中打开 http://localhost:5000/ 即可。由于小明的数据还比较单薄,这里贴两张 fava 作者的示例图:

fava的资产负债表
fava的损益表

在资产负债表(balance sheet)里,你可以一目了然地看到自己有多少资产、资产分别在哪些账户里、有多少负债、是对哪些银行的负债。

在损益表(income statement)里,你可以一目了然地看到自己的每月有哪些收入、收入来自于哪些地方、有多少支出、支出花在了什么地方。

在这些页面里还有更多报表等待着你去探索。

使用 bean-query 进行复杂查询

bean-web 的朴素 Web UI 和 fava 的华丽 Web UI 已经能展现很多有用的财务报表,满足大部分用户的需求,如果用户需要进行一些更复杂的数据统计,比如「我 2015 年吃过的饭店按次数排列」,则可以使用 bean-query 工具用 SQL 语句进行查询,详见 Beancount 作者的文档:Beancount – Query Language。这是一个用来统计光顾麦当劳次数的例子:

最佳实践

编辑器支持

Beancount 的作者是 Emacs 用户,因此自己写了 Emacs 插件。Vim 用户可以使用第三方的插件:nathangrigg/vim-beancount。安装插件之后会为 *.bean*.beancount 文件加上语法高亮和账户名字补全(比如输入 I:S:S 即可补全出 Income:SomeCompany:Salary),还可以将货币那一列的小数点自动对齐。以下是我的 .vimrc 中相关的配置:

1
2
3
let b:beancount_root = '/path/to/your.beancount'
autocmd FileType beancount inoremap . .<C-O>:AlignCommodity<CR>
autocmd FileType beancount inoremap <Tab> <c-x><c-o>

其他编辑器如 Sublime 等也有各自的插件,请自行 Google。

开户日期的选择

账户的开户日期需要在该账户第一笔交易之前。小明为了省事将所有的账户全部开在了 1970-01-01 这个日期。其实可以有一些更有创意的选择:

  • Expenses 账户可以使用自己的生日作为开户日期

  • Income 账户下可以按来源分类,如 Income:SomeCompany:Salary, Income:AnotherCompany:Salary 等,然后以公司入职时间作为开户日期

  • Assets 和 Liabilities 账户中的借记卡和信用卡,可以以在银行的开户日期作为 Beancount 中的开户日期,如果记不得具体日子,写成那个月的 1 号也行

不要惧怕开账户。即使是一些短时间用的小账户(比如只用两个月的储值卡),也可以开账户,因为账户是可以关闭的。关闭后的账户不会出现在关闭后的报表里,不会触发你的强迫症……

多货币账户

在 Beancount 中,一个账户中可以有多种 commodity(通货),比如现在小红的 Expenses:Clothing:Pants 账户就存放了 200.00 CNY 和 100.00 USD。她在出差前想必 Assets:Cash 里也同时存在着 CNY 和 USD 两种 commodity。

如果有多货币的使用,建议将自己主要使用的货币定义到账本中,在账本中添加 option "operating_currency" "CNY" 这一行即可将 CNY 定义为主要货币,在 bean-web 和 fava 中会单独列出来,而其他的 USD、CAD、JPY 等则会列到 Other 里。主要货币可以定义多个。

另外,在账户开户的时候,可以在账户名后面跟上这个账户里允许出现的货币的名字。如人民币-美元的双币信用卡,消费 JPY、CAD 等其他货币的时候,也是以 USD 入账的,为了防止自己在记录一些外币交易时忘记转换货币或是搞错账户,可以在开户时写成 2012-01-01 open Liabilities:CMB:CreditCards CNY,USD 这样,限定这个账户里只能出现 CNY 和 USD 两种货币,如果不慎记入了其他货币,Beancount 会报错。单币信用卡同理,如果你的信用卡不管刷什么外币都是以 CNY 记账,可以在开设账户的时候加上 CNY 这个限制,防止出错(不小心把外币消费没加 @@ 直接记进来)。

账本文件的分割

随着时间的积累,账本文件会越来越大,编辑起来不太方便。Beancount 有 include 语句,可以在一个账本文件里包含另一个账本文件。我的主账本文件里只有一些 option 条目,其他都是 include,各种打开/关闭账户的的条目放单独的文件里,然后每个月的账本是一个单独的文件,也 include 进来。

Beancount 会把所有交易都读到内存里后按日期重新排序,所以每条交易在文件里出现的顺序并不重要。

自定义项目

只有顶级账户类型和一级描述开头必须是大写英文字母,再往下的层级并没有做限制。

项目结构的一些调整:

  • 将账户声明语句按照账户类型组织,单独存放在 account/{类型}.beancount 中;
  • 将交易按年份进行归档,存放在 billflow/{年份}/*.beancount 中;
  • main.beancount 中用 include 语句导入 account/*.beancountbillflow/{年份}/*.beancount

项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.
├── account                            // 账目类型定义文件
│   ├── assets.beancount
│   ├── equity.beancount
│   ├── expence.beancount
│   ├── income.beancount
│   ├── liability.beancount
│   └── balance.beancount
├── billflow                           // 按年份记录的交易数据
│   ├── 2015                           // 年度按月份记录的交易数据
│   │   ├── 2015.beancount
│   │   ├── 201501.beancount
│   │   ├── 201502.beancount
│   │   ├── ...
│   │   ├── 201511.beancount
│   │   ├── 201512.beancount
│   │   ├── 2015shopping.beancount     // 年度特殊类型的交易数据
│   │   ├── 2015income.beancount
│   │   ├── 2015transfer.beancount
│   │   └── ...
│   │
│   ├── 2016
│   │   ├── *.beancount
│   │   └── ...
│   │
│   ├── ...
│   │   ├── *.beancount
│   │   └── ...
│   │
│   ├── 2022
│   │   ├── *.beancount
│   │   └── ...
│   │
│   ├── 2023
│   │   ├── *.beancount
│   │   └── ...
│   │
│   └── other
│       ├── *.beancount
│       └── ...
├── main.beancount            // 主文件,包含所有的交易数据

定期为所有资产账户添加断言,如果断言的数值与 beancount 里计算的数值不一致的话,beancount 就会抛出错误。

大额转账的处理

对于一个大额的转账类收入或支出,如果直接归入收入或支出,会导致统计图的比例被挤压。

我的方案是:创建一个 Equity:Exchange 账户负责转账记录,然后在 事件 中创建 转账 条目用以记录。如下:

1
2
3
4
5
2020-05-12 * "爸爸转账5万元"
Assets:BankCard 50000.00 CNY
Equity:Exchange -50000.00 CNY

2020-05-12 event "转账" "爸爸转账5万元"

比如资产中的银行存款有大量的钱,我全额买车了,如果将车作为自己的资产,这样的账应该怎么考虑呢?

把车作为资产的话,可以使用一个独立的 commodity(通货),比如:

1
2
3
2019-01-01 * "DMC DeLorean"
Assets:Bank
Equity:Cars      1 DELOREAN {30000.00 USD}

除了放在 Equity 里,你也可以选择放在 Assets 里。

金额的浮点误差

使用一段时间发现 beancount 在金额计算上因为浮点数非精确计算的性质,会出现 0.01 的误差(建议所有账户金额保留两位小数,可以消除浮点误差),虽然对总账目影响很小,但还是必死强迫症。

我的方案是:创建一个 Equity:Balance-Error 账户,定期对误差进行消除。

1
2
3
2020-07-10 * "平衡账户浮点误差"
Equity:Balance-Error -0.01 CNY
Assets:BankCard 0.01 CNY

导入银行账单

不同人的记账习惯不同,有的人喜欢消费完一笔立刻就记账,有的人喜欢定期(每天、每周、每月)把之前的收支汇总到账本上。在我看来,所有「有据可查」的交易,如走银行卡的交易,是可以定期汇总的,但是那些无账单无票据的交易,如现金交易,要么就是干脆不记,要么就应该想办法立刻记下来,否则当你定期回忆的时候一定会因为各种原因出错,从而打击记账的信心。

Beancount 有一整套的导入工作流:bean-identify, bean-extract 和 bean-file 分别可用来识别、解析和存档银行账单文件。在 beancount.ingest.importers 里已经有一些现成的导入器了,包括一个可配置的 CSV 导入器。

导入银行账单时,需要注意的一个地方是去重。如果两个银行账户间有转账操作的话,会出现重复的账目,比如用借记卡对信用卡进行还款,在导入的借记卡账单和信用卡账单中都会有体现,然而这两笔交易其实是同一笔,这时候就需要去重。

我现在每月账本由三部分构成:

  • 信用卡账单(从银行账单导入后配合 Vim 插件半自动填写 Expenses 账户)
  • 借记卡账单(从银行账单导入后配合 Vim 插件半自动填写 Income 账户)
  • 现金交易记录(平时用手机记录,月末手工录入)

我的每次导入只需要去重两次,一次是每月借记卡自动还款信用卡,一次是每月 ATM 取款。如果你的账单构成比较复杂,是时候考虑优化一下了,比如第三方支付服务里不留余额,省得还要导入它们的账单并去重……

2019-05-09 更新:若是不想手动去重,可以考虑建立一个 Equity:Transfers 账户。比如「借记卡还信用卡」这一交易,借记卡账单里那一笔计为从「从借记卡转入 Equity:Transfers」,而信用卡账单里那一笔记为「从 Equity:Transfers 转入信用卡」。这样,只要 Equity:Transfers 的余额为零,你的账便是没有问题的。

定期断言

一本维护良好的账本应当定期做断言(assertion),标记在某个日期某个账户(通常是 Assets 或 Liabilities 账户)里有多少豆子。断言的例子如下:

1
2
3
2016-02-01 balance Assets:Cash 500.00 CNY
2016-02-01 balance Assets:Cash 100.00 USD
2016-02-01 balance Assets:CMB:C1234 1000.00 CNY

断言语句告诉 Beancount,这个账户在这个日期凌晨 00:00:00 时间点(也就是前一天深夜 23:59:59 的下一秒),余额为这个数字。小红账本里以上断言告诉 Beancount,截止一月底,小红钱有 500 人民币、100 美元的现金,同时招行尾号 1234 的借记卡里有 1000 人民币的存款。

Beancount 的时间精度是「日」,所以这里必须强调,诸如 open, close, balance 等带日期的语句,均发生在当日的第一笔交易之前,你可以想像它们都是在凌晨 00:00:00 时间点发生的,而普通的交易都是发生在白天。因此,要断言一月份的余额,日期应写作 02-01 而不是 01-31。同样地,信用卡等通常为负数的账户也能进行断言,比如小红的信用卡账单日为 20 日,2 月份账单应还款 5000 元,那她的断言应该这样写(注意日期是第二天,也就是 21 日):

1
2016-02-21 balance Liabilities:CMB:CreditCards -5000.00 CNY

添加了断言之后,Beancount 便会检查那个账户的数字是否与断言的数字相等,如果不相等就会报错。人总是会犯错的,当你因为各种原因在账目上出现了错误,断言能帮助你缩小查错范围——你只需要检查最后一次成功的断言之后的发生的交易即可。

合理填充

Beancount 另一个有趣的功能是填充(padding),填充是配合断言一起用的,当 Beancount 解析到填充语句时,会自动在这条语句和下一条断言语句之间插入一条填充交易,使得断言成功。在填充语句所在日期和断言语句所在日期之间不能再有其他交易。 例子如下:

1
2
2015-11-30 pad Assets:Cash Expenses:Food:Drinks
2015-12-01 balance Assets:Cash 200.00 CNY

小红 11 月底做账目核对的时候,发现钱包里的现金是 200 元,但是根据 10 月底的余额,以及 11 月的交易记录,钱包里应该剩 200 多元才对,她想了下,可能是有几次在路边买了饮料喝忘记记录了,因此她使用填充功能来解决这个问题,在 11 月最后一笔交易和 12-01 的断言之间插入一条 pad 语句,这样 Beancount 便会自动插入一条交易,使 Assets:Cash 里的余额调整为 200.00 CNY,而因此产生的货币变化,则记录到 Expenses:Food:Drinks 账户里。在本例中,自动插入的交易内容即是从 Assets 账户倒出了一些货币到 Expenses 账户里。

Beancount 作者便是这样来使用的填充功能的。他的现金账户几乎只用来购买烟酒和饮料,但是他又懒得记录现金支出,于是他就在月底的时候将现金账户 pad 到 Expenses:Food 一次,然后用断言语句记下月底现金账户的实际余额,中间的差值会由 Beancount 自动算出来并插入。

填充功能另一个用途是开户时设定初始余额。比如小红的借记卡是 2010 年开户的,她从 2015-06-01 开始用 Beancount,她就可以这么写:

1
2
3
2010-01-01 open Assets:CMB:C1234
2015-05-31 pad Assets:CMB:C1234 Equity:Opening-Balances
2015-06-01 balance Assets:CMB:C1234 1000.00 CNY

这样 Beancount 会自动插入一条交易,把 Assets:CMB:C1234 在 2015-06-01 (凌晨)的余额的调整为 1000.00 CNY,因此产生的货币变化(新开的账户余额默认是 0),记录到 Equity:Opening-Balances 账户里。在本例中,自动插入的交易内容即是从 Equity 账户倒出了一些货币到 Assets 账户里。

填充功能比手工写一笔交易有什么好呢?你不需要去计算两个数字之间的差额了——Beancount 会自动算出差额并帮你插入交易。此外,这个差额是动态计算的,在上面两个例子中,如果小红想起了在哪天买了什么饮料,重新记上去,那么这个差额会自动变小;如果小红后来又导入了 2010-01-01 到 2015-05-31 之间的账单,那这个开户余额也会自动根据实际情况调整大小。

Beancount 进阶(一)

以下举几个例子,展现一下 Beancount 和复式簿记能处理多么复杂的交易。这些复杂的交易用单式簿记来记录是困难而极易出错的,但是在复式簿记里却是自然而流畅的。之前说过,复式簿记的「复」是指一笔交易会涉及到复数个账户。小明的例子都是两个账户间「一对一」交易,如一个 Income 账户一个 Assets 账户,或一个 Assets 账户一个 Expenses 账户等。但实际上,生活中会遇到各种「一对多」或「多对一」或「多对多」的交易:购买大件物品时因银行支付限额而使用多张银行卡合并付款;朋友出去唱歌、聚餐每人付的钱不同,事后 AA 平摊等。以下几个例子是有意构造的涉及到两个以上账户的交易,让我们一起来看看小红的账本。

第一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2016-01-31 * "工资 2016-01"
  Income:SomeCompany:Salary                -20000.00 CNY ; 应发工资
  Income:SomeCompany:Reimbursement          -1000.00 CNY ; 餐补
  Income:SomeCompany:Reimbursement           +100.00 CNY ; 餐补扣除
  Expenses:Government:Pension               +1500.00 CNY ; 养老保险
  Expenses:Government:Unemployment           +100.00 CNY ; 失业保险
  Expenses:Government:MedicalCare            +500.00 CNY ; 医疗保险
  Expenses:Government:HousingFund           +3000.00 CNY ; 住房公积金
  Expenses:Government:IncomeTax             +3000.00 CNY ; 个人所得税
  Assets:CMB:C1234                         +12800.00 CNY ; 实发工资

这个例子展现了如何在 Beancount 里体现工资条上的内容。每个月的工资条上总会有各种各样的名目。小红在使用了 Beancount 之后,可以方便地把工资、餐补、三险一金、个税等信息都记录进去,以后能很方便地统计每个月有多少工资是喂狗的。

本例子中有 Income: 账户有三条记录,Expenses: 账户有五条记录,Assets: 账户有一条记录,共八条记录,总和为 0。

第二个例子

1
2
3
4
5
2016-02-01 * "XX购物中心" "购物"
  Liabilities:CMB:CreditCards               -1000.00 CNY ; 信用卡刷卡
  Expenses:Clothing:Pants                    +200.00 CNY ; 长裤一条
  Expenses:Clothing:Shirts                   +200.00 CNY ; 衬衫一条
  Assets:Receivables:Xiao-Mei                +600.00 CNY ; 帮室友小美付钱

小红拿到工资第二天就和小美去购物中心逛街,买了一件衣服一条裤子,花了 400 元,小美没带卡,身上现金不够,于是让小红帮她付钱,以后再还她,于是小红把 1000 元的东西一起刷了信用卡。

本例子中小红的 Liabilities 桶里倒出了 1000 元,往两个 Expenses 桶里各倒了 200 元进去,又往 Assets 桶里倒了 600 元。帮小美付了钱,算是小美欠小红的钱,所以算作资产。所有数字加起来和为 0。

第三个例子

1
2
3
4
5
2016-02-05 * "XX黑心饭店" "和小美吃饭"
  Assets:Cash:Wallet                         -300.00 CNY ; 钱包现金
  Assets:Receivables:Xiao-Mei                -200.00 CNY ; 小美帮我付的现金
  Expenses:Food:DiningOut                    +250.00 CNY ; AA 我的一半
  Assets:Receivables:Xiao-Mei                +250.00 CNY ; AA 她的一半

过了几天小红和小美去一家饭店吃饭。本以为人均消费 100 元左右就可以搞定,没想到了这是家黑心饭店,老板说两人共消费了 500 元,还只能付现金,不能刷卡。小红和小美掏空了钱包,总算凑齐了 500 元现金,其中小红付了 300 元,小美付了 200 元。这顿饭两人还是打算 AA 平分掉。

本例中,倒豆子的桶有两个,分别是代表「小红的钱包」的桶,和代表「小美欠小红的钱」的桶,豆子倒去哪儿了?一半进了「小红的消费」桶,另一半回到了「小美欠小红的钱」桶。整个交易中,数字的总和依然为 0。

第四个例子

1
2
3
4
5
2016-02-10 * "在免税店买东西"
 Assets:Cash                                 -200.00 USD
 Liabilities:CMB:CreditCards                 -650.00 CNY @@ 100.00 USD
 Expenses:Clothing:Pants                     +150.00 USD
 Expenses:Clothing:Shoes                     +150.00 USD

小红去国外出差了,回国前为了把兑换的美元现金花掉,忍不住又在免税店大肆购物,结果现金不够,于是 300 美元的商品用现金支付了 200 美元,用信用卡支付了 100 美元。小红的信用卡开通了外币消费人民币入账功能,刷美元也出人民币账单。

本例中,涉及到了合并付款和货币转换。小红的信用卡被扣掉了 650 人民币,这其实是由 100 美元转换而来。在 Beancount 中使用 @@ 即可连接两种互相转换的 commodity。在本次交易中,负数共 -200.00 USD + (-100.00 USD) = -300.00 USD,正数共 +150.00 USD + (+150.00 USD) = +300.00 USD,正负相加依然得到的是 0。

Beancount 进阶(二)

借贷记账法

账户类别

复式记账的最基本的特点就是以账户为核心,Beancount的系统整体上就是围绕账户来实现的。之前提到的会计恒等式中有资产、负债和权益三大部分,现在我们再增加两个类别,分别是收入和支出。Beancount系统中预定义了五个分类:

  • Assets 资产
  • Liabilities 负债
  • Equity 权益(净资产)
  • Expenses 支出
  • Income 收入

这五类是 Beancount 的约定,除此了 Equity 之下一些特殊的账户外,没有任何预先定义的账户。用户可以定义各种各样的账户,Beancount 对账户的组织是树形的,譬如我分别有这些资产账户:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Assets:Cash:JPY
Assets:Cash:USD
Assets:Bank:CH:UBS
Assets:Bank:CN:BoC
Assets:Bank:US:Chase:Checking
Assets:Bank:US:Chase:Saving
Assets:Bank:JP:SMBC:JPY
Assets:Bank:JP:SMBC:USD
Assets:Broker:US:IB
Assets:Points:Airline:JAL
Assets:Points:Airline:United

Beancount 对这些账户的组织形式如下图:

接下来是声明账户的语法。Beancount 要求每个使用的账户必须声明开户时间,格式是YYYY-mm-dd。之后是关键词open,表示在这个日期开户(或者开始记账)。接下来是账户名称,格式用:隔开的树形语法,最后是(可以省略的)账户的货币种类。

**货币种类不需要事先定义,也没有系统内部的定义,一般来说我们使用三字母的货币代码,但其实可以用任何的名字(惟一的限制是大写字母和下划线)。**我在这个例子中使用了USDJPYCNYCHF四个货币,以及我自定义的P_JALP_UA表示不同航空公司的里程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
2019-01-01 open Assets:Cash:JPY JPY
2019-01-01 open Assets:Cash:USD USD
2019-01-01 open Assets:Bank:CH:UBS CHF
2019-01-01 open Assets:Bank:CN:BoC CNY
2019-01-01 open Assets:Bank:US:Chase:Checking USD
2019-01-01 open Assets:Bank:US:Chase:Saving USD
2019-01-01 open Assets:Bank:JP:SMBC:JPY JPY
2019-01-01 open Assets:Bank:JP:SMBC:USD USD
2019-01-01 open Assets:Broker:US:IB USD, JPY, CHF
2019-01-01 open Assets:Points:Airline:JAL P_JAL
2019-01-01 open Assets:Points:Airline:United P_UA

账户如何组织分类完全看个人需求和喜好,譬如我先分账户类型,再分国家,然后是金融机构名,最后是具体账户。分类的作用是可视化的时候,可以看某个非字节点下面所有账户的汇总。

接下来是负债类别的账户,最常见的就是信用卡,组织方式和使用方法和资产账户没有什么区别,譬如:

1
2
2019-01-01 open Liabilities:CreditCard:US:Discover USD
2019-01-01 open Liabilities:CreditCard:JP:Rakuten JPY

在开始记账之前,还差最后一步,就是收入和支出类别的定义。Beancount把收入支出的类别也想象成了一个账户,在语法上和资产、负债类账户没有区别。譬如下面例子:

1
2
3
4
5
6
2019-01-01 open Expenses:Clothing
2019-01-01 open Expenses:Food:Dinner
2019-01-01 open Expenses:Transport:Airline
2019-01-01 open Expenses:Transport:Railway
2019-01-01 open Income:Salary
2019-01-01 open Income:Rebate

基本借贷记账

有了以上定义的账户以后,我们终于可以开始实践记账了。复式记账又叫作「借贷记账」。之所以这么叫,是因为每一条记录都至少有一条借记(Debit)和一条贷记(Credit)。可以看下面这个例子:

1
2
3
2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover -1000 USD

这个例子表示我在日本航空购买了纽约-东京的机票,消费 1000 美元(贷记),付款的信用卡 Discover 扣款 1000 美元(借记)。

Beancount 基本的语法如下所示:

1
2
3
4
YYYY-mm-dd * ["payee"] "description"
  posting 1
  posting 2
  ...

第一行要有日期,接下来是*。收款者payee是可选的,如果*后面只有一个字符串,那就是省略了payee。从第二行开始,每一行开头空两个缩进,然后是账户名以及金额、货币。这里有一个要点:要保证所有条目的总和是 0,否则就会出现Transaction does not balance: (xxx USD)这样的错误。这个要求很好理解,因为花出去的钱必须和账户上减少的钱一样,否则就是所谓的「账目不平」了。

Beancount 语法的灵活性在于每个记账单元可以有任意多个条目(借记和贷记),只要保证它们的总和是0就可以。于是我们还可以这样记录:

1
2
3
4
5
2019-01-01 * "Walmart" "在超市买两件衣服和晚餐"
  Expenses:Clothing 20 USD
  Expenses:Clothing 10 USD
  Expenses:Food:Dinner 10 USD
  Liabilities:CreditCard:US:Discover -40 USD

更加复杂的例子可能是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
2016-01-01 * "Google" "工资"
  Assets:Bank:US:Chase:Checking 500 USD
  Assets:Bank:US:Chase:Saving 1839.35 USD
  Assets:Pension:US:401k:PreTax 419.23 USD
  Assets:Pension:US:401k:PreTax 209.62 USD
  Expenses:Health:Insurance:Dental 3.14 USD
  Expenses:Finance:Insurance:TermLife 7.67 USD
  Expenses:Health:Insurance:Vision 0.98 USD
  Expenses:Tax:US:Federal 763.26 USD
  Expenses:Tax:US:Medicare 60.84 USD
  Expenses:Tax:US:SocialSecurity 260.15 USD
  Expenses:Tax:US:State:NY 212.59 USD
  Expenses:Tax:US:City:NYC 131.57 USD
  Expenses:Tax:US:State:NYDisability 1.2 USD
  Income:Salary:Regular -4192.31 USD
  Income:Allowance:TermLife -7.67 USD
  Income:Salary:401kMatch -209.62 USD

实际的记账中,一进一出的两个账户占了绝大多数。这个时候把正负的金额写两遍未免有点罗嗦了,所以 Beancount 还提供了金额插值的功能。简单说就是假设总和一定是0,在有N个账户的时候,只要求N-1个账户声明金额。于是最初的例子还可以写成:

1
2
3
2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover

在我的实践中,我只在正好是两个账户的时候才使用这个功能,因为可以避免重复的数字。但有多个账户的时候,把每个金额都写出来有助于避免错误,和「防御性编程」的理念一样。

在上面的例子中,我们还可以看出来,所有Expenses类别的账户都是正数,所有Income类别的账户都是负数。这是 Beancount及类似工具使用负数来表示借记(Debit),正数表示贷记(Credit)的结果。简单来说,借记就是把账户上的资金移除,贷记就是增加账户的资金。同理,通常Assets类是正数,Liabilities类是负数。本质上每个账户的数值只有绝对值有意义,正负号并没有实际含义。

货币转换

如果在去国外旅游,免不了要进行货币转换。事实上 Beancount 本身没有定义任何货币,这也意味着你可以定义任何货币(或商品)。所以哪怕不出国,只要是记录了非主要货币类资产,譬如投资品,代金券,航空公司里程,那么就需要货币转换了。

在使用多个货币之前,需要先定义「工作货币」。工作货币可以不止一个,例如:

1
2
option "operating_currency" "JPY"
option "operating_currency" "USD"

定义了工作货币以后,在 fava 界面中可以看到工作货币单独列出的栏目。

Beancount货币转换的语法有两种,一种是使用@记录单位货币的转换价格,例如:

1
2
3
2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD @ 110 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

另一种方式我更常用,使用@@记录转换后的总额:

1
2
3
2019-01-01 * "日本航空" "纽约-东京"
  Expenses:Transport:Airline 1000 USD @@ 110000 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

货币转换不一定只在一个账户上。下面的这个例子是以2.5日圆每点的价格,买了10000日本航空里程,但是付款的信用卡是以美元计价的,所以两遍都可以转换为25000日圆来平衡。

1
2
3
2019-01-01 * "日本航空" "购买里程"
  Assets:Points:Airline:JAL 10000 P_JAL @ 2.5 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

借贷管理

复式记账的强大之处是每个账户都有状态,而且每个操作都是原子的,这对复杂的资金进出记录非常有帮助。

生活中一个常见的例子是朋友之间的借钱和相互垫付,就拿我最近遇到一个例子来说吧,我和 X、Y 三人一起出游,从东京附近的横须贺坐船到猿岛,费用是每人 1300 日圆的船票和200日圆的登岛费,其中船票可以用信用卡支付,而登岛费只能付现金。我们一共需要付 4500 日圆,但是正好谁都没有这么多现金,于是决定我用信用卡付三人的船票,X 用现金付三人的登岛费,最后再结算。

1
2
3
4
5
6
7
8
9
2019-05-25 * "猿岛" "渡轮"
  Expenses:Transport:Ferry 1300 JPY ; 个人渡轮费用
  Assets:Receivables:X 1300 JPY ; 对X应收账款
  Assets:Receivables:Y 1300 JPY ; 对Y应收账款
  Liabilities:CreditCard:JP:Rakuten -3900 JPY

2019-05-25 * "猿岛" "登岛费"
  Expenses:Transport:Attraction 200 JPY ; 登岛费
  Liabilities:Payable:X -200 JPY ; 欠X的钱

第二天,三人结算完毕,X付给我现金,Y转账给我。

1
2
3
4
5
6
2019-05-26 * "猿岛" "费用结算"
  Assets:Receivables:X -1300 JPY ; X偿还债务
  Liabilities:Payable:X 200 JPY  ; 偿还对X的债务
  Assets:Cash:JPY 1100 JPY ; X实际付给我的钱到账
  Assets:Receivables:Y -1300 JPY ; Y偿还债务
  Assets:Bank:JP:SMBC:JPY 1300 JPY ; Y实际付给我的钱到账

最终可以看出来,我在Expenses:Transport:Ferry类别消费1300 JPY,在Expenses:Transport:Attraction类别消费200 JPY,信用卡扣款3900 JPY,收到了1100 JPY的现金,Assets:Bank:JP:SMBC:JPY收到了1300 JPY的转账。

以上这种记账方法可以让资金的流动一目了然,类别和金额也准确无误。记录对他人的债权(应收账款)和欠他人的债务(应付账款),我分别使用了Assets:ReceivablesLiabilities:Payable下面的账户。

**应收账款和应付账款的另一个用途是区分付款和到货时间。**一般来说交易是当场进行的,一个账户的借记和另一个账户的贷记同时发生,但是有些时候付款和到货并不是同时发生的,如果需要精确区分发生的时间的话,可以用这种方法把他们分成两笔记录。

下面这个例子是,用信用卡买日本航空里程,但是不知道为什么过了1个月才到账:

1
2
3
4
5
6
7
2019-01-01 * "日本航空" "购买里程"
  Assets:Receivables:JAL 10000 P_JAL @@ 25000 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

2019-02-01 * "日本航空" "里程到账"
  Assets:Points:Airline:JAL 10000 P_JAL
  Assets:Receivables:JAL

对账

Beancount的语法检查保证了每一笔交易的借记和贷记是平衡的,这已经可以避免许多会导致「账不平」的错误,但是对于数额本身的错误,或者某调账目漏记并没有办法。这就是为什么我们要定期对账。

资产负债表

所谓对账,就是看每个账户的结余(Balance)是否正确。每个账户的余额可以在资产负债表(Balance Sheet)页面中找到,可以查看样例资产负债表

资产负债表

fava 资产负债表把资产列在左边,负债和权益列在右边。权益即净资产,是根据资产和负债计算出来的(除了Equity:Opening-Balances,之后会讲到)。

需要注意的是 Beancount 的债务和权益是负数,所以并不是资产 = 负债 + 权益,而是资产 + 负债 + 权益 = 0。我之前提过一次,这是 Beancount 使用正数来表示借记(Debit),负数表示贷记(Credit)的结果。在传统的复式记账中,数字的正负号并没有这样的意义,无论是借记还是贷记都是正数,所以绝对值资产 = abs(负债 + 权益)也许更好理解。

在资产负债表上点击任意账户,可以进入账户的明细界面。账户的明细界面列出了涉及该账户的每一笔交易,点开后可以看到具体的交易信息。每一行的最右侧是这一笔交易后的该账户结余,这个数字就是对账的关键。

账户明细

结余断言

假设已知账户的某日结余金额,只要在这个账户明细界面看一看对应的日期的最后一笔交易后结余是否正确就可以了。如果Beancount计算出的结余和已知的是一样的,那么基本上就可以确定账没有问题。

这个步骤看似容易,但是随着账目增多,对账的负担会很重,而且容易看错。更严重的问题是,如果因为某种原因要修改过去的账目,已经对好的账就不一定正确了。惟一保险的办法是每次修改了过去日期的交易后,把涉及到的账户未来的结余再全部重新对一遍。好在这个过程是可以自动化的,方法就是使用结余断言(Balance Assertion)。

结余断言就是在记账中加入已知事实,即某个日期开始的时候的某个账户结余。如果你对单元测试有了解,这个方法肯定不会陌生。结余断言的语法非常简单,如下例所示:

1
2017-08-20 balance Assets:US:BofA:Checking 2298.50 USD

惟一需要留意的地方是,结余断言是所声明日期开始的时候的余额,即当日的交易不算在内。

声明结余断言之后,Beancount会自动检查断言是否正确,如果不正确就会有错误出现。如果每个账户都有适当的结余断言,修改过去的交易就可以放心进行了。

文档链接

有了结余断言,接下来就是这个断言的事实从哪里来。最简单的方法当然是看看现在有多少结余,然后直接写上今天的日期。这种方法适合现金和其他一些不方便查询交易记录的账户。

除此之外,推荐使用银行月结单(Statement)上面的数字。许多国家的许多银行、信用卡都会定期发送交易明细,一般来说是每月。Beancount提供了管理这些文件的一个语法,例如:

1
2013-03-20 document Assets:US:BofA:Checking "path/to/statement.pdf"

在 fava 中这条记录也会被显示出来,并且提供可以点击的链接。这个路径是相对于这条记录所在的Beancount文件的目录,这对于多文件记账很重要(include语法,之后会讲到)。

根据个人经验还有一个重要的提醒,就是要看清楚月结单包含哪些交易,尤其是账单周期末的哪些。因为很多银行、信用卡的交易并不是即时结算的,特别是有跨国交易的时候。如果发现某些交易还没出账或者账单周期开始包含了上个周期的交易,一定要注意余额断言的数额。

结余调整

接下来说一说账对不上的情况。事实上,这是一种常态,人毕竟不是完美的,错记漏记实在是太正常了。一旦结余断言失败,当然是先看自己有没有记错什么,如果实在困难,或者金额差距不大自己没那么在乎,可以考虑用结余调整。

结余调整并不是非要用什么特别的语法不可,实际上只是一种规范。这个规范是使用Equity:Opening-Balances来表示初始结余。下面是一个例子:

1
2
3
2015-01-01 * "账户初始"
  Assets:US:BofA:Checking 3490.52 USD
  Equity:Opening-Balances -3490.52 USD

这个例子的意思是,在2015年1月1日给Assets:US:BofA:Checking增加3490.52美元结余。假设这个账户之前没有记录的话,那么它现在的结余就是3490.52美元,如果有就是在原来的基础上加上3490.52美元。这3490.52美元从哪里来呢?Beancount的规范是使用Equity:Opening-Balances

Equity:Opening-Balances是权益类别下面的账户,它是净资产的一部分。Beancount中权益是负数,所以数字减少代表了净资产的增加。这一部分资产的来源可以理解为表外资产,即来源不明确,在Beancount账本中没有更详细的记录。

Beancount还提供了一个更加简易的语法pad,结合了结余断言,功能是把结余调整到使得下个余额断言满足。用法如下所示:

1
2
2015-01-01 pad Assets:US:BofA:Checking Equity:Opening-Balances
2015-01-02 balance Assets:US:BofA:Checking 3490.52 USD

以上的效果是,无论Assets:US:BofA:Checking之前余额多少,在2015年1月2日开始之前都调整到3490.52 USD,差额从Equity:Opening-Balances来。

一般来说除非是调试错误或者导入数据过程中,否则我不建议使用pad这个语法,因为它会让结余断言丧失一定的准确性。使用pad的风险是,如果自动调整的数额过大,当修改了过去其他的账目导致需要调整的数额发生改变的时候,Beancount并不会出现任何警告和错误。我更倾向于把需要调整的金额明确写出来,这样一旦变化就会有错误提醒,避免更大的错误。

资产购入与折旧

生活中有一些交易我们需要考虑到底要记为花费,还是资产的购入。在光谱的两端一般没什么争议,譬如吃饭肯定是消费,买房无疑是购入资产。中间许多类别就不一定了,这取决于个人的偏好和目的。

拿买汽车作为例子,无论是新车还是旧车,许多人在开一段时间以后会选择卖掉。如果我们把买车记为消费,卖车记为收入,这本身并没有任何错误。问题是,对很多人来说汽车还是一笔不可忽略的资产,如果直接记为消费,没有对应的资产入账,那就意味着净资产突然大幅减值。几年后卖出,净资产又突然增加。

要解决这个问题,我们就要把汽车记为一项资产,下面是例子:

1
2
3
4
5
6
7
8
9
2015-01-01 * "丰田汽车" "买入Corolla"
  Assets:Car:ToyotaCorolla 20000 USD
  Assets:US:BofA:Checking -5000 USD
  Liabilities:Loan:Car -15000 USD

2019-01-01 * "二手车商" "卖出Corolla"
  Assets:Car:ToyotaCorolla -20000 USD
  Assets:US:BofA:Checking 10000 USD
  Expenses:CarUsage 10000 USD

上面这个例子是2015年1月1日贷款买入了价格为 $20000 的汽车,并计入资产Assets:Car:ToyotaCorolla。中间省略还贷款的过程,四年后2019年1月1日,把车卖给了二手商,获得 $10000,剩下的 $10000 就是三年来用车的消费了。

这个方法对买卖交易之间的这段时间内净资产计算仍然不够准确,并没有完全解决净资产跳变。因为汽车的使用是三年来平均花费出去的,而不是最后卖的时候一下子花了 $10000。要解决这个问题就要引入定期进行折旧(Depreciation)计算。一般的会计方法中把因为资产使用或者随着时间自然减值称为折旧,我记为Expenses:Depreciation:CarUsage

接下来需要决定的是折旧的周期,即把车的使用费按照多大的粒度来记录。这个完全因人而异,也因金额的大小而异。对于车我可以选择按年折旧,因为在许多国家(譬如美国),工作原因的资产折旧是可以按年抵税的。如果希望每个月的花销更加细致,那么按月折旧也是一个方案,只是需要多记录几笔而异。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
2015-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2016-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2017-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2018-12-31 * "Corolla折旧"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

下一个问题是,每次折旧减值多少。这个问题就是会计上可以操作的空间了,因为实际的价格只有在出售的时候才知道。一般会计准则是预估一个折旧年限,然后以此为根据来折旧。譬如说,我们预期汽车的寿命是10年,即10年后该车的价值清零,这样我们可以按照每年10%的折旧率每年减记。最终当实际卖出的时候,我们再根据卖出价格做调整,或者差额记为其他类别。如果有特别的事件发生,还可以另外单独折旧。例如发生了车祸,车的估值大幅下降,可以在此时额外减值。

Beancount 并没有自动折旧的功能,每一笔都是要自己写的。如果怕忘了,其实可以把未来日期的折旧也写上。另外还有第三方的插件beancount-interpolate可以尝试使用。

项目管理

版本管理

文本记账的最大优势就是它便于使用版本管理系统,像管理代码一样管理账本。最常见的代码版本管理工具就是git了,所以我推荐使用git管理Beancount的账本文件。使用git的好处是提升账本的可维护性,尤其是能够防止不小心改错、误删这样的动作,这在重构的过程中极其重要。

git如何使用我不再赘述,对于会写代码的人来说属于基本技能。即使不熟悉,网上的资料和教程也是汗牛充栋了,更有Sourcetree这样的可视化工具。

惟一需要注意的是,你需要保管好你的git仓库,尤其是要避免盲目上传到Github之类的网站。账本信息属于非常敏感的个人隐私,因为其中可以透露出的信息非常丰富,甚至超过了日记能包含的。如果要上传git仓库备份,至少要使用支持私人仓库的服务,或者自己搭建git服务器(支持SSH即可)。最好在上传之前对整个仓库加密,譬如使用git-crypt

标签

标签是一个讲交易组织归类的方法,是开支类别之外的另一个维度。每一个交易都能加上一个或多个标签:

1
2
3
2019-06-01 * "奥地利航空" "东京-维也纳" #2019-07-Europe-Trip
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

标签的作用是方便查询,在 fava 和 bean-query 中都可以按照标签来过滤。

为了避免重复,Beancount 还提供了标签堆栈语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pushtag #2019-07-Europe-Trip

2019-06-01 * "奥地利航空" "东京-维也纳"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

2019-06-01 * "奥地利航空" "维也纳-莫斯科"
  Expenses:Transport:Airline 100 USD
  Liabilities:US:CreditCard:Citi

poptag #2019-07-Europe-Trip

除了标签,Beancount 还提供了一个类似的语法^,叫做链接(Link),本质上和标签是一样的作用,但是被建议用作将财务上关联的交易组织在一起的方法。常见的使用场合是有时间跨度的一笔交易,例如汇款和收款,短期的债务,按次记录但是按月征收的某些银行手续费,或者仅仅是两个目的一致的交易。

1
2
3
4
5
6
7
8
2016-05-03 * "Chase" "取现" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking -75 USD
  Assets:Cash:USD 50 USD
  Expenses:Finance:BankFee:Overdraft 25 USD

2016-05-05 * "Chase" "还清欠款" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking 25 USD
  Assets:Bank:US:Chase:Saving

多文件组织

到此为止我一直假设所有的 Beancount 记录都是在单一文件中的,这个文件会随着账目的增多越来越膨胀,直到用编辑器维护不便。使用单一文件就像把一个巨大的程序写到一个源文件中一样,阅读和修改都很困难。所以 Beancount 提供了include文件包含语法,用法和多数编程语言一样。

include后面紧跟着要引入的文件名,路径是相对于当前文件的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
; main.beancount
include "accounts.beancount"
include "categories.beancount"
include "books/books.beancount"

; books/books.beancount
include "2016.beancount"
include "2017.beancount"
include "2018.beancount"
include "2019.beancount"

利用这个简单的语法,一个巨大的账本就可以分成若干个较小的账本组合起来了。

要注意,到目前(Beancount 2.2.1)为止,标签堆栈和文件包含是不能组合使用的,也就是说标签堆栈内的include的文件不会自动加上标签。

账本划分

接下来我终于要讲到我的经验之谈了。尽管有文件包含语法,但每条记录到底怎么划分还是一个见仁见智的话题。我使用了三种分割方法,分别是按日期划分、按类别划分和按账户划分。这三种划分方式都有道理,各有优劣。总而言之,划分的目的是减少错误的可能,降低维护成本,节约注意力资源。下面我详细说来。

按日期划分

按日期划分账本是最直接的分割方法,我们可以按年份或者月份创建文件(譬如2019.beancount),每个文件内只包含这段时间内的记账。一般来说除非是修正错误或者重构,旧的账不会再修改,使用当前的账本来记账,把过去的账分割储存是一种有效节约注意力资源的方法。

除了按照日历时间划分,还可以结合标签,把某些事件提取出来,最常见的是旅行。下面这个例子是我把2019年7月关于欧洲旅行的账目全部放到2019-07-Europe-Trip.beancount中,并且结合标签堆栈,把整个文件中的账目标记上#2019-07-Europe-Trip标签。

1
2
3
4
5
6
7
8
9
; 2019-07-Europe-Trip.beancount
pushtag #2019-07-Europe-Trip

2019-06-01 * "奥地利航空" "东京-维也纳"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

...
poptag #2019-07-Europe-Trip

使用单独的文件来记录某个时间段的某类事件相关的开销的好处是,一旦这个事件结束,这个文件就可以进入封存状态了。此外,哪怕是不使用任何可视化工具(如fava),这个文件的可读性也非常高,甚至可以当作旅行日记了。

按类别划分

按类别划分指的是把统一类的重复记录整理到一起,便于根据时间纵向比较。譬如说每月的工资单、房租、水电费、定期投资、以及其他自动扣费的服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
; Tokyo-Electric-Power.beancount

2018-04-25 * "东京电力" "电费"
  Expenses:Utility 4621 JPY
  Liabilities:JP:PrestiaVisa

2018-05-25 * "东京电力" "电费"
  Expenses:Utility 4956 JPY
  Liabilities:JP:PrestiaVisa

2018-06-25 * "东京电力" "电费"
  Expenses:Utility 6648 JPY
  Liabilities:JP:PrestiaVisa

2018-07-25 * "东京电力" "电费"
  Expenses:Utility 9394 JPY
  Liabilities:JP:PrestiaVisa

以上的例子是每月25日扣费的东京电力账目。这样记录的好处是每个月的开销一目了然,还可以观察不同月份的开销变化。如果可以从扣费的服务导出账单,就可以把单个来源放在一个文件。如果是手动记录,这种组织方式也可以最大程度上减少漏记的可能性。

按账户划分

我使用的第三种分割方法是按账户划分。按账户划分和按类别划分差不多是相互对称的两种划分方法,因为这种方法是以账目相关的账户为依据,分割出了单独的账本。按账户划分的最初目的是方便自动导入脚本,因为许多银行、信用卡都可以提供月对账单,非常适合自动转换为Beancount格式,减少人工。

但是完全按照账户的缺点也显而易见,那就是尤其和「按类别划分」冲突,所以我的实际手段是尽可能不使用这个划分方法。什么时候会使用呢?我的原则是和账户的关联性十分强的消费,包括银行利息、银行手续费、信用卡还款、返现返点、开户奖励、年费、账户间转账。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
; AmexSPG.beancount
2019-02-01 * "American Express" "SPG年费"
  Expenses:Finance:CreditCardFee 33480 JPY
  Liabilities:JP:AmexSPG

2019-02-01 * "American Express" "Amex SPG开卡奖励"
  Assets:Points:US:Marriott 30000 P_MRT
  Income:OpeningBonus

2019-02-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-02.pdf"

2019-03-11 * "American Express" "2019年2月账单还款"
  Assets:Bank:JP:SMBC -3254 JPY
  Liabilities:JP:AmexSPG

2019-03-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-03.pdf"

2019-04-10 * "American Express" "2019年3月账单还款"
  Assets:Bank:JP:SMBC -63935 JPY
  Liabilities:JP:AmexSPG

2019-04-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-04.pdf"

2019-05-10 * "American Express" "2019年4月账单还款"
  Assets:Bank:JP:SMBC -23718 JPY
  Liabilities:JP:AmexSPG

总结

我把我的原则总结成了以下规则,按顺序判断:

  • 如果该收支属于一个特别的事件,那么就将其加入这个事件对应的单独文件,例如#2019-07-Europe-Trip
  • 如果该收支是一个周期项目,但跟某个银行账户无关,则加入按类别划分的文件,例如水电费、房租、工资。
  • 如果该收支跟某个银行账户紧密相关,则加入该账户对应的文化,例如银行手续费、信用卡还款、返现。
  • 不属于以上情况,则加入按日期划分的文件。

以上就是我目前对账本管理的一些经验之谈,这些方法只是为了达成我前面提到的三个目的:减少错误的可能,降低维护成本,节约注意力资源。如果之后我发现了更好的划分方式,我会对账本重构,到时候再来更新这篇文章。

记录证券投资

以美股为例,介绍使用复式簿记软件 Beancount 记录证券投资交易(俗称「炒股」),并计算盈亏的方法。

证券投资作为一种复杂交易,必须要用复式簿记才能精确、清晰地计算其盈亏。

概念

成本与价格

在 Beancount 里,证券与一般货币都是通货(commodity)。它们的不同之处在于,我们关心并记录证券的持有成本(held at cost),只有这样,在卖出证券的时候,我们才能正确地计算出这笔交易的盈亏(PnL, Profit and Loss)。

什么是成本(cost)?当你买入一支股票的时候,每股所花的钱即是成本。比如 2017-07-14 某一时刻,Amazon 的股票(AMZN) 的市场价在 1000 USD 左右浮动,你买入了 10 股 AMZN,总共花了 10000 USD,那你的这些 AMZN 股票,每一股的 cost 便是 1000 USD。

什么是价格(price)?严格意义上讲,在交易完成前,你无法预知你这笔交易最终会被撮合成什么价格,但对于交易量足够大的股票来讲,它总是离上一次交易的成交价差不了太多。因此,我们通常把某支股票的市场价(market price)称为它的价格。更特殊地,在讨论某支股票的历史行情的时候,通常把它的收盘价(closing price)作为它的价格。 比如 2018-08-30 某一时刻,你的 AMZN 股票市场价在 2000 USD 左右浮动(一年上涨 100%?这是真实数据),你卖出了 3 股 AMZN,获得 6000 USD,每股卖出的 price 便是 2000 USD。

在上面的例子中,2017-07-14 你买入 10 AMZN 时,cost 和 price 在那一瞬间是相等的,都是 1000 USD。在那之后,cost 不变,price 有涨有跌;在 2018-08-30 你卖出 3 AMZN 时,cost 依然没变(1000 USD),但是 price 变成了 2000 USD,这中间的差价,便是你的盈亏(PnL)。本例中,你总共获得了 3000 USD 的盈利(profit / capital gains)。

AMZN_YahooFinanceChart

在 Beancount 中,表示 cost 的语法是 {},表示 price 的语法是 @@@。例如,买入 10 股成本为 1000 USD 的 AMZN 记为:

1
2
3
2017-07-14 * "Buy 10 AMZN"
  Assets:Trade:Cash                        -10000.00 USD
  Assets:Trade:Positions                          10 AMZN {1000.00 USD}

以 2000 USD 每股的价格出售 3 股 AMZN 记为:

1
2
3
4
5
; 单价
2018-08-30 * "Sell 3 AMZN"
  Assets:Trade:Positions                          -3 AMZN {1000.00 USD} @ 2000.00 USD
  Assets:Trade:Cash                          6000.00 USD
  Income:Trade:PnL                          -3000.00 USD

1
2
3
4
5
; 总价
2018-08-30 * "Sell 3 AMZN"
  Assets:Trade:Positions                          -3 AMZN {1000.00 USD} @@ 6000.00 USD
  Assets:Trade:Cash                          6000.00 USD
  Income:Trade:PnL                          -3000.00 USD

为什么要带上 {1000.00 USD}?为什么有个 PnL?为什么交易看起来不平(总和不是零)?下文详解。

库存

如果你还记得\桶子之间倒豆子的比喻的话,你会比较容易理解库存(inventory)的概念。把你的股票账户想像成一个桶,买的股票便是放进桶里的豆子。股票桶里的豆子,与其他桶的豆子有一些不同——它们上面贴了标签。你买的那 10 个名为 AMZN 的豆子,每个上面都贴着三个标签「购于 2017-07-14」「成本 1000.00 USD」「备注:无」。在你卖掉 3 个豆子之后,还剩 7 个这样的豆子。然后你在 2019-01-04 又买了 5 个 AMZN 豆子,这 5 个新豆子上的标签是「购于 2019-01-04」「成本 1575.39 USD」「备注:无」。此时,你的股票账户里的一共有 12 个 AMZN 豆子,但是由于它们的标签不同,所以泾渭分明地分成两堆(lot):

1
2
7 AMZN {1000.00 USD, 2017-01-14}
5 AMZN {1575.39 USD, 2019-01-04}

注意:对于 held at cost 的 commodity(比如证券)来说,{} 与前面的字母在任何时候都是不可分离的。 比如 1 AMZN {...} 代表的是「1 颗带标签的 AMZN 豆子」(held at cost 的 AMZN 股票),不能只写 1 AMZN

当你往股票桶里增加这些带标签的豆子时,Beancount 都会比较豆子的种类,以及它的三个标签(成本、日期、备注),只有种类和三个标签完全一致的情况下,这些豆子才会被认为是完全一模一样的豆子,被归在一堆里。一般地,在 Beancount 里,所有的 commodity 其实都有标签的,只是对于一般货币来说,我们不会加 {} 记号,因此它们的标签都是空的,自然符合「种类及三个标签完全一致」的条件,被归成同一堆了。

当你往股票桶里取出这些带标签的豆子时,你需要让 Beancount 知道取哪些。比如上面,一共 12 颗 AMZN 豆子,从中取出 4 颗,那么是哪 4 颗呢?你同样需要通过标签来让 Beancount 知道你要从哪堆豆子里取。写成 -4 AMZN {1000.00 USD} 或是 -4 AMZN {2017-01-14} 都将匹配到 7 颗的那堆,写 -4 AMZN {1575.39 USD} 或是 -4 AMZN {2019-01-04} 则是匹配到 5 颗的那堆。如果你在买入的时候还加了备注的话(如 7 AMZN {1000.00 USD, "foo bar"}),卖出的时候还可以用备注来匹配这一堆。

如果你要取出 8 颗怎么办?两堆中的任何一堆都是不够取的,这时候你可以取两次(7 + 1,或是其他你喜欢的组合),可这样也太麻烦了。而且就算是取 4 颗的情况,你还得先看了一下自己库存里的标签。有没有更自动化的方法?有。对于 held at cost 的证券,Beancount 提供了 5 种簿记方法:

  • STRICT:每次取豆子时,必须要明确匹配到某个 lot,不允许模糊匹配(例外是 {},代表全部取出)。这也是默认的模式。
  • FIFO:即先进先出(First-In, First-Out),每次取豆子时自动从最老的豆子开始取,直到取光。
  • LIFO:即后进先出(Last-In, First-Out),与 FIFO 相反,最新的豆子优先被取出。
  • AVERAGE:每次重新计算 cost basis。该方法暂未实现。
  • NONE:完全不合并,允许不同符号的 lot 同时存在。

比如你可以改用 FIFO 簿记方法:

1
1970-01-01 open Assets:Trade:Positions "FIFO"

在使用 FIFO 簿记方法之后,你只需要写 -4 AMZN {},Beancount 就会自动取出最老的 4 颗豆子。

为什么要搞不同的簿记方法?因为按不同的簿记方法,你的 PnL 计算结果是不同的。还是刚才 12 颗 AMZN 豆子的情况。假设今天 AMZN 的价格是 1300 USD,你希望卖出 4 颗豆子。如果你选择按 FIFO 卖出标签为 {1000.00 USD, 2017-01-14} 的豆子,那么你的 PnL 是盈利 1200.00 USD;如果你选择按 LIFO 卖出标签为 {1575.39 USD, 2019-01-04} 的豆子,那么你的 PnL 是亏损 1101.56 USD。同样一笔交易,按不同的簿记方法,一个是盈利,一个是亏损!

谁会来关心你的 PnL 呢?除了你自己,当然是税务局了。比如 IRS 就是用 FIFO 的方式来计算 PnL 并据此征税的。对于投资者来说,盈利的话要缴税,亏损的话可以抵税;长期持有的股票卖出时所得税率低,短期持有的股票卖出时所得税率高——只有使用规范的 PnL 计算方法,才能算出正确的税务数据。

同质与异质

上面的例子皆是做多(long),那如果是做空(short)呢?Beancount 当然也是支持的。Beancount 在将 lot 放入 inventory 的时候,会判断 lot 与 inventory 是同质(homogeneous)还是异质(heterogeneous)的。在多仓情况下,使用 + 符号是增加仓位(buy to open),使用 - 符号是减少仓位(sell to close);在空仓情况下,使用 - 符号才是增加仓位(sell to open),使用 + 符号则是减少仓位(buy to cover)。只要记住:lot 的符号和已有 inventory 一样,就是增加仓位(无论 long 或 short),否则就是减少仓位。

实践

普通交易

现在再来看最开始买 10 AMZN 又卖 3 AMZN 的两笔交易,就显得清晰明了了。上文的写法是最完整的写法,完整地写出了交易的每一个部件。我们实际记录的时候,并不需要写这么完全,因为 Beancount 会根据已有条件自动补全。那两笔交易完全可以写成这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
1970-01-01 open Assets:Trade:Positions "FIFO"

2017-07-14 * "Buy 10 AMZN"
  Assets:Trade:Cash
  Assets:Trade:Positions                       10 AMZN {1000.00 USD}

2018-08-30 * "Sell 3 AMZN"
  Assets:Trade:Positions                       -3 AMZN {}
  Assets:Trade:Cash                          6000.00 USD
  Income:Trade:PnL

对于买入交易,我们只要记录买入 10 AMZN,每股 1000 USD 就已足够,Beancount 会自动算出这需要 10000 USD 的 Cash;对于卖出交易,我们只要记录卖出 3 AMZN,获得 6000 USD 现金也就足够了——至于卖哪 3 股、成本是多少、卖出时的价格是多少、盈亏是多少,都不用写,因为 Beancount 会根据已有条件自动算出来。最后你在 Fava 里看到的 Beancount 补全之后的交易,就是和本文最开始的完整体是一样的。

在实际交易中,许多劵商收取交易佣金,这时候你还需要增加一条类似 Expenses:Trade:Commissions 的 posting,记录所收取的佣金。Beancount 会自动把佣金从 Cash 里或是 PnL 里扣除。

分红与赠股

对于现金形式的分红,没啥特别的。比如每季度都分红的微软

1
2
3
2018-03-09 * "DIVIDEND: MICROSOFT CORP"
  Income:Trade:Dividend
  Assets:Trade:Cash                             3.36 USD

对于股票形式的分红或赠股(较少,我没遇到过),你需要在劵商的结单上找到加塞到你的股票账户里的新股票的 market price,作为新股票的 cost。这里以 2017-06-05 新浪(SINA)赠送微博(WB)为例:

1
2
3
2017-06-05 * "Spin off: 1 WB for 10 SINA"
  Income:Trade:Dividend
  Assets:Trade:Positions                        2 WB {71.94 USD}

拆股与合股

由于公司结构调整,有些股票会被拆分或合并。对用户来说就是减少了一种股票,增加了另一种股票;两种股票的符号可能是一致的,但是成本通常是不一致的。比如前段时间戴尔搞了一个大新闻,把自己的 Class V Common Stock 以 1.8066 的比例置换成 Class C Common Stock,完成「重新上市」。对投资者来说,就是原本每一股 DVMT 股票,被换成 1.8066 股 DELL 股票(向下取整)。我当时持有 5 股 DVMT,于是换到了 9 股 DELL:

1
2
3
4
2019-01-02 * "CLASS V COMMON STOCK STOCK MERGER @ 1.8066, CLASS C COMMON STOCK SHRS RECEIVED THRU MERGER"
  Assets:Trade:Positions                       -5 DVMT {} @ 80.00 USD
  Assets:Trade:Positions                        9 DELL {48.59 USD}
  Income:Trade:PnL

在这次行动中:

  • 那个 @ 80.00 USD 只是我为了记录 DVMT 退市的时候的收盘价,其实它完全不参与算式平衡;
  • 48.59 USD 是劵商结单上提供的 cost,通常与前一天的收盘价是一致的。

虽然 @ 80.00 USD 不参与算式平衡,但是可以用来验算 PnL。Beancount 自带这么一个插件用来做这个事情:

1
plugin "beancount.plugins.sellgains"

留给读者的测验:已知这次公司行动使我盈利 49.81 USD,求原有 DVMT 股票的(平均)成本。

价格数据库与浮动盈亏

Beancount 支持维护一个价格数据库。Beancount 的核心功能不会用到这些数据,但是有些插件会,比如计算浮动盈亏(Unrealized PnL)。有了价格数据库之后,就可以开启 Beancount 自带的插件,计算 Unrealized PnL:

1
plugin "beancount.plugins.unrealized" "Unrealized"

2019-10-10 更新:在 Fava 中默认不显示 Unrealized PnL 产生的交易,可点击「X」按钮显示出来,也可通过选项来更改默认显示的交易类型。如:

1
1970-01-01 custom "fava-option" "journal-show-transaction" "pending cleared other"

Fava 中看不到 unrealized 插件产生的交易并寄来了示例账本。经调查,我发现原因是 Fava 默认不显示状态为 Pending 和 Cleared 以外的交易。在 Fava 选项中可以使用 journal-show-transaction 改变这一默认行为。我一直都开着 Other 类型

股票实践教程

交易记录

港股交易记录如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2019-12-02 * "港股交易" "30.1元买入600股阅文集团"
    Assets:Broker:FutuHK:Cash              -18060.00 HKD
    Assets:Broker:FutuHK:Positions            600.00 HK_00772 {30.10 HKD}
    Expenses:Broker:FutuHK                     75.89 HKD ;手续费
    Assets:Broker:FutuHK:Cash                 -75.89 HKD ;手续费

2020-01-14 * "港股交易" "加仓阅文集团1000股"
    Assets:Broker:FutuHK:Positions           1000.00 HK_00772 {38.00 HKD}
    Assets:Broker:FutuHK:Cash              -38000.00 HKD
    Expenses:Broker:FutuHK                     96.43 HKD ;手续费
    Assets:Broker:FutuHK:Cash                 -96.43 HKD ;手续费

2020-01-31 price HK_00772 32.55 HKD ;一月底阅文集团收盘价

2020-02-28 * "港股交易" "减仓阅文集团1000股,筹集资金做恒指期货"
    Assets:Broker:FutuHK:Positions          -1000.00 HK_00772 {} @ 36.40 HKD
    Assets:Broker:FutuHK:Cash               36400.00 HKD
    Assets:Broker:FutuHK:Cash                 -95.30 HKD ;手续费
    Expenses:Broker:FutuHK                     95.30 HKD ;手续费
    Income:Broker:FutuHK                                 ;利润(负为盈利,正为亏损)
    
2020-03-09 * "货币兑换" "5000港币兑换美元"
    Assets:Broker:FutuHK:Cash               -5000.00 HKD
    Assets:Broker:FutuUS:Cash                 641.96 USD @@ 5000 HKD

接下来基于案例介绍。

股票账户

和日常账一样,记录股票交易也需要先准备账户。账户定义如下:

1
2
3
;我的港股账户
2019-06-30 open Assets:Broker:FutuHK:Cash HKD ;富途港股账户现金
2019-06-30 open Assets:Broker:FutuHK:Positions "FIFO"  ;富途港股账户持仓

股票账户一般包括现金和持仓,建两个账户。其中现金账户需要指定币种(Assets:Broker:FutuHK:Cash),而持仓账户(Assets:Broker:FutuHK:Positions)有类型之分,我习惯使用 FIFO 类型,另外也可以用 LIFO 类型。类型含义如下:

  • FIFO:「first in, first out」的缩写,表示先进先出,卖出股票时,先卖出「先买入」的。
  • LIFO:「last in, first out」的缩写,表示后进先出,卖出股票时,先卖出「后买入」的。

卖出股票时,beancount 会基于卖出价和当初的买入价(有可能多笔买入,价格不同)计算利润,两种类型本质上是回溯买入价的方式不同,所以涉及多次加减仓时,账户类型会影响每一笔的利润计算,但全部清仓时,累计利润不受影响。

买入记录与 Commodity

再讲个概念,Commodity,翻译为「商品」,即交易过程中的「标的物」。beancount 系统中,货币和股票都是 Commodity

下面是一条买入记录:

1
2
3
4
5
2019-12-02 * "港股交易" "30.1元买入600股阅文集团"
    Assets:Broker:FutuHK:Cash              -18060.00 HKD
    Assets:Broker:FutuHK:Positions            600.00 HK_00772 {30.10 HKD} 
    Expenses:Broker:FutuHK                     75.89 HKD ;手续费
    Assets:Broker:FutuHK:Cash                 -75.89 HKD ;手续费

直观上也能看到 HKDHK_00772 属同一位置,前者是港币,后者是阅文集团股票,都属于 Commodity。

持仓账户记录买入股票数,大括号 {} 中则是 Commodity 的成本价,即买入阅文股票成本为 30.10 港币。

现金账户记录花了多少钱。不难猜到,现金账户的成本价被省略了,如果完整记录,应该如下:

1
Assets:Broker:FutuHK:Cash              -18060.00 HKD {1.00 HKD}

卖出记录与利润计算

下面是案例中的卖出记录。

1
2
3
4
5
6
2020-02-28 * "港股交易" "减仓阅文集团1000股,筹集资金做恒指期货"
    Assets:Broker:FutuHK:Positions           -600.00 HK_00772 {} @ 36.40 HKD
    Assets:Broker:FutuHK:Cash               36400.00 HKD
    Assets:Broker:FutuHK:Cash                 -95.30 HKD ;手续费
    Expenses:Broker:FutuHK                     95.30 HKD ;手续费
    Income:Broker:FutuHK                                 ;利润(负为盈利,正为亏损)

首先,看看持仓账户:

1
Assets:Broker:FutuHK:Positions          -1000.00 HK_00772 {} @ 36.40 HKD

1、@后表示卖出价。 2、{} 中应该给出卖出股票对应的成本价,但因为我们已经约定按 FIFO 规则计算成本,所以可以省略。

现金账户 Assets:Broker:FutuHK:Cash 实际入账多少,记录多少。 利润账户 Income:Broker:FutuHK 直接空着,beancount 会自动计算利润。

看到这里,beancount 记录股票的基本方法讲完了。

不过,为了更好的理解 beancount 的工作原理及 FIFO 规则,我们把 {} 内容补全看看。如下:

1
2
3
4
5
6
7
2020-02-28 * "港股交易" "减仓阅文集团1000股,筹集资金做恒指期货"
    Assets:Broker:FutuHK:Positions           -600.00 HK_00772 {30.10 HKD} @ 36.40 HKD
    Assets:Broker:FutuHK:Positions           -400.00 HK_00772 {38.00 HKD} @ 36.40 HKD
    Assets:Broker:FutuHK:Cash               36400.00 HKD
    Assets:Broker:FutuHK:Cash                 -95.30 HKD ;手续费
    Expenses:Broker:FutuHK                     95.30 HKD ;手续费
    Income:Broker:FutuHK                                 ;利润(负为盈利,正为亏损)

补全后,因为成本不同,减持变为两条记录,这很好的体现了 FIFO 规则。

1、我分两笔共买入 1600 股,先是 30.10 买入的 600 股,后以 38 买入 1000 股。 2、那么卖出 1000 股时,按 FIFO 规则,卖出的 1000 股构成如下: (1)600 股 30.10 的成本(先买入的,先卖出) (2)400 股 38.00 的成本

beancount 自动计算的利润就是:

1
2
; 总收入减去两笔总成本
利润 = 36400 - 600 * 30.10 - 400 * 38.00 = 3140

股票最新价

持仓时股价大幅上涨或下跌,但又没卖掉,如何统计最新市值及浮动盈亏呢。

beancount 提供了 price 关键字记录股票价格,格式如下:

1
2020-01-31 price HK_00772 32.55 HKD ;一月底阅文集团收盘价

fava 查看「资产负债表」时,右上角有统计口径选项,其中两个如下:

  • At Cost:按成本价统计资产,也就是最初买入的成本
  • At Market Value:按市价统计资产,beancount 会使用最新一条 price 指定的价格

相信读者注意到了,beancount 所有记录时间都只精确到天,意味着 beancount 无法记录日内交易

汇率换算

我有时会在美股和港股账户间换汇,平时消费也有美元消费人民币入账的情况,涉及到汇率时,也可以使用 @@进行转化,如下:

1
2
3
2020-03-09 * "货币兑换" "5000港币兑换美元"
    Assets:Broker:FutuHK:Cash               -5000.00 HKD
    Assets:Broker:FutuUS:Cash                 641.96 USD @@ 5000 HKD

beancount 数据结构总结

讲到这,可以对 beancount 数据结构做个总结,便于理解 beancount 的原理。

beancount 一条完整数据格式如下:

1
2
3
4
账户名                           数量      商品      成本价          最新价/总价
account
Assets:Broker:FutuHK:Positions  -1000.00 HK_00772 {30.10 HKD} @ 36.40 HKD
Assets:Broker:FutuHK:Positions  -1000.00 HK_00772 {30.10 HKD} @@ 36400 HKD

(一个 @记录单价,两个 @@记录总价)

fava 界面左侧有个「资产」项目,如下图所示,能看到所有字段。

资产界面

各列数据:

  • account:账户名
  • units:商品及数量
  • cost:成本价
  • price:最新价
  • book_value:成本 * 数量,即投入总成本
  • market_value:最新价 * 数量,即最新总市值
  • acquisition_date:成本对应的日期。一般是买入的日期。

记账时,股票交易会用到 cost 和 price,日常账不涉及。

Comment