本文持续更新中。最后更新时间:11/3/2019
更新日志(11/3/2019):
(1)优化部分表述和排版,修改错误
(2)添加移动平均动量部分内容
(3)修改部分2.1中部分代码错误
(4)增加免责声明
现在也没啥相关回顾,现在只有一篇作者简介绍(笑哭)OvO
[1] 作者简介
动量策略中所谓的动量(Momentum),是指某一对象所具有的一种倾向于保持其原有属性或特征的性质,也可以简单理解成一种惰性(Inertia)。股票的动量,简单地说就是涨的还会接着涨,跌的还会接着跌;过去涨得越猛,未来涨的也就越猛;过去跌得越狠,未来也会跌的越狠。
动量策略(Momentum Strategy),则是指通过某些技术方法或量化指标,挖掘股票的动量特性,从而制定风险可控并且可以大概率盈利的择时策略。
在中国的A股市场中,“追涨杀跌”其实就是一种利用股票的动量特性进行投资的行为:当股票价格迅速上涨的时候,可以认为股票价格有较高的增长动力,或者说在未来一段时间内股票可能具有持续增长的趋势。这种策略虽然听起来简单粗暴,但是诸多技术指标都是建立在这一投资逻辑的基础之上的;通过对大量K线图的分析,我们甚至可以发现股票价格的动量特征在A股市场中是普遍存在的。我们将在第2部分的实证中进一步验证这一说法。实际上,股票价格的动量特征背后是具有经济学逻辑和行为学逻辑支撑的(例如马太效应、羊群效应、锚定效应等),我也将在后面的文章中对这些现象进行进一步阐述。(暗示关注)
在本章描述的动量策略中,我们将分别使用收益率(Rate of Return)、移动平均线(Moving Average)等指标对动量进行量化,实现不同的动量策略。
记每一单位时间内的股票收益率为:
R i ( t ) = P i ( t ) P i ( t − 1 ) − 1 R_i(t) = \frac {P_i(t)}{P_i(t-1)} -1\, Ri(t)=Pi(t−1)Pi(t)−1
这里的一个单位时间差可以是月频、周频等低频数据,也可以是日频、1小时、5分钟、1秒钟等高频数据,取决于你可获取的数据以及你的交易喜好(偏向于高频交易还是低频交易)。
由于股票存在分红与分股,而分红和分股在投资逻辑上既不会影响股票的内在价值,也不会破坏该股票的增长和下降的原有趋势。
注:实际上,这一条并不严格成立,我们暂时做出这一合理假设。对于分红和分股对股票趋势的影响我将择机在未来的文章中做出进一步说明。
在本文中,我们以更好收集的日频数据为例进行说明,并每日的 调整后收盘价格(Adjusted Closed Price) 作为股票的价格P。需要注意的是,这里的调整后价格P我们使用更加直观的先复权价格。
注:容易证明:前复权价格和后复权价格在计算已实现的每日价格变化率时是等价的。
本文的数据来源:tushare数据库
价格动量是指以价格的收益率作为指标衡量动量的大小,这些指标可以是累计收益率(Cumulative Rate of Return),平均收益率(Average Rate of Return),风险调整后收益(Risk-Adjusted Rate of Return)等。
在t时刻,股票i的时间长度为T的累计收益率有:
R i c u m ( t , T ) = P i ( t ) P i ( t − T + 1 ) − 1 = ∏ j = t t − T + 1 ( R i ( j ) + 1 ) − 1 R_i^{cum}(t, T) = \frac {P_i(t)}{P_i(t-T+1)} -1 = \prod_{j = t}^{t-T+1}{(R_i(j)+1)} -1 Ricum(t,T)=Pi(t−T+1)Pi(t)−1=j=t∏t−T+1(Ri(j)+1)−1
从指标构造的角度来说,使用累计收益率的一个理由是:考虑一段时间内股票的价格变化率,而不是某一天的变化率,可以在一定程度上过滤掉一些极端的变化情况(例如今天涨停,明天跌停,实际上可能是由于一些噪音信号导致的)。
注:累计收益率其实是过去各时点几何平均收益率的N次方。因此,信号的构造理论上与几何平均收益率是等价的。
在t时刻,股票i的时间长度为T的算数平均收益率有:
R m e a n ( t , T ) = 1 T ∑ j = 0 T − 1 R i ( t − j ) R_{mean} (t,T)= \frac{1}{T}\sum_{j=0}^{T-1}{R_i(t-j)} Rmean(t,T)=T1j=0∑T−1Ri(t−j)
从本质上讲,算术平均收益率暗含回溯周期内各时间点的收益率对当前时刻动量的造影响是相等的。
在t时刻,股票i的时间长度为T的风险调整后收益率有:
R i a d j ( T ) = R i m e a n σ i R_i^{adj}(T) = \frac {R_i^{mean}}{\sigma_i} Riadj(T)=σiRimean
其中:
σ i 2 = 1 T − 1 ∑ j = 0 T − 1 [ R i ( t − j ) − R i m e a n ( T ) ] 2 \sigma_i^2 = \frac{1}{T-1}\sum_{j=0}^{T-1}{[R_i(t-j) -R_i^{mean}(T)]^2} σi2=T−11j=0∑T−1[Ri(t−j)−Rimean(T)]2
可以看出,风险调整后的收益率考虑的是一种“性价比”,或者也可以叫做“信息比率(Information Ratio)”:每单位风险上的收益越高,意味着更加“健康”的增长动力。
注:值得注意的是,这里使用的风险指标sigma将股票增长与下跌的风险“一视同仁”。在指标构建的时候,也可以将风险指标sigma转换成单向风险指标,例如VaR,cVaR等。
在t时刻,股票i的时间长度为T的指数加权平均收益率有:
R EWM ( λ , T ) = 1 − λ T 1 − λ ∑ j = 0 T − 1 1 λ R i ( t − j ) R_{\text{EWM}} (\lambda, T)= \frac{1-\lambda^T}{1-\lambda}\sum_{j=0}^{T-1}{\frac{1}{\lambda}R_i(t-j)} REWM(λ,T)=1−λ1−λTj=0∑T−1λ1Ri(t−j)
注:与算术平均收益率不同,指数加权平均收益率暗含靠近0时刻的收益率相比于更早前的收益率对动量有更大的影响,且这种影响随着时间的推移以指数形式变弱。
由于到2019年为止,中国的股票市场在做空股票方面还有诸多的限制,现在的融券成本也比较高,限制也比较多,因此本文只考虑纯多头策略(只允许买多和减仓,不允许卖空)。在这一策略中,我们忽略操作的交易成本(在非频繁交易的情况下,手续费用可以近似为0(一般是一笔交易总额的万分之三和5元的最大值)),并且假设股票可以以当日的收盘价格在当日进行交易。
策略流程如下:
Step 0: 初始化。设定初始资金M0,假设在期初是空仓状态(Qi(0)=0)。给定回溯周期T,回测区间长度n,股票标的 i = 1, …, N,设定持仓的最大股票数量m(小于N),设置交易频率△t, 设置回测0时刻点。进入Step 1;
Step 1: 令t = t+1。当持仓个股满足规定的调仓/平仓条件时(本文以每日动态调仓为例)进入Step2,否则返回Step1;
Step 2: 计算当前时刻的各股票的价格动量指标I(t);
Step 3: 将各股票的价格动量指标I(t)从大到小排序,并对最大的m个动量指标所对应的m只股票进行买入操作,对动量指标最小的m只股票进行卖出操作。股票i在该日所应持有的权重应该满足:
ω ( i ) ( t ) = { σ ( i ) ( t ) ∑ j = 1 m σ ( j ) ( t ) , i = 1 , . . . , m 0 , o t h e r w i s e \omega_{(i)} (t)=\begin{cases} \frac{\sigma_{(i)}(t)}{\sum_{j=1}^m\sigma_{(j)}(t)}, i= 1,..., m \\ \qquad 0 \quad ,\enspace otherwise \end{cases} ω(i)(t)={ ∑j=1mσ(j)(t)σ(i)(t),i=1,...,m0,otherwise
也可以规定:
ω ( i ) ( t ) = { σ ( i ) 2 ( t ) ∑ j = 1 m σ ( j ) 2 ( t ) , i = 1 , . . . , m 0 , o t h e r w i s e \omega_{(i)} (t)=\begin{cases} \frac{\sigma_{(i)}^2(t)}{\sum_{j=1}^m\sigma_{(j)}^2(t)}, i= 1,..., m \\ \qquad 0 \quad ,\enspace otherwise \end{cases} ω(i)(t)=⎩⎨⎧∑j=1mσ(j)2(t)σ(i)2(t),i=1,...,m0,otherwise
得到其所对应的持仓数量应该为:
Q i ( t ) = round ( ω i ( t ) ⋅ M ( t ) 100 P i ( t ) ) ⋅ 100 , i = 1 , . . . , N Q_{i}(t) = \text{round}(\frac{ \omega_{i} (t) \cdot M(t) } {100 P_{i}(t) }) \cdot 100, i = 1, ..., N Qi(t)=round(100Pi(t)ωi(t)⋅M(t))⋅100,i=1,...,N
仓位变动为
Δ Q i ( t − Δ t , t ) = Q i ( t ) − Q i ( t − Δ t ) , i = 1 , . . . , N \Delta Q_{i}(t-\Delta t, t) = Q_{i}(t) - Q_{i}(t-\Delta t), i = 1, ..., N ΔQi(t−Δt,t)=Qi(t)−Qi(t−Δt),i=1,...,N
对应的当期收益率为:
r ( t ) = ∑ i = 1 N Q i ( t − 1 ) ⋅ ( P i ( t ) − P i ( t − 1 ) ) ) M ( t − 1 ) r(t) = \frac{\sum_{i=1}^{N} {Q_{i}(t-1)\cdot(P_{i}(t)-P_{i}(t-1)))}}{M(t-1)} r(t)=M(t−1)∑i=1NQi(t−1)⋅(Pi(t)−Pi(t−1)))
累计收益率为:
r c u m ( t ) = ∏ τ = 1 t ( r ( τ ) + 1 ) − 1 r^{cum}(t) = \prod_{\tau=1}^{t}{(r(\tau)+1)} -1 rcum(t)=τ=1∏t(r(τ)+1)−1
注:这里可以根据自己的交易喜好增加额外的限制条件,如:每次所需调仓的数量必须高于10手,否则不调仓
Step 4: 重复Step 1 ~ Step 3,直至到达回测区间终点。
Step 5: 绘制组合策略的净值图与基准组合(如沪深300)的净值图,验证策略的有效性。
Python3 代码如下:
(1)导入相关包
## Section 1: Import relative packages
import math
import tushare as ts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# print(ts.__version__) # check verision (I use 1.2.45 Pro)
(2)获取股票标签(以上证50为例)
## Section 2: Get stock tickers
# stock_list = ts.get_hs300s() # Get HS300 Index stocks
stock_list = ts.get_sz50s() # Get SZ50 Index stocks
tickers = stock_list["code"] # Store the tickers into a list
N = len(tickers) # N stands for the number of stocks we use (To make it simple)
Start_Date = '2018-10-23' # Set look-back start date
End_Date = '2019-10-30' # Set look-back end date
(3)下载并记录数据
## Section 3: Download the data and Store them into a lsit
e = [] # Data Stored in a list
for i in range(N):
# elm = ts.get_h_data(tickers[i], start='2019-10-23', end='2019-10-30') #前复权
# ts.get_h_data('002337', autype='hfq') #后复权
# ts.get_h_data('002337', autype=None) #不复权
# ts.get_h_data('002337', start='2015-01-01', end='2015-03-16') #两个日期之间的前复权数据
elm = ts.get_hist_data(tickers[i],start = Start_Date, end = End_Date)
print("Stock: %s collected successfully!" % tickers[i])
e.append(elm)
(4)设定初始参数
## Section 4: Initiate the parameters
T = 21 # 回溯周期长度
dt = 1 # 交易频率(单位:日)
m = 10 # 选股数量(每一次交易)
n = e[0].shape[0] # 总回测时间长度
(5)计算每天的日收益率、累计收益率、平均收益率、调整后收益率等
## Section 5: Calculate the daily return of each stock; Store in a DataFrame
P = pd.DataFrame( index = e[0].index, columns = tickers)
Daily_Return = pd.DataFrame( index = e[0].index, columns = tickers)
Mean_Return = pd.DataFrame( index = e[0].index, columns = tickers)
Cum_Return = pd.DataFrame( index = e[0].index, columns = tickers)
Sigma = pd.DataFrame( index = e[0].index, columns = tickers)
for i in range(N):
P[tickers[i]][0] = e[i]["close"][0]
for j in range(n-1):
P[tickers[i]][j] = e[i]["close"][j]
Daily_Return[tickers[i]][j] = ( e[i]["close"][j] / e[i]["close"][j+1] - 1 )
for i in range(N):
n = e[i].shape[0]
for j in range(n - T + 1):
sum_r = 0
for s in range(j, j+T):
sum_r += Daily_Return[tickers[i]][s]
Mean_Return[tickers[i]][j] = sum_r / T
sum_sigma = 0
for s in range(j, j+T):
sum_sigma += ( Daily_Return[tickers[i]][s] - Mean_Return[tickers[i]][j] ) ** 2
Sigma[tickers[i]][j] = math.sqrt(sum_sigma) / (T-1)
Cum_Return[tickers[i]][j] = e[i]["close"][j] / e[i]["close"][j+T-1] - 1
(6)计算每天的权重,并记录调仓信息(未完待续)
## Section 5: Calculate the daily return of each stock; Store in a DataFrame
weights = pd.DataFrame(0, index = e[0].index, columns = tickers)
for i in range(n-T):
tem_series = Cum_Return.iloc[i,:]
tem_series = tem_series.sort_values(ascending=False)
sigma_sum = 0
for j in range(m):
sigma_sum += Sigma.ix[i, tem_series.index[j]]
for j in range(m):
weights.ix[i, tem_series.index[j]] = Sigma.ix[i, tem_series.index[j]] / sigma_sum
weights_na_rm = weights.dropna(axis = 0, how = "any")
Q = weights_na_rm * 5e5 / P
print(Q)
##未完待续
价格动量是以价格的收益率作为衡量动量的指标。与价格动量不同的是,移动平均线动量策略则是以某一段历史时段内价格的历史平均值作为衡量股票动量大小的指标,这也就是我们在K线图中经常能看到的MA线。移动平均线(Moving Average, MA)包括简单移动平均线(Simple Moving Average, SMA)和指数移动平均线(Exponential Moving Average, EMA)。下面,我们就来介绍一下这两种平均线的构造。
简单移动平均线(SMA),即为从最近的历史时刻为起点,以某一给定的时间T所构成的历史价格的简单平均值,其表达式为:
SMA i ( T ) = 1 T ∑ t = − 1 − T P i ( t ) , i = 1 , . . . , N \text{SMA}_i(T)= \frac{1}{T}\sum_{t=-1}^{-T}{P_i(t)}, i = 1, ..., N SMAi(T)=T1t=−1∑−TPi(t),i=1,...,N
指数移动平均线(EMA),即为从最近的历史时刻为起点,以某一给定的时间T所构成的历史价格的指数加权平均值,其表达式为:
EWM i ( λ , T ) = ∑ t = − 1 − T λ t P i ( t ) ∑ t = − 1 − T λ t = 1 − λ 1 − λ T ∑ t = − 1 − T λ t P i ( t ) , 0 < λ < 1 , i = 1 , . . . , N \text {EWM}_i (\lambda, T)= \frac{\sum_{t=-1}^{-T}{\lambda^{t} P_i(t)}}{\sum_{t=-1}^{-T}{\lambda^{t} }}=\frac{1-\lambda}{1-\lambda^T}\sum_{t=-1}^{-T}{\lambda^{t} P_i(t)}, 0<\lambda<1, i = 1, ..., N EWMi(λ,T)=∑t=−1−Tλt∑t=−1−TλtPi(t)=1−λT1−λt=−1∑−TλtPi(t),0<λ<1,i=1,...,N
其中:λ为衰减比率,是一个给定常数。
SMA与EMA的关系:从上式可以看出,λ越大,历史价格的影响越大;当λ=1时,EMA与SMA等价。(挺押韵的。嗯。)
注:可能会有朋友问了,我们在策略中应该用SMA还是EMA呀?哪个平均线会更好呢?实际上,我们在评判指标好坏的时候,需要问自己两个问题:(1)所选择的指标否能给我们带来丰厚的盈利?(2)这种盈利性是否在过去和未来都会保持稳定?但是一般来说,任何一种指标都不能在全方面好于另一个指标,我们需要再盈利的大小和盈利的稳定性之间做出一个权衡(trade-off)。因此,人的决策在量化交易之中也是十分关键的。
这是最简单的移动平均线策略。顾名思义,在这一策略中,我们只使用其中一条移动平均线来构造股票的买卖信号。当股票价格向上穿过MA的时候,我们认为未来一段时间内股票有较强的上涨趋势,此时我们应该买入;而当股票价格向下穿过MA的时候,我们认为未来一段时间内股票有较强的下跌趋势,此时我们应该卖出或卖空。
假设A股市场不能卖空,我们仅考虑纯多头策略(Long-only Strategy)。事实上,这个策略也可以构造成货币中性策略(Money-Neutral Strategy )。
策略流程如下:
Step 0: 初始化。设定初始资金M0,假设在期初是空仓状态(Qi(0)=0)。给定回测区间长度n,股票标的 i = 1, …, N, 设置回测0时刻点 t=0。进入Step 1;
Step 1: 令t = t+1。构建如下交易信号(金死叉信号):
s i g n a l i = { 1 , if MA i ( t ) < P i ( t ) and MA i ( t − 1 ) > P i ( t − 1 ) − 1 , if MA i ( t ) > P i ( t ) and MA i ( t − 1 ) < P i ( t − 1 ) 0 , o t h e r w i s e , i = 1 , . . . , N signal_i = \begin{cases} \space\space 1\space,\text {if MA} _i(t)
其中,1表示买入信号,-1表示卖出信号(如果有),0表示维持现状;
也可以使用如下信号(纯动量信号):
s i g n a l i = { 1 , if MA i ( t ) < P i ( t ) − 1 , if MA i ( t ) > P i ( t ) 0 , o t h e r w i s e , i = 1 , . . . , N signal_i = \begin{cases} \space\space 1\space,\text {if MA} _i(t)
其中,1表示买入信号(如果没有),-1表示卖出信号(如果有)。
Step 2: 设置买入权重。为了方便阐述思想,本文规定买入的权重比例均为1/N。有兴趣的朋友可以参考策略2.1中的权重构造方法尝试更多更复杂的策略
Step 3: 重复Step 1 ~ Step 2,直至到达回测区间终点。
Step 4: 绘制组合策略的净值图与基准组合(如沪深300)的净值图,验证策略的有效性。
python3 代码实现
(1) 导入相关包
## Section 1: Import relative packages
import math
import tushare as ts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# print(ts.__version__) # check verision (I use 1.2.45 Pro)
(2)获取股票标签(以上证50为例)
## Section 2: Get stock tickers
# stock_list = ts.get_hs300s() # Get HS300 Index stocks
stock_list = ts.get_sz50s() # Get SZ50 Index stocks
tickers = stock_list["code"] # Store the tickers into a list
N = len(tickers) # N stands for the number of tickers
(3)下载并记录数据
## Section 3: Download the data and Store them into a lsit
e = [] # Data Stored in a list
for i in range(N):
# elm = ts.get_h_data(tickers[i], start='2019-10-23', end='2019-10-30') #前复权
# ts.get_h_data('002337', autype='hfq') #后复权
# ts.get_h_data('002337', autype=None) #不复权
# ts.get_h_data('002337', start='2015-01-01', end='2015-03-16') #两个日期之间的前复权数据
elm = ts.get_hist_data(tickers[i],start='2018-9-30',end='2019-10-30')
print("Stock: %s collected successfully!" % tickers[i])
e.append(elm)
未完待续
未完待续
未完待续
未完待续
未完待续
未完待续
[1] Tushare Document
[2] KaTex Document
[3] 151 Trading Strategies
本系列内容仅作为学术参考和代码实例,仅供参考,不对投资决策提供任何建议。本人不承担任何人因使用本系列中任何策略、观点等内容造成的任何直接或间接损失。
欢迎感兴趣的小伙伴来跟作者一起挑刺儿~ 包括但不限于语言上的、排版上的和内容上的不足和疏漏~ 一起进步呀!
有任何问题,欢迎在本文下方留言,或者将问题发送至勘误邮箱: [email protected]
谢谢大家!
本文持续更新中。最后更新时间:11/3/2019