计算投资组合收益
Martin Blais,2020 年 9 月
http://furius.ca/beancount/doc/returns
本文介绍如何从 Beancount 账本中计算投资组合的回报率。
动机
你可能会惊讶地发现,折扣经纪商通常不会根据你的具体现金流提供准确且完整的投资回报计算。它们倾向于报告其他类型的表现指标:
-
价值变动。 最简单的一种是提供周期(或年度)开始和结束时账户价值的快照。这种方法的问题在于,它没有反映你存入或取出的资金,也没有反映你持仓的变化。例如,如果你年初账户有 50,000 美元,8 月又存入了 30,000 美元,而年底报告的差额为 37,000 美元,这根本无济于事(你必须在心理上扣除年内新增的投资资金,而如果你还想进一步拆解并比较账户内不同工具的回报呢?)。
-
底层资产表现。 它们可能会单独报告底层资产的增长,而忽略你的具体持仓情况。如果你只在下半年(或主要在下半年)投资了 HDV,那么说“HDV 去年上涨了 8.2%”对你意义不大。我真正想知道的是,基于我特定的持仓变动和时机,我的具体投资增长了多少。换句话说,我 的表现如何?
-
忽略股息。 另一个问题在于,通常报告的表现仅包含因价格变动带来的资本增值。理想情况下,我们希望将表现拆分为资本增值和股息两部分,并分别计算每个组成部分以及总体回报,以便比较股票和债券的收益。有时你可以在摘要的“收益率”字段中找到这些信息,但对于不均匀的派息,这无法帮助我确定任何时间段内实际获得的股息回报。
-
佣金和管理费用。 一些经纪商会按月根据账户价值收取管理费。我希望知道扣除这些费用后的投资组合净回报。
我需要基于我实际的现金流和随时间变化的持仓情况,获得准确的回报报告。
如果我将你的投资信息记录在 Beancount 账本中,理论上它包含了计算你真实回报所需的所有数据——这些数据基于我储蓄(现金注入)的具体时间点以及我在不同时间持有的持仓。但这些数据并不以最简单的格式呈现——Beancount 交易的灵活性远超所需,我们需要从中提取出更简洁的现金流序列。本文将说明我如何最终从自己的账本中完成这一过程。 并说明如何根据一些简单规则,将此方法推广到你的账本中。本文大部分内容都致力于详细说明如何提取正确的数据。
源代码可在此处获取:这里。
此外,基于相同的现金流,还应能公平、诚实地与其他投资情景进行比较。例如,你应该能够生成类似这样的数据:“我的 ZZZ 投资回报率为 8.2%,其中 1.1% 来自股息;如果我投资于 60/40 的宽基股票与债券组合,回报率则为 7.2%。” 换句话说,我希望评估我的表现相对于若干常见替代方案的优劣。
(最后,请注意,如果你只需要当前持仓的快照,这已经由导出脚本自动处理了。)
历史记录
2014 年,我曾短暂尝试将账本中的信息进行拆解以完成这项工作。当时我在处理一些时间序列数据时陷入细节,发现提取出的部分结果并不合理(存在异常值)。我过早地追求细节,而有时更高效的做法是先完成整体任务,再回头处理细节。当时我没有记录足够的调试信息,也缺乏对输出结果的足够信心,因此从未真正完成这项工作,最终将脚本移入实验目录并搁置一旁,心想:"以后再说吧。"
2020 年 8 月,我再次坐下来尝试这项工作,但这次目标更为务实:仅求得一个良好的近似值,并生成大量调试输出,将数据提取简化为仅提取现金流,并确保在所有账户中完整完成任务,即使这意味着需要对输入文件进行一些调整。事实证明,这是一个正确的决定:我成功完成了任务,本文将记录我的整个过程、方法、假设、特点以及部分结果。
方法概览
我所采用的方法是为每项投资提取现金流信息。在此语境下,我所说的"一项投资"是指一种金融工具,例如 Vanguard 总股票市场 ETF(VTI)的股份,并投资于特定账户,例如 Vanguard 401k 账户。现金流列表是一组带有日期的正负货币值,代表对该项投资的投入或撤出资金。因此,我为每个账户维护一个现金流列表,且每个账户仅记录一种金融工具。
在报告中,我会将这些按账户划分的现金流序列分组为逻辑上的"报告"单元,例如将同一金融工具在不同账户/券商中的投资合并,或将代表相同底层资产的不同工具的现金流合并,甚至按"策略"进行分组。本质上,通过合并多个账户的现金流,我获得了一种通用方式,可对任意账户子集进行分组并计算其收益。
利用这些现金流数据,我将运行一个简单的求根算法,计算这些现金流需以怎样的年均增长率才能达到其最终市场价值,从而得出整体收益率。这类似于计算内部收益率。我不仅会对整个时间序列进行计算,还会对其中的子区间进行计算,以得出例如日历收益(即每年或每季度)或滚动期间的累计收益。由于现金流会标记为是否为股息,我可以将增值收益与股息收益区分开来。每个分组都会生成包含图表的报告。
下图展示了"configure"、"compute_returns"和"download_prices"三个脚本如何协同工作:

以下将详细说明这些内容。
配置
首先,需要创建一个配置文件,用于定义需要生成报告的投资列表及其分组。该配置以文本格式的协议缓冲区消息提供。
-
投资。 一项投资对应存储在特定账户中的某种金融工具,同时也涉及其他不直接与该账户相关的交易。我们希望提供一组账户名称:
-
资产账户。 存放该投资相关商品的账户名称。
-
匹配账户。 一组额外账户,用于筛选与这些账户发生过记账的交易。
-
现金账户。 一组与该投资无关的外部账户,用于标记该投资的流入和流出资金。
-
-
报告。 虽然我们可以为每个投资单独计算收益,但更通用且实用的做法是计算一组投资的综合收益。报告本质上是一组投资名称的列表,系统将为每个报告生成一个 PDF 文件。
以下是 6 项投资和 3 份报告的示例:
investments {
# Accounts at BTrade.
investment {
currency: "GLD"
asset_account: "Assets:US:BTrade:GLD"
cash_accounts: "Assets:US:BTrade:Cash"
}
investment {
currency: "AMZN"
asset_account: "Assets:US:BTrade:AMZN"
cash_accounts: "Assets:US:BTrade:Cash"
}
investment {
currency: "QQQ"
asset_account: "Assets:US:BTrade:QQQ"
dividend_accounts: "Income:US:BTrade:QQQ:Dividend"
cash_accounts: "Assets:US:BTrade:Cash"
}
# Accounts at IBKR.
investment {
currency: "IAU"
asset_account: "Assets:US:IBKR:IAU"
cash_accounts: "Assets:US:IBKR:Cash"
}
investment {
currency: "SLV"
asset_account: "Assets:US:IBKR:SLV"
cash_accounts: "Assets:US:IBKR:Cash"
}
# Accounts at Schwab.
investment {
currency: "GOOG"
asset_account: "Assets:US:Schwab:GOOG"
cash_accounts: "Assets:US:Schwab:Cash"
cash_accounts: "Assets:AccountsReceivable"
cash_accounts: "Assets:US:GoogleInc:GSURefund"
}
}
groups {
group {
name: "strategy.gold"
investment: "Assets:US:BTrade:GLD"
investment: "Assets:US:IBKR:IAU"
}
group {
name: "strategy.tech"
investment: "Assets:US:BTrade:QQQ"
investment: "Assets:US:BTrade:FB"
investment: "Assets:US:Schwab:GOOG"
}
group {
name: "all"
investment: "Assets:US:*"
currency: "USD"
}
group {
name: "accounts.BTrade"
investment: "Assets:US:BTrade:*"
currency: "USD"
}
}
不同的报告可以包含相同的投资。配置文件中对账户和投资名称的引用支持简单的 UNIX 风格通配符模式,这些模式将在运行时扩展为完整的账户名称,并存储在输出中。
一个名为 "configure.py" 的脚本可自动从现有的 Beancount 账本中推断出一个可用的基础配置。系统将为每个唯一金融工具生成一份报告,并遵循 "export" 脚本所支持的相同元数据字段(如 "assetcls"、"strategy")来生成报告。我建议您先在您的账本上运行此脚本,然后手动调整配置。
查找账户
此脚本需要确定需要报告的投资列表。按照惯例,我在账本中为每种商品类型保留一对专用的叶子账户:一个用于存放实际持仓(资产),另一个用于接收股息。例如,对于在经纪商 "BTrade" 持有的 "VTI",账户如下:
2012-03-01 open Assets:US:BTrade:VTI VTI
2012-03-01 open Income:US:BTrade:VTI:Dividend USD
这带来两个结果:(a) 它使查找包含投资的账户变得简单(任何叶子账户名称与账本中商品名称匹配的账户);(b) 它通过查找所有与该账户发生过记账的交易,清晰地隔离了与每个投资相关的所有活动。我建议您在会计科目表中遵循相同的惯例。 最终得到的是一组类似 "Assets:US:BTrade:VTI" 的账户列表,每个账户都对应特定的(金融工具,机构)组合。
我进一步将此列表过滤为截至 15 年前仍处于开放状态的账户子集(我使用 Beancount 的 Close 指令关闭已完成的账户)。在我的情况下,那时储蓄很少,因此没有必要为如此久远的投资历史中的零散数据做标准化处理。
提取现金流量数据
本节描述了我从账本中提取相关数据所采取的各个步骤。
提取相关交易
对于每个已识别的资产账户,我们需要从账本的交易记录中提取影响该账户的所有交易。我们遍历账本中的全部交易,保留至少有一笔记账涉及投资的资产账户、股息收入账户或其定义的任何其他“匹配账户”的交易。
例如,现金股息支付的交易不会显示资产科目记账,因此如果支付股息,典型的股息支付通常仅包含股息收入科目和现金科目(用于存款):
2019-11-27 * "Dividend"
Income:US:BTrade:VTI:Dividend -123.45 USD
Assets:US:BTrade:Cash 123.45 USD
因此,有必要在配置中包含股息科目,因为这些交易通常不涉及资产科目。
科目分类
下一步是将交易泛化为模板:我们记录每个投资所提取交易中涉及的所有科目,并根据其在交易中的角色分配到通用类别。例如,如果我查看我的“VTI”交易,我会遇到以下科目:
Assets:US:BTrade:Cash
Assets:US:BTrade:VTI
Expenses:Financial:Commissions
Income:US:BTrade:VTI:Dividend
我将每个科目映射到几个通用类别中(我现在可能可以进一步简化):
ASSET # The account holding the commodity.
CASH # Cash accounts, employer matches, contributions.
DIVIDEND # Dividend income account.
EXPENSES # Commissions, fees and other expenses.
INCOME # Non-dividend income, P/L, gains, or other.
OTHERASSET # Other assets than the primary asset for this investment.
OTHER # Any other account.
如下所示:
'Assets:US:BTrade:Cash': CASH
'Assets:US:BTrade:VTI': ASSET
'Expenses:Financial:Commissions': EXPENSES
'Income:US:BTrade:VTI:Dividend': DIVIDEND
通过这种方式,我可以跨不同投资工具比较相似的交易,并使用相同的代码提取相关信息。例如,涉及股息科目和现金科目的交易即为现金股息支付,我可以编写一个通用处理程序来提取此类交易的现金流。该分类最初是通过一组临时规则进行原型设计的,但现在配置已作为分类的依据。
Note: In the process of doing this, I noticed many irregularities in how I named my accounts. For example, I used ":Dividend" and ":Dividends" sometimes. I went through my ledger and had to make some changes to name accounts coherently, and iterated until all my accounts were categorized correctly. You may have to review some of your data entry as well.
使用签名处理交易
利用上一节中的科目-类别映射,我为每笔交易推导出唯一的“签名”。例如,像这样的交易:
2020-03-12 * "(DOI) ORDINARY DIVIDEND"
Income:US:BTrade:VTI:Dividend -1312.31 USD
Assets:US:BTrade:Cash 1312.31 USD
其签名将是
CASH_DIVIDEND
这很可能始终代表一笔股息支付。Beancount 的语法非常灵活,并不要求你的交易必须遵循特定模板,因此在我开始这个项目时,我并不清楚在长达 12 年的临时数据录入记录中会发现哪些模式……我无法确定这种分类和签名是否足以正确转换为现金流。因此,我让脚本生成了两组调试文件:
-
投资详情。 为每个投资生成一个文件,列出为该投资提取的所有交易,并附上每个记账项推断出的分类元数据,以及所有遇到的科目分类映射。我通过人工检查这些文件,确保配置中的科目/模式正确提取了与该投资相关的全部交易。
-
签名交易。 为每个唯一签名生成一个文件,列出所有投资中匹配该签名的完整交易列表。通过检查这些文件,我确认了所有匹配同一签名的交易确实扮演相同的角色,因此每个签名只需一个处理程序即可。
此时,我得到了一组有限的唯一签名,每个签名都有明确且独特的角色:
ASSET_CASH : Purchase or sale
ASSET_CASH_EXPENSES : Purchase or sale with commission
ASSET_CASH_INCOME : Purchase or sale with profit
ASSET_CASH_INCOME_EXPENSES : Purchase or sale with commission and profit
ASSET_EXPENSES : Fee paid from liquidation
ASSET_INCOME : Cost basis adjustment (with P/L)
ASSET_INCOME_EXPENSES : Fee from liquidation (with P/L)
ASSET : Stock splits
ASSET_DIVIDEND : Dividend reinvested
CASH_DIVIDEND : Dividend payment
CASH_INCOME_DIVIDEND : Dividend payment and gains distribution
ASSET_OTHERASSET : Exchange of stock/symbol
…
请注意,具体的列表实际上取决于你的账本内容,你应该检查生成的文件以确保其正确性。
随后,我为每个交易签名编写了专门的处理程序,以生成相应的现金流,并独立分析每种情况。这使我能够正确生成每个投资的完整现金流列表。
注意:在实际操作中,我遇到了三到四种更为特殊的签名类型,通过修正我的账本,我成功将这些交易转换为等效但更简单的形式。特别是,我的某个导入器会将同一天发生的交易合并,我后来回溯并手动修正了这些交易。尤其是 ASSET_OTHERASSET 签名,代表的是股票兑换(例如 Google -> GOOG,GOOGL)。执行此类操作会暴露出你记账方法中的特殊性。保持一致性并减少模板数量会很有帮助。
开发一个配套插件,将可用模板限制为少数几种,使数据录入能更好地适配本回报生成代码,将是一个非常有价值的想法。
通用现金流量生成
在检查了每个签名处理程序后,我尝试将它们统一为一个通用处理程序,以适用于所有交易。结果发现,至少在我的现有账本交易中,这是完全可行的。
本质上,记录现金账户或其他资产的流入或流出就已足够。例如,在以下交易中:
2013-09-18 * "Buy shares of HOOL"
Assets:US:BTrade:Cash -818.55 USD
flow: CASH
Assets:US:BTrade:HOOL 8 HOOL {101.20 USD}
flow: ASSET
Expenses:Financial:Commissions 8.95 USD
flow: EXPENSES
“CASH”条目已足以代表现金流入,因此我们记录 -818.55 美元。在现金股息支付中:
2013-12-17 * "Cash Dividend payment"
Assets:US:BTrade:Cash 38.50 USD
flow: CASH
Income:US:BTrade:HOOL:Dividends -38.50 USD
flow: DIVIDEND
同样,38.50 是一笔足够的现金流出,因此我们记录 +38.50 美元。另一方面,某些共同基金中的再投资资产股息不会产生任何现金流动;它仅保留在投资中并增加其总价值:
2013-12-30 * "Reinvested dividend"
Assets:US:BTrade:HOOL 0.356 {103.41} USD
flow: ASSET
Income:US:BTrade:HOOL:Dividends -36.81 USD
flow: DIVIDEND
这一规则似乎足以正确处理我账本中的所有内容。
最终,我同时实现了两种方法:我使用通用规则生成现金流量列表,但同时也调用显式处理程序,并交叉验证提取的现金流量是否完全一致,以确保万无一失。此功能由 compute_returns.py 中的一个标志启用(--check-explicit-flows)。这迫使我对所有可能的交易模板进行彻底分析。
Note: If in using this script you find cases from your ledger that aren't handled by using a series of cash accounts, please let me know (on the mailing-list).
现金流量
上一节中描述的处理程序分别为每笔交易生成了一组现金流量,对整个账户而言,它们本质上构成了如下列表:
(Date, Amount, IsDividend)
现在,这是一个更简单的模型。对于每个账户,我们现在拥有一组按日期排序的现金流量序列。请注意,金额包含其计价货币(我同时持有美元和加元),是否为股息是一个标识该现金流量是否为股息支付的标志(用于计算不含股息的回报)。这些现金流量序列可以轻松在账户间合并,并通过插入对应日期市场价值的期初或期末现金流量来截断时间范围。渲染后可能如下所示(由于规模原因,使用对数刻度能揭示 otherwise 难以观察的细节;股息以绿色显示):

请注意,由于许多交易不产生现金流量,仅凭投资的现金流量列表不足以计算其随时间的价值。在按时间区间截断时,投资的市场价值需通过交易列表推导得出。
最后,每个投资组报告的现金流量列表可以通过简单拼接轻松合并。
计算回报
计算平均增长率
对于每一系列现金流,我们将这些现金流合并在一起。我使用scipy.optimize.fsolve来计算满足净现值为零的利率:
cfi/(1 + r)ti= 0
其中 cfi 为各笔现金流的金额(带符号),ti 为各笔现金流距离今天的时间(以年为单位)。我们求解 r。若要计算不含股息的回报,只需排除来自股息的现金流即可。两者的差异即表明了回报中有多少完全来自股息收入。
需要注意的是,如果相关头寸仍处于投资状态,您必须在最新日期插入一笔负现金流,金额等于当前市场价值,以使净现值归零。这相当于模拟了一次卖出操作。如果涉及显著的交易成本,您可能也希望模拟这些成本(例如,当您计算房产回报时尤其如此)。
这种方法的精妙之处在于:整个过程中从未使用底层价格,除了在期末将头寸按市场价值标记外。我们没有依赖任何外部回报指标。这些回报完全基于资金的流入与流出计算得出。它们是真实的、已实现的回报,绝无虚假。
时间区间。 为计算日历期回报(例如 2016、2017、2018、2019、2020 年的回报),我将现金流截断,仅保留落在目标区间内的部分,例如 2018 年对应区间为 2018-01-01 至 2018-12-31;若区间开始时已有持仓,则在区间起始日插入一笔负现金流,金额等于该日的市场价值。在区间结束时,我也按前述方法插入一笔正现金流。
理想情况下,我希望考察不同类别的区间:
-
投资生命周期。 计算头寸整个生命周期的总回报,最终归结为一个单一数值。
-
日历周期。 过去 10 年或 15 年内的年度或季度回报,以观察回报随时间的变化情况。
-
累计回报。 过去 10 年或 15 年内的累计回报,按日历周期对齐,以判断整体趋势是改善还是恶化,并评估我的策略在近期(如最近 3 年)的表现。
-
高频累计回报。 过去 12 个月内的累计回报,按月度或周度日期对齐,以评估短期决策的影响。
这些结果可以以表格形式呈现,便于相互比较不同投资的表现。
填补缺失的价格数据
在不同时间区间内计算回报时,需要在区间的起止日期将头寸按市场价值标记。由于 Beancount 的设计是封闭的——即它不会从外部获取价格,仅使用账本中已记录的数据——其价格查询会自动返回请求日期之前最近一次可用的价格及其记录日期。根据您的记录规范,部分价格可能已过时,从而引入误差。这一点尤其重要,因为在期末转换的金额(用于估算当前头寸价值)可能较大,会显著影响甚至改变整个生命周期的回报数值。
因此,保持账本价格数据库中的价格点相对新鲜非常重要。现在的问题是,在随着时间变化的一组持仓情况下,对于一组给定的时间区间,需要哪些价格条目才能产生准确的结果?
由于这个问题高度依赖于收益脚本的具体输入,为了解决它,我简单地用一个外观模式封装了价格数据库,在生成报告时收集所有请求转换的(工具,日期)组合,并根据某个年龄阈值进行筛选(例如,价格不超过 3 天)。这些本质上就是文件中缺失的价格点。
在脚本结束时,我会将这些价格点输出到一个包含 Price 指令的文件中,另一个程序(download_prices.py)可以读取该文件并获取这些价格的历史数据。它会生成更新后的汇率,你可以将这些数据一次性粘贴到你的账本文件中,然后重新计算更准确的收益。从 Yahoo! Finance 拉取数据对我 90%的持仓都有效,但一些较老的工具可能已经过时、退市或不可用(例如某些退休基金),因此我不得不通过浏览手动输入部分价格点(大约有 30 个左右……并不算麻烦)。
滚动窗口。 一个重要要点是,未来最好将报告区间对齐到某种基于日历的周期(例如按月),这样我就不必每次生成收益时都重新生成价格数据。按月对齐对于我的时间范围来说可能已经足够了。
股票分割。 Beancount 并不会显式地对发生分割的股票价格进行调整,因此如果你记录的是分割前的价格,你的价格数据源应返回分割前的价格。你可以使用 Fava 检查你的价格时间序列是否存在错误。
货币兑换
另一个重要细节是,每项投资都有其自身的报价货币。我曾居住在加拿大,一些较早的投资是以加元(CAD)计价的。因此问题来了:我应该以本地货币(CAD)计算收益,还是以我的基准货币(美元)计算?
Beancount 的 Inventory 对象提供了方便的函数,可以轻松完成所需的货币转换。而且,由于我提取的现金流是使用 Beancount 的 Amount 对象存储的,我的提取数据中已正确包含了报价货币。在任何一组中,如果所有工具的报价货币都相同,我就以该货币报告收益;如果组内包含多种报价货币,我会进一步将所有金额转换为美元(从而得到以美元计的收益)。
报告
账户分组
在每个“报告”中,根据配置定义,每个账户组的收益会一起计算。以下是一些合理的分组示例:
-
不同账户中的同一工具。 如果你在不同账户中购买了同一只股票,那么将该股票在所有账户中的收益合并报告是合理的。
-
相同底层资产。 有些工具代表同一只股票,例如 GOOG 和 GOOGL(不同股份类别,但属于同一家公司)。此外,IAU 和 GLD(黄金)是两种不同的黄金 ETF,它们的价值都源自伦敦银行地窖中的实物黄金储备。
-
同一资产类别。 来自同一资产类别的工具,例如“金属”,包括 IAU、GLD、SLV、COPX 等,或“房地产投资信托基金(REITs)”,包括 VNQ、VGSLX 等。或“所有股票”与“所有债券”的对比。
-
按策略划分。 在我的投资组合方法中,我采用多策略方法,定义特定的广泛策略,然后选择一组工具来实施这些策略。
-
例如,我有一个“科技板块”策略,包含 FAANG 公司。
-
或一个“成长型股票”策略,可能包括 VUG、IWO 和 RGAGX 等不同指数。我可以跨经纪商报告这些策略的表现。
-
或按地域划分,例如“发达亚太地区”,包括 EWY、EWT、EWS、EWA。
-
-
按经纪商划分。 我可以按经纪商或经纪账户报告收益。特别是,这可以轻松区分按税务处理方式划分的已实现收益(例如,401k 为递延纳税账户)。
-
资产类型。 比较所有指数基金与所有主动管理型基金(例如共同基金)。
请注意,不同的报告可能包含相同的投资。分组并非互斥。您应根据自身情况定义最合理的分组方式。作为参考,我使用了超过 20 个报告分组。
运行代码
只需调用
./experiments/returns/compute_returns.py <file.beancount> <config.pbtxt> <output-dir>
以生成所有报告和调试文件,其中 <config.pbtxt> 的格式如“配置”部分所示。运行速度稍慢——可能存在一些性能优化空间——但如果您在最后一个参数后提供一份报告名称列表,则仅会处理这些投资和报告,从而加快迭代速度。详情请参阅 --help 标志和 config.proto 中的配置输入文档。
结果呈现
对于每个报告分组,我目前生成以下内容:
-
一个现金流随时间变化的图表,以及一个较小的对数(现金流)图表。股息支付通常远小于本金的现金流,因此对数图有助于我观察时间点。这主要用于概览随时间的活动情况及调试。
-
一个累计价值图表,包含两条曲线:
-
累计现金流,其增长曲线与我回归计算的收益一致。结果应为一条在现金流之间呈平缓斜率的曲线(对应总收益增长),并在最后降至零。
-
随时间变化的市场价值:显示投资组合随时间的市值曲线。这使我能够根据价格变动理解日历收益。
-
-
一个包含总收益、不含股息的收益和仅股息收益的表格。
-
一个包含每年日历收益的表格,也按总收益、不含股息收益和仅股息收益细分。(我未来可能会将其渲染为图表。)
-
一个包含滚动累计收益的表格。
由于我正在积极开发此代码 [2020 年 9 月],这些内容将持续优化和扩展。
示例
以下是针对一组以“成长型”为重点、分布在不同经纪商的账户生成的示例报告。我为每个报告分组都会生成一份此类报告。(为保护隐私,部分内容已灰化。)

解读注意事项
需要说明几点:
所有收益率均为年化。这使得比较不同数据变得容易,但也意味着短期持有的头寸所产生的数字在长期外推时可能不切实际。特别是,几个月前才建立的新头寸可能经历了高速增长或大幅下跌,而将这些波动外推至一整年时,可能会显示出惊人的百分比。请特别注意这一点,尤其是在查看您投资组合中近期新增的头寸时。
未考虑税收因素。应税账户和税收递延账户的收益应分别评估,若税收差异较大,则难以直接比较。请注意,在大多数国家,资本利得仅在实现时(即出售时)征税,因此长期持有的投资实际上与税收递延投资的效果差不多。只需少卖一些即可。这在持有广泛分散的 ETF 时是一个巨大优势(但通常未被量化,因为人们的注意力仍过度集中在注册账户的收益上,例如 401k 计划)。
成本基础。请注意,在我们的计算中从未使用或考虑过成本基础,因此不要将其与市场价值混淆。成本基础仅对税务相关影响有用。
其他投资工具类型
请注意,这种计算收益率的方法不仅限于股票和债券。使用相同的方法,我们还可以纳入其他类型的投资工具:
点对点借贷
在对冲基金尚未介入、尚无低垂果实可摘时,我曾使用过 LendingClub,最终让所有我投资的债券自然到期。用同样的方法计算这些投资的收益非常简单。最初,我坚持通过月度 PDF 报表记录这些投资,交易记录如下:
2016-10-31 * "2016-10-31.Monthly_Statement.pdf"
Assets:US:LendingClub:FundsLent -451.52 LENCLUB {1 USD}
Assets:US:LendingClub:Cash 451.52 USD
Income:US:LendingClub:LoanInterest -21.68 USD
Income:US:LendingClub:Recoveries -5.92 USD
Expenses:Financial:Fees 1.08 USD ;; Recovery fees
Expenses:Financial:Fees 4.71 USD ;; Service fees
Expenses:Financial:Fees 0.45 USD ;; Collection fees
Assets:US:LendingClub:Cash
Assets:US:LendingClub:FundsLent -23.05 LENCLUB {1 USD}
Income:US:LendingClub:ChargedOff 23.05 USD
后来,当本金债券到期后,交易记录如下:
2018-11-30 * "2018-11-30.Monthly_Statement.pdf"
Income:US:LendingClub:Recoveries -2.73 USD
Expenses:Financial:Fees 0.49 USD ;; Recovery fees
Assets:US:LendingClub:Cash
计算收益率只需为此项投资配置如下参数:
investment {
currency: "LENCLUB"
asset_account: "Assets:US:LendingClub:FundsLent"
match_accounts: "Income:US:LendingClub:Recoveries"
cash_accounts: "Assets:US:LendingClub:Cash"
}
请注意,必须配置匹配账户,以便捕捉后续仅包含回收款(未记入资产账户)的交易。值得一提的是,我在这项投资中实现了 6.75%的收益率。唉。
房地产
您可以从与房屋相关的所有交易中提取现金流,从而计算出投入房屋的所有资金所产生的收益,就像这些资金纯粹用于投资一样。
通常,购房是出于其他原因——为孩子提供稳定性、自主装修的自由、定期偿还本金带来的强制储蓄,以及常常只是"拥有一个家"的感觉——但在绝大多数情况下,房产拥有更多是一种成本中心,同样的资金若投入市场可能获得更好的回报(参见这本书,它对利弊有精彩剖析)。我个人更愿意做别的事,而不是修理马桶或担心冬天漏水的窗户,因此我又回到了租房状态。但我曾亲身体验过一次,这段经历非常有价值,既是一次学习机会,也让我体会了"拥有自己居所"的"喜悦"。通过这一过程,您可以计算出拥有自己的住房实际花费了多少,以及如果将同样的现金流投入市场,本可能获得多少收益。
这稍微复杂一些,因为:
-
您需要有纪律,将那些如果您租房就不会产生的支出,单独归入与您的房屋相关的账户中。
-
您需要考虑因居住在该房屋中而无需支付的等效租金收入。
-
如果您仍拥有该房屋,则需要模拟一笔可观的中介费和其他交易费用(通过插入一笔交易)。
-
大多数地区在出售房产时享有大幅税收减免(有时涵盖全部资本增值),因此这一点确实必须纳入考量。计算起来并不总是简单,尤其是如果您在持有期间曾将该房产出租,此时您可能只能扣除部分资本利得。
-
对某些人而言,能够轻松且低成本地搬迁(例如接受位于国家另一端的新工作)所带来的选择权具有巨大价值,而这种个人价值难以估算。
这将要求我进行更多的分类工作,最好将其作为独立项目记录,尽管使用相同的代码。我已经成功创建了一个简单配置,并从中提取出约 5%的数值,但我认为还需要更多工作才能宣告完成。敬请期待后续内容。
选项
我有时会通过持有长期虚值看跌期权来对冲投资组合的部分下行风险,并通过卖出短期虚值看跌和看涨期权来为此融资。本文中描述的股票方法同样适用于期权,即从现金中提取现金流。
主要区别在于:
-
货币名称。工具名称因每份合约而异——包含到期日和行权价——因此我不会将期权存储在包含工具名称的账户叶节点下。我仅使用通用的叶账户名称,例如“期权”或“对冲”,并在开仓或平仓时选择使用。
-
期权价格。期权价格不易通过编程方式获取,我不得不使用私有 API。更重要的是,由于 Theta 随时间递减、波动率变化以及接近行权价时的非线性特征,我必须在账本中保留较新的价格估算,才能准确计算未实现盈亏的回报率。我认为这种影响足够显著,因此最终我需要编写一段代码,在生成报告前自动更新价格。
未来工作
本节描述了我对回报计算代码的未来改进设想,我可能将实施这些改进,并说明相应的挑战。
Note: As I'm writing this in September 2020, I'm actively working on this code and probably will continue over the next few months' weekends. It's possible this document falls slightly out-of-date and that some of the proposals below become implemented. Refer to the source code for the latest.
随时间变化的相对规模
我可以轻松为报告添加的另一项有用数据是市场价值随时间变化的堆叠图,以及报告组内各项投资的相对占比。这有助于监控组内特定工具的增长情况,从而辅助再平衡决策。
与基准比较
管理自有投资组合成功与否的重要指标之一,是将其与一个定期再平衡的、由多元化股票和债券构成的简单投资组合进行比较。毕竟,如果我的回报率无法超越这一基准,在正常市场环境下,人们有理由认为我无需采用更复杂的资产配置。
假设我的资金获取渠道相同(确实如此,因为本项目关注的是来自收入的储蓄),我可以将我的现金流替换为对这一模拟投资组合的现金流,即使用相同的时机模拟购买其他资产。我需要排除股息支付,因为它们仅针对我所使用的特定工具;同时,需根据基准组合中同等规模的头寸生成模拟股息。
应该可以在不修改我的账本的情况下实现这一点。一个问题是我需要一组在历史现金流日期上的基准价格。就像我的脚本已经为老化价格点所做的那样,这些数据可以轻松地存储在一个文件中以便读取。
更复杂的是,Beancount 目前不支持每股票分红分配的数据库。这可以通过在 Price 指令上附加并遵循元数据来实现,而无需引入新语法,例如
2020-09-01 price LQD 136.16 USD
distribution: 0.295 USD
总体而言,实现这一点不会太困难。
包括未投资现金
我在管理自己的投资组合时面临的一个问题是,缺乏在时机合适时及时投资可用现金的纪律,尤其是在市场不确定、需要做出艰难决策的时候。为了反映这种未投资现金对收益造成的拖累,我应该加入一个“投资”,它纯粹反映随着时间推移的未投资现金总额。
由于该数值变化较大,可以通过每月采样一次可投资现金总额,并根据差值合成现金流来获得一个不错的近似值。如果将这一系列现金流纳入投资组合,它将通过稀释效应适当地降低整体收益。
税后价值
目前,我使用导出脚本将所有持仓导出到 Google Sheets 文档中,然后将其划分为长期与短期头寸,以及税前、税后、Roth 和应税账户。从这八个汇总数据中,我扣除预估税费,报告一个“估算税后净资产”及相应的税务负债。这只是一个粗略的估算。
然而,收益报告要详细得多,我可以模拟不仅在变现时,而且在每年年底(来自股息和卖出)的税务支付。我拥有每项持仓的全部成本基础日期,因此可以自动判断短期与长期持仓。
通货膨胀调整
收益图表和日历收益中报告的市场价值未考虑通货膨胀。在长期内,这可能对收益产生重要影响。
通过使用年度 CPI(或其他通胀估算值;你甚至可以根据账本中的支出自行设定)对收益进行折现,将非常有用,这样我们就能观察到实际增长曲线,而不仅仅是名义增长。
交易佣金
配置可以轻松改进,允许用户指定投资卖出时的预期佣金,无论是绝对金额还是百分比(%)。这将用于更真实地标记持仓的变现价值。对于金额较小或佣金较高的投资(例如通过普通经纪商交易,或房地产),这可能会产生显著影响。
风险估算与贝塔值
一个更高级的主题可能是根据投资组合的具体构成计算方差,以评估并报告某种风险指标,例如夏普比率。这需要足够数量的均匀分布的价格点。随时间变化的指标变化也很有趣,还可以计算当前投资组合对整体市场的敏感性(贝塔值)。
结论
我原本以为,从 Beancount 数据中生成一份清晰的投资回报图像是可行的,而当我完成这项工作后,我对所达到的细节程度和清晰度感到比预期更加满意。这远远超出了单纯绘制净资产随时间变化的图表——它实际上表现得非常好,我可以用它来公平地比较不同投资的表现。我希望至少有一些 Beancount 用户能够将它应用到自己的账本中,我也期待听到那些成功设置它的人的反馈。
或许最重要的是,我在查看自己投资组合回报结果时感到非常惊讶。我向来是那种被问及回报率时只会耸耸肩、随意猜个平平无奇的数字的人,比如“嗯,大概 6%左右吧,不太满意”。而这项工作的初衷,正是源于我无法准确回答这个问题。结果发现,在过去 15 年里,我的年均回报率接近 12%,而在过去 5 年更是达到了 14%。我尚未进行基准对比,这些数字当然需要与市场表现并列比较才能做出公正评估。但无论如何,通过这一过程,我重新获得了对未来的信心与希望,也希望它能为其他 Beancount 用户带来同样清晰、启发性的洞察。