6. 交易管理与库存基础

6.交易管理与库存基础

在掌握了 Beancount 的核心语法和指令后,我们终于要触及复式记账系统中最强大的部分:如何处理持有成本(Cost Basis)的资产,比如股票、基金,以及如何管理它们的库存(Inventory)。这不仅仅是记录简单的收支,而是要精确追踪每一笔投资的来龙去脉,为计算资本利得和税务申报打下坚实的基础。

交易的构成与金额插值

一个 Beancount 交易(Transaction)由多个分录(Posting)组成。最基本的规则是:所有分录的总和必须为零。为了方便输入,Beancount 允许我们省略其中一个分录的金额,系统会自动计算并填充它。这个过程被称为金额插值(Amount Interpolation)。

例如,一笔简单的资金转移:

2024-05-20 * "从储蓄账户转账"
  Assets:Bank:Checking      -1000.00 USD
  Assets:Bank:Savings

在这个例子中,Assets:Bank:Savings 分录的金额被省略了。当 bean-checkbean-query 处理这笔交易时,它会看到另一分录是 -1000.00 USD,为了使总和为零,它会自动将缺失的金额填充为 +1000.00 USD。

这个规则同样适用于持有成本的资产交易。假设你卖出股票,收到了现金,但忘记了填写资本利得账户的金额:

2024-05-20 * "卖出股票"
  Assets:Investment:Brokerage   -10 AAPL {150.00 USD} @ 180.00 USD
  Assets:Investment:Cash         1800.00 USD
  Income:CapitalGains

这里的 Income:CapitalGains 分录没有金额。系统会计算其他分录的权重(Weight):

  • Assets:Investment:Brokerage 分录的权重是 -10 * 150.00 USD = -1500.00 USD(成本价)。
  • Assets:Investment:Cash 分录的权重是 +1800.00 USD(成交价)。
  • 为了平衡,Income:CapitalGains 的权重必须是 -300.00 USD。这正是这笔交易实现的资本利得。

成本基础与价格的区别

在 Beancount 中,区分成本(Cost)价格(Price)至关重要,它们代表了完全不同的语义。

  • 成本 {...}:用于记录资产的获取成本。它与资产本身绑定,是资产库存的一部分。当你买入股票时,你记录成本;当你卖出时,你用成本来匹配库存中的批次(Lot),从而计算损益。
  • 价格 @:用于记录汇率转换交易时的市场价格。它是一个瞬时的比率,不改变资产的库存属性。

让我们看一个例子来理解它们的差异:

场景一:货币兑换(只用价格)

2024-05-20 * "兑换加元"
  Assets:Bank:Checking      -1000.00 USD @ 1.35 CAD
  Assets:Bank:Canadian       1350.00 CAD

这里,我们用 @ 指定了汇率。交易平衡后,Assets:Bank:Canadian 账户里只有 1350.00 CAD,系统不记录这 1350 CAD 是由 1000 USD 兑换而来的。这是一种纯粹的货币转换。

场景二:买入资产(只用成本)

2024-05-20 * "买入苹果股票"
  Assets:Investment:Brokerage   10 AAPL {150.00 USD}
  Assets:Investment:Cash       -1500.00 USD

这里,我们用 {...} 指定了每股成本。交易平衡后,Assets:Investment:Brokerage 账户里不仅有 10 股 AAPL,还记住了这些股票的成本是每股 150 USD。这个成本信息会一直跟随这些股票,直到它们被卖出。

场景三:卖出资产(成本与价格并存)

2024-05-20 * "卖出苹果股票"
  Assets:Investment:Brokerage   -10 AAPL {150.00 USD} @ 180.00 USD
  Assets:Investment:Cash        1800.00 USD
  Income:CapitalGains

这里同时使用了成本和价格:

  • {150.00 USD} 告诉系统:去库存里找成本为 150 USD 的 AAPL 股票,卖掉 10 股。
  • @ 180.00 USD 是一个记录性注释,它告诉系统这笔交易的市场价格是 180 USD。它会被用来记录这笔交易的成交价,或者通过插件自动生成一条价格记录,但它不参与交易的平衡计算。平衡计算依然依赖于成本(1500 USD)和收到的现金(1800 USD),差额自动计入资本利得。

库存管理与批次匹配

当你持有一种资产(比如股票)时,你可能在不同时间、以不同价格买入。你的账户里就不是简单地持有“100 股”,而是持有一系列批次(Lots),每个批次都有自己的数量、成本和获取日期。

; 2024-01-15 买入 50 股,每股 100 USD
2024-01-15 * "买入"
  Assets:Investment:Brokerage   50 AAPL {100.00 USD}
  Assets:Investment:Cash       -5000.00 USD

; 2024-03-10 买入 50 股,每股 120 USD
2024-03-10 * "买入"
  Assets:Investment:Brokerage   50 AAPL {120.00 USD}
  Assets:Investment:Cash       -6000.00 USD

此时,你的 Assets:Investment:Brokerage 账户库存里有两个批次:

  1. 50 AAPL {100.00 USD}
  2. 50 AAPL {120.00 USD}

现在,你想卖出 60 股。问题来了:系统应该卖掉哪些股票?这会直接影响你的资本利得计算。这就是库存匹配(Inventory Matching)的核心。

显式匹配

你可以通过在卖出分录中指定成本来明确告诉系统要卖哪些批次。

2024-05-20 * "卖出 60 股,指定成本"
  Assets:Investment:Brokerage   -50 AAPL {100.00 USD}
  Assets:Investment:Brokerage   -10 AAPL {120.00 USD}
  Assets:Investment:Cash        10800.00 USD
  Income:CapitalGains

在这个例子中,我们明确地卖掉了第一批的 50 股和第二批的 10 股。系统会精确计算这两部分的损益。

隐式匹配与预订方法(Booking Methods)

如果你不想手动指定每个批次,或者库存中存在模糊匹配,你就需要依赖系统的预订方法(Booking Method)。你可以在打开账户时指定,或者全局设置。

; 全局设置
option "booking_method" "FIFO"

; 或者在打开账户时为特定账户设置
2024-01-01 open Assets:Investment:Brokerage  AAPL  booking:"FIFO"

常见的预订方法有:

  • STRICT(严格模式,默认):如果系统无法唯一确定要减少哪个批次(例如,你有多个批次成本相同,但你只写了 {}),它会报错。这是最安全的模式,能帮你发现数据输入的歧义。
  • FIFO(先进先出):卖出时,优先卖掉最早买入的批次。这在很多国家是税务部门默认或允许的方法。
  • LIFO(后进先出):卖出时,优先卖掉最近买入的批次。
  • AVERAGE(平均成本):将所有批次的成本合并计算出一个平均成本,每次卖出都按这个平均成本计算。这在某些税务场景(如退休账户)或特定国家(如加拿大)很常用。

让我们看看 FIFO 的效果。继续上面的例子,我们有 50 股 @100 和 50 股 @120。现在我们卖出 60 股,账户设置为 FIFO:

2024-05-20 * "卖出 60 股 (FIFO)"
  Assets:Investment:Brokerage   -60 AAPL {}
  Assets:Investment:Cash        11200.00 USD
  Income:CapitalGains

系统会自动匹配:

  • 先卖掉 50 股 @100 的批次(成本 5000 USD)。
  • 再卖掉 10 股 @120 的批次(成本 1200 USD)。
  • 总成本为 6200 USD,收到现金 11200 USD,资本利得为 5000 USD。

交易完成后,库存中还剩下 40 股 @120 的批次。

多商品交易

一个交易可以涉及多种商品和货币,只要最终所有货币的净额为零。

2024-05-20 * "用美元和欧元购买设备"
  Assets:Bank:USD          -1200.00 USD
  Assets:Bank:EUR           -100.00 EUR @ 1.20 USD
  Expenses:Office:Equipment  1320.00 USD

这个交易平衡了:

  • USD 部分:-1200 + 1320 = 0
  • EUR 部分:-100 EUR 被 @ 1.20 USD 转换成 -120 USD,与 USD 部分相加也为 0。

这种语法在处理跨国交易或投资时非常有用。

库存减少的基本原理

理解库存减少的关键在于:它是一个匹配和移除的过程

  1. 触发:当一个分录的符号与账户当前余额的符号相反时,就触发了库存减少。例如,账户余额为正(持有资产),分录为负(卖出)。
  2. 匹配:系统根据分录中 {...} 内的信息(成本、日期、标签)去筛选账户的库存批次。
  3. 移除:找到匹配的批次后,系统从该批次中移除相应数量的单位。
  4. 计算损益:移除的单位的成本,与该分录在交易中对应的现金价值(或权重)之间的差额,就是资本利得。

如果匹配过程出现歧义(例如,有多个批次都符合 {} 的描述),系统就会根据预订方法(STRICT/FIFO/LIFO)来决定如何处理。如果预订方法是 STRICT,它会报错,提示你必须明确指定要卖哪个批次。这正是 Beancount 严谨性的体现,它强迫你思考每一笔交易的精确来源,从而避免了潜在的记账错误和税务风险。

通过本章的学习,你现在掌握了 Beancount 处理投资和库存的核心机制。你已经能够记录股票的买卖,并理解系统是如何通过成本基础来自动追踪损益的。这是从简单的收支记账迈向专业级投资组合管理的关键一步。在下一章,我们将学习如何通过余额断言来进一步保证账本数据的准确性。