目录

07 · interpolate.py:残差、容差与自动补数的量化

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对。 本篇范围beancount/core/interpolate.py(394 行)与 beancount/core/interpolate_test.py(635 行)。 上游依赖convert.get_weightbeancount/core/convert.py:63-106)、Inventoryinventory.py)、Amountdata.Posting/Transaction/Balance/Metanumber.D/ONE/ZERO/MISSINGposition.Cost/CostSpecgetters.get_entry_accountsutils/defdict.py:14-51 (ImmutableDictWithDefault)下游使用者beancount/parser/booking_full.py:171,175,230,239,893,945,1016beancount/ops/validation.py:375-376beancount/ops/balance.py:13(导入 BalanceError);beancount/parser/context.py:110,168,174beancount/plugins/sellgains.py:133beancount/ops/summarize.py:542beancount/parser/printer.py:239

1. 模块解决什么问题

复式记账要求一笔交易所有分录的权重之和为零,但账本里的数字带有各自的小数位,成本与价格换算又会产生长尾小数,"严格等于零"在实际数据上不成立。interpolate.py 定义三件事:残差怎么算(compute_residual)、多大的残差算平(infer_tolerances,从用户写的数字小数位推断,按币种给出容差)、自动补出的数字要不要按容差取整(quantize_with_tolerance)。另外提供把残差记到指定账户的工具(get_residual_postings/fill_residual_posting)和两个余额统计函数(compute_entries_balance/compute_entry_context)。

模块名是历史遗留:真正"填补缺失数字"的插值逻辑曾经在这里(balance_incomplete_postingsCHANGES:3269-3272),2016-10-14 commit 8f14afb6 把它移到 parser/booking_simple.py,2018-03-13 commit 2be15243 连同 SIMPLE 记账法一起删除;现行插值在 beancount/parser/booking_full.py:798 (interpolate_group) 起的函数里,本模块只提供它和最终校验共用的数值规则。

2. 结构一览

名称 位置 作用
MAXIMUM_TOLERANCE = D("0.5") interpolate.py:28-30 由成本/价格推出的容差上限
MAX_TOLERANCE_DIGITS = 5 :33-35 判定"用户手写容差"的系数位数阈值
class BalanceError(NamedTuple) :38-43 source: Metamessage: strentry: Balance;本模块不实例化,只被 ops/balance.py 使用
is_tolerance_user_specified :46-58 系数位数 < 5 即视为用户指定
has_nontrivial_balance :61-69 返回 posting.cost or posting.price,不是布尔值
compute_residual :72-94 权重求和为 Inventory,跳过 __residual__ 分录
infer_tolerances :97-231 按币种推断容差,返回 ImmutableDictWithDefault
AUTOMATIC_META / AUTOMATIC_RESIDUAL / AUTOMATIC_TOLERANCES :234-242 三个 meta 键:__automatic____residual____tolerances__
get_residual_postings / fill_residual_posting :245-286 把残差做成打到 account_rounding 的分录
compute_entries_balance :289-312 按账户前缀、截止日期汇总所有分录
compute_entry_context :315-361 某条目涉及账户在它之前/之后的余额
quantize_with_tolerance :364-394 容差像用户手写时才 quantize

3. 设计点

3.1 三个 meta 标记

# Meta-data field appended to automatically inserted postings.
# (Note: A better name might have been '__interpolated__'.)
AUTOMATIC_META = "__automatic__"

# Meta-data field appended to postings inserted to absorb rounding error.
AUTOMATIC_RESIDUAL = "__residual__"

# Meta-data field added for the tolerances inferred for this entry.
AUTOMATIC_TOLERANCES = "__tolerances__"

interpolate.py:234-242。写入方与读取方分散在三个文件:__automatic__booking_full.py:1013-1016 打在插值补出的 posting 上,infer_tolerances:177)据此跳过,plugins/check_commodity.py:50 把它列入不扫描币种的 meta 键;__residual__get_residual_postings:255)与 __automatic__ 一起写入,compute_residual:90)据此跳过;__tolerances__booking_full.py:238-240 写进交易 meta,值是 tolerances_maxinterpolate_test.py:540-555booking_full_test.py:3499,3520 直接断言它。__automatic__ 标记随 2015-05-17 的容差改版引入(CHANGES:3916-3917);__residual__ 来自 2015-05-20 commit ca3d3b28__tolerances__ 来自 2016-10-14 commit cc82c533

3.2 残差:compute_residual 与 #48

inventory = Inventory()
for posting in postings:
    # Skip auto-postings inserted to absorb the residual (rounding error).
    if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False):
        continue
    # Add to total residual balance.
    inventory.add_amount(convert.get_weight(posting))
return inventory

interpolate.py:87-94。权重规则在 convert.py:63-106 (get_weight):有 Costcost.numberDecimal 时取 cost.number × units.number(币种为成本币种);否则有价格时取 price.number × units.number(币种为价格币种);否则就是 units。残差以 add_amount 累加,不带成本,所以残差 Inventory 里的 position 永远 cost is None

跳过 __residual__ 分录的原因是 issue #48(ca3d3b28,2015-05-20;CHANGES:3836-3838):account_rounding 自动插入吸收尾差的分录后,交易在数值上已经严格为零,平账校验被"顺带"关掉了;把这类分录排除出残差计算,校验看到的仍是用户原始分录的残差。interpolate_test.py:125-129 锁定:插入 Equity:Rounding 0.0000001 USD 后再算残差,结果仍是 -0.0000001 USD

3.3 容差推断:只看 units 的小数位

units = posting.units
if not (isinstance(units, Amount) and isinstance(units.number, Decimal)):
    continue

# Compute bounds on the number.
currency = units.currency
expo = units.number.as_tuple().exponent
if expo < 0:
    # Note: the exponent is a negative value.
    tolerance = ONE.scaleb(expo) * tolerance_multiplier

interpolate.py:179-188。容差 = 最小小数位 × tolerance_multiplier5.00 USD 的 exponent 为 -2ONE.scaleb(-2)0.01,乘默认 0.50.005。乘数默认 0.5 的理由写在 options.py:523-525:"We normally assume that the institution we're reproducing this posting from applies rounding, and so the default value for the multiplier is 0.5, that is, half of the smallest digit encountered"。整数金额(exponent 为 0 或正数)自身不推断容差——if expo < 0::186)为假时连同其内嵌套的成本、价格容差计算(:199-222,见 §3.7)一起跳过。该币种最终容差取决于 inferred_tolerance_default(见 §3.6):只有既没有该币种的显式默认值、也没有 * 默认值时才是 ZERO,此时 Inventory.is_small 要求分毫不差(inventory.py:161-163)。interpolate_test.py:288-297 锁定三条整数分录、且未配置默认容差时得到 {}

成本与价格数字的小数位默认不参与:5 VHT @ 102.2340 USD-511.11 USD 只得到 {"USD": 0.005}interpolate_test.py:318-346 三个 ignore_* 测试)。

3.4 同币种多分录:max 与 min 两种聚合

# Note that we take the max() and not the min() here because the
# tolerance has a dual purpose: it's used to infer the resolution
# for interpolation (where we might want the min()) and also for
# balance checks (where we favor the looser/larger tolerance).
if currency in tolerances:
    tolerances[currency] = agg(tolerance, tolerances[currency])
else:
    tolerances[currency] = tolerance

interpolate.py:190-197aggmode 决定(:149-150assert mode in ("max", "min"))。注释是 2024-05-27 commit bc776058 补的,当时代码只有 maxmode 参数由 2026-04-28 commit ec057e11 加入。interpolate_test.py:299-316 锁定 5.0000/5.000/5.00/5.0/5 USD 混合时取 0.05:590-606 锁定 4.8 EUR2.97 EURmax 下得 0.05min 下得 0.005

3.5 跳过 __automatic__ 分录

interpolate.py:176-178:"Skip the precision on automatically inferred postings"。插值补出的数字由程序算出,其小数位不代表用户的记录精度。真实案例在 CHANGES:3252-3257(2015-08-30):split_expenses 插件生成的分录漏打这个标记,"their automatically calculated values would end up being used for inferring the tolerances"。

判断方式是键存在性 AUTOMATIC_META in posting.meta:177),不看值;compute_residual__residual__ 用的是 .get(AUTOMATIC_RESIDUAL, False):90),只有真值才跳过。两处标记语义不对称:{"__automatic__": False} 仍会被 infer_tolerances 跳过,同样写法不会让 compute_residual 跳过对应分录。

3.6 inferred_tolerance_default 只保留本交易币种

tolerances = {
    currency: tol
    for currency, tol in default_tolerances.items()
    if currency == "*" or currency in seen_currencies
}
...
default = tolerances.pop("*", ZERO)
return defdict.ImmutableDictWithDefault(tolerances, default=default)

interpolate.py:168-172,230-231seen_currencies 收集 units、cost、price 三处币种(:160-167)。2025-05-23 commit a3a6c644 之前是整份 default_tolerances.copy(),提交信息说 "This was unnecessary"。* 键弹出后成为返回字典的默认值;ImmutableDictWithDefault.get 忽略调用方传入的第二个参数,转调 __getitem__:键存在时返回存值,缺键时才回落到 self.defaultdefdict.py:24-37),所以 inventory.py:162tolerances.get(currency, ZERO)ZERO 实际由字典自己的默认值决定,未配置 * 时为 ZERO。选项语法 <currency>:<tolerance>*:0.5options.py:484-503

这份初始 tolerances 不是"推断失败时才用"的兜底值:它在循环之前写入(:168-172),此后 units 推出的容差与它按 agg 合并(:194-197),成本/价格容差也与合并结果再按 agg 合并一次(:224-228)。因此显式配置的默认容差始终参与 max/min 比较——mode="max" 下可能被推断值取代或维持,mode="min" 下同样可能被更小的推断值压低。

3.7 infer_tolerance_from_cost:成本与价格对容差的贡献

cost = posting.cost
if cost is not None:
    cost_currency = cost.currency
    if isinstance(cost, Cost):
        cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE)
    else:
        assert isinstance(cost, CostSpec)
        cost_tolerance = MAXIMUM_TOLERANCE
        for cost_number in cost.number_total, cost.number_per:
            if cost_number is None or cost_number is MISSING:
                continue
            cost_tolerance = min(tolerance * cost_number, cost_tolerance)
    cost_tolerances[cost_currency] += cost_tolerance

interpolate.py:202-215,价格分支 :217-222 同形;整段(:199-222)嵌套在 if expo < 0: 内(见 §3.3),units 是整数的分录即使开启本选项也不贡献成本或价格容差。use_costNone 时读 options_map["infer_tolerance_from_cost"]:152-153),选项默认 Falseoptions.py:577),说明文字承诺 "Enabling this flag only makes the tolerances potentially wider"(:575)——这只在默认的 mode="max" 下成立。booking_full.py:174-177use_precise_interpolation 开启时以 mode="min" 调用同一函数(agg 变为 mininterpolate.py:149-150,224-228),成本/价格候选若比已有容差更小会把它压低,是收紧而非放宽(详见 §3.9)。规则:units 的容差乘以成本或价格数值,封顶 MAXIMUM_TOLERANCE = 0.5CostSpecnumber_totalnumber_per 都是 None/MISSING 时循环整体跳过,cost_tolerance 保留初始值 MAXIMUM_TOLERANCE:208-215)——成本数字完全缺失反而贡献封顶容差。同一成本币种的贡献用 defaultdict(D) 累加(:174,215,222),再与 units 推出的容差按 agg 合并(:224-228)。累加而非取最大来自 2015-06-04(CHANGES:3766-3771):"We assume no more rounding events than the number of postings held at cost"。docstring :111-131 给出算例:两条 18.572 VWELX {30.96 USD} 各贡献 0.001 × 0.5 × 30.96 = 0.01548,USD 容差 max(0.005, 0.03096) = 0.03096interpolate_test.py:491-556 (test_tolerances__bug53b) 按 2、3、16 条持仓分录分别断言 0.030960.046440.247680

这段有过反复:2015-05-03 dbe1fcc1 删除("causes too many problems"),同日 9c055c66 又加回带注释的 use_cost 参数,2015-05-30 8ca3abe7 恢复,2015-06-06 3a767c99 引入乘数选项与 MAXIMUM_TOLERANCE,2016-10-23 36da2cd3 增加 CostSpec 分支,2017-04-20 f7facc1f(#164)在 CostSpec 循环里跳过 MISSING,2017-06-25 a2d2f6a1 把 units/price 的判断改成 isinstance(..., Amount) and isinstance(.number, Decimal),修复 Assets:Checking USD @ 1.32 CAD 这种只缺数字的分录(interpolate_test.py:579-588)。

3.8 用户手写容差的判定与量化

tolerance = tolerances.get(currency)
if tolerance:
    # TODO(blais): "2" is used here but really it ought to be the reciprocal
    # of the "tolerance_multiplier" value. The better fix would be to apply
    # the multiplier late elsewhere, and to just not apply the multiplier
    # here.
    quantum = (tolerance * 2).normalize()
    ...
    if is_tolerance_user_specified(quantum):
        number = number.quantize(quantum)
return number

interpolate.py:375-394is_tolerance_user_specified:46-58,判据是 len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS。四个要点:

interpolate_test.py:609-631 锁定:{"USD": 0.01} 加默认 0.000005100.123123123 分别量化为 100.12(USD)和 100.12312(CAD,走默认值);默认为 ZEROif tolerance: 为假,CAD 原样返回。

3.9 双容差:插值用 min,校验与 meta 用 max

# Get the list of tolerances.
tolerances_max = interpolate.infer_tolerances(
    entry.postings, options_map, mode="max"
)
if options_map["use_precise_interpolation"]:
    tolerances_interp = interpolate.infer_tolerances(
        entry.postings, options_map, mode="min"
    )
else:
    tolerances_interp = tolerances_max

booking_full.py:170-179tolerances_interp 传给 interpolate_group:230),tolerances_max 写入 __tolerances__:239)。来龙去脉:ec057e11(2026-04-28)随提交附带的 rounding_fix_explanation.md(次日 573f04bc 删除,git show ec057e11:rounding_fix_explanation.md 可取回)描述了问题——4.8 EUR + 2.97 EUR 的缺失腿应为 -7.77,但单一的 max 容差 0.05 让 quantum 变成 0.1,补出 -7.8,账户里留下 0.03 EUR 的不平;修复是插值用 min 容差、校验和 meta 仍用 max 以保持 "backward compatibility with permissive balancing rules"。ec057e11 最初无条件启用;2026-05-02 5704a861 加入 use_precise_interpolation 选项并默认关闭(options.py:700-717),提交信息:"This change makes that behavior optional and disabled by default to maintain backward compatibility"(其中引用的 8e4bdd99 是合并该修复分支的 merge commit)。booking_full_test.py:3481-3520 两个对照测试锁定默认得 -7.8、开启得 -7.77,两者 __tolerances__["EUR"] 都是 0.05

量化只发生在缺 units 的情形:booking_full.py:945-947quantize_with_tolerance 的唯一生产调用点。COST_PER:964)与 PRICE:1003)是除法,COST_TOTAL:979number_total = weight - cost.number_per * units.number)是乘减,三者结果都不经过 quantize_with_tolerance,直接采用调用当时的 Decimal 上下文精度:新进程默认是 28 位(getcontext().prec),但可被调用者改变(本机在 localcontext 内把 prec 设为 6 后 Decimal(1)/Decimal(7)0.142857),且精确结果不会被补足到 28 位有效数字(默认上下文下 Decimal(1)/Decimal(2) 就是 0.5)。

3.10 残差分录与 account_rounding 的接线状态

meta = {AUTOMATIC_META: True, AUTOMATIC_RESIDUAL: True}
return [
    Posting(account_rounding, -position.units, position.cost, None, None, meta.copy())
    for position in residual.get_positions()
]

interpolate.py:255-259fill_residual_posting:262-286)在残差非空时追加这些分录。docstring :266-269 写明它是为导出 Ledger 而做,"A better method would be to enable the feature that automatically inserts these rounding postings on all transactions"。options.py:424-439account_rounding 选项说明仍承诺 "setting this value to an account name will automatically enable the addition of postings on all transactions that have a residual amount"。全仓库搜索 account_rounding,除 interpolate.py(参数名)、options.py(选项声明)与测试外,还命中 CHANGES:1850,3229,3233,3237,3837,3880,3883 这几处历史记录;排除测试与 CHANGES 后,生产 Python 代码没有读取 options_map["account_rounding"] 的调用路径,fill_residual_posting 没有非测试调用点;get_residual_postings 的唯一非测试调用者是同模块的 fill_residual_posting:284),因此整条链在生产路径上都不可达。自动插入曾经存在:2015-05-16 ef6e0a2d 实现于解析阶段,2016-10-14 8f14afb6balance_incomplete_postings 移到 booking_simple.py:176(该文件被删除前,这处已移到 :276),2018-03-13 2be15243 删除 booking_simple 后失去最后一个生产读取点;fill_residual_posting 的最后调用者 reports/convert_reports.py:169,299 随 2020-07-05 a7c4f14f 删除 bean-report 一起消失。

3.11 余额统计:compute_entries_balancecompute_entry_context

interpolate.py:289-312:遍历条目,date 为排他截止日,遇到第一条 entry.date >= datebreak:306-307,依赖条目已排序);prefix 按账户名前缀过滤(:310);用 add_position(posting) 累加,保留成本(interpolate_test.py:179-200 断言 10 HOOL {40 USD}-400 USD),价格被忽略(:202-220 断言 2000.00 EUR-3560.00 GBP)。生产调用点是 summarize.py:542,用来算换算差额。

interpolate.py:315-361:先取 context_entry 涉及的账户集合,additional_accounts 可补充(:337-339,2021-01-30 06127f16bean-doctor context 加入,让 booking 报错、分录被丢弃的交易也能显示账户余额);顺序累加直到 entry is context_entry:344-346),再深拷贝并叠加该交易自身的分录(:355-359)。:357 用的是循环变量 entry 而非 context_entry,正确性依赖 break 命中。context_entryTransaction 但不在 entries 里时,循环耗尽后 entry 停在列表最后一项:末条恰好是 Transaction 则错误叠加它的分录;末条不是 Transaction(没有 postings 属性)则抛 AttributeErrorentries 为空时 entry 从未被赋值,抛 UnboundLocalError(本机对三种情形分别调用验证)。

4. 接口一览

函数 签名 返回 生产调用点
compute_residual(postings) :72 Inventory booking_full.py:893(其它分录的残差决定缺失腿权重)、validation.py:375context.py:168
infer_tolerances(postings, options_map, use_cost=None, mode="max") :97 ImmutableDictWithDefault booking_full.py:171,175validation.py:376context.py:174sellgains.py:133
quantize_with_tolerance(tolerances, currency, number) :364 Decimal booking_full.py:945
get_residual_postings(residual, account_rounding) / fill_residual_posting(entry, account_rounding) :245,262 list[Posting] / Transaction
compute_entries_balance(entries, prefix=None, date=None) :289 Inventory summarize.py:542
compute_entry_context(entries, context_entry, additional_accounts=None) :315 (dict, dict) context.py:110
has_nontrivial_balance(posting) :61 cost or price printer.py:239(决定是否渲染权重列)
is_tolerance_user_specified(tolerance) :46 bool :392

最终裁决在 validation.py:350-386 (validate_check_transaction_balances):对每笔交易 compute_residual + infer_tolerances(默认 mode="max")+ residual.is_small(tolerances),不平则报 "Transaction does not balance: {residual}":365-372 的注释以 IMPORTANT 标注:"This must come after the user routines, because unbalancing input is legal, as those types of transactions may be 'fixed up' by a user-plugin"。加载顺序在 loader.py:605-627:booking → run_transformations(插件)→ validation.validate;该校验登记在 validation.py:390-398 (BASIC_VALIDATIONS)

5. 设计决策与理由

决策 理由 证据
容差从 units 小数位推断而非全局固定值 2015-05-17 改版:"inferred tolerances may be smaller than the fixed value we used previously";不同币种、账户的记录精度不同 CHANGES:3847-3893interpolate.py:184-188
乘数默认 0.5 假设数据来源机构已做舍入,误差至多半个最小位 options.py:523-525CHANGES:3746-3748
多分录同币种取 max 容差同时服务插值与校验,校验偏向宽松 interpolate.py:190-193bc776058
插值另算一份 min 容差,默认不启用 单一 max 容差会把补出的数字过度取整;默认关闭为兼容旧账本 booking_full.py:170-179ec057e115704a861options.py:701-708
跳过 __automatic__ 分录 程序算出的小数位不代表记录精度 interpolate.py:176-178CHANGES:3252-3257
残差排除 __residual__ 分录 吸收尾差的分录会让校验失效(#48) interpolate.py:89-91ca3d3b28
成本/价格的贡献累加并封顶 0.5 每条持仓分录各有一次舍入;上限防止大额成本放大容差 CHANGES:3766-3771interpolate.py:28-30,207,210,221
只有"像用户手写"的容差才量化 推断出的长小数容差量化会得到无意义的位数或 InvalidOperation interpolate.py:46-58,383-392c8ec7c0b
默认容差只复制本交易币种 复制整份字典无必要 interpolate.py:157-172a3a6c644
Balance/Pad 容差取乘数的两倍 用户手填的核对数字误差来源更杂,"Be generous" ops/balance.py:36-41
平账校验放在所有插件之后 允许用户输入不平的交易由插件修正 validation.py:365-372loader.py:605-627

6. 行为细节与边界

现象 后果 证据
整数金额自身不推断容差 若该币种无显式默认容差、无 * 默认值,容差才是 ZEROis_small 用严格 > 比较,此时任何非零残差都报错 interpolate.py:157-172,186-222inventory.py:161-163interpolate_test.py:288-297
边界是"严格大于才不平" 乘数 1.1 时差 0.011 EUR 通过、差 0.012 EUR 报错 inventory.py:164interpolate_test.py:442-459
quantize 只取 quantum 的 exponent 容差 0.01 的 quantum 0.02 量化到两位小数,不是 0.02 的倍数 interpolate.py:381,393interpolate_test.py:614-617
乘数不是 0.5 时 quantum 与容差错位 0.011 → quantum 0.022,量化到三位小数 interpolate.py:377-381
guard 不防 InvalidOperation 大数量化仍抛异常 interpolate.py:383-393;本机验证
舍入模式取自调用者当前 context,非函数自身保证 新进程默认 ROUND_HALF_EVEN7.85 量化到一位得 7.8;context 改为 ROUND_HALF_UP 时得 7.9 interpolate.py:393;非测试代码无 ROUND_;本机验证
两个调用点看到的成本类型不同 booking_full.py:171 在 booking 前调用,成本是 CostSpecvalidation.py:376 在 booking 后调用,成本是 Cost。开启 infer_tolerance_from_cost 且写 {{总价}} 时,语法层把 number_per 置为 ZEROgrammar.py:602-606),min(tolerance × 0, ...) 为 0,成本对 __tolerances__ 无贡献;校验阶段 Cost.number 是总价除以数量,贡献不为零(本机直接调用验证:-1.729 CAAPX {{521.67787 USD}} @ 49.65 USD 两阶段 USD 容差分别为 0.0248250.1756… interpolate.py:206-214grammar.py:590-606
只有缺 units 的插值结果被量化 COST_PER/PRICE 的除法与 COST_TOTAL 的乘减都不经过 quantize_with_tolerance,采用调用时的 Decimal 上下文精度(新进程默认 28 位,可被调用者改变) booking_full.py:945-947,964,979,1003
has_nontrivial_balance 返回对象而非布尔 printer.py:239any(map(...)) 消费,真值语义足够 interpolate.py:69
compute_entry_context:357 使用循环变量 context_entry 不在列表中时:末条是 Transaction 则叠加其分录,否则抛 AttributeError;空列表抛 UnboundLocalError interpolate.py:344-359;本机验证
残差 position 无成本 get_residual_postingsposition.cost 实际总是 None interpolate.py:93,257convert.py:63-106
BalanceError 定义在此但只被 ops/balance.py entry 字段类型标注为 Balance,与本模块"postings 平账"的 docstring 不对应 interpolate.py:38-43ops/balance.py:13,113,127,161
【文档漂移】compute_residual docstring 说返回 "the per-currency precision" 实际只返回 Inventory;2015-04-13 d67cb7e6 加的是 "an unimplemented 'precision' return argument",参数后来删去、文字留下 interpolate.py:73-80,94
【文档漂移】quantize_with_tolerance docstring 参数顺序 numbercurrency 前,且写作 "Decimalvalues" 签名是 (tolerances, currency, number) interpolate.py:364-370
【文档漂移】is_tolerance_user_specified docstring 夹有孤立的 # 例子 "0.1234 but not 0.123456" 本身与代码一致(4 位通过、6 位不通过;5 位如 0.12345 也不通过) interpolate.py:47-51,58
【文档漂移】account_rounding 选项说明承诺自动插入 生产路径无读取点,见 3.10 options.py:425-430
【文档漂移】测试名 test_tolerances__minimum_on_costs 断言的是三条 VHT 分录取 max 后的 0.000005 interpolate_test.py:406-416

7. 测试锁定了什么

interpolate_test.py 共 27 个测试,四个类;:24-29 定义的 OPTIONS_MAP 在文件内没有被引用。

测试 行号 锁定的行为
test_has_nontrivial_balance 40-56 无 cost/price 为假;有价格、有成本、两者皆有为真
test_compute_residual 58-81 两条与四条分录的残差;用 reduce(convert.get_units) 比较
test_fill_residual_posting 83-146 已平衡不插分录;-100.00000010.0000001-112.69 CAD @ 0.88750.012375 USD;插入后残差计算忽略新分录
test_compute_entries_balance_* 150-220 全部分录汇总为空;带成本保留成本;带价格只累加 units
test_compute_entry_context 222-284 交易前后余额;非 Transaction 条目前后相同
test_tolerances__no_precision / dubious_precision 288-316 整数得 {};混合精度取 0.05
test_tolerances__ignore_* / cost_and_number_ignored 318-356 默认不看 cost/price;整数 units 加成本仍 {}
test_tolerances__number_on_cost_used / _overrides 358-380 use_cost=TrueUSD: 0.051117;与 -511.00.05 取 max
test_tolerances__number_on_cost_fail_to_succ 382-404 同一交易:选项关闭报 0.20000 USD 不平,开启无错
test_tolerances__minimum_on_costs 406-416 三种精度取 0.000005
test_tolerances__with_inference / capped_inference 418-440 缺失腿存在时的推断;0.05 × 102.2340 封顶为 0.5
test_tolerances__multiplier 442-459 乘数 1.1:差 0.011 通过、0.012 报错
test_tolerances__bug / bug53a / bug53b / bug53_price 461-577 成本与价格累加后的 __tolerances__ 精确值;无报错
test_tolerances__missing_units_only 579-588 USD @ 1.32 CAD 可加载(无显式断言,靠 load_doc 默认要求零错误,loader.py:790-794
test_infer_tolerances_modes 590-606 max0.05min0.005
test_quantize_with_tolerance 610-631 显式容差与默认容差各自量化;默认 ZERO 不量化

interpolate_test.py 内没有直接用例的行为:is_tolerance_user_specified 的直接调用与 5 位边界;get_residual_postings 的直接调用;mode="min"use_cost 组合;quantize_with_toleranceInvalidOperationcompute_entries_balanceprefix 参数;compute_entry_contextadditional_accounts__automatic__ 分录被跳过(本文件内无用例,booking_full 路径间接依赖)。

以下两项本文件内虽无用例,但由其它模块的测试间接覆盖:compute_entries_balancedate 参数——summarize_test.py:945-1009 多处以 date=date 调用并断言结果为空;inferred_tolerance_default* 默认值经 infer_tolerances 的传递——validation_test.py:401-410 (test_tolerance_implicit_fractional_global) 用全局 *:0.005 加载一笔残差 0.002237 CAD7.9599 × 125.63 − 1000)的交易并要求零错误,若 * 默认值未被传递到该交易的 tolerances,此残差在 ZERO 容差下会报错。

8. 演变史

日期 提交 / 记录 变化
2014-11-03 ad01c96c 在当时的 core/complete.py 加入 fill_residual_posting,为 Ledger 导出补 Equity:Rounding 分录
2014-11-09 51130f08 complete.py 更名 interpolate.py
2015-04-13 d67cb7e6 compute_residual 加入未实现的 precision 返回参数(docstring 残留至今)
2015-05-03 / 05-30 dbe1fcc19c055c668ca3abe7 成本容差推断删除、加回 use_cost 注释、恢复
2015-05-16 ef6e0a2d 实现 account_rounding 自动插入残差分录
2015-05-17 CHANGES:3845-3917 容差机制整体改版:自动推断、default_toleranceaccount_rounding__automatic__
2015-05-20 ca3d3b28 #48:残差计算跳过 __residual__ 分录
2015-05-31 c8ec7c0b is_tolerance_user_specifiedMAX_TOLERANCE_DIGITS
2015-06-04 CHANGES:3766-3771 成本容差改为累加
2015-06-06 3a767c99 inferred_tolerance_multiplier 选项与 MAXIMUM_TOLERANCEinfer_tolerance_from_cost 脱离实验状态(CHANGES:3755-3758
2015-08-30 CHANGES:3252-3257 split_expenses 漏打 __automatic__ 导致容差误推断
2016-03-06 CHANGES:2398-2399 #83:default_tolerance 更名 inferred_tolerance_default
2016-10-14 cc82c533bbdb744b8f14afb6 FULL 记账法写入 __tolerances__、实现 quantize_with_tolerancebalance_incomplete_postings 移出本模块
2016-10-23 36da2cd3 成本容差增加 CostSpec 分支
2016-10-30 / 12-17 99ee920adfc75f3bCHANGES:1836-1851 删除 use_legacy_fixed_tolerancesLEGACY_DEFAULT_TOLERANCES;删除 default_tolerancetoleranceexperiment_explicit_tolerances 等废弃选项及 account_rounding 的前缀警告代码
2017-01-14 43c5630ef30c2e46 废弃 compute_cost_basisget_posting_weight(改用 convert.get_weight
2017-04-20 f7facc1f #164:CostSpec 循环跳过 MISSING
2017-06-25 a2d2f6a1 接受只缺数字的分录 USD @ 1.32 CAD
2018-03-13 2be15243 删除 booking_simpleaccount_rounding 失去生产读取点
2018-03-23 b04cafdd 删除已废弃方法
2020-07-05 a7c4f14f 删除 bean-report,fill_residual_posting 失去调用者
2021-01-30 06127f16 compute_entry_context 增加 additional_accounts
2024-05-27 bc776058 补注释说明取 max() 的取舍
2024-12-22 85950542 BalanceError 改为带类型注解的 NamedTuple
2025-05-23 a3a6c644ae5d5f14 默认容差只复制本交易币种;inferred_tolerance_multiplier 更名 tolerance_multiplier(旧名以 alias 保留并标记废弃,options.py:544-560
2026-04-28 ec057e11(merge 8e4bdd99 mode="max"/"min";插值用 min 容差;附 rounding_fix_explanation.md(次日 573f04bc 删除)
2026-05-02 5704a861 use_precise_interpolation 选项,默认关闭

9. 与其他模块的关系

10. 参考索引

beancount/core/interpolate.py:1 模块 docstring;28-30 MAXIMUM_TOLERANCE;33-35 MAX_TOLERANCE_DIGITS;38-43 BalanceError;46-58 is_tolerance_user_specified;61-69 has_nontrivial_balance;72-94 compute_residual(89-91 跳过);97-231 infer_tolerances(105-131 算例;149-150 agg;152-155 选项;157-172 默认容差;174-181 循环入口;184-188 容差;190-197 聚合与注释;199-222 成本与价格;224-231 合并与返回);234-242 meta 键;245-259 get_residual_postings;262-286 fill_residual_posting;289-312 compute_entries_balance;315-361 compute_entry_context;364-394 quantize_with_tolerance(377-380 TODO;381 quantum;383-393 guard)。

beancount/core/interpolate_test.py:24-29、40-56、58-81、83-146、150-177、179-200、202-220、222-284、288-297、299-316、318-326、328-336、338-346、348-356、358-368、370-380、382-404、406-416、418-428、430-440、442-459、461-474、476-489、491-556、558-577、579-588、590-606、610-631。

其它beancount/parser/booking_full.py:170-179,230,238-240,893-895,944-947,949,964,979,1003,1013-1016beancount/parser/booking_full_test.py:3481-3520beancount/ops/validation.py:350-386,390-398beancount/ops/balance.py:13,20-45beancount/core/inventory.py:132,152-166,402,459beancount/core/convert.py:63-106beancount/utils/defdict.py:14-51beancount/parser/grammar.py:590-606beancount/parser/options.py:424-439,483-512,513-543,523-525,544-560,561-578,700-717beancount/parser/context.py:110,168,174beancount/plugins/sellgains.py:133beancount/plugins/check_commodity.py:50beancount/ops/summarize.py:542beancount/parser/printer.py:239beancount/loader.py:605-627,790-794CHANGES:1836-1851,2398-2399,3229,3233,3237,3252-3257,3269-3272,3746-3748,3755-3758,3766-3771,3836-3838,3845-3917,3880,3883

commitad01c96c(2014-11-03)、51130f08(2014-11-09)、d67cb7e6(2015-04-13)、dbe1fcc1/9c055c66(2015-05-03)、ef6e0a2d(2015-05-16)、ca3d3b28(2015-05-20)、8ca3abe7(2015-05-30)、c8ec7c0b(2015-05-31)、3a767c99(2015-06-06)、cc82c533/bbdb744b/8f14afb6(2016-10-14)、36da2cd3(2016-10-23)、99ee920a(2016-10-30)、dfc75f3b(2016-12-17)、43c5630e/f30c2e46(2017-01-14)、f7facc1f(2017-04-20)、a2d2f6a1(2017-06-25)、2be15243(2018-03-13)、b04cafdd(2018-03-23)、a7c4f14f(2020-07-05)、06127f16(2021-01-30)、bc776058(2024-05-27)、85950542(2024-12-22)、a3a6c644/ae5d5f14(2025-05-23)、ec057e11/8e4bdd99(2026-04-28)、573f04bc(2026-04-29)、5704a861(2026-05-02)。