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基金的回测,则效果十分不理想。

后续有待改进该思路。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
股票回测量化交易中非常重要的一环,它可以通过历史数据对交易策略进行模拟和评估,从而评估策略的可行性和优劣性。在Python中,有很多开源的量化交易框架可以用来进行股票回测,如zipline、backtrader等。 下面是一个使用zipline框架进行简单交易策略回测的例子: 1. 安装zipline ```python pip install zipline ``` 2. 编写交易策略代码 ```python from zipline.api import order_target_percent, record, symbol def initialize(context): context.asset = symbol('AAPL') def handle_data(context, data): # 获取过去10天的收盘价 prices = data.history(context.asset, 'price', 10, '1d') # 计算平均价 mean_price = prices.mean() # 如果当前价格低于平均价,则买入 if data.current(context.asset, 'price') < mean_price: # 调整持仓比例至100% order_target_percent(context.asset, 1.0) # 否则卖出 else: # 调整持仓比例至0% order_target_percent(context.asset, 0.0) # 记录当前持仓比例 record(position=context.portfolio.positions[context.asset].amount) ``` 3. 运行回测 ```python from zipline import run_algorithm from zipline.api import symbol from datetime import datetime start = datetime(2016, 1, 1) end = datetime(2017, 1, 1) result = run_algorithm( start=start, end=end, initialize=initialize, capital_base=10000, handle_data=handle_data, bundle='quandl' ) ``` 在上述代码中,我们定义了一个简单的交易策略,即如果当前价格低于过去10天的平均价,则买入,否则卖出。然后我们使用zipline框架进行回测,设定回测开始和结束时间、初始资本、数据来源等参数,最终得到回测结果。 需要注意的是,这只是一个简单的例子,实际的交易策略可能会更加复杂,需要考虑更多的因素。另外,在进行股票回测时,也需要注意避免过度拟合或过度优化,以免出现回测虚高的情况。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马尔可夫宽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值