配对交易是统计套利中的非常经典的策略。众所周知,A股市场无法卖空个股,所以中性化的配对交易策略并不能直接“拿来主义”。但这并不妨碍我们学习配对交易的思想,将卖空改成卖出,构造适合A股市场的策略。下面我们就开始学习吧~
配对交易是基于数理分析的策略中的一个典型例子。策略的准则很简单:假设你持有一对股票X和Y,它们具有经济上的潜在联系。举例来说,比如两家公司可能生产同一类产品,或者两家公司处在同一条供应链上。如果我们将这类经济上的联系用数学方法建模,那么我们就可以从中觅得交易机会,下面我们用代码构造一个场景来阐明这一点。
import numpy as npimport pandas as pdimport statsmodelsimport statsmodels.api as sm# 用于生成随机数的种子,咱们不用管~np.random.seed(107)import matplotlib.pyplot as plt
我们假设X股票的日收益服从正态分布,然后我们画出它的累积收益图。
X_returns = np.random.normal(0, 1, 100) #用正态分布随机生成100天的收益# 将这100天的收益累加,假设原始股价为50X = pd.Series(np.cumsum(X_returns), name='X') + 50X.plot(figsize=(16,8));
现在我们来构造Y。由于我们假定Y与X有很强的联系,所以Y的价格应该与X高度相关。我们不妨对X添加一点噪声(可用正态分布),得到Y的价格。
some_noise = np.random.normal(0, 1, 100)Y = X + 5 + some_noiseY.name = 'Y'pd.concat([X, Y], axis=1).plot(figsize=(16,8));
我们已经构建了两个协整序列的例子。协整是一种比相关更微妙的关系。如果两个时间序列是协整的,那么一定存在它们的某个线性组合,围绕着其平均值在较小范围内波动。用数学的语言说,在所有的时间点上,这个线性组合构成的新随机变量服从相同的概率分布。
我们下面将X和Y的价差绘图展示。
plt.figure(figsize=(16,8))(Y - X).plot() # 将价差绘图plt.axhline((Y - X).mean(), color='red', linestyle='') # 计算价差的平均数,并用虚线在图中标出plt.xlabel('Time')plt.legend(['Price Spread', 'Mean']);
折线是X和Y的价差,虚线为价差的平均值。可以看到,折线在虚线两侧波动。
协整的定义非常直观,但我们该如何在统计意义上对协整进行检验呢?万幸万幸,“statsmodels.tsa.stattools”包中有很方便的检验工具:coint函数。我们取5%的置信水平,只需要看协整检验的p值是否大于0.05即可。
我们的例子中,p值远小于0.05,因此我们认为X和Y具有协整关系。
from statsmodels.tsa.stattools import cointscore, pvalue,array = coint(X,Y)print (pvalue)
2.0503418653412224e-16
相关和协整,概念上容易混淆,但它们并不相同。为了阐明它们的区别,我们来看看协整但不相关,和相关但不协整的例子。就从我们刚才考察的序列入手吧~
X.corr(Y)
0.9497090646385932
噢,它们的相关性也很高。但这并不奇怪,毕竟Y是根据X构造出的序列。那么,我们想找的特例究竟长什么样呢?
下面的例子就很好地说明了相关序列未必协整。源于两个正态分布生成的随机数序列,它们高度相关,但协整检验的p值大于0.05,未能通过检验。
X_returns = np.random.normal(1, 1, 100)Y_returns = np.random.normal(1, 1, 100)X_diverging = pd.Series(np.cumsum(X_returns), name='X')Y_diverging = pd.Series(np.cumsum(Y_returns), name='Y')pd.concat([X_diverging, Y_diverging], axis=1).plot(figsize=(16,8));
相关系数接近1,说明两个序列高度相关;协整检验的p值大于0.05(实际上是远远大于),说明两个序列并不存在协整关系。
print ('Correlation: ' + str(X_diverging.corr(Y_diverging)))score, pvalue, _ = coint(X_diverging,Y_diverging)print ('Cointegration test p-value: ' + str(pvalue))
Correlation: 0.9883877288043449 Cointegration test p-value: 0.9022713342921657
一个典型的例子,就是正态分布序列和方波序列。
下面我们进行构造,Y2为服从标准正态分布的序列,Y3为方波序列。
Y2 = pd.Series(np.random.normal(0, 1, 1000), name='Y2') + 20Y3 = Y2.copy()
Y3[0:100] = 30Y3[100:200] = 10Y3[200:300] = 30Y3[300:400] = 10Y3[400:500] = 30Y3[500:600] = 10Y3[600:700] = 30Y3[700:800] = 10Y3[800:900] = 30Y3[900:1000] = 10
plt.figure(figsize=(16,8))Y2.plot()Y3.plot()plt.ylim([0, 40]);
# correlation is nearly zeroprint ('Correlation: ' + str(Y2.corr(Y3)))score, pvalue, _ = coint(Y2,Y3)print ('Cointegration test p-value: ' + str(pvalue))
Correlation: -0.04130406958091662 Cointegration test p-value: 0.0
让我们来看看,Y2和Y3的相关性非常非常低,但!是! ——协整检验的p值竟然几乎为0:这说明它们一定存在一个能保持“平稳”的线性组合,我们自然会想找到这个线性组合。
聊到这儿,配对交易终于要出场了。
由于你需要在市场行情不好时避险,空头交易常常用来对冲你的多头头寸。股票跌了,空头头寸获利;股票涨了,多头头寸获利。我们可以从市场中的股票里选择一些作为多头头寸,一些作为空头头寸。这样,即使大盘遭受了股灾,我们也可以从空头头寸中弥补损失甚至反而盈利。
因为不同的股票之间有时走势方向相同,有时相反,所以我们总能找到它们的价差高点和价差低点:反映在图像中就是它们股价曲线的距离。配对交易的技巧就是由股票X和股票Y构造对冲组合。如果两只股票都保持同步涨幅或者跌幅,我们既不会赚钱也不会亏钱。如果两只股票的价差开始向历史平均值靠近,我们就能赚得收益。
具体怎么做呢?例如当股票Y的价格比股票X的价格高出许多,价差已经明显高于历史平均值时,我们买入Y的空头,X的多头。类似得,如果两只股票的价差已经远低于历史平均值,我们可以持有Y的多头,X的空头,等待价差回复到均值水平。
注意:这儿的价差并不是两只股票的价格简单相减!后面会说明应该如何计算价差。
实际上,配对交易是一种*:你在赌,价差会回复到历史均值水准。而有一个重要的点是,当我们在某件事上*时,要尽量减少这件事与其他因子的依赖性,比如市场。市场中性即意味着,市场的涨跌与你的收益无关,只要你下的注没走眼,那么就能赚钱。
配对交易,最好的方法是先给出一些你觉得可能具有协整关系的股票,然后对它们进行协整检验。特别地,如果你一开始就对所有股票进行协整检验,那你会陷入多重比较偏差的误区中!
我们用下面的函数就可以筛选出协整的股票组合。
def find_cointegrated_pairs(data):n = data.shape[1]score_matrix = np.zeros((n, n))pvalue_matrix = np.ones((n, n))keys = data.keys()pairs = []for i in range(n):for j in range(i+1, n):S1 = data[keys[i]]S2 = data[keys[j]]result = coint(S1, S2)score = result[0]pvalue = result[1]score_matrix[i, j] = scorepvalue_matrix[i, j] = pvalueif pvalue < 0.05:pairs.append((keys[i], keys[j]))return score_matrix, pvalue_matrix, pairs
我们选取了几只来自化工行业的A股股票(原文是选取了几家美股市场上的太阳能企业),来看它们之中是否具有协整关系。我们选取它们2018年的历史价格数据作为考量标准。这几只股票是:沧州大化、华鲁恒升、巨化股份、利尔化学、玲珑轮胎(与下面股票代码顺序对应)。
我们已经提出了假设,化工行业中的一些股票之间存在某种联系。接下来我们希望通过统计方法来检验它们是否具有协整关系。
注意:我们引入了沪深300指数,即把市场因素也纳入我们的数据集中。这是因为市场会使很多股票的价格漂移趋势相似,以至于你会觉得两只股票很像是协整的,但实际上只是因为它们都与市场协整。也就是说,市场因素是一个“混淆变量”:与其他变量都有关系的变量,导致我们想考察的变量间出现虚假关系。因此不管你得到什么样的关系,记得检查是不是市场因素在“作祟”。
symbol_list = ['600230.XSHG', '600426.XSHG', '600160.XSHG', '002258.XSHE', '601966.XSHG','000300.XSHG']prices_df = get_price(symbol_list, start_date='2018-01-01', end_date='2019-01-01')['close']prices_df.head()
/opt/conda/lib/python3.6/site-packages/jqresearch/api.py:86: FutureWarning: Panel is deprecated and will be removed in a future version. The recommended way to represent these types of 3-dimensional data are with a MultiIndex on a DataFrame, via the Panel.to_frame() method Alternatively, you can use the xarray package http://xarray.pydata.org/en/stable/. Pandas provides a `.to_xarray()` method to help automate this conversion. pre_factor_ref_date=_get_today())
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
600230.XSHG | 600426.XSHG | 600160.XSHG | 002258.XSHE | 601966.XSHG | 000300.XSHG | |
---|---|---|---|---|---|---|
2018-01-02 | 30.35 | 17.38 | 8.35 | 17.30 | 17.70 | 4087.40 |
2018-01-03 | 31.65 | 17.17 | 8.38 | 18.02 | 17.75 | 4111.39 |
2018-01-04 | 32.69 | 17.77 | 8.58 | 18.06 | 17.87 | 4128.81 |
2018-01-05 | 32.11 | 17.31 | 8.60 | 17.72 | 18.01 | 4138.75 |
2018-01-08 | 34.06 | 17.50 | 8.70 | 18.10 | 17.95 | 4160.16 |
prices_df['000300.XSHG'].head()
2018-01-02 4087.40 2018-01-03 4111.39 2018-01-04 4128.81 2018-01-05 4138.75 2018-01-08 4160.16 Name: 000300.XSHG, dtype: float64
scores, pvalues, pairs = find_cointegrated_pairs(prices_df)import seabornplt.figure(figsize=(16,8))seaborn.heatmap(pvalues, xticklabels=symbol_list, yticklabels=symbol_list, cmap='RdYlGn_r' , mask = (pvalues >= 0.05))print (pairs)
[('600230.XSHG', '601966.XSHG'), ('601966.XSHG', '000300.XSHG')]
由热力图我们看到有两组具有协整关系的序列对。在排除市场因素影响之后,我们认为'600230.XSHG'和'601966.XSHG',即沧州大化和玲珑轮胎是具有协整关系,适合用于配对交易的一对股票。
S1 = prices_df['600230.XSHG']S2 = prices_df['601966.XSHG']
让我们再来看一看协整检验的p值,果然与热力图对应,通过检验。因此,沧州大化和玲珑轮胎可以用作配对交易。
score, pvalue, _ = coint(S1, S2)pvalue
0.007535005037556131
现在我们来计算两个序列的价差。为了计算它们的实际价差,我们首先要用线性回归来得到两个序列的回归系数。回归方程的常数项alpha就是我们需要的价差。也就是说,一只股票的价格减去另一只股票的价格乘回归系数beta,得到的才是它们的价差。
顺便一提,线性回归也能用于估计参数并检验序列的协整性,这种方法在计量经济学中被称作Engle-Granger方法(我们一般简称E-G两步法),感兴趣的话可以参考李子奈老师的《计量经济学》,系统地学习学习~
S1 = sm.add_constant(S1)results = sm.OLS(S2, S1).fit()S1 = S1['600230.XSHG']b = results.params['600230.XSHG']
spread = S2 - b * S1plt.legend(['Spread']);spread.plot(figsize=(16,8))plt.axhline(spread.mean(), color='black')
<matplotlib.lines.Line2D at 0x7fd7638e1da0>
蓝色的折线为玲珑轮胎与沧州大化的价差,黑色的直线是它们一年内价差的平均值。用这种方法,我们能很直观地判断某时的价差是处于高位还是低位。
除了价差,我们还可以直接检测两个序列的比率。请看下面代码:
ratio = S1/S2ratio.plot(figsize=(16,8))plt.axhline(ratio.mean(), color='black')plt.legend(['Price Ratio']);
检测价格比率是配对交易中一种很传统的方法。它能作为择时信号的一部分原因是股票价格一般被认为服从对数正态分布。由这个假设我们可以推测,当我们在衡量价格比率时,实际上等价于在考察它们收益的线性组合(考虑到价格是收益的指数累积)。
价格比率的方法可能看起来比较刺激,它在实盘上往往是不可行的策略。我们还是应该选择用价差作为配对交易的选择标准。对协整的股票使用线性回归计算价差固然方便,但它在实盘上也不见得会有好的效果。其实还有很多其他计算价差的方法,这取决于你对处理协整序列的统计学方法有多了解。Qua*pian上也有专门介绍协整的教程,
回到我们的例子,直接对股票数据进行价差计算在统计学上并不是很合适的手段。我们应该先进行标准化,比如使用熟悉的z-score法。但是我们需要注意,在写实盘策略时,假设我们的数据服从正态分布并不明智。金融数据常常是尖峰厚尾的分布,这意味着它们比正态分布更可能出现极端值。
def zscore(series):return (series - series.mean()) / np.std(series)
zscore(spread).plot(figsize=(16,8))plt.axhline(zscore(spread).mean(), color='black')plt.axhline(1.0, color='red', linestyle='')plt.axhline(-1.0, color='green', linestyle='')plt.legend(['Spread z-score', 'Mean', '+1', '-1']);
蓝色折线是标准化后的序列,黑色实线是它的平均值,上下两条橙色虚线代表平均值加减一倍标准差,即“阈值”。
当z-score低于1时做多
当z-score低于1时做空
当z-score趋近0时空仓
当然,这个策略只是配对交易的冰山一角,纯粹是为了解释配对交易的流程。如果你想学会写一个更有实战意义的配对交易策略(比如多空头寸比怎么取),
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程