13.精度控制与容差管理
在 Beancount 的世界里,数字是神圣不可侵犯的。与许多其他软件不同,Beancount 内部使用十进制(Decimal)而非浮点数来存储所有金额,这意味着它在数学上是精确的,不会出现 0.1 + 0.2 != 0.3 这种计算机科学里的经典尴尬。然而,现实世界的会计充满了“差不多”的情况:银行转账、股票交易、货币兑换,这些操作往往会产生微小的舍入误差。
这就是“容差”(Tolerance)存在的意义。Beancount 必须允许这些微小的误差存在,否则你的账本将因为几分钱甚至几厘钱的差异而无法通过校验。但同时,它又必须足够严格,能够捕捉到真正错误的记账。
在早期的 Beancount 版本中,容差是固定的。比如,交易平衡检查的默认容差是 0.005,而余额断言(Balance Assertion)和自动补平(Pad)的默认容差是 0.015。这种“一刀切”的方法简单粗暴,但并不总是适用。例如,对于日元(JPY)这种通常没有小数位的货币,0.005 的容差太小了;而对于某些加密货币或高精度的外汇交易,这个容差又太大了。
为了解决这个问题,现代 Beancount 采用了一种基于本地上下文(Local Context)的自动容差推断机制。这意味着,对于每一笔交易,Beancount 都会独立地分析其中出现的数字,来决定这一笔交易应该使用多大的容差。
这个推断过程遵循以下核心逻辑:
- 检查交易中的“简单金额”:Beancount 会查看交易中那些没有成本(Cost)或价格(Price)的金额。这些是用户直接输入的、最原始的数字。
- 寻找最小精度单位:它会找出这些数字中,小数点后位数最多的那个。例如,在一笔交易中,如果出现了
10.22626和384.61,那么最小精度单位就是0.00001(来自 10.22626)。 - 计算容差值:容差通常被设定为最小精度单位的一半。这在统计学上假设了舍入误差不会超过半个最小单位。因此,对于上面的例子,容差就是
0.000005。 - 货币独立性:这个过程是按货币进行的。如果一笔交易涉及多种货币,每种货币都有自己的容差。
- 忽略成本和价格:在推断容差时,成本和价格(例如
{37.61 USD}或@ 0.93324 CHF)会被忽略。因为这些数字通常精度更高,如果把它们也算进去,会导致容差被不必要地缩小,从而导致本应平衡的交易报错。
这种“本地化”的推断方式非常优雅,因为它确保了每一笔交易的平衡检查都是独立的,不会受到账本中其他交易的影响,从而避免了 Ledger 那种基于文件顺序的“上下文污染”问题。
价格与成本的容差规则
在处理带有成本或价格的交易时,容差的计算会变得更加微妙。我们需要区分“记账金额”和“权重金额”。
考虑一个购买股票的例子:
2013-04-03 * "Buy Mutual Fund"
Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD}
Assets:US:Vanguard:Cash -384.61 USD
在这个交易中:
- 记账金额是
10.22626 RGAGX和-384.61 USD。 - 权重金额(即换算成统一货币的价值)是
10.22626 * 37.61 = 384.6096386 USD。
Beancount 在进行平衡检查时,比较的是所有posting的权重金额之和。在这个例子中,权重金额的和是 384.6096386 - 384.61 = -0.0003614 USD。
那么,容差是多少呢?
RGAGX的容差是0.000005(来自10.22626)。USD的容差是0.005(来自-384.61)。
虽然 RGAGX 有容差,但在这个交易中,最终的残差是以 USD 结算的。因此,我们只关心 USD 的容差。|-0.0003614| < 0.005,所以交易平衡。
这里的关键规则是:价格和成本本身不参与容差的推断。它们只是用来计算权重。这样做是为了避免因为价格的高精度而“污染”了整个交易的容差要求。毕竟,银行报出的汇率可能有 5 位小数,但这并不意味着你账户里的美元余额需要精确到小数点后 5 位。
整数金额的容差处理
整数金额在容差计算中是一个特例。默认情况下,整数金额不贡献任何容差。
例如,如果你写:
2013-04-03 * "Buy something"
Assets:Investments 10.21005 RGAGX {37.61 USD}
Assets:Cash -384 USD
这个交易会失败。为什么?因为 -384 USD 是一个整数,它没有小数部分,所以 Beancount 无法从中推断出任何精度。对于 USD 这种货币,如果没有其他带小数的金额来提供精度信息,其默认容差就是零。计算出的残差(约 -0.00036 USD)不为零,因此交易不平衡。
这在实践中可能会带来不便。为了解决这个问题,有两种方法:
- 显式声明容差:在配置文件中为该货币设置一个默认的推断容差(我们稍后会讲到)。
- 输入带小数的金额:将
-384改为-384.00。这样,0.00就为USD提供了精度信息,容差会被推断为0.005,交易就能通过了。
这个设计背后的哲学是:Beancount 倾向于精确。如果你输入一个整数,它就假设你想要一个精确的匹配。如果你想允许舍入误差,就应该在输入时明确表示出来。
显式容差声明
虽然自动推断非常方便,但有时你可能需要手动控制容差。Beancount 提供了配置选项来实现这一点,主要用于设置“后备”容差和调整推断行为。
1. inferred_tolerance_default
这个选项用于设置当无法自动推断容差时,应该使用的默认值。它的格式是 货币:容差。
例如,如果你经常处理日元(JPY),并且希望在无法推断时使用 1 日元的容差:
option "inferred_tolerance_default" "JPY:1"
或者,为所有货币设置一个通用的后备容差:
option "inferred_tolerance_default" "*:0.01"
这个选项只在无法自动推断时生效。如果你的交易中已经包含了可以推断精度的数字,这个选项将被忽略。
2. inferred_tolerance_multiplier
默认情况下,容差是最小精度单位的一半(即乘数为 0.5)。这个选项允许你改变这个乘数。
例如,如果你发现某个机构的舍入行为特别“粗暴”,误差有时会超过半个最小单位,你可以稍微放宽限制:
option "inferred_tolerance_multiplier" "0.6"
这会将容差从 0.005 放宽到 0.006(对于两位小数的货币)。
3. infer_tolerance_from_cost
这是一个高级选项,它允许从成本或价格中推断额外的容差。它的想法是,如果一个posting带有成本,那么成本乘以数量的舍入误差也应该被考虑进去。
例如,对于 2.345 RGAGX {45.00 USD},它会计算 0.001 * 45.00 * multiplier,并将这个值加到 USD 的容差中。
option "infer_tolerance_from_cost" "True"
这个功能默认是关闭的,因为它可能会导致容差过大,掩盖真正的错误。但在某些复杂的交易场景下可能有用。
余额断言的容差
余额断言(Balance Assertion)是验证账本数据准确性的重要工具。它的容差逻辑与交易平衡略有不同,但同样遵循“本地推断”原则。
当你写下:
2015-05-08 balance Assets:Investments:RGAGX 4.271 RGAGX
你并不是在断言余额恰好是 4.271。你是在断言余额在 4.271 的某个微小范围内。这个范围就是容差。
Beancount 会查看断言中的数字 4.271,推断出其最小精度单位是 0.001,然后使用一半作为容差,即 0.0005。因此,只要账户余额在 4.2705 到 4.2715 之间,断言就会通过。
如果你想进行一个更宽松的断言,比如允许 4.26 到 4.28 之间的余额,你可以直接写:
2015-05-08 balance Assets:Investments:RGAGX 4.27 RGAGX
这里,最小精度单位是 0.01,容差是 0.005。
显式容差
对于某些特殊情况(比如 Vanguard 的某些账户,其舍入规则很奇怪),你可能需要一个非常规的容差。Beancount 允许在断言中直接指定容差:
2015-05-08 balance Assets:Investments:RGAGX 4.271 ~ 0.01 RGAGX
这里的 ~ 0.01 覆盖了自动推断的容差,明确告诉 Beancount 使用 0.01 作为允许的误差范围。
金额插值的精度
Beancount 允许你省略一个 posting 的金额,让系统自动计算并填充,这被称为“插值”(Interpolation)。这个填充的金额也需要进行舍入,以符合现实世界的会计实践。
例如:
2014-05-06 * "Buy mutual fund"
Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD}
Assets:Investments:Cash
这里,第二个 posting 的金额会被自动计算为 -227.2067 USD。但现实中,你不会收到或支付这个精确的金额。银行会把它舍入到最接近的分,即 -227.21 USD。
Beancount 的插值精度规则如下:
- 推断精度:它会尝试从同一交易的其他 posting 中推断精度。例如,如果交易中还有一个
Expenses:Commissions 9.95 USD的 posting,那么USD的精度就被确定为两位小数,插值金额会被舍入为-227.21 USD。 - 使用默认容差:如果无法从交易中推断精度(如上例),它会查看
inferred_tolerance_default配置。如果配置了USD:0.01,那么插值金额会被舍入到百分位。 - 无舍入:如果既无法推断,也没有配置默认值,插值金额将保持其计算出的全部精度。这可能会导致账本中出现很多位小数,通常不是我们想要的结果。
舍入误差的处理
即使有了容差机制,我们仍然面临一个哲学问题:交易在逻辑上是平衡的,但在数学上可能有微小的残差。有些人对此感到不安,希望账本在数学上也是完美的。
Beancount 提供了一个优雅的解决方案:将舍入误差显式地记录下来。
通过配置 account_rounding 选项,你可以指定一个账户来专门存放这些“找零”:
option "account_rounding" "Equity:RoundingError"
启用后,对于任何有残差的交易,Beancount 会自动添加一个 posting 到这个账户,使交易在数学上精确平衡。
例如,一个本应有 -0.00036 USD 残差的交易,会变成:
2014-05-06 * "Buy mutual fund"
Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD}
Assets:Investments:Cash -227.2067 USD
Equity:RoundingError 0.0003 USD
这个功能是可选的。对于大多数用户来说,接受容差范围内的逻辑平衡就足够了。但对于那些追求极致精确,或者需要审计追踪所有微小差异的场景,这是一个非常强大的工具。
显示精度的舍入
到目前为止,我们讨论的都是 Beancount 内部如何存储和计算数字。但最终,我们需要通过报告和界面看到这些数字。这就是“显示精度”(Display Precision)的作用。
你肯定见过这样的数字:134.2374736427639678237437 USD。这在报告中毫无意义,只会造成混乱。通常,我们希望看到 134.24 USD。
Beancount 的显示系统(DisplayContext)会自动处理这个问题。它会分析整个账本,为每种货币确定一个“自然”的显示精度。
- 推断规则:它会查看你在账本中输入的数字。如果你输入了大量的
USD金额,且都是两位小数,它就会将USD的默认显示精度设置为 2。 - 成本与价格:对于成本和价格,它会使用在账本中看到的最大精度。例如,如果你输入过
53.21077 USD作为成本,即使你后来只输入10.00 USD的交易,成本的显示也会保留足够的精度。 - 手动配置:从 2025 年开始,Beancount 引入了
display_precision选项,允许你手动覆盖这种自动推断:option "display_precision" "USD:0.01" option "display_precision" "JPY:1"
需要强调的是,显示精度不影响内部计算。无论显示成什么样,Beancount 内部存储的数字都是精确的。显示舍入只发生在生成报告和渲染界面的时候。
总结
Beancount 的精度与容差管理是一个复杂但设计精良的系统。它在“数学精确性”和“现实世界实用性”之间取得了完美的平衡。
- 核心思想:基于本地上下文(每个交易独立)自动推断容差。
- 关键机制:从用户输入的简单金额中推断最小精度单位,并取其一半作为容差。
- 灵活性:提供了配置选项来处理特殊情况,如默认容差、乘数调整和显式容差声明。
- 可审计性:通过
account_rounding选项,可以将微小的舍入误差显式化,实现数学上的完美平衡。 - 用户友好:通过智能的显示精度系统,确保报告清晰易读。
掌握了这套逻辑,你就能理解为什么有些交易会报错,以及如何通过调整输入或配置来解决它们,从而让你的纯文本会计之旅更加顺畅。