管理数值精度是复式记账的基石。在数字簿记中,尤其是处理多种货币、股票价格和小数份额时,微小的舍入差异会很快导致令人沮丧的平衡错误。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 USDBeancount 按如下方式推断容差:
- 对于
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 会计算每个过账的“权重”。计算规则如下:
- 简单金额:如果一个过账只有金额(例如,
Assets:Cash -100.00 USD),其权重就是该确切金额。 - 价格过账:如果一个过账有单价(例如,
10 FUND @ 38.46 USD),其权重为amount × price。 - 单价成本:单花括号
{...}表示 一个单位 的成本,因此10 FUND {384.61 USD}的权重为10 × 384.61 = 3,846.10 USD,而不是384.61 USD。 - 总成本:双花括号
{{...}}表示 整个过账 的成本,因此10 FUND {{384.61 USD}}的权重为384.61 USD。在存储该批次时,Beancount 会将其转换为每单位成本38.461 USD。 - 成本与价格:如果一个过账同时有成本和单价(例如,
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。
精度推断规则
自动推断系统遵循一些特定规则:
-
数字格式
- 整数金额(例如
10 USD)不参与 精度推断。 - 一位小数是金额能暗示的最粗略精度:
0.1 × 0.5 = 0.05单位。如果需要比这更宽松的容差,你需要设置tolerance_multiplier或使用下文提到的货币特定默认值。 - 成本和价格(例如
{37.61 USD})默认排除 在容差推断之外。只有过账的主要金额会被使用。 - 如果同一货币的不同过账具有不同精度(例如
-10.10 USD和5.123 USD),Beancount 使用最粗略(最大) 的容差。在这个例子中,将基于-10.10 USD推断,产生的容差为 $0.005$USD。
- 整数金额(例如
-
默认值处理 如果一笔交易中没有带小数位的数字可供推断,你可以设置全局或特定货币的默认容差。
; 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" -
容差乘数
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 USDinferred_tolerance_multiplier是旧选项名,设置它也会生效,但会报告Renamed to 'tolerance_multiplier'.的加载错误。 -
基于成本的推断 虽然成本通常被排除在容差推断之外,但你可以指示 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在此交易中,。交易失衡量为 $-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'——也没有任何设置会量化存储的金额。
-
存储始终是精确的。 写入
53.82135 USD,账本就会保存53.82135 USD,无论你的容差设置如何。容差只决定交易是否被接受;它从不修改任何数字。 -
显示是独立的设置。
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- 舍入是你需要自己编写的过账。 如果你希望从计算中移除残差,请在源文件中舍入金额,并明确记录差额,如同上面的三过账示例。
实现细节
以下几点技术细节有助于阐明 Beancount 如何实现这种可靠性。
-
数字表示:Beancount 使用 Python 的
decimal模块,而非浮点数。默认上下文携带 28 位有效数字——这是总位数,而非小数点后的位数——这避免了浮点数常见的二进制表示错误。 -
DisplayContext 类:这个内部类负责所有用于显示目的的数字格式化。它会根据你文件中的数字推断每种货币的精度,除非
display_precision固定了精度,并且可以格式化输出,实现列对齐和千位分隔符。 -
精度 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断言,使用显式容差(~)。 - 通过真实的第三个过账,将残差记入专门账户,以便你可以报告其发生频率。
- 如果你经常处理具有不同惯例的货币(例如,日元没有小数),请考虑设置特定货币的默认值。
迁移策略
在对现有、混乱的账本应用这些概念时:
- 从宽松的全局容差(例如,
*:0.05)和较高的tolerance_multiplier开始,使文件能够通过验证。 - 逐步收紧容差,并修复出现的错误。
- 在有问题交易的金额中添加显式小数位,让推断机制发挥作用。
- 监控舍入账户的余额。如果余额很大或增长迅速,可能表明存在需要调查的系统性问题。