ETF定投的均线偏离策略分析

最近看到一个根据均线偏离度来进行投资的策略,今天在ETF基金定投中测试一下看看有无效果。

一、均线偏离策略

均线偏离策略是通过短期收盘价偏离长期均线的程度来设定不同的定投比例。

均线偏离程度是用偏离百分比来衡量,均线偏离百分比是指当前收盘价与长期均价差值占长期均价的百分比。长期均价为N日移动平均收盘价价格,N可以去不同的时间长度。

策略的思路是:根据收盘价偏离长期均线的程度不同,设置不同的定投投比例。

为了对比不同的偏离程度对定投的影响,采用如下两种方式:

(1)在偏离程度为负时将比例设的较大,负向偏离越远定投比例越大;在偏离程度为正时将定投比例设的较小,正向偏离越远定投比例越小。

(2)将正负偏离程度相同的定投比例设为一致。

定投比例设置如下表所示:

均线偏离度

<=-1.2

-1.2~-0.8

-0.8~-0.5

-0.5~0

0~0.5

0.5~1

1~2

>2

定投比例1

2.0

1.8

1.5

1.2

1.0

0.8

0.5

0

定投比例2

0.3

0.5

0.8

1.0

0.8

0.5

0.3

0

二、结果分析

本次测试直接采用的标的有跟踪大盘指数的沪深300ETF基金510300和跟踪创业板指数的创业板ETF基金159915。延续一致以来的习惯,还是采用傻瓜定投策略,即定时定额购买以上标的并一直持有至回测结束,定时每月的第一个交易日进行定投。

为了方便对比,这里首先采用5年定投方式,时间从2012年1月1日开始到2016年12月31日结束,采用的费率为0.05%,没有印花税。

代码如下:

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 get_rate(data):  # 比例1
#     if data > 2:
#         return 0.0
#     elif data > 1:
#         return 0.5
#     elif data > 0.5:
#         return 0.8
#     elif data > 0:
#         return 1.0
#     elif data > -0.5:
#         return 1.2
#     elif data > -0.8:
#         return 1.5
#     elif data > -1.2:
#         return 1.8
#     else:
#         return 2.0

def get_rate(data):  #比例2
    if data > 2:
        return 0.0
    elif data > 1:
        return 0.3
    elif data > 0.5:
        return 0.5
    elif data > 0:
        return 0.8
    elif data > -0.5:
        return 1.0
    elif data > -0.8:
        return 0.8
    elif data > -1.2:
        return 0.5
    else:
        return 0.3


# 计算定投策略并得到买卖信号和仓位
def period_invest(stock_data, type='M', n=500):
    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['short'] = stock_data['close'].shift(1)
    stock_data['long'] = stock_data.close.rolling(n).mean()
    # NAN值如何处理?填1??
    stock_data['price_div'] = stock_data['short']/stock_data['long']-1
    stock_data['price_div'].fillna(3, inplace=True)
    # stock_data = stock_data.reset_index(drop=True)
    stock_data['rate'] = stock_data['price_div'].apply(lambda x: get_rate(x))

    # 定投购买指数基金
    stock_data['poschg'] = 0
    stock_data['OP_SIG'] = 0
    stock_data['invest'] = 0
    old_invest = 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
        i += 1
    i = 0
    stock_data.index = range(len(stock_data))
    while i < len(stock_data):
        if stock_data.loc[i, 'OP_SIG'] == 1:
            price = stock_data.loc[i, 'open']
            rate = 1  # stock_data.loc[i, 'rate']
            # print(date, price, rate, amount * rate * (1 - comm_rate) / price)
            stock_data.loc[i, 'poschg'] = amount * rate * (1 - comm_rate) / price  # 定投日增加固定金额
            old_invest += amount * rate
            if abs(rate) < 0.1:
                stock_data.loc[i, 'OP_SIG'] = 0
            # print('%f,%f,%f' % (rate, amount, old_invest))
        stock_data.loc[i, 'invest'] = old_invest  # 累计投入金额
        i += 1

    stock_data['position'] = stock_data['poschg'].cumsum()  # 累计申购份额
    # stock_data['invest'] = amount * stock_data['rate'] * 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()  # 日涨跌幅
    stock_data['capital'].fillna(1, inplace=True)
    # 处理第一行数据为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_small = pd.read_csv(code+'.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'  # 回测结束日期
# slippage_rate = 0.1 / 1000  # 滑点率
comm_rate = 5 / 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', 250)

# 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)
df['invest_chg'] = df['invest']-df['invest'].shift(1)
df['invest_chg'].fillna(amount, inplace=True)
pll = df.loc[df['OP_SIG'] == 1, 'invest_chg']
pll = -pll
invest_num = len(df[df['OP_SIG'] == 1])
fund = df['invest'].iloc[-1]
pl = [-1000] * (invest_num + 1)  # 建立irr计算用list
pl[:len(pll)] = pll[:len(pll)]
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('策略年化收益:%.2f%%  基金年化收益:%.2f%%' % (round(100*capital_annual, 2), round(100*stock_annual, 2)))
print('策略最大回撤:%f,开始日期:%s,结束日期:%s' % capital_drawdown)
print('股票最大回撤:%f,开始日期:%s,结束日期:%s' % stock_drawdown)


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()



1.不同定投比例及定投年限的影响

对于创业板ETF和沪深300ETF,在长期均价参数N分别等于500、250和180天的回测结果如下:

从表中可以看出,定投比例1和比例2对于收益结果的影响非常小。长期均价参数N的取值对于收益结果的影响较大,取值较大时年化收益较小。主要原因是跟定投实际发生的次数有关,定投次数过少时,长期均价波动加大,会导致整体收益会显著下降。策略对于5年定投最大回撤的影响较小。

另外,长期均线参数N的取值对沪深300ETF和创业板ETF的收益影响不同。对于沪深300ETF,N取250时可以获得最好收益;而对于创业板ETF,N取180时可以获得最好收益。

对于创业板ETF和沪深300ETF,在长期均价参数N分别等于500、250和180天的回测结果如下:

从表中可以看出,定投比例1和比例2对于收益结果的影响同样非常小。长期均价参数N的取值对于收益结果的影响也不大。主要原因应该是在定投次数达到一定界限后,长期均价波动对收益的影响会减弱。同样,策略对于10年定投最大回撤的影响较小。

另外,长期均线参数N的取值对沪深300ETF和创业板ETF的收益影响不同。对于沪深300ETF,N取250时可以获得最好收益;而对于创业板ETF,N取180时可以获得最好收益,N的取值继续降低,收益还会增加,但幅度很小。。

定投5年和10年的数据对比表明,定投时间增加后,长期均价的波动对收益的影响会减弱,年化收益基本上是小数点的区别了。

2.与无策略傻瓜定投的对比

下表是直接每月定投持续5年和10年的收益情况:

对比发现,均线偏离策略在参数设置不好的情况下,5年定投的收益不如每月直接定投;10年定投的收益跟每月直接定投也相差不大。

三、存在的问题

1.扣款比例的设置对定投结果影响较大。

比如扣款比例设置的过于极端,会导致定投的次数很少,结果最终收益会很差。主要原因是定投其实是通过在时间线上的平均分布来分摊风险,因为你既可能买到高价,也可能买到低价,概率上来说是均等的。如果参与次数过少,就会导致购买价格偏向某一个极端,而且这个是不可控的,如果价格偏高总体收益就很低。所以策略的选择需要考虑平衡,避免走极端。因为你错过了最好的收益,意味着大概率你也避开了最坏的收益。

2.过于复杂的策略存在很多意想不到的问题

比如刚开始预想的是当均线负偏离时多投,在正偏离是少投或者不投,结果发现在市场一直处于上升趋势时,基本没有多少定投的机会,然后收益也非常差。

四、结论

最近一段时间把ETF基金定投的各种方式都分析了一下,总体的感觉是,极端的策略不一定好,策略越极端,越有可能是在某些特定场景下才管用,碰到不合适的场景收益可能会很差。而且策略不是越复杂越好,没准弄了很久的一个策略,得到的结果跟直接定投差不太多,就是瞎折腾☹~~~

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

原创不易,请多支持

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值