目录

18 · prices.py:价格表的构建与查询

核对基线 · 范围 · 依赖

核对基线:beancount 仓库 commit 97472138(2026-08-22)。路径相对仓库根目录,引用格式 文件:起-止行 (名称),行号已逐条核对;裸文件名(如 prices.py:xx)与裸 :起-止行 均指本篇范围声明的两个文件,就近归属最近一次出现的完整路径。 本篇范围beancount/core/prices.py(382 行)与 beancount/core/prices_test.py(321 行)。 上游依赖beancount/core/data.pyPrice 指令、Currencyentry_sortkey)、beancount/core/number.pyONEZERO:22,24)、beancount/utils/bisect_key.pybeancount/utils/misc_utils.pysorted_uniquify)。 下游使用者:非测试代码里 6 个文件引用本模块——beancount/core/convert.pyget_price,05 篇)、beancount/ops/summarize.py:506beancount/projects/export.py:176,192beancount/scripts/doctor.py:401beancount/scripts/example.py:1711beancount/api.py:68-71(只导出 PriceMapbuild_price_mapget_latest_priceget_price 四个)。

1. 模块解决什么问题

账本里的 price 指令是离散的:某天有一条 2013-06-01 price USD 1.10 CAD,之后可能几周没有新报价,而估值要回答"任意一天 1 单位 X 值多少 Y"。prices.py 把散落的 Price 记录整理成按币种对索引、按日期排序的 PriceMap,再提供查询函数把任意日期映射到表中某一条。

模块只做建表与查表。取价(联网抓行情)不在范围内:docstring 说价格来自 Price 指令或脚本写入账本(prices.py:5-7),抓价代码后来迁往独立仓库 beanprice(CHANGES:184-186 记录 2020-07-25 的迁移准备工作);估值口径属于 convert.py(05 篇)。

2. 结构一览

名称 位置 作用
get_last_price_entries(entries, date) prices.py:25-43 取给定日期前每个币种对最后一条 Price 指令,非数值
class PriceMap(dict) :46-57 dict 子类,__slots__ = ("forward_pairs",) 多挂一属性
build_price_map(entries) :60-135 建表:过滤、合并反向、去重、生成倒数
project(price_map, from, to, base_currencies=None) :138-244 from 计价的价格投影成 to 计价
normalize_base_quote(base_quote) :247-262 "USD/CAD"("USD","CAD") 归一
_lookup_price_and_inverse(price_map, base_quote) :265-291 内部查键,兼管异常
get_all_prices / get_latest_price / get_price :294-309 / :312-344 / :347-382 整条序列 / 最后一条 / 不晚于给定日期的最后一条

表的形状:(base, quote) -> [(date, Decimal), ...],按日期升序,每个日期只出现一次:76-81);("USD","CAD") -> 1.10 即 1 USD = 1.10 CAD。

3. build_price_map 的四个步骤

3.1 只收 Price 指令

price_entries = [entry for entry in entries if isinstance(entry, Price)]

prices.py:83-84。交易里的 @ 1.10 CAD{564.20 USD} 不会自动进表——它们挂在 Posting.price / Posting.cost 上,不是 Price 指令,补齐靠可选插件 implicit_prices.py(第 6 节)。键取 (price.currency, price.amount.currency)、值取 (price.date, price.amount.number)defaultdict(list) 承接(:86-91)。

3.2 正反向报价合并:点少的一方并入点多的

for base, quote in inversed_units:
    bq_prices = price_map[(base, quote)]
    qb_prices = price_map[(quote, base)]
    remove = (base, quote) if len(bq_prices) < len(qb_prices) else (quote, base)
    base, quote = remove
    remove_list = price_map[remove]
    insert_list = price_map[(quote, base)]
    del price_map[remove]
    inverted_list = [(date, ONE / rate) for (date, rate) in remove_list if rate != ZERO]
    insert_list.extend(inverted_list)

prices.py:100-113。账本同时写过 price USD ... CADprice CAD ... USD 时两个方向各自成键,合并规则是条数少的一方取倒数并入条数多的一方,只留一个方向(:67-70)。条数是去重前的原始条数;相等时 remove(quote, base),即先在账本里出现的方向被保留(inversed_units 按插入序遍历且同时含两个方向,第二遍循环是依赖 defaultdict 才无害的空转,:93-98)。

3.3 同一天多条:保留最后一条

sorted_price_map = PriceMap(
    {
        base_quote: list(
            misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True)
        )
        for (base_quote, date_rates) in price_map.items()
    }
)

prices.py:115-123sorted_uniquifymisc_utils.py:203-231)先按 key 排序再去重,last=True 取同 key 的最后一个。key 只取日期、Python 排序稳定,"最后一个"即列表里最后出现的那条;加载器已按 data.entry_sortkeydata.py:708-719,同日按 lineno 排)排序,等价于"账本里写在最后的那条生效",prices_test.py:212-231 (test_ordering_same_date) 以注释 "The last one to appear in the file should be selected" 锁定。理由写在 Price 指令的 docstring 里:beancount 不存时刻只存日期,同日多价本身无意义,且声明不打算解决(data.py:377-381)。

3.4 反向汇率的生成与零价格防护

forward_pairs = list(sorted_price_map.keys())
for (base, quote), price_list in list(sorted_price_map.items()):
    # Note: You have to filter out zero prices for zero-cost postings, like
    # gifted options.
    sorted_price_map[(quote, base)] = [
        (date, ONE / price) for date, price in price_list if price != ZERO
    ]

prices.py:125-132。每个存活方向自动生成反向键,逐条取倒数;Decimal 除法不量化,默认 28 位有效数字,反向汇率通常是长尾小数(prices_test.py:264-270 的期望值即 28 位)。

零价格在两处被过滤,都是补丁而非一次性设计::112(合并分支)来自 2018-04-18 commit 2dd25c8b,修 #157(CHANGES:908-909);:131(反向生成)来自 2015-04-21 commit 68f73905,提交信息写 "Now that zero-cost is supported, we have to make sure to ignore those price entries"。两处都跳过该条而非填哨兵,后果是正反向列表长度可以不等

forward_pairs 记录反向键自动生成前、经 3.2 正反向合并后仍存活的方向键(:46-57,126,134),只是生成前那一刻键列表的快照,不保存指令来源信息——同一个方向下的价格可能是账本原文,也可能来自 implicit_prices 等插件合成或脚本写入,PriceMap 无从区分。它由 2014-05-18 commit 79c9ff34 引入,供当时的 web 界面枚举价格时排除反向对;v3 删掉 web 后只剩写入点。

4. 查询:阶梯取值,不插值

4.1 键的归一与内部查找

normalize_base_quote:247-262)接受 ("USD","CAD")"USD/CAD",字符串按 / 切分并 assert 恰好两段(prices_test.py:54-58 锁定三段抛 AssertionError)。

try:
    return price_map[base_quote]
except KeyError:
    base, quote = base_quote
    prices = price_map.get((quote, base), None)
    if prices:
        return prices
    else:
        raise

prices.py:283-291 (_lookup_price_and_inverse)。正向键缺失时回退查反向键,但返回的列表不取倒数,数值方向是错的,docstring 用大写 INTERNAL 标注它只供内部使用(:269-270)。它无害是因为 build_price_map 保证两个方向同时存在,回退分支对正常构建的表不命中;真正目的是异常信息——正反都查不到时用裸 raise 重抛正向键KeyError,这是 2014-05-18 commit f8cc61bf 的意图,prices_test.py:107-119 断言异常文本含 ('EUR', 'USD')

4.2 get_price 的日期语义

if date is None:
    return get_latest_price(price_map, base_quote)
base_quote = normalize_base_quote(base_quote)
base, quote = base_quote
if quote is None or base == quote:
    return (None, ONE)
try:
    price_list = _lookup_price_and_inverse(price_map, base_quote)
    index = bisect_key.bisect_right_with_key(price_list, date, key=lambda x: x[0])
    if index == 0:
        return None, None
    else:
        return price_list[index - 1]
except KeyError:
    return None, None

prices.py:364-382。四条语义:date=None 退化为 get_latest_price 取最后一条;给定日期用 bisect_right_with_keybisect_key.py:55-78)取不晚于该日期的最后一条——价格是阶梯函数,报价在下一条出现前一直有效,中间不插值;早于首条报价(index == 0)与键不存在都返回 (None, None)base == quote(或 quote is None)短路返回 (None, ONE),不查表。

线性插值曾有接口但从未实现:2014 年的 get_priceinterpolation=PREVIOUS 参数与 PREVIOUS, LINEAR = object(), object() 哨兵,2014-05-13 commit 59416619 连同测试末尾的 __incomplete__ = True 一并删除,此后阶梯是唯一语义。prices_test.py:166-210 (test_get_price) 用 7 个日期点锁定:查询日落在两条报价之间时返回前一条及前一条的日期

get_latest_price:312-344)除同样的短路分支外,把 KeyError 与空列表统一成 (None, None):337-344)。get_all_prices:294-309)不短路也不吞异常,键查不到即抛 KeyErrorprices_test.py:146-148),且返回表里那个列表本身而非副本。

4.3 get_last_price_entries

for entry in entries:
    if date is not None and entry.date >= date:
        break
    if isinstance(entry, Price):
        price_entry_map[(entry.currency, entry.amount.currency)] = entry
return sorted(price_entry_map.values(), key=data.entry_sortkey)

prices.py:36-43(原文分两行取 base_quote)。它不碰 PriceMap,返回 Price 指令,供 ops/summarize.py:506 截断账本时把截断日之前每个币种对的最后一条价格插回,使截断后仍能估值。上界是开区间(>= 即 break),date=None 取到最后(该分支来自 2014-07-10 commit dfae4a27)。

5. project:跨报价币种投影

project:138-244)解决"表里有 (HOOL, USD)(USD, CAD),想直接查 (HOOL, CAD)":遍历 quote == from_currency 的键,对每条价格查一次 from/to 汇率,相乘后插入 (base, to_currency),同时插入取倒数的 (to_currency, base):216-242)。原表不改,先做浅复制(:193-194);from == to 直接返回原对象(:190-191prices_test.py:246-247assertIs 锁定)。

docstring 列了四条限制(:157-177),关键一条带标记 {c1bd24f8d4b7}只在基础品种已有报价点的日期上投影(USD,CAD) 在 A、C 两天变动而 HOOL 只在 B 天有报价时,不会在 A、C 合成新的 HOOL/CAD 点;替代方案是多次查表,即 convert.convert_amountviaconvert.py:215-229)。这类花括号标记(另有 {b2b23353275d}{97a5703ac517}{4bb702d82c8a})在源码与测试里成对出现。

它与 build_price_map 在不变量上不一致,两处:日期不再唯一——后者承诺每个日期只出现一次(:79-80),project 只做 extend + sort 不去重(:233-242),目标键已有同日价格时投影那条照样加入,prices_test.py:298-317 (test_project_collisions)("HOOL","CAD") 就有两条 2013-06-15forward_pairs 丢失——:194 新建的 PriceMap 从未赋该属性。

防冲突判断本身也不成立:

rate_date, rate = get_price(price_map, currency_pair, date)
...
if rate_date in existing_prices:
    # Skip collisions in date. {97a5703ac517}
    continue

prices.py:219-226existing_prices 是目标键上已有价格的日期集合:209-214),rate_date 却是那条汇率的日期,而非当前被投影价格的日期 date——这段判断实际比较的是"目标日期是否等于所用汇率的生效日",与"是否等于当前投影价格的日期"是两回事,因此不能可靠拦住同日冲突:test_project_collisionsrate_date2013-06-01date2013-06-15,投影照样插入。这段判断表达式自 2020-11-01 commit effb4701 引入起未变。forward_pairs 缺失也是同一来源:effb4701 引入时 :194 返回的就是普通 dict,读取该属性已抛 AttributeError;2024-12-23 commit 0f879811(标题 "enable mypy")把该容器改成 PriceMap:194),但仍未初始化 forward_pairs,只是把原因换成未赋值的 slot,外部报错行为不变,其余改动属格式化与类型标注。docstring 说"投影价与已有价同日时不覆盖后者"(:164-165),实际两条并存,查询时 bisect_right 取同日最后一条,而列表按 (date, number) 排序,胜出的是数值较大那条;测试里原有价 1125.00 大于投影价 1120.0000,结论才恰好成立。

另有几处偏差见第 8 节:base_currencies 传空集合时短路(:206)、零价格在反向表里填 ZERO 而非跳过(:240)、遍历反向键时产出 (to, to) 自反键、对同一结果重复调用不去重。仓库内除测试外无 project 的调用点。

6. 哪些价格不会自动进表

交易里的 @{} 要经 beancount/plugins/implicit_prices.py 合成为 Price 指令才能进表。规则(implicit_prices.py:65-89):写了 price@)就用 units.currencyposting.pricePrice,元数据 __implicit_prices__ = "from_price";没写 price 但有 cost,且该腿在该账户 add_position() 的判定结果不是 MatchResult.REDUCED,则用成本造,元数据值 "from_cost":44-57,72-89)。紧邻代码留有 TODO(:77-79):账户没有 booking strategy 时减仓判断是否仍会失效、从而把成本插成价格,作者标注待查,未收窄这一边界。

插件不在 loader.py 默认插件列表(loader.py:53-68)中;显式配置 beancount.plugins.implicit_prices 会启用该插件,配置 beancount.plugins.auto 也会通过 loader.combine_plugins(auto_accounts, implicit_prices) 间接包含它(auto.py:13-17)。2015-07-11 的变更把它移出默认列表并迁到 beancount.plugins,理由见 CHANGES:3549-3560。元数据字段是 2020-09-12 commit c142e339 加的(CHANGES:155-156)。

去重键是 (date, currency, number, cost-currency):92-97),含数值,所以同日同货币对的多个不同价格全部保留、一条都不报错,只有数值也相同的重复才折叠。代码里留着被注释掉的报错逻辑与解释:股票拆分、同日两笔不同成交价都是合法场景,"Keeping both for now",并指出理想情况下去重键不该含数值(:101-119),这段自 2014-07-05 commit d9d65159 就在。严格模式另有可选插件 plugins/unique_prices.py,同日同货币对出现不同数值即报 UniquePricesError,其 docstring 称这是 "very strict mode"、可能不现实,故只做成插件(unique_prices.py:1-13,46-60)。这些重复最终由 3.3 步收敛成每日一条。

7. 设计决策与理由

决策 理由 证据
表里同时存正向与反向键 查询方不必关心账本写哪个方向 prices.py:49-51,125-132
两个方向都有报价时合并,保留点多的一方 docstring 称目标是把两套序列"reconciled into a single one",避免查询结果因方向不同而不一致;为何具体选点少的一方取倒数并入,源码未说明 :67-70,100-113,105
同一天多条只留最后一条 不存时刻,同日多价无从排序,作者声明不解决 :63-65data.py:377-381
取值用阶梯而非插值 曾有 interpolation/LINEAR 接口但从未实现,59416619 直接删除;提交信息("Removed linear interpolation feature.")未说明选阶梯而非插值的动机 59416619prices.py:376-380
base == quote 短路返回 ONE 源码注释:处理"currency priced into its own"这一退化情形,不查表 :330,369
零价格在生成倒数时跳过 零成本持仓(赠予期权)合法,取倒数会除零 :112,128-13268f739052dd25c8b
交易里的 @/{} 不自动进表 build_price_map 只消费已存在的 Price 指令,成交价与收盘价语义可能不同 :83-84unique_prices.py:6-12
implicit_prices 不设为默认插件 由用户显式选择成交价是否进价格库 CHANGES:3549-3560
隐含价格重复不报错 拆分、同日多笔成交都合法,暂无更好方案 implicit_prices.py:101-107
查表失败返回 (None, None) 缺价是常态,估值路径要能退回 :337-344,377-382convert.py:154-156
get_all_prices 反而抛 KeyError 语义是取出该序列,不存在即调用错误 :294-309prices_test.py:146-148

8. 行为细节与边界

现象 后果 证据
正反向列表长度可不等 某日报价为 0 时正向留 (date, 0),反向缺这天 prices.py:130-132
base == quote 的报价(如 price EFA 57.53 EFA)自成反向对 该键进入 inversed_unitsremoveinsert 指向同一列表且随后被 del,整个币种对从表里消失 :97-113prices_test.py:96-105 只断言非 None
反向汇率不量化 1/1.12 展开成 28 位有效数字,逐级换算累积长尾 :131prices_test.py:264-270
合并按去重前条数比较,且合并早于去重 一方 3 条同日报价按 3 点计;被并入的反向条目排在尾部,同日两向都有报价时点少一方胜出 :103-105,113,115-123
_lookup_price_and_inverse 回退不取倒数 手工拼的单向表经 get_* 查询得到方向相反的数值,正常构建的表不命中 :283-291,269-270
get_all_prices 返回表内列表本身 调用方修改会改到表,project 故显式浅复制 :309:194
get_price 返回报价日期而非查询日期,base == quote 时日期位是 None convert.get_valueoutput_date_prices 记的即报价日;自反查询无法判断新鲜度 :333,372,380convert.py:149-151
【文档漂移】build_price_map docstring 说输入 "hopefully including some Price and/or Transaction entries" 代码只过滤 Price;这是 2014-06-07 commit c9dd4062 前的遗留,那时本模块自己从交易合成价格 :73-74,83-84
【文档漂移】project docstring 说同日冲突不覆盖已有价格 实际两条并存,查询取排序后最后一条即数值较大那条 :164-165,233-242
project 的冲突判断比较 rate_date 而非 date 判断几乎不成立,同日重复照常插入 :219-226prices_test.py:298-317
project 返回值无 forward_pairs,且产出 (to, to) 自反键 前者读属性抛 AttributeError;后者被 get_price/get_latest_pricebase == quote 短路忽略,但 get_all_prices 没有该短路,直接查询会返回该键下的合成列表而非报错——project 除测试外无调用点(第 5 节),此差异只影响直接组合调用两者的场景 :57,194,198-242,294-309,330-333,369-372
project(..., base_currencies=set()) 不限制;零价格填 ZERO 而非跳过;对同一结果重复调用相同参数不去重 空集合是假值致条件短路;零价填充与 build_price_map 不一致,反向表出现 (date, 0)extend+sort 不做按日期去重,二次调用会把同批点再插一遍 :206:240 vs :131:233-242
normalize_base_quote 只在字符串输入路径断言切分结果恰为两段 非字符串输入既不检查长度也不检查元素类型,一元/三元 tuple 或非字符串元素均原样透传;assertpython -O 下整体失效 :257-262
forward_pairs 无读取点,get_last_price_entriesbreak 前者在 v3 删除 web 后成只写属性;后者依赖输入按日期有序,上界为开区间 prices.py:51,54,57,126,134:37-39
同日多条的"最后一条"由 lineno 决定 include 时行号不反映全局顺序 data.py:719prices.py:119

9. 测试锁定了什么

prices_test.py 共 12 个测试,分 TestPriceEntries:13)与 TestPriceMap:53)两类。除 test_normalize_base_quote 外全部用 @loader.load_doc() 从 docstring 加载账本,该装饰器默认断言加载无错误(loader.py:760-794)。

测试 行号 锁定的行为
test_get_last_price_entries 15-50 上界开区间;晚于全部报价取最后一条;早于全部则空
test_normalize_base_quote 54-58 元组透传;"USD/CAD" 切分;三段抛 AssertionError
test_build_price_map 60-94 键集恰是正反两个;同日三条取最后;合并后 5 个点;反向同长
test_build_price_map_zero_prices 96-105 含零价格的账本不抛异常(只断言非 None
test_lookup_price_and_inverse 107-119 正反都查不到时异常文本含正向键
test_get_all_prices 121-148 序列完整有序;反向等长;未知键 KeyError
test_get_latest_price 150-164 取最后一条;未知键 (None, None)
test_get_price 166-210 7 个日期点的阶梯语义;返回报价日期;date=None 取最新;未知键 (None, None)
test_ordering_same_date 212-231 同日多条按文件顺序取最后一条
test_project 233-279 from == to 返回原对象;原表不改;不合成中间日期点;反向表同步;base_currencies 生效;有序
test_project_missing 281-296 汇率首条前的基础价格不投影
test_project_collisions 298-317 同日冲突两条并存

未覆盖:quote is None 短路分支;get_latest_price()base == quote 分支由 convert_test.py:test_convert_amount__noopget_price(date=None) 间接覆盖,get_price() 在给定具体日期时的同名短路分支未覆盖;get_last_price_entries(entries, date=None) 分支,本文件四次调用均传入具体日期;_lookup_price_and_inverse 反向回退的成功路径;零价格账本建表后的实际键集与内容;forward_pairs 的取值;get_latest_price() 键存在但列表为空的分支(:150-164 只测非空列表与缺键两种情形,两者结果恰好都是 (None, None));normalize_base_quote 非字符串输入的边界——一元/三元 tuple、非字符串元素、python -Oassert 失效(:54-58 只测合法二元 tuple 与字符串路径);projectforward_pairs、产出自反键、生成反向投影时零价格填 ZERO:238-241,全部 project 测试数据无零价格,零价格只见于 :97-105 的建表测试)、base_currencies=set()、连续调用两次的重复投影;test_project_collisions 只用 get_all_prices 断言两条同日报价并存,未锁定文档所述"随后 get_price() 取数值较大者胜出"这一组合行为;get_all_prices 返回列表可被外部修改。

10. 演变史

2017-04-30 的 commit 859f341e 之前仓库布局带 src/python/ 前缀;下表中 2016-12-24 5a818e47 之前的提交对应路径是 src/python/beancount/ops/prices.py5a818e47 之后到 859f341e 之前是 src/python/beancount/core/prices.py

日期 提交 / 记录 变化
2014-05-13 59416619 删除线性插值:去掉 interpolation 参数与 PREVIOUS, LINEAR 哨兵,该功能从未实现
2014-05-18 f8cc61bf 加入 _lookup_price_and_inverse,保证异常报正向键
2014-05-18 79c9ff34 加入 PriceMap 子类与 forward_pairs,供 web 界面区分正向与反向对
2014-06-07 c9dd4062 build_price_map 改为只过滤 Price 指令;从交易合成价格拆成独立函数
2014-07-05 d9d65159 合成逻辑迁出本模块成为插件 ops.implicit_prices,"重复价格暂不报错"注释同时写下
2014-07-10 dfae4a27 get_last_price_entries 支持 date=None
2015-04-21 68f73905 生成反向汇率时过滤零价格,修除零
2015-07-10 02d4286486b775d5CHANGES:3543-3560(归入 2015-07-11 记录段) implicit_prices 移出默认列表并迁到 beancount.plugins
2016-12-24 5a818e47 模块从 beancount.ops.prices 迁到 beancount.core.prices,以便 core.convert 依赖
2017-01-15 722d09fadf2cf311CHANGES:1515-1524 删除本模块的 convert_amountget_position_market_valueget_inventory_market_value,估值职责交给 convert.py
2018-04-18 2dd25c8bCHANGES:908-909 合并反向报价时也过滤零价格,修 #157
2020-09-12 c142e339CHANGES:155-156 隐含价格指令带 __implicit_prices__ 元数据
2020-11-01 effb4701CHANGES:124-125 新增 project(),修 #255
2020-11-15 2e8291cd inventory.Booking 改名 MatchResult,插件的减仓判断随之改写
2024-12-23 0f879811 project() 内部返回容器由 dict 改为 PriceMap(属 "enable mypy" 一次性改动的一部分),未同步初始化 forward_pairs

11. 与其他模块的关系

12. 参考索引

beancount/core/prices.py:1-8 docstring;13-22 导入;25-43 get_last_price_entries(36-39 上界与提前退出;43 排序返回);46-57 PriceMap(49-51 键含正反向;57 __slots__);60-135 build_price_map(62-70 合并与去重规则;73-74 输入描述;76-81 返回描述;83-84 只收 Price;86-91 累加;93-98 找反向对;100-113 合并;105 保留判定;112 零价过滤;115-123 排序去重;125-132 生成反向;128-129 零价注释;134-135 forward_pairs 与返回);138-244 project(144-156 用途;157-177 四条限制;189-191 退化;193-194 浅复制;196-207 遍历与限制;209-214 已有日期集合;216-230 逐条投影;219 查汇率;224-226 冲突跳过;232-242 排序与反向;240 零价填 ZERO);247-262 normalize_base_quote;265-291 _lookup_price_and_inverse(269-270 INTERNAL;283-291 回退与重抛);294-309 get_all_prices;312-344 get_latest_price(330-333 短路;337-344 异常与空表);347-382 get_price(364-365 无日期;369-372 短路;374-382 二分与失败)。

beancount/core/prices_test.py:13、15-50、53、54-58、60-94、96-105、107-119、121-148、150-164、166-210、212-231、233-279、281-296、298-317。

其它beancount/core/data.py:30,372-395,377-381,705,708-719beancount/core/number.py:22,24beancount/core/convert.py:109-156,149-151,190-232,215-229beancount/utils/bisect_key.py:55-78beancount/utils/misc_utils.py:203-231beancount/plugins/implicit_prices.py:15,21,65-89,92-97,101-119beancount/plugins/unique_prices.py:1-13,46-60beancount/ops/summarize.py:505-506beancount/projects/export.py:174-186,188-200beancount/scripts/doctor.py:401beancount/scripts/example.py:1711beancount/api.py:68-71,135,142,155-156beancount/loader.py:53-67,760-794CHANGES:124-125,155-156,184-186,908-909,1515-1524,3543-3560

commit59416619(2014-05-13)、f8cc61bf(2014-05-18)、79c9ff34(2014-05-18)、c9dd4062(2014-06-07)、d9d65159(2014-07-05)、dfae4a27(2014-07-10)、68f73905(2015-04-21)、02d4286486b775d5(2015-07-10)、5a818e47(2016-12-24)、722d09fadf2cf311(2017-01-15)、859f341e(2017-04-30)、2dd25c8b(2018-04-18)、c142e339(2020-09-12)、effb4701(2020-11-01)、2e8291cd(2020-11-15)、0f879811(2024-12-23)。