A股月份效应的研究——基于python量化视角(backtrader回测)

A股月份效应的研究

前言

《易经》早就揭示出:物极必反,盛极必衰!

阴阳总是不断交替的。股票市场也一样,涨跌互现,涨多了会出现调整,跌多了会出现反弹,因此我们看到K线组合总是红(阳)绿(阴)相间的。

正是由于市场行情总是阴阳交替出现,交易者们才孜孜不倦地想通过择时(选股)来获取超额收益。指数的走势是各方资金博弈的结果,而博弈的过程存在一个时间的延续性,也就是说过去的走势对未来走向有一定的参考价值。

尽管过去不能代表未来,但统计发现历史总是“惊人的相似”,比如“月份效应”。实际上,不少实证研究发现大多数市场存在“月份效应”,即存在某个或某些特定月份的平均收益率年复一年显著地异于其他各月平均收益率的现象。

一、月度收益率分析

# 月份效应
# 获取数据
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
#正常显示画图时出现的中文和负号
from pylab import mpl
mpl.rcParams['font.sans-serif']=['SimHei']
mpl.rcParams['axes.unicode_minus']=False
def get_daily_ret(security,start_date,end_date):
    df=get_price(security, start_date,end_date, frequency='daily', fields=['open','close','high','low','volume','money'])
    df.index=pd.to_datetime(df.index)
#     计算收益率
    daily_ret=df['close'].pct_change()
#     删除缺失值
    daily_ret=daily_ret.dropna()
    return daily_ret
# 月度收益情况
def plot_monthly_ret(security,title):
    daily_ret=get_daily_ret(security,start_date,end_date)
    monthly_ret=daily_ret.resample('M').apply(lambda x:((1+x).prod()-1))
    plt.rcParams['figure.figsize']=[20,5]
    monthly_ret.plot()
    start=monthly_ret.index[0]
    end=monthly_ret.index[-1]

#显示月收益率大于3/4分位数的点
    dates=monthly_ret[monthly_ret>monthly_ret.quantile(0.75)].index   
    for i in range(0,len(dates)):
        plt.scatter(dates[i], monthly_ret[dates[i]],color='r')
    labs = mpatches.Patch(color='red',alpha=.5, label="月收益率高于3/4分位")
    plt.title(title+'月度收益率',size=15)
    plt.legend(handles=[labs])
    plt.xlabel('时间')
    plt.ylabel('收益率')
    plt.show()
security='000300.XSHG'
start_date='2010-01-01'
end_date='2022-01-01'
plot_monthly_ret(security,'沪深300指数')

在这里插入图片描述
从图中可以看出,沪深300指数的月收益率围绕均线上下绕动。

#月波动率情况
def plot_votil(security,title):
    daily_ret=get_daily_ret(security,start_date,end_date)
    monthly_annu=daily_ret.resample('M').std()*np.sqrt(12)
    plt.rcParams['figure.figsize']=[20,5]
    monthly_annu.plot()
    start=monthly_annu.index[0]
    end=monthly_annu.index[-1]
    dates=monthly_annu[monthly_annu>0.07].index
    for i in range(0,len(dates)-1,3):
        plt.axvspan(dates[i],dates[i+1],color='r',alpha=0.5)
    plt.title(title+'月度收益率标准差',size=15)
    labs=mpatches.Patch(color='red',alpha=.5, label="波动集聚")
    plt.legend(handles=[labs])
    plt.xlabel('交易日')
    plt.ylabel('月度波动率')
    plt.show()

在这里插入图片描述
从图中可以看出,沪深300指数的波动率存在聚集的现象。


def plot_mean_ret(security,title):
    daily_ret=get_daily_ret(security,start_date,end_date)
    monthly_ret=daily_ret.resample('M').apply(lambda x:((1+x).prod()-1))
    mrets=(monthly_ret.groupby(monthly_ret.index.month).mean()).round(2)
    x=list(mrets.index)
    y=list(mrets.values)
      
    
    plt.rcParams['font.sans-serif']=['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    plt.figure(figsize=(16,8))

    ax=plt.gca()  
    #设置图片的右边框和上边框为不显示
    ax.spines['right'].set_color('none')
    ax.spines['left'].set_color('none')
    ax.spines['bottom'].set_color('none')
    ax.spines['top'].set_color('none')

    # 百分比设置
    from matplotlib.ticker import PercentFormatter
    plt.gca().yaxis.set_major_formatter(PercentFormatter(1))

    # 坐标轴刻度大小
    #设置x轴
    plt.xticks(fontname="SimHei",fontsize=15,rotation=0)
    plt.yticks(fontname="SimHei",fontsize=15,rotation=0)
    bar=plt.bar(x,y,0.3,color='r')
    plt.ylabel('月平均收益率',fontsize=15)
    plt.xlabel('月份',fontsize=15)
    

    
    plt.title(title+'月平均收益率',fontsize=15)
    return bar,mrets

在这里插入图片描述
从图中可以看出,平均月份的收益率均值也存在较大的差异,由此大致看出,月份效应确实存在。

二、月份效应策略构建

# 月份均值
def monthly_ret_stats(security,title):
    daily_ret=get_daily_ret(security,start_date,end_date)
    monthly_ret=daily_ret.resample('M').apply(lambda x:((1+x).prod()-1))
    ret_stats=monthly_ret.groupby(monthly_ret.index.month).describe()
    pnm=ret_stats[ret_stats['mean']>0.01].index.to_list()
    nnm=ret_stats[ret_stats['mean']<-0.01].index.to_list()
    return pnm,nnm

构建一个简单的月度择时策略并进行历史回测。即先对指数历史数据进行统计分析,计算月度收益率的历史均值,当月度收益率均值大于1%时做多当月,当月度收益率均值小于-1%时做空当月。

首先计算出看多月份和看空月份,看多月份为[2, 4, 10, 12], 看空月份为[1, 6, 8]。

接下来设置模拟的交易策略

def Month_Strategy(security,is_short):
    daily_ret = get_daily_ret(security,start_date,end_date)
    #月度收益率
    mnthly_ret = daily_ret.resample('M').apply(lambda x : ((1+x).prod()-1))
    #设计买卖信号
    df=pd.DataFrame(mnthly_ret.values,index=mnthly_ret.index,columns=['ret'])
    #做多月份
    ret_stats=monthly_ret.groupby(monthly_ret.index.month).describe()
    pnm=ret_stats[ret_stats['mean']>0.01].index.to_list()
    nnm=ret_stats[ret_stats['mean']<-0.01].index.to_list()
      

    print(f'做多月份:{pnm}')
    df['signal']=0
    for m in pnm:
        df.loc[df.index.month==m,'signal']=1
    #如果可以做空
    if is_short==True:
        for n in nnm:
            df.loc[df.index.month==n,'signal']=-1
        print(f'做空月份:{nnm}')

    df['capital_ret']=df.ret.mul(df.signal)
    #计算标的、策略的累计收益率
    df['策略净值']=(df.capital_ret+1.0).cumprod()
    df['指数净值']=(df.ret+1.0).cumprod()
    return df

不能做空时的情况:

title='沪深300指数'
is_short='False'
long=Month_Strategy(security,is_short)
long

在这里插入图片描述
可以做空的情况:

title='沪深300指数'
is_short=True
long_short=Month_Strategy(security,is_short)
long_short

在这里插入图片描述
将两者进行对比可以发现,做空策略下的收益率更高一些

plt.figure(figsize=(12,10))
plt.plot(long['策略净值'],label='不可做空下的策略净值')

plt.plot(long_short['策略净值'],label='可做空下的策略净值')
plt.plot(long['指数净值'],label='指数净值')
plt.legend()
plt.show()

在这里插入图片描述

三、backtrader回测

发现对沪深300指数进行月份策略有不错的收益率,那么接下来实盘操作一下,以沪深300指数的场内连接基金(ETF)为例进行实盘回测。

from datetime import datetime
import backtrader as bt
# 通过数据库获取数据
def get_data(security,start_date,end_date):
    df = get_price(security, start_date, end_date, frequency='daily')
    
    df['daily_ret']=df.close.pct_change()
    month_ret=df.daily_ret.resample('M').apply(lambda x : ((1+x).prod()-1))
    df['month']=month_ret
    df=df.dropna()
    df['openinterest']=0
    df=df[['open','high','low','close','volume','openinterest','month']] #按照back trader数据格式进行整合
    return df
security='510300.XSHG'
start_date='2010-01-01'
end_date='2022-01-01'
stock_df=get_data(security,start_date,end_date)
stock_df

数据如下所示:
在这里插入图片描述

# 将数据导入到back trader中
fromdate=datetime(2010,1,1)
todate=datetime(2021,12,31)
data=bt.feeds.PandasData(dataname=stock_df,fromdate=fromdate,todate=todate)

将做多和做空的月份安排如下:

# 做多月份:[2, 4, 10, 12]
# 做空月份:[1, 6, 8]

构建回测策略:

class TestStrategy(bt.Strategy):
    
    def log(self,txt,dt=None):
        dt=dt or self.datas[0].datetime.date(0)
        print('%s,%s'%(dt.isoformat(),txt))
        
    def __init__(self):
        self.dataclose = self.datas[0].close
        self.datatime=self.datas[0].datetime.date(0)
        self.order=None  #跟踪挂单
        self.buyprice = None
        self.buycomm = None #加入手续费
        
    def notify_order(self,order):
        if order.status in [order.Submitted,order.Accepted]: #经纪商提交/接受/接受的买入/卖出订单 
            return
        if order.status in [order.Completed]: ## 检查订单是否完成
                                              # 注意:如果没有足够的现金,经纪人可能会拒绝订单
            
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
                         
            else:
                 self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                          (order.executed.price,order.executed.value,order.executed.comm))
                      
            self.bar_executed=len(self)
            
        elif order.status in [order.Canceled,order.Margin,order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
            
        self.order=None #写下:无挂单
    def next(self):
        self.log('Close,%.2f'%self.dataclose[0])
        if self.order: ## 检查订单是否处于待处理状态...如果是,我们不能发送第二个
            return
        if not self.position: #检查我们是否在市场上
            for m in [2, 4, 10, 12]:
                if self.datas[0].datetime.date(0).month==m:
                    self.log('BUY CREATE,%.2f'%self.dataclose[0])
                    self.order=self.buy() #跟踪创建的订单以避免第二个订单
        else:
            for n in [1, 6, 8]:
                if self.datas[0].datetime.date(0).month==n:
                    self.log('SELL CREATE,%.2f'%self.dataclose[0])
                    self.order=self.sell()

进化回测

import backtrader.analyzers as btanalyzers
import backtrader.feeds as btfeeds
import backtrader.strategies as btstrats

if __name__== '__main__':
    cerebro=bt.Cerebro()
    cerebro.addstrategy(TestStrategy)
    cerebro.adddata(data)
    cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='mysharpe')

    cerebro.broker.setcash(20000.0)
    cerebro.broker.setcommission(commission=0.001) 
    print(f'组合初始价值:%.2f'%cerebro.broker.getvalue())
#     运行broker
    thestrats = cerebro.run()
    print(f'组合期末价值:%.2f'%cerebro.broker.getvalue())
    cerebro.plot()
    thestrat = thestrats[0]

    print('Sharpe Ratio:', thestrat.analyzers.mysharpe.get_analysis())

回测结果如下所示:


不出意外就要出意外了


回测的具体买卖阶段如下,可以看出这样的回测结果,是不理想的,基本没有盈利,余额宝也没有跑赢。

组合初始价值:20000.00
2012-05-31,Close,2.26
2012-07-31,Close,2.04
2012-08-31,Close,1.93
2012-10-31,Close,1.96
2012-10-31,BUY CREATE,1.96
2012-11-30,BUY EXECUTED, Price: 1.85, Cost: 1.85, Comm 0.00
2012-11-30,Close,1.86
2012-12-31,Close,2.20
2013-01-31,Close,2.34
2013-01-31,SELL CREATE,2.34
2013-02-28,SELL EXECUTED, Price: 2.27, Cost: 1.85, Comm 0.00
2013-02-28,Close,2.32
2013-02-28,BUY CREATE,2.32
2013-05-31,BUY EXECUTED, Price: 2.30, Cost: 2.30, Comm 0.00
2013-05-31,Close,2.27
2013-07-31,Close,1.95
2013-09-30,Close,2.15
2013-10-31,Close,2.12
2013-12-31,Close,2.07
2014-02-28,Close,1.93
2014-03-31,Close,1.91
2014-04-30,Close,1.91
2014-06-30,Close,1.94
2014-06-30,SELL CREATE,1.94
2014-07-31,SELL EXECUTED, Price: 2.11, Cost: 2.30, Comm 0.00
2014-07-31,Close,2.13
2014-09-30,Close,2.22
2014-10-31,Close,2.27
2014-10-31,BUY CREATE,2.27
2014-12-31,BUY EXECUTED, Price: 3.10, Cost: 3.10, Comm 0.00
2014-12-31,Close,3.18
2015-03-31,Close,3.63
2015-04-30,Close,4.26
2015-06-30,Close,4.03
2015-06-30,SELL CREATE,4.03
2015-07-31,SELL EXECUTED, Price: 3.46, Cost: 3.10, Comm 0.00
2015-07-31,Close,3.46
2015-08-31,Close,3.05
2015-09-30,Close,2.90
2015-11-30,Close,3.25
2015-12-31,Close,3.40
2015-12-31,BUY CREATE,3.40
2016-02-29,BUY EXECUTED, Price: 2.67, Cost: 2.67, Comm 0.00
2016-02-29,Close,2.62
2016-03-31,Close,2.93
2016-05-31,Close,2.90
2016-06-30,Close,2.89
2016-06-30,SELL CREATE,2.89
2016-08-31,SELL EXECUTED, Price: 3.08, Cost: 2.67, Comm 0.00
2016-08-31,Close,3.10
2016-09-30,Close,3.04
2016-10-31,Close,3.10
2016-10-31,BUY CREATE,3.10
2016-11-30,BUY EXECUTED, Price: 3.31, Cost: 3.31, Comm 0.00
2016-11-30,Close,3.29
2017-02-28,Close,3.20
2017-03-31,Close,3.21
2017-05-31,Close,3.24
2017-06-30,Close,3.42
2017-06-30,SELL CREATE,3.42
2017-07-31,SELL EXECUTED, Price: 3.51, Cost: 3.31, Comm 0.00
2017-07-31,Close,3.52
2017-08-31,Close,3.61
2017-10-31,Close,3.77
2017-10-31,BUY CREATE,3.77
2017-11-30,BUY EXECUTED, Price: 3.81, Cost: 3.81, Comm 0.00
2017-11-30,Close,3.77
2018-01-31,Close,4.01
2018-01-31,SELL CREATE,4.01
2018-02-28,SELL EXECUTED, Price: 3.78, Cost: 3.81, Comm 0.00
2018-02-28,Close,3.77
2018-02-28,BUY CREATE,3.77
2018-05-31,BUY EXECUTED, Price: 3.52, Cost: 3.52, Comm 0.00
2018-05-31,Close,3.57
2018-07-31,Close,3.36
2018-08-31,Close,3.18
2018-08-31,SELL CREATE,3.18
2018-10-31,SELL EXECUTED, Price: 2.98, Cost: 3.52, Comm 0.00
2018-10-31,Close,3.01
2018-10-31,BUY CREATE,3.01
2018-11-30,BUY EXECUTED, Price: 3.00, Cost: 3.00, Comm 0.00
2018-11-30,Close,3.04
2019-01-31,Close,3.06
2019-01-31,SELL CREATE,3.06
2019-02-28,SELL EXECUTED, Price: 3.51, Cost: 3.00, Comm 0.00
2019-02-28,Close,3.50
2019-02-28,BUY CREATE,3.50
2019-04-30,BUY EXECUTED, Price: 3.72, Cost: 3.72, Comm 0.00
2019-04-30,Close,3.73
2019-05-31,Close,3.48
2019-07-31,Close,3.73
2019-09-30,Close,3.70
2019-10-31,Close,3.77
2019-12-31,Close,3.98
2020-03-31,Close,3.57
2020-04-30,Close,3.80
2020-06-30,Close,4.06
2020-06-30,SELL CREATE,4.06
2020-07-31,SELL EXECUTED, Price: 4.58, Cost: 3.72, Comm 0.00
2020-07-31,Close,4.62
2020-08-31,Close,4.74
2020-09-30,Close,4.51
2020-11-30,Close,4.89
2020-12-31,Close,5.14
2020-12-31,BUY CREATE,5.14
2021-03-31,BUY EXECUTED, Price: 5.00, Cost: 5.00, Comm 0.01
2021-03-31,Close,4.97
2021-04-30,Close,5.05
2021-05-31,Close,5.26
2021-06-30,Close,5.17
2021-06-30,SELL CREATE,5.17
2021-08-31,SELL EXECUTED, Price: 4.81, Cost: 5.00, Comm 0.00
2021-08-31,Close,4.80
2021-09-30,Close,4.86
2021-11-30,Close,4.84
2021-12-31,Close,4.93
2021-12-31,BUY CREATE,4.93
组合期末价值:20001.73

不敢相信的夏普比率

Sharpe Ratio: OrderedDict([('sharperatio', -557.5313463920106)])

在这里插入图片描述
回测的买卖图如上。

总结

对A股的月份效应进行了探索,根据沪深300指数的模拟能够取得十分不错的收益,但是基于ETF基金的回测,则效果十分不理想。

后续有待改进该思路。