估值择时对ETF基金定投的影响

前面的文章中我们分析了ETF基金在不同时间周期的定投,周内定投等几种情况,今天我们来看一下择时对于ETF基金定投的影响。

按照前面一贯的思路,我们还是采用傻瓜定投的方式,就是在符合预设条件的时候买入,然后一直持仓至回测结束。

一、择时策略

基本思路就是在估值位于低位时多投,设定较大的投入比例;在估值位于高位时少投,设定较小的比例。

比例设定分为如下两种:

(1)估值低位设置高定投比例,估值高位设置低定投比例

PE百分位

<=20%

20~50%

50~80%

>80%

定投比例1

1.5

1.2

0.8

0.5

(2)估值低位设置高定投比例,估值高位时不投

PE百分位

<=20%

20~50%

50~80%

>80%

定投比例2

1.5

1.2

0.5

0

定投比例3

2.0

1.5

0.5

0

指数低估和高估的判断采用PE历史百分位方法。大家都知道PE有静态PE、动态PE和滚动PE三种(分不清这三个区别的看这里),这里我们采用滚动市盈率,也就是PE-TTM。

通过判断大盘指数当前PE-TTM的历史分位数来确定当前的估值是高还是低。这个历史分位数的计算可以通过DataFrame的rank()函数来实现。

df.rank(axis=0,method='average',numeric_only=None,na_option='keep',ascending=True,pct=False)

这里主要使用的参数有以下两个:

ascending:排序方法,True/False

pct:百分比显示排名,True/False

其他的参数和用法可以看这篇文章《python pandas rank()详解》,讲的非常详细。

测试用的代码如下:

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


# PE百分位低时多买入,高时少买入
def strategy(index, etf, type):
    # sz50数据有缺失
    # diff1 = set(etf50['date'])-set(sz50['date'])
    # print(diff1)
    # print((len(sz50), len(etf50)))
    stock_data = pd.DataFrame({'date': etf['date'], 'index_close': index['close'],
                               'index_pettm': index['averageTtmPe'],
                               'index_pepercent': index['pct'],
                               'open': etf['open'], 'close': etf['close'],
                               'change': etf['change']})
    stock_data['index_change'] = stock_data['index_close'].pct_change()
    stock_data.index = range(len(stock_data))
    if type == '高位少投':
        # 策略1:低位多投高位少投策略
        stock_data.loc[stock_data['index_pepercent'] <= 0.2, 'rate'] = 1.5
        stock_data.loc[(stock_data['index_pepercent'] > 0.2) & (stock_data['index_pepercent'] <= 0.5), 'rate'] = 1.2
        stock_data.loc[(stock_data['index_pepercent'] > 0.5) & (stock_data['index_pepercent'] <= 0.8), 'rate'] = 0.8
        stock_data.loc[stock_data['index_pepercent'] > 0.8, 'rate'] = 0.5
        stock_data['datex'] = pd.to_datetime(stock_data['date'])
        stock_data.loc[stock_data['datex'].dt.dayofweek == 4, 'sig'] = 1  # 0~4表示周一~周五
    elif type == '高位不投':
        # 策略2:低位多投高位不投
        stock_data.loc[stock_data['index_pepercent'] <= 0.2, 'rate'] = 1.5
        stock_data.loc[(stock_data['index_pepercent'] > 0.2) & (stock_data['index_pepercent'] <= 0.4), 'rate'] = 1.2
        stock_data.loc[(stock_data['index_pepercent'] > 0.4) & (stock_data['index_pepercent'] <= 0.6), 'rate'] = 0.5
        stock_data.loc[stock_data['index_pepercent'] > 0.6, 'rate'] = 0
        stock_data['datex'] = pd.to_datetime(stock_data['date'])
        stock_data.loc[stock_data['datex'].dt.dayofweek == 4, 'sig'] = 1  # 0~4表示周一~周五

    #策略3:
    global amount, commis_rate, slippage_rate
    old_pos_big = 0
    old_invest = 0
    stock_data['poschg'] = 0
    stock_data['pos'] = 0
    stock_data['invest'] = 0
    i = 0
    while i < len(stock_data):  # 由于市盈率为当天收盘后才能确定,因此要下一日才能进行定投
        if stock_data.loc[i, 'sig'] == 1:  # i+1日买入etf300
            # print(i,stock_data.loc[i, 'position'])
            rate = stock_data.loc[i, 'rate']
            stock_data.loc[i, 'poschg'] = old_pos_big
            stock_data.loc[i, 'invest'] = old_invest  # 累计投入金额
            old_pos_big = amount * rate * (1 - commis_rate - slippage_rate) / stock_data.loc[i, 'open']  # 定投日增加固定金额
            old_invest += amount * rate
            i += 1
        else:
            stock_data.loc[i, 'poschg'] = old_pos_big
            stock_data.loc[i, 'invest'] = old_invest
            old_pos_big = 0
            i += 1
    stock_data['pos'] = stock_data['poschg'].cumsum()  # 累计申购份额
    # stock_data['invest_big'] = amount * stock_data['big_sig'].cumsum()  # 累计投入金额
    stock_data['current'] = stock_data['close'] * stock_data['pos']  # 累计现值
    stock_data['capital'] = stock_data['current'] / stock_data['invest']  # 累计净值
    stock_data['capital_rtn'] = stock_data['capital'].pct_change()  # 日涨跌幅
    # 处理第一行数据为NAN问题
    stock_data['capital'] = stock_data['capital'].fillna(1)  # 将资金列的NAN改为1
    stock_data['capital_rtn'] = stock_data['capital_rtn'].fillna(0)  # 将资金列的NAN改为0

    buy_act = stock_data[stock_data['datex'].dt.dayofweek == 4]  # 0~4表示周一~周五
    buy_act.to_excel("buy.xlsx", sheet_name='trade', index=False)
    stock_data.to_excel("trade.xlsx", sheet_name='trade', index=False)
    return stock_data, buy_act



# 计算最大回撤
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


def 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 - lagPrice
    moment = moment.dropna()

    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    plt.subplot(211)
    plt.plot(price, 'b*')
    plt.xlabel('date')
    plt.ylabel('Close')
    plt.grid(True)
    plt.title('收盘价时序图(上)&{}日动量图(下)'.format(period))


# ====设置回测参数
trade_day = 244  # 每年平均交易日天数
n_short = 10  # 双均线短周期
n_long = 30  # 双均线长周期
s_date = '20171201'  # 回测开始日期
e_date = '20220901'  # 回测开始日期
# s_date = '20180101'  # 回测开始日期
# e_date = '20220309'  # 回测开始日期
amount = 1000
slippage_rate = 0.1 / 1000  # 滑点率
commis_rate = 5 / 10000  # 交易费率

# 获取沪深300指数的PE值
# df = ak.index_value_name_funddb()  # 无法正常返回?
# df.to_csv('index.csv', index=False, mode='w', encoding='gbk')
# df = ak.index_value_hist_funddb(symbol="沪深300", indicator="市盈率")  # 无法正常返回?
# df.to_csv('大盘成长.csv', index=False, mode='w', encoding='gbk')
# df = ak.stock_a_ttm_lyr()  # 获得的数据不完整?很多日期缺失
# df.to_excel("stock_ttm_lyr.xlsx", sheet_name='sheet1', index=False)
# df.to_csv('stock_ttm_lyr.csv', index=False, mode='w', encoding='gbk')
# df = ak.stock_a_pe(symbol="all")  # 数据缺失较多
# df.to_csv('A股市盈率.csv', index=False, mode='w', encoding='gbk')
# df = ak.stock_a_pe(symbol="000300.XSHG")  # 数据缺失较多
# df.to_csv('000300市盈率.csv', index=False, mode='w', encoding='gbk')
# df = ak.stock_a_pe_and_pb(symbol="000300.SH")  # 只有20171106以后的数据
# df.to_csv('000300市盈率.csv', index=False, mode='w', encoding='gbk')
# df = ak.stock_a_pe_and_pb(symbol="000852.SH")  # 只有20171106以后的数据
# df.to_csv('000852市盈率.csv', index=False, mode='w', encoding='gbk')
# df=ak.fund_etf_hist_sina(symbol='sh510300')
# df.to_csv('510300.csv', index=False, mode='w', encoding='gbk')
# df=ak.fund_etf_hist_sina(symbol='sh512100')
# df.to_csv('512100.csv', index=False, mode='w', encoding='gbk')

index_code = '000300'
etf_code = '510300'
index_code = '000852'
etf_code = '512100'
df_hs300 = pd.read_csv(index_code+'市盈率.csv', encoding='gbk')  # 数据不完整?
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_hs300['change'] = df_hs300['close'].pct_change()
# maxpe = df_hs300['averageTtmPe'].max()
# minpe = df_hs300['averageTtmPe'].min()
# df_hs300['quantileInAllHistoryAveragePeTtm'] = (df_hs300['averageTtmPe'] - minpe) / (maxpe - minpe)
df_hs300['pct'] = df_hs300['averageTtmPe'].rank(ascending=True, pct=True)
# df_hs300.to_excel("hs300.xlsx", sheet_name='Sheet1', index=False)
df_etf300 = pd.read_csv(etf_code+'.csv', encoding='gbk')
# cols_etf = ['date', 'code', 'name', 'open', 'high', 'low', 'close', 'volume']
cols_etf = ['date', 'open', 'high', 'low', 'close', 'volume']
df_etf300.columns = cols_etf
df_etf300['change'] = df_etf300['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()


# ====根据策略,计算仓位,资金曲线等
# 计算买卖信号
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()
etf300 = df_etf300[df_etf300['date'] >= start_date.strftime('%Y-%m-%d')].copy()
etf300 = etf300[etf300['date'] <= end_date.strftime('%Y-%m-%d')].copy()
type = '高位少投'
type = '高位不投'
(df, buy_act) = strategy(hs300, etf300, type)

# ====根据策略结果,计算评价指标
# 计算股票和策略年收益
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)
# 计算策略和股票的年胜率
yearly_win_rate = len(year_rtn[year_rtn['capital_rtn'] > 0]) / len(year_rtn[year_rtn['capital_rtn'] != 0])
yearly_win_rates = len(year_rtn[year_rtn['change'] > 0]) / len(year_rtn[year_rtn['change'] != 0])
date_list = list(df['date'])
capital_list = list(df['capital'])
index_list = list(df['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)) - 1
stock_annual = (dft['close'].iloc[-1] / dft['close'].iloc[0]) ** (trade_day / len(rng)) - 1
# 计算年化收益率
buy_sig = buy_act[buy_act['rate'] > 0]
invest_num = len(buy_sig)
tl = list(buy_sig['invest'] - buy_sig['invest'].shift(1))
tl1 = [-l for l in tl]
pl = [-amount] * invest_num  # 建立irr计算用list
pl[0:invest_num-1] = tl1[1:len(tl1)]
pl[invest_num-1] = df['current'].iloc[-1]  # 资金现值
month_capital = npf.irr(pl)  # 计算收益
capital_annual = pow((1 + month_capital), 52)-1  # 根据周收益计算年化收益
capital_drawdown = max_drawdown(date_list, capital_list)
stock_drawdown = max_drawdown(date_list, index_list)
tt = drawdown(date_list, capital_list)
print("回测区间:%s-%s" % (s_date, e_date))
print('定投次数:%d  投入资金:%d  账户现值:%d' % (invest_num, df['invest'].iloc[-1], df['current'].iloc[-1]))
print('策略累积收益:%f  股票累积收益:%f' % (capital_cum, stock_cum))
print('策略年化收益:%.2f%%  股票年化收益:%.2f%%' % (round(100*capital_annual, 2), round(100*stock_annual, 2)))
print('策略年胜率:%.2f%%  股票年胜率:%.2f%%' % (round(100*yearly_win_rate, 2), round(100*yearly_win_rates, 2)))
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_stock = capital_annual
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('收益波动率:%.2f%%' % round(100*volatility, 2))
print('阿尔法:%f   贝塔:%f   夏普比率:%f 信息比率:%f' % (a, b, sharpe, info))

# df.to_excel("test.xlsx", sheet_name='600036', index=False)
# 净值曲线图
df['stock'] = (1 + df['index_change']).cumprod()
plt.plot(df['date'], df['stock'], label='Index-'+index_code)
plt.plot(df['date'], df['capital'], label='ETF-'+etf_code)
buy_sig = buy_act[buy_act['rate'] > 0]  # 0~4表示周一~周五
plt.scatter(buy_sig['date'], buy_sig['capital'], marker="^", c='red', label="买点")
plt.ylabel("净值", fontproperties="SimSun")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.title("择时定投%s-%s净值曲线(%s-%s)" % (etf_code, type, 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 + '-' + etf_code + type + '.png')
plt.show()

二、大盘ETF定投结果分析

这里采用的指数是沪深300指数000300,对应的ETF基金是沪深300ETF基金510300。定投的频率是每周一次,根据前面的周内效应分析,哪天定投其实差别不大,这里选择的是每个周五检查大盘指数的PE百分位,然后下一个交易日以开盘价买入。

回测中每次定投的金额为1000元乘以设定比例,滑点率为0.01%,交易费率为0.05%。回测区间为2017年12月1日到2022年9月1日。采用傻瓜定投的方式,买入后一直持有,直到回测结束。

三种不同的比例设定方式的测试结果如下:

大盘指数和ETF基金的净值曲线以及定投的买点如下图所示。

从表中数据可以看出,相比于高位少投策略,在估值高位不投,总体资金投入会减少,但收益提高幅度较大。策略的最大回撤也有所下降,与大盘指数回撤比较接近。加大估值低位的投入比例,最终收益提高的幅度很小。

从净值曲线可以看出,低位多投高位少投或不投的策略能够保证定投曲线的收益高于指数收益,高位不投时最终的收益更好。

三、小盘ETF定投结果分析

这里采用的小盘指数是沪深300指数000852,对应的ETF基金是中证1000ETF基金512100。其它参数与大盘指数回测相同。

三种不同的比例设定方式的测试结果如下:

小盘指数和ETF基金的净值曲线以及定投的买点如下图所示:

从表中数据可以看出,小盘指数与大盘指数的情况类似。相比于高位少投策略,在估值高位不投,总体资金投入会减少,但收益会大幅度提高。策略的最大回撤变化不大,均小于小盘指数的回撤。同样,加大估值低位的投入比例,最终收益提高的幅度也很小。

从净值曲线可以看出,低位多投高位少投或不投的策略能够保证定投曲线的收益高于指数收益,高位不投时最终的收益更好。

另外,与大盘指数ETF比起来,小盘指数ETF定投的收益更高,与前面的大小盘轮动定投测试结果一致。

四、数据完整性问题

采用AKShare获取指数的市盈率数据出现了很多问题,记录如下:

(1)函数index_value_name_funddb()和index_value_hist_funddb()都无法正常返回数据,看错误提示可能是数据源改了格式,导致获得的结果无法正确解析。

(2)stock_ttm_lyr()函数可以获取全部A股指数或者特定指数的市盈率数据,时间是从2005年1月5日开始的。这个函数的好处是获得的数据已经帮我们把各种PE及其历史百分位计算好了。但是计算到中间跟ETF数据对比的时候,发现大盘指数有很多交易日的数据不存在。

df = ak.stock_a_ttm_lyr()

(3)stock_a_pe()函数获得的A股数据和沪深300数据是从2005年1月5日开始的,但是也有同样的问题,很多交易日的数据漏掉了。

df = ak.stock_a_pe(symbol="all")

df = ak.stock_a_pe(symbol="000300.XSHG")

(4)stock_a_pe_and_pb()函数获取的沪深300指数数据比较完整,但是只有2017年11月6日以后的。

df = ak.stock_a_pe_and_pb(symbol="000300.SH")

由于一下子也没办法找到其他合适的数据源来补齐这些数据,因此回测中使用的是stock_a_pe_and_pb()函数获取的数据,然后把回测开始时间调整到2017年12月1日开始。

由于数据缺失问题,本次测试只完成了5年定投的测试。后续考虑将指数缺失数据补齐,测试一下更长时间的结果如何。大家有兴趣也可以试一试不同的定投比例或者不同的估值方式对定投的影响。

-----------------------------------

原创不易,请多支持

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值