单因子专题——规模
1. 来源
规模因子的历史要追溯到1981年。Banz(1981)基于纽交所数据发现,将股票按照市值分成5组后,市值最小的一组股票月收益率比其他股票高0.4%,差异非常显著。并且这种效应非线性,而是只对市值最小的股票起作用。
Fama-French的三因子模型将规模、账面市值比和市场因子作为因子预测股票预期收益,发现效果优于CAPM模型。随后,又提出了五因子模型,纳入盈利和投资两个因子,发现能够解释更多的截面差异。
2. 成因
关于规模因子,一种解释来自风险补偿说。Chan and Chen认为,小市值公司普遍在过去遭遇过困境使得市值大幅下滑;Fama and French进一步指出规模效应可能与小市值股票更大的财务风险有关。除此之外,还有一种解释认为非流动性是一个定价因子,规模与非流动性相关,因此呈现出规模效应。由于小市值公司存在这些风险和问题,为了吸引投资者购买股票,必然要用更高的收益弥补这些风险。
另一些研究认为,规模效应来自设定偏误,是遗漏变量问题的自然结果。Berk认为,如果定价模型形式设定错误,则公司规模将总是与股票收益中未被解释的部分负相关。也就是说,规模越大,未被解释的收益率越低;规模越小,未被解释的收益率越高。
3. 实证(单变量排序)
由于A股特殊的IPO制度和壳价值,实证采用流动市值代替总市值作为排序变量构建规模因子(与原书中不同)。每月将股票按照流动市值大小分成10组,计算每组的平均月收益,各组表现如下图所示。
由图可知,规模因子在2017年效果很差。在2017年的12个月中,只有极少数月份满足收益率随着市值下降而增加的特点。甚至在很多月份中(如6月、8月、12月)呈现出收益率随着市值增加而上升的特点,违背了小市值效应的结果。实证结果与书中论述一致。
4. 策略
利用规模因子构建套利策略(利用流通市值代替总市值),回测期为2020年1月1日至2020年12月01日。计划每月调仓一次,每次调仓基准为:按照上个月流通市值平均排序,选取市值最小的前10支股票买入。回测结果如下:
回测期累计收益率为4.29%,年化收益率为4.66%,同期沪深300指数收益率为22.03%,策略整体跑输指数。最大回撤为15.22%,胜率只有57.50%。
如果反向套利,买入市值最大的10支股票,得到的年化收益为12.98%,最大回撤为14.19%,胜率为39.13%,大市值策略整体优于小市值策略。由此可见,规模因子在使用时需要具体情况具体分析。
5. 代码实现
(回测代码基于掘金量化平台)
# coding=utf-8
from __future__ import print_function, absolute_import
from gm.api import *
import pandas as pd
import numpy as np
import time
import datetime
# 策略部分
def init(context):
# 获取全部A股代码
stockall = get_instruments(exchanges='SHSE, SZSE', fields='symbol,listed_date, delisted_date,sec_type', df=True)
context.stockall = stockall[(stockall['listed_date'] < context.backtest_start_time) &
(stockall['delisted_date'] > context.backtest_end_time) &
(stockall['symbol'].str[5] != '9') &
(stockall['symbol'].str[5] != '2') &
(stockall['sec_type'] == 1)]['symbol'].tolist()
# 股票数量
context.num = 10
# 资金比例
context.percent = 0.8
# 初始化上次交易时间
context.last_time = context.now.date()
# 设置定时任务:每个月调仓一次
schedule(schedule_func=algo, date_rule='1m', time_rule='09:40:00')
def algo(context):
# 获取开始时间和结束时间
last_month_end = datetime.datetime(context.now.year, context.now.month, 1) - datetime.timedelta(days=1) # 上一个月的最后一天
# 遍历股票
fin = get_fundamentals(table='trading_derivative_indicator', symbols=context.stockall, start_date=last_month_end,
end_date=last_month_end, fields='TOTMKTCAP', limit=10000, df=True)
# 按照市值排序,买入市值排名靠前的股票
choose = fin.sort_values(by='TOTMKTCAP', ascending=True).head(context.num)['symbol'].tolist()
print('%d年%d月的选股完成'%(context.now.year, context.now.month))
# 查持仓
positions = context.account().positions()
percent = context.percent / context.num
for position in positions:
symbol = position['symbol']
if symbol not in choose:
order_target_percent(symbol=symbol, percent=percent, position_side=PositionSide_Short,
order_type=OrderType_Market)
print('{}不在买入列表中,平仓'.format(symbol))
for symbol in choose:
order_target_percent(symbol=symbol, percent=percent, position_side=PositionSide_Long,
order_type=OrderType_Market)
print('{}在买入列表中,调仓至{}'.format(symbol, percent))
if __name__ == '__main__':
run(strategy_id='输入策略id',
filename='main.py',
mode=MODE_BACKTEST,
token='输入token',
backtest_start_time='2020-01-01 13:00:00',
backtest_end_time='2020-12-01 15:00:00',
backtest_adjust=ADJUST_PREV,
backtest_initial_cash=10000000,
backtest_commission_ratio=0.0001,
backtest_slippage_ratio=0.0001)