前言
最近写了不少以动量因子为核心的量化策略,结果收益和回撤都不太理想
跑去社区发了个帖子贴了下自己的回测情况,有人推荐我看看这本书,遂有如下经历
原书思想
简单点说,整个策略是期望基于动量因子寻找到有上升趋势的股票,买入并持有直到趋势消失,从而在牛市中获取超额利润
作者以标普500作为股票池,他的理由是:
一只股票能加入到指数的部分原因是其过去价格走势强劲。当股票从指数中退出时,通常是价格表现不佳,跌到所要求市值之下。这使得标普500指数和其他大多指数一样在一定程度上都是动量策略。
关于动量效应的解释:
当一只股票的股价上涨一段时间后,继续上涨的可能性要高于回落的可能性,比其他股票上涨的快的股票会继续比其他股票上涨的快
而且作者不是仅仅是通过计算股票的年化收益率作为动量因子来筛选股票(后面我会列举具体的计算步骤),他还有一些附加条件,比如:
股票价格必须高于其100日均线。如果不是,说明它并不符合动量的标准。因为在上涨的市场中,排名靠前的股票价格都远远高于其100日均线,但是如果在熊市或者牛熊市转换之际,上涨的股票很少,这条规则可以确保你不会买入那些横盘或者下跌的股票。
注意价格缺口。如果某只股票在过去90天里有超过15%的价格缺口(股价大幅变动并伴随极少的交易量),那么它也会被取消买入资格,因为如果你不排除这一情况,就有可能买入并非真正动量股的股票。比如说短期冲击可能导致股价大幅波动,有时即使我们对年化收益率做了一定的修正,仍无法抵消这一影响,这就与我们希望买入稳步上升的股票的初衷相违背了。
归根结底,对动量因子的应用,其实就是一种趋势跟随的实操方案,而趋势跟随的弊端及补偿方案,原书中也提到过:
当市场横盘整理或是快速切换方向的时候,趋势跟随者便会亏损。对于个别市场或行业,这一现象可能会持续多年。在极端情况下,甚至会持续十年。趋势跟随的核心前提是基于多元化。通过同时交易多种不同类别的资产,其获得成功的概率是非常高的,以至于有足够的资金来弥补在某些资产类别上的损失
这一点在实际回测中也被很好的印证了,基本跑不过大盘
上图15年熊市末期到18年末的收益情况,红线是沪深300收益,蓝线是策略收益
当然,除了依据动量因子给池中的股票做排名之外,作者也提到了头寸规模
他说的这句话一定要画上重点:我们不是分配资金,而是分配风险
很多人给资产分配不同的权重时往往容易忽视背后的逻辑,要记住,我们正在做的是均衡风险,而不是表面上的资金分配
最后,关于卖出时机,这一点不是动量策略要考虑的,因此书中并未提及止损方法
回测的具体步骤
首先贴下代码逻辑,这是我最近的心得之一,回测前一定要画好流程图,后面撸代码时效率会高很多
上面是根据原书的策略优化后作出的第一版流程图,实际回测时我是有部分改动的,后续会提到
下面讲讲核心算法
股票排名
指导思想:找到一类稳步上升的股票,不仅随着时间的推移获得可观的收益,而且还尽可能平稳地移动
这里主要以两个指标为依据:
- 股票年化收益 2. 股票的波动率
关于第一点,这里所说的年化收益其实是通过指数回归计算日涨幅从而得到的,目的是为了量化动量这一指标,收益越高,动量越大
股票的波动率是借助r-squared这个判定系数来衡量价格序列与回归直线的拟合程度,拟合性越差,判定系数越低,给最终分数添加更高的惩罚
所以最后将二者乘积作为股票的分值
具体计算方法:
对价格序列取自然对数
对处理后的价格序列计算线性回归方程
将方程的斜率作为日收益,再计算其250次方获得年化收益
通过RSQ()函数计算判定系数(r-squared)
以上是原书的计算方法,详情可参考我的代码:
def get_socre(stock):
''' 基于股票年化收益和判定系数打分
Returns:
score (float): score of stock
'''
data = attribute_history(stock, g.stock_mean_day, '1d', ['close'])
y = data['log'] = np.log(data.close)
x = data['num'] = np.arange(data.log.size)
slope, intercept = np.polyfit(x, y, 1)
annualized_returns = math.pow(math.exp(slope), 250) - 1
r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))
return annualized_returns * r_squared
头寸规模
关于波动的计算,作者引入ATR,即Average True Range(平均真实波幅)这一指标,来衡量股票价格波动
其中,TR =∣今日最高价 - 今日最低价∣和∣今日最高价 -昨日收盘价∣和∣今日最低价 - 昨日收盘价∣的最大值
原书ATR取近20日内TR的均值
关于风险因子,用于设定头寸规模,举个例子:
总资金10万,风险因子10个基点(0.001),目标股价118.93,ATR为3.26(股价日振幅)
则,对应股票应持股数量 = 10万 X 0.001 / ATR, 股票权重 = 持股数量 X 股价(118.93)
最后,设定一个阈值,一旦目标股票当前的资金暴露风险与期望风险相差大于阈值,则触发再平衡操作,这么做的一个考量是为了减少换手率,防止过多的小额交易
参考代码:
def get_expected_position(stock, context):
''' 根据ATR和风险因子计算股票的期望仓位
Returns:
float: 目标股票的期望投入金额
'''
data = attribute_history(stock, g.ATR_day+1, '1d',
['high', 'low', 'close'])
ATR = talib.ATR(data.high, data.low, data.close, timeperiod=g.ATR_day)[-1]
stock_price = data.close[-1]
expected_position = context.portfolio.total_value * g.risk_factor * stock_price / ATR
return expected_position
def get_diff_position(stock, context):
''' 股票的当前仓位与期望仓位的差值百分比
Returns:
float
'''
expected_position = get_expected_position(stock, context)
now_position = context.portfolio.positions[stock].value
return abs(now_position / expected_position - 1)
最后
策略表现:
本次回测只是复制原书的整个策略,作者选用的标普500,我选取的沪深300,并未依据A股市场做任何变动
但是从牛市情况来看,原策略在国内的表现也还是可圈可点的,至于熊市和震荡市的表现,就得结合其他策略做进一步的改进了
比如原策略对熊市的判断是依据标普500的200日均线,这一点用在A股的结果从上图就能看出
关于参数的优化,作者说的一些话我很中意,比如:
如果你运行一个优化算法,你可能得出这样的结论:一个237天或178天的移动平均是最有效的。那会让你以为这跟未来一定是有关系的。其实你得到的就是一个在特定历史时期下的曲线拟合。你真正要做的是仔细思考一些理念,而不是某个精确的数字。
做量化交易很容易陷入过度优化的误区,我的观念是定性首先比定量要重要,一个策略如果要求一套非常精确的规则和参数,那这样的策略通常不会是健壮的策略。
从这一点看,上面的动量策略的表现其实我是比较满意的