量化选股——基于动量因子的行业风格轮动策略(第1部分—因子测算)

文章探讨了动量因子在投资中的作用,特别是在行业风格轮动中的应用。通过阿隆指标(Aroon)计算动量,对申万一级行业进行测算,发现部分行业的动量效应明显。同时,文章提到了投资者的决策行为、奈特不确定性对市场动态的影响,以及信息传播的时间差在行业轮动中的角色。最后,文章提供了动量因子效果的统计方法和初步结论,指出并非所有行业都能保持稳定的胜率和正向年化收益率。
摘要由CSDN通过智能技术生成

【量化选股——基于动量因子的行业风格轮动策略】分为两部分:

  1. 第1部分—因子测算
  2. 第2部分—策略回测

动量因子与行业轮动概述

动量因子的理解

动量,可以理解为“势头”,“强势的程度”。汽车遇到红灯时,不是一下子停下,而是滑行一段再停。滑行的这一段就解释为“动量”造成的。动量因子表示,即便情况发生了变化,这些因子所代表的势头仍会持续一段时间。

动量因子一直饱受争议,因为它的前提假设是股票会表现出马太效应。意思是:股票的相对强弱趋势会延续,并且表现出“强者恒强,弱者恒弱”的态势,除非有意外情况发生才会导致强弱之势逆转。动量因子表现出两种效应:

  1. 动量效应:股票的收益率会延续
  2. 反转效应:经过较长时间后,收益会翻转

挖掘动量因子的过程往往是先算出结果,再找一个逻辑来解释。因此动量因子其实没有太多的基础理论支撑,是在实践中被摸索出来的。使用动量因子的风险也很高,因为对动量因子的解释性的逻辑往往超脱了我们过去所知的对市场理解的框架体系,有一种“唯数据论”的感觉。这里博主希望大家能够辩证的看待动量因子,有自己的看法。

通常学界对动量因子的理解有以下几种:

  1. 投资者的决策行为,导致了动量因子的产生。如中国人个人炒股比例高,而且由于缺乏背景知识,缺乏对基本面的了解,同时好大喜功求快,导致短线操作居多,跟风者居多;

    但反对者认为这种猜测无法定量评估,而这样的解释是“为了发展一个理论模型而寻找不合理的逻辑假设”

  2. 因为每个市场的参与者,接收同一个信息的时间都不同,因此即便某个事件发生了,依旧会因为接收信息的时间差导致参与者在操作上存在时间差,所以时间差导致了动量这种“效应”的产生。同样,也可能因为存在一些信息差,不对称的信息差会随着时间流逝慢慢对称,这个过程也会造成动量。

    但反对者同样认为:既然存在操作的时间差,那么利好和利空的消息都应该存在时间差,而往往动量因子只能体现出某一边的动量。(如,某些代表利好的动量因子无法解释利空的情况;或者说,动量因子本身就不应该被分为利好或利空,否则就代表它只验证了对一边有效)

  3. 市场的参与者都有自己入场的动机与离场的目的,不同参与者也是在市场中不断博弈与进化的(市场具有“奈特不确定性”)。按照书本所述,股票现在合理的价格,就是对未来现金流的折现。那么不同的参与者因为伴随有不同的入场与离场的目的,因此站在各自的立场上,对“未来现金流折现”的估值也不同,而这种行为伴随着市场的进化与发展,导致“动量”作为一个观测现象而产生。

    这就意味着“动量”是一种表面现象,它不是因为固定的几个逻辑直接导致的结果,而是博弈造成的一种被观测出来的现象。

目前动量因子在个股上是作为多因子模型的一部分存在,而如果将视野扩大到行业层面,就可以单独拿动量因子进行建模测算,此时因子的动量效应较为明显。通常认为是因为暴露的因子动量会随着个股传递到整个行业组合上,因此观测较为明显。

我们常见的动量因子通常包含:

  1. 过去一段时间(1周、10天、一个月、3个月)的收益率
  2. 5、10、20、30、60天收盘价均线
  3. 过去一段时间板块、行业的高开低收等数据

投资视角下的行业轮动现象

我们以接收信息的时间差导致参与者在操作上存在时间差,所以时间差导致了动量这种“效应”的产生为视角,就可以发现在行情走势上:

  1. 市场上有大量未接受到该信息的投资者:标的买卖双方造成的买卖供需关系并不会显著到立即把价格推向其应当达到的位置,此时表现在信息的“反应不足”

  2. 越来越多的投资者接触到了该信息并做出反应:此时价格逐渐被推至合理价格附近,但后入市场的投资者中有一部分因为各种各样的原因,错误或过分估计了该信息价值,从而将该股票价格再进一步推离了合理价格,此时“反应过度”,出现超买、超卖现象

  3. 超买超卖现象被发现:市场的投资者捕捉到反映过度带来的盈利空间,通过交易获取超额收益的同时,将股票价格再度往合理价格处推动。

在这里插入图片描述
比如:在2022年11月30日ChatGPT就已经向公众开放:

  • 11月30日OpenAI(ChatGPT的作者)在维基百科发布关于ChatGPT的百科信息
  • 12月6日开始维基百科词条开始普通用户参与修改,并且参与修改的用户账号量每日增长显著
  • 12月新闻既有“小学生击败ChatGPT”,“差点被ChatGPT骗了”,“举一千反一,人类离xxx还有多远”等负面看法,也有“爆火的ChatGPT太强了”等正面观点

在这里插入图片描述
复盘12、1、2月份到如今的ChatGPT概念(BK1126),可以发现站在投资者的视角上:

  1. 投资者无法判断信息是从何时开始反映在市场上的,也无法判断未来将持续多长时间
  2. 投资者难以评估信息的市场价值,信息带来的对预期的影响,能造成的股价涨跌幅难以度量
  3. 信息源种类、观点、立场繁多,难以主观评价
    如今,百度指数上,ChatGpt的搜索指数与资讯关注度依旧很高,但相对高点已经走弱,由于是境外资讯,而百度搜索面向国内用户,因此国内资讯会优先与国内的搜索指数呈现出增长的趋势

在这里插入图片描述

因此在行业轮动上,动量指标盈利的来源:

  1. 市场上有大量未接受到该信息的投资者
  2. 越来越多的投资者接触到了该信息并做出反应(动量指标盈利主因)
  3. 超买超卖现象被发现(动量指标亏损主因)

投资者视角与奈特不确定性

奈特把对未来的不确定性分为两种,一种称为“风险”,另一种称为“奈特不确定性”,将未知分为了两类:

  1. 风险:具有特定概率分布的不确定性
  2. 奈特不确定性:没有特定概率分布的不确定性

后续的研究证明:

  1. 人们常对奈特不确定性表现出厌恶的倾向,并愿意为避免奈特不确定性而支付溢价
  2. 厌恶奈特不确定性的人不一定厌恶风险,即“赌鬼也讨厌奈特不确定”
  3. 在人们面临奈特不确定性时,会出现群体盲从的现象,形成羊群效应,此时人们更在乎别人的想法

当信息被越来越多的人接触到的时候,投资者面临的其实就不是完全未知的“奈特不确定性”,而是可以评估盈利与亏损区间与概率的“风险”了。但相对的,不同的市场参与者对信息的敏感程度与评估是不同的,从这个角度也验证了上述的第二条:“投资者难以评估信息的市场价值,信息带来的对预期的影响,能造成的股价涨跌幅难以度量”。对于信息的动量与价格走势趋势的判断需要策略研究员进行细致的研究。

动量因子在行业风格上的效果测算

动量因子效果测算流程概述

  1. 首先我们选择“申万一级”行业指数,阿隆指标(Aroon)作为动量指标进行测算
  2. 然后根据指数数据集,选择公共的时间段(2015-01-01 至 2023-01-01)将日期分为两个部分:
    • 选择 2015-01-01 至 2020-01-01 这一段时期进行测算
    • 选择 2020-01-01 至 2023-01-01 这一段时期进行回测
  3. 选择测算的时间段进行测算
  4. 选择回测的时间段进行回测

1. 行业选择:申万一级行业

我们选取申万一级行业指数来测算

申万行业分类规则请参考:申万行业分类标准(2021版)

行业代码行业名称成份个数静态市盈率TTM(滚动)市盈率市净率静态股息率
0801010.SI农林牧渔9947.1347.642.840.62
1801030.SI基础化工34315.9215.022.482.11
2801040.SI钢铁447.2712.031.065.65
3801050.SI有色金属12824.5515.512.691.21
4801080.SI电子30823.8827.292.941.21
5801880.SI汽车24029.2828.172.271.31
6801110.SI家用电器7917.6016.002.822.97
7801120.SI食品饮料11940.7336.847.841.77
8801130.SI纺织服饰11316.8217.741.973.48
9801140.SI轻工制造14821.6725.742.381.81
10801150.SI医药生物36029.3426.323.571.08
11801160.SI公用事业12321.3219.211.902.28
12801170.SI交通运输1249.878.481.284.62
13801180.SI房地产1159.5512.150.983.40
14801200.SI商贸零售10426.1031.572.601.51
15801210.SI社会服务7362.3462.043.570.55
16801780.SI银行424.974.660.555.79
17801790.SI非银金融8813.6416.671.382.56
18801230.SI综合2457.8330.572.310.78
19801710.SI建筑材料749.3412.551.483.83
20801720.SI建筑装饰1588.507.910.892.35
21801730.SI电力设备26540.1929.254.070.60
22801890.SI机械设备39824.0727.192.341.69
23801740.SI国防军工9855.9150.133.740.49
24801750.SI计算机26839.4943.273.890.88
25801760.SI传媒14018.9321.971.882.49
26801770.SI通信10717.8915.991.454.08
27801950.SI煤炭3810.126.641.426.61
28801960.SI石油石化4710.618.141.006.81
29801970.SI环保10916.6018.721.581.75
30801980.SI美容护理2843.9542.615.690.61

这里申万一级的行情数据不在之前的股票数据里,这里提供获取代码:

import akshare as ak

# 申万一级行业信息
sw_index_first_info_df = ak.sw_index_first_info()
for _, sw_series in sw_index_first_info_df.iterrows():
    sw_symbol = sw_series["行业代码"].split(".")[0]
    _ak_df = ak.index_hist_sw(symbol=sw_symbol, period="day")
    _ak_df.to_csv("../data/select_factor_data/sw_{}.csv".format(sw_symbol),index=False)

2. 动量因子选择:阿隆指标(Aroon)

阿隆(Aroon)指标是由图莎尔·钱德(Tushar Chande)1995 年发明的,它通过计算当前价格达到近期最高值和最低值以来所经过的天数,帮助投资者预测证券价格趋势或反转的变化

阿隆指标计算步骤:

  1. 确定滑动窗口的长度,比如25个工作日);获取这个窗口中日线的最高价与最低价
  2. 用最高价计算AroonUp = [(计算期天数-达到最高价后的天数)/计算期天数]*100,即:
    Aroonup = [ ( 25 - 到达最高价后的天数 ) / 25] * 100
  3. 用最低价计算AroonDown = [(计算期天数-达到最低价后的天数)/计算期天数]*100,即:
    Arrondown = [ ( 25 - 达到最低价后的天数 ) / 25 ] * 100

根据公式我们可以推算出:

  1. 最高价屡创新高时,arronup=1;最低价屡创新低时,arrondown=1;
  2. 最高价不断走低时,arronup=0;最低价不断走高时,arrondown=0;
  3. arronup越小,代表离上一次创新高的时间越久;arrondown越小,代表离上一次创新低的时间越久;
  4. arronup=1且arrondown=1,代表最高价屡创新高的同时,最低价也屡创新低(柱子拉长)

在这里插入图片描述

3. 测算方法

1.选择特定的时间区间

我们删除数据不足的“石油石化”,“环保”,“美容护理”,这三个指数,然后划分公共数据区间为两段:

  • 统计测算:2015-01-01 至 2020-01-01
  • 回测:2020-01-01 至 2023-01-01
#(部分代码)
train_data_dict = {}
test_data_dict = {}
for _sw_key, _sw_df in sw_data_dict.items():
    train_data_dict[_sw_key] = _sw_df[_sw_df["日期"].between("2015-01-01", "2020-01-01")]
    test_data_dict[_sw_key] = _sw_df[_sw_df["日期"].between("2020-01-01", "2023-01-01")]

2.计算阿隆指标(Arron)

选择时间区间:2015-01-01 至 2020-01-01,所有指数单独计算,以25天为滑动窗口长度,计算aroonup与aroondown指标

规定交易规则:当arronup>arrondown时,以当天收盘价买入;arronup<arrondown时以当天收盘价卖出

def measure_aroon(dataframe:pd.DataFrame):
    dataframe.columns = ["code","date",'close','open','high','low','volume','business_volume']
    dataframe.set_index(["date"], inplace=True)
    dataframe.index.name = ""
    dataframe['aroondown'], dataframe['aroonup'] = talib.AROON(dataframe['high'], dataframe['low'], timeperiod=14)
    dataframe = dataframe.dropna()
    return dataframe

3. 统计收益率&胜率

统计每一笔完整的交易(从买到卖的完整交易)的年化收益率,并且逐笔统计,以年化收益率>2%记为胜,否则为负

# (部分代码)
total_measure_record = {} # 测算结果

for _train_lable,_train_df in train_data_dict.items():
    measure_record = {} # 测算结果
    if _train_df.shape[0] ==0:
        continue
    mea_df = measure_aroon(_train_df.copy())
    # 开始测算
    trade_record_list = []
    this_trade = {
        "close_record":[],
    }
    for index,series in tqdm(mea_df.iterrows(),total=mea_df.shape[0]):
        if series['aroondown'] < series['aroonup']:
            mea_df.loc[index,"label"] = "sell"
            if "buy_date" not in this_trade.keys():
                continue
            this_trade['sell_date'] = index.to_pydatetime()
            trade_record_list.append(this_trade)
            this_trade = this_trade = {
                "close_record":[],
            }
        else:
            mea_df.loc[index,"label"] = "buy"
            this_trade['buy_date'] = index.to_pydatetime()
            this_trade['close_record'].append(series['close'])
        if "buy_date" in this_trade.keys():
            this_trade['close_record'].append(series['close'])
    trade_record_df = pd.DataFrame(trade_record_list)
    for _i,_trade_series in trade_record_df.iterrows():
        _trade_record_year_rate = (_trade_series['close_record'][-1] - _trade_series['close_record'][0])/_trade_series['close_record'][0]/(
                                _trade_series['sell_date'] - _trade_series['buy_date']).days * 365 # 年化收益
        if _trade_record_year_rate > 0.02:
            trade_record_df.loc[_i,'victory'] = 1
        else:
            trade_record_df.loc[_i,'victory'] = 0
        trade_record_df.loc[_i,'年化收益率']  = _trade_record_year_rate
    # trade_record_df 即为每一个行业真实的测算结果
    
    measure_record['胜率'] = round(sum(trade_record_df['victory']) / trade_record_df.shape[0], 4)
    measure_record['胜率详情'] = "{}/{}".format(round(sum(trade_record_df['victory']),3), trade_record_df.shape[0])
    measure_return = trade_record_df['年化收益率'].describe()
    measure_record['收益率均值'] = measure_return['mean']
    measure_record['收益率方差'] = measure_return['std']
    measure_record['25%'] = measure_return["25%"]
    measure_record['75%'] = measure_return["75%"]
    measure_record['中位数'] = measure_return['50%']
    total_measure_record[_train_lable] = measure_record

4. 测算结论

按照上述的测算方法,测算结论如下:

  1. 综合测算下来,28个申万一级行业中,有15个行业的综合胜率>=50%,有18个年化收益率中位数>=0。
  2. 没有一个行业可以每年的胜率都达到50%以上
胜率胜率详情“年化收益率均值”“年化收益率方差”“年化收益率25%”“年化收益率75%”“年化收益率中位数”
801210.SI0.6526.0/401.10132813.854513-3.567328.3831933.277827
801110.SI0.62520.0/32-0.43676116.563576-7.3248557.5505731.551451
801750.SI0.605323.0/38-2.00612122.928017-6.9598759.2676092.499013
801120.SI0.589723.0/39-0.54581212.693933-3.1803855.8975451.632406
801890.SI0.588220.0/34-3.36067722.453205-14.6205418.865592.704538
801080.SI0.588220.0/34-5.75448225.290329-8.0245436.4357951.120838
801200.SI0.575819.0/33-5.14804120.819803-12.9407574.3527821.052707
801140.SI0.575819.0/33-4.73596818.721499-10.9718666.1674021.135974
801160.SI0.571420.0/35-4.53512617.491513-7.791965.7381180.732191
801730.SI0.558819.0/34-2.88049719.378822-12.309228.1740410.882168
801010.SI0.555620.0/36-2.93808817.975981-11.3953038.5876680.848525
801130.SI0.545518.0/33-4.97715521.158295-8.8583126.2438431.500513
801760.SI0.531217.0/32-10.96552527.899573-16.3725745.7345610.556964
801770.SI0.515217.0/33-4.68487724.414547-10.70240610.3028370.887308
801050.SI0.516.0/32-4.53906122.368268-16.5867359.2855820.261408
801040.SI0.487219.0/39-1.24763415.297324-10.7822486.7690930.011439
801180.SI0.487219.0/39-0.80418516.592557-3.9955525.568170
801720.SI0.486518.0/37-3.74782419.616045-15.0682356.5086590
801710.SI0.473718.0/38-3.01019717.654178-7.4173625.759873-0.061242
801030.SI0.468815.0/32-1.80782217.684925-6.3255938.010072-0.742137
801880.SI0.468815.0/32-3.9287619.513424-9.6945966.653341-1.287274
801170.SI0.4518.0/40-0.84516113.940707-5.7243374.644774-0.482324
801790.SI0.447417.0/38-5.05709720.558703-15.5258314.800522-0.754778
801150.SI0.406213.0/32-6.39906418.282151-14.5345014.495543-1.321404
801230.SI0.406213.0/32-7.86619327.87652-13.1456536.940749-1.782862
801740.SI0.333311.0/33-9.56387620.308132-17.155551.213517-6.112185
801950.SI0.28574.0/14-15.70347737.098001-25.4346440.683044-0.548628
801780.SI0.268311.0/41-3.81688410.615774-6.9792130.250093-0.675727

不同指数分年的胜率统计图(0.5为纯白色,越偏红胜率越高,越偏蓝胜率越低):

在这里插入图片描述

本表以Fama-French三因子资产定价模型为依据,提供市场溢酬因子(Rm-Rf),市值因子(SMB)和账面市值比因子(HML)的月序列数据。 表中计算所用的无风险收益数据选择标准为:开始--2002年8月6日用三个月期定期银行存款利率; 2002年8月7日--2006年10月7日用三个月期中央银行票据的票面利率; 2006年10月8日--当前,用上海银行间3个月同业拆放利率。 三因子数据包括: 市场溢酬因子__流通市值加权 Rm-Rf 市值因子__流通市值加权 SMB 账面市值比因子__流通市值加权 HML 市场溢酬因子__总市值加权 Rm-Rf 市值因子__总市值加权 SMB 账面市值比因子__总市值加权 HML 有以下3种方式计算的月惯性因子(又称动量因子)。 计算方法1:惯性因子=前n个月累积收益最高的30%的所有股票组合加权收益率-前n个月累积收益最低的30%的所有股票组合加权收益率。 计算方法2:惯性因子=前n个月累积收益最高的10%的所有股票组合加权收益率-前n个月累积收益最低的10%的所有股票组合加权收益率。 计算方法3:惯性因子=前n个月累积收益大于零的所有股票组合加权收益率-前n个月累积收益小于零所有股票组合加权收益率。 其中,n=3、4、5、6、7、8、9、10、11、12、18、24;加权方式为等权、流通市值加权、总市值加权。 在Carhart四因子模型经典文献中,惯性因子=前11个月累积收益最高的30%的股票组合等权收益率-前11个月累积收益最低的30%的股票组合等权收益率。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

呆萌的代Ma

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

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

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

打赏作者

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

抵扣说明:

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

余额充值