导语:配对交易(Pairs Trading)是通过一买一卖的手段来赚取两只股票走势的差价的投资策略。本文介绍如何通过协整关系实现配对交易,以及在缺乏卖空机制情况下的搬砖策略。
阅读本文需要掌握协整(level-0)的知识。
相信很多同学都了解过 Pairs Trading,即配对交易策略。其基本原理就是找出两只走势相关的股票。这两只股票的价格差距从长期来看在一个固定的水平内波动,如果价差暂时性的超过或低于这个水平,就买多价格偏低的股票,卖空价格偏高的股票。等到价差恢复正常水平时,进行平仓操作,赚取这一过程中价差变化所产生的利润。
使用这个策略的关键就是“必须找到一对价格走势高度相关的股票”,而高度相关在这里意味着在长期来看有一个稳定的价差,这就要用到协整关系的检验。
在量化课堂介绍协整关系的文章里,我们知道如果用 XtXt
Xt
X_t和 YtYtYt
Y_t代表两支股票价格的时间序列,并且发现它们存在协整关系,那么便存在实数 aaa
a和 bbb
b,并且线性组合 Zt=aXt?bYtZt=aXt?bYtZt=aXt?bYt
Z_t=aX_t-bY_t是一个(弱)平稳的序列。如果 ZtZtZt
Z_t的值较往常相比变得偏高,那么根据弱平稳性质,ZtZtZt
Z_t将回归均值,这时,应该买入 bbb
b份 YYY
Y并卖出 aaa
a份 XXX
X,并在 ZtZtZt
Z_t回归时赚取差价。反之,如果 ZtZtZt
Z_t走势偏低,那么应该买入 aaa
a份 XXX
X卖出 bbb
b份 YYY
Y,等待 ZtZtZt
Z_t上涨。所以,要使用配对交易,必须找到一对协整相关的股票。
这里要提醒读者,无论是原始的 Pairs Trading 策略,还是本篇的搬砖策略,在寻找股票对时,数据上的检验都只是辅助手段。我们首先要做的还是在基本面的角度进行分析,分析公司的主营业务,产品链,业内地位等。在此基础上,我们才会对有可能具有协整关系的股票进行数据上的检验。这是非常重要的。
我们想使用协整的特性进行配对交易,那么要怎么样发现协整关系呢?
在 Python 的 Statsmodels 包中,有直接用于协整关系检验的函数 coint,该函数包含于 statsmodels.tsa.stattools 中。
首先,我们构造一个读取股票价格,判断协整关系的函数。该函数返回的两个值分别为协整性检验的 p 值矩阵以及所有传入的参数中协整性较强的股票对。我们不需要在意 p 值具体是什么,可以这么理解它: p 值越低,协整关系就越强;p 值低于 0.050.05
0.05
0.05时,协整关系便非常强。
import numpy as npimport pandas as pdimport statsmodels.api as smimport seaborn as sns
# 输入是一DataFrame,每一列是一支股票在每一日的价格def find_cointegrated_pairs(dataframe): # 得到DataFrame长度n = dataframe.shape[1] # 初始化p值矩阵pvalue_matrix = np.ones((n, n)) # 抽取列的名称keys = dataframe.keys() # 初始化强协整组pairs = [] # 对于每一个ifor i in range(n): # 对于大于i的jfor j in range(i 1, n): # 获取相应的两只股票的价格Seriesstock1 = dataframe[keys[i]]stock2 = dataframe[keys[j]] # 分析它们的协整关系result = sm.tsa.stattools.coint(stock1, stock2) # 取出并记录p值pvalue = result[1]pvalue_matrix[i, j] = pvalue # 如果p值小于0.05if pvalue < 0.05: # 记录股票对和相应的p值pairs.append((keys[i], keys[j], pvalue)) # 返回结果return pvalue_matrix, pairs
其次,我们挑选10只银行股,认为它们是业务较为相似,在基本面上具有较强联系的股票,使用上面构建的函数对它们进行协整关系的检验。在得到结果后,用热力图画出各个股票对之间的 p 值,较为直观地看出他们之间的关系。
我们的测试区间为2014年1月1日至2015年1月1日。热力图画出的是 11
1
1减去 p 值,因此颜色越红的地方表示 p 值越低。
stock_list = ["002142.XSHE", "600000.XSHG", "600015.XSHG", "600016.XSHG", "600036.XSHG", "601009.XSHG", "601166.XSHG", "601169.XSHG", "601328.XSHG", "601398.XSHG", "601988.XSHG", "601998.XSHG"] prices_df = get_price(stock_list, start_date="2014-01-01", end_date="2015-01-01", frequency="daily", fields=["close"])["close"] pvalues, pairs = find_cointegrated_pairs(prices_df) sns.heatmap(1-pvalues, xticklabels=stock_list, yticklabels=stock_list, cmap='RdYlGn_r', mask = (pvalues == 1)) print pairs
可以看出,上述10只股票中有5对具有较为显著的协整性关系的股票对(红色表示协整关系显著)。我们选择使用其中 p 值最低(0.01060.0106
0.0106
0.0106)的工商银行(601398.XSHG)和中国银行(601988.XSHG)这一对股票来进行研究。首先调取工商银行和中国银行的历史股价,画出两只股票的价格走势。
stock_df1 = prices_df["601398.XSHG"] stock_df2 = prices_df["601988.XSHG"] plot(stock_df1); plot(stock_df2) plt.xlabel("Time"); plt.ylabel("Price") plt.legend(["601988.XSHG", "601998.XSHG"],loc='best')
接下来,我们用这两支股票的价格来进行一次OLS线性回归,以此算出它们是以什么线性组合的系数构成平稳序列的。
x = stock_df1 y = stock_df2 X = sm.add_constant(x) result = (sm.OLS(y,X)).fit() print(result.summary())
系数是 0.99380.9938
0.9938
0.9938,画出数据和拟合线。
fig, ax = plt.subplots(figsize=(8,6)) ax.plot(x, y, 'o', label="data") ax.plot(x, result.fittedvalues, 'r', label="OLS") ax.legend(loc='best')
设中国银行的股价为 YY
Y
Y,工商银行为 XXX
X,回归拟合的结果是
Y=?0.7248 0.9938?XY=?0.7248 0.9938?XY=?0.7248 0.9938?X
也就是说 Y?0.9938?XY?0.9938?XY?0.9938?X
是平稳序列。
依照这个比例,我们画出它们价差的平稳序列。可以看出,虽然价差上下波动,但都会回归中间的均值。
plot(0.9938*stock_df1-stock_df2);plt.axhline((0.9938*stock_df1-stock_df2).mean(), color="red", linestyle="")plt.xlabel("Time"); plt.ylabel("Stationary Series")plt.legend(["Stationary Series", "Mean"])
这里,我们先介绍一下 z-score。z-score 是对时间序列偏离其均值程度的衡量,表示时间序列偏离了其均值多少倍的标准差。首先,我们定义一个函数来计算 z-score:
一个序列在时间 tt
t
t的 z-score,是它在时间 ttt
t的值,减去序列的均值,再除以序列的标准差后得到的值。
def zscore(series):return (series - series.mean()) / np.std(series)
对于工商银行与中国银行的平稳线性组合,用上面的函数计算 z-score 并绘出图。
plot(zscore(0.9938*stock_df1-stock_df2))plt.axhline(zscore(0.9938*stock_df1-stock_df2).mean(), color="black")plt.axhline(1.0, color="red", linestyle="")plt.axhline(-1.0, color="green", linestyle="")plt.legend(["z-score", "mean", " 1", "-1"])
我们认为,当两这个序列的 z-score 突破 11
1
1或者 ?1?1?1
-1时,说明两支股票的价差脱离了统计概念中的合理区间,如果它们的协整关系能够保持,那么它们的价差应该收敛。所以,在发现上述序列突破 111
1或 ?1?1?1
-1时,应该按照比例买多一支股票并做空另外一支,从而赚取之后收敛的差价。
结合上图,当 z-score 突破上方红线时,说明工商银行的价格相对于中国银行高估,因此我们买入 11
1
1份中国银行并卖空 0.99380.99380.9938
0.9938份工商银行(系数根据前面的线性回归得出),并当 z-score 回归于 000
0时清仓获利。如果 z-score 突破下方绿线的话,反方向操作即可。
标准的配对交易策略是通过一买一卖的行为来对冲掉系统性风险,从而以较低的风险赚取到持续的利润。但目前A股市场是不允许进行直接卖空操作的。融券的渠道我等散户又搞不定。因此,我们对原始的配对交易进行修改,只进行做多操作,不进行做空操作。这种操作被形象的成为“搬砖”。其目标不是追求绝对收益,而是追求收益率比一直持有一个股票的高。
在选定一组协整关系为 aX?bYaX?bY
aX?bY
aX-bY的股票后,我们有以下策略:
???
选定比例 ppp
p和 qqq
q,初始仓位为 p%p%p%
p\%的 XXX
X和 q%q%q%
q\%的 YYY
Y。选定测试 z-score 天数的参数 test_daystest_daystest_days
\text{test_days}。
???
每天执行:
- 计算两支股票的线性组合序列 aX?bYaX?bYaX?bY
aX-bY在过去 test_daystest_daystest_days
\text{test_days}的标准差和均值,以此计算 z-score。
- 如果当天 z-score 高出 111
1,则将仓位调整为全仓 YYY
Y。如果当天 z-score 小于 ?1?1?1
-1,则将仓位调整为全仓 XXX
X。
- 如果上一交易日处于全仓一支股票的状态,并且今日 z-score 回归 000
0点,则调整回至 p%p%p%
p\%和 q%q%q%
q\%的比例。
在之前章节中我们通过分析发现,工商银行和中国银行在2014年全年有着非常强的协整关系,但我们不能在14年进行回测,因为这样等同于使用未来函数(比如文章最后的回测),因此我们在2015至2016的区间内进行回测。使用的参数是 p%=50%p%=50%
p%=50%
p\%=50\%,q%=50%q%=50%q%=50%
q\%=50\%,test_days=120test_days=120test_days=120
\text{test_days}=120。
由于没有对冲机制,所以不能依靠绝对收益评估策略的效益,而要用它和配对的两支股票对比。如果分别跑赢了两支股票,那么说明该策略是有效的。
首先是和工商银行的对比,我们的策略完全跑赢了这支股票。
其次是和中国银行的对比,虽然前半年被工商银行拖了后腿,但后半年还是稳稳地跑赢了。
本策略全程满仓,持有股票只限于以上两支。通过判断两支股票的相对强度进行仓位调整,最后收益胜过了其中的任何一支,效果是非常显著的。
搬砖的策略能跑赢个股,但也只能跑赢个股,所以若想投入使用并赚取稳定收益,还要结合其他策略一起使用。比如,使用基本面或者技术面的思路来判断银行指数在未来一段时间中的走势。如果趋势乐观,则可以在指数成分股中选取两支具有协整性质的个股进行搬砖。如果趋势不乐观,则空仓对待。这样,通过搬砖,在其他策略之上产生更多的超额收益。
函数说明书
全局变量说明书
本文由JoinQuant量化课堂推出,版权归JoinQuant所有,商业转载请联系我们获得授权,非商业转载请注明出处。 文章更迭记录:v1.2,2016-07-28,更改难度标签v1.1,2016-07-16,更新函数说明书和回测v1.0,2016-07-15,文章上线
import numpy as npimport pandas as pdimport statsmodels.api as smimport seaborn as sns
# 输入是一DataFrame,每一列是一支股票在每一日的价格def find_cointegrated_pairs(dataframe):# 得到DataFrame长度n = dataframe.shape[1]# 初始化p值矩阵pvalue_matrix = np.ones((n, n))# 抽取列的名称keys = dataframe.keys()# 初始化强协整组pairs = []# 对于每一个ifor i in range(n):# 对于大于i的jfor j in range(i+1, n):# 获取相应的两只股票的价格Seriesstock1 = dataframe[keys[i]]stock2 = dataframe[keys[j]]# 分析它们的协整关系result = sm.tsa.stattools.coint(stock1, stock2)# 取出并记录p值pvalue = result[1]pvalue_matrix[i, j] = pvalue# 如果p值小于0.05if pvalue < 0.05:# 记录股票对和相应的p值pairs.append((keys[i], keys[j], pvalue))# 返回结果return pvalue_matrix, pairs
stock_list = ["002142.XSHE", "600000.XSHG", "600015.XSHG", "600016.XSHG", "600036.XSHG", "601009.XSHG", "601166.XSHG", "601169.XSHG", "601328.XSHG", "601398.XSHG", "601988.XSHG", "601998.XSHG"]prices_df = get_price(stock_list, start_date="2014-01-01", end_date="2015-01-01", frequency="daily", fields=["close"])["close"]pvalues, pairs = find_cointegrated_pairs(prices_df)sns.heatmap(1-pvalues, xticklabels=stock_list, yticklabels=stock_list, cmap='RdYlGn_r', mask = (pvalues == 1))print pairs
[('600000.XSHG', '600015.XSHG', 0.03384597872190559), ('600000.XSHG', '601988.XSHG', 0.012356888964434419), ('600015.XSHG', '600036.XSHG', 0.038816934807400762), ('601166.XSHG', '601988.XSHG', 0.044783082564135782), ('601398.XSHG', '601988.XSHG', 0.010624349301350767)]
stock_df1 = prices_df["601398.XSHG"]stock_df2 = prices_df["601988.XSHG"]plot(stock_df1); plot(stock_df2)plt.xlabel("Time"); plt.ylabel("Price")plt.legend(["601398.XSHG", "601988.XSHG"],loc='best')
<matplotlib.legend.Legend at 0x7f1e8e60bb10>
x = stock_df1y = stock_df2X = sm.add_constant(x) result = (sm.OLS(y,X)).fit()print(result.summary())
OLS Regression Results ============================================================================== Dep. Variable: 601988.XSHG R-squared: 0.972 Model: OLS Adj. R-squared: 0.972 Method: Least Squares F-statistic: 8437. Date: Fri, 15 Jul 2016 Prob (F-statistic): 1.09e-190 Time: 20:43:25 Log-Likelihood: 363.71 No. Observations: 245 AIC: -723.4 Df Residuals: 243 BIC: -716.4 Df Model: 1 Covariance Type: nonrobust =============================================================================== coef std err t P>|t| [95.0% Conf. Int.] - const -0.7248 0.034 -21.117 0.000 -0.792 -0.657 601398.XSHG 0.9938 0.011 91.851 0.000 0.972 1.015 ============================================================================== Omnibus: 7.092 Durbin-Watson: 0.225 Prob(Omnibus): 0.029 Jarque-Bera (JB): 8.337 Skew: 0.256 Prob(JB): 0.0155 Kurtosis: 3.745 Cond. No. 34.0 ============================================================================== Warnings: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
fig, ax = plt.subplots(figsize=(8,6))ax.plot(x, y, 'o', label="data")ax.plot(x, result.fittedvalues, 'r', label="OLS")ax.legend(loc='best')
<matplotlib.legend.Legend at 0x7f1e8e6700d0>
plot(0.9938*stock_df1-stock_df2);plt.axhline((0.9938*stock_df1-stock_df2).mean(), color="red", linestyle="")plt.xlabel("Time"); plt.ylabel("Stationary Series")plt.legend(["Stationary Series", "Mean"])
<matplotlib.legend.Legend at 0x7f1e9210f*>
def zscore(series):return (series - series.mean()) / np.std(series)
plot(zscore(0.9938*stock_df1-stock_df2))plt.axhline(zscore(0.9938*stock_df1-stock_df2).mean(), color="black")plt.axhline(1.0, color="red", linestyle="")plt.axhline(-1.0, color="green", linestyle="")plt.legend(["z-score", "mean", "+1", "-1"])
<matplotlib.legend.Legend at 0x7f1e8e46fe10>
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程