**导语**:配对交易(Pairs Trading)是通过一买一卖的手段来赚取两只股票走势的差价的投资策略。本文介绍如何通过协整关系实现配对交易,以及在缺乏卖空机制情况下的搬砖策略。
```
作者:Haozun,肖睿
编辑:宏观经济算命师
本文由JoinQuant量化课堂推出,难度为进阶下,理解深度为level-0。
```
阅读本文需要掌握[协整(level-0)](https://www.joinquant.com/post/1731)的知识。
$ $
#### **配对交易**
相信很多同学都了解过 Pairs Trading,即配对交易策略。其基本原理就是找出两只走势相关的股票。这两只股票的价格差距从长期来看在一个固定的水平内波动,如果价差暂时性的超过或低于这个水平,就买多价格偏低的股票,卖空价格偏高的股票。等到价差恢复正常水平时,进行平仓操作,赚取这一过程中价差变化所产生的利润。
使用这个策略的关键就是“必须找到一对价格走势高度相关的股票”,而高度相关在这里意味着在长期来看有一个稳定的价差,这就要用到协整关系的检验。
在量化课堂介绍[协整关系](https://www.joinquant.com/post/1731)的文章里,我们知道如果用 $X_t$ 和 $Y_t$ 代表两支股票价格的时间序列,并且发现它们存在协整关系,那么便存在实数 $a$ 和 $b$,并且线性组合 $Z_t=aX_t-bY_t$ 是一个(弱)平稳的序列。如果 $Z_t$ 的值较往常相比变得偏高,那么根据弱平稳性质,$Z_t$ 将回归均值,这时,应该买入 $b$ 份 $Y$ 并卖出 $a$ 份 $X$,并在 $Z_t$ 回归时赚取差价。反之,如果 $Z_t$ 走势偏低,那么应该买入 $a$ 份 $X$ 卖出 $b$ 份 $Y$,等待 $Z_t$ 上涨。所以,要使用配对交易,必须找到一对协整相关的股票。
这里要提醒读者,无论是原始的 Pairs Trading 策略,还是本篇的搬砖策略,在寻找股票对时,数据上的检验都只是辅助手段。我们首先要做的还是在基本面的角度进行分析,分析公司的主营业务,产品链,业内地位等。在此基础上,我们才会对有可能具有协整关系的股票进行数据上的检验。这是非常重要的。
$ $
#### **协整关系的检验**
我们想使用协整的特性进行配对交易,那么要怎么样发现协整关系呢?
在 Python 的 Statsmodels 包中,有直接用于协整关系检验的函数 coint,该函数包含于 statsmodels.tsa.stattools 中。
首先,我们构造一个读取股票价格,判断协整关系的函数。该函数返回的两个值分别为协整性检验的 p 值矩阵以及所有传入的参数中协整性较强的股票对。我们不需要在意 p 值具体是什么,可以这么理解它: p 值越低,协整关系就越强;p 值低于 $0.05$ 时,协整关系便非常强。
```
import numpy as np
import pandas as pd
import statsmodels.api as sm
import 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 = []
# 对于每一个i
for i in range(n):
# 对于大于i的j
for j in range(i+1, n):
# 获取相应的两只股票的价格Series
stock1 = 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.05
if pvalue < 0.05:
# 记录股票对和相应的p值
pairs.append((keys[i], keys[j], pvalue))
# 返回结果
return pvalue_matrix, pairs
```
其次,我们挑选10只银行股,认为它们是业务较为相似,在基本面上具有较强联系的股票,使用上面构建的函数对它们进行协整关系的检验。在得到结果后,用热力图画出各个股票对之间的 p 值,较为直观地看出他们之间的关系。
我们的测试区间为2014年1月1日至2015年1月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
```
![图1.png][1]
可以看出,上述10只股票中有5对具有较为显著的协整性关系的股票对(红色表示协整关系显著)。我们选择使用其中 p 值最低($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')
```
![图2.png][2]
接下来,我们用这两支股票的价格来进行一次[OLS线性回归](https://www.joinquant.com/post/1786),以此算出它们是以什么线性组合的系数构成平稳序列的。
```
x = stock_df1
y = stock_df2
X = sm.add_constant(x)
result = (sm.OLS(y,X)).fit()
print(result.summary())
```
![图3.png][3]
系数是 $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')
```
![图4.png][4]
设中国银行的股价为 $Y$,工商银行为 $X$,回归拟合的结果是
$Y=-0.7248+0.9938\cdot X$
也就是说 $Y-0.9938\cdot 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"])
```
![图5.png][5]
$ $
#### **买卖时机的判断**
这里,我们先介绍一下 z-score。z-score 是对时间序列偏离其均值程度的衡量,表示时间序列偏离了其均值多少倍的标准差。首先,我们定义一个函数来计算 z-score:
一个序列在时间 $t$ 的 z-score,是它在时间 $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"])
```
![图6.png][6]
我们认为,当两这个序列的 z-score 突破 $1$ 或者 $-1$ 时,说明两支股票的价差脱离了统计概念中的合理区间,如果它们的协整关系能够保持,那么它们的价差应该收敛。所以,在发现上述序列突破 $1$ 或 $-1$ 时,应该按照比例买多一支股票并做空另外一支,从而赚取之后收敛的差价。
结合上图,当 z-score 突破上方红线时,说明工商银行的价格相对于中国银行高估,因此我们买入 $1$ 份中国银行并卖空 $0.9938$ 份工商银行(系数根据前面的线性回归得出),并当 z-score 回归于 $0$ 时清仓获利。如果 z-score 突破下方绿线的话,反方向操作即可。
$ $
#### **中国式 Pairs Trading 策略:搬砖**
标准的配对交易策略是通过一买一卖的行为来对冲掉系统性风险,从而以较低的风险赚取到持续的利润。但目前A股市场是不允许进行直接卖空操作的。融券的渠道我等散户又搞不定。因此,我们对原始的配对交易进行修改,只进行做多操作,不进行做空操作。这种操作被形象的成为“搬砖”。其目标不是追求绝对收益,而是追求收益率比一直持有一个股票的高。
在选定一组协整关系为 $aX-bY$ 的股票后,我们有以下策略:
$\bullet$ 选定比例 $p$ 和 $q$,初始仓位为 $p\%$ 的 $X$ 和 $q\%$ 的 $Y$。选定测试 z-score 天数的参数 $\text{test_days}$。
$\bullet$ 每天执行:
$\qquad$\- 计算两支股票的线性组合序列 $aX-bY$ 在过去 $\text{test_days}$ 的标准差和均值,以此计算 z-score。
$\qquad$\- 如果当天 z-score 高出 $1$,则将仓位调整为全仓 $Y$。如果当天 z-score 小于 $-1$,则将仓位调整为全仓 $X$。
$\qquad$\- 如果上一交易日处于全仓一支股票的状态,并且今日 z-score 回归 $0$ 点,则调整回至 $p\%$ 和 $q\%$ 的比例。
在之前章节中我们通过分析发现,工商银行和中国银行在2014年全年有着非常强的协整关系,但我们不能在14年进行回测,因为这样等同于使用未来函数(比如文章最后的回测),因此我们在2015至2016的区间内进行回测。使用的参数是 $p\%=50\%$,$q\%=50\%$,$\text{test_days}=120$。
由于没有对冲机制,所以不能依靠绝对收益评估策略的效益,而要用它和配对的两支股票对比。如果分别跑赢了两支股票,那么说明该策略是有效的。
首先是和工商银行的对比,我们的策略完全跑赢了这支股票。
![图7.png][7]
其次是和中国银行的对比,虽然前半年被工商银行拖了后腿,但后半年还是稳稳地跑赢了。
![图8.png][8]
本策略全程满仓,持有股票只限于以上两支。通过判断两支股票的相对强度进行仓位调整,最后收益胜过了其中的任何一支,效果是非常显著的。
$ $
#### **策略的应用**
搬砖的策略能跑赢个股,但也只能跑赢个股,所以若想投入使用并赚取稳定收益,还要结合其他策略一起使用。比如,使用基本面或者技术面的思路来判断银行指数在未来一段时间中的走势。如果趋势乐观,则可以在指数成分股中选取两支具有协整性质的个股进行搬砖。如果趋势不乐观,则空仓对待。这样,通过搬砖,在其他策略之上产生更多的超额收益。
$ $
#### **代码说明书与变量说明书**
函数说明书
![函数说明书.jpg][9]
全局变量说明书
![全局变量说明书.jpeg][10]
$ $
```
本文由JoinQuant量化课堂推出,版权归JoinQuant所有,商业转载请联系我们获得授权,非商业转载请注明出处。
文章更迭记录:
v1.2,2016-07-28,更改难度标签
v1.1,2016-07-16,更新函数说明书和回测
v1.0,2016-07-15,文章上线
```
[1]: https://image.joinquant.com/9b2ddd88ae0d89a5a8e13cfea2cb82be
[2]: https://image.joinquant.com/e57c4648ef4c39b9859751e5d2d68f7c
[3]: https://image.joinquant.com/9d1d7e3c0e445ff1d97fba77ea9e8d0d
[4]: https://image.joinquant.com/a3a2960cb4e6a082d17efc83a477e89a
[5]: https://image.joinquant.com/6b1717417a9514f44f9fffc74ed04d10
[6]: https://image.joinquant.com/a12f73b535868b9bebd2457b35459c72
[7]: https://image.joinquant.com/55d25f153206f8d6ee74d178e557edbf
[8]: https://image.joinquant.com/b1af6445788d203f68144d77a0540ea7
[9]: https://image.joinquant.com/7c7d6a21a8c4fa9c46a880d9d150b135
[10]: https://image.joinquant.com/122e0784e823f7cf0ceef92c1eb13d37
那个代码说明书和全局变量说明书不错,值得学习借鉴。
2016-07-16
有一个问题:
```
prices_df = get_price(stock_list, start_date="2014-01-01", end_date="2015-01-01", frequency="daily", fields=["close"])["close"]
```
我们用的是2014-01-01至2015-01-01的数值来计算相关性,得出系数是 0.9938
但是在回测过程中,时间是2010年开始,似乎有未来函数的嫌疑,系数 0.9938对回测结果是有巨大关联的
2016-07-28
@旮旯忐忑 我觉得这个应该不算问题,从2015年开始回测就行了么
2016-07-29
要不要把这个协整性的计算搞成实时的,选股也搞成实时的,我记得好像以前有个类似的策略,实时计算协整性选股
2016-07-29
@旮旯忐忑 @kuhn 哈哈,是的,结尾的回测确实有未来函数,不过这就是为了把曲线拉长一点比较好看,文中有一个地方也说了
```
在之前章节中我们通过分析发现,工商银行和中国银行在2014年全年有着非常强的协整关系,但我们不能在14年进行回测,因为这样等同于使用未来函数(比如文章最后的回测)
```
正文中的两个回测图片是正规的使用方法。
动态调整协整配对是一个很好的办法。这篇文章介绍的只是协整搬砖的内核,就像一个发动机,其他的配件和功能大家可以自己去探索。
2016-07-29
请问p和q应该如何优化?50%是应为回归参数是0.9938?理论上说p是不是等于aX-bY这个公式里的a?q等于b?
2016-07-30
@oliversea 我们没法卖空,不能进行对冲,所以只能按照一定比例配置原本仓位来“模拟 ”对冲。直觉上是应该按 a 和 b 的比例分配 p 和 q,但问题在于 a 和 b 是由收益率计算的,而 p 和 q 是按价格分配的,稍作计算会发现(除了极端情况)p 和 q 设为多少都没法模拟对冲。并且事实上,在我们这个模型中,当我们按原始比例 p 和 q 持有股票时,两支股票的走势应该是大致趋同的,所以 p 和 q 并无大所谓,干脆设成 50% 或者凭感觉设就好。
2016-07-31
换一个年度,换一对股票,效果就没那么好了,甚至跑不过参照基准,test day 选120天也经过优化? 换100天效果就变差了。
还得继续深入研究。
2016-08-01
本人小白一个,有一个问题请教哈(提的问题也有可能是错的,见谅哈)~在进行回测的时候首先获得了2015年的历史数据,然后用2015年的全年的历史数据计算出stable_series的均值,然后再拿这个均值与每个stable_series的历史价格计算z_score。可是当我们实盘的时候是无法提前获得未来一整年的全部价格来计算均值,所以本文源码中计算的均值用全年数据来算是不是有点未卜先知啊?所以我想,计算的均值是不是应该根据每天(每分钟)新获得的数据不断进行更新,以这个“动态的均值”与每个新的stabel_series值计算z_score作为调仓信号啊?
2016-08-02
@柯克船长 是的,这里介绍的只是协整交易的内核,并不是一个成熟的交易体系,大家可以在这之上自己去探索或优化。
2016-08-02