2023年终总结的时候,就定了一个目标:学习资产管理,坚持定期的进行review。学习资产管理的目的还是希望能够对自己的资产有个清晰的认识,以及对外来的一个预期和管理。
从推友那里得知了beancount:一款基于纯文本和复式记账原理的开源记账软件,基于数据完全自主可控和提供相对完善的可视化界面,所以选择通过beancount来入门财务管理的基础和软件使用方式,下面记录了一些学习过程,供参考。
财务管理基础
Double-entry bookkeeping
在会计学中有一个最基本的金融交易的标准系统:复式簿记,又称之为复式记账法。
复式簿记的基本原则是每一笔交易都至少会影响两个账户,因此需要在至少两个账户中进行相应的记录,这也是为什么称之为:Double-entry bookkeeping。
Accounting equation
复式记账的基础是Accounting equation(会计恒等式或会计方程式)。怎么理解呢?其实就是用来平衡账本的方式。在任何时候,所有账户的借方总额必须等于所有账户的贷方总额。
Accounting equation是复式记账的基础,更是会计学的核心和基石。
复式簿记的每笔交易的结果至少被记录在一个Debit(借方)的账户和一个Credit(贷方)的账户,且该笔交易的借贷双方总额相等,即”有借必有贷,借贷必相等”。该方法能够确保账目的完整性和准确性,是现代会计体系的基础。
会计恒等式有多种形式,常见的为:
$$
Assets = Liabilities + Equity
$$
- 资产(Assets):公司拥有的所有经济资源,包括现金、应收账款、存货、固定资产等。
- 负债(Liabilities):公司所承担的债务或义务,包括短期借款、应付账款、长期借款等。
- 股东权益(Equity):公司在扣除负债后的剩余资产,通常包括股本、留存收益、资本公积等。
每一笔会计交易都至少会影响会计恒等式中的一个元素,但始终保持平衡。下面是简单的交易记录:
交易编号 | 资产 | 负债 | 股东权益 | 说明 |
---|---|---|---|---|
1 | +6,000 | +6,000 | 发行股份换取现金或其他资产 | |
2 | +10,000 | +10,000 | 通过借款购买资产(从银行贷款或以信用购买) | |
3 | −900 | −900 | 通过出售资产获取现金并偿还负债:资产和负债同时减少 | |
4 | +1,000 | +400 | +600 | 通过支付现金(600)和借款(400)购买资产 |
5 | +700 | +700 | 赚取收入 | |
6 | −200 | −200 | 支付费用(如租金或专业费用)或分红 | |
7 | +100 | −100 | 记录费用,但暂未支付 | |
8 | −500 | −500 | 偿还负债 | |
9 | 0 | 0 | 通过出售资产获得现金:一个资产交换另一个资产,资产和负债没有变化 |
资产负债表(Balance Sheet)
资产负债表(Balance Sheet)反映了某一特定日期(如2023年12月31日)的财务状况,通过它可以直观的评价一个公司的财务健康度(如流动性、偿债能力)。
资产负债表是通过会计恒等式进行制作的,它是会计恒等式的具体体现:
$$
Assets = Liabilities + Equity
$$
所有 Assets
、Liabilities
、Equity
账户的余额都会体现在资产负债表中。每个上市公司的财报都会有资产负债表。如下是一个资产负债表的结构图:

下面是腾讯2024 2季度的财报中关于资产负债表(财报中的财务状况表)的部分截图:

关于资产负债表和财务状况表的概念,他们两个都是会计横等式的体现,只是不同会计准则中的叫法差异,如下:
- 国际财务报告准则(IFRS):采用财务状况表(Statement of Financial Position)作为正式名称,强调反映企业某一时点的财务状况;
- 国企业会计准则(CAS):通常使用资产负债表这一传统名称。
损益表(Income Statement)
损益表反映某一时间段(如2023年1月1日至12月31日)的收入、费用和利润。用于分析公司的盈利能力(如毛利率、净利率)。核心等式如下:
$$
净利润(Net Income)= 收入(Income)−费用(Expenses)
$$
下面是腾讯2024 2季度的财报中关于损益表(财报中的收益表表)的部分截图:

- 资产负债表:静态的“财务快照”,回答“现在有多少钱和债”。
- 损益表:动态的“经营成绩单”,回答“过去赚了多少钱,花了多少钱”。
- Beancount 优势:通过复式记账自动维护两大报表的关联性,确保数据一致性和准确性。
Beancount介绍
Beancount一款开源的记账软件,它是一种基于复式记账原理创造的独特的计算机语言,允许用户通过文本文件定义财务交易记录。其核心功能包括:
- 文本化记账:在纯文本文件中编写结构化交易记录,便于版本控制与长期维护。
- 内存加载:将账务数据读取至内存中进行高效处理。
- 多样化报表:自动生成余额表、损益表、资产负债表等多维度财务报告。
- 可视化交互:内置 Web 界面提供账务浏览、查询与图表分析功能。
我们先看一下Beancount的语法速查表,然后后面仔细看一下语法的详细介绍,

Accounts
Beancount中所有的Commodities/Currency的出入都是基于Account的,Beancount提供了5种类型的账户,每一笔交易都必须隶属于这些账户中的一个,如下:
1 | Assets Liabilities Equity Income Expenses |
一个账户名可以用:
分割的多个大写字母开头的单词组成,第一个单词作为主账户必须是上面这5类账户。这五类账户不需要自己创建,但所有的子账户都需要通过open来创建,不能自动创建。
子账户名称的语法要求:必须以大写字母或数字开头,后面可以跟随字母、数字或短横线(-)字符。不允许使用其他字符。
所有账户名称集合隐式地定义了一个账户层次结构(有时称为chart-of-accounts,会计科目表),类似于文件系统中文件的组织方式。例如,以下账户名称:
1 | Assets:US:BofA:Checking |
隐式地声明了一个如下所示的账户树结构:这种层次结构通过冒号(:
)分隔账户名称的各个部分,形成一个树状结构,便于组织和管理账户。
1 | `-- Assets |
可以说Assets:US:BofA
是Assets:US:BofA:Checking
的父账户,而后者是前者的子账户。
作为首次接触财务知识的我们,可能对beancount内置的这五类账户不太理解,其实这几类账户是基于标准会计原则设计的,它们对于理解和使用Beancount系统至关重要。我们一起看一下:
Assets (资产)
资产代表个人或企业拥有的资源,具有经济价值,未来能带来利益(如现金、房产、投资等)。特点是:当您增加资产时,账户余额增加;减少资产时,余额减少。
常见子账户:
1 | Assets:Cash:现金(钱包或保险柜中的纸币)。 |
如下示例交易:
1 | 2023-10-01 * "收到工资" |
需要注意:
- 购买耐用物品(如电脑)属于资产;日常消耗品(如文具)属于费用。
- 资产的价值可能随时间变动(如折旧),需定期调整(通过
pad
或balance
指令)。
Liabilities (负债)
负债代表个人或企业欠他人的债务(如贷款、信用卡欠款等)。特点:是通常有负余额。
常见的子账户:
1 | Liabilities:CreditCard ; 信用卡债务 |
当您增加负债时,账户余额变得更负;偿还负债时,余额向零方向移动。
如下示例交易:
1 | 2023-10-05 * "使用信用卡购物" |
注意事项
- 预收款项(如押金)属于负债,直到服务完成后转为收入。
- 偿还负债时,负债减少(借记),资产(如现金)同时减少(贷记)。
Equity (权益)
权益是资产减去负债后的净值,代表所有者对企业的所有权(如初始投资、留存收益等),通俗的讲代表所有者在业务中的财务权益,或个人净资产。权益账户通常用于平衡初始资产和负债,或记录净收入。
常见的子账户:
1 | Equity:Owner:Capital:初始投资或注资。 |
示例交易:
1 | ; 初始注资 |
注意事项:
通常有负余额(因为它平衡资产和负债)。
权益账户通常不直接参与日常交易,主要用于初始化和年末结转。
在 Beancount 中,损益表的净收入会自动通过
Equity:RetainedEarnings
反映在资产负债表中。
Income (收入)
收入代表通过销售、服务或投资获得的经济利益流入(如工资、利息收入等)。 通常有负余额(因为它增加净资产)。
常见的子账户:
1 | Income:Salary ; 工资收入 |
示例交易:
1 | 2023-10-10 * "销售商品" |
注意事项:
预收款项(如预收一年房租)应先记录为负债(
Liabilities:UnearnedRevenue
),再按月转为收入。收入账户的余额最终会通过
Equity:RetainedEarnings
结转到权益。当您记录收入时,收入账户的余额变得更负,而资产账户的余额增加。
Expenses (支出/费用)
费用代表为获取收入或维持运营所消耗的资源(如房租、餐饮、交通等)。
常见的子账户:
1 | Expenses:Rent ; 租金支出 |
示例交易:
1 | 2023-10-15 * "支付房租" |
注意事项:
资本性支出(如购买设备)应记为资产,而非费用。
费用账户的余额最终会通过
Equity:RetainedEarnings
结转到权益。当您记录支出时,支出账户的余额增加,而资产账户的余额减少。
通常有正余额。
通过正确使用这些账户类型,您可以在Beancount中精确地追踪您的财务状况,生成准确的财务报告,并获得对您财务健康的全面了解。
Commodities / Currencies
在Beancount中,账户中的金额单位称为货币(currencies),有时也称作商品(commodities),这两个术语可互换使用。与账户名称类似,货币名称通过其语法规则识别,但无需预先声明即可直接使用。货币名称的语法规则如下:
- 长度不超过24个字符。
- 必须以大写字母开头。
- 必须以大写字母或数字结尾。
- 中间字符仅允许大写字母、数字或以下标点符号:
'
(撇号)、.
(句点)、_
(下划线)、-
(连字符)
如下示例:
1 | USD |
使用示例如下:
1 | 2023-10-15 * "支付房租" |
关于货币类型,建议使用ISO 4217 currency code 标准的国际货币编码,当然我们也可以根据需要自定义一个货币类型,例如统计假期小时数的类型 (VACHR
)。
Strings
在交易条目中,所有自由文本(如交易对手方 payee
、说明 narration
等非结构化内容)需用双引号 "
包裹。:日期、数字、货币符号、账户名称等结构化字段无需引号。
1 | 2023-10-01 * "网购商品:笔记本电脑(分两期支付) |
Comments
Beancount文件不仅用于存放指令,也可以自由的在其中添加注释和标题来组织文件,任何分号”;”之后的行内文本将被忽略,
其实:任何不以有效Beancount语法开头的行都会被忽略,这样用户可以自由组织文件结构。这种方式允许我们插入各种大纲模式(如 Emacs 的 org-mode)的标记来组织文件。例如,您可以按机构组织输入文件并独立折叠展开各部分:
1 | * Banking |
Directives
Beancount采用声明式的语法结构,Beancount的账本文件主要有两个重要的部分组成:
- 一系列的
directives
(指令)或者称之为entries
(记录); - 由
option
指令定义的选项;
directive
的语法格式如下:
1 | YYYY-MM-DD <type> … |
每一条directive
必须以关联的日期开头和对应的指令类型,日期决定了此条指令的生效日期,类型决定了指令表示的事件。
如下是三条Beancount的指令示例,功能为
1 | ; 开启一个新的账户 |
需要注意的是:多条指令之间的声明顺序是不重要的,Beancount会在处理账本前,先解析账本,然后按照时间先后重新排序所有的条目,这是该语言的一个重要功能,因此它使您可以以任何方式组织输入文件,而不必担心影响指令的含义。
除交易外,每个指令都假设发生在每天的开始。例如,您可以声明帐户在其首次交易的同一天开立:
1 | 2014-02-03 open Assets:US:BofA:Checking |
如果您想尽快立即关闭该帐户,则无法声明其在同一天关闭,您将不得不通过声明来捏造一个日期,如下:
1 | 2014-02-04 close Assets:US:BofA:Checking |
这也解释了为什么在同一日期发生和余额断言和交易,余额断言会优先进行验证。这是为了一致性。
下面看一下Beancount支持的指令类型:
Open
所有的子账户在使用之前必须通过open
指令进行声明创建,格式如下:
1 | YYYY-MM-DD open Account [ConstraintCurrency,...] ["BookingMethod"] |
例如:
1 | 2014-05-01 open Liabilities:CreditCard:CapitalOne USD |
用逗号分隔的可选约束货币列表强制要求所有记入该账户的变动必须使用声明的货币之一。建议指定货币约束。你为Beancount提供的约束越多,输入数据时出错的可能性就越小,因为一旦出错,它会向你发出警告。
每个账户应在首次有金额记入该账户的交易日期之前(或同一天)被声明为open
。需要明确的是:open
指令不必出现在文件中的交易之前,但open
指令的日期必须早于记入该账户的日期。如前面提到过的:多条指令之间的声明顺序是不重要的,Beancount会在处理账本前,先解析账本,然后按照时间先后重新排序所有的条目,这是该语言的一个重要功能。
如下是一个合法的内容:
1 | 2014-05-05 * "Using my new credit card" |
Close
和open
指令类似的,有一个close
指令,用于表示一个子账户为关闭状态,语法格式如下:
1 | YYYY-MM-DD close Account |
例如:
1 | ; Closing credit card after fraud was detected. |
该指令的用途包括:
- 数据完整性检查:
如果在账户关闭日期之后仍有金额记入该账户,系统会报错。这有助于避免数据输入错误。 - 报告过滤:
它帮助报告代码识别哪些账户仍然活跃,并过滤掉在报告时间段之外已关闭的账户。
随着时间的推移,账本会积累大量数据,有些账户可能已经停止使用。通过关闭指令,可以避免在关闭后的年份中看到这些账户,从而保持报告的整洁。
需要注意的是,close
指令目前不会自动生成余额为零的检查。你可能需要在关闭日期之前手动添加一个余额检查,以确保账户在关闭时确实为空。
目前,一旦账户被关闭,你就不能在关闭日期之后重新打开它(当然,你可以删除或注释掉关闭该账户的指令)。此外,代码中还有一些实用函数可以帮助你确定在特定日期哪些账户是打开的。
强烈建议:当账户在现实中关闭时,及时在账本中关闭它们,这会让你的账本更加整洁有序。
Commodity
commodity
指令用于声明货币、金融工具或商品(在 Beancount 中,这些概念本质上是相同的),指令的格式:
1 | YYYY-MM-DD commodity Currency |
这个指令是后来加入的,并且完全是可选的:你可以直接使用商品而无需显式声明。该指令的主要目的是为商品附加特定的元数据字段,以便后续插件可以收集这些信息。例如,你可能希望为每种商品提供一个详细的描述名称,比如为商品“CHF”提供“瑞士法郎”的描述,或为“HOOL”提供“Hooli 公司 C 类股票”的描述,如下:
1 | 1867-07-01 commodity CAD |
例如,插件可以收集这些元数据属性名称,并按资产类别进行一些汇总。
你可以为商品使用任何日期……但更相关的日期通常是该商品创建或引入的日期。例如,加拿大元(CAD)于 1867 年首次引入,新以色列谢克尔(ILS)自 1986 年 1 月 1 日开始使用。对于公司股票,公司成立并发行股票的日期可能是一个合适的日期。由于该指令的主要目的是收集与商品相关的信息,因此你选择的特定日期并不那么重要。
需要注意的是,重复声明同一种商品会导致错误。
Transactions
交易(Transactions)是账本中最常见的指令类型。它们与其他指令略有不同,因为交易后面可以跟随多个 过账项(postings)。以下是一个示例:
1 | 2014-05-05 txn "Cafe Mogador" "Lamb tagine with wine" |
与其他指令一样,交易指令以 YYYY-MM-DD
格式的日期开头,后跟指令名称 txn
。然而,由于交易是复式记账系统的核心,因此在 Beancount 输入文件中,交易是最常见的指令类型。为此,我们允许用户省略 txn
关键字,而仅使用一个 flag 来代替:
1 | 2014-05-05 * "Cafe Mogador" "Lamb tagine with wine" |
flag
用于指示交易的状态,其具体含义由用户定义。我们建议使用以下解释:
*
:已完成交易,金额已知,“看起来正确”。!
:未完成交易,需要确认或修订,“看起来不正确”。
在上面第一个示例中,使用 txn
关键字时,交易默认会被标记为 *
标志(这也是我们最常使用的)。
你也可以为过账项本身附加标志,特别是当你希望标记交易的某一部分时:
1 | 2014-05-05 * "Transfer from Savings account" |
现在我们再来开始看看交易的格式:
1 | YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Flag] Account Amount [{Cost}] [@ Price] [Flag] Account Amount [{Cost}] [@ Price] ... |
第一行之后的每一行都是 过账项(Postings)。你可以为交易附加任意数量的过账项。例如,工资条目可能如下所示:
1 | 2014-03-19 * "Acme Corp" "Bi-monthly salary payment" |
在过账项中,金额也可以是算术表达式,支持 ( ) * / - +
运算符。例如:
1 | 2014-10-05 * "Costco" "Shopping for birthday" |
过账项的唯一关键约束是:所有过账项的金额总和必须为零。这是复式记账的基本原则,确保每一笔交易都平衡。
Payee & Narration
每个交易中可以包含可选的 收款方(Payee)和描述(Narration) 。例如下面的示例中,这比交易付款方是"Cafe Mogador"
,描述是"Lamb tagine with wine"
。
1 | 2014-05-05 * "Cafe Mogador" "Lamb tagine with wine" |
- 收款方(Payee)
收款方是一个字符串,表示交易中涉及的外部实体。收款方在某些交易中非常有用,特别是当金额记入 费用账户(Expense accounts) 时,因为这些账户会累积来自多个企业的费用。一个典型的例子是 Expenses:Restaurant
,该账户会记录你光顾过的所有餐厅的费用。
- 描述(Narration)
描述是你对交易的文字说明。它可以是对交易背景的注释、与你同行的人、购买商品的备注……任何你想记录的内容。你可以根据自己的喜好插入任何信息。例如,在对账时,我喜欢快速插入一些备注,以便日后参考,比如回答“去年冬天我和 Andreas 在西区去的那家很棒的小寿司店叫什么名字?”这样的问题。
如果你在交易行中只写一个字符串,它会被视为描述,如下:
1 | 2014-05-05 * "Lamb tagine with wine" |
如果你只想设置收款方,可以添加一个空的描述字符串:如下,
1 | 2014-05-05 * "Cafe Mogador" "" |
出于历史原因,收款方和描述之间可以使用管道符号(“|”)分隔(但未来可能会移除这种写法):
1 | 2014-05-05 * "Cafe Mogador" | "" |
你也可以省略收款方或描述(但必须提供标志):
1 | 2014-05-05 * |
Costs and Prices
交易中的过账项(postings)表示存入或从账户中提取的单个金额,可以包含相关的成本和价格。最简单的过账项仅包含金额:
1 | 2012-11-03 * "Transfer to pay credit card" |
如果你将金额从另一种货币转换而来,则必须提供汇率以平衡交易。这可以通过在过账项中附加”价格”来实现,价格即转换汇率:如下:
1 | 2012-11-03 * "Transfer to account in Canada" |
你也可以使用** @@
语法指定总成本**:
1 | 2012-11-03 * "Transfer to account in Canada" |
Beancount 会自动计算单位价格,即 1.090025 CAD(注意最后两个示例中的精度会有所不同)。
交易完成后,我们通常不会跟踪存入账户的美元单位的汇率;这些“美元”单位只是简单地存入。从某种意义上说,它们的转换汇率已经被“遗忘”。
然而,某些存入账户的商品必须按成本持有。这种情况通常发生在你需要跟踪这些商品的成本基础时。典型的用例是投资,例如当你将股票存入账户时。在这种情况下,你希望将存入商品的购买成本附加到这些单位上,以便在以后移除这些单位(卖出时),你可以根据成本确定要移除哪些单位,从而控制税务影响(并避免错误)。你可以想象,账户中的每个单位都有一个附加的成本基础。这将使我们能够自动计算资本收益。
为了指定某个过账项按特定成本持有,可以在花括号中包含成本:
1 | 2014-02-11 * "Bought shares of S&P 500" |
这是一个需要深入讨论的主题。有关这些主题的详细讨论,请参阅库存如何工作和使用 Beancount 进行交易文档。
最后,你可以在过账项中同时包含成本和价格,如下针对指定成本买入的股票,以指定的价格卖出:
1 | 2014-07-11 * "Sold shares of S&P 500" |
价格仅用于在价格数据库中插入价格条目(见下文“价格”部分)。本文档的“平衡过账项”部分对此进行了更详细的讨论。
重要提示:无论是每股价格、总价格还是成本,金额始终为无符号数。使用负号或负成本是错误的,Beancount 会在你尝试这样做时报错。
总结一下关于过账项中的价格和成本:
- 按成本持有过账:
{成本价}
,帐单平衡的时候必须成本一致,Beancount在账目平衡的时候,会根据成本价自动计算过账金额; - 按单价/汇率过账:
@
- 按总成本过账:
@@
Balancing Rule
复式记账法的一个核心原则是确保所有过账的金额总和在所有货币中等于零。这是一个不可妥协的核心条件,它确保了会计等式的成立,并使得我们可以筛选任意交易子集并生成平衡至零的资产负债表。
但当涉及多种单位类型(如货币转换和“按成本持有”的资产单位)时,这一原则如何应用呢?
我们设计了一条简单清晰的规则,用于从每个过账中提取一个金额及其对应货币,以便进行整体平衡。这一金额被称为过账的“平衡金额”(即权重)。以下示例展示了四种成本/价格组合情况下如何计算平衡金额:
1 | YYYY-MM-DD |
- 仅含金额,无成本或价格
直接使用过账中的金额和货币。例如上节中第一个示例的-400.00 USD
,需与另一笔400.00 USD
过帐平衡。 - 仅含价格(汇率)
将价格乘以单位数量,并使用价格对应的货币。例如上节第二个案例中的-400.00 USD × 1.09 CAD/USD = -436.00 CAD
,需与另一笔436.00 CAD
过账平衡。 - 含成本(按成本计价)
将成本乘以单位数量,并使用成本货币。例如前文第三个案例中的10 IVV × 183.08 USD/IVV = 1830.70 USD
,需与现金账目-1830.70 USD
平衡。 - 同时含成本和价格
仅使用成本计算,忽略价格。价格在此仅用于生成内存中的价格数据库条目,不参与平衡计算。
通过这一规则,所有交易均可轻松实现平衡。该规则使 Beancount 能够自动计算资本收益(详见 Trading with Beancount)。
Reducing Positions
当我们减少仓位(或者称之为头寸)的时候,减少量必须始终匹配现有的批次,例如,若某账户持有3200美元,当有交易对该账户进行-1200美元的变动时,1200美元将匹配现有的3200美元,最终形成单笔2000美元的持仓。此机制同样适用于负值场景。例如,若账户余额为-1300美元并对其应用+2000美元的变动,将获得700美元的新余额。
对任何类型账户的变动操作都可能产生正或负的余额,简单商品金额(即无关联成本的金额)的余额不受限制。例如,虽然资产账户通常保持正余额而负债账户多为负余额,但您仍可合法地将资产账户贷记至负余额,或将负债账户借记至正余额。这是因为现实世界中确实存在此类情况:您可能因多开一张支票获得银行支票账户的临时信用(通常伴随高额”透支”费用),或误操作重复支付信用卡账单。
对于按成本持有的商品,在应用交易前,记账的成本说明必须与库存中的某个批次匹配。系统会收集所有批次列表,并与记账中{…}部分的规定进行匹配。例如,若您指定成本值,则仅保留成本匹配的批次;若指定日期,则保留日期匹配的批次;您亦可使用标签进行匹配。当同时指定成本和日期时,系统将对候选批次列表进行双重匹配筛选。
若筛选后仅剩单一批次,该批次将被选定用于减少操作。若结果包含多个批次,但减少总量等于这些批次的总量,则所有相关批次都将被相应调整。
例如,若历史交易如下:
1 | 2014-02-11 * "Bought shares of S&P 500" |
以下减少操作均具有明确性:
1 | 2014-05-01 * "Sold shares of S&P 500" |
下面的操作存在歧义:
1 | 2014-05-01 * "Sold shares of S&P 500" |
当多个批次匹配减少操作且数量不符总数时,系统将触发账户的预订方法。默认所有账户使用”严格”(STRICT)模式,此时会报错提示歧义匹配。
您可将账户设置为”先进先出”(FIFO)选择最早批次,或”后进先出”(LIFO)选择最新批次,系统将自动选取所需批次完成减少操作。
对于按成本持有的商品,通常不允许出现负数量单位。当前Beancount禁止持有成本商品的负数量。例如,仅包含此交易的输入将失败:
1 | 2014-05-23 * |
若允许此操作,将导致-10单位的MSFT持仓。但若账户在2014-05-23
确有12单位43.40美元的MSFT,该交易将正常执行,持仓减至2单位。常见错误场景是账户持有不同成本的MSFT(如20单位42.10美元),而用户错误指定了成本值(如43.40美元),此时因无对应成本的足够数量可供减少,交易将被拒绝。
此限制的设立基于以下考量:
- 股票单位的录入错误较常见且影响重大(金额通常较大),该限制能有效捕获此类错误
- 成本商品的负持仓非常罕见,多数用户无需此功能(例外包括:股票做空、期货价差持仓、外汇空头头寸等)
因此默认启用此验证。
未来版本将放宽此限制,允许在特定条件下持有负数量商品(当账户无其他同商品持仓时),或允许标记账户完全解除此限制,以支持商品空头头寸的记录。当前主要障碍即为此限制机制。
更多库存预订算法细节,请参阅 How Inventories Work
Amount Interpolation
Beancount能够自动补全交易的某些细节。这也是beancount账本中常见的用法,当前您可在交易中省略至多一个记账条目的金额,如下:
1 | 2012-11-03 * "Transfer to pay credit card" |
上例中信用卡条目的金额被省略。Beancount将自动计算为400.00 USD以平衡交易。此机制同样适用于多条目及含成本价的场景:
1 | 2014-07-11 * "Sold shares of S&P 500" |
此处IVV以197.90美元/股卖出(高于买入价183.07美元)。首条资产条目计算权重为-10 × 183.07 = -1830.70 USD,现金条目直接计入1979.00 USD。末条资本收益将自动计算差额-148.30 USD(即实现收益148.30美元)。
在计算平衡金额时,系统使用与校验交易平衡相同的逻辑来补全缺失金额。例如下列交易不会报错:
1 | 2014-07-11 * "Sold shares of S&P 500" |
现金账户将自动获得1830.70 USD(因IVV条目的平衡金额为-1830.70 USD。若条目同时含成本价与市价,始终使用成本基础,市价被忽略)。尽管从平衡角度此操作正确,但从会计视角并不完整:
- 资本收益需单独记账
- 按此操作现金账户实际到账金额(1979.00 USD)与系统补全值(1830.70 USD)存在差额
- 后续现金账户的余额断言将触发错误提示此遗漏
该机制同样支持多币种平衡:
1 | 2014-07-12 * "Uncle Bob gave me his foreign currency collection!" |
系统将插入多个条目(每个需平衡的币种对应一条)来补全省略的金额。
Tags
每一笔交易可被标记任意字符串标签,如下:
1 | 2014-04-23 * "Flight to Berlin" #berlin-trip-2014 |
此机制类似于Twitter等平台的”话题标签”(#标签)。通过标签可标记特定交易子集,随后用作过滤条件生成专项报告。其应用场景广泛,例如标记所有差旅行程。
支持多标签联合标记:
1 | 2014-04-23 * "Flight to Berlin" #berlin-trip-2014 #germany |
Tag Stack
通常,与单一标签相关的多个交易会在文件中连续录入。为简化操作,解析器可自动对文本块内的交易应用标签。其工作原理如下:解析器维护一个当前标签”栈”,在逐行读取交易时,栈顶标签将应用于所有交易。您可通过指令动态管理标签栈,示例如下:
1 | pushtag #berlin-trip-2014 |
通过此机制,您可将多个标签推入栈中,应用于一系列连续交易,无需逐一重复输入标签。
Links
交易可通过链接实现跨时段关联。您可将链接视为一种特殊标签,用于将财务相关的多个交易归组管理。典型应用场景包括:
- 发票追踪:将发票创建与后续支付(或核销)交易关联:
1 | 2014-02-05 * "Invoice for January" ^invoice-pepe-studios-jan14 |
- 多步骤资金流转追踪:关联涉及同一目的的多笔转账操作:
1 | 2014-02-05 * "Moving money to Isle of Man" ^transfers-offshore-17 |
特性说明:
- 链接符号使用
^
前缀(如^invoice-001
) - 同一链接可跨多个交易重复使用
- 在Web界面中,链接交易可独立展示为专用日志(不受当前视图/过滤器影响)
- 与标签不同,链接强调交易间的业务连续性关联(如发票生命周期、复杂资金流转等)
Balance Assertions
余额断言是一种将您的账户对账单余额纳入交易流水的方法。它指示Beancount验证特定账户中某种商品单位数量在某个时间点应等于预期值。例如:
1 | 2014-12-26 balance Liabilities:US:CreditCard -3492.02 USD |
表示检查在2014-12-26
时间,Liabilities:US:CreditCard
账户中的USD单位数量是否为-3492.02 USD。在处理条目列表时,如果Beancount发现USD余额与此不符,将报告错误。
若未报错,您可对该账户此前交易记录的准确性保持较高信心。这在实践中非常实用,因为许多情况下某些交易可能从其各分录账户单独导入(参见去重问题)。这可能导致您无意中重复记录同一交易,定期插入余额断言可及时发现此类问题。
余额指令的通用格式为:
1 | YYYY-MM-DD balance Account Amount |
注意,余额断言与所有其他非交易指令一样,适用于其日期的起始时刻(即当日凌晨)。可将其想象为在当日午夜时分立即执行的余额检查。
余额断言仅适用于资产负债表账户(资产与负债)。由于收支类账户的价值具有时效性特征(即我们关注的是其时间段内的变动总和而非绝对值),在损益表账户使用余额断言意义不大。
另请注意,每个账户在开户指令日期后都隐含着该账户余额应为零的断言。开设账户时无需显式声明零余额断言。
Multiple Commodities
一个Beancount账户可能包含多种货币(尽管实际上这种情况并不常见,明智的做法是创建专门的子账户来持有每种货币,例如持有股票组合)。余额断言仅适用于断言中的货币;它不会检查余额中的其他货币。如果您想检查多种货币,请使用多个余额断言,如下所示:
1 | ; 检查钱包中的现金余额 |
目前没有办法彻底检查账户中的所有货币((a proposal is underway)。
请注意,在此示例中,如果彻底检查对您真的很重要,您可以通过定义现金账户的子账户来分别隔离每种货币,例如Assets:Cash:USD 、Assets:Cash:CAD 。
Lots Are Aggregated
余额断言适用于特定货币的单位总和,无论其成本如何。例如,如果您在一个账户中持有三批相同的货币,例如5 HOOL {500 USD}和6 HOOL {510 USD},则以下余额检查应成功:
1 | 2014-08-09 balance Assets:Investing:HOOL 11 HOOL |
所有批次汇总在一起,您可以验证它们的单位数量。
Checks on Parent Accounts
余额断言可以在父账户上执行,并将包括其及其子账户的余额:
1 | 2014-01-01 open Assets:Investing |
请注意,这确实要求父账户已被声明为Open,以便在余额断言指令中合法使用。
Before Close
在关闭账户之前插入一个0单位的余额断言非常有用,以确保在关闭时其内容为空。Close指令不会自动为您插入该断言(我们可能会最终为其构建一个插件)。
Local Tolerance
有时需要覆盖余额检查的误差以在该余额断言上放宽它,这是很常见的。可以使用余额金额的局部浮动金额来完成,如下所示:
1 | 2013-09-20 balance Assets:Investing:Funds 319.020 ~ 0.002 RGAGX |
Pad
填充指令会自动插入一个交易,该交易将使后续的余额断言成功(如果需要)。它会插入满足该余额断言所需的差额。(类似LaTeX中的”rubber space”,填充指令就是Beancount中的余额。)
请注意,“后续”是指按日期顺序,而不是按文件中声明的顺序。这在概念上等同于一个交易,该交易将自动扩展或收缩以填补两个余额断言之间的差异。它看起来像这样:
1 | 2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances |
填充指令的通用格式为:
1 | YYYY-MM-DD pad Account AccountPad |
第一个账户是自动计算金额的贷方账户。这是应该在其后具有余额断言的账户(如果该账户没有余额断言,则填充条目是无害的并且不执行任何操作)。
第二个账户是交易的另一部分,它是资金的来源,这几乎总是一些权益账户。这样做的原因是,此指令通常用于初始化新账户的余额,以节省我们手动插入此类指令或输入将账户带到当前余额的完整过去交易历史的需要。
以下是一个实际使用场景示例:
1 | ; Account was opened way back in the past. |
这将导致在填充指令之后插入以下交易,日期相同:
1 | 2002-01-17 P "(Padding inserted for balance of 987.34 USD)" |
这是一个正常的交易——您将看到它与其他交易一起出现在呈现的日记账中。(请注意特殊的“P”标志,脚本可以使用它来查找这些。)
请注意,如果没有余额断言,填充指令就没有意义。因此,与余额断言一样,它们通常仅用于资产负债表账户(资产和负债)。
您也可以在余额断言之间插入填充条目,它也可以工作。例如:
1 | 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD |
如果没有中间交易,这将插入以下填充交易::
1 | 2014-08-08 P "(Padding inserted for balance of 1137.23 USD)" |
149.89 USD是1137.23 USD和987.34 USD之间的差额。如果有更多的中间交易将金额发布到支票账户,金额将自动调整以使第二个断言通过。
Unused Pad Directives
目前,您不能在输入文件中保留未使用的填充指令。它们会触发错误:
1 | 2014-01-01 open Assets:US:BofA:Checking |
上面的指令,由于存在正常的交易,所以pad
指令并没有生效,这会触发错误;
Commodities
前面在介绍Multiple Commodities的时候提到,余额断言只针对指定的货币/商品,但填充指令本身不指定任何商品类型。该指令会影响账户中所有存在对应余额断言的商品。例如,以下代码将通过填充指令插入包含USD和CAD两种货币分录的交易:
1 | 2002-01-17 open Assets:Cash |
如果账户中存在未声明余额断言的其他商品(货币),则不会为这些货币插入分录。
Multiple Paddings
当前版本中,无法在同一个平衡余额指令前为同一账户及商品插入多个填充条目。例如以下代码将触发错误:
1 | 2002-01-17 open Assets:Cash |
Notes
Note 指令用于在特定账户的日记账中附加一条带日期的注释,例如:
1 | 2013-11-03 note Liabilities:CreditCard "Called about fraudulent card." |
在渲染日记账时,注释会显示在上下文中。这对于记录与财务事件相关的事实和声明非常有用。我经常使用它来记录一些无法直接写入交易的信息片段。
Note 指令的通用格式为:
1 | YYYY-MM-DD note Account Description |
描述字符串可以跨越多行。
Documents
Document 指令可用于将外部文件附加到账户的日记账中,例如:
1 | 2013-11-03 document Liabilities:CreditCard "/home/joe/stmts/apr-2014.pdf" |
在 Web 界面的日记账中,文件名会渲染为浏览器链接,点击即可查看文件内容。这对于将账户对账单和其他下载文件整合到账户流程中非常有用,只需点击几下即可轻松访问。还可以编写脚本,根据账户名称获取文档列表并对其进行处理。
Document 指令的通用格式为:
1 | YYYY-MM-DD document Account PathToDocument |
Documents from a Directory
一种更便捷的方式是使用特殊选项指定目录,这些目录包含与账户结构镜像的子目录层次结构。例如,上一节中的 Document 指令可以通过如下目录结构生成:
1 | stmts |
只需将根目录指定为选项(注意末尾没有斜杠):
1 | option "documents" "/home/joe/stmts" |
系统会提取以日期(格式为 YYYY-MM-DD)开头的文件。如果有多个此类文档存档,可以多次指定此选项。过去,我曾为每一年使用一个目录(如果扫描所有文档,目录可能会变得非常大,因为扫描文档通常是较大的文件)。
使用账本账户的层次结构来组织电子文档对账单和扫描件,是一种极佳的方式,可以为这些文档建立清晰、明确的位置,便于日后需要时查找。
Prices
Beancount 有时会为每种商品创建一个内存中的价格数据存储,用于多种用途,特别是用于报告账户持仓的未实现收益。Price 指令可用于为此数据库提供数据点。Price 指令用于建立一种商品(基础货币)与另一种商品(报价货币)之间的汇率:
1 | 2014-07-09 price HOOL 579.18 USD |
这条指令表示:“2014 年 7 月 9 日,1 单位 HOOL 的价格为 579.18 美元。”货币汇率的 Price 条目同样适用:
1 | 2014-07-09 price USD 1.08 CAD |
Price 指令的通用格式为:
1 | YYYY-MM-DD price Commodity Price |
需要注意的是,Beancount 并不了解 HOOL、USD 或 CAD 是什么。它们是不透明的“事物”,其含义由用户赋予。因此,如果您以小时为单位核算未使用的假期,为假期小时设置价格也是完全有效且有用的:如下:
1 | 2014-07-09 price VACHR 38.46 USD ; 相当于年薪 $80,000 |
Prices from Postings
如果您使用beancount.plugins.implicit_prices
插件,每当出现包含成本或可选价格的Posting记录时,系统将自动利用该成本或价格生成Price指令。例如,以下交易:
1 | 2014-05-23 * |
在解析后会自动生成:
1 | 2014-05-23 * |
这种方式非常便捷,启用后您账本价格数据库中的多数价格点都将来源于此。您可以从账本中打印已解析价格表(这本身就是一种报表类型)。
Prices on the Same Day
需要注意的是系统没有时间粒度概念——虽然Beancount完全能够处理单日多次交易,但其设计目标并非服务于日内交易者。系统仅按日存储价格(通常来说,如果需要处理大量依赖日内时间粒度的功能,建议使用其他专业系统,这已超出簿记系统的设计范畴)。
当同一日期确实存在多个Price指令时,系统将选择文件中最后出现的那个载入价格数据库。
Events
事件指令用于追踪您自定义的某个变量随时间推移的值。例如,记录您的位置信息:
1 | 2014-07-09 event "location" "Paris, France" |
上述指令表示:“自2014年7月9日起,将location
(位置)事件的值更改为"Paris, France"
。同一事件类型每日仅保留一个最新值。
事件指令的通用格式为:
1 | YYYY-MM-DD event Name Value |
- Name:事件名称字符串无需预先声明,首次使用时即自动创建。
- Value:事件值可以是任意字符串,无固定结构限制。
使用场景示例
- 位置追踪
记录所在城市或国家,便于关联消费行为与地理位置。例如,在记录日常现金支出的“现金流水账”模块中,可同步添加位置事件。 - 地址记录
频繁搬迁时,记录历史住址有助于应对政府文件(如移民申请、美国绿卡办理)的地址追溯需求。 - 雇主信息
记录每份工作的入职与离职日期,方便统计工作时长。 - 交易窗口期
若任职于上市公司,可记录允许交易公司股票的时间段,避免在禁止期内违规操作。
事件的应用与计算
事件常用于天数统计场景。例如:
加拿大魁北克省规定,若一年内在本省停留≥183天(短期离境≤21天的情况除外),即可享受免费医保。通过脚本分析事件记录,可预警剩余天数,避免失去保障资格。
Query
查询指令允许在Beancount文件中嵌入SQL查询,便于自动生成定制化报告。当前该功能处于早期实验阶段,语法示例如下:
1 | 2014-07-09 query "france-balances" " |
指令格式:
1 | YYYY-MM-DD query Name SqlContents |
查询名称用于标识报告类型,执行时可通过名称调用。指令日期表示查询截止时间,后续交易将被忽略(相当于SQL中的隐式CLOSE
操作)。
Custom
Beancount长期规划支持通过插件或外部客户端扩展自定义指令类型,并由解析器验证其结构。当前版本提供临时通用指令供功能原型设计(如预算跟踪):
1 | 2014-07-09 custom "budget" "..." TRUE 45.30 USD |
指令格式:
1 | YYYY-MM-DD custom TypeName Value1 ... |
指令类型为唯一字符串标识符(如"budget"
),用于区分自定义逻辑。参数列表支持混合类型(字符串、日期、布尔值、金额、数值等),但系统不会验证参数数量或类型的一致性。
Metadata
您可为所有账目条目(Entry)及过账(Posting)附加自定义元数据。语法示例如下:
1 | 2013-03-14 open Assets:BTrade:HOOLI |
- 元数据键名规则:首字符必须为小写字母(a-z),后续可包含大小写字母、数字、短横线(
-
)及下划线(_
)。 - 支持的数据类型
- 字符串(String)
- 账户(Account)
- 货币单位(Currency)
- 日期(datetime.date)
- 标签(Tag)
- 数值(Decimal)
- 金额(beancount.core.amount.Amount)
元数据应用场景
查询与过滤
Beancount内置工具(如bean-query
)支持通过元数据值进行交易筛选与聚合分析。自定义脚本开发
所有指令的元数据可通过Python脚本的.meta
属性(字典类型)直接访问,例如:1
transaction.meta.get("statement") # 获取交易对账单文件名
注意事项
- 系统保留属性:所有指令默认包含
filename
(来源文件名,字符串)和lineno
(行号,整数)属性。 - 空值与重复处理
- 未赋值的元数据键值为
None
。 - 同一键名重复定义时,仅首个值生效,后续值被忽略。
- 未赋值的元数据键值为
Options
Beancount输入文件除包含各类指令外,还支持通过无日期型option
指令设置全局参数。例如:
1 | option "title" "Ed’s Personal Ledger" |
指令格式:
1 | option Name Value |
- 参数名称与参数值均为字符串类型。
- 参数类型差异:部分选项为单值设定,部分为列表追加(具体取决于选项定义)。
查看可用选项
官方文档
本文档this document定期更新,提供最新选项说明。命令行查询
执行以下命令获取当前安装版本支持的完整选项列表:1
bean-doctor list-options
源码追溯
可直接查阅Beancount源码中的选项定义( peek at the source code )。
Operating Currencies
一个值得注意的选项是 “operating_currency”(操作货币)。默认情况下,Beancount 不会对任何商品进行特殊区分。特别是,它并不知道用户最常使用的商品——货币——有何特殊性。例如,如果您生活在新西兰,您的交易记录中可能会出现大量以新西兰元(NZD)计价的商品。
但实用的报表会尝试将所有非货币商品折算为使用的主要货币之一。此外,将货币单位单独列在专用列中也十分有用。这在导出数据时可能尤其方便,可避免为列指定单位,并能在导入电子表格后直接处理数值。
因此,您可以通过选项声明最常用的货币:
1 | option "operating_currency" "USD" |
您可以声明多个操作货币。
需注意的是,此选项仅用于报告生成代码,不会改变 Beancount 的核心处理逻辑或语义规则。
Plugins
若需加载插件的 Python 模块,请使用专用的 plugin
指令:
1 | plugin "beancount.plugins.module_name" |
插件名称应为 PYTHONPATH
环境变量中可识别的 Python 模块名。Beancount 加载器会导入这些模块,并在解析条目列表后运行插件代码,以便插件对条目进行转换或输出错误信息。通过这种方式,您可以将自定义代码集成到 Beancount 中,对条目进行任意转换处理。详情请参阅脚本编写与插件。
插件还支持可选配置参数。这些参数可通过最后一个字符串参数传递,例如:
1 | plugin "beancount.plugins.module_name" "configuration_data" |
插件指令的通用格式为:
1 | plugin ModuleName StringConfig |
配置数据的格式因插件而异。当前,任意字符串均可传递给插件。具体支持的配置内容,请参阅各插件的文档说明。
另请参阅 plugin_processing_mode
选项,该选项会影响内置插件的运行列表。
Includes
Beancount 支持通过 include
指令引入其他文件,便于将大型账务文件拆分为多个子文件管理。语法示例如下:
1 | include "path/to/include/file.beancount" |
通用格式为:
1 | include Filename |
被包含的文件路径可以是绝对路径或相对路径。若为相对路径,解析时会基于当前文件的所在目录进行定位。这种设计使得用户可通过相对路径构建层级化的文件结构,便于版本控制及跨环境迁移。
需注意的是,Beancount 对包含指令的处理机制与 C 语言等严格逐行包含的方式不同。解析器会先收集所有 include
指令,随后由加载器统一处理。这种设计可行是因为 Beancount 条目声明的顺序无关紧要。
关于配置选项的特殊说明:当前版本中,每个文件的配置选项(option
)会独立解析,但最终生效的选项集以主文件(即顶层调用文件)的配置为准。此行为未来可能会调整,请关注更新日志。
构建自己的账本
生成beancount文件
根据beancount的README,安装beancount后,就可以开始记录自己的beancount文件了,beancount提供了bean-example
命令,生成一个简易但是不失简单的beancount文件模版,基于此就可以进行账本的修改和账目的记录啦。
如下是自己简单的账本:
1 | ; Birth: 1990-08-11 |
账本的可视化
Beancount项目提供了fava,一个账本的可视化webui来查看和管理自己的账本,常规的可以看到自己的资产负债表和损益表,如下:
1 | $ fava -H 0.0.0.0 -p 8080 walkerdu.beancount |

其实beancount本身也提供了bean-web
的可视化管理工具,是fava的简洁版本,如下,也提供了资产负债表和损益表:
1 | $ bean-web --public walkerdu.beancount |

