Beancount 的库存系统是一个强大功能,用于追踪随时间买卖的资产,如股票、共同基金或外币。它能够精确追踪成本基础,这对于计算资本利得和理解投资组合表现至关重要。本教程涵盖在账本中管理库存的核心机制。
核心概念
库存管理的核心在于追踪仓位。一个“仓位”就是账户中持有的某种商品的数量。Beancount 区分两种基本类型的仓位。
仓位类型
-
简单仓位(无成本):这是一个标准的余额过账。它表示某种商品的数量,不附带任何获取成本。适用于现金或简单的余额断言。
Assets:Bank:Checking 100.00 USD -
带成本基础的仓位:这种仓位不仅包括单位数量和商品,还包括获取时的成本。这是库存追踪的基础。成本在花括号
{}内指定。Assets:Invest:VTSAX 10 VTSAX {100.00 USD, "lot-1"}在此示例中,我们持有 10 个单位的
VTSAX。每个单位的获取成本为 100.00 美元。这一特定批次的股票被标识为一个“批次”。
库存操作
你可以对库存执行两种主要操作:
-
增加(添加到库存):当你买入一种商品时,你增加了库存。你创建一个具有特定单位数量和成本基础的新批次。
2024-01-15 * "Buy shares" Assets:Invest:STOCK 50 STOCK {25.00 USD, "lot-1"} Assets:Bank:Checking -1250.00 USD这里,我们以每单位 25.00 美元的成本买入 50 个单位的
STOCK。这在Assets:Invest:STOCK账户中创建了一个批次。 -
减少(从库存中移除):当你卖出一件商品时,你减少了库存。你必须指定从哪个批次卖出。这是通过提供花括号中的匹配信息来完成的。
2024-01-20 * "Sell shares" Assets:Invest:STOCK -25 STOCK {25.00 USD} Assets:Bank:Checking 625.00 USD在此交易中,我们从以每单位 25.00 美元买入的批次中卖出 25 个单位的
STOCK。
记账方法
当你减少库存时,如果有多个批次匹配减少条件,Beancount 需要一个规则来决定从哪个批次扣除。这个规则称为“记账方法”。你可以通过选项为整个文件设置默认方法,或者在 open 指令中为某个账户指定其自己的方法。
Beancount 3.2.3 接受七种方法名称:STRICT(默认)、STRICT_WITH_SIZE、NONE、FIFO、LIFO、HIFO 和 AVERAGE。其中六种已实现;AVERAGE 可以解析,但一旦需要记账减少时会引发错误,如下面的 AVERAGE 部分所示。
1. STRICT(默认)
STRICT 方法是默认且最安全的记账方法。它强制进行明确且无歧义的匹配。
2024-01-01 open Assets:Invest:STOCK "STRICT"- 需要精确批次匹配:减少过账的成本说明符(
{...})必须标识单个批次——按成本、获取日期、标签或其任意组合。 - 歧义匹配报错:如果说明符匹配多个批次,Beancount 会引发
AmbiguousMatchError,而不是猜测。 - 例外:如果减少操作恰好移除了说明符匹配的总单位数,允许使用空说明符(
{}),减少操作会在这些批次之间分配。
此账本持有两个批次,并通过指定成本来卖出其中一个,这是无歧义的:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Strict STK "STRICT"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy the first lot"
Assets:Broker:Strict 10 STK {100.00 USD}
Assets:Broker:Cash -1000.00 USD
2024-02-10 * "Buy the second lot"
Assets:Broker:Strict 10 STK {120.00 USD}
Assets:Broker:Cash -1200.00 USD
; The cost identifies exactly one lot, so STRICT is satisfied.
2024-06-01 * "Sell the $120.00 lot"
Assets:Broker:Strict -10 STK {120.00 USD} @ 150.00 USD
Assets:Broker:Cash 1500.00 USD
Income:Gains它加载无错误,将 300.00 美元的收益记入 Income:Gains,账户中留有 10 STK {100.00 USD}。
将最后一个过账替换为空说明符,同一文件将失败:
; Rejected under STRICT: "-10 STK {}" matches both lots.
2024-06-01 * "Sell 10 shares"
Assets:Broker:Strict -10 STK {} @ 150.00 USD
Assets:Broker:Cash 1500.00 USD
Income:GainsBeancount 报告 Ambiguous matches for "-10 STK {}" 并列出了候选批次。不过,卖出整个仓位是可以的,因为没有剩余选择:
; Allowed under STRICT: -20 STK is the entire holding, so the empty
; specifier is split across both lots.
2024-06-01 * "Close the position"
Assets:Broker:Strict -20 STK {} @ 150.00 USD
Assets:Broker:Cash 3000.00 USD
Income:Gains这将记入 800.00 美元的收益——3000.00 美元的收益减去 1000.00 美元 + 1200.00 美元的成本——账户清空。这是 STRICT 本身的属性,不需要切换到 STRICT_WITH_SIZE。
2. FIFO(先进先出)
FIFO 方法自动将减少与最早的可用批次对冲。
2024-01-01 open Assets:Invest:STOCK "FIFO"- 自动解析:通过选择最旧的匹配批次来解决歧义。
- 时间顺序匹配:你假设卖出的是持有时间最长的资产。若干税务机关在你未指定批次时将其视为默认方法。
3. LIFO(后进先出)
LIFO 方法则相反。它将减少与最新的可用批次对冲。
2024-01-01 open Assets:Invest:STOCK "LIFO"- 逆时间顺序:它选择最近获取的匹配批次。
- 最新,而非最贵:LIFO 仅按获取日期选择。当价格一直上涨时,它碰巧会卖出成本最高的股票,但如果你的最新批次是最便宜的——下面的示例正是为了展示这一点——LIFO 将实现最大的收益,而非最小。始终卖出最贵股票的方法是下面描述的
HIFO。
4. HIFO(最高进,先出)
HIFO 方法将减少与最贵的可用批次对冲,无论其日期如何。
2024-01-01 open Assets:Invest:STOCK "HIFO"- 按成本排序匹配:它选择成本基础最高的匹配批次。
- 最小已实现收益:对于给定的卖出价格,卖出成本最高的股票实现最小的收益(或最大的亏损)。是否可以使用它取决于司法管辖区——例如,在美国,选择批次需要在卖出时进行具体指定——因此请将其视为记账机制,并单独确认税务选择。
5. 比较 FIFO、LIFO 和 HIFO 在相同批次上的表现
这三种方法仅在最早、最新和最贵的批次是三个不同的批次时有所不同。此账本正好安排了这种情况——批次 A 是最早的,批次 C 是最新的,中间批次 B 是最贵的——然后从三个仅在记账方法上不同的账户中卖出 10 股:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Fifo STK "FIFO"
1970-01-01 open Assets:Broker:Lifo STK "LIFO"
1970-01-01 open Assets:Broker:Hifo STK "HIFO"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
; Lot A - the oldest, at $100.00 per share
2024-01-10 * "Buy lot A"
Assets:Broker:Fifo 10 STK {100.00 USD}
Assets:Broker:Lifo 10 STK {100.00 USD}
Assets:Broker:Hifo 10 STK {100.00 USD}
Assets:Broker:Cash -3000.00 USD
; Lot B - the most expensive, at $120.00 per share
2024-02-10 * "Buy lot B"
Assets:Broker:Fifo 10 STK {120.00 USD}
Assets:Broker:Lifo 10 STK {120.00 USD}
Assets:Broker:Hifo 10 STK {120.00 USD}
Assets:Broker:Cash -3600.00 USD
; Lot C - the newest, at $90.00 per share
2024-03-10 * "Buy lot C"
Assets:Broker:Fifo 10 STK {90.00 USD}
Assets:Broker:Lifo 10 STK {90.00 USD}
Assets:Broker:Hifo 10 STK {90.00 USD}
Assets:Broker:Cash -2700.00 USD
; Sell 10 shares out of each account at $150.00 and let each
; account's booking method choose which lot leaves.
2024-06-01 * "Sell 10 shares from each account"
Assets:Broker:Fifo -10 STK {} @ 150.00 USD
Assets:Broker:Lifo -10 STK {} @ 150.00 USD
Assets:Broker:Hifo -10 STK {} @ 150.00 USD
Assets:Broker:Cash 4500.00 USD
Income:Gains它加载零错误,并总计记入 1400.00 美元的收益,分配如下:
| 账户 | 方法 | 记账的批次 | 成本基础 | 已实现收益 | 剩余批次 |
|---|---|---|---|---|---|
Assets:Broker:Fifo | FIFO | 批次 A, 2024-01-10 | $100.00 | $500.00 | 10 @ $120.00, 10 @ $90.00 |
Assets:Broker:Lifo | LIFO | 批次 C, 2024-03-10 | $90.00 | $600.00 | 10 @ $100.00, 10 @ $120.00 |
Assets:Broker:Hifo | HIFO | 批次 B, 2024-02-10 | $120.00 | $300.00 | 10 @ $100.00, 10 @ $90.00 |
LIFO 行值得关注:它实现了三者中最大的收益,因为最新批次也是最便宜的。
6. STRICT_WITH_SIZE
STRICT_WITH_SIZE 是 STRICT 加上一个额外的决胜规则:当多个批次匹配时,如果恰好有一个批次持有你要移除的精确单位数量,则选择该批次。
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Sized STK "STRICT_WITH_SIZE"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy 10 shares"
Assets:Broker:Sized 10 STK {100.00 USD}
Assets:Broker:Cash -1000.00 USD
2024-02-10 * "Buy 7 shares"
Assets:Broker:Sized 7 STK {120.00 USD}
Assets:Broker:Cash -840.00 USD
; Only one lot holds exactly 7 units, so the empty specifier resolves.
2024-06-01 * "Sell 7 shares"
Assets:Broker:Sized -7 STK {} @ 150.00 USD
Assets:Broker:Cash 1050.00 USD
Income:Gains这将记入 210.00 美元的收益,针对 $120.00 的批次。在 open 行使用 "STRICT" 的相同文件会因 Ambiguous matches for "-7 STK {}" 而失败。
7. AVERAGE(接受但未实现)
AVERAGE 是一个有效名称——option "booking_method" "AVERAGE" 和 open … "AVERAGE" 都能解析——但 Beancount 3.2.3 背后没有实现。这里的所有内容在卖出前都能加载:
1970-01-01 commodity STK
1970-01-01 open Assets:Broker:Avg STK "AVERAGE"
1970-01-01 open Assets:Broker:Cash USD
1970-01-01 open Income:Gains USD
2024-01-10 * "Buy 10 shares at $10.00"
Assets:Broker:Avg 10 STK {10.00 USD}
Assets:Broker:Cash -100.00 USD
2024-02-10 * "Buy 10 more at $8.00"
Assets:Broker:Avg 10 STK {8.00 USD}
Assets:Broker:Cash -80.00 USD
; An average-cost engine would book this at $9.00 per share. This one refuses.
2024-06-01 * "Sell 5 shares"
Assets:Broker:Avg -5 STK {}
Assets:Broker:Cash 45.00 USD
Income:Gains一旦该减少操作需要记账,加载器会停止并报:
AVERAGE method is not supported不要围绕它规划账本。如果你今天想要平均成本的行为,请将仓位保留在 NONE 账户中并自行计算平均值,或者追踪每个批次并接受批次级别的收益。
8. NONE
NONE 方法完全禁用批次匹配。
2024-01-01 open Assets:Invest:STOCK "NONE"- 无批次匹配:Beancount 不会尝试将减少与增加匹配。
- 允许混合符号:这允许一个账户同时持有同一商品的正负余额。这种行为类似于 Ledger CLI 工具处理商品的方式。
批次规格
一个“批次”是在特定时间和价格获取的特定商品块。当你创建或减少仓位时,你可以详细指定其批次属性。
完整规格
当你增加库存(买入)时,你可以在单对花括号内指定最多三个属性,用逗号分隔:
Assets:Invest:STOCK 10 STOCK {100.00 USD, 2024-01-15, "lot-identifier"}100.00 USD— 成本基础,以每单位表示。2024-01-15— 获取日期。当你省略时,Beancount 会从交易日期自动填写,这就是上面的错误消息在每个批次上都显示日期的原因。"lot-identifier"— 一个可选的字符串标签。
虽然这三个都是可选的,但提供至少成本基础是标准做法。花括号必须保持在一行内,账本中的注释以 ; 开头,绝不是 #。
匹配方法
当你减少库存(卖出)时,你使用相同的语法来指定从哪个批次卖出。
-
按成本匹配:这是最常见的方法。
Assets:Invest:STOCK -5 STOCK {100.00 USD} -
按日期匹配:如果成本相同,你可以使用获取日期来消除歧义。
Assets:Invest:STOCK -5 STOCK {2024-01-15} -
按标签匹配:标签提供了识别批次的万无一失的方法。
Assets:Invest:STOCK -5 STOCK {"lot-identifier"} -
将批次留给记账方法:空花括号
{}不指定批次,因此由账户的记账方法选择。在FIFO、LIFO或HIFO下,这是最旧、最新或最贵的匹配批次;在默认的STRICT下,除非减少操作恰好清空匹配的批次,否则会是一个AmbiguousMatchError。Assets:Invest:STOCK -5 STOCK {}
价格处理
理解成本基础({})和价格(@)之间的区别至关重要。它们服务于不同的目的,不可互换。
价格与成本
{cost}:定义资产的获取成本。它是库存批次本身的一部分,用于记账减少和计算资本利得。@ price:在交易时记录市场价格的注解。它用于货币转换或在特定日期记录市场价值。
以下是三种场景:
-
价格注解(转换):使用
@将一种货币转换为另一种。Assets:Forex 1000 USD @ 0.85 EUR -
成本基础(获取):买入资产时使用
{}来确立其成本。Assets:Invest 10 STOCK {100.00 USD} -
两者兼用(带价格记录的卖出):卖出资产时,使用
{}标识要卖出的批次,并使用@记录卖出价格。这允许自动计算资本利得。Assets:Invest -10 STOCK {100.00 USD} @ 105.00 USD此条目从成本为每股 $100.00 的批次中卖出 10 个
STOCK,卖出价格为每股 $105.00。
价格使用规则
- 价格注解(
@)不影响哪个批次被记账。批次匹配完全由成本基础({})和账户的记账方法处理。 @符号仅用于:
- 货币转换。
- 在交易时记录资产的市场价值。
- 为资本利得计算提供卖出价格。
配置
你可以在全局或按账户基础上配置记账方法。
全局记账方法
你可以使用 option 指令为整个 Beancount 文件设置默认记账方法。
option "booking_method" "STRICT"接受的值是 "STRICT"(未设置时的默认值)、"STRICT_WITH_SIZE"、"NONE"、"FIFO"、"LIFO"、"HIFO" 和 "AVERAGE"。任何其他字符串都会在加载时被拒绝,并显示 Error for option 'booking_method'。"AVERAGE" 在此处和 open 中均被接受,但对其进行减少记账时会失败,如上面的 AVERAGE 部分所示。
按账户覆盖
为不同账户设置不同方法通常很有用。例如,你可能希望退休账户使用 FIFO,但应税经纪账户使用 STRICT,以确保你卖出特定的税务批次。你可以在开设账户时设置记账方法。
2024-01-01 open Assets:Retirement:401K "FIFO"
2024-01-01 open Assets:Taxable:Stock "STRICT"最佳实践
-
库存组织:为了保持账本整洁和简单,强烈建议为每种唯一商品使用单独的账户,并在其
open指令中将其限制为该商品。; GOOD: separate accounts by commodity, each constrained to one 2024-01-01 open Assets:Invest:VTSAX VTSAX 2024-01-01 open Assets:Invest:VFIAX VFIAX避免在同一账户中混合不同的股票或基金,因为这会使库存管理复杂化。
open上的商品列表使 Beancount 会拒绝不匹配的过账,而不是悄悄混合两个库存。 -
批次管理:
-
为批次使用有意义的标签,特别是用于特定交易,如税务亏损收割或员工股票授予。
Assets:Invest:STOCK 10 STOCK {100.00 USD, "tax-loss-harvest-2024"} -
用注释记录你的交易。这使你的账本以后更易于阅读和理解。
Assets:Invest:STOCK -10 STOCK {100.00 USD} @ 110.00 USD ; Gain: 10%
- 调试:如果遇到错误或意外行为,Beancount 提供工具来检查你的库存状态。
-
检查库存状态:使用
bea doctor context main.beancount 42检查第 42 行的交易,包括其过账和受影响的账户余额。将文件名和行号替换为你想要检查的交易。将
<LINENO>替换为交易后一行的行号以查看其效果。 -
验证批次匹配:
bea check工具验证你的整个文件。它会捕获任何记账错误,例如STRICT模式下的歧义批次匹配。