投资组合的评价和可视化(上)——评价指标的计算
- 投资结果评价
-
-
- 本文示例数据下载
-
- 投资过程回顾
- 基于收益的投资组合评价
-
- 收益率、年化收益、每日收益率
- 月度历史收益率
- 基于风险度量的投资组合评价
-
- Volatility波动率
- Max Drawdown 最大回撤MDD
- 综合考虑风险和收益的投资组合评价
-
- sharp夏普率
- Calmar卡尔玛比率
- 与基准投资组合相比较的评价指标
-
- beta: β\betaβ系数/贝塔系数
- alpha: α\alphaα系数/阿尔法系数
- 内容回顾、下篇预告
-
- 完整代码
投资结果评价
投资组合评价是量化投资工作中的重要一环。
如果我们要评价一个投资策略的好坏,有很多的标准可以选用。在这篇文章里,我们就来盘点几种常见的评价指标,我们不仅一一解释这些指标,总结它们的计算公式。还从一个实例出发,给出详细计算代码,逐步讲解、最终生成所有评价指标,并生成一张专业的可视化图表。这里涉及的评价指标以及最终效果如下图:
可视化图表的最终效果:(如果想直接跳到matplotlib图表可视化的内容,请点击这里访问下篇)
本文示例数据下载
本文使用的示例数据来自于一个“大小盘轮动交易策略”在过去十年中的模拟交易结果,关于这个交易策略的详细说明,请参考这篇文章。这个交易策略在沪深300指数和创业板指数之间轮动持有,以沪深300指数为基准收益,实现866%的总收益,26%的年化收益率,夏普率为0.999,最大回撤为20.4%。
在这篇文章里,我们就用这个策略的模拟交易结果来逐一讲解各个评价指标的计算方式以及上面图表的生成过程。原始的交易结果数据在这里下载
原始交易结果数据是一个DataFrame
,包含从2011年知道2020年共十年里每个交易日结束时持有的两种资产的份额,持有现金金额、持有资产和现金的总价值和业绩比较基准(沪深300指数)的价格:
000300.SH | 399006.SZ | cash | value | benchmark | |
---|---|---|---|---|---|
2011-01-04 | 0.000 | 0.000 | 100000.00 | 100000.00 | 3189.68 |
2011-01-05 | 0.000 | 86.554 | 0.00 | 100000.00 | 3175.66 |
2011-01-06 | 0.000 | 0.000 | 98210.63 | 98210.63 | 3159.64 |
2011-01-07 | 31.011 | 0.000 | 9.82 | 98210.63 | 3166.62 |
2011-01-10 | 31.011 | 0.000 | 9.82 | 96398.64 | 3108.19 |
… | … | … | … | … | … |
2020-12-25 | 0.000 | 323.592 | 6199.98 | 925461.34 | 5042.01 |
2020-12-28 | 0.000 | 323.592 | 6200.60 | 926112.38 | 5064.41 |
2020-12-29 | 0.000 | 323.592 | 6201.22 | 916466.71 | 5042.94 |
2020-12-30 | 0.000 | 323.592 | 6201.84 | 944794.61 | 5113.71 |
2020-12-31 | 0.000 | 323.592 | 6201.84 | 966061.10 | 5211.29 |
DataFrame中每一列的含义如下:
000300.SH
当天持有的沪深300指数(大盘股)的份额399006.SH
当天持有的创业板指(小盘股)的份额cash
当天持有的现金金额value
当天持有的现金和资产总价值benchmark
沪深300指数(业绩比较基准)的当天价格
用下面的代码可以从磁盘中读取DataFrame
并显示出来:
import pandas as pdlooped_value = pd.read_csv('example_data.csv', index_col=0)
looped_value.head()
Out: 000300.SH 399006.SZ cash value benchmark
2011-01-04 0.000000 0.000000 1.000000e+05 100000.000000 3189.68
2011-01-05 0.000000 86.553858 -1.455192e-11 100000.000000 3175.66
2011-01-06 0.000000 0.000000 9.821063e+04 98210.630631 3159.64
2011-01-07 31.011239 0.000000 9.821063e+00 98210.631613 3166.62
2011-01-10 31.011239 0.000000 9.822045e+00 96398.645884 3108.19
投资过程回顾
在正式开始投资组合的评价以前,我们需要对模拟交易的结果进行一次回顾,主要的目的是掌握一些最基本的信息,包括:
- 回测时间长度, 分别用年、月、天数表示,年的类型为
float
,月和日的类型都是int
- 投资组合交易历史:也就是说,检查投资组合中包含哪些资产,每种资产在模拟交易过程中的买入次数、卖出次数,以及总交易次数。由于针对不同的股票分别统计,因此操作次数并不是一个数字,而是一张表格
- 总投资额:整个投资历史的总投资额
熟悉Pandas
的同学应该对上面的过程不会陌生,我们逐步实现:
首先从回测历史数据中获取时间长度,由于模拟交易结果的index
本身就是交易日期,因此很容易通过日期对象计算年、月、日的数量。不过在计算之前需要把index的类型从str
转化为datetime
:
looped_value.index = pd.to_datetime(looped_value.index)
total_rounds = len(looped_value.index)
total_days = (looped_value.index[-1] - looped_value.index[0]).days
total_years = total_days / 365.
total_months = int(np.round(total_days / 30))
total_rounds, total_days, total_years, total_months
Out:
(2432, 3649, 9.997260273972604, 122)
从上面的结果可以看出,整个回测结果表包含了3649个交易日的结果,整整十年的回测结果,122个月。
接下来,分析完整的交易过程,为了用一个表格记录整个交易过程,我们需要生成一个新的DataFrame
,记录投资组合中每一个股票的买入次数和卖出次数,持有多头仓位和空头仓位的比例等等:
sell – 买入次数 | buy – 卖出次数 | total – 总交易次数 | long – 持有多头仓位的时间比例 | short – 持有空头仓位的时间比例 | empty – 空仓比例 | |
---|---|---|---|---|---|---|
share1 | 12 | 13 | 25 | 0.2 | 0.3 | 0.5 |
share2 | 5 | 6 | 11 | 0.1 | 0.2 | 0.7 |
share3 | 7 | 8 | 15 | 0.15 | 0.1 | 0.85 |
share4 | 2 | 1 | 3 | 0.3 | 0.1 | 0.6 |
# 投入的初始资金总额为第一个交易日的持有现金总额
total_invest = looped_value.iloc[0].cash
# 建立一个新的DataFrame,删除不需要的列
holding_stocks = looped_value.copy()
holding_stocks.drop(columns=['cash', 'value', 'benchmark'], inplace=True)
# 计算股票每一轮交易后的变化,增加者为买入,减少者为卖出
holding_movements = holding_stocks - holding_stocks.shift(1)
# 分别标记多仓/空仓,买入/卖出的位置,全部取sign()以便后续方便加总统计数量
holding_long = np.where(holding_stocks>0, np.sign(holding_stocks), 0)
holding_short = np.where(holding_stocks<0, np.sign(holding_stocks), 0)
holding_inc = np.where(holding_movements>0, np.sign(holding_movements), 0)
holding_dec = np.where(holding_movements<0, np.sign(holding_movements), 0)
# 统计数量
sell_counts = -holding_dec.sum(axis=0)
buy_counts = holding_inc.sum(axis=0)
long_percent = holding_long.sum(axis=0) / total_rounds
short_percent = -holding_short.sum(axis=0) / total_roundsop_counts = pd.DataFrame(sell_counts, index=holding_stocks.columns, columns=['sell'])
op_counts['buy'] = buy_counts
op_counts['total'] = op_counts.buy + op_counts.sell
op_counts['long'] = long_percent
op_counts['short'] = short_percent
op_counts['empty'] = 1 - op_counts.long - op_counts.short
# 查看交易过程汇总
op_counts
Out: sell buy total long short empty
000300.SH 84.0 84.0 168.0 0.259457 -0.0 0.740543
399006.SZ 83.0 84.0 167.0 0.411595 -0.0 0.588405
>>> total_invest
Out[36]: 100000.0
从交易过程汇总中我们可以对整个投资组合在十年中的交易历史有一个概况的了解:
- 总投入资金为10万元(在投资的第一天)
- 整个投资组合包含000300.SH(沪深300指数)和399006.SH(创业板指数)两个指数
- 在十年里分别买入/卖出各大约84次
- 大约26%的时间持有沪深300指数
- 大约41%的时间持有创业板指数
基于收益的投资组合评价
获得了关于模拟交易过程的相关信息以后,我们就可以开始对投资组合的模拟交易结果进行评价了。自然,我们投资的目的是为了获取收益,那么收益率肯定是我们最关心的目标,因此,我们首先需要计算的都是跟收益相关的评价指标:
收益率、年化收益、每日收益率
我们可以直接在looped_value
中补充完整的收益率和年化收益率数据,在looped_value
中添加以下数据列:
invest
: 每个交易日累计投入资金总额rtn
: 计算investment return投资回报率,也就是资产总额和投资总额的比率annual_rtn
: 年化投资收益率,每个交易日累计投资收益率的年化收益pct_change
: 每日收益率,也就是今天资产相对于昨天的变化比例
首先我们滚动计算回测收益的年化收益率和总收益率,注意收益率和每日收益率是不一样的,rtn收益率是当天的总资产相对于投资总额的收益率,而每日收益率是今天的总资产相对于昨天总资产的收益率。
例如,2017年9月4日当天的总资产约为45万8千元,相对于投入的十万元,收益率是358%,而前一交易日的总资产约为45万4千元,因此当天的每日收益率约为0.9%
计算代码如下:
looped_value['invest'] = 100000
looped_value['rtn'] = looped_value.value / looped_value['invest'] - 1
ys = (looped_value.index - looped_value.index[0]).days / 365.
looped_value['annual_rtn'] = (looped_value.rtn + 1) ** (1 / ys) - 1
looped_value['pct_change'] = looped_value.value / looped_value.value.shift(1) - 1
looped_value.tail()
Out: 000300.SH 399006.SZ cash value benchmark invest rtn annual_rtn pct_change
2020-12-25 0.0 323.592425 6199.980340 925461.340068 5042.01 100000 8.254613 0.249745 0.007061
2020-12-28 0.0 323.592425 6200.600338 926112.380839 5064.41 100000 8.261124 0.249604 0.000703
2020-12-29 0.0 323.592425 6201.220398 916466.710723 5042.94 100000 8.164667 0.248219 -0.010415
2020-12-30 0.0 323.592425 6201.840520 944794.611692 5113.71 100000 8.447946 0.251951 0.030910
2020-12-31 0.0 323.592425 6201.840520 966061.105835 5211.29 100000 8.660611 0.254664 0.022509
现在,我们在looped_value
中增加了几列,分别记录十年间每天的总收益率、年化收益率以及当日变化率,打印十年记录的最后几列,可以看到十年的总收益率是866%,年化收益率是25%。
从收益率的角度来说,这个结果还是相当不错的!
月度历史收益率
现在我们知道了这个投资组合在十年的投资期间内收益很不错,但是,如果具体到十年里的每一年、甚至每一个月,它的收益如何呢?收益是否稳定?为了对整个投资期间的收益率有一个概况地了解,我们还可以分别计算十年里每一年的收益率,每一个月的收益率,自然,我们可以用一个DataFrame来分别存储每年和每月的收益率:
first_year = looped_value.index[0].year
last_year = looped_value.index[-1].year
starts = pd.date_range(start=str(first_year - 1) + '1231',end=str(last_year) + '1130',freq='M') + pd.Timedelta(1, 'd')
ends = pd.date_range(start=str(first_year) + '0101',end=str(last_year) + '1231',freq='M')
# 计算每个月的收益率
monthly_returns = list()
for start, end in zip(starts, ends):val = looped_value['value'].loc[start:end]if len(val) > 0:monthly_returns.append(val.iloc[-1] / val.iloc[0] - 1)else:monthly_returns.append(np.nan)
year_count = len(monthly_returns) // 12
monthly_returns = np.array(monthly_returns).reshape(year_count, 12)
monthly_return_df = pd.DataFrame(monthly_returns,columns=['Jan', 'Feb', 'Mar', 'Apr','May', 'Jun', 'Jul', 'Aug','Sep', 'Oct', 'Nov', 'Dec'],index=range(first_year, last_year + 1))
# 计算每年的收益率
starts = pd.date_range(start=str(first_year - 1) + '1231',end=str(last_year) + '1130',freq='Y') + pd.Timedelta(1, 'd')
ends = pd.date_range(start=str(first_year) + '0101',end=str(last_year) + '1231',freq='Y')
# 组装出月度、年度收益率矩阵
yearly_returns = list()
for start, end in zip(starts, ends):val = looped_value['value'].loc[start:end]if len(val) > 0:yearly_returns.append(val.iloc[-1] / val.iloc[0] - 1)else:yearly_returns.append(np.nan)
monthly_return_df['y-cum'] = yearly_returnsmonthly_return_df
Out[50]: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec y-cum
2011 -0.029565 0.004999 -0.082161 -0.027755 0.000771 0.017896 0.036120 -0.089740 0.009318 0.001401 -0.029218 0.002102 -0.134492
2012 0.029766 0.043612 0.010853 0.008814 -0.004902 -0.036098 -0.031729 -0.039526 -0.020958 -0.022452 -0.013789 0.085873 0.053817
2013 0.077273 0.033820 -0.020762 0.002204 0.173830 -0.024849 0.026676 -0.074971 0.030109 -0.077782 0.048875 -0.054761 0.154530
2014 0.121668 -0.063620 -0.009860 -0.002996 0.009583 0.064157 0.048142 -0.015106 0.060307 -0.028095 0.117326 0.252034 0.751125
2015 -0.060772 0.135744 0.174663 0.064227 0.216783 -0.143777 0.045193 0.048436 -0.019001 0.131076 0.098636 0.002394 1.135973
2016 -0.028169 -0.009662 0.079805 -0.040826 0.007602 0.013114 0.026142 -0.011543 -0.046850 -0.007132 0.053011 -0.043511 -0.036378
2017 0.015494 0.034207 -0.013918 -0.017658 0.018582 0.047976 0.023719 0.007475 0.003254 0.027097 0.002371 -0.010655 0.184567
2018 0.043260 -0.044899 0.057105 -0.040944 -0.041885 0.021316 -0.028817 -0.048421 0.049822 0.002025 -0.014282 -0.028081 -0.114206
2019 0.006190 0.153495 0.079587 -0.026437 0.001902 0.033116 -0.009870 0.008138 -0.002944 0.011165 0.009196 0.041282 0.456783
2020 0.051435 0.152352 -0.081691 0.056878 -0.011090 0.128821 0.164158 -0.031281 -0.041442 -0.057777 0.039970 0.031110 0.548550
从上面的代码运行结果可以看出,我们生成了一个DataFrame
,表格中每一行存储一整年的月度收益率数据、还有当年的年度收益率数据。这张表给了我们非常好的“上帝视角”收益率概况。
为了更直观地了解收益率的稳定性,将数据复制到Excel中,可以生成一张热力图(如下图),不过,在本文的最后,我们同样会用matplotlib
生成一张热力图,可以更清晰地看到收益率的变化情况。
至此,我们已经完成了投资结果评价的最基础部分,计算出了投资组合的收益率,年化收益率、以及历年、历月的当年、当月收益率。
基于风险度量的投资组合评价
在文章的前面部分,我们都是以收益率为核心来评价投资组合的,但是,在实践中我们不仅仅需要关注收益率,更需要关心风险,理想状况下,我们希望得到风险较低、收益较高的投资组合。因此,我们还必须设法度量投资组合的风险。
因此,我们会介绍几个评价指标,用于评估投资组合的风险水平。
Volatility波动率
波动率是评价投资组合风险水平的一个主要指标。直观来讲,一个投资组合的净值上下波动越厉害,我们就说它的风险越高。例如:银行存款的收益率(利息)几乎是固定不变的,因此它的风险最小,接近于0,或被称为无风险收益,相对来说,债券的收益率就不太稳定,时高时低,但是波动并不大,而股票的波动更加厉害,因此相对于银行存款和债券,股票的风险最大。
波动率就是投资组合收益的标准差,大家知道,一组数据偏离其均值的程度越大,标准差也越大,因此波动率(标准差)就可以用来衡量投资组合的风险。它的定义如下:
波动率度量投资组合的风险程度,波动率越大,表示投资组合的风险越大。通常定义波动率为策略每日收益的年化标准差。
Va=σa250∗250{V_a} = \sigma_{a250} * \sqrt{250}Va=σa250∗250其中:
VaV_aVa:投资组合的波动率
σa\sigma_aσa:投资组合250日滚动收益率的标准差
波动率的计算:
ret = (looped_value['value'] / looped_value['value'].shift(1)) - 1
volatility = ret.rolling(250).std() * np.sqrt(250)
looped_value['volatility'] = volatility
looped_value.volatility.tail()
Out:
2020-12-25 0.259188
2020-12-28 0.258994
2020-12-29 0.259192
2020-12-30 0.260808
2020-12-31 0.261328
Name: volatility, dtype: float64
>>> volatility.mean()
Out:
0.2117668212247773
就此,我们算出了投资组合的年化波动率,不过,这个指标只有在跟其他投资组合进行比较的时候更有用。
Max Drawdown 最大回撤MDD
没有任何一个投资组合是只涨不跌的,如果有,那么它的收益率一定跟银行存款利息差不多。既然投资组合一定会在短期内下跌,那么我们就必须关注最大回撤,因为它让我们对最大可能的损失有所准备。
最大回撤描述的是投资组合在一个确定的时间段里,投资者可能蒙受的最大损失。它表示这个投资组合在一段时间里由一个前期高点下跌到最低点时净值的损失率。
MDD=(1−VminVmax)∗100%MDD = (1-\frac{V_{min}} {V_{max}} )* 100\%MDD=(1−VmaxVmin)∗100%其中:
MDDMDDMDD:回测区间的最大回撤
VminV_{min}Vmin:回测区间内最大的一次持续下跌的最低点
VmaxV_{max}Vmax:回测区间内最大的一次持续下跌前的最高点
在实践中,我们除了关注最大回撤的深度(比率)之外,还需要关注最大回撤的恢复时间,例如,一个投资组合的最大回撤尽管只有10%,但是三年后才涨回来,那么这个投资组合的效果也是需要怀疑的。
我们可以用一个underwater图来记录历史曲线中的回撤全景,通过比较每天总资产额与前期最高点的差值来记录每天的回撤比例,然后通过遍历整个历史数据每天的回撤比例,来找到最大回撤比例以及回撤的开始、低谷以及恢复日期。
在这里,我们遍历整个历史记录,可以记录历史上所有的回撤,并把它们记录在一个DataFrame
中,通过排序筛选出最大的五次回撤,这样对整个历史交易过程的回撤有更深刻的理解:
# cummax记录了整个历史区间上所有的前期高点,这样可以计算每日的回撤比例“underwater"
cummax = looped_value['value'].cummax()
looped_value['underwater'] = (looped_value['value'] - cummax) / cummax
drawdown_sign = np.sign(looped_value.underwater)
diff = drawdown_sign - drawdown_sign.shift(1)
drawdown_starts = np.where(diff == -1)[0]
drawdown_ends = np.where(diff == 1)[0]
drawdown_count = min(len(drawdown_starts), len(drawdown_ends))
all_drawdowns = []
for i_start, i_end in zip(drawdown_starts[:drawdown_count], drawdown_ends[:drawdown_count]):dd_start = looped_value.index[i_start - 1]dd_end = looped_value.index[i_end]dd_min = looped_value['underwater'].iloc[i_start:i_end].idxmin()dd = looped_value['underwater'].loc[dd_min]all_drawdowns.append((dd_start, dd_min, dd_end, dd))
if len(drawdown_starts) > drawdown_count:dd_start = looped_value.index[drawdown_starts[-1] - 1]dd_end = np.nandd_min = looped_value['underwater'].iloc[drawdown_starts[-1]:].idxmin()dd = looped_value['underwater'].loc[dd_min]all_drawdowns.append((dd_start, dd_min, dd_end, dd))
# 生成包含所有回撤的DataFrame
dd_df = pd.DataFrame(all_drawdowns, columns=['peak_date', 'valley_date', 'recover_date', 'drawdown'])
dd_df.sort_values(by='drawdown', inplace=True)
dd_df.head()
Out[60]: peak_date valley_date recover_date drawdown
14 2013-08-06 2013-12-17 2014-09-04 -0.204100
35 2015-06-03 2015-06-19 2015-10-16 -0.200785
52 2018-01-24 2018-11-26 2019-03-04 -0.190837
0 2011-01-05 2012-10-29 2013-02-20 -0.171134
42 2015-11-25 2016-06-13 2017-08-01 -0.149738
OK, 从上面的DataFrame
中可以看出,整个十年里最深的回撤是20.4%,从2013年8月持续到2014年9月,其次依次为20%、19%、17%。。。因此我们知道,在过去十年里,这个投资组合或投资策略产生了最大20%左右的回撤,最长一年左右能够反弹并超出前期高点。
综合考虑风险和收益的投资组合评价
到目前为止,我们介绍的评价指标要么只关注投资组合的收益,要么只关注投资组合的风险。如果我们希望了解投资组合的综合表现:既要关注风险,也要关注收益,应该使用什么指标呢?实际上,我们有许多常用的综合性指标正好解决这个问题。
sharp夏普率
夏普率是最常用的综合评价指标之一,它的定义和计算公式如下:
夏普率度量了收益与风险的比值,它代表的含义是:一个投资组合每承担一份风险,获取的收益率多大。
SharpRatio=Ra−RiVaSharp Ratio = \frac{R_a-R_i} {V_a}SharpRatio=VaRa−Ri其中:
RaR_aRa:投资组合年化收益率
RiR_iRi:无风险投资利率
VaV_aVa:投资组合波动率
正如夏普率的定义所说,它关注的并不仅仅是收益率的绝对值,也不仅仅关注风险大小,而是度量每承担一份风险,获取的相对收益,因此,对于两个投资组合来说,例如A和B,其中A承担一份风险,能获取2份收益,而B承担3份风险,获取4份收益。从绝对收益的角度来说,A的收益小于B,看起来B较好,然而,从综合收益的角度来说,A每承担一份风险可以获得两份收益,而B每承担一份风险只能获取1.33份收益,这样看起来A较好。
sharp率的计算公式如下,我们假设无风险投资利率为3.5%,即0.035:
loop_len = len(looped_value)
# 计算年化收益,如果回测期间大于一年,直接计算滚动年收益率(250天)
ret = looped_value['value'] / looped_value['value'].shift(1) - 1
roll_yearly_return = ret.rolling(250).mean() * 250
looped_value['sharp'] = (roll_yearly_return - 0.035) / looped_value['volatility']
looped_value.sharp.mean()
Out:
0.947206045513695>>> looped_value.sharp
Out[65]:
2011-01-04 NaN
2011-01-05 NaN
2011-01-06 NaN
2011-01-07 NaN
2011-01-10 NaN...
2020-12-25 1.629035
2020-12-28 1.587170
2020-12-29 1.565318
2020-12-30 1.678330
2020-12-31 1.800827
Name: sharp, Length: 2432, dtype: float64
Calmar卡尔玛比率
卡尔玛比率与夏普率类似,也是以收益和风险的比例的方式揭示投资组合的性能,其定义如下:
卡尔玛比率是年化收益率与区间最大回撤的比率,它代表的含义是:一个投资组合每承担一份最大回撤,获取的收益有多大。
CalmarRatio=RaMDDCalmar Ratio = \frac{R_{a}} {MDD}CalmarRatio=MDDRa
Ra:平均年化收益率R_{a}: 平均年化收益率Ra:平均年化收益率
MDD:最大回撤比率MDD:最大回撤比率MDD:最大回撤比率
卡尔玛比率的计算如下:
value = looped_value['value']
cummax = value.cummax()
drawdown = (cummax - value) / cummax
ret = value / value.shift(250) - 1
looped_value['calmar'] = ret / drawdown.rolling(250).max()
looped_value['calmar'].mean()
Out: 2.0765861340287looped_value.calmar
Out[68]:
2011-01-04 NaN
2011-01-05 NaN
2011-01-06 NaN
2011-01-07 NaN
2011-01-10 NaN...
2020-12-25 3.678191
2020-12-28 3.560636
2020-12-29 3.503937
2020-12-30 3.839910
2020-12-31 4.199519
Name: calmar, Length: 2432, dtype: float64
与基准投资组合相比较的评价指标
前面我们介绍的评价指标只单纯考察投资组合本身的绩效表现,然而,在实际投资过程中,我们往往会把投资组合跟一个基准组合相比较,得到它的相对表现。这也就是通常所说的“是否跑赢大盘”。
例如,假设一个投资组合在某个时期获得了30%的收益率,这个表现好还是不好呢,这个结论往往与同期市场平均收益有关:假如同一时期市场整体收益率达40%,那么显然我们的投资组合没有跑赢大盘,然而,如果同期市场整体表现为跌10%,那么我们的投资组合就可以说是非常优异了。
鉴于此,我们还需要引入两个与基准投资组合相比较的评价指标,以判断我们的投资组合“是否跑赢了大盘”。
beta: β\betaβ系数/贝塔系数
贝塔系数(β\betaβ系数)是一个用来度量投资组合与市场或大盘基准之间相关性的指标。
贝塔系数的定义和计算公式如下:
贝塔系数。考察投资组合与基准投资组合之间的相关性,它度量了投资组合相对于基准组合的风险大小或波动大小。
βa=Cov(ra,rm)σm2\beta_a = \frac{Cov(r_a, r_m)} {\sigma^2_{m}}βa=σm2Cov(ra,rm)
其中:
Cov(ra,rm)Cov(r_a, r_m)Cov(ra,rm):投资组合与基准组合的收益率相关系数
σm2\sigma^2_{m}σm2:基准组合的收益率的方差
贝塔系数越大,表示该投资组合相对于基准组合波动越大(通常使用市场平均水平作为基准):
- 当β=1\beta=1β=1时,表示投资组合的波动等于市场平均水平
- 当β>1\beta>1β>1时,投资组合的波动大于市场平均水平
- 当0>β>10>\beta>10>β>1时,投资组合的波动小于市场平均水平
- 当β<0\beta<0β<0时,投资组合的波动与市场平均水平相反,也就是说,市场涨时,投资组合跌,反之亦然
# 获取基准组合组合的收益率(在这里为沪深300指数的价格)
ref = looped_value['benchmark']
ref_ret = (ref / ref.shift(1)) - 1
ret_dev = looped_value['pct_change'].rolling(250).var()
looped_value['beta'] = looped_value['pct_change'].rolling(250).cov(ref_ret) / ret_dev
looped_value['beta'].mean()
Out:
0.6287569257923367>>> looped_value.beta
Out[72]:
2011-01-04 NaN
2011-01-05 NaN
2011-01-06 NaN
2011-01-07 NaN
2011-01-10 NaN...
2020-12-25 0.661178
2020-12-28 0.660216
2020-12-29 0.659835
2020-12-30 0.657160
2020-12-31 0.659337
Name: beta, Length: 2432, dtype: float64
从上面的计算可以看出,投资组合的平均β\betaβ系数为0.6左右,表明投资组合的平均波动与市场正相关,但是波动比市场平均波动更小,因此风险更小。
alpha: α\alphaα系数/阿尔法系数
了解beta系数的概念之后,我们就可以顺理成章地了解alpha的概念了。阿尔法系数度量的是投资组合获取超额收益的能力,定义和计算公式如下:
阿尔法系数度量了投资组合获取超市场收益的能力,它用策略的超额收益率(策略收益减去无风险利率)减去策略的期望收益率(策略由于市场波动带来的“自然增长率”)之后得到。
αa=(Ra−Ri)−βa∗(Rm−Ri)\alpha_a = (R_a- R_i)- \beta_a * (R_m-R_i)αa=(Ra−Ri)−βa∗(Rm−Ri)其中:
RaR_aRa:投资组合与基准组合的收益率相关系数
RmR_mRm:基准组合的收益率的方差
RiR_iRi:无风险投资利率
βa\beta_aβa:投资组合的贝塔系数
在使用alpha系数评价投资组合时,我们实际上把投资组合的收益看作两部分,第一部分是“自然增长率”,也就是随着市场波动自然产生的收益。通俗来讲,就是市场涨了,我也跟着涨。前面我们介绍过beta系数,如果一个投资组合的beta系数为1,就是说市场涨多少,我也涨多少。很多人认为,这部分的收益是“自然”发生的,随市场波动,无法控制,
然而真正体现了投资管理人水平的,是所谓的“超市场收益”也就是收益的第二部分:alpha收益。这是投资组合产生的超过市场波动的收益,这正是投资组合管理人所追求的目标。因此alpha系数评价了投资组合超越市场平均水平获利的能力。
也就是俗话说的,风口上的猪飞起来不算什么,市场的水退去时能保住收益的才是真本事。
阿尔法系数的计算如下:
loop_len = len(looped_value)
# 计算年化收益,如果回测期间大于一年,直接计算250日滚动收益率
year_ret = looped_value.value / looped_value['value'].shift(250) - 1
bench = looped_value['benchmark']
bench_ret = (bench / bench.shift(250)) - 1
looped_value['alpha'] = (year_ret - 0.035) - looped_value['beta'] * (bench_ret - 0.035)
alpha = looped_value['alpha'].mean()
alpha
Out:
0.2773149669933309>>> looped_value.alpha
Out[77]:
2011-01-04 NaN
2011-01-05 NaN
2011-01-06 NaN
2011-01-07 NaN
2011-01-10 NaN...
2020-12-25 0.340100
2020-12-28 0.331030
2020-12-29 0.324656
2020-12-30 0.360654
2020-12-31 0.393568
Name: alpha, Length: 2432, dtype: float64
从上面可以看出,这个投资组合实现了平均27%的年度超市场收益率,这还是相当不错的水平。
内容回顾、下篇预告
在本文中,我们通过一个投资组合实例,计算了这个投资组合的所有评价指标。投资组合的模拟交易结果存储在一个名为looped_value
的DataFrame
中,在此基础上计算出的评价指标如下:
- 收益率和年化收益率:整个模拟交易历史上的投资收益率、年化收益率和当日收益率存储在
looped_value
的rtn
,annual_rtn
,pct_change
三列中 - 月度收益率:投资组合在模拟交易历史上每个月的月度收益率存储在一个名为
montly_return_df
的DataFrame
中 - 波动率Volatility:这里我们以250个交易日为单位,滚动计算每一天的波动率,这个数字保存在
looped_value
的volatility
列中,同时,计算出十年间的滚动波动率的平均值:0.211 - 最大回撤Max Drawdwon:我们通过遍历十年间的资产总额,统计出所有的回撤情况,存储在一个名为
dd_df
的DataFrame
中,并按回撤深度从大到小排序,知道最大回撤位20%。 - 夏普率Sharp Ratio:反映投资组合每承担一份风险,可以获取多少超额收益,我们滚动计算了十年间每一天的250日夏普率,存储在
looped_value
的sharp
列中,并计算出十年间滚动夏普率的平均值:0.94,说明承担的风险稍大于获取的超额收益。 - 卡尔玛比率 Calmar Ratio:反映投资组合每承受一份回撤,能够获取多少超额收益,我们滚动计算了十年间每一天的滚动卡尔玛比率,存储在
looped_value
的sharp
列中,并计算出十年间滚动卡尔玛率的平均值:2.07,说明相对于收益来说,回撤幅度可以接受。 - 贝塔系数:体现投资组合随市场波动的情况,我们同样计算了滚动贝塔系数,存储在
beta
列中,同时计算出其平均值:0.62,说明投资组合的总体风险(波动率)小于市场平均水平。 - 阿尔法系数:揭示投资组合获取超越市场因素带来的超市场收益,滚动阿尔法系数存储在
looped_value
的alpha
列中,平均值为27%,这是超过市场因素的“内生”收益率。
我们的评价指标计算过程就到此为止了。不过,数字看起来枯燥且不直观,最好能用图表的形式来展示所有指标。matplotlib是用来进行数据可视化处理的最佳工具。在下一篇文章里,我们讲在现在的计算结果基础上,利用matplotlib的强大功能,绘制一张完整、全面、美观的可视化数据表。对数据可视化感兴趣的朋友请点击这里访问下一篇文章!
完整代码
下面是本文的完整代码:
import pandas as pdimport numpy as np# 读取数据并处理数据looped_value = pd.read_csv('example_data.csv', index_col=0)looped_value.index = pd.to_datetime(looped_value.index)total_rounds = len(looped_value.index)total_days = (looped_value.index[-1] - looped_value.index[0]).daystotal_years = total_days / 365.total_months = int(np.round(total_days / 30))total_invest = looped_value.iloc[0].cashfinal_value = looped_value.iloc[-1].value# 建立一个新的DataFrame,删除不需要的列holding_stocks = looped_value.copy()holding_stocks.drop(columns=['cash', 'value', 'benchmark'], inplace=True)# 计算股票每一轮交易后的变化,增加者为买入,减少者为卖出holding_movements = holding_stocks - holding_stocks.shift(1)# 分别标记多仓/空仓,买入/卖出的位置,全部取sign()以便后续方便加总统计数量holding_long = np.where(holding_stocks > 0, np.sign(holding_stocks), 0)holding_short = np.where(holding_stocks < 0, np.sign(holding_stocks), 0)holding_inc = np.where(holding_movements > 0, np.sign(holding_movements), 0)holding_dec = np.where(holding_movements < 0, np.sign(holding_movements), 0)# 统计数量sell_counts = -holding_dec.sum(axis=0)buy_counts = holding_inc.sum(axis=0)long_percent = holding_long.sum(axis=0) / total_roundsshort_percent = -holding_short.sum(axis=0) / total_roundsop_counts = pd.DataFrame(sell_counts, index=holding_stocks.columns, columns=['sell'])op_counts['buy'] = buy_countsop_counts['total'] = op_counts.buy + op_counts.sellop_counts['long'] = long_percentop_counts['short'] = short_percentop_counts['empty'] = 1 - op_counts.long - op_counts.short# 计算总收益率和年化收益率looped_value['invest'] = total_investlooped_value['rtn'] = looped_value.value / looped_value['invest'] - 1total_return = looped_value['rtn'].iloc[-1]ys = (looped_value.index - looped_value.index[0]).days / 365.looped_value['annual_rtn'] = (looped_value.rtn + 1) ** (1 / ys) - 1annual_return = looped_value['annual_rtn'].iloc[-1]looped_value['pct_change'] = looped_value.value / looped_value.value.shift(1) - 1ref_return = looped_value.benchmark.iloc[-1] / looped_value.benchmark.iloc[0]ref_annual_rtn = (ref_return + 1) ** (1 / ys[-1]) - 1# 计算月度历史收益率first_year = looped_value.index[0].yearlast_year = looped_value.index[-1].yearstarts = pd.date_range(start=str(first_year - 1) + '1231',end=str(last_year) + '1130',freq='M') + pd.Timedelta(1, 'd')ends = pd.date_range(start=str(first_year) + '0101',end=str(last_year) + '1231',freq='M')# 计算每个月的收益率monthly_returns = list()for start, end in zip(starts, ends):val = looped_value['value'].loc[start:end]if len(val) > 0:monthly_returns.append(val.iloc[-1] / val.iloc[0] - 1)else:monthly_returns.append(np.nan)year_count = len(monthly_returns) // 12monthly_returns = np.array(monthly_returns).reshape(year_count, 12)monthly_return_df = pd.DataFrame(monthly_returns,columns=['Jan', 'Feb', 'Mar', 'Apr','May', 'Jun', 'Jul', 'Aug','Sep', 'Oct', 'Nov', 'Dec'],index=range(first_year, last_year + 1))# 计算每年的收益率starts = pd.date_range(start=str(first_year - 1) + '1231',end=str(last_year) + '1130',freq='Y') + pd.Timedelta(1, 'd')ends = pd.date_range(start=str(first_year) + '0101',end=str(last_year) + '1231',freq='Y')# 组装出月度、年度收益率矩阵yearly_returns = list()for start, end in zip(starts, ends):val = looped_value['value'].loc[start:end]if len(val) > 0:yearly_returns.append(val.iloc[-1] / val.iloc[0] - 1)else:yearly_returns.append(np.nan)monthly_return_df['y-cum'] = yearly_returns# 计算Volatilityret = (looped_value['value'] / looped_value['value'].shift(1)) - 1volatility = ret.rolling(250).std() * np.sqrt(250)looped_value['volatility'] = volatilityavg_volatility = looped_value.volatility.mean()# 生成MDD DataFramecummax = looped_value['value'].cummax()looped_value['underwater'] = (looped_value['value'] - cummax) / cummaxdrawdown_sign = np.sign(looped_value.underwater)diff = drawdown_sign - drawdown_sign.shift(1)drawdown_starts = np.where(diff == -1)[0]drawdown_ends = np.where(diff == 1)[0]drawdown_count = min(len(drawdown_starts), len(drawdown_ends))all_drawdowns = []for i_start, i_end in zip(drawdown_starts[:drawdown_count], drawdown_ends[:drawdown_count]):dd_start = looped_value.index[i_start - 1]dd_end = looped_value.index[i_end]dd_min = looped_value['underwater'].iloc[i_start:i_end].idxmin()dd = looped_value['underwater'].loc[dd_min]all_drawdowns.append((dd_start, dd_min, dd_end, dd))if len(drawdown_starts) > drawdown_count:dd_start = looped_value.index[drawdown_starts[-1] - 1]dd_end = np.nandd_min = looped_value['underwater'].iloc[drawdown_starts[-1]:].idxmin()dd = looped_value['underwater'].loc[dd_min]all_drawdowns.append((dd_start, dd_min, dd_end, dd))# 生成包含所有回撤的DataFramedd_df = pd.DataFrame(all_drawdowns, columns=['peak_date', 'valley_date', 'recover_date', 'drawdown'])dd_df.sort_values(by='drawdown', inplace=True)mdd = dd_df.iloc[0].drawdownmdd_date = dd_df.iloc[0].valley_datemdd_peak = dd_df.iloc[0].peak_datemdd_recover = dd_df.iloc[0].recover_date# 计算sharp率loop_len = len(looped_value)# 计算年化收益,如果回测期间大于一年,直接计算滚动年收益率(250天)ret = looped_value['value'] / looped_value['value'].shift(1) - 1roll_yearly_return = ret.rolling(250).mean() * 250looped_value['sharp'] = (roll_yearly_return - 0.035) / looped_value['volatility']avg_sharp = looped_value.sharp.mean()# 计算卡尔玛比率value = looped_value['value']cummax = value.cummax()drawdown = (cummax - value) / cummaxret = value / value.shift(250) - 1looped_value['calmar'] = ret / drawdown.rolling(250).max()avg_calmar = looped_value['calmar'].mean()# 计算贝塔系数:# 获取基准组合组合的收益率(在这里为沪深300指数的价格)ref = looped_value['benchmark']ref_ret = (ref / ref.shift(1)) - 1ret_dev = looped_value['pct_change'].rolling(250).var()looped_value['beta'] = looped_value['pct_change'].rolling(250).cov(ref_ret) / ret_devavg_beta = looped_value['beta'].mean()# 计算alpha系数loop_len = len(looped_value)# 计算年化收益,如果回测期间大于一年,直接计算250日滚动收益率year_ret = looped_value.value / looped_value['value'].shift(250) - 1bench = looped_value['benchmark']bench_ret = (bench / bench.shift(250)) - 1looped_value['alpha'] = (year_ret - 0.035) - looped_value['beta'] * (bench_ret - 0.035)avg_alpha = looped_value['alpha'].mean()