第十篇:动量类多因子策略

导语:作为策略集锦压轴篇,向大家介绍完整的多因子选股策略,结合了光大证券多因子选股研报,希望能在多因子研究模块,给大家打好坚实基础。

一、策略阐述

动量因子

  动量与反转效应是市场上经常出现的一种情况。所谓动量效应就是在前一段时间强势的股票,在未来一段时间继续保持强势;反转效应就是前一段时间弱的股票,未来一段时间会变强。但问题的关健是这个强势和弱势会保持多长时间和多大幅度,这是动量、反转策略需要考虑的关健问题。

  衡量市场、个股的动量效应,需要运用动量因子。以下是光大证券-多因子研究系列(五):动量类因子测试研报中对于动量类因子的定义。
  本篇内容选取PM_1M、PM_3M、PM_6M、PM_9M、PM_12M、PM_12MC6M、PM_12MC1M、PM_6MC1M这8个动量因子,向大家讲述动量因子选股策略。

动量多因子选股

   第一步:以沪深300指数成分股为股票池,计算股票池内个股上月的8个动量因子。

   第二步:计算上个月8个动量因子的超额收益数值,作为本期因子权重值。公式:上月因子超额收益=因子值前10%的个股上月收益率-因子值后10%的个股上月收益率。

   第三步:删除上月无超额收益的动量因子,如果数量超过4个,则本月不再选股,定义为市场当前无明显动量,空仓避险。(择时模块)

   第四步:计算出本月个股本月的8个动量因子,进行因子标准化处理,每个因子乘以上月因子超额收益值,并求和,最终值即为动量alpha值。

   第五步:按alpha值挑选出前20只股票作为持仓。

   注:标准化公式:因子标准值=(因子值-所有个股的因子平均值)/所有个股的因子标准差

动量多因子选股注意点

   1.A股市场历来以反转市著称,因此简单的通过动量选股在很大程度上是违背市场主流的,因此我们在第三步中,特意加入了动量因子择时模块,事先对市场的动量效应进行简单检测,随后选股操作。

   2.考虑到动量类因子中,各因子的值差异严重,因子需要做标准化处理,更好的体现出每个因子的权重。

   3.计算出因子超额收益后,需剔除无超额收益的上月动量因子,以更好体现有效因子的价值。

  以下为策略实现的基本信息:
  策略实现难度:5
  实现过程中所需要用到的API函数,ps:通过SuperMind量化交易平台API文档快速掌握:

需要用到的API函数 功能
init() 初始化函数
handle_bar() 定时运行函数,按交易日或按分钟运行
set_benchmark() 设置基准指数
history() 获取多只股票多属性的历史行情数据

二、代码示意图

三、编写释义

  动量类多因子选股的核心部分是计算因子超额收益,标准化后求出最终alpha值,建议初学者前往研究环境操作。
  当期因子值和当期因子权重计算完毕后需整理成一个DataFrame格式,该部分操作需要同学们熟悉DataFrame的常用操作,以下是作者实现动量类因子选股策略过程,研究环境的代码草稿,分享给同学们。

In [1]:
#===============以沪深300为股票池,除去ST和停牌=====  
def stock(date):  
    stk=list(get_index_stocks('000300.SH',date))
    price=get_price(stk, None, date, '1d', ['is_paused', 'is_st'], False, None, 1, is_panel=1)
    stopstk=price['is_paused'].iloc[-1]
    ststk=price['is_st'].iloc[-1]
    startstk=(stopstk[stopstk==0].index)
    okstk=(ststk[ststk==0].index)
    tradestk=list(set(startstk)&set(okstk))
    return tradestk    
date='20180102'
stock_list=stock(date)
quote_rate_all=get_price(stock_list, None, date, '1d', ['quote_rate'], False, 'pre', 273, is_panel=1)['quote_rate']
nowmonth=quote_rate_all.iloc[-21:]
quote_rate=quote_rate_all.iloc[:-21]
PM_1M=quote_rate.iloc[-21:].sum()
PM_3M=quote_rate.iloc[-63:].sum()
PM_6M=quote_rate.iloc[-126:].sum()
PM_9M=quote_rate.iloc[-189:].sum()
PM_12M=quote_rate.iloc[:].sum()
PM_12MC6M=quote_rate.iloc[:].sum()-quote_rate.iloc[-126:].sum()
PM_12MC1M=quote_rate.iloc[:].sum()-quote_rate.iloc[-21:].sum()
PM_6MC1M=quote_rate.iloc[-126:].sum()-quote_rate.iloc[-21:].sum()
quote_rate=quote_rate.T
quote_rate['PM_1M']=PM_1M
quote_rate['PM_3M']=PM_3M
quote_rate['PM_6M']=PM_6M
quote_rate['PM_9M']=PM_9M
quote_rate['PM_12M']=PM_12M
quote_rate['PM_12MC6M']=PM_12MC6M
quote_rate['PM_12MC1M']=PM_12MC1M
quote_rate['PM_6MC1M']=PM_6MC1M
yinzi_list=['PM_1M','PM_3M','PM_6M','PM_9M','PM_12M','PM_12MC6M','PM_12MC1M','PM_6MC1M']
for i in quote_rate.columns:
    if i in yinzi_list:
        pass
    else:
        del quote_rate[i]
nowmonth=nowmonth.sum()
quote_rate['nowmonth']=nowmonth
stocknum=int(len(quote_rate.index)*0.1)
yinzi_IClist=[]
zeshi=0
for i in quote_rate.columns:
    if i in yinzi_list:
        quote_rate = pd.DataFrame(quote_rate).sort_values(by =i, ascending=False)
        d=quote_rate['nowmonth'].iloc[:stocknum].mean()-quote_rate['nowmonth'].iloc[-stocknum:].mean()
        if d <0:
            zeshi=zeshi+1
            yinzi_IClist.append(d)
        else:
            yinzi_IClist.append(d)
quote_rate_all=quote_rate_all.iloc[-252:]
PM_1M=quote_rate_all.iloc[-21:].sum()
PM_3M=quote_rate_all.iloc[-63:].sum()
PM_6M=quote_rate_all.iloc[-126:].sum()
PM_9M=quote_rate_all.iloc[-189:].sum()
PM_12M=quote_rate_all.iloc[:].sum()
PM_12MC6M=quote_rate_all.iloc[:].sum()-quote_rate_all.iloc[-126:].sum()
PM_12MC1M=quote_rate_all.iloc[:].sum()-quote_rate_all.iloc[-21:].sum()
PM_6MC1M=quote_rate_all.iloc[-126:].sum()-quote_rate_all.iloc[-21:].sum()
df=pd.DataFrame(columns=yinzi_list)
df['PM_1M']=PM_1M
df['PM_3M']=PM_3M
df['PM_6M']=PM_6M
df['PM_9M']=PM_9M
df['PM_12M']=PM_12M
df['PM_12MC6M']=PM_12MC6M
df['PM_12MC1M']=PM_12MC1M
df['PM_6MC1M']=PM_6MC1M
for i in yinzi_list:
    df[i] = (df[i] - df[i].mean())/df[i].std()
df=df.T
df['IC']=yinzi_IClist
df=df[df['IC']>0]
for i in df.columns:
    if i =='IC':
        pass
    else:
        df[i]=df[i]*df['IC']
del df['IC']
alpha=df.sum()
df=df.T
df['alpha']=alpha
df['syboml']=df.index
df=pd.DataFrame(df).sort_values(by ='alpha', ascending=False)
df.head()
Out[1]:
PM_1M PM_3M PM_6M PM_9M PM_12M PM_12MC6M PM_12MC1M PM_6MC1M alpha syboml
601012.SH 4.123145 14.061065 19.366088 9.526972 9.216536 0.746046 10.245820 19.580743 86.866414 601012.SH
002714.SZ 6.562176 17.113390 17.594895 7.662588 7.252438 0.159777 7.357948 16.378895 80.082108 002714.SZ
601155.SH 8.274691 21.117733 10.907329 7.345679 7.595568 1.531257 7.408383 8.045741 72.226382 601155.SH
000858.SZ 11.908460 16.180331 8.739185 7.133272 7.257843 1.768452 6.229325 3.801573 63.018442 000858.SZ
000063.SZ 3.649492 12.271792 10.394366 8.107323 7.273521 1.475492 8.001821 9.789831 60.963638 000063.SZ

四、最终结果

策略回测区间:2021.01.01-2022.12.31
回测资金:1000000
回测频率:日级
回测结果:红色曲线为策略收益率曲线,蓝色曲线为对应的基准指数收益率曲线

策略源代码:

In [ ]:
import pandas as pd
import numpy as np
# 初始化函数,全局只运行一次
def init(context):
    g.n = 20 #设置最大持仓数量为20只股票
    run_monthly(handle_bar,date_rule=1) 
    #将trade交易函数设置为定时运行:每个月第一个交易日
    context.stock = []#储存上期的股票池
    g.stock='000300.SH'#设置指数获取其成分股
    g.kc=False #因子择时结果是够为空仓
## 开盘时运行函数
def handle_bar(context, bar_dict):
    date=get_last_datetime().strftime('%Y%m%d')
    #执行选股函数:alphastock(date),并将结果导入,该股票列表是需要买入的个股。
    needstock_list = alphastock(date)
    if g.kc ==True:
        context.stock=[]
        for i in list(context.portfolio.positions):
            order_target(i,0)
        pass
    else:
        #获取上期持仓个股
        holdstock_list = list(context.stock)
        #确定本期需要卖出的个股
        sell_list = list(set(holdstock_list)-set(needstock_list))
        #执行卖出操作,运用for循环,逐个操作。
        for s in sell_list:
            order_target(s,0)
        #确定本期需要买入的个股,其余即为继续持仓的个股
        buy_list=[]
        for i in needstock_list:
            if i in holdstock_list:
                pass
            else:
                buy_list.append(i)
        #确定可用资金,平分分配至需买入的个股
        n=len(buy_list)
        cash=context.portfolio.available_cash/n
        #执行买入操作
        for s in range(0,n,1):
            stock=list(buy_list)[s]
            order_value(stock,cash)
        #操作完毕,将选股结果放到上期股票池储存变量中,以备下次使用。
        context.stock = frozenset(needstock_list)
#===============以沪深300为股票池,除去ST和停牌=====  
def stock(date):  
    stk=list(get_index_stocks(g.stock,date))
    #2015年前上市的
    stk2=list(get_all_securities('stock','20150105').index)
    price=get_price(stk, None, date, '1d', ['is_paused', 'is_st'], False, None, 1, is_panel=1)
    stopstk=price['is_paused'].iloc[-1]
    ststk=price['is_st'].iloc[-1]
    startstk=(stopstk[stopstk==0].index)
    okstk=(ststk[ststk==0].index)
    tradestk=list(set(startstk)&set(okstk)&set(stk2))
    return tradestk  
def alphastock(date):
    stock_list=stock(date)
    quote_rate_all=get_price(stock_list, None, date, '1d', ['quote_rate'], False, 'pre', 273, is_panel=1)['quote_rate']
    nowmonth=quote_rate_all.iloc[-21:]
    quote_rate=quote_rate_all.iloc[:-21]
    PM_1M=quote_rate.iloc[-21:].sum()
    PM_3M=quote_rate.iloc[-63:].sum()
    PM_6M=quote_rate.iloc[-126:].sum()
    PM_9M=quote_rate.iloc[-189:].sum()
    PM_12M=quote_rate.iloc[:].sum()
    PM_12MC6M=quote_rate.iloc[:].sum()-quote_rate.iloc[-126:].sum()
    PM_12MC1M=quote_rate.iloc[:].sum()-quote_rate.iloc[-21:].sum()
    PM_6MC1M=quote_rate.iloc[-126:].sum()-quote_rate.iloc[-21:].sum()
    quote_rate=quote_rate.T
    quote_rate['PM_1M']=PM_1M
    quote_rate['PM_3M']=PM_3M
    quote_rate['PM_6M']=PM_6M
    quote_rate['PM_9M']=PM_9M
    quote_rate['PM_12M']=PM_12M
    quote_rate['PM_12MC6M']=PM_12MC6M
    quote_rate['PM_12MC1M']=PM_12MC1M
    quote_rate['PM_6MC1M']=PM_6MC1M
    yinzi_list=['PM_1M','PM_3M','PM_6M','PM_9M','PM_12M','PM_12MC6M','PM_12MC1M','PM_6MC1M']
    for i in quote_rate.columns:
        if i in yinzi_list:
            pass
        else:
            del quote_rate[i]
    nowmonth=nowmonth.sum()
    quote_rate['nowmonth']=nowmonth
    stocknum=int(len(quote_rate.index)*0.1)
    yinzi_IClist=[]
    zeshi=0
    for i in quote_rate.columns:
        if i in yinzi_list:
            quote_rate = pd.DataFrame(quote_rate).sort_values(by =i, ascending=False)
            d=quote_rate['nowmonth'].iloc[:stocknum].mean()-quote_rate['nowmonth'].iloc[-stocknum:].mean()
            if d <0:
                zeshi=zeshi+1
                yinzi_IClist.append(d)
            else:
                yinzi_IClist.append(d)
    if zeshi > 4:
        g.kc = True 
    else:
        g.kc = False
    quote_rate_all=quote_rate_all.iloc[-252:]
    PM_1M=quote_rate_all.iloc[-21:].sum()
    PM_3M=quote_rate_all.iloc[-63:].sum()
    PM_6M=quote_rate_all.iloc[-126:].sum()
    PM_9M=quote_rate_all.iloc[-189:].sum()
    PM_12M=quote_rate_all.iloc[:].sum()
    PM_12MC6M=quote_rate_all.iloc[:].sum()-quote_rate_all.iloc[-126:].sum()
    PM_12MC1M=quote_rate_all.iloc[:].sum()-quote_rate_all.iloc[-21:].sum()
    PM_6MC1M=quote_rate_all.iloc[-126:].sum()-quote_rate_all.iloc[-21:].sum()
    df=pd.DataFrame(columns=yinzi_list)
    df['PM_1M']=PM_1M
    df['PM_3M']=PM_3M
    df['PM_6M']=PM_6M
    df['PM_9M']=PM_9M
    df['PM_12M']=PM_12M
    df['PM_12MC6M']=PM_12MC6M
    df['PM_12MC1M']=PM_12MC1M
    df['PM_6MC1M']=PM_6MC1M
    for i in yinzi_list:
        df[i] = (df[i] - df[i].mean())/df[i].std()
    df=df.T
    df['IC']=yinzi_IClist
    df=df[df['IC']>0]
    for i in df.columns:
        if i =='IC':
            pass
        else:
            df[i]=df[i]*df['IC']
    del df['IC']
    alpha=df.sum()
    df=df.T
    df['alpha']=alpha
    df['syboml']=df.index
    df=pd.DataFrame(df).sort_values(by ='alpha', ascending=False)
    #获取前20只股票的代码
    needstock_list=[]
    for s in range(0,g.n,1):
        needstock_list.append(df.iloc[s]['syboml'])
    #完成函数,输入即为10只股票代码,作为策略的股票池
    return needstock_list