最近看了不少关于基金定投策略的回测,不少文章里面的定投累计收益动辄百分之几百,年化收益高达百分之几十,真有这么牛?本文里我以跟踪指数的ETF基金来回测一下看看。因为很多文章都没有给出实际的计算过程,我只能自己撸代码了。
一、什么是ETF基金?
ETF(ExchangeTraded Fund)基金全称交易型开放式指数基金,是一种在交易所上市交易的、基金份额可变的一种开放式基金。投资者不仅可以通过基金公司或基金销售机构申购该类基金,还可以通过二级市场像交易股票一样交易该基金。ETF基金具有独特的实物申购和赎回机制以及较高的申购门槛,普通投资者基本不太可能直接在基金公司购买,本文主要关注的是场内交易。
ETF基金的可分为股票ETF、债券ETF、商品ETF和货币ETF几类。
1.股票ETF
股票ETF常见类型有宽基指数基金、行业或主题指数基金、海外指数基金和商品指数基金。
(1)宽基指数基金
宽基指数基金就是跟踪大盘指数的基金,例如跟踪上证50、沪深300、中证500、创业板指数的基金。这些大盘指数代表了市场平均收益,买入这些指数基金意味着获取市场平均收益。
上证50:510050.SH 华夏上证50ETF
沪深300:510300.SH 华泰柏瑞沪深300ETF、159919.SZ 嘉实沪深300ETF、510330.SH 华夏沪深300ETF
中证500:510500.SH 南方中证500ETF
创业板指:159915.SZ 易方达创业板ETF
创业板50:159949.SZ 华安创业板50ETF
(2)行业/主题指数基金
行业/主题基金指投资一些特定的策略、风格、行业或者主题的相关指数,因为可选范围比宽基要小,也称为窄基指数基金。比较常见的如医药、证券、银行、科技、互联网、军工、环保、传媒等等。
大消费:159928.SZ 汇添富中证主要消费ETF
医药:159929.SZ 汇添富中证医药卫生ETF
酒:512690.SH 鹏华中证酒ETF
科技互联网:515000.SH 华宝中证科技龙头ETF、510050.SH 华夏中证5G通信主题ETF、512760.SH 国泰CES半导体芯片ETF、512480.SH 国联安中证全指半导体ETF、515030.SH 华夏中证新能源汽车ETF、513050.SH 易方达中证海外互联ETF、164906.SZ 交银中证海外中国互联网
军工:512660.SH 国泰中证军工ETF
环保:512580.SH 广发中证环保产业ETF
传媒:159805.SZ 鹏华中证传媒ETF
银行:512800.SH 华宝中证银行ETF
证券:512880.SH 国泰中证全指证券公司ETF、512000.SH 华宝中证全指证券ETF、159993.SZ 鹏华国证证券龙头ETF
(3)海外指数基金
境内投资者想直接投资境外资本市场存在着比较多的障碍,海外指数基金给没有合适的投资渠道的普通投资者提供了投资海外股市的机会。
港股:159920.SZ 华夏恒生ETF、510900.SH 易方达恒生H股ETF
美股:513100.SH 国泰纳斯达克100ETF、513500.SH 博时标普500ETF
2.债券ETF
债券市场一般参与者都是机构,普通散户对于债券参与比较少。一般分为国债ETF、地债ETF、转债ETF、城投债ETF,这里就不详细介绍了。
3.商品指数基金
国内商品指数基金规模最大的还是黄金指数基金,其它还有豆粕EF、商品ETF、能源ETF等类型。
黄金:518880.SH 华安易富黄金ETF、159934.SZ 易方达黄金ETF
4.货币ETF
场内的货币基金一般分为两类,交易型货币基金和交易兼申赎型货币基金。
交易型货基金是511开头,规模较大的有:511990华宝添益、511880银华日利、511660建信添益、519888添富快线。
交易兼申赎型货基是159开头,目前规模都比较小。
东方财富数据显示,截至2022年7月,ETF基金数量已经超过700只,但是只有上证50ETF、沪深300ETF、中证500ETF、中国互联网ETF、证券ETF、科创50ETF等头部ETF最新规模均超过300亿元,规模超过100亿元的ETF有25只。另外,市场上有127只ETF基金的规模低于5000万元。大家在选择定投的时候还是要好好挑一挑,尽量选规模大流动性好的产品。
二、投资策略及回测
投资指数基金的方法常见的有定投和趋势交易。趋势交易其实跟股票的低买高卖一个意思,普通投资者很难把握好。普通的投资者投资指数基金最好还是以定投为主,简单易学。
本次采用的标的有跟踪大盘指数的沪深300ETF基金510300和跟踪创业板指数的创业板ETF基金159915。本文采用一种最傻瓜的定投策略,即定时定额购买以上标的并一直持有至回测结束。
为了方便对比,这里首先采用5年定投方式,时间从2016年1月1日开始到2020年12月31日结束,采用的费率为0.03%,没有印花税,也没有考虑交易滑点问题。
老规矩,先上代码,对于指标计算不清楚的可以看这里。回测的代码如下:
import akshare as ak
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import pandas as pd
from math import sqrt, pow
import numpy as np
import numpy_financial as npf
# 计算定投策略并得到买卖信号和仓位
def period_invest(stock_data, type='M'):
stock_data.index = pd.to_datetime(stock_data.date)
stock_data = stock_data.sort_index()
# 每月第一个交易日定投
buy_month = stock_data.resample(type, kind='period').first()
#print(buy_month)
stock_data['short'] = stock_data.close.rolling(5).mean()
stock_data['long'] = stock_data.close.rolling(30).mean()
stock_data['MA60'] = stock_data.close.rolling(60).mean()
# 定投购买指数基金
stock_data['poschg'] = 0
stock_data['OP_SIG'] = 0
#stock_data.index = range(len(stock_data))
buy_month.index = range(len(buy_month))
i = 0
# comm_rate = 3 / 10000 # 费率
# amount = 1000 # 定投金额
global comm_rate
global amount
while i < len(buy_month):
date = buy_month.loc[i,'date']
stock_data.loc[stock_data['date'] == date, 'OP_SIG'] = 1
price = stock_data.loc[stock_data['date'] == date, 'open']
# print(date, price, 1000 * ( 1 - 0.0005) / price)
stock_data.loc[stock_data['date'] == date, 'poschg'] = amount * ( 1 - comm_rate) / price # 定投日增加固定金额
i += 1
stock_data['position'] = stock_data['poschg'].cumsum() # 累计申购份额
stock_data['invest'] = amount * stock_data['OP_SIG'].cumsum() # 累计投入金额
stock_data['current'] = stock_data['close'] * stock_data['position'] # 累计现值
stock_data['capital'] = stock_data['current'] / stock_data['invest'] # 累计净值
stock_data['capital_rtn'] = stock_data['capital'].pct_change() # 日涨跌幅
# 处理第一行数据为NAN问题
stock_data.index = range(len(stock_data))
stock_data.loc[0, 'change'] = stock_data.loc[0, 'close'] / stock_data.loc[0, 'open'] - 1
stock_data.loc[0, 'capital_rtn'] = stock_data.loc[0, 'capital'] - 1
return stock_data, buy_month
# 计算最大回撤
def max_drawdown(date_list, capital_list):
df = pd.DataFrame({'date': date_list, 'capital': capital_list})
df['max2here'] = df['capital'].expanding().max() # 计算当日之前的账户最大价值
df['dd2here'] = df['capital'] / df['max2here'] - 1 # 计算当日的回撤
# 计算最大回撤和结束时间
temp = df.sort_values(by='dd2here').iloc[0][['date', 'dd2here']]
max_dd = temp['dd2here']
end_date = temp['date']
# 计算开始时间
df = df[df['date'] <= end_date]
start_date = df.sort_values(by='capital', ascending=False).iloc[0]['date']
# df.to_excel("dropdown.xlsx", sheet_name='capital', index=False)
# print('最大回撤为:%f,开始日期:%s,结束日期:%s' % (max_dd, start_date, end_date))
return max_dd, start_date, end_date
# ====读取基金数据
# code = '510300'
code = '159915'
df_big = pd.read_csv('510300.csv', encoding='gbk')
df_small = pd.read_csv('159915.csv', encoding='gbk')
# 使用copy()避免SettingWithCopyWarning
data = df_small[['交易日期', '开盘价', '最高价', '最低价', '收盘价', '成交量']].copy()
# 列名改为英文方便下面操作
data.rename(columns={'交易日期': 'date', '开盘价': 'open', '最高价': 'high', '最低价': 'low',
'收盘价': 'close', '成交量': 'volume'}, inplace=True)
data.index = pd.to_datetime(data.date)
data.index = data.index.strftime('%Y%m%d')
data = data.sort_index()
data['change'] = data['close'].pct_change(periods=1) # 计算涨跌幅=change
# ====设置回测参数
amount = 1000 # 定投金额
trade_day = 244 # 每年平均交易日天数
s_date = '20120101' # 回测开始日期
e_date = '20161231' # 回测结束日期
comm_rate = 3 / 10000 # 费率
# ====根据策略,计算仓位,资金曲线等
# 计算买卖信号
start_date=pd.to_datetime(s_date)
end_date=pd.to_datetime(e_date)
cond1 = data['date'] >= start_date.strftime('%Y-%m-%d') # 从指定时间开始
cond2 = data['date'] <= end_date.strftime('%Y-%m-%d') # 到指定时间结束
print("回测区间:%s-%s" % (s_date, e_date))
df = data[cond1 & cond2]
(df, buy_month) = period_invest(df, 'M')
# df['capital1'] = (df['capital_rtn'] + 1).cumprod() # 计算累积收益率
df['stock'] = (df['change'] + 1).cumprod() # 计算累积收益率
df.to_excel("ETF定投策略-"+code+".xlsx", sheet_name='回测结果')
# ====根据策略结果,计算评价指标
# 计算股票和策略年收益
dft = df[['date', 'change', 'capital_rtn']].copy()
dft['date'] = pd.to_datetime(dft['date']) # 将str类型改为时间戳格式
# 计算每一年股票、资金曲线的收益
year_rtn = dft.set_index('date')[['change', 'capital_rtn']].resample('A').apply(lambda x: (x + 1.0).prod() - 1.0)
year_rtn.dropna(inplace=True)
date_list = list(df['date'])
capital_list = list(df['capital'])
index_list = list(df['stock'])
dft = pd.DataFrame({'date': date_list, 'capital': capital_list, 'close': index_list})
dft.sort_values(by='date', inplace=True)
dft.reset_index(drop=True, inplace=True)
rng = pd.period_range(dft['date'].iloc[0], dft['date'].iloc[-1], freq='D') # 创建时间范围,用于计算回测天数
capital_cum = dft['capital'].iloc[-1] / dft['capital'].iloc[0]
stock_cum = dft['close'].iloc[-1] / dft['close'].iloc[0]
# capital_annual = (dft['capital'].iloc[-1] / dft['capital'].iloc[0]) ** (trade_day / len(rng)) - 1
stock_annual = (dft['close'].iloc[-1] / dft['close'].iloc[0]) ** (trade_day / len(rng)) - 1
invest_num = len(buy_month)
fund = invest_num * amount
pl = [-1000] * (invest_num + 1) # 建立irr计算用list
pl[invest_num] = df['current'].iloc[-1] # 资金现值
month_capital = npf.irr(pl) # 计算月收益
capital_annual = pow((1 + month_capital), 12)-1 # 计算年化收益
capital_drawdown = max_drawdown(date_list, capital_list)
stock_drawdown = max_drawdown(date_list, index_list)
print('定投次数:%d 投入资金:%d 账户现值:%d' % (invest_num, fund, df['current'].iloc[-1]))
print('策略累积收益:%f 基金累积收益:%f' % (capital_cum, stock_cum))
print('策略年化收益:%f 基金年化收益:%f' % (capital_annual, stock_annual))
print('策略最大回撤:%f,开始日期:%s,结束日期:%s' % capital_drawdown)
print('股票最大回撤:%f,开始日期:%s,结束日期:%s' % stock_drawdown)
# 将数据序列合并为一个datafame并按日期排序
capitalrtn_list = list(df['capital_rtn'])
indexrtn_list = list(df['change'])
dft = pd.DataFrame({'date': date_list, 'capital': capital_list, 'index': index_list, 'capital_rtn': capitalrtn_list,
'index_rtn': indexrtn_list})
dft.sort_values(by='date', inplace=True)
dft.reset_index(drop=True, inplace=True)
volatility = dft['capital_rtn'].std() * sqrt(trade_day) # 计算收益波动率
b = dft['capital_rtn'].cov(dft['index_rtn']) / dft['index_rtn'].var() # 计算beta值
rng = pd.period_range(dft['date'].iloc[0], dft['date'].iloc[-1], freq='D') # 创建时间范围,用于计算回测天数
# print(len(rng),len(df))
rf = 0.0269 # 无风险利率取10年期国债的到期年化收益率(2022-9-16)
annual_stock = (dft['capital'].iloc[-1] / dft['capital'].iloc[0]) ** (trade_day / len(rng)) - 1 # 账户年化收益
annual_index = (dft['index'].iloc[-1] / dft['index'].iloc[0]) ** (trade_day / len(rng)) - 1 # 基准年化收益
beta = dft['capital_rtn'].cov(dft['index_rtn']) / dft['index_rtn'].var() # 计算beta值
a = (annual_stock - rf) - beta * (annual_index - rf) # 计算alpha值
sharpe = (annual_stock - rf) / volatility # 计算夏普比率
dft['diff'] = dft['capital_rtn'] - dft['index_rtn']
annual_mean = dft['diff'].mean() * trade_day
annual_std = dft['diff'].std() * sqrt(trade_day)
info = annual_mean / annual_std # 计算信息比率
print('收益波动率:%f' % volatility)
print('阿尔法:%f 贝塔:%f 夏普比率:%f 信息比率:%f' % (a, b, sharpe, info))
# 绘图显示结果
plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(100)) # 设置x轴密度
plt.xticks(rotation=45) # 旋转45度显示
plt.plot(df['date'], df['stock'], label=code+'收益')
plt.plot(df['date'], df['capital'], label='ETF定投收益')
plt.plot(df['date'], df['close'], label='收盘价')
plt.scatter(buy_month['date'], buy_month['close'], c='r', label='买点')
plt.ylabel("y轴", fontproperties="SimSun")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.title("ETF定投策略回测(%s-%s)" % (s_date, e_date))
plt.legend(loc='best')
plt.savefig(code + '_' + '定投回测结果' + '.png')
plt.show()
不同年度回测得到的基金定投买点及回测收益曲线如下图所示。
统计不同年份开始对510300进行定投,每月投入1000元,在每月的第一个交易日以开盘价买入基金并一直持有,时间5年。在第5年的最后一个交易日以收盘价和持有份额计算账户现值,结果如下:
由于510300上市交易时间是2012年5月28日,因此最早只能到2012年,且2012年少投了4次。从上表可以看出,从不同年份开始定投的结果还是有一些差别,其中5年盈利,2年亏损,总体来说还是盈利多余亏损。
同样,对159915采用同样的方式进行回测,结果如下:
实际上159915是2011年上市交易的,但是为了方便对比,也从2012年开始定投。从上表可以看出,创业板指数的年化收益波动更大,盈利的比例高,亏损的时候比例也高,但总体来说收益要高于510300。这个在一定程度上说明市场波动越大,定投的收益越好。
这里需要注意的是以上表中的年化收益不是用总收益率来计算的。总收益率等于期末总资产-投入总本金)/投入总本金,用总收益率计算年化收益只适用于在开始时一次性投入总本金的情况。基金定投的年化收益率需要用以下公式来计算:
M=a(1+x)[-1+(1+x)^n]/x
其中,M为期末收益;a为每期定投金额;x为收益率,n为定投次数。
这个公式一看就很复杂,无法直接计算,实际上Python提供了相关函数,我们直接用就好了。Python计算基金定投的年化收益率需要使用irr()函数,该函数现在包含在包numpy_financial中,需要安装该包后使用。也可以直接使用scipy的xirr()计算。
pip install numpy_financial
import numpy_financial as npf
pl = [-1000] * (invest_num + 1) # 建立irr计算用list
pl[invest_num] = df['current'].iloc[-1] # 资金现值
month_capital = npf.irr(pl) # 计算月收益
capital_annual = pow((1 + month_capital), 12)-1 # 计算年化收益
其中定投次数invest_num=126,总投资额为126000,现值为142890。
注意,irr()函数计算的为月收益率,需要转换为年收益率。
另外,在Excel中也有相应的函数IRR和XIRR可以使用。IRR是先计算每月收益率,然后根据月收益率计算年收益率。无需指定时间序列,但最好是计算等时间间隔的数据。XIRR可以直接计算年化收益率,但是要指定数据对应的时间序列。
解释一下为什么定投策略的年化收益会有时高于ETF本身,有时又低于ETF本身。因为定投策略是每个月投入固定金额购买ETF,目前的时间是固定在每个月的第一个交易日。在市场处于高位时定投会拉高平均成本,因此市场出现下跌的时候整体收益会下降;而在市场处于低位时定投会拉低平均成本,在市场出现上涨的时候整体收益会大增。ETF本身的收益计算相当于在回测开始日期一次性投入所有资金,然后一直持仓不动,这个其实跟存定期差不多一回事,只不过每年的利率是随市场波动的。
三、对比分析
基金定投感觉上跟银行的零存整取比较像,这里把基金定投跟零存整取、5年定期存款以及5年期国债的收益做一下对比分析,看看哪个好。
1.定期存款的收益
5年定期到期利息=本金×5年定期年利率×年数
2.零存整取的收益
零存整取利率一般为同期定期存款利率的60%。零存整取利息计算公式如下:
利息金额=月存金额×累计月积数×月利率
3.同期国债的收益
5年期国债的到期利息=本金×5年期国债利率×年数
从上面的数据可以看出,基金定投5年的平均年化收益率6.26~7.88%,要高于同期的定期存款和国债的年化收益率。
四、结论
说实话,回测结果很是令人有点失望的,因为看文章有不少人随随便便的傻瓜定投就能获得百分之几十的年化收益,不知道是怎么达到。我检查了一圈也没发现哪里算错了,如果大家觉得哪里有问题请批评指正。当然也可能是我这个策略是真傻,而他们在通过择时之类的方式装傻,要么就是我跟他们不是在同一个中国股市里~~~
考虑到上一篇文章策略评价指标里面提到的美国股市和中国股市长期的年化收益率都是10%左右,回测得到的5年基金定投的年化收益率还是属于正常的。定投绝对是一个很适合懒人的投资方法。
因为本文采用的是傻瓜定投,没有对定投策略做任何优化,既没有择时,也没有轮动,自然也没有低买高卖的倒腾。读者有兴趣可以在本文代码的基础上对定投方式做一些优化,也许有惊喜哦~~~
-----------------------------------
原创不易,请多支持!