解析器交出的是不完整的交易:分录可以只写币种不写数字、可以整行只有账户名(自动过账),成本可以写成
{}、{USD} 或
{100.00 # 9.95 USD}。补全它要同时解决两件互相依赖的事:批次匹配(减仓应匹配到哪个库存批次,从而知道成本)与插值(按借贷平衡反推唯一缺失的数字)。
beancount/parser/booking_full.py:1-71 的模块 docstring
用两笔几乎一样的交易说明依赖是双向的。:13-16 中
-10 HOOL {USD} 配 1000 USD 与
-200 USD,两条已知腿的权重和为
1000 + (-200) = 800 USD,取负除以 -10
单位反推出单位成本
80 USD(beancount/parser/booking_full.py:892-905,957-965)——docstring
正文声称由此能 "back out a cost of 100 USD /
HOOL",与实际公式算出的结果不符;:23-26 去掉
Income:Gains 的数字后插值失败,只能先按 FIFO 选出最老的 10
股定下成本再回头算收益。两条观察(:32-42):后者远比前者常见;只有减仓能匹配,加仓不能。由此定下顺序(:52-65)——先只对减仓匹配、失败即早退;加仓保持
CostSpec
类型以待插值,若未显式写成本日期则先补成父交易日期(:700-704);再插值,此时才可能补出加仓成本;最后统一转
Cost。末尾(:66-70)承认更完整的做法是在两步间迭代到收敛,作者判断收益太小未实现。
| 名称 | 位置 | 作用 |
|---|---|---|
booking.BookingError |
beancount/parser/booking.py:20-23 |
source/message/entry |
booking.book |
beancount/parser/booking.py:26-54 |
入口:建记账法字典 → 转调 → 兜底校验 |
booking.validate_missing_eliminated |
beancount/parser/booking.py:57-83 |
扫残留的 MISSING |
booking.validate_inventory_booking |
beancount/parser/booking.py:86-136 |
死代码,见 3.1 |
booking.convert_lot_specs_to_lots /
convert_spec_to_cost |
:139-192、:195-224 |
死代码及其转换函数 |
booking_full.book / _book |
beancount/parser/booking_full.py:108-127、:130-254 |
主循环,book 丢掉第三个返回值 |
Refer / get_bucket_currency |
:287-293、:265-284 |
分录的三个币种引用;桶币种优先级 |
categorize_by_currency |
:296-486 |
按币种分组 |
replace_currencies |
:489-528 |
把推断的币种写回 Posting |
has_self_reduction |
:539-561 |
自我对冲检测,已关闭 |
book_reductions |
:564-715 |
减仓匹配 |
compute_cost_number /
convert_costspec_to_cost |
:718-744、:747-774 |
两段重叠的单价计算 |
MissingType |
:780-786 |
UNITS/COST_PER/COST_TOTAL/PRICE |
interpolate_group |
:798-1049 |
组内补一个数字 |
| 四个错误类型 | :100-105、:257-262、:531-536、:790-795 |
SelfReduxError、CategorizationError、ReductionError、InterpolationError |
booking_methods = collections.defaultdict(lambda: options_map["booking_method"])
for entry in incomplete_entries:
if isinstance(entry, data.Open) and entry.booking:
booking_methods[entry.account] = entry.booking
beancount/parser/booking.py:41-44。账户的记账法来自
open 指令的 booking 字段,缺省落到全局选项
booking_method(beancount/parser/options.py:640-660,默认
STRICT)。调用方传入 defaultdict,使未在
open 里显式配置 booking
的账户自动继承全局记账法;beancount/parser/booking_full.py:628,555
直接下标 methods[account],换普通 dict
须预先塞满所有会被访问的账户键,否则抛
KeyError。validate_missing_eliminated(:57-83)在其后再扫一遍,units
的两个字段或 cost 的四个字段还留着 MISSING
就报 "Transaction has incomplete
elements"(:67-80);此检查假定 cost 非
None 时必是 Cost——.number 是
Cost
独有字段,CostSpec(beancount/core/position.py:28-66)只有
number_per/number_total,3.2 节说明何时会有
CostSpec 残留触发
AttributeError。另有三个函数无生产调用点,只被测试引用:
book() 收尾调
validate_missing_eliminated(beancount/parser/booking.py:57-83)扫残留的
MISSING:逐条 posting 判
MISSING in (units.number, units.currency),以及
cost is not None and MISSING in (cost.number, cost.currency, cost.date, cost.label)(:72-77)。它只按
Cost 的字段名取值——CostSpec 没有
number 字段(它是
number_per/number_total,beancount/core/position.py:45-66),所以这个兜底只对已转成
Cost 的结果安全。interpolate_group
在"带成本的分录不能推价格"这条错误路径上原样返回仍带
CostSpec 的
postings(beancount/parser/booking_full.py:986-995),若
units 已完整、流程继续走到这里,访问 cost.number 会抛
AttributeError 而不是记一条错误。
validate_inventory_booking(:86-136)顶上挂
# FIXME: This goes away. Maybe moves to a pedantic plugin.(:86)。docstring
说 "no position at cost is allowed to go
negative"(:88),实现检查的却是
is_mixed()(:124),即正负批次并存;纯负库存不报错,beancount/parser/booking_test.py:117-128
锁定这一点。convert_lot_specs_to_lots(:139-192)自称
"only there for the sake of transition to the new matching
logic"(:144-148),它服务的 SIMPLE 记账法已于 2018-03-13
删除(CHANGES:1098-1102)。convert_spec_to_cost(:195-224)是前者调用的单条转换,也被
beancount/parser/booking_full_test.py:1568,1599,1651
当作断言的参照实现。beancount/parser/booking_full.py:161-254。对每条
Transaction:分类(:164)→
回填币种(:168)→ 算容差(:171-179)→
逐币种组做匹配(:215-217)与插值(:229-231)→
装回交易并写 __tolerances__(:238-240)→
用最终结果更新余额(:248-250)。
两处 continue
层级不同,后果差别很大::164-167 那处在
for entry in entries 一层,跳过末尾的
new_entries.append(entry)(:252),分类失败丢弃整条交易;:220-222
那处在币种组循环里,匹配失败只丢该组分录,交易保留但可能少腿。插值错误不
continue(:233-235),主循环无条件
repl_postings.extend(inter_postings),但去留取决于
interpolate_group 的返回值:缺口数超过一个时函数把
out_postings
置空(:874),该币种组全部分录从结果消失;总价无法反推单价、或带成本分录无法推价格这两个具体错误分支则直接返回原始
postings(:924,997),未补全、仍含
CostSpec/MISSING 的分录原样并入
repl_postings。两支后果不同::924 支
units.number 仍是
MISSING,validate_missing_eliminated 的
or 短路,只报预期错误;:997 支("Cannot infer
price for postings with units held at cost")units
已完整、cost 仍是 CostSpec,触发前述
AttributeError。
:184-186 提醒组内 postings 是
entry.postings 的子集且可能含复制出的自动过账,"Never use
entry.postings going forward";但 :171,175
算容差用的正是全集——容差整笔交易一份,不分组。:242-250
不复用 book_reductions
的局部余额而重新聚合,注释说这是一致性自检。:194-200 是被
if False:
关掉的自我对冲检查:同账户同币种既加仓又减仓时正确顺序是先减后加,has_self_reduction(:539-561)能检出,但整块调用禁用,注释指向
http://furius.ca/beancount/doc/self-reductions(:188)。
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
beancount/parser/booking_full.py:170-179。容差承担两个冲突职责:校验"这笔平不平"要宽容(取最大值容纳外部数据的舍入误差),补数字要精确(取最小值以匹配用户写下的最细精度)。2026-04-28
commit ec057e11 修的就是二者混用,随它提交、次日被
573f04bc 删除的
rounding_fix_explanation.md(git show ec057e11:rounding_fix_explanation.md
可取回)给的例子:4.8 EUR 推出容差
0.05、2.97 EUR 推出 0.005,取最大值后补出的
7.77 被量化成 7.8,账面多出 0.03 EUR
悬空;修法是插值用 min、__tolerances__ 仍写 max。四天后
commit 5704a861 改为默认关闭并加
use_precise_interpolation
选项(beancount/parser/options.py:700-717,默认
False),理由是向后兼容。beancount/parser/booking_full_test.py:3481-3520
用同一输入锁定两条路径:打开得 -7.77,默认得
-7.8,两种情况 __tolerances__["EUR"] 都是
0.05。
beancount/parser/booking_full.py:296-486。把分录按"跟谁配平"的币种分组,之后匹配与插值都在组内独立进行。桶币种优先级由
get_bucket_currency(:265-284)定死:成本
> 价格 > units,最后一条还要求成本与价格都为
None。推断分五步,前一步定不下来才轮到后一步:
:356-362):只写了一个时把已知的复制给
MISSING 的那个,即
docstring(:306-308)说的"两者必须一致"。:364-374):并用
sortdict
记下该币种首次出现的下标供末尾排序(:485),保证输出顺序可重复。:381-399):条件是
len(unknown) == 1 and len(groups) == 1;无成本无价格则推
units 币种,否则推成本/价格币种。:401-434):units 币种缺失看
balance.currencies(),成本/价格币种缺失看
balance.cost_currencies(),恰好一个才采用;仍定不下来报
CategorizationError("Failed to categorize posting N")(:430-434,下标从
1 开始,源自 fe6ec215/#249)。:436-448):靠成本或价格入桶但 units 仍
MISSING 的,再用账户余额的唯一币种补一次。自动过账(units is MISSING
且无价格币种,:366-368)单独装桶,最后复制进每个已存在的币种组(:462-465)——一条空行在
USD、CAD 两组各出现一次,各补各的数。超过一条报 "You may not have more
than one auto-posting per currency"
并只留第一条(:451-461)。收尾(:467-483)对仍
MISSING 的币种各报一条 "Could not resolve X currency"。
beancount/parser/booking_full.py:489-528。逐组按原始下标排序(:506)取
posting 写回币种。units 整个是
MISSING(自动过账)时造
Amount(MISSING, refer.units_currency)(:509-510),数字留空等插值;否则只替换
currency is MISSING 的字段,并用 replace
标志位保证无字段需改时返回原对象(:512-525)。自动过账在多个组各复制一次,同一条原始分录产出多个
Posting,这正是 :184-186 警告的由来。
beancount/parser/booking_full.py:564-715。对
balances 只读(:609-611),自己维护浅拷贝
local_balances(:616-619),使同一交易内多条减仓不重复吃掉同一批持仓(:685-691)。判定减仓要三条同时成立(:629-633):记账法不是
Booking.NONE、账户有余额、balance.is_reduced_by(units)。不带成本或
units 数字缺失的分录原样通过(:622-624)——后半条是 commit
686d5c3f(2017-09-30)补的,此前
HOOL {115.00 USD}
这种省略数量的写法会进匹配逻辑并失败,回归测试见
beancount/parser/booking_full_test.py:2046-2056。
候选批次的筛选是六道 continue
过滤(:639-657):
for position in balance:
if units.currency and position.units.currency != units.currency: continue
if position.cost is None: continue
if cost_number is not None and position.cost.number != cost_number: continue
if isinstance(costspec.currency, str) and position.cost.currency != costspec.currency: continue
if costspec.date and position.cost.date != costspec.date: continue
if costspec.label and position.cost.label != costspec.label: continue
即币种、必须带成本、单价、成本币种、日期、标签。cost_number
由 compute_cost_number
先算(:637),算不出来(含 MISSING
或两数皆无)返回 None,该道跳过。CostSpec
写了什么就筛什么:{} 全不筛,{2016-01-15}
只按日期筛。
匹配为空时(:660-670)报
ReductionError('No position matches ...') 并
return [], errors,注释写 "This is irreconcilable, remove
these
postings"——整组作废,不做部分生效;候选非空但无法消歧或数量不够时,handle_ambiguous_matches(:675-677)的错误同样触发
return [], errors(:678-680)。加仓分支(:692-713)只做一件事:CostSpec
没写日期就填交易日期(:702-704),成本仍是
CostSpec。紧接的注释代码(:706-711)本想给新批次插入唯一标签以支持交易跟踪,FIXME
说它会造成不该有的歧义匹配错误。
if number_total is not None:
cost_total = number_total
units_number = abs(units.number)
if number_per is not None:
cost_total += number_per * units_number
unit_cost = cost_total / units_number
beancount/parser/booking_full.py:732-739 (compute_cost_number)。convert_costspec_to_cost(:762-771)几乎逐行重复,差别两处:判断
number_per 的哨兵一个比 None、一个比
MISSING;前者在 number_per is None 时返回
None,后者直接把它当单价。:777 挂着
# FIXME: Refactor compute_cost_number() and convert_costspec_to_cost().
两处的 abs() 来自 2020-09-07 commit
2f14cb5d(修
#364):此前保留符号、只在最后除法里取绝对值,-23 MSFT {106.935 # 6.90 USD}
这类空头带总费用的写法算成
(6.90 + 106.935 × (-23)) / 23,单价为负;修法是把
abs() 提前到累加之前,正确结果
107.235。回归测试两条:beancount/parser/booking_full_test.py:1238-1265
走完整流程,:1334-1340 直接断言
compute_cost_number(CostSpec(3, 6, ...), -12 HOOL) == 3.5。
两处除法都不量化:Decimal
除法按上下文精度(默认 28 位有效数字)展开,除不尽时留长尾小数,而整个
Cost 元组是 Inventory 的
key(beancount/core/inventory.py:428:key = (units.currency, cost)),长尾数字原样成为那批持仓的身份。后续减仓若按四舍五入的单价书写,第三道过滤就匹配不上。
beancount/parser/booking_full.py:798-1049。先扫一遍收集缺口(:823-853):units.number is MISSING
记 UNITS;cost 还是 CostSpec 时
number_per/number_total 为
MISSING 分别记
COST_PER/COST_TOTAL,已是 Cost
的则断言其 number 是
Decimal(:845-850,注释说明减仓成本已在匹配阶段定好,无必要插值);price.number is MISSING
记 PRICE。
三个分支(:858-874):零个缺口就把 CostSpec
全转 Cost 返回;两个及以上报 "Too many missing numbers for
currency group '{}'"
并返回空列表;恰好一个才计算。目标值来自
compute_residual(:893-895)对其余分录求权重和取负(:905),其余为空时
weight = ZERO(:907-909)。
| 缺口 | 公式 | 位置 | 附加规则 |
|---|---|---|---|
UNITS |
有成本 (weight − number_total) / number_per;有价格
weight / price;都没有 weight |
:911-947 |
唯一调 quantize_with_tolerance 的分支 |
COST_PER |
(weight − number_total) / units.number |
:957-971 |
units.number == ZERO 时置
new_posting = None |
COST_TOTAL |
weight − number_per × units.number |
:973-984 |
无除法 |
PRICE |
abs(weight / units.number) |
:986-1006 |
cost is not None 时报 "Cannot infer price for postings
with units held at cost" |
三条要点:
UNITS
量化(:944-947,commit bbdb744b
加入);另外三支都不量化,COST_PER(:964)与
PRICE(:1003)的除法可能留长尾小数(同第 7
节),COST_TOTAL(:979)只做乘减、本无除法但同样不量化。beancount/parser/booking_full_test.py:1173-1236
用 inferred_tolerance_default 锁定:USD 设 0.00005 时
100.123412341234 补成 -100.1234,JPY 设 0.5
补成 -100,未设的 CAD 保留全部小数。:914-924)。{{100.00 USD}}
总价语法在语法层被译成
number_per = ZERO, number_total = 100.00(beancount/parser/grammar.py:590-606),无法从总价反推数量,报
"Cannot infer per-unit cost only from total"
并原样返回未补全的分录。:949-955、:1021-1022)。自动过账所在组的其余分录已自己配平时
weight == ZERO,new_posting 置
None,随后
del new_postings[index],多余的行被静默移除且不报错;beancount/parser/booking_full_test.py:1079-1129
锁定该行为,尾部各留
# FIXME: This ought to return a "Superfluous posting" error。COST_PER
遇零数量走同一路径。补出的分录打上
__automatic__(:1013-1016)并转
Cost(:1020)。出口先断言不再有
CostSpec(:1025),再检查带成本的分录:units.number == ZERO
报 "Amount is zero"、cost.number < ZERO 报 "Cost is
negative"(:1029-1047);紧邻的注释(:1032-1035)写
"we don't allow either a cost value of zero",但实现并未检查
cost.number == ZERO——零成本实际被接受,注释与代码已经漂移,beancount/parser/booking_test.py:47-55 (test_cost_zero)
直接断言 {0.00 USD}
不产生错误。posting.cost.number is not None 这道前置判断是
commit 76365107 补的(导入器直接构造分录时
Cost.number 可能为 None)。第三个返回值
interpolated 即
new_posting is not None(:1049),所以"删掉多余分录"报告
False。
| 决策 | 理由 | 证据 |
|---|---|---|
| 先做减仓匹配,加仓不动 | 匹配只对减仓有定义;加仓成本可能要靠插值 | beancount/parser/booking_full.py:38-42,52-58,692-713 |
| 不在匹配与插值间迭代到收敛 | 需要迭代的实际用例极少 | beancount/parser/booking_full.py:66-70 |
| 按币种分组后各组独立处理 | 一组一个未知数才有唯一解 | beancount/parser/booking_full.py:296-302,863-874 |
| 桶币种取成本 > 价格 > units | 带成本或价格的腿,权重记在该币种上 | beancount/parser/booking_full.py:265-284 |
| 匹配失败整组作废 | 作者注释 "This is irreconcilable, remove these postings" | beancount/parser/booking_full.py:660-680 (book_reductions) |
book_reductions 不改 balances |
无副作用,用局部拷贝承接组内累积 | beancount/parser/booking_full.py:609-619 |
加仓 CostSpec 只补日期不转 Cost |
转早了分不清单位成本与总成本哪个待插 | beancount/parser/booking_full.py:56-58,694-704 |
只有 UNITS 分支调用容差量化 |
提交记录只说明这是给 auto-posting 加的量化,未说明为何只挑这一分支 | beancount/parser/booking_full.py:944-947;commit
bbdb744b |
| 精确插值默认关闭 | 会改变已有账本补出的数字 | beancount/parser/options.py:700-717 |
| 带成本时禁止再推价格 | get_weight() 存在 Cost 时直接忽略
price,价格不参与权重,无法由残差反推 |
beancount/core/convert.py:84-96;beancount/parser/booking_full.py:986-997 |
| 价格可为零,成本也可为零,只有负成本报错 | 实现只判 units.number == ZERO 与
cost.number < ZERO,未检查
cost.number == ZERO |
beancount/parser/booking_full.py:1029-1047;beancount/parser/booking_test.py:47-55 |
| 现象 | 后果 | 证据 |
|---|---|---|
分类失败的 continue 在外层循环 |
整条交易被剔除,不只是丢分录 | beancount/parser/booking_full.py:164-167 vs
beancount/parser/booking_full.py:252 |
匹配失败的 continue 在内层循环 |
交易保留但少了该组全部分录,可能不平 | beancount/parser/booking_full.py:220-222 |
| 插值错误的去留按返回值分流,不统一处理 | 缺口数
>1:整组消失(out_postings=[]);两个具体错误分支:未补全分录原样并入 |
beancount/parser/booking_full.py:233-235,874,924,997;beancount/parser/booking.py:57-83 |
methods 传 defaultdict
是调用方选择而非硬性要求 |
beancount/parser/booking_full.py:628,555
直接下标取值;换普通 dict 须预填全部会访问的账户键,否则
KeyError——无直接测试覆盖,属源码可推出的边界 |
beancount/parser/booking.py:41-44;beancount/parser/booking_full.py:555,628 |
六处 assert X.currency == weight_currency,非四处 |
三处比 cost.currency、两处比
price.currency、一处比 units.currency;不对抛
AssertionError 中断加载 |
beancount/parser/booking_full.py:926-928,933-935,939-941,960-962,976-978,1000-1002 |
| 自动过账权重为零被静默删除 | 多写的空行不报错直接消失 | beancount/parser/booking_full.py:949-955,1021-1022;beancount/parser/booking_full_test.py:1079-1101,1103-1129 |
COST_PER 遇零数量同样静默删除 |
该分录整条消失,无错误 | beancount/parser/booking_full.py:963-971 |
COST_PER/COST_TOTAL/PRICE
不量化 |
长尾小数进入 Cost,而 Cost 是库存 key
的一部分 |
beancount/parser/booking_full.py:964,979,1003 |
兜底校验只认 Cost 的字段名 |
validate_missing_eliminated 取
cost.number,而 CostSpec 只有
number_per/number_total;"带成本不能推价格"的错误路径原样返回
CostSpec,走到这里会抛 AttributeError |
beancount/parser/booking.py:72-77;beancount/parser/booking_full.py:986-995;beancount/core/position.py:45-66 |
【文档漂移】validate_inventory_booking
说禁止持仓为负 |
实现检查 is_mixed(),纯负库存不报错 |
beancount/parser/booking.py:88-93 vs
beancount/parser/booking.py:124;beancount/parser/booking_test.py:117-128 |
【文档漂移】convert_lot_specs_to_lots
自称"仅为过渡" |
SIMPLE 已删,只剩测试引用 | beancount/parser/booking.py:144-148;CHANGES:1098-1102 |
自我对冲检查被 if False: 关闭 |
不参与生产路径,却仍有 9 个测试 | beancount/parser/booking_full.py:194-200;beancount/parser/booking_full_test.py:2058-2158 |
| 容差按整笔交易算,不分组 | beancount/parser/booking_full.py:171,175
传全集而非分组子集 |
beancount/parser/booking_full.py:171-177 vs
beancount/parser/booking_full.py:184-186 |
| 成本币种与价格币种都显式但彼此不同 | 不验证二者相等,只取成本币种入桶 | beancount/parser/booking_full.py:265-284,356-374,467-483 |
| 减仓/加仓由 ante-inventory 中同币种持仓的符号决定,非数量正负本身 | 空账户里的负数量不算减仓,走增仓分支生成新空头 | beancount/core/inventory.py:186-202;beancount/parser/booking_full.py:629-633;beancount/parser/booking_full_test.py:1687-1719 |
Booking.NONE 在 book_reductions
判定条件里即被排除 |
即使有唯一精确匹配的既有批次也不匹配,直接进增仓分支 | beancount/parser/booking_full.py:628-704;beancount/parser/booking_method.py:250-274 |
| 仅一条自动过账且没有任何已知币种组 | 复制循环零迭代,交易保留但 postings 为空,无错误 |
beancount/parser/booking_full.py:183,252,366-368,450-465,485-486 |
总成本转换与 PRICE 推断的除法无零除保护 |
零 units 时除法先于末尾 "Amount is zero" 检查抛出
Decimal 除零异常 |
beancount/parser/booking_full.py:762-771,986-1006,1027-1047 |
beancount/parser/booking_test.py 有 10 个
test_*
方法:TestInvalidAmountsErrors(:25-67)4
个,锁定四种组合——不带成本的 0 MSFT
不报错,0 MSFT {200.00 USD} 报 "Amount is
zero",{0.00 USD} 不报错,{-200.00 USD} 报
"Cost is
negative";TestBookingValidation(:70-181)6
个,覆盖的全是死函数 validate_inventory_booking。
beancount/parser/booking_full_test.py 3524 行、21 个类共
138 个测试方法。本篇按类名与 docstring
归纳覆盖范围,只逐行读了与论点直接相关的段落(:53-93、:366-423、:594-720、:1079-1266、:1413-1480、:1985-2057、:2706-2740、:3123-3175、:3481-3520),其余按索引取样:
| 测试类 | 行号 / 数量 | 锁定的行为 |
|---|---|---|
TestAllInterpolationCombinations |
:53-93,2 |
穷举解析器对缺失币种/数量/成本/价格字段组合的语法接受情况;只调
parser.parse_string(),不覆盖 booking、匹配或插值结果 |
TestCategorizeCurrencyGroup |
:101-420,12 |
五级推断各分支;多自动过账报错;靠余额消歧;混合成本币种归桶失败 |
TestReplaceCurrenciesInGroup |
:423-575,2 |
自动过账复制进每组;各字段的币种回填 |
TestInterpolateCurrencyGroup |
:594-1266,17 |
四种 MissingType
的公式;两个缺口报错;三种多余自动过账;容差量化 |
TestComputeCostNumber |
:1268-1341,8 |
单价计算八种输入/边界,含 2f14cb5d 的负数量回归 |
TestParseBookingOptions |
:1344-1365,3 |
booking_method 选项的解析与非法值 |
_BookingTestBase 系列 |
:1413-3121 |
账本语法搭的 DSL:#ante 给前置库存、#apply
给待匹配分录,#booked/#reduced/#ex/#ambi-matches/#ambi-resolved
断言各阶段结果(:1439-1477) |
TestBookAugmentations /
TestBookReductions |
:1671-2056,6 + 15 |
加仓不匹配;六道过滤;无匹配、歧义、数量不足报错;省略数量的回归 |
TestHasSelfReductions |
:2058-2158,9 |
已被 if False: 关闭的函数 |
TestBookReductionsSelf |
:2160-2230,4 |
同交易内先加仓后减仓的场景;4 个测试仅第 1
个(:2161-2176)实际执行,其余 3 个标
@unittest.skip 跳过 |
TestBookAmbiguous* |
:2232-2704,9 + 8 + 8 |
NONE/STRICT/FIFO/LIFO
的消歧结果 |
TestBookCrossover(不构成当前覆盖) |
:2706-2735,1 |
整类
@unittest.skip("Crossing is not supported yet…{d3cbd78f1029}")(:2706-2708),与
beancount/parser/booking_full.py:672-674 的 TODO
同标记——保存的是单腿穿越零线拆一减一加的期望行为,不是已锁定的当前行为 |
_TestBookAmbiguousAVERAGE |
:2738-3033,15 |
unittest 正常收集该类,但整类被
@unittest.skip("Booking.AVERAGE is disabled.")(:2737)标记跳过;下划线前缀不影响收集 |
TestBasicBooking / TestStrictWithSize |
:3035-3082,3 + :3084-3121,2 |
前者锁定同日同成本归并、不同日/成本各留批次;后者锁定
STRICT_WITH_SIZE 按数量匹配单批与多批 |
TestBookingApi / TestBook |
:3123-3478,1 + 11 |
对 Booking 每个取值跑
bf.book;匹配整体行为,后者挂
# FIXME: TODO - Rewrite these tests |
TestInterpolationRounding |
:3481-3520,2 |
双容差的开与关 |
未覆盖,按程度分三类。全无调用:initial_balances(_book()
直接赋给 balances
并原地修改,不做复制,beancount/parser/booking_full.py:154-158,249,传普通
dict 遇新账户会在 :249 抛 KeyError)、六处
assert X.currency == weight_currency
的失败路径。执行到但去留未断言:"分类失败丢整条交易" 与
"匹配失败只丢一组"
均被场景测试触发(beancount/parser/booking_full_test.py:1548-1559,1723-1739,1807-1826),但
assertPostings 在有 book_errors
时被跳过(:1557-1559),不验证具体去留。集成执行但无针对性断言:validate_missing_eliminated
随 booking.book()
集成测试(beancount/parser/booking_test.py:25-67)执行过,但无测试直接构造残留
MISSING 断言其自身错误分支。
| 日期 | 提交 / 记录 | 变化 |
|---|---|---|
| 2015-07-23 | 700ad2bc |
booking 逻辑从 parser.grammar 移出,独立成
parser.booking |
| 2015-09-05 | db9d7f14 |
simple 与 full(新)两套 booking 算法代码分拆 |
| 2015-09-06 | 836c914a |
Lot/LotSpec 迁移为
Cost/CostSpec 数据结构,全仓库跟改 |
| 2016-02-06 | CHANGES:2613-2622 |
CHANGES 集中记录该结构迁移:解析器改输出
CostSpec,Posting.position 拆平为
.units/.cost |
| 2016-06-29 | 80c939ce |
修价格插值汇率取反 |
| 2016-08-25 | 5f60b500 |
主干禁用 AVERAGE |
| 2016-10-14 | bbdb744b、cc82c533 |
自动过账按容差量化;加 __tolerances__ |
| 2016-10-23 | 666518e2(21:28)→4581d78e(21:37) |
前者加负成本检查;后者标题写 "zero cost",实加的却是零 units 检查("Amount is zero"),零成本仍不报错 |
| 2016-10-30 | CHANGES:1710-1719 |
FULL 成为默认记账算法 |
| 2017-04-30 | 859f341e |
布局由 src/python/beancount/… 改为
beancount/…,此前 commit 用旧路径 |
| 2017-09-30 | 686d5c3f |
省略 units 数字时减仓匹配不再失败(CHANGES:1254) |
| 2018-03-13 | e30335ef、41b8ef51、0da6a23c |
删 SIMPLE 与 booking_algorithm,convert_*
成死代码 |
| 2018-03-22 | fe6ec215 |
错误信息里的分录序号从 0 改为 1(#249) |
| 2020-09-07 | 2f14cb5d |
修 #364:空头 {per # total} 算出负单价 |
| 2021-01-09 | 76365107 |
负成本检查前先判 Cost.number is not None |
| 2021-02-28 | 0abd49e4 |
加入被 skip 的 TestBookCrossover 为 v3 留文档 |
| 2021-03-13 | 2578ee2c、9ef7d0a7 |
book() 增加 initial_balances 并修其
bug |
| 2026-04-28 | ec057e11 |
双容差:插值用 min、元数据写 max |
| 2026-04-29 | 573f04bc |
删除该说明文件 |
| 2026-05-02 | 5704a861 |
加 use_precise_interpolation,双容差默认关闭 |
beancount/core/interpolate.py(07
篇):infer_tolerances 的
mode(:97)、compute_residual(:72-94)、quantize_with_tolerance(:364-394)、AUTOMATIC_META/AUTOMATIC_TOLERANCES(:236,242)。beancount/core/inventory.py(04
篇):is_reduced_by(:186-202)判定减仓;currencies()/cost_currencies()
供币种推断;key = (units.currency, cost)(:428)决定成本数字如何成为批次身份。beancount/core/position.py(03
篇):提供
CostSpec(模糊成本)、Cost(确定成本)与
Position 三个数据结构;CostSpec →
Cost(第 7、8
节)是本篇一条主线,但币种分组、批次筛选、记账法消歧、插值、交易去留等(第
3-6、8 节)不能归约为此。beancount/parser/booking_method.py:handle_ambiguous_matches(:31-70)按
Booking 分派,数量不足时补 "Not enough lots to
reduce";booking_method_NONE(:250-274,见第
10 节)无条件放行。枚举在
beancount/core/data.py:55-77:STRICT/STRICT_WITH_SIZE/NONE/AVERAGE/FIFO/LIFO/HIFO;Booking.AVERAGE
仍能被解析和分派到,但 booking_method_AVERAGE() 无条件先
return,报错 "AVERAGE method is not
supported"(:277-291),随后的平均成本实现整段被
if False: 关闭(:295-296,376-384)。beancount/parser/grammar.py::519
为裸 {} 造
CostSpec(MISSING, None, MISSING, None, None, False);:586-611
处理复合成本,{{...}} 把 number_per 置
ZERO(:590-606),正是 :916
拦截的值。beancount/parser/options.py::640-660
给全局默认记账法,:700-717
控制双容差。beancount/loader.py::605-606
在解析后、插件前调用,错误并入
parse_errors;"交易不平"的校验在更后面的
beancount/ops/validation.py。beancount/parser/cmptest.py::40-47
两种模式,allow_incomplete=True 走不做匹配的退化转换
_local_booking(:62-68)。beancount/parser/booking.py:1-3 docstring;20-23
BookingError;26-54
book(41-44、47-49、52);57-83
validate_missing_eliminated(72-76);86-136
validate_inventory_booking(86、88-96、119-120、124-134);139-192
convert_lot_specs_to_lots(144-148、178-186);195-224
convert_spec_to_cost(210-221)。
beancount/parser/booking_full.py:1-71
docstring(13-16、23-26、32-42、52-65、66-70);100-105
SelfReduxError;108-127 book;130-254
_book(154-159、164-168、170-179、183-186、194-200、215-222、229-235、238-240、242-250、252);257-262
CategorizationError;265-284
get_bucket_currency;287-293 Refer;296-486
categorize_by_currency(304-321、342-364、356-362、366-368、381-399、401-434、436-448、450-465、467-483、485-486);489-528
replace_currencies(506、509-510、512-525);531-536
ReductionError;539-561
has_self_reduction;564-715
book_reductions(596-619、622-624、629-633、637-657、660-670、672-674、675-680、685-691、700-704、706-711);718-744
compute_cost_number(732-739);747-774
convert_costspec_to_cost(762-771);777 FIXME;780-786
MissingType;790-795
InterpolationError;798-1049
interpolate_group(812-817、823-853、845-850、858-874、883-909、911-955、914-924、944-947、957-971、973-984、986-1006、1012-1022、1025、1029-1047、1049)。
beancount/parser/booking_test.py:19-23、25-67、70-116、117-128、130-142、144-161、163-181。
beancount/parser/booking_full_test.py:39-50、53-93、101-420、423-575、578-591、594-655、659-1266、1079-1129、1173-1236、1238-1266、1268-1341、1344-1365、1413-1477、1548-1559、1568、1599、1651、1671-1764、1687-1719、1766-2056、2046-2056、2058-2158、2160-2230、2232-2392、2394-2548、2550-2704、2706-2735、2737-3033、3035-3085、3087-3121、3123-3149、3137、3152-3478、3481-3520。
其它:beancount/core/data.py:55-77;beancount/core/inventory.py:186-202,428,459;beancount/core/interpolate.py:72-94,97,236,242,364-394;beancount/core/convert.py:84-96;beancount/core/position.py:28-66;beancount/parser/booking_method.py:31-70,250-274,277-296,376-384;beancount/parser/grammar.py:519,586-611,590-606;beancount/parser/options.py:640-660,700-717;beancount/parser/cmptest.py:40-47,62-68;beancount/loader.py:605-606;beancount/scripts/example.py:172-190;pyproject.toml:57;CHANGES:1098-1102,1254,1710-1719,2613-2622。
commit:700ad2bc(2015-07-23)、db9d7f14(2015-09-05)、836c914a(2015-09-06)、80c939ce(2016-06-29)、5f60b500(2016-08-25)、bbdb744b/cc82c533(2016-10-14)、666518e2/4581d78e(2016-10-23)、859f341e(2017-04-30)、686d5c3f(2017-09-30)、e30335ef/41b8ef51/0da6a23c(2018-03-13)、fe6ec215(2018-03-22)、2f14cb5d(2020-09-07)、76365107(2021-01-09)、0abd49e4(2021-02-28)、2578ee2c/9ef7d0a7(2021-03-13)、ec057e11(2026-04-28)、573f04bc(2026-04-29)、5704a861(2026-05-02)。