Beancount 与 Ledger
Martin Blais,2014 年 9 月
http://furius.ca/beancount/doc/comparison
在邮件列表和私人邮件中,多次有人提出 Beancount 与 Ledger 和 Hledger 的区别。本文重点阐述了这些系统之间的关键差异,因为它们在设计和实现上存在显著不同。
请注意,本文是从 Beancount 的视角撰写的,作为其作者,反映了我对命令行会计系统设计的个人偏见。我的目的并非贬低其他系统,而是突出实质性的差异,以帮助新手理解这些系统在功能和运行方式上的不同,并可能激发与其他开发者就设计选择展开有益的讨论。
哲学差异
(参见此讨论帖。)
首先,Ledger 是乐观的。它假设用户能够轻松地输入正确数据。但根据我处理此类数据录入的经验,若没有大量自动化校验,几乎不可能做到完全正确。例如,未断言账户的符号错误非常普遍。相比之下,Beancount 则极为悲观:它假设用户不可靠,因此对输入施加了诸多约束。例如,如果你向一个空账户添加了一股 AAPL 股票(价格为 $100),它就不会允许你从中移除一股价格为 $101 的 AAPL 股票——因为你根本就没有这股股票。它不假设用户能够或应当按正确顺序输入交易(使用日期断言而非文件顺序断言)。它可选地检查收益是否与售价匹配(通过 sellgains 插件)。此外,它允许你对会计科目表施加额外约束,例如:一个拒绝记入非叶节点账户的插件,或拒绝同一账户包含多种商品的插件,或要求你必须使用 Open 指令声明所有账户的插件——你可以按需选择自己的严谨程度。它比复式记账法本身提供了更多的自动化交叉校验。毕竟,我们之所以选择复式记账法,正是为了交叉校验,那为何不更彻底地追求正确性呢?Beancount 非常适合那些不太信任自己的人。正因如此,它不支持不平衡/虚拟记账;这不是缺陷,而是有意为之。
其次,两者的设计理念存在差异。从用户手册可见,Ledger 提供了大量选项。这固然吸引许多人,但在我看来,它已演变成一个极其复杂的单体工具。这些选项之间的相互作用,以及许多选项带来的语义后果,既令人困惑又极其微妙。Beancount 则采取极简主义:虽然存在一些少量选项,但它极力避免增加任何选项。那些影响交易语义的选项,始终出现在输入文件中(命令行中无任何此类选项),且与特定工具的选项明确区分。无论使用何种报告工具消费,加载同一个文件总会产生完全相同的交易流。命令行中仅有的选项,仅影响所调用报告工具的特定行为;它们绝不会改变交易流本身的语义。
第三,Beancount 更深入地采用了流式处理。其加载器会创建一个单一的、有序的指令列表,所有指令共享一些通用属性(如名称、日期、元数据)。这就是全部的数据。在 Ledger 中被视为“语法”的指令,在 Beancount 中只是普通的指令对象,例如 'Open' 在 Beancount 中并无特殊之处,本身不执行任何操作,仅用于某些施加约束的例程(例如:某个账户是否已出现过 Open 指令?)或用于附加账户级元数据。价格同样以指令形式指定,并嵌入在流中,可通过这种方式生成。所有内部操作均被定义为处理并输出指令流。这使得用户能够在处理管道中插入自定义代码,对指令流执行任意转换——任何功能都可能实现,不受表达式语言语义的限制。这是一种机制,允许用户通过编写简短的 Python 插件来构建新功能,这些插件在 Beancount 的核心运行,而非仅在边缘访问其数据。事实上,Beancount 的内部处理正朝着越来越精简的方向演进,将更多工作交由这些插件完成,甚至可能允许插件自行声明指令类型(交易对象除外)。它正在演变为一个轻量级驱动器,仅负责组装处理管道以生成指令流,并提供一套便捷的库和函数式操作。
具体差异
库存登记与成本基础处理
Beancount 对账户库存中持仓减少实施严格的规则。这意味着你只能从账户中取出之前已存入的资产(按时间顺序),否则将触发错误。这一规则适用于以成本计价的单位(例如股票份额),以确保
-
成本基础绝不会从账户中泄露,
-
我们可以检测交易数据录入中的错误(这类错误非常普遍),以及
-
我们能够正确计算资本利得。
相比之下,Ledger 不对库存随时间的变动实施登记检查:所有批次一律累加,不考虑库存先前的内容(无批次增加与减少之分)。在 Beancount 中,对库存的减少必须与特定批次及其指定的成本基础相匹配。
在 Ledger 中,输出对此问题略有误导:为简化报表输出,用户可选择几种批次合并算法之一。默认情况下,系统会打印所有批次的单位总和;但通过使用这些选项,你可以让报表生成器考虑成本基础(其称为“价格”)和/或批次创建日期,此时它将分别报告具有不同成本基础和/或日期的批次。你可以通过命令行选项(例如 --lot-dates)选择合并类型。最重要的是,这意味着在 Ledger 中,从账户中移除从未添加过的批次是合法的。这会导致长仓与短仓混合,无法准确反映账户的实际变动。然而,它却能轻松实现平均成本基础的报表。
我相信这不仅令人困惑,而且是对账户库存的错误处理,我已指出这可能导致资金外流和计算错误。更多细节和示例请见此处。此外,直到最近,Ledger 还未使用成本基础来平衡记账,以正确计算资本收益。因此,我怀疑 Ledger 用户可能从未用它来计算并将其收益与经纪商报告的数值进行比较。
为了让 Beancount 能够有意义地检测此类错误,并在减少库存批次时实施严格的匹配规则,事实证明,它唯一需要施加的约束是:不允许同一账户同时持有同一种商品的多头和空头头寸。例如,它只需确保你不能在同一库存中同时持有 1 单位 GOOG 和 -1 单位 GOOG(无论其购入成本如何)。任何一批单位的减少都会被识别为“批次减少”,系统会搜索与减少记账规格匹配的批次,通常是在与记账指定相同成本下持有的批次。在实践中,实施这一约束并无太大问题,因为没有任何场景会希望在同一账户中同时持有长仓和短仓;但如果你确实需要这样做,只需使用不同的账户分别持有长仓和短仓即可(事实上,建议你为每种商品使用子账户来追踪头寸,这将非常自然)。
最后,Beancount 正在实现一项提案,显著扩展其库存记账的范围:新语法将允许对批次减少进行宽松的指定,使用户能够轻松编写无歧义的记账,并支持按平均成本记账,这在加拿大和全球所有税收递延账户中都很常见。它还将提供一种新语法,允许同时指定单位成本和总成本,这一功能将使我们能够正确在不考虑佣金的情况下追踪资本收益。
货币兑换
Beancount 在简单货币兑换与需按成本追踪的商品兑换之间做出了重要的语义区分。例如,将 20,000 美元兑换为 22,000 加元属于货币兑换(例如银行间兑换),在将所得加元存入目标账户后,它们不会被视为“每加元成本为 1.1 美元”,而只是作为加元单位保留在账户中,不记录兑换所用的汇率。这准确地模拟了现实世界的运作方式。另一方面,将 5,000 美元兑换为 10 股 GOOG 股票,且每股成本为 500 美元,则被视为与货币兑换不同的操作:所得 GOOG 股票附带特定的成本基础,库存批次中会保留每单位价格的记录(以及日期),并应用严格的规则来处理此类批次的减少(我称之为“记账”),如前所述。这用于计算和报告资本收益,更重要的是,它能检测出大量数据录入错误。
相比之下,Ledger 不区分这两种类型的兑换。Ledger 将货币兑换视为与按成本计价的商品兑换相同的方式。这并未像人们直觉中那样带来诸多麻烦的原因在于,它不进行库存记账——所有以不同兑换率发生的交易批次都会累积在账户中,表现为正值或负值;而对于简单的商品(如美元等货币),直接对单位总数进行净额计算,而不考虑每个单位的成本,即可得出正确结果。查看账户所有库存批次的完整列表是作为一个可选功能提供的(参见 Ledger 的 “--lot-dates”),但并非默认的账户余额显示方式。我怀疑很少有用户会使用这一功能:如果你对那些过去发生过大量货币兑换的真实账户渲染批次列表,你会发现大量无关的批次。我认为,货币兑换的成本基础最好直接省略。
HLedger 不解析成本基础语法,因此无法识别它。
输入隔离
Beancount 仅从你提供的文本文件中读取其全部输入内容。这种隔离是刻意为之的设计。它不与任何外部数据格式或在线服务(如历史价格获取器)建立关联。获取和转换外部数据的任务过于分散,我认为应由独立项目来处理。这些问题领域本身也自然地划分为清晰的边界:Beancount 提供一个独立的核心,让你能够导入所有交易数据并生成各种聚合报告,其语法正是连接外部交易仓库或价格数据库的枢纽。它通过这种方式将外部数据源的繁琐细节完全隔离在外。
可下载的包含交易信息的外部格式种类繁多,几乎不可能全部覆盖。从大多数机构获取的数据文件通常以多种格式提供:OFX、Quicken、XLS 或 CSV,查看其内容会明显发现,编写这些文件输出程序的程序员并未注重细节或格式的标准定义;情况相当混乱。这些文件几乎总是杂乱无章,且其混乱的细节会随着时间推移和文件格式的演变而不断变化。
获取历史或当前价格信息同样令人烦恼。虽然雅虎和谷歌财经能够为美国交易所的常见股票提供基本价格数据,但当你需要获取在外国交易所交易的金融工具,或通常不在交易所交易的工具(如共同基金)的信息时,数据要么不可用,要么即使可用,你也必须弄清楚它们使用了哪个代码符号——这方面几乎没有统一标准。你必须手动指定代码符号。此外,你完全可能希望管理那些没有任何外部价格来源的金融工具,因此你的记账软件必须提供一种手动输入价格的机制(Beancount 和 Ledger 都提供了此功能)。同一声明机制也用于缓存历史价格数据,从而使 Beancount 完全无需依赖网络连接。
大多数用户会希望编写自己的导入脚本,但已有一些现成的库可用:Beancount 内置的 beancount.ingest 库提供了一个框架,用于自动化识别、从可下载文件中提取交易记录并归档不同机构的文件。详情请参阅其 设计文档。
相比之下,Ledger 和 HLedger 支持 从 CSV 文件进行基本的交易转换,以及 通过外部脚本自动获取当前价格(您需自行提供 getquote 脚本)。如果您需要追踪所有账户,CSV 导入功能远远不够,必须加以扩展。接入外部脚本是正确的做法,但 Beancount 对此问题持明确立场:不提供任何触发网络访问的代码,也不支持任何外部格式作为输入。在 Beancount 中,您需自行集成价格更新,例如通过自己的脚本,并可能通过 include 文件导入数据。(当然,如果您不喜欢这种方式,也可以编写自己的插件模块来获取实时价格。)
语言语法
Beancount 的语法比 Ledger 的更简单,也更为严格。在 2.0 版本中,Beancount 的语法被重新设计,以便通过文法轻松指定给解析器生成器。词法单元被简化,以确保词法分析无歧义。例如:
-
货币代码必须全部使用大写字母(允许使用数字及某些特殊字符,如“_”或“-”)。不支持货币符号(如 $ 或 €)。(另一方面,Ledger 中包含数字的货币需要加引号。)
-
账户名称不允许包含空格(但可以使用连字符),且必须至少包含两个由冒号分隔的组成部分。
-
描述字符串必须加引号,例如:“AMEX PMNT”。不再支持自由格式的文本作为字符串。
-
日期仅支持 ISO8601 格式,即 “YYYY-MM-DD”。
-
标签必须以 “#” 开头,链接必须以 “^” 开头。
-
除了标签栈之外,所有上下文信息均已移除。例如,不再支持账户别名,也不再有 Ledger 中的 “apply” 概念(参见 “apply root” 和 “apply tag”)。这要求输入更冗长——必须使用完整账户名称——因此假设您已在编辑器中启用了账户名称自动补全功能。
作为副作用,这些更改使输入语法看起来更像一种编程语言。这些限制可能会让部分用户感到不便,但总体而言,它们简化了记账文件内容的解析任务,这种简洁性将有助于人们更轻松地用其他语言编写解析器。(在解析缩进方面仍存在一些细微问题,因为缩进在语法中具有意义,但通过构建自定义词法分析器,应在所有场景中都易于解决。)
由于其更宽松、更用户友好的语法,Ledger 使用的是自定义解析器。如果您需要从其他语言解析其内容,最佳方法可能是为其源代码创建绑定,或使用它将记账文件导出为 XML,然后解析 XML(这种方法效果良好)。我推测 Ledger 的下一个版本可能会重新审视解析方法,因为使用解析器生成器能为实验提供更大的自由度。
顺序无关性
Beancount 保证输入文件中指令的顺序不会影响其计算结果。您可以根据自己的便利自由组织和重新排列任何声明,而无需担心软件如何进行计算。甚至声明账户的指令(“Open”)也不必在账户被使用之前出现。所有指令都会先被解析,然后在任何计算或验证之前进行稳定的排序。这也使得合并多个文件变得非常简单(您只需将文件连接起来即可)。
相比之下,Ledger 在解析输入文件时会立即处理其过账金额。从实现角度来看,这种方法的优势在于只需单次遍历即可检查所有断言和余额,而 Beancount 则需要对整个指令列表进行多次遍历(不过,这在 Beancount 中并不是大问题,因为即使现实中数量庞大的交易记录,对我们的计算机而言也相对较小;Beancount 的大部分处理时间都消耗在解析和数值计算上)。
Ledger 计算方法的一个不幸副作用是,用户必须谨慎处理交易在文件中的出现顺序。在编辑大型输入文件时,这种依赖关系可能非常危险且难以理解。这种差异在余额断言中尤为明显。Ledger 的余额断言附加在交易指令的过账项上,并按文件顺序计算(我称之为“文件断言”)。而 Beancount 的余额断言是独立的指令,无论其在文件中的位置如何,都会在声明日期的起始时刻生效(我称之为“带日期断言”)。我认为带日期断言更有用且更少出错,因为它们不依赖声明的顺序。另一方面,Ledger 风格的文件断言天然支持对日内余额进行检查,而无需在交易中指定时间,这一点是带日期断言无法实现的。
因此,已提出一项建议,考虑在 Beancount 中实现文件断言(作为带日期断言的补充)。这可能会以插件形式实现。Ledger 不支持带日期断言。
账户类型
Beancount 的账户必须属于以下五类之一:资产、负债、收入、支出和权益。Ledger 的账户则不受此限制,您可以自由定义任意根账户,且无需将账户归类为上述类别之一。这反映了 Ledger 更宽松的设计理念:其目标是成为一个通用的“计算器”,适用于您想计算的任何内容。系统不会强制或使用任何账户类型。
根据我的经验,我从未遇到过无法将我的任何一个账户归入这些类别中的情况。对于一些更特殊的商品,例如“可用于 IRA 供款的美元”,可能需要一些想象力来判断哪个账户属于哪一类,但其中是有逻辑可循的:如果账户的绝对数值是我们关心的,那么它属于资产或负债账户;如果我们只关心过渡性数值,或某个时间段内累计的数值,那么它应属于收入或支出账户。如果符号为正,则应为资产或支出账户;反之,如果符号为负,则应为负债或收入账户。权益账户几乎从不显式使用,而是由 Beancount 自身用于将期初余额、留存收益和净收入转移至特定报告期(您选择的任何期间)的资产负债表中。这一原则使得确定账户类型变得非常简单。我拥有 2008 年至 2014 年的数据,并且能够用这五类账户完整地表示我所有需要记录的内容。此外,我认为要求用户以这种方式对账户进行分类并不会造成任何限制,只是需要一些前瞻性思考。
要求这些账户类型的原因在于,它使我们能够根据其类型执行逻辑操作。我们可以隔离收入和支出账户,从而生成损益表并计算单一的净收入值。然后,我们可以将留存收益(本期之前产生的收入)和净收入(本期内的收入)转入权益账户,并编制资产负债表。我们还可以生成持仓清单,自动排除收入和支出项目,以计算净资产、各账户价值等。此外,我们可以利用账户类型识别一组账户的外部资金流动,并以可与目标配置的市场回报相比较的方式计算这些账户的投资回报率(注意:尚未集成,但已原型化并完成规范,功能可用)。总而言之,账户类型是一个有用的属性,因此我们强制要求您为每个账户选择一个类型。
账户类型缺失可能也是 Ledger 仅提供试算平衡表,而不提供资产负债表或损益表的原因。其优势在于账户命名结构看似更宽松、更宽松。但请注意,要求账户类型本身并不会在两个系统之间造成任何计算差异——您仍可以在这些账户中累积任何您希望的“豆子”,其通用性并未降低。类型只是 Beancount 报告系统所利用的额外信息。
交易必须平衡
Beancount 的交易必须保持平衡,这是硬性规定。我对此绝不妥协,没有任何例外。这样做的好处是,任何一组交易的分录余额总和始终精确为零(我会对此进行检查)。
Ledger 允许用户使用两种特殊类型的分录:
第二种情况可以被证明等同于两个交易:一个包含所有常规条目的交易,以及另一个仅包含平衡的虚拟条目的交易(这不会造成任何问题)。第一种情况才是问题所在:初学者在试图解决会计问题时,通常会退而使用虚拟条目作为逃避手段,而不是用复式记账法来建模他们的实际问题。显然,大多数 CLI 会计系统的使用者是计算机科学家,而非会计专业人士。在我们所有人学习如何设计会计科目表并填充账簿的过程中,难免会犯错。很多时候,如何解决这些问题并不明显;这仅仅需要经验。
但事实上,在八年使用过程中,我从未遇到过一个真正必须使用虚拟条目的案例。Beancount 的第一个版本曾支持虚拟条目,但随着时间推移,我已逐步移除了所有这些功能。我总能找到更好的科目结构,或使用一种虚构货币来追踪我需要追踪的内容。而且,这始终带来了更优的解决方案,并常常产生意想不到甚至优雅的附带效果。
但这些系统必须易于使用,那么我们该如何解决这个问题?邮件列表是一个很好的起点,可以提出问题,人们会分享他们如何解决类似问题的经验(实际上,所谓的‘会计问题’本身并不多)。我目前正在将自己解决会计问题的所有方案整理记录在 Beancount 食谱 中,涵盖我至今所学的一切;这仍是一个进行中的工作。我希望这份不断演进的文档能成为帮助他人找到符合复式记账框架解决方案的实用参考,并提供大量示例,作为他人复制和适配自身数据的模板。
正如 Ledger 的作者所说:
“If people don't want to use them [virtual accounts], that's fine. But Ledger is not an accounting tool; it's a tool that may be used to do accounting. As such, I believe virtual accounts serve a role that others with non-accounting problems may wish to fill.”
我恭敬地持不同意见。因此,Beancount 采取了更激进的立场,明确不支持虚拟条目。如果你强烈认为自己需要它们,你应该使用 Ledger。
数字与运算精度
Beancount、Ledger 和 HLedger 在内部表示数字的方式以及处理交易条目余额检查精度方面均有所不同。
首先,关于数字的表示方式:Ledger 使用有理数 以期在数学运算后保持数字的完全精度。这种方法有效,但我认为这或许并非最恰当的选择。绝大多数运算场景涉及将数量与价格或成本转换为账户变动的总价值(例如:数量 × 成本 = 总成本)。我们在表示交易信息时的任务,是复现主要发生在金融机构中的操作。这些操作始终涉及对数量和货币的四舍五入(银行确实采用随机舍入),而从这些机构以及政府的角度来看,正确的数字正是这些经过四舍五入的数字本身。这并非数学纯粹性的问题,而是实用性的问题,我们的系统应当效仿银行的做法。因此,我认为我们应当始终将四舍五入后的数字记入账户。使用有理数在这一点上并非限制,但我们必须谨慎地在关键处存储四舍五入后的数值。我认为 Ledger 的实现方式是尽可能保留原始精度。
Beancount 选择使用 十进制数 来存储从输入中解析出的数字,并保持其原始书写精度。这种方法与使用有理数存在同样的问题:十进制数之间的数学运算结果目前仍以完整精度存储(尽管是以十进制形式)。诚然,我尚未在必要处显式应用量化操作,而这才是正确的做法。我们需要设计一种方案,以自动推断运算后对数字进行量化所需的合适精度。十进制表示为运算后自然地进行舍入提供了便利,因此这是一个合适的选择,许多实现甚至提供了用于设定精度的上下文环境。还需注意的是,永远无需以无限精度存储数字:金融机构自身也从不这样做。
Hledger(自 2014 年起)在内部以 十进制数 存储数字,允许“无限”位数的整数部分和最多 255 位小数部分。
其次,在检查交易的分录是否平衡至零时,这三个系统都需要允许一定程度的容差。这一点在考虑文本文件中输入数字所隐含的有限小数表示时显而易见。例如,如果你要将一个数量与一个成本相乘,假设两者均以两位小数书写,最终结果可能产生四位小数,而你需要将该结果与通常仅输入两位小数的现金金额进行比较。因此,你必须以某种方式允许一定的宽松度。
这些系统在选择容差方式上有所不同:
-
Ledger 试图通过使用最近解析的上下文(按文件顺序)自动推导其余额检查所用的精度。所使用的精度是针对当前商品最近解析的数值精度。这可能存在问题:它可能导致交易之间产生难以调试的非预期副作用。
-
另一方面,Hledger 使用每种商品的全局显示精度。系统首先处理整个文件,检测每种商品的推断或配置的显示精度;然后,若交易总和在以这些精度显示时为零,则视为平衡。(未来 Hledger 可能仅根据当前日记条目推断的局部精度进行平衡,以提升局部性和鲁棒性。)
-
目前,Beancount 在其余额检查算法中使用一个固定的容差值(任何单位的 0.005)。这过于薄弱,至少应根据商品类型进行调整,甚至应进一步根据商品所使用的特定账户进行调整。最终,这取决于表示特定记账项时所使用的数字位数。我们有一个提案正在推进以解决此问题。
我计划修复此问题:Beancount 最终将通过一种完全局部于每笔交易的方法来推导其精度,可能辅以全局默认值(此功能仍在开发中——2014 年 10 月)。容差将取最精确数字的一半。该方法将类似于 HLedger 的方式,但针对每笔交易分别计算。这将允许用户通过在输入中添加更多数字来使用任意精度。仅使用小数位来推导精度。不会应用任何由交易引起的全局影响。任何交易都不应影响其他交易的平衡上下文。如果您希望监控,舍入误差可选择性地累积到权益账户中。
至于自动对运算结果进行数量化,我仍需确定一种自动方法来实现这一点。
在交易级别进行过滤
系统之间的另一个区别是,Beancount 不支持在记账项级别进行过滤,而仅支持在交易级别进行过滤。也就是说,当对输入数据应用过滤器时,即使过滤条件作用于记账项,也只会生成一组完整的交易。这样做的目的是确保生成的记账项集合总和精确为零。我们不会忽略某笔交易中的部分记账项。不过,我们仍允许在所有余额计算完成后,从报告中剔除任意账户子集。
Ledger 的过滤功能可在交易级别或记账项级别进行。我认为这令人困惑,我不太理解为何如此设计,也不清楚其实际工作原理。
(请注意,这一点目前并不十分相关,因为我尚未实现自定义的任意过滤功能;目前仅可通过网页界面获取“视图”,但我将很快提供一种简单的逻辑表达式语言,以便像 Ledger 那样对解析后的交易应用自定义过滤器,因为我认为这是一个强大的功能。直到最近,大多数报告仅通过网页界面呈现,而随着 Beancount 实现了控制台报告,这一需求已变得更为迫切。)
扩展机制
Beancount 和 Ledger 都提供了针对其数据集编写脚本的机制。这些机制各有用处,但有所不同。
Ledger 提供:
-
一种自定义表达式语言,用于解释并生成报告
-
一个将内容导出为 XML 的命令
-
一个将内容导出为 LISP 的命令
-
一组 Python 绑定库,可访问其 C++ 数据结构
-
(… 其他?)
(注意:由于其依赖项、使用的 C++ 特性及构建系统,我在普通的 Ubuntu 机器或 Mac OS 计算机上构建 Ledger 非常困难。若要构建带 Python 绑定的版本则更加困难。如果您有耐心、时间和条件,那当然很好;但如果能找到预打包版本,我建议优先使用它。)
Beancount 提供
-
一个原生的 Python 插件系统,允许您指定要导入并调用的 Python 模块列表,以过滤和转换已解析的指令,从而实现新功能;
-
一个简单的加载函数,使您能够访问解析和处理 Beancount 账本后生成的内部数据结构。这也是一个原生的 Python 库。
因此,基本上您必须使用 Python 来扩展 Beancount。我计划提供 XML 和 SQL 输出(由于我使用的数据结构非常简单,实现起来将非常容易)。此外,使用 Beancount 自己的“打印”模块生成的文本,可以保证完全解析回完全相同的数据结构(Beancount 保证其语法和数据结构的双向转换)。
其中一个优势是,插件系统允许您对生成的指令列表进行流式任意转换,这是原型化新功能和更简洁语法的绝佳方式。例如,您可以分配一个特殊标签,以触发对这些带标签交易的任意转换。这些模块无需集成到 Beancount 中:它们可以位于您自己的 PYTHONPATH 的任何位置,因此您可以在不向主项目贡献新功能或修改源代码的情况下进行实验。
(注意:Beancount 是用 Python 3 实现的,因此您可能需要安装一个较新版本才能使用它,例如 Python-3.4(如果您尚未安装)。目前,Python 3 已经相当普及,因此我认为这不是问题,但您可能正在使用较旧的操作系统。)
通过插件实现自动交易
Ledger 提供了一种特殊语法,可根据某些匹配条件自动在现有交易中插入分录。该语法允许用户访问分录的某些数据,例如金额和账户名称。该语法还专门支持应用交易和分录的“标签”。
Beancount 通过其插件扩展机制,允许您实现相同甚至更多的功能。您编写的插件能够完全修改、创建或删除解析流程中任何对象及其属性,从而实现您所需的任何自动化和汇总操作。这不需要特殊语法——您只需使用 Python 编写,并可访问其所有功能,同时可以基于现有语法叠加自动化功能。一些示例请参见 beancount.plugins.*。
然而,对于提供一种快速简便的方法来指定最常见的仅添加分录的情况,仍存在一定的合理性。我目前尚未完全确信,但 Beancount 最终可能会支持这一功能(如果您希望,现在就可以在插件文件中轻松原型化此功能)。
不支持时间或生效日期
Beancount 不表示交易的日内时间,其最小粒度为一天。Ledger 允许您将交易时间精确到秒。我选择限制其范围以追求简洁性,同时也认为支持日内操作的使用场景非常有限。
请注意,虽然 Beancount 的最小时间粒度为一天,但在排序指令时,它会保持同一天内所有交易的相对顺序,因此可以在 Beancount 中表示同一天发生的多笔交易,并仍能正确进行库存记账。但我认为,如果你进行日内交易,你需要一个更专业的系统来计算日内收益、进行技术分析以及日内盈亏计算。Beancount 并不适合这些用途(如果这是其目标,还需要大量其他功能)。
Ledger 也支持有效日期,这本质上是交易的替代日期。其报告功能允许用户使用主日期或替代日期。我曾为 Beancount 实现过此功能,但后来移除了它,主要原因是我希望避免引入报告选项;而为了处理两个日期(例如交易日期和结算日期),我希望确保在任何时间点所有交易都能保持平衡。此外,我本人也极少使用该功能,这表明它可能并无实际价值。若允许不同时间点的分录发生,会导致不平衡,或者我必须设计一种涉及“暂存”或“中转”账户的解决方案。我最终选择直接移除该功能:在八年数据中,我始终能够通过调整日期使所有账目平衡。这并不是一个大问题。
请注意,处理这些分拆交易并将其合并的功能将被支持;相关提案正在推进中。
文档
Beancount 支持将文档目录层次结构与账本的会计科目表集成。你可以提供一个目录路径,Beancount 将自动查找并为那些位于与账户名称相对应目录中、且文件名以日期开头的文件创建相应的 Document 指令,并将这些文档关联到对应账户。此外,通过少量配置,bean-file 工具可自动将下载的文档归档到此类目录结构中。
Ledger 将文档绑定到交易的方式是通过通用元数据。用户可以为交易附加任意键值对,其中可以是文件名。除此之外,Ledger 并无专门的文档组织支持。
更简单且更严格
最后,Beancount 的输入语法通常比 Ledger 更简单。命令行选项极少——这是有意为之,我希望将所有输入都限定在文件内部——且指令语法更加统一:所有交易均以日期和关键字开头。如果你重视简洁性,你可能会更倾向于使用 Beancount。我认为 Ledger 提供的选项过多,令人望而生畏,我也无法声称自己完全理解它们之间所有可能的交互方式。如果你对此并不在意,你可能会更倾向于使用 Ledger。
Beancount 也比 Ledger 更严格。某些类型的 Beancount 输入是无效的。任何账户中的交易都必须先有开户指令来初始化该账户(尽管部分此类限制可通过可选插件放宽)。如果你维护一个 Beancount 账本,你可能需要对其进行规范化以修正报告中常见的若干错误。我认为这是好事:它能检测许多潜在问题,并对输入施加一系列严格约束,从而让我们在处理指令流时能做出合理的假设。如果你不关心精确性或发现潜在错误,Ledger 会允许你更宽松地处理;但如果你希望获得精确无误的交易记录,Beancount 在输入验证方面提供了更多支持。
网页界面
Beancount 内置了网页界面,同时还有一个名为 Fava 的外部项目,在此基础上进行了显著增强。这是浏览报告的默认方式。我认为 HLedger 也具备网页界面。
缺失功能
Beancount 通常力求 最小化所提供的功能数量。这与 Ledger 的实现形成对比,后者为探索复式记账法而添加了大量功能,选项繁多。这反映了两种不同的设计理念:我认为应识别出一小部分核心必要功能,只有当我们能最小化并移除任何非绝对必需的功能时,才能取得真正的进步。我的目标是提供尽可能精简的功能内核,足以支持完整的记账活动,并允许用户扩展系统以自动化重复性语法。
但以下列出了一些 Beancount 目前不支持、而 Ledger 支持的功能,我认为这些功能将来值得加入。以下列表未必完整。
控制台输出
Beancount 的原始实现专注于为其所有内容提供网页视图。在 2.0 重写期间,我开始实现一些控制台/文本输出,主要是因为我希望报告能够导出并与其他用户共享。我已实现了试算平衡视图(类似 Ledger 的 "bal" 报告),但目前尚未实现日记账视图。
另一方面,Ledger 一直专注于控制台报告。
在初始发布后,我会第一时间为 Beancount 的所有报告增加文本格式输出功能,因为我越来越喜欢文本报告。请使用 bean-query --list-formats 查看当前状态。
过滤语言
Beancount 目前尚无过滤语言。直到最近,其网页界面仍是浏览报告和探索账本内容的主要方式,并通过“视图”提供有限的交易子集,例如按年份、按标签等。拥有过滤语言尤其能减少对子账户的依赖。我希望简化我的科目表,因此需要这一功能。
我正在开发一种简单的逻辑表达式语言,用于对 Beancount 交易集进行任意过滤。该功能实现起来很简单,且优先级很高。
无元数据
Beancount 目前不支持元数据。Ledger 用户经常大量使用元数据。这已被认定为一项强大功能,并且已经实现了原型。元数据将支持任何指令类型以及任何分录。每个对象都将附加一个键值对字典。支持的值包括字符串、日期、数字、货币和金额。
到目前为止,计划是故意限制 Beancount 自身代码不作任何特定的元数据使用。元数据将严格仅供用户插件和自定义脚本使用。
无算术表达式
Beancount 不支持在允许输入数字的位置进行任意表达式求值,而 Ledger 支持。我至今尚未使用过此功能,但也没有特别反对添加它,只是因为我个人不需要,所以尚未实现。
我认为实现起来会很简单且风险极低,只需对解析器做一点修改,而数值回调已存在。我认为它有许多正当的用途。
有限的 Unicode 支持
Beancount 仅在字符串中(即引号内的输入)支持 UTF8 或其他编码。例如,您可以输入包含非 ASCII 字符的收款方和摘要,但不能用于账户名称(账户名称不在引号内)。Ledger 则在整个文件中支持其他编码。
Beancount 缺乏更通用编码支持的原因在于当前词法分析工具的限制。我使用 GNU flex 实现词法分析器,而它不支持任意编码。我只需要 编写更好的词法分析器 并使其与 Bison 配合使用,这并非困难任务。我最终将手动编写自己的词法分析器——这还有其他优势——并使其支持 Unicode(Python 3 完全支持 Unicode,因此只需修改词法分析器,它只是一个简单的编译单元)。这是一个相对简单且独立的任务。
不支持预测或周期性交易
Beancount 目前不支持自动生成周期性交易以用于预测,但提供了一个插件,实现了简单的形式作为插件工作示例(参见 beancount.plugins.forecast)。Ledger 支持 周期性交易生成。
我最终希望将此功能加入 Beancount 核心,但在实现前我希望明确定义其语义。更新账本本质上是复制和复制发生在别处的交易数据。我认为现实中的常规交易并不那么“规律”;根据我的经验,实际交易总存在一些细微变化,使得通过生成器自动创建一系列交易并让用户完全放弃逐条更新变得不可能。在我看来,它的真正用途是生成暂定的未来交易。我强烈认为这些交易不应跨越已对账的历史记录,且已对账的历史记录应取代任何自动生成的交易。
我对实现此功能有一些相当完整的思路,但目前我本人并未使用预测功能,因此该功能被搁置了。理论上,你可以使用 Ledger 的周期性交易进行预测,但要精确地反映你的账户历史,每次向账户追加新交易并替换预测交易时,你可能都需要调整这些交易的起始日期。我认为 Ledger 中自动化交易的当前语义除了近似模拟账户内容外,并没有太大用处。
(与此同时,鉴于 Beancount 插件扩展的便捷性,我建议你先自行尝试实现预测交易;如果我们能提炼出一种通用的创建方式,我愿意将其合并到主代码中。)