从前面的ETF基金定投策略回测和周内效应分析文章中可以看到,代表大盘指数的沪深300ETF基金510300和代表小盘的创业板ETF基金159915的收益在长期来看差别较大。但是单独定投小盘指数收益高,但是回撤比较大;单独定投大盘指数回撤也不小,收益也不够高。
根据组合资产理论,通过不同标的组合,可以在一定程度上分散风险。当然,前提是所选标的最好是不相关的。但实际上如果只在A股市场的指数里选择,要找两个完全不相关的标的估计是不太可能。这里要求不高,找两个相关性不高的标的就好了。这里我偷个懒,根据网友的研究结果,沪深300指数000300和和中证1000指数000852的差异是最大的,而这两个指数刚好也能够作为大盘股和小盘股的代表。那么下面就选择这两个指数作为标的,对应的ETF基金分别是沪深300ETF基金510300和中证1000ETF基金512100。
由于我是想寻找傻瓜定投策略,所以这里不会把ETF当作股票来买卖做趋势跟踪,还是采取每月或者每周定投的方式。
一、轮换策略
风格轮换定投策略如下:
(1)每个交易日收盘后计算沪深300指数和中证1000指数过去N个交易日的累积涨幅(动量)。
(2)根据两个指数的动量的相对大小确定下一个交易日买入动量大的指数对应的ETF基金,金额固定为1000元。
(3)买入后一直持有,回测结束时计算收益。
二、回测结果
下面我们来测试一下看看轮动买入的策略是否会更好。
本次回测中每次定投固定金额1000元,滑点率为0.01%,交易费率为0.05%。回测区间从2016年12月1日到2022年9月1日。由于是傻瓜定投,因此买入后一直持有,直到回测结束。
回测使用的代码如下:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import pandas as pd
import akshare as ak
from math import sqrt, pow
import numpy as np
import numpy_financial as npf# 计算动量策略
def momentum(sz50, zz1000, etf50, etf1000, n=20):diff = set(etf50['date'])-set(etf1000['date'])diff1 = set(etf50['date'])-set(sz50['date'])print(diff, diff1)stock_data = pd.DataFrame({'date': etf50['date'], 'big_index_close': sz50['close'],'small_index_close': zz1000['close'],'big_open': etf50['open'], 'big_close': etf50['close'],'big_change': etf50['change'], 'small_open': etf1000['open'],'small_close': etf1000['close'], 'small_change': etf1000['change']})# 动量策略momentum_days = 20stock_data['big_mom'] = stock_data['big_index_close'].pct_change(periods=momentum_days) # 计算N日的动量momentumstock_data['small_mom'] = stock_data['small_index_close'].pct_change(periods=momentum_days)stock_data.index = range(len(stock_data))stock_data.loc[stock_data['big_mom'] > stock_data['small_mom'], 'big_sig'] = 1stock_data.loc[stock_data['big_mom'] < stock_data['small_mom'], 'small_sig'] = 1# 下跌趋势停投stock_data.loc[(stock_data['big_mom'] < 0) & (stock_data['small_mom'] < 0), 'big_sig'] = 0stock_data.loc[(stock_data['big_mom'] < 0) & (stock_data['small_mom'] < 0), 'small_sig'] = 0# 上涨趋势停投# stock_data.loc[(stock_data['big_mom'] > 0) & (stock_data['small_mom'] > 0), 'big_sig'] = 0# stock_data.loc[(stock_data['big_mom'] > 0) & (stock_data['small_mom'] > 0), 'small_sig'] = 0global amount, commis_rate, slippage_rateold_pos_big = 0old_invest = 0stock_data['big_poschg'] = 0stock_data['big_pos'] = 0stock_data['invest_big'] = 0i = 0while i < len(stock_data) : # 大盘ETF处理if stock_data.loc[i, 'big_sig'] == 1: # i+1日买入etf300# print(i,stock_data.loc[i, 'position'])stock_data.loc[i, 'big_poschg'] = old_pos_bigstock_data.loc[i, 'invest_big'] = old_invest # 累计投入金额old_pos_big = amount * (1 - commis_rate - slippage_rate) / stock_data.loc[i, 'big_open'] # 定投日增加固定金额old_invest += amounti += 1else:stock_data.loc[i, 'big_poschg'] = old_pos_bigstock_data.loc[i, 'invest_big'] = old_investold_pos_big = 0i += 1stock_data['big_pos'] = stock_data['big_poschg'].cumsum() # 累计申购份额# stock_data['invest_big'] = amount * stock_data['big_sig'].cumsum() # 累计投入金额stock_data['current_big'] = stock_data['big_close'] * stock_data['big_pos'] # 累计现值stock_data['capital_big'] = stock_data['current_big'] / stock_data['invest_big'] # 累计净值stock_data['capital_rtn_big'] = stock_data['capital_big'].pct_change() # 日涨跌幅# 处理第一行数据为NAN问题stock_data['capital_big'] = stock_data['capital_big'].fillna(1) # 将资金列的NAN改为1stock_data['capital_rtn_big'] = stock_data['capital_rtn_big'].fillna(0) # 将资金列的NAN改为0old_pos_small = 0old_invest = 0stock_data['small_poschg'] = 0stock_data['small_pos'] = 0stock_data['invest_small'] = 0i = 0while i < len(stock_data): # 小盘ETF处理if stock_data.loc[i, 'small_sig'] == 1: # i+1日买入etf1000# print(i,stock_data.loc[i, 'position'],oldPosition)stock_data.loc[i, 'small_poschg'] = old_pos_smallstock_data.loc[i, 'invest_small'] = old_invest # 累计投入金额old_pos_small = amount * (1 - commis_rate - slippage_rate) / stock_data.loc[i, 'small_open'] # 定投日增加固定金额old_invest += amounti += 1else:stock_data.loc[i, 'small_poschg'] = old_pos_smallstock_data.loc[i, 'invest_small'] = old_investold_pos_small = 0i += 1stock_data['small_pos'] = stock_data['small_poschg'].cumsum() # 累计申购份额# stock_data['invest_small'] = amount * stock_data['small_sig'].cumsum() # 累计投入金额stock_data['current_small'] = stock_data['small_close'] * stock_data['small_pos'] # 累计现值stock_data['capital_small'] = stock_data['current_small'] / stock_data['invest_small'] # 累计净值stock_data['capital_rtn_small'] = stock_data['capital_small'].pct_change() # 日涨跌幅# 处理第一行数据为NAN问题stock_data['capital_small'] = stock_data['capital_small'].fillna(1) # 将资金列的NAN改为1stock_data['capital_rtn_small'] = stock_data['capital_rtn_small'].fillna(0) # 将资金列的NAN改为0stock_data.to_excel("trade.xlsx", sheet_name='trade', index=False)return stock_data# 计算股票和策略年收益
def calc_rtn(data, invest_num, current, type):dft = data[['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(data['date'])capital_list = list(data['capital'])index_list = list(data['close'])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)) - 1stock_annual = (dft['close'].iloc[-1] / dft['close'].iloc[0]) ** (trade_day / len(rng)) - 1# 计算年化收益率global amountfund = invest_num * amountpl = [-amount] * (invest_num + 1) # 建立irr计算用listpl[invest_num] = current # 资金现值month_capital = npf.irr(pl) # 计算收益print(month_capital)capital_annual = pow((1 + month_capital), 244) - 1 # 根据日收益计算年化收益capital_drawdown = max_drawdown(date_list, capital_list)stock_drawdown = max_drawdown(date_list, index_list)tt = drawdown(date_list, capital_list)print(type)print('定投次数:%d 投入资金:%d 账户现值:%d' % (invest_num, fund, current))print('策略累积收益:%f 基金累积收益:%f' % (capital_cum, stock_cum))print('策略年化收益:%.2f%% 基金年化收益:%.2f%%' % (round(100*capital_annual, 2), round(100*stock_annual, 2)))print('策略最大回撤:%f,开始日期:%s,结束日期:%s' % capital_drawdown)print('股票最大回撤:%f,开始日期:%s,结束日期:%s' % stock_drawdown)# 计算最大回撤
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_datedef drawdown(date_list, capital_list): # 计算最大回撤,但无法记录出现日期df = pd.DataFrame({'date': date_list, 'capital': capital_list})# df['capital'] = (1 + df['capital_rtn']).cumprod() # 计算净值previos_max = df['capital'].cummax() # 计算上一个最高点drawdowns = (df['capital'] - previos_max) / previos_max # 计算回撤tt = drawdowns.min() # 找出最大回撤return tt# 定义求动量且绘时序图及动量图的函数
def momentum_plot(price, period):lagPrice = price.shift(period)moment = price - lagPricemoment = moment.dropna()plt.rcParams['font.sans-serif'] = ['SimHei']plt.rcParams['axes.unicode_minus'] = Falseplt.subplot(211)plt.plot(price, 'b*')plt.xlabel('date')plt.ylabel('Close')plt.grid(True)plt.title('收盘价时序图(上)&{}日动量图(下)'.format(period))# ====读取股票数据
df_hs300 = pd.read_csv('000300.csv', encoding='gbk')
df_zz1000 = pd.read_csv('000852.csv', encoding='gbk')
cols = ['date', 'code', 'name', 'open', 'high', 'low', 'close', 'volume', 'money', 'amp', 'change', 'chg', 'turnover']
df_hs300.columns = cols
df_zz1000.columns = cols
df_hs300.index = pd.to_datetime(df_hs300.date)
df_hs300.index = df_hs300.index.strftime('%Y%m%d')
df_hs300 = df_hs300.sort_index()
df_zz1000.index = pd.to_datetime(df_zz1000.date)
df_zz1000.index = df_zz1000.index.strftime('%Y%m%d')
df_zz1000 = df_zz1000.sort_index()
df_etf300 = pd.read_csv('510300.csv', encoding='gbk')
df_etf1000 = pd.read_csv('512100.csv', encoding='gbk')
cols_etf = ['date', 'code', 'name', 'open', 'high', 'low', 'close', 'volume']
df_etf300.columns = cols_etf
df_etf1000.columns = cols_etf
df_etf300['change'] = df_etf300['close'].pct_change()
df_etf1000['change'] = df_etf1000['close'].pct_change()
df_etf300.index = pd.to_datetime(df_etf300.date)
df_etf300.index = df_etf300.index.strftime('%Y%m%d')
df_etf300 = df_etf300.sort_index()
df_etf1000.index = pd.to_datetime(df_etf1000.date)
df_etf1000.index = df_etf1000.index.strftime('%Y%m%d')
df_etf1000 = df_etf1000.sort_index()# ====设置回测参数
trade_day = 244 # 每年平均交易日天数
n_short = 10 # 双均线短周期
n_long = 30 # 双均线长周期
s_date = '20161201' # 回测开始日期
e_date = '20220901' # 回测开始日期
# s_date = '20180101' # 回测开始日期
# e_date = '20220309' # 回测开始日期
amount = 1000
slippage_rate = 0.1 / 1000 # 滑点率
commis_rate = 5 / 10000 # 交易费率
# ====根据策略,计算仓位,资金曲线等
# 计算买卖信号
start_date=pd.to_datetime(s_date)
end_date=pd.to_datetime(e_date)
# 从指定时间开始
hs300 = df_hs300[df_hs300['date'] >= start_date.strftime('%Y-%m-%d')].copy()
hs300 = hs300[hs300['date'] <= end_date.strftime('%Y-%m-%d')].copy()
zz1000 = df_zz1000[df_zz1000['date'] >= start_date.strftime('%Y-%m-%d')].copy()
zz1000 = zz1000[zz1000['date'] <= end_date.strftime('%Y-%m-%d')].copy()
etf300 = df_etf300[df_etf300['date'] >= start_date.strftime('%Y-%m-%d')].copy()
etf300 = etf300[etf300['date'] <= end_date.strftime('%Y-%m-%d')].copy()
etf1000 = df_etf1000[df_etf1000['date'] >= start_date.strftime('%Y-%m-%d')].copy()
etf1000 = etf1000[etf1000['date'] <= end_date.strftime('%Y-%m-%d')].copy()
df = momentum(hs300, zz1000, etf300, etf1000, n=20)# ====根据策略结果,计算评价指标
print("回测区间:%s-%s" % (s_date, e_date))
# 大盘ETF收益
data = pd.DataFrame({'date': df['date'], 'change': df['big_change'], 'close': df['big_close'],'capital_rtn': df['capital_rtn_big'], 'capital': df['capital_big']})
invest_big = len(df[df['big_sig'] > 0]) - 1
current_big = df['current_big'].iloc[-1]
calc_rtn(data, invest_big, current_big, '沪深300')
# 小盘ETF收益
data = pd.DataFrame({'date': df['date'], 'change': df['small_change'], 'close': df['small_close'],'capital_rtn': df['capital_rtn_small'], 'capital': df['capital_small']})
invest_small = len(df[df['small_sig'] > 0]) - 1
current_small = df['current_small'].iloc[-1]
calc_rtn(data, invest_small, current_small, '中证1000')# 净值曲线图
df['stock_big'] = (1 + df['big_change']).cumprod()
df['stock_small'] = (1 + df['small_change']).cumprod()
hs300['stock'] = (1 + hs300['change']/100).cumprod()
zz1000['stock'] = (1 + zz1000['change']/100).cumprod()
# plt.plot(df['date'], df['stock_big'], label='510300')
# plt.plot(df['date'], df['stock_small'], label='512100')
plt.plot(df['date'], hs300['stock'], label='index-000300')
plt.plot(df['date'], zz1000['stock'], label='index-000852')
plt.plot(df['date'], df['capital_big'], label='动量-ETF300')
plt.plot(df['date'], df['capital_small'], label='动量-ETF1000')
plt.ylabel("净值", fontproperties="SimSun")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.title("大小盘轮动定投净值曲线(%s-%s)" % (s_date, e_date))
plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(100)) # 设置x轴密度
plt.xticks(rotation=45) # 旋转45度显示
plt.legend(loc='best')
plt.savefig('大小盘轮动定投-' + s_date + '-' + e_date + '.png')
# plt.show()
plt.close()
# 收盘价曲线图
# plt.plot(df['date'], hs300['close'], label='index-000300')
# plt.plot(df['date'], zz1000['close'], label='index-000852')
plt.plot(df['date'], hs300['stock'], label='index-000300')
plt.plot(df['date'], zz1000['stock'], label='index-000852')
# plt.plot(df['date'], df['big_mom'], label='动量-000300')
# plt.plot(df['date'], df['small_mom'], label='动量-000852')
# plt.plot(df['date'], df['capital_big'], label='510300')
# plt.plot(df['date'], df['capital_small'], label='512100')
plt.ylabel("收盘价", fontproperties="SimSun")
plt.rcParams['font.sans-serif'] = ['SimHei']
# plt.title("大小盘轮动定投净值曲线(%s-%s)" % (s_date, e_date))
plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(100)) # 设置x轴密度
plt.xticks(rotation=45) # 旋转45度显示
plt.legend(loc='best')
plt.savefig('大小盘指数动量对比-' + s_date + '-' + e_date + '.png')
plt.show()
对于N=20的情况,回测结果如下:
累积净值曲线如下:
对于N=10的情况,回测结果如下:
对于N=30的情况,回测结果如下:
结果分析:
(1)大盘指数ETF的收益较差,收益随着N的增大而有所增加。说明大盘指数对动量指标的变化不敏感?轮动效应不明显?
(2)小盘指数的收益还不错,但回撤更大。
(3)N的选择年化收益的影响不明显。
由于选择的是大盘指数和小盘指数轮动,因此基本上没有都有买入,市场的涨涨跌跌基本上都没落下。大盘指数的这个结果基本跟直接持有指数没有区别,但是小盘指数定投真的是出乎意料的好啊。
三、策略优化
观察策略收益曲线会发现,策略在指数上涨时能保持同步上涨,但在市场下跌时也会同步下跌。很明显,单边市场会影响定投的结果。
由于本策略是只买不卖,与股票的涨时持仓跌时空仓有所不同。理论上来讲,如果能够在市场下跌时连续定投,可以降低总体成本;反之,在市场上涨时连续定投,则会增加总体成本。
1.上涨或下跌停止定投策略分析
下面来回测一下这一判断是否正确。将策略修改为在市场上涨时停投和在市场下跌时停投,看看是否有区别。
对于上涨时停止定投(N=20)的情况,回测结果如下:
对于下跌时停止定投(N=20)的情况,回测结果如下:
从结果可以看出,在上涨时停止定投,大盘和小盘指数ETF的年化收益都能够增加,小盘指数ETF增加幅度非常大;在下跌时停止定投,大盘指数ETF收益变为负,小盘指数ETF收益还是有一定幅度的增加。
四、存在的问题
1.年化收益的计算问题
本文采用的是numpy中的irr()函数计算,具体解释参考《ETF基金定投策略回测分析》。
fund = invest_num * amount
pl = [-amount] * (invest_num + 1) # 建立irr计算用list
pl[invest_num] = current # 资金现值
month_capital = npf.irr(pl) # 计算收益
print(month_capital)
capital_annual = pow((1 + month_capital), 244) - 1 # 根据日收益计算年化收益
这里有个问题,采用本文的定投策略,不管是定投大盘还是小盘ETF,其时间间隔都是不确定的。这里都是把irr()的结果当做日收益率,然后采用一年244个交易日来计算年化收益。目前还没有完全想清楚这个方式是否存在问题~~~
2.回测区间问题
回测区间为2016年12月1日到2022年9月1日。回测区间本来是打算一直到写文章这天的,但是512100在2022年9月2日做了一次份额合并,然后导致AKShare中下载的当天数据缺失,现在还不知道怎么把这个数据补上(知道的朋友麻烦告诉我一下^-^~~~)。为了避免这个问题的影响,就把回测区间的结束日期取到这天之前。
3.沪深300指数ETF的定投收益问题
从上面的回测结果可以看到,沪深300指数ETF的定投怎么弄收益都不行。开始我以为是因为它对动量指标不敏感,于是我用以前的周内定投方式测试了一下,结果如下:
大家看到了,结果更差!好吧,必须要承认被沪深300 指数ETF打败了。这就是一头死猪,完全不怕开水烫啊~~~
感觉上是跟这个回测区间及指数在这期间的走势有关,不过还没找到具体原因,后续有时间仔细分析下。现在我只能说,这个区间选择真的是神了。
-----------------------------------
原创不易,请多支持!