16.投资组合收益计算
在投资的世界里,最常被问到的问题不是“你买了什么”,而是“你赚了多少?”。然而,对于大多数个人投资者来说,准确回答这个问题却异常困难。主流的券商平台提供的收益报告往往不尽如人意,它们通常只展示账户净值的变化,却忽略了你在不同时期投入或取出的真金白银。这就好比一家公司只展示期末的资产,却不考虑期间的融资和分红,得出的利润率必然是扭曲的。
作为一个严谨的投资者,我们希望得到的是基于真实现金流的、能够反映我们投资决策质量的精确回报率。好消息是,如果你一直使用 Beancount 记录你的每一笔投资交易,那么你已经拥有了计算这一切所需的最原始、最准确的数据。本章将带你深入探索如何利用 Beancount 的数据,通过提取现金流、计算内部收益率(IRR)等方法,来生成一份真正属于你自己的、精确的投资回报报告。
投资组合现金流提取
计算投资回报的核心,在于将复杂的交易记录简化为一条清晰的现金流(Cash Flow)时间线。一笔投资的回报,本质上就是投入的本金经过时间的增值,最终再以某种形式(市值或现金)回到你手中。因此,我们的首要任务就是从海量的交易记录中,识别并提取出与特定投资相关的每一笔资金流入和流出。
为什么现金流至关重要?
主流券商的计算方式通常存在以下缺陷:
- 忽略资金进出:它们常常用“当前市值”减去“初始投入”,得出一个看似不错的回报率。但如果你在年中追加了一大笔投资,这个计算方式就会严重高估你的实际收益。例如,年初投入 10 万,年中追加 2 万,年底市值变成 11.5 万。券商可能会告诉你赚了 1.5 万(15%),但实际上,考虑到你投入了 12 万,你是亏损的。
- 忽略股息和利息:很多报告只关注资产价格的涨跌,而忽略了股息、利息等现金回报。对于债券或高股息股票来说,这部分是收益的重要组成部分。
- 无法进行横向对比:你无法知道,如果当初把这笔钱投入到一个简单的指数基金里,收益会是怎样。
Beancount 的优势在于,它记录了所有细节。我们可以通过分析这些细节,还原出最真实的资金流动。
提取现金流的策略
我们的目标是为每一个“投资标的”(例如,某只股票、某只基金)建立一个独立的现金流序列。这里的“投资标的”不仅指代资产本身,还与它所在的账户有关,因为不同账户(如免税账户和应税账户)的资金进出性质不同。
提取现金流的关键在于识别交易中哪些部分代表了真实的资金投入或取出。我们可以通过分析交易中涉及的账户类型来实现这一点。
一个典型的 Beancount 投资交易可能包含多个账户。例如,用现金购买股票:
2023-01-15 * "购买 VTI"
Assets:Investment:Vanguard:VTI 10 VTI {100.00 USD}
Assets:Investment:Vanguard:Cash -1000.00 USD
在这个交易中,Assets:Investment:Vanguard:Cash 账户的减少(-1000.00 USD)代表了资金从你的“现金池”流入了“投资池”。对于这个投资组合来说,这是一笔现金流出(从投资组合的角度看是投入)。
再看一个收到股息的例子:
2023-03-20 * "VTI 股息"
Assets:Investment:Vanguard:Cash 50.00 USD
Income:Investment:Vanguard:VTI:Dividend -50.00 USD
这里,Assets:Investment:Vanguard:Cash 的增加(+50.00 USD)代表了投资产生的现金回报。对于投资组合来说,这是一笔现金流入。
因此,提取现金流的通用规则是:
- 定义投资账户:确定代表该投资的资产账户,例如
Assets:Investment:Vanguard:VTI。 - 定义关联账户:
- 现金账户:与该投资直接关联的现金账户,如
Assets:Investment:Vanguard:Cash。所有与此账户的交易都代表资金进出投资组合。 - 股息/收益账户:记录该投资产生的收益,如
Income:Investment:Vanguard:VTI:Dividend。这些账户的贷方发生额(负数)代表现金流入。 - 费用账户:记录交易佣金等,如
Expenses:Financial:Commissions。这些代表了投资组合的现金流出。
- 现金账户:与该投资直接关联的现金账户,如
- 遍历交易:扫描所有交易,找出那些至少有一个分录(posting)属于上述定义的账户的交易。
- 归类和汇总:对于每个找到的交易,根据分录的账户类型和金额,计算出该交易对投资组合产生的净现金流。
例如,对于一个复杂的交易:
2023-04-10 * "买入股票并支付佣金"
Assets:Investment:Broker:Stock 5 STOCK {200.00 USD}
Assets:Investment:Broker:Cash -1000.00 USD
Expenses:Financial:Commissions 5.00 USD
这个交易的净现金流是:-1000.00 (现金减少) + -5.00 (佣金支出) = -1005.00 USD。这代表了 1005 美元的资金投入。
通过这种方式,我们可以将所有与特定投资相关的交易,转化为一系列带时间戳的 (日期, 金额, 类型) 现金流记录。这个序列就是我们后续所有计算的基础。
现金流特征检测
在提取出现金流序列后,直接进行计算是危险的。因为我们的账本中可能存在各种各样的交易,有些交易产生的现金流模式需要特殊处理。因此,在计算之前,对现金流进行“特征检测”或“分类”是至关重要的一步。
交易模板与账户分类
为了系统化地处理各种交易,我们可以将交易中涉及的账户进行分类,然后根据这些分类组合来识别交易的“模板”(Signature)。
我们可以将账户分为以下几类:
- ASSET:投资资产账户本身,如
Assets:Investment:VTI。 - CASH:关联的现金账户,如
Assets:Investment:Cash。 - DIVIDEND:股息收入账户,如
Income:Investment:VTI:Dividend。 - EXPENSES:费用账户,如
Expenses:Financial:Commissions。 - INCOME:其他收入(如资本利得),如
Income:Investment:Capital-Gains。 - OTHER:其他不相关的账户。
通过识别一个交易中包含的账户分类,我们就能确定它的模板。例如:
- 模板:买入 (BUY)
- 特征:
CASH(负数) +ASSET(正数) + 可能有的EXPENSES(正数)。 - 现金流:
CASH的金额(负数)和EXPENSES的金额(正数)共同构成了投资的现金流出。
- 特征:
- 模板:卖出 (SELL)
- 特征:
CASH(正数) +ASSET(负数) + 可能有的EXPENSES(正数) + 可能有的INCOME(负数,资本利得)。 - 现金流:
CASH的金额(正数)减去EXPENSES的金额,构成了投资的现金流入。
- 特征:
- 模板:现金股息 (CASH DIVIDEND)
- 特征:
CASH(正数) +DIVIDEND(负数)。 - 现金流:
CASH的金额(正数)是投资的现金流入。
- 特征:
- 模板:股息再投资 (REINVESTED DIVIDEND)
- 特征:
ASSET(正数) +DIVIDEND(负数)。 - 现金流:无。这是一个关键点!虽然你的资产增加了,但没有发生任何现金的进出。这种交易不产生现金流,因此在计算回报时应该被忽略。如果错误地将其计入,会导致回报率计算失真。
- 特征:
简化的通用规则
经过实践,一个更简单的通用规则往往就足够了:只关注现金账户(CASH)和资产账户(ASSET)的变动。
- 任何导致现金账户余额增加的交易分录,都视为一笔现金流入(+CF)。
- 任何导致现金账户余额减少的交易分录,都视为一笔现金流出(-CF)。
- 对于股息再投资这类不涉及现金账户的交易,它们不会被这个规则捕捉到,这恰好是正确的行为。
这个通用规则极大地简化了现金流的提取过程,你不再需要为每一种复杂的交易组合编写特定的处理逻辑。你只需要定义好哪些是你的“现金账户”,然后让程序去筛选和汇总即可。
最终,对于每个投资标的,你将得到一个清晰的、按时间排序的现金流列表,例如:
- 2022-01-15: -10000.00 USD (初始投入)
- 2022-03-20: +250.00 USD (收到股息)
- 2022-06-10: -5000.00 USD (追加投资)
- ...
- 2023-12-31: +15000.00 USD (期末市值,作为最后一笔负现金流的对冲)
这个列表就是我们计算回报率的“原材料”。
投资账户分类
在上一节中,我们提到了为投资定义“现金账户”、“股息账户”等。这个过程,即“投资账户分类”,是配置回报计算工具的核心步骤。一个清晰、规范的账户命名体系会让这一步变得非常简单。
账户命名约定的重要性
虽然 Beancount 对账户命名没有强制要求,但遵循一个良好的约定可以极大地提高自动化处理的效率。对于投资,推荐的命名结构是:
Assets:投资类型:机构:标的
例如:
Assets:Investment:Vanguard:VTIAssets:Retirement:401k:BondFundAssets:Taxable:Fidelity:GOOG
这种结构化命名的好处是:
- 易于识别:一眼就能看出账户的性质(投资、退休、应税)、所在的机构(Vanguard、Fidelity)以及持有的标的(VTI、GOOG)。
- 便于分组:可以轻松地使用通配符(globbing)来选择一组账户进行分析。例如,
Assets:Investment:Vanguard:*可以选择 VTI、VTSAX 等所有在 Vanguard 投资账户中的资产。 - 自动化友好:程序可以根据账户名称的叶子节点(例如
VTI)自动推断出投资标的,并将其与同名的收入账户(如Income:Investment:Vanguard:VTI:Dividend)关联起来。
配置文件的结构
为了告诉回报计算工具如何处理你的投资,你需要创建一个配置文件。这个文件通常采用易于阅读的格式(如 YAML 或 Protobuf Text Format),其核心内容是定义“投资”和“报告组”。
一个“投资”(Investment)的定义通常包含:
- 资产账户 (asset_account):持有该投资的主账户。
- 现金账户 (cash_accounts):一个或多个与此投资相关的现金账户。所有进出这些账户的资金都被视为该投资的现金流。
- 股息账户 (dividend_accounts):可选,用于明确指定股息收入账户,以确保能捕捉到所有股息交易。
- 匹配账户 (match_accounts):可选,用于捕捉那些不直接涉及资产账户或现金账户,但又与该投资相关的交易(例如,某些复杂的公司行为)。
一个“报告组”(Group)的定义则更简单:
- 名称 (name):给这个组起个名字,如 "Tech_Stocks" 或 "All_Portfolio"。
- 投资列表 (investments):包含在这个组里的投资列表。可以使用通配符。
示例配置 (YAML 格式):
investments:
- name: "VTI_in_Vanguard"
asset_account: "Assets:Investment:Vanguard:VTI"
cash_accounts:
- "Assets:Investment:Vanguard:Cash"
dividend_accounts:
- "Income:Investment:Vanguard:VTI:Dividend"
- name: "GOOG_in_Fidelity"
asset_account: "Assets:Taxable:Fidelity:GOOG"
cash_accounts:
- "Assets:Taxable:Fidelity:Cash"
dividend_accounts:
- "Income:Taxable:Fidelity:GOOG:Dividend"
groups:
- name: "US_Stocks"
investments:
- "VTI_in_Vanguard"
- "GOOG_in_Fidelity"
- name: "All_Investments"
investments:
- "Assets:Investment:*" # 使用通配符选择所有投资账户
有了这个配置文件,回报计算工具就能自动地:
- 找到所有定义的投资。
- 为每个投资提取现金流。
- 将不同投资的现金流按组合并。
- 进行回报计算。
对于大型账本,可以编写一个辅助脚本(如 configure.py)来扫描你的 Beancount 文件,并根据你的账户结构自动生成这个配置文件的草稿,然后你再进行微调。
内部收益率 (IRR) 计算
有了干净的现金流数据,我们就可以开始计算核心指标了:内部收益率(Internal Rate of Return, IRR)。IRR 是一个用来衡量投资盈利能力的指标,它指的是使投资净现值(NPV)等于零的折现率。通俗地讲,IRR 就是你的资金在考虑了时间价值后,实现的年化复合增长率。
IRR 的数学原理
IRR 的计算基于一个等式:
$$ \sum_{i=0}^{n} \frac{CF_i}{(1 + IRR)^{t_i}} = 0 $$
其中:
- $CF_i$ 是第 $i$ 笔现金流(正数代表流入,负数代表流出)。
- $t_i$ 是从今天算起,第 $i$ 笔现金流发生的时间(通常以年为单位)。
- $IRR$ 是我们要求解的未知数。
这个等式的意义是:将未来每一笔现金流都折算回今天的价值(现值),然后把它们全部加起来,总和应该为零。因为从整个投资周期来看,你投入的钱(负现金流)和收回的钱(正现金流)在考虑了时间成本后,应该是盈亏平衡的。
如何计算 IRR
在实践中,我们不需要手动解这个复杂的方程。Python 的 scipy 库提供了强大的数值求解器,可以轻松计算 IRR。
计算步骤如下:
- 准备现金流序列:获取一个投资或投资组合的完整现金流列表。
- 确定时间点:将每笔现金流的日期转换为相对于“今天”(通常是最后一笔现金流的日期,即当前日期)的时间偏移量(以年为单位)。例如,一年前的现金流,$t_i = -1$。
- 使用求解器:调用
scipy.optimize.fsolve或numpy.irr(如果可用) 等函数,传入现金流和时间点,求解 IRR。
代码示例 (使用 scipy.optimize.fsolve):
import numpy as np
from scipy.optimize import fsolve
def calculate_irr(cash_flows, dates):
"""
计算内部收益率 (IRR)
Args:
cash_flows (list of float): 现金流列表,正数为流入,负数为流出。
dates (list of datetime.date): 现金流对应的日期。
Returns:
float: IRR 值 (例如 0.12 代表 12%)。
"""
# 确保至少有一笔正现金流和一笔负现金流
if all(cf > 0 for cf in cash_flows) or all(cf < 0 for cf in cash_flows):
return None # 无法计算
# 将日期转换为相对于最后一天的年数
end_date = max(dates)
times = [(date - end_date).days / 365.0 for date in dates]
# 定义 NPV 函数,我们要找它的根
def npv(rate):
return sum(cf / (1 + rate) ** t for cf, t in zip(cash_flows, times))
# 使用 fsolve 求解,初始猜测值为 0.1 (10%)
try:
irr, = fsolve(npv, 0.1)
return irr
except:
return None # 求解失败
# 示例数据
# 2022-01-01: 投入 10000
# 2023-01-01: 追加 5000
# 2024-01-01: 账户价值 18000 (作为最后一笔正现金流)
import datetime
cash_flows = [-10000, -5000, 18000]
dates = [datetime.date(2022, 1, 1), datetime.date(2023, 1, 1), datetime.date(2024, 1, 1)]
irr_value = calculate_irr(cash_flows, dates)
if irr_value is not None:
print(f"投资的年化内部收益率 (IRR) 为: {irr_value:.2%}")
else:
print("无法计算 IRR,请检查现金流数据。")
处理未实现的收益
一个关键点是,对于仍在持有的投资,我们不能简单地忽略它。为了计算到“今天”为止的回报,我们需要在现金流序列的末尾,添加一笔代表当前市值的正现金流。这相当于模拟你在今天卖掉了所有持仓,将资金收回。这笔“虚拟的卖出”使得整个投资周期的净现金流为零,从而可以计算出截至当前的真实回报。
价格插值与回报计算
在计算回报时,尤其是在计算分段回报(如年度回报、季度回报)或处理多币种投资时,价格数据的准确性和完整性至关重要。
价格数据的重要性
Beancount 本身不主动获取价格,它依赖于你在 prices 文件或交易中手动记录的价格。为了计算投资在某个时间点(例如,年初、月末)的市值,我们需要知道当时资产的价格。
如果账本中缺少某个日期的精确价格,Beancount 的价格查询功能会返回最后已知的价格。如果这个价格已经很旧了,就会导致市值估算严重偏差,进而影响回报率的准确性。
价格插值与补充
为了解决这个问题,我们需要一个机制来确保在需要计算回报的时间点,都有可用的价格数据。这个过程可以分为两步:
- 识别缺失的价格:在计算回报之前,先遍历所有需要计算市值的日期(例如,每个季度末、每年末),检查账本中是否存在对应日期的价格。如果没有,就记录下来。
- 补充价格数据:将所有缺失的价格点导出到一个临时文件中。然后,可以使用外部工具(如
download_prices.py)从 Yahoo Finance 等数据源自动下载这些历史价格,并将结果格式化为 Beancount 的price指令,供你手动或自动合并回账本。
对于某些无法自动获取价格的资产(如非上市基金、私募股权),则需要手动维护价格记录。
多币种回报计算
如果你的投资组合包含不同币种的资产(例如,同时持有美元计价的 VTI 和加元计价的 TD 股票),计算总回报时就需要进行币种转换。
处理多币种的策略如下:
- 统一币种:在计算一个投资组合的总回报时,需要将所有现金流和最终市值都转换为一个共同的基准货币(例如,你的本币 USD)。
- 使用 Beancount 的转换功能:Beancount 的
Amount对象和Inventory对象内置了币种转换功能。只要你的账本中记录了足够的price指令(例如,USD/CAD 的历史汇率),它就能自动完成转换。 - 计算:将所有现金流转换为基准货币后,就可以像处理单币种投资一样计算 IRR 了。
例如,一个加元投资的现金流是 -10000 CAD 和 +12000 CAD。如果要计算美元回报,你需要在现金流发生时,根据当时的汇率将其转换为美元,然后再进行 IRR 计算。
多币种回报计算
在上一节中,我们简要提到了多币种问题。这里我们深入探讨其细节和最佳实践。处理多币种投资回报的核心挑战在于,汇率的波动会同时影响资产的市值和现金流的价值。
汇率风险与真实回报
假设你是一个美国投资者,用美元投资了加拿大的股票。
- 场景一:加元兑美元升值。即使股票本身价格没变,你的美元回报也是正的。
- 场景二:加元兑美元贬值。即使股票价格上涨,你的美元回报也可能为负。
因此,从你的本币(假设是美元)视角计算回报,才能真实反映你的购买力增长情况。
实践中的多币种处理
- 现金流转换:如前所述,每笔现金流在发生时,都应该根据当时的汇率转换为基准货币。例如,2022年1月1日投入 10000 CAD,如果当时汇率是 1 CAD = 0.8 USD,那么这笔现金流在计算中应记为 -8000 USD。
- 市值转换:在计算投资组合的当前价值或在分段回报计算中需要期初/期末价值时,使用当前或对应日期的汇率将所有资产的本地货币价值转换为基准货币。
- 汇率数据:这再次强调了维护一个准确的
prices文件的重要性,其中不仅要有股票价格,还要有你需要的货币对的汇率历史数据(例如,2023-01-01 price CAD 0.75 USD)。
一个设计良好的回报计算工具应该能够自动处理这些转换,你只需要在配置文件中指定基准货币即可。
回报报告解读
计算出 IRR 和其他回报指标后,最后一步是理解这些数字的含义,并从中获得洞察。一份完整的回报报告通常包含多个维度。
报告的核心组成部分
- 总回报率 (Total Return):这是最重要的一个数字,即考虑了所有现金流(投入、取出、股息)和最终市值后计算出的 IRR。它告诉你,从整体上看,你的投资策略表现如何。
- 资本增值回报 (Price Return):只考虑资产价格变化的回报。计算方法是,将所有股息视为现金流出(即从投资中取出),然后计算 IRR。这反映了资产本身的增值能力。
- 股息回报 (Dividend Return):总回报与资本增值回报之间的差额。这代表了你的收益中有多少是来自现金分红。
- 分段回报 (Period Returns):
- 年度/季度回报:计算特定年份或季度内的回报。这有助于你了解投资表现的波动性。
- 累计回报:计算从某个起始点到现在的总回报,通常以图表形式展示,可以清晰地看到资产的增长曲线。
- 现金流图:可视化你的资金投入和取出的时间点和金额。这有助于你审视自己的投资纪律(例如,是否在市场高点追涨杀跌)。
- 与基准的比较:这是最有价值的分析之一。将你的投资回报与一个简单的基准(如标普500指数 ETF)进行比较。如果你的 IRR 高于基准,说明你的投资策略或择时带来了超额收益;反之,则说明你可能跑输了市场。
解读时的注意事项
- 年化效应:IRR 是一个年化指标。对于持有时间很短的投资,一个看似很高的 IRR 可能只是短期市场波动的结果,不代表长期的盈利能力。在解读时要结合投资的持有期限。
- 费用的影响:交易佣金、管理费等会直接拉低你的回报率。在计算时,务必将这些费用作为现金流出计入。
- 税前 vs. 税后:标准的 IRR 计算通常是税前的。对于应税账户,实际到手的回报会因资本利得税、股息税而降低。更高级的分析可以尝试模拟税收影响,计算税后回报,但这需要更复杂的模型。
- 未实现的损益:当前报告的回报是基于“当前市值”的。如果市场下跌,你的回报率会随之下降,直到你真正卖出锁定利润或亏损。
通过这份详尽的报告,你不再需要依赖券商模糊的图表,而是可以对自己的投资表现有一个清晰、量化、多维度的认识。这不仅能让你更有信心地坚持或调整你的投资策略,也是你作为自己财富的真正管理者,所应具备的核心能力。