基金定投如何选择买卖点?——关于定投的择时研究

作者:易执

来源:Python读财

提示:本文根据数据研究所得,不构成任何投资意见!

基金定投如何选择买卖点?

关于定投的择时研究

基金定投是近几年各种理财平台比较推崇的一种理财方式,大多数关于定投的宣传语中,会有这”六字箴言“:不择时,长期投。所谓择时,便是选择合适的买点和卖点,那基金定投真的用不着择时么?本文就用数据来探究一下是否需要择时以及该如何进行择时。

是否需要择时?

下面上的这张图是2004年到2020年16年间,上证指数和道琼斯指数的走势对比图,大致代表了两个不同资本市场的行情走势。

从图中可以看出两大指数的走势都颇具特点,道指基本是一种长牛的走势,对于这种走势,如果进行指数基金定投是可以不择时的。

而对上证指数而言,走势呈现一种比较明显的周期性,牛短熊长,一个牛熊周期大概为7.5年,整体的重心是向上移动的。如果进行长期投资但不进行择时,虽然长周期来看也可以获得正向的收益,但却享受不了牛市带来的情绪溢价,就像坐了趟过山车,图个刺激。

所以,对基金定投而言,还是有必要进行择时操作的,在指数低估时买入,等牛市来时卖出,那么该如何进行择时呢?

择时的基本策略

定投中可以根据股指的点位或估值高低等指标进行择时操作,本文主要研究基于估值的择时操作,在估值较低时开始定投,待估值达到历史较高水平时卖出,因此如何定义估值是高还是低便是择时操作的关键。

由于沪深300指数能够较好地反映沪深两市的走势,常被设为基金业绩的比较基准。所以本文其为例进行研究,并以市盈率(PE)为估值指标,探究不同估值区间的择时效果

具体策略:择时的指标为该指数当前的PE估值在过去7年(兼顾一轮牛熊周期)的分位数水平,若分位数水平低于定义的区间下限,开启定投周期,直到估值的分位数水平高于区间上限,将持有的份额全部卖出。卖出后,要等到估值水平再次低于区间下限,开始下一轮定投,按照这个规则持续运行。

回测的具体参数如下:

投资标的:易方达沪深 300ETF联接A(110020.OF)

回测时间范围:2013年1月1日-2020年1月1日

估值指标:市盈率(PE)

申购费率:0.15%

赎回费率:0.5%

估值区间:20%-80,30%-80%,40%-80%,20%-70%,30%-70%,40%-70%

每次定投金额:5000

频率:每周一

回测结果

具体的回测框架已经被我封装成可以直接调用的函数run_strategy,代码部分较长,已贴到文末,大家可以直接复制使用。下面直接以图形化的方式展示回测结果。

注: 结果图中,用基金净值走势近似指数走势,黄色阴影代表定投轮此区间,蓝色阴影代表当前估值水平在过去7年的百分位数(右轴表示)。

不择时,一直定投

如不进行择时,则在这七年间每周一均进行买入,一共定投了334期,总收益率为37.71%,年化的收益率大概为4.7%,总体结果算不上理想。

20%-80%区间

设定低估区间为20%以下,高估区间为80%以上则完美的吃到了2014-2015年的这轮牛市,在指数达到顶点前于左侧卖出。期间共进行了113次定投,该轮定投的收益率达到了92.8%,但由于后续估值百分位再也没有达到20%以下,之后没有产生任何买卖行为。

30%-80%区间

将低估区间的范围扩大到30%后,定投轮次加多了一轮,几乎在2018年末最低点抄底,赶上了2019年初的小阳春行情,第二阶段的定投了52期,整体收益率为10.2%。

40%-80%区间

将低估区间进一步放宽到40%后,2013-2020年这七年间共进行了3轮定投,三次定投的期数分别为113次,88次,以及68次,三个轮次定投所产生的收益也较为可观,虽然在16年和18年四季度定投产生了较大回撤,但也把握住了后期的行情。

下面将高估区间降低到70%,看进一步的回测情况。

20%、30%、40%-70%区间

下面一起显示定投区间分别为20%-70%,30%-70%以及40%-70%区间的回测结果图

若将估值70%分位数以上标定为高估区,则相较80%更早的逃顶卖出,在牛市行情中更早落袋为安,也损失了部分潜在收益。与此同时,以70%分位数为卖出点会使得卖出频率相应地也增加了。尤其是40%-70%区间,此时的高估和低估区距离较为接近,买入和卖出更加频繁。

汇总结果

将以上所有回测结果进行整理,得到下面的统计表

表中

绝对收益比 = 各区间回测总收益/不择时总收益

年化收益率 = 期间绝对收益/(首轮定投总期数*每期定投额)

由于定投是采用增量资金进行投资,如果期间定投了多轮,其实总收益率比较难去定义,所以这里选用绝对收益和我自己定义的年化收益综合去衡量(不一定科学)

如果从绝对收益的结果来看,不择时的绝对收益是最高的,但其总共定投了334期。40%-80%估值区间的回测结果用269期定投取得了和334期定投差不多的绝对收益值,且年化收益上更是远远跑赢。如果单从本次回测结果来看,综合考虑资金利用效率等因素,采用40%-80%区间进行择时的效果最好。

但实际进行选择时应该结合自己的风险偏好,低估区间设置得越高,一方面能收集更多筹码,但同时也意味着可能会承担更大的回撤风险。高估区间设置得越低,虽然会错过潜在的收益,但也能提早落地为安。

代码

具体的代码我已封装好,有兴趣的可以自己进行其他指数的研究。根据目前Tushare支持的数据,下面回测框架可以研究沪深300指数、创业板指数、中证500指数、上证50指数等主流的宽基指数的定投策略。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tushare as ts
import warnings
from IPython.display import display
from tqdm import tqdm_notebook
from datetime import *
warnings.filterwarnings("ignore")
%matplotlib inline

pro = ts.pro_api()
plt.rcParams["font.sans-serif"] = ["FangSong"]

def run_strategy(index_code="",fund_code="",money_per_time=5000,
                 start_date=None,end_date=None,v_low = None,
                 v_high = None,plot=True):

    fund_df = pro.fund_nav(ts_code = fund_code)
    fund_df = fund_df.loc[fund_df.update_flag=='0',['ts_code','end_date','unit_nav']].rename(columns = {'end_date':'trade_date'})
    fund_df = fund_df[(fund_df.trade_date>=start_date) & (fund_df.trade_date<=end_date)]
    fund_df["trade_date"] = pd.to_datetime(fund_df.trade_date)
    fund_df.sort_values(by="trade_date",inplace=True)
    fund_df.reset_index(drop=True,inplace=True)

    # tushare提取数据有限制
    # 分开提取然后合并数据
    index_start_date = str(int(start_date[:4])-7) + start_date[4:]
    index_end_date = end_date
    index_valuation_1 = pro.index_dailybasic(ts_code = "000300.SH",start_date = index_start_date,end_date = start_date,fields="trade_date,pe_ttm")
    index_valuation_2 = pro.index_dailybasic(ts_code = "000300.SH",start_date = start_date,end_date = index_end_date,fields="trade_date,pe_ttm")
    index_valuation = pd.concat([index_valuation_1,index_valuation_2],axis=0)
    index_valuation["trade_date"] = pd.to_datetime(index_valuation.trade_date)
    index_valuation.sort_values(by="trade_date",inplace=True)
    index_valuation.set_index("trade_date",inplace=True)



    # 回测时需要计算的指标
    fund_df["units"] = np.nan
    fund_df["total_units"] = np.nan
    fund_df["market_value"] = np.nan
    fund_df["signal"] = np.nan
    fund_df["avg_unit_cost"] = np.nan
    fund_df["return_rate"] = np.nan
    fund_df["round"] = np.nan
    fund_df["percentile"] = np.nan
    round_num = 0
    round_on = False

  #  money_per_time = 5000

    # 循环进行回测
    # 申购费率设置为0.15%
    # 赎回费率设置为0.5%
    for i in tqdm_notebook(range(len(fund_df))):
        valuation_start_date = (fund_df.loc[i,"trade_date"]-pd.Timedelta(7,unit="y")).date()
        valuation_end_date = fund_df.loc[i,"trade_date"]
        valuation_data = index_valuation.loc[valuation_start_date:valuation_end_date].iloc[:,0].values.tolist()

        valuation_high = np.percentile(valuation_data,v_high)
        valuation_low = np.percentile(valuation_data,v_low)
        now = valuation_data[-1]
        
        fund_df.loc[i,"percentile"] = np.round((sorted(valuation_data).index(now)+1)/len(valuation_data),4)
        weekday = valuation_end_date.dayofweek
        

        
        if now<=valuation_low and round_on == False:
            round_on = True
            round_num += 1
                
        if round_on:
            if now < valuation_high:
                if weekday==0:
                    fund_df.loc[i,"round"] = round_num
                    fund_df.loc[i,"signal"] = 1
                    fund_df.loc[i,"units"] =np.round((money_per_time - money_per_time * 0.0015)/fund_df.loc[i,"unit_nav"],2)
                    fund_df.loc[i,"total_units"] = fund_df.loc[(fund_df.signal==1).values & (fund_df["round"]==round_num).values,"units"].sum()
                    fund_df.loc[i,"market_value"] = fund_df.loc[i,"total_units"] * fund_df.loc[i,"unit_nav"]
                    fund_df.loc[i,"avg_unit_cost"] = (len(fund_df[(fund_df.signal==1).values & (fund_df["round"]==round_num).values])*money_per_time)/fund_df.loc[i,"total_units"]
                    fund_df.loc[i,"return_rate"] = np.round(fund_df.loc[i,"unit_nav"]/fund_df.loc[i,"avg_unit_cost"] -1,4)

            else:

                fund_df.loc[i,"signal"] = 0
                fund_df.loc[i,"units"] = -1 * fund_df.loc[fund_df.signal==1,"total_units"].iloc[-1]
                fund_df.loc[i,"total_units"] = 0
                fund_df.loc[i,"market_value"] = 0
                fund_df.loc[i,"avg_unit_cost"] = 0
                fund_df.loc[i,"return_rate"] = np.round((fund_df.loc[i,"unit_nav"]*0.995)/fund_df.loc[fund_df.signal==1,"avg_unit_cost"].iloc[-1] -1,4)
                fund_df.loc[i,"round"] = round_num
                
                round_on = False


    # 结果指标的计算
    action_rounds = fund_df['round'].value_counts().index
    result_list = []
    for action_round in action_rounds:
        round_df = fund_df.loc[fund_df["round"]==action_round].reset_index(drop=True)
        is_sell = True if round_df.loc[len(round_df)-1,'signal']== 0 else False
        #判断该定投轮次是否已卖出
        if is_sell:
            start_date = round_df.loc[0,'trade_date']
            n_period = len(round_df) - 1
            end_date = round_df.loc[n_period,'trade_date']
            avg_unit_cost = round_df.loc[n_period-1,"avg_unit_cost"]
            final_unit_nav = round_df.loc[n_period,"unit_nav"]
            return_rate = round_df.loc[n_period,"return_rate"]
        else:
            start_date = round_df.loc[0,'trade_date']
            n_period = len(round_df)
            end_date = round_df.loc[n_period-1,'trade_date']
            avg_unit_cost = round_df.loc[n_period-1,"avg_unit_cost"]
            final_unit_nav = round_df.loc[n_period-1,"unit_nav"]
            return_rate = round_df.loc[n_period-1,"return_rate"]
        result_list.append([start_date,end_date,n_period,avg_unit_cost,final_unit_nav,return_rate])
    
    result_df = pd.DataFrame(result_list,columns=['起始日','截止日','投资期数','单位平均成本','期末单位净值','总收益率'])
    display(result_df)
    y_rate = ((result_df["投资期数"]*result_df["总收益率"]).sum()/result_df["投资期数"].sum()+1)**(1/7)-1
    income = (np.sum(result_df["投资期数"]*result_df["总收益率"])*5000)
    print("年化收益率:{:.2f}%".format(y_rate*100))
    print("绝对收益:{}".format(income))

    if plot:
        fig,ax1 = plt.subplots(figsize=(12,8))
        fig.text(x=0.1, y=0.92, s='              {low}%-{high}%区间定投结果           '.format(low=v_low,high=v_high), fontsize=32,
                 weight='bold', color='white', backgroundcolor='#3c7f99')
        hq_plot = fund_df[["trade_date","unit_nav"]]
        x1 = hq_plot["trade_date"].values
        y1 = hq_plot["unit_nav"].values
        buy_signal = fund_df.loc[fund_df.signal==1,["trade_date","unit_nav"]]
        x2 = buy_signal["trade_date"].values
        y2 = buy_signal["unit_nav"].values
        sell_signal = fund_df.loc[fund_df.signal==0,["trade_date","unit_nav"]]
        x3 = sell_signal["trade_date"].values
        y3 = sell_signal["unit_nav"].values

        # 绘制基金净值走势
        # 绘制买点和卖点
        ax1.plot(x1,y1,linewidth=1.5,label='指数走势')
        ax1.scatter(x2,y2,marker="^",c = "r",label = "买点")
        ax1.scatter(x3,y3,marker="v",c="g",label = "卖点")
        ax1.spines['top'].set_visible(False)
        ax1.spines['bottom'].set_visible(False)
        ax1.set_ylabel("基金净值",fontdict={"size":16})
        ax1.tick_params(axis='x',length=0,labelsize=16)
        ax1.tick_params(axis='y',labelsize=16)
        ax1.margins(0.01,0.02)
        ax1.legend(fontsize=14)

        # 绘制每日的估值百分位数,并用蓝色阴影部分表示
        ax2 = ax1.twinx()
        ax2.plot(x1,fund_df["percentile"].values,color="#87cefa",alpha=0.1,label="近七年PE")
        ax2.fill_between(x1,0,fund_df["percentile"].values,color = "#87cefa",alpha=0.2)
        ax2.spines['top'].set_visible(False)
        ax2.spines['bottom'].set_visible(False)
        ax2.set_ylabel("估值百分位数",fontdict={"size":16,'rotation':270},labelpad=20)
        ax2.tick_params(axis='x',length=0,labelsize=16)
        ax2.tick_params(axis='y',labelsize=16)
        ax2.set_yticklabels(['' ,'0%','20%','40%','60%','80%'])
        ax2.margins(0.01,0.02)

        # 用黄色阴影表示定投轮次
        for i in range(len(result_df)):
            round_start = result_df.loc[i,'起始日']
            round_end = result_df.loc[i,'截止日']
            date_span = pd.date_range(round_start, round_end)
          
        	ax1.fill_between(date_span,np.min(y1),np.max(y1),facecolor="#ffff4d",alpha=0.2)

        plt.show()

注:本文根据数据研究所得,不构成任何投资意见!

-------------------End-------------------

 公众号后台回复「微信群」,将邀请加入读者交流群。

为防意外,Lemon 还有一个小号叫  「柠檬数据」(ID:LemonDataLab),会不定期分享关于数据的故事,也墙裂建议大家一并关注,以防突然某天就和 Lemon 失联了

扫描回复 “600

获取《Python知识手册

对于纳斯达克的日定投、周定投以及月定投这三种不同的定期投资策略,其收益对比取决于市场波动性、投资者的投资成本平均化效果及交易费用等因素。 1. 定期投资的概念在于无论市场价格如何变动,都在固定的时间间隔以固定的金额购资产。这种做法可以帮助减少市场择时的风险,并可能降低整体购入成本,尤其是在市场下跌期间。 2. 对于日定投来说,由于频繁地入,可能会好地实现成本平均效应,特别是在市场的短期波动较大的情况下。然而,这也意味着高的累计交易次数,可能导致较高的累积手续费,除非是在不收取每次交易费的情况下进行。 3. 周定投相比日定投减少了每周的交易频率至一次,这样可以在一定程度上平衡成本平均化的利益和交易成本之间的关系。如果市场在一周内的价格变化不大,则周定投的效果可能接近日定投;若一周内出现大幅波动,那么两者的结果则会有差异。 4. 月定投是最少频次的一种方式,适合那些希望简化投资过程或者想要避免过于频繁关注市场的人士。长期来看,月度定投同样能够提供分散投资时机的好处,而且通常因为较低的交易频率而拥有低的成本。 5. 实际上的收益率会受到具体时间段的影响,例如牛市中的持续上升趋势或熊市中的连续下降趋势都会对不同频率的定投产生不一样的影响。此外,还需要考虑所选基金本身的管理费率和其他潜在费用。 综上所述,没有一种定投方法绝对优于其他两种,最佳选择依赖于个人财务状况、投资目标、市场环境和个人偏好。建议投资者根据自身情况进行评估,并参考历史数据模拟测试来决定最适合自己的投资节奏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值