今天我用Python做了定投回测实验,想从历史数据上看,什么时候定投获得的收益最大。
先上结论:
对于沪深300/中证500,周定投的话,周5定投收益最高,但由于周5申购下周一才确认,因此会多占用2天时间,考虑到资金的时间价值,周5投也不一定最合适,另外从回测结果上看,周2、周3收益率垫底,因此周定投的朋友,周1、周4、周5选哪天投都ok,差不多。
对于沪深300/中证500,月定投的朋友,尽量避免月中,至于为什么月中定投收益低,我也没想明白,期待高手解惑。
以上结论基于10年定投&2年无脑定投(定时定额)回测结果,历史不一定代表未来,谨慎参考~
以下为啰里啰嗦的分析过程。
这几年定投很火,大家都知道,简单说就是分批买入基金。
定投的好处有很多:
每月拿出工资的一部分买基金,相当于强制储蓄,避免乱花钱;
对于我们这种非专业投资者,不太会择时,既然不知道什么时候该买,那就佛系一点,定时自动扣款,不用盯盘,省时省力;
一次性买入有可能会买在高点被套,而定投可以摊平成本,避免这种情况。
Baostock是一个免费、开源的证券数据平台,我们可以用它来遍历沪深市选股,监测股票行情,进行量化分析和定投回测。
Baostock的安装方法和其他Python包一样,pip install baostock就行。
类似的包还有Dtshare、Tushare,这两个都是免费的Python金融数据接口库,可以自行选择,不过Tushare现在取数需要注册和积分,稍有点麻烦。
Baostock官方说明文档:baostock.com
Tushare官方说明文档:tushare.pro/document/1
Dtshare官方说明文档:dt-share.com
一些问题:
定投哪些基金?
定投频率和日期?
定投金额?
定投日遇到节假日(不开盘)如何操作?
收益率如何计算?
第一个问题,我们定投基金一般选指数基金(债券基金不适合定投,这里不展开说了)。
“指数”很好理解,就是选取一组股票,把他们的价格进行加权平均。也就是说,你买股票买的是单个股票,买指数买的是多个股票。
从代表性来看,指数可以分为宽基指数和窄基指数。
宽基指数包含了不同行业,不同类型的指数,能够避免单个股票、单个行业出现的黑天鹅事件,风险相对较小。在中国,最合适新手的宽基指数,就是沪深300指数、中证500指数(这2个可以说是定投入门经典组合了)。
窄基指数中比较有代表性的是行业指数,行业指数要比宽基指数投资风险高,当然,出现利好时,获得超额收益的可能性也更大。我也会投行业指数,例如消费指数、医疗指数,这个以后再说。
今天就先看沪深300、中证500这2个指数。
定投周期通常有周定投、月定投,这两都可以测一下。
周定投:测周一到周五,每次定投1单位金额(不设置具体金额了,毕竟最后计算的收益率是一个相对值的概念,这里1单位你可以理解为1千元,1万元之类)。
月定投:测每月5日、10日、15日、20日、25日,每次定投4单位金额。
节假日:为了方(偷)便(懒),遇到节假日就直接跳过不投。
收益计算逻辑:累计收益率=(市值-本金)/本金
简便起见,这里不考虑资金效率,因为本文主要涉及周定投、月定投的不同定投日的横向比较,也是一个相对值的概念,所以在计算收益率上没那么严谨(其实还是我懒得计算IRR>.<)。
我是从2010年5月31日(周一)开始取数,看近10年的定投情况(我们一般认为中国股市的牛熊周期是7-8年,因此年限太短的话覆盖不了一个周期)。
上图是在果仁网上拉的近20年上证指数(大盘)的走势,我简单标注了一下,差不多2001年到2008年是一个熊牛周期,7年,2008年到2015年又是一个7年周期(粗略地看)。
整体上来讲中国股市的熊市时间很长,所以长期定投真的很考验人的耐心啊(当然了,没有遇到大牛市,也可以做短线策略,吃一些短期波动的收益)。
import baostock as bs
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, date
# 登陆系统
lg = bs.login()
code = 'sh. 000300'
start = '2010-05-31'
end = '2020-06-01'
# 获取指数基金指数历史数据
hs300_price = bs.query_history_k_data_plus(code, "date,code,open,high,low,close,preclose,pctChg",
start_date=start, end_date=end, frequency="d")
# 整合为DataFrame格式
data_list = []
while (hs300_price.error_code == '0') & hs300_price.next():
data_list.append(hs300_price.get_row_data())
hs300 = pd.DataFrame(data_list, columns=hs300_price.fields)
# 最后一行对应的日期执行卖出操作
sell_index = hs300[hs300['date']==end].index[0]
hs300_buy = hs300.drop(sell_index, axis=0)
sell_price = float(hs300.loc[sell_index, 'close'])
场外定投都是按照当日收盘价格计算,因此hs300表里close列是我们要用到的,如下图,最后一行2020年6月1日执行卖出。
参数说明:
date-交易所行情日期,code-证券代码,open-今开盘价格 ,high-最高价,low-最低价,close-今收盘价,preclose-上个交易日收盘价,pctChg-涨跌幅。
# 周定投
total = [0] * 5
count = [0] * 5
for index,row in hs300_buy.iterrows():
# 将日期转化成星期,周一到周五用数字0到4来表示
weekday = int(datetime.strptime(row['date'], '%Y-%m-%d').weekday())
# 1单位金额除以收盘价格得到购买基金份额,并计算累计份额
total[weekday] = total[weekday] + 1/float(row['close'])
count[weekday] += 1
for index, unit in enumerate(total):
print("每周{}定投,累计投入{}单位金额,最终卖出{}单位金额,收益率{}%;".format(index+1, count[index],\
round(sell_price*unit,2), round(100*(sell_price*unit-count[index])/count[index],2)))
# 月定投
date = ['05', '10', '15', '20', '25']
hs300_buy['day'] = hs300_buy['date'].map(lambda x: x[-2:])
for i in date:
close_series = hs300_buy[hs300_buy['day']==i]['close']
buy = close_series.shape[0]
unit = close_series.map(lambda x: 4/float(x)).sum()
print("每月{}日定投,累计投入{}单位金额,最终卖出{}单位,收益率{}%;".format(i, 4*buy,\
round(sell_price*unit, 2), round(100*(sell_price*unit-4*buy)/(4*buy),2)))
结果:
每周1定投,累计投入473单位金额,最终卖出618.48单位金额,收益率30.76%;
每周2定投,累计投入489单位金额,最终卖出637.93单位金额,收益率30.46%;
每周3定投,累计投入493单位金额,最终卖出642.89单位金额,收益率30.4%;
每周4定投,累计投入491单位金额,最终卖出642.0单位金额,收益率30.75%;
每周5定投,累计投入484单位金额,最终卖出634.12单位金额,收益率31.02%;
每月05日定投,累计投入284单位金额,最终卖出371.59单位,收益率30.84%;
每月10日定投,累计投入328单位金额,最终卖出429.2单位,收益率30.85%;
每月15日定投,累计投入324单位金额,最终卖出419.69单位,收益率29.53%;
每月20日定投,累计投入340单位金额,最终卖出444.31单位,收益率30.68%;
每月25日定投,累计投入340单位金额,最终卖出444.6单位,收益率30.76%;
可以看到,如果是定时定额的定投方式,周定投和月定投最终的累计收益率差别不算大,按周投的话周五的收益率最高,周三收益率最低,按月投的话月中的收益率最低。
中证500的回测思路也一样,把股票代码改成000905就可以,回测结果:
每周1定投,累计投入473单位金额,最终卖出552.1单位金额,收益率16.72%;
每周2定投,累计投入489单位金额,最终卖出567.74单位金额,收益率16.1%;
每周3定投,累计投入493单位金额,最终卖出572.07单位金额,收益率16.04%;
每周4定投,累计投入491单位金额,最终卖出572.81单位金额,收益率16.66%;
每周5定投,累计投入484单位金额,最终卖出566.01单位金额,收益率16.94%;
每月05日定投,累计投入284单位金额,最终卖出332.63单位,收益率17.12%;
每月10日定投,累计投入328单位金额,最终卖出381.31单位,收益率16.25%;
每月15日定投,累计投入324单位金额,最终卖出373.2单位,收益率15.18%;
每月20日定投,累计投入340单位金额,最终卖出397.25单位,收益率16.84%;
每月25日定投,累计投入340单位金额,最终卖出394.91单位,收益率16.15%;
中证500的结果和沪深300相似,按周投的话也是周五的收益率最高,周三收益率最低,按月投的话月中的收益率最低。
单测一个时间段,可能不具备代表性,而且基本上很少有人坚持十年买入,一朝卖出。
那么我再实验一下定投2年的结果,也就是在2010.05.31-2018.05.31这个时间段,随机取100个日期,并计算从这100个日期开始定投2年的平均收益。
代码如下:
import random
# 随机生成100个索引
end_index = hs300[hs300['date']=='2018-05-31'].index[0]
random_index = random.sample(range(0, end_index), 100)
random_index.sort()
start_list = []
week_day = []
buy_amt = []
sell_amt = []
return_rate = []
for i in random_index:
# 生成每个随机数对应的定投区间和卖出价
random_start = hs300.loc[i]['date']
random_end = random_start[:2] + str(int(random_start[2:4])+2) + random_start[4:]
random_buy = hs300[(hs300['date']>=random_start) & (hs300['date']<=random_end)]
random_end_index = int(random_buy.index[-1])+1
sell_price = float(hs300.loc[random_end_index, 'close'])
# 周定投
total = [0] * 5
count = [0] * 5
for index,row in random_buy.iterrows():
# 将日期转化成星期,周一到周五用数字0到4来表示
weekday = int(datetime.strptime(row['date'], '%Y-%m-%d').weekday())
# 1单位金额除以收盘价格得到购买基金份额,并计算累计份额
total[weekday] = total[weekday] + 1/float(row['close'])
count[weekday] += 1
for index, unit in enumerate(total):
start_list.append(random_start)
week_day.append(index+1)
buy_amt.append(count[index])
sell_amt.append(round(sell_price*unit,2))
return_rate.append(round(100*(sell_price*unit-count[index])/count[index],2))
df = pd.DataFrame({'定投开始日期':start_list,
'每周几':week_day,
'累计投入单位金额':buy_amt,
'最终卖出单位金额':sell_amt,
'收益率(%)':return_rate})
相应的,我们可以测100次月定投结果,也会得到上方形式的表格,但是这么看肯定不够直观,还是得画个图,那就画个箱线图吧~
沪深300周定投结果:周5>周4>周1>周3>周2。
沪深300月定投结果:20号>25号>10号>5号>15号。
中证500周定投结果:周5>周1>周4>周2>周3。
中证500月定投结果:20号>25号>5号>10号>15号。
图中紫色数字为100个随机样本的收益率均值,可以看到2年定投,不论是沪深300,还是中证500,都依旧是周五定投收益最高,月中定投收益最低。
各位可以把代码拿去,自行实验不同时期,不同定投年限的收益结果~
周五定投收益率高,其实比较符合经验,因为通常节假日前的那个交易日股市跌的可能性大于涨的可能性,股市跌就意味着基金价格可能更便宜了,买的便宜自然赚得多。
从近十年的沪深300收盘价数据上看也是如此,这10年共有512周有交易数据,我看了下每一周的最低价出现在周几,发现一周最低价出现在周五的次数占比最高,其次是周一。
中证500的结果稍微有点不一样,周一出现最低价的频次最高,其次才是周五(不过周一分母较小,应该是很多假日都遇上了周一)。
虽然周五定投收益率最高,但是因为场外定投的规则是:当日15点以前买入,次日确认份额,也就意味着如果周五申购,要到下周一确认,会平白多出2天资金占用时间(如果你其他理财渠道在周末有收益,周五定投就会造成你这两天的收益损失哟)。
结合前面的实验结果,周二和周三的收益率基本都是垫底,因此周定投沪深300、中证500,可以避开周2、3,选周1、4、5。
不过为什么每月15号定投收益率低,我一直没想明白?
大家有没有发现,就是,我测试了10年的数据,说好的“定投十年赚十倍”(这也是个公众号名称,想学定投的话挺值得关注的),实际上定投沪深300十年,收益才30%左右(中证500才16%)?
问题在哪呢,第一个就是统计口径的问题了,我计算的是累计收益率,如果计算内部收益率肯定要比这个值高。
第二个问题就在于,这10年跨越了一个牛熊周期,而傻傻的定投会导致买入在牛市高点,徒增成本。因此定投要想多赚,需要有比较好的择时卖出策略。
上图是中证500近十年走势,站在我们现在这个时点往回看,我们知道该指数在2015年6月11日冲顶到11366.29。
好,我们马后炮地回测一下2010.05.31-2015.06.10的定投结果:
每周1定投,累计投入238单位金额,最终卖出661.06单位金额,收益率177.76%;
每周2定投,累计投入245单位金额,最终卖出676.76单位金额,收益率176.23%;
每周3定投,累计投入246单位金额,最终卖出678.82单位金额,收益率175.94%;
每周4定投,累计投入246单位金额,最终卖出683.81单位金额,收益率177.97%;
每周5定投,累计投入245单位金额,最终卖出681.71单位金额,收益率178.25%;
定投5年赚176%,真香(醒醒,实际上你根本猜不准哪儿是顶)。
不过,我又算了下2010年5月31日一次性申购,到顶后卖出,收益率是(11366.29-4104.36)/4104.36=177%,也就是说分批买入和一次性买入差别不大。
这种是基金长期卧倒装死,然后突然起跳的行情,这样的场景下定投优势无法显现(因为底部波动小,分批投,跟一次性投,成本都差不多)。
那什么样的场景下适合定投?这就要说到定投达人常提起的“微笑曲线”。
还是看上面的走势图,2015年6月11日后,中证500经历了一个月的单边暴跌, 接着小幅上涨,后续有一段上下波动。
假如有个人三傻,他前期没有累计筹码,到了6月份看见基金涨势喜人,想要追涨一波,结果没想到后面跌跌不休,心态崩了,后面有点上涨就赶紧趁势卖了。也就是他2015.06.11在顶部买入,走完上图那个深V(绿色的行情)后卖出,此时收益率是(8551.99-11366.29)/11366.29=-25%。
如果有个二蛋,他虽然也有点二,但是比三傻好点,也比较有钱,买在顶部后,发现后期跌了,“越跌越买”嘛,就继续定投,他的收益将会是:
每周1定投,累计投入5单位金额,最终卖出5.11单位金额,收益率2.19%;
每周2定投,累计投入6单位金额,最终卖出5.99单位金额,收益率-0.16%;
每周3定投,累计投入6单位金额,最终卖出6.1单位金额,收益率1.63%;
每周4定投,累计投入6单位金额,最终卖出5.87单位金额,收益率-2.12%;
每周5定投,累计投入6单位金额,最终卖出5.95单位金额,收益率-0.8%;
二蛋明显比三傻亏得少,如果时间选的好,还能略微有点收益!
后面那个黄色的对勾行情也一样,时间范围为15.08.20-15.11.25。
一次性买入:(7922.38-8049.51)/8049.51=-1.58%
周定投:
每周1定投,累计投入13单位金额,最终卖出15.12单位金额,收益率16.32%;
每周2定投,累计投入13单位金额,最终卖出15.3单位金额,收益率17.71%;
每周3定投,累计投入12单位金额,最终卖出14.3单位金额,收益率19.15%;
每周4定投,累计投入12单位金额,最终卖出13.81单位金额,收益率15.07%;
每周5定投,累计投入12单位金额,最终卖出13.73单位金额,收益率14.42%;
这结果也不用多说了吧~
以上都是定时定额策略的结果,如果是均线法、价值平均法等智能定投策略,还能有更高的收益。
定投虽然稳,但是也考验人。虽然大家都知道定投需要耐心,但是真正能一直坚持的人其实很少。牛市拿不住早早卖了止盈的人有很多,熊市扛不住早早割肉离场的人也有很多,前者还好只是赚的少点,后者就是白折腾半天的赔钱货了。
假设我们回到过去,从11年开始定投,我们在当时是不知道未来的情况的,如果你和我一样是个有耐心但是偏保守的人,那么,会发生如下情况:
11年开始,指数一直在下跌,越跌越买啊,累计筹码,等待时机;
又一年过去,12年一直在跌,这时候很多人会很焦虑,但你一方面有持续的现金流(工资),一方面又是耐得住寂寞扛得住跌的人,所以还是会继续定投(甚至加大投入金额);
13年起,指数开始缓缓稳步上涨了,虽然涨得不多,但是渐渐回本,心里也不慌了;
到14年,指数一直在涨,定投的思想是涨得多买的少嘛,所以你开始减少投入金额;
15年上半年你每天都在关注走势,因为大家都说大牛市来了,很多以前不投资的人都开始进场追涨,天天涨那么多,激动的同时你也有点害怕,万一将来爆跌那前面的功夫不就白费了?而且现在市场过热,感觉心里有点不踏实,所以开始分批逐步卖出;
4月中旬,指数涨到了8000,可怕,你觉得也赚够了(翻了一番),就全卖了,并且还有点沾沾自喜,还想着后面指数暴跌;
当然后面没有暴跌,一路飙升到6月的11000多……md……
设想上面这种场景,是想展示我作为一个普通投资者的定投心态(不一定最赚钱,也不一定适合你,仅供参考):
只要指数一直处于低估状态(可以参考支付宝的指数红绿灯,或者螺丝钉估值、果仁估值、且慢估值之类),就要坚持持有;
现金流充裕的情况下,一定要在底部多累积筹码(但千万不要梭哈),有计划地买入,打长期有准备之仗;
市场不可预测,不要贪心,不要想着赚最后一个铜板(虽然事后肯定会有遗憾,人之常情嘛),达到预期收益后收手就好,切勿恋战。
买卖基金是有手续费等费用的,本文未考虑这部分费用。
本文的收益率是累计收益率,未考虑资金的时间占用成本。
本文测的沪深300指数(用的收盘点数),而实际上我们购买基金买的是各个基金公司的产品(按净值计算),这些产品跟踪沪深300指数,例如易方达沪深300ETF联接A(代码:110020),天弘沪深300ETF联接C(代码:005918),虽然都叫沪深300,但还是有差异的,例如跟踪误差不一样,收费不一样等等。此外还会有一些增强型,也就是加入了跟多的主观人为操作的指数,不完全被动跟踪,例如我比较喜欢买的兴全沪深300增强A(代码:163407)。
历史不一定代表未来,谨慎参考。
本文所测定投为无脑定时定额投资,后续计划:回测智能定投,回测窄基指数。