库存记账

关于命令行记账的改进提案

Martin Blais,2014 年 6 月

http://furius.ca/beancount/doc/proposal-booking

动机

问题描述

批次日期

平均成本基础

不含佣金的资本利得

成本基础调整

处理股票分割

先前的解决方案

Ledger 的不足

Beancount 的不足

需求

调试工具

显式与隐式记账减少

设计提案

库存

输入语法与过滤

算法

隐式记账方法

默认插入的日期

无信息时的匹配

减少多个批次

示例

无冲突

按成本显式选择

按日期显式选择

按标签显式选择

按组合显式选择

单位不足

重复选择同一批次

自动价格外推

平均成本记账法

未来工作

实现说明

结论

动机

“库存记账”问题,即在平仓时选择从库存的哪个交易批次中减少单位,是一个棘手的问题。迄今为止,在命令行会计社区中,对于支持处理许多常见现实场景的灵活性,进展相对有限。本文将讨论当前的状况,描述我们希望解决的常见用例,确定一种更好记账方法所需的一组需求,并提出支持这些需求的语法和实现设计,同时为记账方法提供明确的语义定义。

问题描述

本问题的核心在于:对于一笔旨在在特定时间点减少特定账户持仓的复式记账交易,应决定从该时间点账户库存中的哪个批次中减少单位。此选择应通过一种简单的数据输入语法来指定,以最大限度地减轻用户负担。

例如,可以通过在不同时间点购买两批股票来建立对 HOOL 股票的持仓:

2014-02-01 * "First trade"
  Assets:Investments:Stock        10 HOOL {500 USD}
  Assets:Investments:Cash
  Expenses:Commissions          9.95 USD

2014-02-15 * "Second trade"
  Assets:Investments:Stock         8 HOOL {510 USD}
  Assets:Investments:Cash
  Expenses:Commissions          9.95 USD

我们将这两批股票称为“交易批次”,并假设底层系统分别记录每一批的买入成本和购入日期。

问题是,如果我们打算卖出部分股票,应选择哪一批股票? 是第一批还是第二批?这是一个重要的决策,因为它会影响资本利得,进而影响税收。(如果您需要了解这一点的详细解释,请参阅《Beancount 食谱》中的“股票交易”章节。)根据您的财务状况、本年度的交易历史以及交易可能实现的未实现收益,有时我们可能希望优先选择某一批次。如今,大多数折扣经纪商甚至允许您在下达平仓或减仓交易时自行选择具体的批次。

在此必须强调库存记账的两个方向:

  1. 增加持仓。 这是创建新的交易批次,或在成本和其他属性相同时向现有批次添加单位的过程。这很简单,只需向描述账户余额的某种映射(我称之为“库存”)中添加一条记录即可。

  2. 减少持仓。 这通常是记账复杂性的主要来源,问题本质上是确定应从现有持仓的哪个批次中移除单位。

本文的大部分内容专注于第二个方向。

批次日期

即使每个仓位的成本相同,您打算平仓的仓位的购入日期仍然很重要,因为可能适用不同的税务规则。例如,在美国,持有超过 12 个月的仓位适用显著更低的税率(即‘长期资本利得税率’)。例如,如果您以如下方式建仓:

2012-05-01 * "First trade"
  Assets:Investments:Stock        10 HOOL {300 USD}
  Assets:Investments:Cash
  Expenses:Commissions          9.95 USD

2014-02-15 * "Second trade at same price"
  Assets:Investments:Stock         8 HOOL {300 USD}
  Assets:Investments:Cash
  Expenses:Commissions          9.95 USD

如果我们记录 2014-03-01 的交易,选择第一个仓位将产生长期(低)收益,因为自购入以来已过去两年(仓位持有期为 2012-05-01 至 2014-03-01);而选择第二个仓位则会产生短期(高)资本利得税。因此,这不仅仅是仓位成本的问题。我们的系统目前并不旨在自动报税,但我们希望以一种最终能支持此类报告的方式录入数据。

请注意,本讨论假设您能够自行决定使用哪个仓位进行交易。可能存在多种税务规则,在某些情况下,根据您居住的国家,您可能没有选择权,政府会规定您必须如何申报收益。

在某些情况下,您甚至可能需要对同一账户应用多种记账方法(例如,如果一个账户受到多个税收管辖区的税务要求影响)。本文件将忽略这种罕见的特殊情况。

平均成本基础

对于免税账户,情况会变得更加复杂。由于在这些账户中平仓不会产生税务后果,经纪商通常会将您的仓位视为单一仓位来计算账面价值。也就是说,每股的成本是根据您所持仓位的加权平均成本计算的。这也可以通过将所有仓位的总成本相加,再除以总股数来等效计算。继续使用我们第一个例子:

10 HOOL × 500 美元/HOOL = 5000 美元

8 HOOL × 510 美元/HOOL = 4080 美元

成本基础 = 5000 美元 + 4080 美元 = 9080 美元

总股数 = 10 HOOL + 8 HOOL = 18 HOOL

每股成本基础 = 9080 美元 / 18 HOOL = 504.44 美元/HOOL

因此,如果您部分平仓并卖出部分股票,将以每股 504.44 美元的成本进行。您的收益将相对于此成本计算;例如,如果您以每股 520 美元的价格卖出 5 股,您的收益将按如下方式计算:

(520 USD/HOOL - 504.44 USD/HOOL) x 5 HOOL = 77.78 USD

这种记账方法在以下两类财务事件中会明显体现出来:

  1. 费用可能通过按当前市价卖出部分股票来扣除。如果您在美国 Vanguard 持有退休账户,您可能会看到这种情况。经纪商每季度收取固定金额(例如 5 美元,根据我所在公司的计划),并以每个基金的少量零股形式扣除。这笔交易不考虑资本利得:他们仅计算需要卖出多少股来覆盖费用,而不会向您报告任何收益。您必须假设这无关紧要。大多数人不会追踪免税账户中的收益,但像我们这样的极客仍可能希望计算实际回报。

  2. 成本基础调整可能会自发发生。这种情况很少见,但我曾见过一两次在加拿大免税账户中的共同基金:基于其交易活动的一些未公开细节,基金公司不得不对成本基础进行调整,您需要将此调整应用于您的持仓。您会收到一封电子邮件,告知调整的金额。99% 的人可能根本不会在意,也不理解,直接忽略这条信息……也许他们这样做是正确的,因为这对他们的税务没有影响;但如果您希望准确核算该账户内的交易收益,则必须考虑这一调整。因此,我们需要能够增加或减少现有持仓的成本基础。

请注意,您的持仓的平均成本会在每次交易后发生变化,每次新增持仓时都需要重新计算。这个问题在于,如果您跟踪每股成本,并使用固定数值表示(通常是十进制数)存储金额,多次重新计算可能会导致一些误差。一种更稳定的数值方法是,在数据结构中仅跟踪总成本,而不是每股成本。另外请注意,使用分数数字并不能充分解决此问题:实际上,经纪商报告的成本可能是四舍五入到特定小数位的数值,我们可能希望使用该报告数值来记录您的减仓交易,而不是维持分数表示所对应的理想化金额。无论是使用固定十进制还是分数表示,都会带来问题。我们在此将基本忽略这些问题。

总结来说:我们需要支持按平均成本记账,同时需要能够调整现有持仓的成本基础。我一直在思考一种解决方案,无需强制将账户标记为特殊状态。我认为仅通过库存操作即可实现这一点。我们只需要对语法做一点小改动,允许用户指示系统:应按所有持仓的平均成本进行记账。

使用上述第一个示例,购入股票的形式与之前相同:在买入两批分别为 10 股和 8 股的 HOOL 股票后,我们的库存将包含两个批次:一批是 10 股 HOOL {500 美元},另一批是 8 股 HOOL {510 美元}。然而,当需要卖出时,将使用以下语法(这是我早有计划实现的一个旧想法):

2014-02-01 * "Selling 5 shares at market price 550 USD"
  Assets:Investments:Stock               -5 HOOL {*}
  Assets:Investments:Cash           2759.95 USD
  Expenses:Commissions                 9.95 USD
  Income:Investments:CapitalGains

当遇到“*”代替成本时,系统将执行以下操作:

  1. 将所有批次合并,并重新计算每股平均价格(504.44 美元)

  2. 按合并后的库存进行记账,减少所得批次。

交易完成后,我们将只剩下一个持仓:13 股 HOOL {504.44 美元}。若之后以不同价格增持,将创建一个新的批次;下次再以平均成本减仓时,这些批次将再次合并。我们无需在每次增持时都合并批次,仅在减仓时才需合并。除了累积舍入误差的顾虑外,这种方案在数学上是正确的,并保持了账户无需强制采用特定记账方式的特性——每次减仓时都可以混合使用多种方法。这是一个很好的特性,即使并非绝对必要,即使我们希望为每个账户设定一个“默认”记账方式并一直沿用。拥有支持例外情况的灵活性总是有益的,因为在现实生活中,例外情况确实时有发生。

资本利得(不含佣金)

我们希望能够支持在不包含佣金的情况下自动计算资本利得。这一问题在《交易文档的佣金部分》(链接)和邮件列表中的一篇帖子中有详细描述。问题的核心在于,不能简单地从包含佣金的收益中减去报告期间发生的佣金,因为为建立头寸而产生的佣金必须按所售股份的比例分摊。最简单且最常用的方法是将获取头寸的成本纳入头寸本身的成本基础,并在头寸减少时从市场价值中扣除卖出成本。

无论我们设计出何种新方案,都必须允许我们将这些调整后的收益计入计算,因为这对许多个人的财务状况至关重要。在 Beancount 中,我已经找到了一种解决方案,该方案只需对带有特殊标记的交易进行自动化转换——前提是,我必须能找到一种方式,在不指定其成本的情况下指定要匹配的仓位。因为一旦佣金成本被合并进头寸成本,调整后的成本就不会出现在输入文件中,用户必须手动计算,这是不合理的。在我提出的解决方案中,以下交易将由插件自动转换,触发条件是存在“C”标记:

2014-02-10 * "Buy"
  Assets:US:Invest:Cash    -5009.95 USD
  C Expenses:Commissions       9.95 USD
  Assets:US:Invest:HOOL       10.00 HOOL {500 USD / aa2ba9695cc7}

2014-04-10 * "Sell #1" 
  Assets:US:Invest:HOOL       -4.00 HOOL {aa2ba9695cc7}
  C Expenses:Commissions       9.95 USD
  Assets:US:Invest:Cash     2110.05 USD
  Income:US:Invest:Gains

2014-05-10 * "Sell #2"
  Assets:US:Invest:HOOL       -6.00 HOOL {aa2ba9695cc7}
  C Expenses:Commissions       9.95 USD
  Assets:US:Invest:Cash     3230.05 USD
  Income:US:Invest:Gains

它们将由插件自动转换为以下形式,并替换上述原始交易:

2014-02-10 * "Buy"
  Assets:US:Invest:Cash        -5009.95 USD
  X Expenses:Commissions           9.95 USD
  X Income:US:Invest:Rebates      -9.95 USD
  X Assets:US:Invest:HOOL         10.00 HOOL {500.995 USD / aa2ba9695cc7}

2014-04-10 * "Sell #1" 
  X Assets:US:Invest:HOOL         -4.00 HOOL {aa2ba9695cc7}
  X Expenses:Commissions           9.95 USD
  X Income:US:Invest:Rebates      -9.95 USD
  Assets:US:Invest:Cash         2110.05 USD
  Income:US:Invest:Gains ; Should be (530-500)*4 - 9.95*(4/10) - 9.95 = 
                         ; 106.07 USD

2014-05-10 * "Sell #2"
  X Assets:US:Invest:HOOL         -6.00 HOOL {aa2ba9695cc7}
  X Expenses:Commissions           9.95 USD
  X Income:US:Invest:Rebates      -9.95 USD
  Assets:US:Invest:Cash         3230.05 USD
  Income:US:Invest:Gains ; Should be (540-500)*6 - 9.95*(6/10) - 9.95 = 
                         ; 224.08 USD

“X”标记仅用于标识已被转换的条目,以便将来可以用特殊颜色或属性渲染。返利账户作为独立的反向科目加入,仅用于使交易平衡。

我需要放宽仓位匹配的歧义性,是因为用户必须能够在不指定头寸成本的情况下指定匹配的交易方。因为在头寸减少时(2014-04-10),无法推断出原始仓位的成本是多少。为明确起见,在上述示例中,这意味着如果我们只知道 4 股 HOOL 和 500 美元,就无法反推出 500.995 美元的成本来匹配初始交易仓位,因为初始交易涉及的是 10 股 HOOL。

因此,我们需要更明确地指定仓位匹配方式。在上述示例中,我是通过“标签”来选择匹配的仓位,即用户可以在成本说明中提供一个唯一的仓位标识符,之后可用于区分我们希望匹配减少/出售的仓位。上述示例中使用了字符串“aa2ba9695cc7”来实现这一点。

(解决此问题的另一种方案是同时追踪原始成本(不含佣金)和实际成本(含佣金),然后根据前者匹配仓位,但使用后者来平衡交易。这一思路允许用户继续使用头寸成本来选择仓位,但我甚至不确定在库存频繁变动的情况下是否可行。对此仍需进一步思考。)

成本基础调整

为了调整成本基础,可以显式地替换账户内容,如下所示:

2014-03-15 * "Adjust cost basis from 500 USD to 510 USD"
  Assets:US:Invest:HOOL      -10.00 HOOL {500 USD}
  Assets:US:Invest:HOOL       10.00 HOOL {510 USD}
  Income:US:Invest:Gains

这种方法效果良好,能让系统自动计算收益。但若能支持以价格单位对持仓总成本进行调整,例如“向此持仓的成本基础增加 340.51 美元”,那就更好了。问题是,调整后的每股成本现在需要用户自行计算……这很不方便。不过,我们可以通过以下方式很好地支持这一功能:

2014-03-15 * "Adjust cost basis from 500 USD to 510 USD"
  Assets:US:Invest:HOOL      -10.00 HOOL {500 USD}
  Assets:US:Invest:HOOL       10.00 HOOL {}
  Income:US:Invest:Gains    -340.51 USD

如果所有分腿都已完全指定——即具有可计算的余额——我们允许省略单个价格。这很好地解决了这个问题。

处理股票分割

会计处理股票分割会带来一些复杂性。一般来说,问题在于我们需要应对商品含义随时间变化的情况。例如,你目前在 Beancount 中可以这样做,并且它是有效的:

2014-01-04 * "Buy some HOOL"
  Assets:Investments:Stock        10 HOOL {1000.00 USD}
  Assets:Investments:Cash

2014-04-17 * "2:1 split into Class A and Class B shares"
  Assets:Investments:Stock       -10 HOOL {1000.00 USD}
  Assets:Investments:Stock        10 HOOL  {500.00 USD}
  Assets:Investments:Stock        10 HOOLL {500.00 USD}

以这种方式分割持仓的一个问题是,分割前后 HOOL 单位的价格和含义发生了变化,但我们现在暂且假设可以接受这一点(这可以通过重命名商品来解决,最好以用户不可见的方式进行——我们有一个其他地方的待讨论议题)。

与会计相关的问题是:当你卖出该持仓时,应使用哪个成本?库存中包含的是 500 美元的成本持仓,因此你应该使用这个数值,但这对用户来说是否显而易见?我们可以假设这可以通过一个操作指南来学习掌握。

现在,如果我们自动附加交易日期呢?该日期在分割时是否会重置,从而你必须使用 2014-04-17?如果是这样,我们就无法自动检查交易列表来判断这是长期交易还是短期交易。我们需要以某种方式保留原始事件的某些属性,包括原始交易日期(而非分割日期)以及用户指定的持仓标签(如果有的话)。

强制每个账户使用单一方法

对于将使用平均成本会计方法的账户,允许指定该账户仅能使用此会计方法可能是有用的。其目的是在我们知道某个账户只能使用此方法时,避免数据录入错误。

确保这一点的一种方式是在增加持仓时自动聚合库存批次。这能确保在任何时刻,每种商品都只有一个批次。如果我们为每个账户关联一个库存会计类型,我们可以定义一种特殊类型,它在添加持仓时自动触发聚合。

另一种方法是不强制执行这些聚合,而是提供一个插件,用于检查那些默认标记为使用平均成本库存会计方法的账户,是否确实仅发生了此类会计操作。

先前的解决方案

本节回顾了命令行记账系统中现有会计方法的实现,并指出其具体不足,以说明我们为何需要定义一种新方法。

Ledger 的不足之处

(请注意:我对 Ledger 内部细节并不十分熟悉;我是通过构建测试示例和阅读文档推断其行为的。如果我有任何理解错误,请通过留言告知我。)

Ledger 的记账方式相当宽松。在内部,Ledger 不区分按成本价转换和按市价转换:所有转换均默认按成本价处理,交易批次仅在报告时(使用其 --lots 选项)才出现。

它的 “{}” 成本语法 旨在用于在减少持仓时区分批次,而非在获取持仓时使用。获取持仓的成本应通过 “@” 价格符号指定:

2014/05/01 * Buy some stock (Ledger syntax)
  Assets:Investments:Stock        10 HOOL @ 500 USD
  Assets:Investments:Cash

这将导致库存为 10 HOOL {500 USD},即成本始终存储在持有该持仓的库存中:

$ ledger -f l1.lgr bal --lots
10 HOOL {USD500} [2014/05/01]
            USD-5000  Assets:Investments
            USD-5000    Cash
10 HOOL {USD500} [2014/05/01]    Stock
--------------------
10 HOOL {USD500} [2014/05/01]
            USD-5000

在成本价转换与无成本价转换之间没有区别——所有转换均被追踪为“按成本价”处理。正如我们在下一节所见,这种行为与 Beancount 的语义不同,Beancount 要求在增加和减少记账时都必须使用 “{}” 符号来指定按成本价的转换,并区分按成本价持有的仓位与按市价的转换。

Ledger 方法的优势在于输入机制更简单,但如果你在同一个账户中累积了大量不同货币的转换,就会导致结果令人困惑。库存的批次组成很容易变得碎片化。例如,如果你在五种不同货币之间进行各种方向的转换……你可能会轻易地得到以 CAD、JPY、EUR 和 GBP 计价的 USD,反之亦然……这些货币之间可能存在任何可能的组合。例如,以下货币转换将保留其原始成本与兑换后货币的对应关系:

2014/05/01 Transfer from Canada
  Assets:CA:Checking             -500 CAD
  Assets:US:Checking              400 USD @ 1.25 CAD

2014/05/15 Transfers from Europe
  Assets:UK:Checking             -200 GBP
  Assets:US:Checking              500 USD @ 0.4 GBP

2014/05/21 Transfers from Europe
  Assets:DE:Checking             -100 EUR
  Assets:US:Checking              133.33 USD @ 0.75 EUR

这将在 Assets:US:Checking 账户中产生如下库存:

$ ledger -f l2.lgr bal --lots US:Checking
500.00 USD {0.4 GBP} [2014/05/15]
133.33 USD {0.75 EUR} [2014/05/21]
400.00 USD {1.25 CAD} [2014/05/01]  Assets:US:Checking

这些货币被当作股票处理。但 没有人会这样看待自己的货币余额,人们通常将账户中的货币视为独立的单位,而不是以相对于其他货币的价格来衡量。在我看来,这令人困惑。经过这些转换后,我只将该账户的余额视为 1033.33 USD。

对此观点的一个可能反驳是,大多数情况下用户并不关心仅含货币的账户的库存批次,因此他们只是不打印出来。眼不见,心不烦。但没有任何方式能区分渲染例程何时应显示批次、何时不应显示。如果要求生成报告,余额例程理想上应仅对某些账户(如投资账户,其中单位成本很重要)显示批次,而不对其他账户(如因转换而偶尔存入货币的账户)显示。此外,当我生成资产负债表时,对于按成本价持有的单位,我们需要报告账面价值而非股数;但对于货币,必须始终报告货币单位本身。如果你不区分这两种情况,你如何知道该显示哪一个?我认为答案是通过选项选择你想要渲染的视图。问题是,这两种方式都无法提供一个统一的视图,能同时正确处理这两类商品。用户不应当被迫通过指定选项来查看某种风格下部分错误的视图——这应当自动完成,并取决于我们是否关心这些单位的成本。如果你能区分这两种转换类型,这个问题就能得到正确处理。

但或许最重要的是,这种方法并未真正解决转换问题:你仍可通过以不同汇率反复进行转换来凭空创造货币:

2014/05/01 Transfer to Canada
  Assets:US:Checking             -40000 USD
  Assets:CA:Checking              50000 CAD @ 0.80 USD

2014/05/15 Transfer from Canada
  Assets:CA:Checking             -50000 CAD @ 0.85 USD
  Assets:US:Checking              42500 USD

这导致了一个仅有 2500 美元的试算平衡表:

$ ledger -f l3.lgr bal 
            2500 USD  Assets:US:Checking

这不是一个理想的结果。我们应该要求试算平衡表始终总和为零(虚拟交易除外)。毕竟,复式记账法的优雅之处就在于:所有交易的总和为零,因此任何交易子集的总和也应为零……但事实并非如此。在我看来,任何生成的资产负债表都必须严格符合会计恒等式。

在使用 Ledger 的探索过程中,我原本期望其库存跟踪机制能够自动处理这些兑换,从而为以成本记录所有单位提供充分理由,但我发现同样的问题依然存在。(在 Beancount 中,兑换问题一直是一个棘手的难题,但目前已解决,我认为同样的解决方案也可应用于 Ledger:在关键节点——例如生成资产负债表时——自动插入一条特殊的兑换条目,而无需更改其当前的记账方式。)最终,我发现为每笔交易追踪成本,并未比忽略价格兑换的成本带来明显优势,兑换问题与此无关。我非常希望听到更多支持这种设计选择的理由。

由于所有兑换都保留其成本,Ledger 必须对记账采取相当宽松的态度,因为强制用户为每种常见货币兑换指定原始汇率会非常不便。这给那些我们希望按成本保留的兑换带来了不利影响:允许任意减少仓位,即对不存在的仓位进行减记。例如,以下交易在 Ledger 中不会触发错误:

2014/05/01 Enter a position
  Assets:Investments:Stock        10 HOOL @ 500 USD
  Assets:Investments:Cash

2014/05/15 Sale of an impossible lot
  Assets:Investments:Stock       -10 HOOL {505 USD} @ 510 USD
  Assets:Investments:Cash

这导致库存中出现 10 股 HOOL 的多头头寸和 10 股 HOOL 的空头头寸(成本不同):

$ ledger -f l4.lgr bal --lots stock
10 HOOL {USD500} [2014/05/01]
   -10 HOOL {USD505}  Assets:Investments:Stock

我认为,对 505 美元的 HOOL 仓位进行减记不应当被允许;Beancount 则主动将此类操作视为错误。

Ledger 显然支持针对现有库存记账,因此其本意显然是允许对现有头寸进行减记。例如,以下交易确实会导致库存清零:

2014/05/01 Enter a position
  Assets:Investments:Stock      10 HOOL @ 500 USD
  Assets:Investments:Cash

2014/05/15 Sale of matching lot
  Assets:Investments:Stock     -10 HOOL {500 USD} [2014/05/01]  @ 510 USD
  Assets:Investments:Cash

输出结果为:

$ ledger -f l5.lgr bal --lots 
              USD100  Equity:Capital Gains

如你所见,HOOL 头寸已被清空。(请忽略输出中自动生成的权益条目,我相信这将在一个待定补丁中修复。)但要实现这一点,必须明确指定日期和成本。以下交易则会产生上述类似的有问题的多头+空头库存:

2014/05/01 Enter a position
  Assets:Investments:Stock      10 HOOL @ 500 USD
  Assets:Investments:Cash

2014/05/15 Sale of matching lot?
  Assets:Investments:Stock     -10 HOOL {500 USD} @ 510 USD
  Assets:Investments:Cash

输出结果为:

$ ledger -f l6.lgr bal --lots 
10 HOOL {USD500} [2014/05/01]
   -10 HOOL {USD500}  Assets:Investments:Stock
              USD100  Equity:Capital Gains
--------------------
10 HOOL {USD500} [2014/05/01]
   -10 HOOL {USD500}
              USD100

这有点令人意外,我原本预期这些仓位会相互抵消。我怀疑这可能是一个未报告的 bug,而非预期行为。

最后,"{}"成本语法也可用于增加方。文档指出这些方法是等效的。这会产生一个没有关联日期的库存仓位,但另一方并未转换为成本:

2014/05/01 Enter a position
  Assets:Investments:Stock      10 HOOL {500 USD}
  Assets:Investments:Cash

输出结果为:

$ ledger -f l7.lgr bal --lots
                   0  Assets:Investments
   -10 HOOL {USD500}    Cash
    10 HOOL {USD500}    Stock
--------------------
                   0

我可能只是不理解这种用法的意图;在 Beancount 中,自动计算的另一方会向现金账户记入-5000 美元,而不是股票。

我认为实际情况是,Ledger(很可能)会累积所有仓位而不尝试匹配它们以实现抵消,仅在报告时——且仅在此时——根据某些报告选项将仓位进行匹配:

  • 按成本/价格和日期分组仓位,使用 --lots

  • 仅按成本/价格分组批次,使用 --lot-prices

  • 仅按日期分组批次,使用 --lot-dates

文档并未明确说明这一点是如何运作的,但其行为与此一致。

(如果这是正确的,我认为这反映了其设计上的一个问题:将库存减少记入现有批次的机制绝对不应该是报表功能。它必须在处理报表之前发生,以便实现唯一且确定的库存记账历史。如果支持记账变体——我认为这并非好主意——也应在报表阶段之前支持。在我看来,通过命令行选项更改库存记账策略是错误的,这些策略应完全由语言本身定义。)

Beancount 的不足之处

Beancount 也有各种不足之处,但表现不同。

与 Ledger 不同,Beancount 区分了货币兑换与“按成本持有”的兑换:

2014-05-01 * "Convert some Loonies to Franklins"
  Assets:CA:Checking          -6000 CAD
  Assets:Investments:Cash      5000 USD @ 1.2 CAD   ; Currency conversion

2014-05-01 * "Buy some stock"
  Assets:Investments:Stock        10 HOOL {500 USD} ; Held at cost
  Assets:Investments:Cash

在第一笔交易中,Assets:Investment:Cash 账户的库存为 5000 美元,且未关联任何成本。然而,在第二笔交易后,Assets:Investment:Stock 账户的库存为 10 股 HOOL,并关联了 500 美元的成本。这或许稍显复杂,需要用户具备更多知识:系统中有两种兑换类型,用户必须理解并区分这两种情况,这对新手而言并不明显。这需要一定的用户教育(我承认,如果我能写出更多文档,会很有帮助)。

Beancount 的方法也更严格,它要求严格执行批次减少规则。也就是说,如果你输入了一笔减少不存在批次的交易,系统将报错。这样做的动机是让用户难以在数据录入时出错。任何库存头寸的减少都必须精确匹配一个批次。

这一选择带来了一些缺点:

  • 它要求用户始终找到要匹配的批次成本。这意味着自动化交易导入必须由用户手动干预,因为用户需要查阅账本以插入匹配的批次。(理论上,导入代码可以加载账本内容以列出其持仓,若结果明确,可自动查找成本并插入输出。但这样会使导入代码依赖用户的账本,而大多数导入代码并未这样做。)这是一个重要步骤,因为正确计算资本收益必须找到正确的成本,但我们更希望采用一种更灵活的方法,允许用户在选择显而易见时更“偷懒”一些,例如当仅存在一个可匹配的批次时,无需手动输入现有头寸的成本。我希望适度放宽这一规则。

  • 严格要求只能与现有仓位进行对冲,这意味着在同一账户中,同一种商品的多头和空头仓位‘按成本持有’无法共存。然而,这在实践中并不是大问题,因为空头仓位非常罕见——很少有人进行空头交易;如果需要,用户完全可以创建独立账户来持有空头仓位。事实上,目前普遍的做法是在投资账户中为每种商品设立专门的子账户,因为这样能自然地组织交易活动(否则报告会显得杂乱无章,按股票分组查看总仓位价值会更清晰,因为它们提供了不同的市场风险敞口)。只要我们在对冲时正确处理符号,不同的账户完全可以正常工作——例如,买入股票以对冲现有空头仓位,应被视为仓位减少(符号仅相反)。

Beancount 记账方法的另一个问题是其如何匹配带有日期的仓位。例如,如果你试图区分两个价格相同的仓位,最直观的输入方式将无法生效:

2012-05-01 * "First trade"
  Assets:Investments:Stock        10 HOOL {500 USD}
  Assets:Investments:Cash

2014-02-15 * "Second trade at very same price"
  Assets:Investments:Stock         8 HOOL {500 USD}
  Assets:Investments:Cash

2014-06-30 * "Trying to sell"
  Assets:Investments:Stock        -5 HOOL {500 USD / 2012-05-01}
  Assets:Investments:Cash

这会报告一个记账错误,令人困惑。在这种情况下,Beancount 为了保持严格性,会对库存仓位进行精确匹配,试图将(HOOL, 500 USD, 2012-05-01)的单位与(HOOL, 500 USD, None)的单位匹配,结果直接失败。请注意,Beancount 在增加和减少交易的两端都接受仓位日期语法,因此‘解决方案’是用户在建立仓位时必须明确指定仓位日期。例如,以下方式可以正常工作:

2012-05-01 * "First trade"
  Assets:Investments:Stock        10 HOOL {500 USD / 2012-05-01}
  Assets:Investments:Cash

2014-02-15 * "Second trade at very same price"
  Assets:Investments:Stock         8 HOOL {500 USD}
  Assets:Investments:Cash

2014-06-30 * "Trying to sell"
  Assets:Investments:Stock        -5 HOOL {500 USD / 2012-05-01}
  Assets:Investments:Cash

这里的问题在于,当用户输入第一笔交易时,他无法预知未来是否还会以相同价格进行另一笔交易,通常也不会主动插入仓位日期。更可能的情况是,用户不得不回头修改之前的交易以消除歧义。我们可以做得更好。

这是其实现上的一个缺陷,反映出最初将仓位减少视为精确匹配问题而非模糊匹配问题——当时我并不清楚如何安全地处理这种情况。此问题需在本提案之后修复,而本文的目的正是帮助我理清自己希望这种模糊匹配具体意味着什么,并确保设计出一种清晰、易于数据录入的方案。Ledger 总是自动将交易日期附加到库存仓位上,我认为这才是正确的做法(下文我们还将提议附加更多信息)。

上下文

[2014 年 11 月更新]

区分两种记账类型非常重要:

  1. 记账。 当我们考虑全部交易列表时所进行的严格记账类型。我们将此称为“记账”过程,它应仅执行一次。此过程必须严格:若无法正确将减少操作匹配到现有仓位,应触发致命错误。

  2. 库存聚合。 当我们仅考虑部分交易条目时,可能某些增加仓位的条目被移除,而某些减少或清除这些仓位的条目被保留。这会导致一种情况:必须支持对库存变化进行时间上的聚合,即使这些变化无法精确匹配到某个仓位。如果我们希望支持任意子集交易的聚合,就必须考虑这一点。此聚合过程绝不应导致错误。

让我们举一个例子来说明为何我们需要非记账的汇总方式。假设我们有一笔股票买入和卖出交易:

2013-11-03 * "Buy"
  Assets:Investments:VEA      10 AAPL {37.45 USD} ;; (A)
  Assets:Investments:Cash     -370.45 USD

2014-08-09 * "Sell"
  Assets:Investments:VEA     -5 AAPL {37.45 USD}
  Assets:Investments:Cash     194.40 USD
  Income:Investments:PnL

如果我们按“年份”筛选,仅查看 2014 年发生的交易,并且不“关闭”上一年度——即不创建期初余额条目以在年初将持仓存入账户——这种情况可能发生在按标签或其他条件组合筛选时——那么(A)持仓将不存在于库存中供借记。此时我们必须以某种方式处理-5 AAPL 的情况。在报告时,用户可自行决定如何汇总并尽可能“匹配”这些交易分录,但可将其转换为单位或成本,或转换为按加权平均成本计算的单一持仓。

然而,记账过程不应允许任何此类宽松处理。一旦无法匹配持仓,就必须严格触发错误。此规则仅适用于全部交易集合,且仅执行一次,绝不针对任何子集。记账阶段的目的应有三重:

  1. 匹配现有持仓,若无法匹配则发出错误。请使用用户指定的方法执行此操作。

  2. 替换所有部分指定的持仓,替换为记账过程中匹配到的完整持仓规格。

  3. 插入链接构成交易的交易记录:买入及其对应的卖出应相互链接。这本质上是识别交易,以便后续用于报告。(按加权平均成本的交易需特殊处理。)

我们可以将 Ledger 的方法视为仅包含聚合阶段,而缺乏记账阶段。其聚合方法通过 --lots、--lot-dates 或 --lot-prices 选项选择。这表明,可以在不改变 Ledger 整体结构的前提下,为其添加一个“记账”阶段:它可作为前置或后置处理过程插入,但要求 Ledger 按日期对交易排序——而目前 Ledger 并未执行此操作(它仅对数据进行单次遍历)。

Beancount 已长期支持这两种方法,但此前并未明确区分这两个操作——相反,库存仅通过可选标志在记账失败时抛出错误。此外,库存聚合也未被认为需要高度定制,而是被设计为与记账过程统一。如今我们已明确,它们是两种独立的算法,且存在多种相关的聚合方法(尽管其功能与 Ledger 不同)。注意:可将库存聚合方法视为 Inventory 对象内持仓的 GROUP BY 操作:你按哪些列分组?这需要具体用例支持。无论如何,默认情况下,完全指定的持仓应相互匹配:我们需要支持这种退化情形,以用于简单货币(非按成本持有)……我们不希望将同一货币的所有变动累积为独立持仓,它们应在应用时立即相互抵消。

我们希望对此进行修改,以使代码更清晰:应增加一个独立的“预订”阶段,由一个插件提供,该插件使用本文档中描述的算法解决部分指定的仓位减少问题;而库存聚合方法则无需关心这些细节。事实上,库存聚合甚至可以简化为一个累积的仓位列表,其汇总操作可在事后进行,这与 Ledger 的聚合方式在概念上类似。一旦我们开始引入更多关于仓位的详细信息(例如始终包含购入日期、标签,甚至可能包含创建该仓位的条目链接),使用简单的聚合方式将变得更加重要。为了合理触发汇总,可以在命令行中提供用于转换和汇总累积库存的函数,例如“UNITS(inventory)”。

插值精度

插值功能将扩展至支持省略单个数字,即在任何一组“权重”解析为特定货币(例如“所有权重为美元的条目”)的交易中自动省略任意数字。插值过程需在特定精度下进行,插值结果应自动进行量化,小数位数将根据从输入文件派生的 DisplayContext 自动推断——使用文件中出现最频繁或最多的数字位数。

有关用例,请参见此线程

2014 年 11 月 24 日星期一中午 12:33,ELI <eliptus@gmail.com> 写道:

关于你我之间似乎存在误解的最后一点,让我提供一个独立于插件的示例。由于 Vanguard 仅提供三位小数的精度,我需要根据价格和交易金额手动计算实际的股份数量。

例如,我的活动页面上有一笔交易,详情如下:

交易股数:0.000

每股价格:48.62

金额:0.02

要使这笔交易平衡,唯一的方法是手动以四位小数精度计算“交易股数”。我更倾向于仅输入“每股价格”和“金额”,让 Beancount 根据我的 Vanguard 账户关联的精度自动计算“交易股数”。此外,作为元数据,我会记录“交易股数:0.000”作为“对账单所载内容”的历史信息。

您能否就此类情况如何通过 Beancount 的计划功能或我可编写的插件来解决,提供一些您的看法?

可下载文件中包含的是 0.000、0.0004,还是 0.00041?

这很可能就是他们收取的季度费用,该费用以股份形式计价。如果您是 Vanguard 账户的持有人,将所有类似交易汇总后,您会发现总额正好是每季度 5 美元。

我会这样记录:

  2014-11-23 * "Quarterly fee"
    Assets:US:Vanguard:VIIPX      -0.00041 VIIPX {* 48.62 USD}
    Expenses:Financial:Fees    0.02 USD

必须使用“*”将所有仓位合并为一个(平均成本预订),因为 Vanguard 声明的 48.62 美元并非您任一仓位的成本,而只是他们在收取费用当日该资产的市价。换句话说,他们以每种资产的单位数量计费,按当日市价计算,并从您所有仓位的总成本基础中扣除(由于这是免税账户,无需报告资本利得,因此具体扣除哪个仓位无关紧要)。

现在你可能会问:我该如何避免手动计算 0.00041 呢?

我的第一个回答是:我们先看看来自 Vanguard 的 OFX 下载文件是否包含足够的精度。如果包含,这个问题就迎刃而解了。我相信它确实包含(我检查了自己的下载历史,里面有一些数字保留了四位小数)。

我的第二个回答是:如果你无法从文件中获取份额数量,在我实现急需的新库存记账方案(http://furius.ca/beancount/doc/proposal-booking)之后,插值功能将得到极大扩展,甚至可以完全省略份额数量,如下所示:

  2014-11-23 * "Quarterly fee"
    Assets:US:Vanguard:VIIPX      VIIPX {* 48.62 USD}
    Expenses:Financial:Fees    0.02 USD

但仅靠这一点仍无法解决问题:这会存储 0.000411353353,由于默认上下文的限制,它最多保留 12 位小数,因此这是错误的。要解决这个问题,需要推断 VIIPX 各单位在文件中出现的最常见小数位数,并将结果四舍五入到该位数(我认为这足够安全)。文件中每种货币出现的小数位数 已经由解析器存储在 options_map 中的 DisplayContext 对象中进行追踪。我必须在库存记账方案中考虑这一点。我已将此内容添加到提案中。

需求

前几节介绍了记账的一般性问题,并指出了 Beancount 和 Ledger 实现中的重要不足。本节将提出一套针对命令行记账软件新改进记账机制的期望要求。

以下是新方法的合理要求:

  • 必须支持显式库存批次选择。用户应能为每笔交易精确指定用以冲减仓位的特定批次。一个账户或商品不应强制在整个交易历史中使用一致的方法。

  • 显式库存批次选择应支持部分匹配,即在批次匹配存在歧义时,用户只需提供最少的信息即可消除歧义,例如日期、标签、成本或这些信息的组合。例如,若仅存在一个可匹配的批次,用户可将成本指定为“{}”,告诉系统按成本记账,实质意思是:“与任意批次匹配”。只有当存在多个批次时,才应触发错误。

  • 应支持多种信息来指定现有批次,且这些信息应可组合使用:

    • 仓位的购入成本;

    • 仓位的购入日期;

    • 用户自定义的标签。

  • 当提供的信息不足以进行显式批次选择时,应允许调用默认记账方法。用户应能全局按账户指定默认记账方法。这为隐式批次选择铺平了道路。应提供一种退化方法,在无法自动选择时触发错误,强制用户显式指定。

  • 必须支持加权平均成本记账,因为这在退休账户或税收优惠账户中非常常见。这是加拿大投资账户的默认方法。

  • 成本基础调整也需要得到支持。问题应能通过指定特定批次(或按平均成本计算的某种商品的所有批次)以及一个美元金额来调整头寸来定义。

  • 股票分割需要能够保留头寸的部分原始属性,特别是原始交易日期和用户指定的标签(如果已提供)。这应允许在分割后减少头寸时使用通用语法指定原始交易批次。

  • 减少头寸时必须始终抵扣现有批次。我在此保留了 Beancount 的行为,此前我曾论证过其有效性,因为它能很好地避免数据录入错误。这自然定义了库存的一个不变量:在同一个库存中,所有以成本计价的特定商品头寸都应具有相同的符号(这可作为合理性检查)。

  • 改变单位数量符号的记账操作应引发错误,除非明确请求例外(即使如此,我也不确定我们是否需要它)。请注意,记账仅针对以成本计价的单位,因此货币兑换不受此要求影响。Beancount 已长期具备此功能,并已被证明有助于检测错误。例如,若库存中有一个 8 HOOL {500 USD} 的头寸,而你尝试对该批次记账 -10 单位,则结果单位数变为负数:-2。这应表明用户操作有误。我能想到的唯一允许这种情况的用例是期货价差和货币交易,它们自然会报告在同一账户中(货币的空头头寸并不像其他资产那样被视为不同工具);这是唯一需要提供例外的理由,但我怀疑 99% 的用户根本不需要它。

调试工具

由于这是输入交易数据过程中一个非平凡但极为重要的部分,我们应该提供工具,详细列出库存的累计余额,包括其所有批次的详细信息。

报告的记账错误应包含足够的上下文,即:

  • 包含错误记账的交易

  • 出错的记账项

  • 应用该记账前库存的当前状态,包括其所有批次

  • 用于消除批次歧义的隐式记账方法

  • 记账失败的详细原因

显式与隐式记账减少

在设计新记账方法时可能提出的另一个问题是:我们是否要求用户明确说明这是对头寸的增加还是减少。例如,可以通过对成本使用略有不同的语法来实现:使用“{}”表示增加,“[]”表示减少。我不确定这种区分是否必要,或许给用户增加的负担并不值得,但它可能是一种验证用户预期与实际计算结果是否一致的方法。目前我暂且放弃这一想法。

设计提案

本节提出了一种满足前述需求的具体设计方案。我们希望尽可能保持其简洁性。

库存

库存只是批次描述符的列表。每个库存批次将由以下内容定义:

UNITS, (CURRENCY, COST-UNITS, COST-CURRENCY, DATE, LABEL)

字段:

  • UNIT:该批次持有的单位数量

  • CURRENCY:持有的商品类型,为字符串

  • COST-UNITS:该批次价格中的数值

  • COST-CURRENCY:成本所用的计价商品

  • DATE:该批次的获取日期

  • LABEL:用于标识此批次的任意用户指定字符串

如果这代表按成本持有的批次,在此处理后,只有 LABEL 字段可选地为 NULL 值。任何使用此库存的代码都应假设其他所有值均为非 NULL(或全部为 NULL)。

输入语法与过滤

输入语法应允许以以下任意组合指定成本:

  • 单位成本,如金额“500 USD

  • 总成本,将自动按单位数量均分,例如:{... +9.95 USD}。您可以使用单位成本、总成本,或两者结合,例如:{500 + 9.95 USD}。这在输入佣金时非常有用。此语法取代了之前的{{...}}语法。

  • 批次日期,格式为 YYYY-MM-DD,例如“2014-06-20

  • 标签,即任何非数值标识符,例如 first-apple 或类似“aa2ba9695cc7”的随机 UUID

  • 特殊标记*”,表示按库存余额的平均成本记账

以下所有记账条目均为有效语法:

...
  Assets:Investments:Stock        10 HOOL {500 USD}      ; cost
  Assets:Investments:Stock        10 HOOL {339999615d7a} ; label
  Assets:Investments:Stock        10 HOOL {2014-05-01}   ; lot-date
  Assets:Investments:Stock        10 HOOL {}             ; no information

... Combinations
  Assets:Investments:Stock        10 HOOL {500 USD, 2014-05-01}
  Assets:Investments:Stock        10 HOOL {2014-05-01, 339999615d7a}

算法

所有记账条目应在解析后单独处理,按日期顺序与每个账户的累计库存余额进行匹配。成本金额应在这一阶段完全确定;若无法解析成本,应抛出错误并将该交易从指令流中删除(程序会大声报告此问题)。

处理条目时,应根据用户提供的所有筛选条件匹配并过滤所有库存批次。通常,在根据用户限制过滤批次后:

  • 如果批次集合为空,应抛出错误;

  • 如果仅有一个批次,应选择该批次(成功情况);

  • 如果有多个批次,应调用对应账户的默认隐式记账方法

隐式记账方法

如果在记账时有多个匹配批次可供选择,可调用以下隐式记账方法:

  • STRICT:选择库存中唯一的批次。若存在多个批次,则抛出错误。

  • FIFO:选择日期最早的批次。

  • LIFO:选择日期最晚的批次。

  • AVERAGE:按平均成本记账。

  • AVERAGE_ONLY:与 AVERAGE 相同,但同时在增加仓位时触发聚合。可用于强制所有批次仅以平均成本记账。

  • NONE:不对该账户执行任何库存记账。允许同一商品的批次混合,或库存中出现正负数。(这本质上等同于总账的记账方式。)

此方法仅在根据用户提供的表达式筛选批次后,需要对多个批次进行消歧时才会被调用。STRICT 方法是一种退化的消歧方式,只要存在任何歧义就会报错,应作为默认选项。

应为所有账户设置一个默认方法,且该默认值应可被覆盖。在 Beancount 中,我们将新增一个“选项”以允许用户更改此设置:

option "booking_method" "FIFO"

特定账户的默认值也应可指定,因为不同账户使用不同方法是常见情况,且在账户内部通常是固定的。在 Beancount 中,这可能会成为 open 指令的一部分,类似如下形式:

2003-02-17 open Assets:Ameritrade:HOOL  HOOL  booking:FIFO

(我不确定语法是否合适。)

解决相同日期的歧义

当自动记账方法(如 FIFO)被调用以解决模糊的批次减少时,若存在多个相同日期的批次,则需确定选择哪一个批次。应选择交易在文件中出现的行号。例如,在以下情况下,将选择 8 英镑的 WIDGET 批次:

2014-10-15 * "buy widgets"
  Assets:Inventory     10 WIDGET {} ;; Price inferred to 8 GBP/widget
  Assets:Cash         -80 GBP

2014-10-15 * "buy another widget"
  Assets:Inventory      1 WIDGET {} ;; Price inferred to 9 GBP/widget
  Assets:Cash          -9 GBP

2014-10-16 * "sell a widget"
  Assets:Cash           11 GBP 
  Assets:Inventory      -1 WIDGET {} ;; Ambiguous lot

默认插入的日期

默认情况下,若成本中未明确指定日期,则应自动将持有该条目的交易的日期附加到交易批次上。此日期可被覆盖,以便通过转换为如下形式的交易来实现股票分割:

2014-01-04 * "Buy some HOOL"
  Assets:Investments:Stock   10 HOOL {1000.00 USD}
  Assets:Investments:Cash

2014-04-17 * "2:1 split into Class A and Class B shares"
  Assets:Investments:Stock  -10 HOOL {1000.00 USD, 2014-01-04} ; reducing
  Assets:Investments:Stock   10 HOOL  {500.00 USD, 2014-01-04} ; augment
  Assets:Investments:Stock   10 HOOLL {500.00 USD, 2014-01-04} ; augment

无信息匹配

应支持不提供任何成本信息,这在某些情况下很有用:在增加头寸时,若其他所有条目均已指定数值,我们应能自动推断所交易单位的成本:

2012-05-01 * "First trade"
  Assets:Investments:Stock            10 HOOL {}
  Assets:Investments:Cash       -5009.95 USD
  Expenses:Commissions              9.95 USD

减少头寸的情况下——我们可以通过将交易应用到账户余额时的库存情况判断是否属于此类情况——空规格应触发默认记账方法。例如,若方法为 STRICT,则仅当仅存在一个可用批次时(选择无歧义)才会选择该批次,这在交易频率较低时可能是常见情况。其他方法将按其定义执行。

请注意,用户仍需指定成本为“{}”,以告知系统该条目需被视为“按成本持有”。这一点至关重要,用以区别于无关联成本的价格转换。

减少多个批次

若单笔交易需关闭库存中的多个现有批次,可通过为每个批次插入一条条目来轻松处理。我认为这是完全合理的要求。例如,若您的经纪商允许,这可以代表一笔单一交易:

2012-05-01 * "Closing my position"
  Assets:Investments:Stock           -10 HOOL {500 USD}
  Assets:Investments:Stock           -12 HOOL {510 USD}
  Assets:Investments:Cash       12000.00 USD
  Income:Investments:Gains

注意:若批次的成本基础规范与库存中的多个批次匹配,且结果无歧义,则会正确选择这些批次。例如,若交易前库存仅包含这两个批次(共 22 HOOL),您应能使用如下单条减少条目:

2012-05-01 * "Closing my position"
  Assets:Investments:Stock           -22 HOOL {}
  Assets:Investments:Cash       12000.00 USD
  Income:Investments:Gains

两者都将被包含。

另一方面,如果结果不明确(例如,您拥有超过这两个批次的其他批次),则会调用该账户的预约策略。默认情况下,此策略为“STRICT”,将生成错误;但如果该账户的策略设置为“FIFO”(或未设置且全局默认策略为“FIFO”),则将根据您的意愿自动选择 FIFO 批次。

批次基础修改

另一种思路是支持在单笔交易中修改特定批次的成本基础。实现方式是指定要修改的单位数量,并使用“+ 数量 货币”语法来按特定数值调整成本基础,如下所示:

2012-05-01 * "Adjust cost basis by 250 USD for 5 of the 500 USD units"
  Assets:Investments:Stock            ~5 HOOL {500 + 250 USD}

如果指定的单位数量小于该批次的总单位数,则应在应用成本基础调整前先拆分该批次。我不确定是否喜欢这种做法。

如果不指定数量,也可能调整整个头寸(我不确定这是否合理,因为它与当前语法差异较大):

2012-05-01 * "Adjust cost basis by 250 USD for the 500 USD units"
  Assets:Investments:Stock            HOOL {500 + 250 USD}

无论如何,除总成本调整外,所有其他字段都将用于选择要调整的批次。

标签复用

在库存预约过程中,我们必须谨慎警告批次标签的重复使用。标签应具有唯一性。

示例

没有什么比具体的例子更清晰了。若无特别说明,我们默认 Assets:Investments:Stock 账户的预约方法为 STRICT

无冲突

给定以下库存批次:

21, (HOOL, 500, USD, 2012-05-01, null)

22, (AAPL, 380, USD, 2012-06-01, null)

以下预约应成功:

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {}
  ...

以下预约应失败(因为金额不属于有效批次):

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {520 USD}
  ...

这个也应失败(无货币匹配批次):

2013-05-01 *
  Assets:Investments:Stock           -10 MSFT {80 USD}
  ...

这个同样应失败(指定了日期但日期无效):

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {500 USD, 2010-01-01}
  ...

通过成本显式选择

给定以下库存:

21, (HOOL, 500, USD, 2012-05-01, null)

32, (HOOL, 500, USD, 2012-06-01, “abc”)

25, (HOOL, 510, USD, 2012-06-01, null)

以下预约应成功:

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {510 USD}
  ...

如果股票账户的方法为 STRICT,此预约应失败(存在歧义):

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {500 USD}
  ...

如果股票账户的方法为 FIFO,此预约应成功,优先匹配 2012-05-01 的批次(最早):

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {500 USD}
  ...

通过日期显式选择

给定与之前相同的库存,此预约应成功:

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {2012-05-01}
  ...

如果方法为 STRICT,此预约应失败(存在歧义)

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {2012-06-01}
  ...

通过标签显式选择

此预约应成功,因为只有一个批次带有“abc”标签:

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {abc}
  ...

如果多个批次具有相同标签,则可能出现歧义情况;例如,使用以下库存:

32, (HOOL, 500, USD, 2012-06-01, “abc”)

31, (HOOL, 510, USD, 2012-07-01, “abc”)

相同的预订应失败。

通过组合显式选择

在初始库存下,此预订应成功(无歧义):

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {500 USD, 2012-06-01}
  ...

只有一个批次,成本为 500 美元,购入日期为 2012-06-01。

单位不足

以下预订虽然无歧义,但会失败,因为库存中没有足够的该批次单位可供扣除:

2013-05-01 *
  Assets:Investments:Stock           -33 HOOL {500 USD, 2012-06-01}
  …

重复选择同一批次

如果同一笔交易中的两个独立记账项选择了同一个库存批次,它应该能够正常工作:

2013-05-01 *
  Assets:Investments:Stock           -10 HOOL {500 USD, 2012-06-01}
  Assets:Investments:Stock           -10 HOOL {abc}
  ...

当然,如果份额不足,它应该失败:

2013-05-01 *
  Assets:Investments:Stock           -20 HOOL {500 USD, 2012-06-01}
  Assets:Investments:Stock           -20 HOOL {abc}
  ...

自动价格推算

如果一笔交易的所有记账项都能计算出其余额,我们允许自动计算一个价格——这应该可以正常工作:

2014-03-15 * "Adjust cost basis from 500 USD to 510 USD"
  Assets:US:Invest:HOOL      -10.00 HOOL {500.00 USD}
  Assets:US:Invest:HOOL       10.00 HOOL {}
  Income:US:Invest:Gains    -340.51 USD

其效果等同于:

2014-03-15 * "Adjust cost basis from 500 USD to 510 USD"
  Assets:US:Invest:HOOL      -10.00 HOOL {500.00 USD}
  Assets:US:Invest:HOOL       10.00 HOOL {534.51 USD}
  Income:US:Invest:Gains    -340.51 USD

当然,保留批次的原始日期也应有效,因此以下情况也应能正常工作:

2014-03-15 * "Adjust cost basis from 500 USD to 510 USD"
  Assets:US:Invest:HOOL      -10.00 HOOL {500.00 USD}
  Assets:US:Invest:HOOL       10.00 HOOL {2014-02-04}
  Income:US:Invest:Gains    -340.51 USD

平均成本记账

使用平均成本语法扩充批次应失败:

2014-03-15 * "Buying at average cost, what does this mean?"
  Assets:US:Invest:Stock      10.00 HOOL {*}
  Income:US:Invest:Gains   -5000.00 USD

减少操作应有效,这是主要使用场景:

2014-03-15 * "Buying a first lot"
  Assets:US:Invest:Stock      10.00 HOOL {500.00 USD}
  Assert:US:Invest:Cash    -5000.00 USD

2014-04-15 * "Buying a second lot"
  Assets:US:Invest:Stock      10.00 HOOL {510.00 USD}
  Assets:US:Invest:Cash    -5100.00 USD

2014-04-28 * "Obtaining a dividend in stock"
  Assets:US:Invest:Stock         1.00 HOOL {520.00 USD}
  Income:US:Invest:Dividends  -520.00 USD

2014-05-20 * "Sell some stock at average cost"
  Assets:US:Invest:Stock      -8.00 HOOL {*}
  Assets:US:Invest:Cash     4240.00 USD
  Income:US:Invest:Gains    ; Automatically calculated: -194.29 USD

其效果是将 10 HOOL {500.00 USD} 批次、10 HOOL {510.00 USD} 批次和 1.00 HOOL {520.00 USD} 批次合并为一个 21 HOOL {505.714285 USD} 的批次,并从中扣除 8 HOOL,即 8 HOOL × 505.714285 USD/HOOL = 4045.71 USD。我们收到 4240.00 USD,从而可自动计算资本收益:4240.00 - 4045.71 = 194.29 USD。减少后,剩余一个 13 HOOL {505.714285 USD} 的批次。

如果账户中包含多种货币,这也应能正常工作,即在上述基础上添加以下交易不应导致失败:

2014-04-15 * "Buying another stock"
  Assets:US:Invest:Stock      15.00 AAPL {300.00 USD}
  Assets:US:Invest:Cash    -4500.00 USD

然而,如果同一账户中同一种货币以不同的成本货币计价,则应失败:

2014-03-15 * "Buying a first lot"
  Assets:US:Invest:Stock      10.00 HOOL {500.00 USD}
  Assert:US:Invest:Cash    -5000.00 USD

2014-04-15 * "Buying a second lot"
  Assets:US:Invest:Stock      10.00 HOOL {623.00 CAD}
  Assets:US:Invest:Cash    -62300.00 CAD

2014-05-20 * "Sell some stock at average cost"
  Assets:US:Invest:Stock      -8.00 HOOL {*}  ; Which HOOL, USD or CAD?
  Assets:US:Invest:Cash    4240.00 USD
  Income:US:Invest:Gains

但当然,这种情况在实际中从不发生,因此我并不太担心。我们或许可以支持指定成本货币来解决此情况,例如将卖出方替换为:

2014-05-20 * "Sell some stock at average cost"
  Assets:US:Invest:Stock      -8.00 HOOL {* USD}
  Assets:US:Invest:Cash    4240.00 USD
  Income:US:Invest:Gains

我确信这没什么用处,但我们也可以做到尽可能通用。

未来工作

本提案尚未处理成本基础调整!我们需要一种方法,能够向头寸的成本基础中添加或移除一个固定的美元金额,即,从(1)批次标识符和(2)成本货币金额出发,我们应能更新该批次的成本。交易仍需保持平衡。

以下是可能的实现方式:

2014-05-20 * "Adjust cost basis of a specific lot"
  Assets:US:Invest:Stock       10.00 HOOL {500.00 USD + 230.00 USD}
  Income:US:Invest:Gains     -230.00 USD

最终库存为 10 HOOL,每股成本为 523.00 美元,其余原始批次则减少 10 HOOL,每股成本为 500.00 美元(可能仍有部分剩余,这没有问题)。这等价于:

2014-05-20 * "Adjust cost basis of a specific lot"
  Assets:US:Invest:Stock      -10.00 HOOL {500.00 USD}
  Assets:US:Invest:Stock       10.00 HOOL {523.00 USD}
  Income:US:Invest:Gains     -230.00 USD

但你无需手动进行计算。因此,这应作为转换功能实现。然而,这并不特别有用,因为该功能已得到支持……调整通常针对平均成本批次进行,而问题恰恰在于:你必须自己进行计算!以下是一个相关用例:

2014-05-20 * "Adjust cost basis of a specific lot"
  Assets:US:Invest:Stock       10.00 HOOL {* + 230.00 USD}
  Income:US:Invest:Gains     -230.00 USD

这将首先将所有 HOOL 批次平均化,并根据新金额重新计算成本基础。这应该可以正常工作:

2014-03-15 * "Buying a first lot"
  Assets:US:Invest:Stock       5.00 HOOL {500.00 USD}
  Assert:US:Invest:Cash    -2500.00 USD

2014-04-15 * "Buying a second lot"
  Assets:US:Invest:Stock        5.00 HOOL {520.00 USD}
  Assets:US:Invest:Cash     -2600.00 USD

2014-05-20 * "Adjust cost basis of the Hooli units"
  Assets:US:Invest:Stock       10.00 HOOL {* + 230.00 USD}
  Income:US:Invest:Gains     -230.00 USD

请注意,我们仍需指定要重新定价的 HOOL 股份数量。我喜欢这一点,但我怀疑我们是否应该允许像这样指定“所有单位”(这将是对语法的更激进更改):

2014-05-20 * "Adjust cost basis of the Hooli units"
  Assets:US:Invest:Stock           * HOOL {* + 230.00 USD}
  Income:US:Invest:Gains     -230.00 USD

或许自动计算成本基础调整的最佳方式是通过强大的插值,如下所示:

2014-05-20 * "Adjust cost basis of a specific lot"
  Assets:US:Invest:Stock      -10.00 HOOL {500 USD}  ; reduce
  Assets:US:Invest:Stock       10.00 HOOL {}         ; augment, interpolated
  Income:US:Invest:Gains     -230.00 USD

有关该提案的更多详情,请参阅《更智能的省略》文档。

跨账户记账

某些税法要求用户按照特定方法记账,这可能适用于所有账户。这意味着需要应用某种转账机制以正确处理此情况。详情请参阅单独的文档

待办 - 补充更多关于平均成本记账的测试!

实现说明

分离解析与插值

为了实现一种不指定成本的缩减语法,我们需要对解析器进行修改。由于记账项可能没有成本信息,因此在解析阶段无法执行余额检查。因此,我们需要将余额检查推迟到解析之后的阶段进行。这是合理且有益的:最好让 Beancount 解析器在此阶段不输出过多复杂错误。我们将在运行插件之前新增一个“解析后”阶段。

结论

本文档仍在进行中。我非常希望获得反馈,以改进所建议的方法,因此我选择将想法写下来,而不是直接编写代码。我相信通过迭代此设计,可以达成更好的语义,使其在所有系统中表现更优,并可作为后续开发者理解其设计意图的文档。

非常感谢您的反馈。