跳转到主要内容

Beancount 的批次、成本基础和记账方法

当你卖出股票或货币时,Beancount 如何记录批次:成本基础、批次规格、STRICT、FIFO、LIFO 和 HIFO 记账、价格与成本、按账户覆盖。

Beancount 的库存系统是一个强大的功能,用于追踪随时间买入和卖出的资产,例如股票、共同基金或外币。它可以精确追踪成本基础,这对于计算资本收益和理解投资组合表现至关重要。本教程涵盖在账本中管理库存的核心机制。

核心概念​

库存管理的核心在于追踪持仓。一个"持仓"就是账户中持有的某一商品的某个数量。Beancount 区分两种基本类型的持仓。

仓位类型​

  1. 简单持仓(无成本):这是标准的余额过账。它表示某一商品的数量,不附带任何获取成本。它适用于现金或简单的余额断言。

    Assets:Bank:Checking      100.00 USD
  2. 带成本基础的持仓:这类持仓不仅包括单位数量和商品,还包括获取该商品时的成本。这是库存追踪的基础。成本在花括号 {} 中指定。

    Assets:Invest:VTSAX      10 VTSAX {100.00 USD, "lot-1"}

    在这个例子中,我们持有 10 个单位的 VTSAX。每个单位的获取成本为 $100.00 USD。这一批特定的股份被称为一个"批次"。

库存操作​

你可以对库存执行两种主要操作:

  1. 增加(添加到库存):当你买入某一商品时,你增加了库存。你创建一个具有特定单位数量和成本基础的新批次。

    2024-01-15 * "Buy shares"
      Assets:Invest:STOCK     50 STOCK {25.00 USD, "lot-1"}
      Assets:Bank:Checking   -1250.00 USD

    这里,我们以每单位 $25.00 USD 的成本买入 50 个单位的 STOCK。这在 Assets:Invest:STOCK 账户中创建了一个批次。

  2. 减少(从库存中移除):当你卖出某一商品时,你减少了库存。你必须指定要从哪个批次卖出。这通过在花括号中提供匹配信息来完成。

    2024-01-20 * "Sell shares"
      Assets:Invest:STOCK    -25 STOCK {25.00 USD}
      Assets:Bank:Checking    625.00 USD

    在这笔交易中,我们从以每单位 $25.00 USD 买入的批次中卖出 25 个单位的 STOCK。

记账方法​

当你减少库存时,如果有多个批次匹配这次减少,Beancount 需要一条规则来决定从哪个具体批次中扣减。这条规则称为"记账方法"。你可以用 option 为整个文件设置默认值,也可以在 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:Gains

Beancount 会报告 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 的收益——$3,000.00 的收益额对 $1,000.00 + $1,200.00 的基础——并让账户清空。这是 STRICT 本身的性质,不是你必须切换到 STRICT_WITH_SIZE 才能实现的东西。

2. FIFO(先进先出)​

FIFO 方法自动将减少记账到最早可用的批次上。

2024-01-01 open Assets:Invest:STOCK "FIFO"
  • 自动解决:它通过选择最早的匹配批次来解决歧义。
  • 按时间顺序匹配:你假设卖出的是你持有最久的资产。当你没有指定批次时,多个税务机关将其视为默认做法。

3. LIFO(后进先出)​

LIFO 方法与 FIFO 相反。它将减少记账到最新可用的批次上。

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

它加载时零错误,总共记入 $1,400.00 的收益,分摊如下:

账户方法记账批次成本基础已实现收益剩余批次
Assets:Broker:FifoFIFO批次 A,2024-01-10$100.00$500.0010 @ $120.00、10 @ $90.00
Assets:Broker:LifoLIFO批次 C,2024-03-10$90.00$600.0010 @ $100.00、10 @ $120.00
Assets:Broker:HifoHIFO批次 B,2024-02-10$120.00$300.0010 @ $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

这记入相对于 $120.00 批次的 $210.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:一个注释,记录交易时的市场价格。它用于货币换算或注明特定日期的市场价值。

以下是三种场景:

  1. 价格注释(换算):使用 @ 从一种货币换算为另一种货币。

    Assets:Forex     1000 USD @ 0.85 EUR
  2. 成本基础(获取):买入资产时使用 {} 来确立其成本。

    Assets:Invest    10 STOCK {100.00 USD}
  3. 两者兼有(带价格记录的卖出):卖出资产时,使用 {} 标识要卖出的批次,使用 @ 记录卖出价格。这允许自动计算资本收益。

    Assets:Invest    -10 STOCK {100.00 USD} @ 105.00 USD

    这笔条目从成本为每个 $100.00 的批次中卖出 10 个 STOCK,卖出价为每个 $105.00。

独立的 price 指令为市场估值提供参考数据。实时价格可以为托管账本中受支持的资产维护这些指令。刷新会让你的批次、记账方法、获取成本和记录的卖出收益保持不变。

价格使用规则​

  1. 价格注释(@)不会影响记账哪个批次。批次匹配完全由成本基础({})和账户的记账方法处理。
  2. @ 符号仅用于:
  • 货币换算。
  • 在交易时记录资产的市场价值。
  • 为资本收益计算提供卖出价格。

配置​

你可以全局配置记账方法,也可以按账户配置。

全局记账方法​

你可以使用 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"

最佳实践​

  1. 库存组织:为了保持账本整洁简单,强烈建议为你持有的每个独特商品使用单独的账户,并在其 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 拒绝一个误放的过账,而不是静默地混合两个库存。

  2. 批次管理:

  • 为批次使用有意义的标签,尤其是对于税收亏损收割或员工股票授予等特定交易。

    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%
  1. 调试:如果你遇到错误或意外行为,Beancount 提供了检查库存状态的工具。
  • 检查库存状态:使用 bea doctor context main.beancount 42 检查第 42 行的交易,包括其过账和受影响的账户余额。将文件名和行号替换为你想检查的交易。

    将 <LINENO> 替换为某笔交易之后的行号,以查看其效果。

  • 验证批次匹配:bea check 工具会校验你的整个文件。它会捕获任何记账错误,例如 STRICT 模式下的模糊批次匹配。

来源:https://beancount.io/zh/docs/Basics/inventories