跳转到主要内容

精度与容差

了解 Beancount 的精度与容差系统如何帮助维持复式记账的平衡,尤其是在处理涉及多种货币和小数份额的复杂交易时。

管理数值精度是复式记账的基石。在数字簿记中,尤其是处理多种货币、股票价格和小数份额时,微小的舍入差异会很快导致令人沮丧的平衡错误。Beancount 提供了一套精妙而直观的系统来处理精度和设置可接受的容差。本指南将带你了解其工作原理。⚙️

本页所有数字均经过 Beancount 3.2.3 验证,包括边界情况:每个示例都说明了哪个余数是可接受的,哪个只差一位数就无法通过。

核心精度概念

Beancount 的首要目标是确保每笔交易都能平衡至零。然而,涉及价格或成本的计算 产生的结果往往包含比实际可记录更多的十进制位数。容差系统允许存在微小、可接受的失衡。

自动容差推断

默认情况下,Beancount 自动推断每笔交易所需的容差。这种推断是针对每笔交易单独处理,并为涉及的每种货币分别计算的。

规则是一次乘法:某种货币的容差等于该货币在过账金额中出现的最小位数单位,乘以 tolerance_multiplier 选项的值(默认值为 0.5)。在该默认值下,容差即为最小有效位的一半

例如,考虑这笔购买:

2013-04-03 * "Buy Fund"
  Assets:Fund     10.22626 FUND {37.61 USD}
  Assets:Cash     -384.61 USD

Beancount 按如下方式推断容差:

  • 对于 FUND 货币,数字 10.22626 有 5 位小数。容差为最小位的一半,即 $0.00001 \div 2 = 0.000005$ FUND
  • 对于 USD 货币,数字 -384.61 有 2 位小数。容差为最小位的一半,即 $0.01 \div 2 = 0.005$ USD

现金部分是用来衡量容差的基准:10.22626 × 37.61 等于 384.6096386,因此这笔交易与零的差额是 0.0003614 USD,在容差范围内,可以正常加载。如果将现金部分四舍五入为 -384.60,差额将变为 0.0096386 USD,超出了 0.005 的容差,Beancount 将报告 Transaction does not balance

交易权重规则

在检查交易是否平衡时,Beancount 会计算每个过账的“权重”。计算规则如下:

  1. 简单金额:如果一个过账只有金额(例如,Assets:Cash -100.00 USD),其权重就是该确切金额。
  2. 价格过账:如果一个过账有单价(例如,10 FUND @ 38.46 USD),其权重为 amount × price
  3. 单价成本:单花括号 {...} 表示 一个单位 的成本,因此 10 FUND {384.61 USD} 的权重为 10 × 384.61 = 3,846.10 USD,而不是 384.61 USD
  4. 总成本:双花括号 {{...}} 表示 整个过账 的成本,因此 10 FUND {{384.61 USD}} 的权重为 384.61 USD。在存储该批次时,Beancount 会将其转换为每单位成本 38.461 USD
  5. 成本与价格:如果一个过账同时有成本和单价(例如,10 FUND {384.61 USD} @ 400.00 USD),仅使用成本 来计算平衡。价格仅用于报告目的,不参与平衡计算。

规则 3 和 4 经常让人花费整个下午去理解,所以这里将它们放在一个可加载的文件中并排展示:

1970-01-01 open Assets:Fund
1970-01-01 open Assets:Cash
 
; Per-unit cost: ten units at 384.61 each, so 3,846.10 USD leaves the
; cash account.
2013-04-03 * "Broker" "Buy at a per-unit cost"
  Assets:Fund     10 FUND {384.61 USD}
  Assets:Cash  -3846.10 USD
 
; Total cost: the braces double and 384.61 USD is the entire purchase.
; The lot is stored at 38.461 USD per unit.
2013-04-04 * "Broker" "Buy at a total cost"
  Assets:Fund      10 FUND {{384.61 USD}}
  Assets:Cash   -384.61 USD

账户最终持有 20 个 FUND,分为两个批次,总成本基础4,230.71 USD

精度推断规则

自动推断系统遵循一些特定规则:

  1. 数字格式

    • 整数金额(例如 10 USD不参与 精度推断。
    • 一位小数是金额能暗示的最粗略精度:0.1 × 0.5 = 0.05 单位。如果需要比这更宽松的容差,你需要设置 tolerance_multiplier 或使用下文提到的货币特定默认值。
    • 成本和价格(例如 {37.61 USD}默认排除 在容差推断之外。只有过账的主要金额会被使用。
    • 如果同一货币的不同过账具有不同精度(例如 -10.10 USD5.123 USD),Beancount 使用最粗略(最大) 的容差。在这个例子中,将基于 -10.10 USD 推断,产生的容差为 $0.005$ USD
  2. 默认值处理 如果一笔交易中没有带小数位的数字可供推断,你可以设置全局或特定货币的默认容差

    ; Sets a default tolerance for all currencies without explicit rules
    option "inferred_tolerance_default" "*:0.001"
     
    ; Sets a specific default tolerance for USD
    option "inferred_tolerance_default" "USD:0.003"
  3. 容差乘数 tolerance_multiplier 选项定义了最小位单位的倍数,该倍数构成容差。其默认值为 0.5,因此设置 1.2 并不会将检查放宽 20%:它只是使推断出的容差变为默认值的 2.4 倍

    option "tolerance_multiplier" "1.2"
     
    1970-01-01 open Assets:Cash
    1970-01-01 open Expenses:Fees
     
    ; The coarsest amount has two decimals, so the tolerance is
    ; 1.2 x 0.01 = 0.012 USD, and this residual of exactly 0.012 passes.
    ; At the default 0.5 the tolerance would be 0.005 and this would fail.
    2024-05-01 * "Bank" "Wire fee"
      Expenses:Fees      100.00 USD
      Assets:Cash       -99.988 USD

    inferred_tolerance_multiplier 是旧选项名,设置它也会生效,但会报告 Renamed to 'tolerance_multiplier'. 的加载错误。

  4. 基于成本的推断 虽然成本通常被排除在容差推断之外,但你可以指示 Beancount 使用它们。当最终金额(例如,现金提款)是交易中最精确的数字时,这会很有帮助。

    option "infer_tolerance_from_cost" "TRUE"

这里是完全使用默认设置,在精确边界上的情况:

1970-01-01 open Assets:Cash
1970-01-01 open Expenses:Fees
 
; Two decimals on the coarsest amount, so the tolerance is
; 0.5 x 0.01 = 0.005 USD. This residual is exactly 0.005 and passes;
; -99.994 would be 0.006 and would fail.
2024-05-01 * "Bank" "Wire fee"
  Expenses:Fees      100.00 USD
  Assets:Cash       -99.995 USD

余额断言

余额断言balance)用于在特定日期验证你账户的余额是否与已知数值匹配。它们也具有关联的容差。

基本格式

balance 断言的容差是根据金额中的小数位数推断的,但其宽松程度是交易内部容差的两倍tolerance_multiplier × 2 × the smallest digit。在默认乘数下,这恰好等于你写下的小数位最后一位的一个单位。

; Asserts the balance is 4.271 RGAGX with a tolerance of +/-0.001
2015-05-08 balance Assets:Fund  4.271 RGAGX
 
; Asserts the balance is 4.27 RGAGX with a tolerance of +/-0.01
2015-05-08 balance Assets:Fund  4.27 RGAGX

该检查是包含性的:差额恰好等于容差仍可通过。对于第二个示例,任何在 $4.26$$4.28$ 之间的余额都能通过检查,而 4.2801 则会失败,并报告 Balance failed for 'Assets:Fund': expected 4.27 RGAGX != accumulated 4.2801 RGAGX (0.0101 too much)

1970-01-01 open Assets:Fund
1970-01-01 open Equity:Opening-Balances
 
1970-01-02 * "Broker" "Opening position"
  Assets:Fund                4.28 RGAGX
  Equity:Opening-Balances   -4.28 RGAGX
 
; 4.28 is 0.01 away from the asserted 4.27, which is the whole tolerance.
2015-05-08 balance Assets:Fund   4.27 RGAGX

显式容差

如果推断出的容差不合适,你可以使用波浪号(~)字符显式指定一个容差。这是 Beancount 中唯一的显式容差语法,并且仅适用于 balance 指令——在交易过账中使用波浪号是语法错误。

1970-01-01 open Assets:Fund
1970-01-01 open Equity:Opening-Balances
 
1970-01-02 * "Broker" "Opening position"
  Assets:Fund                4.281 RGAGX
  Equity:Opening-Balances   -4.281 RGAGX
 
; Asserts the balance is 4.271 RGAGX with a custom tolerance of
; +/-0.01 RGAGX, so anything from 4.261 to 4.281 passes.
2015-05-08 balance Assets:Fund   4.271 ~ 0.01 RGAGX

将持有量提高到 4.2811,同样的断言将因 0.0101 的差额而失败。

舍入管理

由成本和价格计算产生的小额残差是正常现象。Beancount 对这些残差的处理方式比看起来更有限。

舍入误差跟踪

account_rounding 选项指定一个用于吸收残差的账户。它接受完整的账户名称,并完全按照你编写的方式存储——与 equity 账户选项 不同,它不会自动添加 Equity: 前缀。

option "account_rounding" "Equity:Rounding"
 
1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
 
; 1.245 x 43.23 = 53.82135, so this is 0.00135 USD short of balancing.
2013-02-23 * "Broker" "Purchase"
  Assets:Invest     1.245 RGAGX {43.23 USD}
  Assets:Cash      -53.82 USD

在此交易中,1.245×43.23=53.821351.245 \times 43.23 = 53.82135。交易失衡量为 $-0.00135$ USD,在推断出的 0.005 USD 容差范围内,因此可以加载。

在 Beancount 3.2.3 上,不会有任何过账进入 Equity:Rounding 该选项可以被解析并存储,但加载器的任何阶段都不会插入残差过账。因此,该账户余额始终为零,任何超出容差的残差仍然是一个错误,而不会被自动清除:

option "account_rounding" "Equity:Rounding"
 
1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
 
; 0.10135 USD out, far past the 0.005 tolerance. Setting
; account_rounding does not rescue it:
;   Transaction does not balance: (0.10135 USD)
2013-02-23 * "Broker" "Purchase"
  Assets:Invest     1.245 RGAGX {43.23 USD}
  Assets:Cash      -53.72 USD

因此,请将此版本中的 account_rounding 视为无效选项。如果你希望记录残差而不是仅仅容忍它,请自行编写第三个过账:

1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
 
2013-02-23 * "Broker" "Purchase"
  Assets:Invest      1.245 RGAGX {43.23 USD}
  Assets:Cash       -53.82 USD
  Equity:Rounding   -0.00135 USD

该版本交易恰好平衡至零,并且这笔“灰尘”金额会显示在你可以在报表中查看的账户里。

推断数字精度

Beancount 不会对你编写的数字进行舍入。不存在 default_tolerance 选项——它不存在,并会导致加载失败并报错 Invalid option: 'default_tolerance'——也没有任何设置会量化存储的金额。

  1. 存储始终是精确的。 写入 53.82135 USD,账本就会保存 53.82135 USD,无论你的容差设置如何。容差只决定交易是否被接受;它从不修改任何数字。

  2. 显示是独立的设置。 display_precision 决定货币显示时保留多少小数位,但它不会改变存储值或余额检查。

option "display_precision" "USD:0.01"
 
1970-01-01 open Assets:Cash
1970-01-01 open Income:Interest
 
; Rendered as 53.82 USD, stored as 53.82135 USD.
2024-06-30 * "Bank" "Interest"
  Assets:Cash          53.82135 USD
  Income:Interest     -53.82135 USD
  1. 舍入是你需要自己编写的过账。 如果你希望从计算中移除残差,请在源文件中舍入金额,并明确记录差额,如同上面的三过账示例。

实现细节

以下几点技术细节有助于阐明 Beancount 如何实现这种可靠性。

  1. 数字表示:Beancount 使用 Python 的 decimal 模块,而非浮点数。默认上下文携带 28 位有效数字——这是总位数,而非小数点后的位数——这避免了浮点数常见的二进制表示错误。

  2. DisplayContext 类:这个内部类负责所有用于显示目的的数字格式化。它会根据你文件中的数字推断每种货币的精度,除非 display_precision 固定了精度,并且可以格式化输出,实现列对齐和千位分隔符。

  3. 精度 vs. 容差:区分这两个概念至关重要:

    • 精度 涉及数字的_显示格式_(显示多少位小数)。
    • 容差 是验证检查期间用于判断_允许的失衡量_。

最佳实践 ✨

以下是一些在你的账本中管理精度的实用建议。

初始设置

对于大多数新账本,以下是一个稳健的起始配置:

; A floor for currencies that have no decimals to infer from
option "inferred_tolerance_default" "*:0.005"
 
; Leave the multiplier at its 0.5 default unless a real institution
; forces your hand; 1.2 would mean 2.4x the usual tolerance.
option "tolerance_multiplier" "0.5"

故障排除技巧

如果遇到平衡错误:

  • 为某个过账金额添加小数位,以创建更严格、更准确的局部容差推断。
  • 对因可预见的差异而失败的 balance 断言,使用显式容差~)。
  • 通过真实的第三个过账,将残差记入专门账户,以便你可以报告其发生频率。
  • 如果你经常处理具有不同惯例的货币(例如,日元没有小数),请考虑设置特定货币的默认值

迁移策略

在对现有、混乱的账本应用这些概念时:

  1. 宽松的全局容差(例如,*:0.05)和较高的 tolerance_multiplier 开始,使文件能够通过验证。
  2. 逐步收紧容差,并修复出现的错误。
  3. 在有问题交易的金额中添加显式小数位,让推断机制发挥作用。
  4. 监控舍入账户的余额。如果余额很大或增长迅速,可能表明存在需要调查的系统性问题。

来源:https://beancount.io/zh/docs/Basics/precision