配对交易策略及其在RiceQuant量化交易平台上的实现

0
回复
8417
查看
[复制链接]

8

主题

1

回帖

58

积分

注册会员

积分
58
来源: 2020-2-19 21:02:36 显示全部楼层 |阅读模式
什么是配对交易?
配对交易是一个有经济意义做基础的理论,因此是一个站得住脚的策略。配对策略利用一些股票对,即两只股票, 它们的价格走势倾向于一致这一性质来进行交易。当股票对之间的价格变化出现异常时,配对交易策略认为这一异常在未来会消失,回归到之前的情况。配对交易背后利用的是证券的相对价值这一概念。我们知道投资的一个原则是买入低估值的股票,卖出高估值的股票。然而股票的真实价值很难得知,从而也让我们无法知道当前股票的价值是被高估还是低估。而配对交易中的两只股票,它们的相对价值是一个平稳的时间序列,因此我们可以在其相对价值偏离均值到一定程度时做空估值高的股票,做多估值低的股票,然后在相对价值回归均值时反向平仓获利,后面我们会用价差(spread)来表示相对价值。
什么样的股票对适合配对交易策略?
从之前的阐述中已经可以看出,适合用于配对交易的股票对它们的相对价值一定要是一个平稳的时间序列。接下来我们就来看看为什么会存在两只股票,它们的价差会是一个平稳的时间序列。我们知道股价的对数值的时间序列是一个随机行走过程,也就是一个非平稳的时间序列。简单来说,平稳的时间序列即时间序列。然而计量经济学家Engle和Granger发现:两个非平稳的时间序列的线性组合是有可能得到一个平稳的时间序列的。
  1. $ y_t - \gamma x_t为一个平稳的时间序列 $
  2. $其中y_t,x_t为非平稳的时间序列,\gamma 为一个特定的常数$
复制代码
Engle和Granger也把有这种性质的时间序列称为协整(cointegration)。接下来我们给出价差的表达式:
  1. $spread = log(P_t^{B}) - \gamma log(P_t^{A})$
  2. $P_t^{A}和P_t^{B}为两只股票A和B在t时刻的股价$
复制代码
这样我们证明了可以用两只股票价格的对数值的时间序列这两个非平稳时间序列来构造一个平稳时间序列,从而对这一平稳时间序列来用配对交易策略进行交易。因此,具有协整性质的股票对是我们所寻找的适于交易的标的。
怎样找到适合的配对?
首先寻找出满足协整的必要条件的股票对。因为如果股票对具有协整的性质,那么它必然满足协整的必要条件。我们首先引入一个共有走向模型来描述时间序列。共有走向模型认为一个时间序列可以表示成一个平稳的时间序列和一个非平稳的时间序列的简单线性叠加叠加。
  1. $y_t = n_{y_t} + \varepsilon_{y_t}$
  2. $z_t = n_{z_t} + \varepsilon_{z_t}$
  3. $n_{y_t},n_{z_t}为非平稳的时间序列,即共有走向项。\varepsilon_{y_t},\varepsilon_{z_t}为平稳的时间序列,即特有项。$
复制代码
取它们的线性组合:
  1. $y_t - \gamma z_t = (n_{y_t} - \gamma n_{z_t}) + (\varepsilon_{y_t} - \gamma \varepsilon_{z_t})$
复制代码
因此若这两个时间序列满足协整,那么一定有:
  1. $n_{y_t} = \gamma n_{z_t}$
复制代码
这是满足协整的一个必要条件,即两个时间序列的共有走向项必须成正比的。 接下来我们来看下对于两只股票扁和扂来说,它在时间扩内的回报为:
  1. $r_A = log (Price_t^{A}) - log (Price_{t-i}^{A})=n_t^{A} - n_{t-i}^{A} + \varepsilon_t^{A} - \varepsilon_{t-i}^{A}= r_t^{c,A} + r_t^{s,A} \
  2. r_B = log (Price_t^{B}) - log (Price_{t-i}^{B})=n_t^{B} - n_{t-i}^{B} + \varepsilon_t^{B} - \varepsilon_{t-i}^{B}= r_t^{c,B} + r_t^{s,B}$
  3. $r_t^{c}, r_t^{s}为共有走向回报和特有回报$
复制代码
从之前我们从协整推出的必要条件可以发现,如果两只股票协整且协整系数为γ,那么可以推出它们的共有走向回报必须成正比关系:
  1. $r_t^{c,B} = \gamma r_t^{c,A}$
  2. $这个条件就是两只股票满足协整的一个必要条件,也是我们用来选择适合交易的股票对的一个依据。$
复制代码
两只股票满足这一关系的时候,我们接下来就可以再检验它们的价差是不是平稳时间序列。 我们不直接检验任意两只股票之间的价差是否为平稳的原因是如果直接检验价差的平稳性的话,由于股票数量很多,需要用大量的时间,因此我们先利用协整的必要条件来缩小平稳性检验的股票对的数量。
我们可以发现上述推出两只股票满足协整时的必要条件的推出引入了一个共有模型理论,现在的问题来了,为什么两只股票会有相似的回报?这背后的支撑即为套利定价理论。我们只简单的介绍一下套利定价理论。在套利定价理论中,如果不同的股票具有相同的风险因子,那么这些股票的共同因子回报是相同的,这里的共同因子回报即之前共有走向模型中的共有走向回报。 有了套利定价理论和共有走向模型之间的这种对应关系,也就保证了我们是可以找到两只具有相同或相似回报的股票对,这也是配对交易策略背后的经济学基础之一。
我们现在知道了为了减少用于平稳性检验的股票对的数量,我们首先要找出具有相同或相似的回报的股票对,因为这是两只股票协整的必要条戲件。如果两只股票没有相同或相似的回报,那么这两只股票一定不是协整的,也就无法构造出一个平稳的价差时间序列来用于配对交易。我们通过计算不同股票之间的回报的相关性(correlation)来选择可能具有协整性质的股票对。计算方式如下:
  1. 对于两只股票A和B,
  2. $d(A, B) = |\rho| = |\frac{Cov(r_A, r_B)}{\sqrt{Var(r_A)Var(r_B)}}|$
复制代码
通过以上步骤,我们已经选出了可能具有协整性质的股票对,这就大大减少了我们的计算量。接下来的任务就是验证这些选出的股票对是否真的是具有协整性质。检验的原则为:如果两个时间序列是协整的,那么对这两个时间序列做一个简单的线性回测就可以获得一个很好的线性关系。在这一线性关系中,斜率即为我们所需的协整系数γ,残差即为我们所需的价差。总的来说分两步:
1..我们对这两只股票的时间序列做线性回测。
2.我们检验价差的稳定性。
用于检验时间序列的稳定性有很多种方法 , 比如Augumented Dickey-Fuller(ADF) test, Elliott-Rothenberg-stock test, Schmidt-Phillips test等, 我们将会采用的为Augumented Dickey-Fuller test.
策略的具体实施步骤
实际中运用配对交易策略可以分为3步:
1.发现可能具有协整性质的股票对。利用的方法为计算两只股票回报的相关系数,选出相关系数高的股票对。
2.一旦确定了可能具有协整性质的股票对,我们就可以利用统计学的方法来检验这些股票对是否真的具有协整的性质。在这一过程中我们就可以确定协整系数以及价差是否具有均值回归的行为。
3.最后我们需要确定策略的一些参数,比如利用多长的历史数据来确定股票对是否具有协整性质,当价差偏离均值多远时进场或退场等。

我们把策略分为两个部分,研究部分和执行部分。研究部分包括确定交易的股票对和进出场的时间点等,执行部分即为执行交易。由于Python做策略研究的方便性,研究部分用Python执行,执行部分用RiceQuant量化交易平台来执行(RiceQuant量化交易平台即将推出Python研究平台,以后策略研究和执行可以在一个平台执行)。
策略的研究与执行策略研究
我们首先用Python来选择适合交易的股票对。 用于选取的股票池为:
600815 厦工股份 机械行业
600841 上柴股份 机械行业
600855 航天长峰 机械行业
600860 京城股份 机械行业
600984 *ST建机 机械行业
601038 一拖股份 机械行业
601002 晋亿实业 机械行业
601100 恒立油缸 机械行业
601106 中国一重 机械行业
601177 XD杭齿前 机械行业

计算所用历史数据为2012年全年的日线数据。
  1. import operator
  2. import numpy as np
  3. import statsmodels.tsa.stattools as sts
  4. import matplotlib.pyplot as plt
  5. import tushare as ts
  6. import pandas as pd
  7. from datetime import datetime
  8. from scipy.stats.stats import pearsonr

  9. > sector = pd.read_csv('sector.csv', index_col=0)
  10. sector_code = sector['code'][100:110]
  11. resectorcode = sector_code.reset_index(drop=True)
  12. stockPool = []
  13. rank = {}
  14. Rank = {}
  15. for i in range(10):
  16.     stockPool.append(str(resectorcode[i]))
复制代码
以上为策略研究部分的第一部分代码,我们创建了一个股票池,即stockPool。
  1. for i in range(10):
  2.     for j in range(i+1,10):
  3.         if i != j:
  4.                 # get the price of stock from TuShare
  5.                 price_of_i = ts.get_hist_data(stockPool[i], start='2012-01-01', end='2013-01-01')
  6.                 price_of_j = ts.get_hist_data(stockPool[j], start='2012-01-01', end='2013-01-01')
  7.                 # combine the close price of the two stocks and drop the NaN
  8.                 closePrice_of_ij = pd.concat([price_of_i['close'], price_of_j['close']], axis = 1)
  9.                 closePrice_of_ij = closePrice_of_ij.dropna()
  10.                 # change the column name in the dataFrame
  11.                 closePrice_of_ij.columns = ['close_i', 'close_j']
  12.                 # calculate the daily return and drop the return of first day cause it is NaN.
  13.                 ret_of_i = ((closePrice_of_ij['close_i'] - closePrice_of_ij['close_i'].shift())/closePrice_of_ij['close_i'].shift()).dropna()
  14.                 ret_of_j = ((closePrice_of_ij['close_j'] - closePrice_of_ij['close_j'].shift())/closePrice_of_ij['close_j'].shift()).dropna()
  15.                 # calculate the correlation and store them in rank1
  16.                 if len(ret_of_i) == len(ret_of_j):
  17.                     correlation = np.corrcoef(ret_of_i.tolist(), ret_of_j.tolist())
  18.                     m = stockPool[i] + '+' + stockPool[j]
  19.                     rank[m] = correlation[0,1]
  20.     rank1 = sorted(rank.items(), key=operator.itemgetter(1))
  21.     potentialPair = [list(map(int, item[0].split('+'))) for item in rank1]
  22.     potentialPair = potentialPair[-5:]
  23.    
  24. # 选出的相关系数最高的五对股票。 比如 ('600815+601177', 0.59753123459010704),600815+601177为两只股票的代码, 0.59753123459010704为它们之间的相关系数。
  25. [('600815+601177', 0.59753123459010704), ('601100+601106', 0.60006268751560954), ('601106+601177', 0.66441434941650324), ('600815+601100', 0.6792572923561927), ('600815+601106', 0.76303679456471019)]
复制代码
以上为策略研究部分的第二部分代码。我们从股票池中选取两只股票,计算它们的回报然后算出它们之间的相关系数,最后取相关系数最高的五对股票来进行下一步的协整检验。
  1. for i in range(len(potentialPair)):
  2.     m = str(potentialPair[i][0])
  3.     n = str(potentialPair[i][1])
  4.     price_of_1 = ts.get_hist_data(m, start='2012-01-01', end='2013-01-01')
  5.     price_of_2 = ts.get_hist_data(n, start='2012-01-01', end='2013-01-01')

  6.     closeprice_of_1 = price_of_1['close']
  7.     closeprice_of_2 = price_of_2['close']

  8.     if len(closeprice_of_1) != 0 and len(closeprice_of_2) != 0:
  9.         model = pd.ols(y=closeprice_of_2, x=closeprice_of_1, intercept=True)   # perform ols on these two stocks
  10.         spread = closeprice_of_2 - closeprice_of_1*model.beta['x']
  11.         spread = spread.dropna()
  12.         sta = sts.adfuller(spread, 1)
  13.         pair = m + '+' + n
  14.         Rank[pair] = sta[0]
  15.         rank2 = sorted(Rank.items(), key=operator.itemgetter(1))
复制代码
以上为策略研究部分的第三部分代码。我们对选取出来的相关系数高的股票对进行协整检验,即检验它们的价差是否为稳定序列。 比如的对于股票对600815和601002,我们进行Augumented Dickey-Fuller test 得到结果如下:
  1. (-3.34830942527566, 0.0128523914172048, 0, 115, {'5%': -2.8870195216569412, '1%': -3.4885349695076844, '10%': -2.5803597920604915}, -11.392077815567461)
复制代码
现在来解释一下几个比较重要的结果。第一个值-3.34830942527566为T-统计量,第二个值0.0128523914172048为p-value。字典里面包含的内容为置信度为5%,1%和10%时的T-统计量的值。比如对于我们所选择的股票对600815和601002, T-统计量为-3.34830942527566,小于5%所对应的-2.8870195216569412,那么很大可能我们发现了一个平稳的时间序列。
通过以上策略研究部分,我们发现最适合做配对交易的股票对为厦工股份(600815), 晋亿实业(601002).接下来我们用RiceQuant量化交易平台来执行我们的策略,回测时间为2014年全年,初始资金为100000.0。在计算价差时,我们对价差时间序列进行了归一化处理,处理后的价差用zScore来表示,具体计算方式如下:
  1. $zScore = \frac{spread - spread_{mean}}{spread_{variance}}$
复制代码
策略执行
通过以上策略研究部分,我们发现最适合做配对交易的股票对为厦工股份(600815), 晋亿实业(601002).接下来我们用RiceQuant量化交易平台来执行我们的策略,回测时间为2013年全年 (一定要手选然后选对),初始资金为100000.0。
  1. import org.apache.commons.math3.stat.regression.SimpleRegression;
  2. import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
  3. import org.apache.commons.math3.analysis.function.Log;
  4.    
  5.     public class PairTrading implements IHStrategy{
  6.         int count = 0;
  7.         double zScore;
  8.         double beta;
  9.         double shareStock1;
  10.         double shareStock2;
  11.         double spread;
  12.         double betShare;
  13.         double buyShare;
  14.         double portfolioValue;
  15.         double dailyReturn;
  16.         double initialCash;
  17.         @Override
  18.         public void init(IHInformer informer, IHInitializers initializers) {
  19.             
  20.    
  21.             String stockId1 = "600815.XSHG";
  22.             String stockId2 = "601002.XSHG";
  23.             double closePrice[][] = new double[200][2];
  24.             // 这些参数值是在研究部分获取的
  25.             double beta = 0.418142479833;
  26.             double mean=7.27385228021;
  27.             double std  = 0.41596412236;
  28.    
  29.    
  30.    
  31.             int numRows = closePrice.length;
  32.             int numCols = closePrice[0].length;
  33.             int period = 199;
  34.         
  35.    
  36.    
  37.             initializers.instruments((universe) -> universe.add(stockId1, stockId2));
  38.             initializers.shortsell().allow();
  39.             initializers.events().statistics((stats, info, trans) -> {
  40.                 //获取两只股票的日线数据
  41.                 double[] closePxInStockId1 = stats.get(stockId1).history(period + 1, HPeriod.Day).getClosingPrice();
  42.                 double[] closePxInStockId2 = stats.get(stockId2).history(period + 1, HPeriod.Day).getClosingPrice();
  43.                 //每次对冲的多头头寸控制为当前持有现金的0.6
  44.                 betShare = info.portfolio().getAvailableCash()*0.6/closePxInStockId2[199];   
  45.                 portfolioValue = info.portfolio().getPortfolioValue();
  46.                 dailyReturn = info.portfolio().getDailyReturn();
  47.                 initialCash = info.portfolio().getInitialCash();
  48.                 buyShare = beta*betShare;
  49.                
  50.                 if (dailyReturn < 0){
  51.                     count = count + 1;
  52.                 }
  53.                
  54.                
  55.                 if (buyShare < 100){
  56.                     buyShare =100;
  57.                 }
  58.                 shareStock1 = info.position(stockId1).getNonClosedTradeQuantity();
  59.                 shareStock2 = info.position(stockId2).getNonClosedTradeQuantity();
  60.                 //计算两只股票之间的价差
  61.                 spread = closePxInStockId2[199] - beta*closePxInStockId1[199];
  62.                 //计算zScore
  63.                 zScore = (spread - mean)/std;
  64.                 informer.plot("zScore", zScore);
  65.     //当入场信号来的时候,进入市场  
  66.                 if ((zScore > 1.1  ) && (shareStock1 == 0) && (shareStock2 == 0)){               
  67.                     trans.sell(stockId2).shares(betShare).commit();
  68.                     trans.buy(stockId1).shares(buyShare).commit();
  69.                 }
  70.                 if ((zScore < -1.5) && (shareStock2 == 0) && (shareStock1 == 0)){
  71.                     trans.sell(stockId1).shares(buyShare).commit();
  72.                     trans.buy(stockId2).shares(betShare).commit();
  73.                 }
  74.     //当出场信号来的时候,离开市场
  75.                 if ((zScore < 0.8) && (zScore > -1.0) && (shareStock1 != 0) && (shareStock2 != 0) ){
  76.                     if (shareStock1 > 0){
  77.                         trans.sell(stockId1).shares(shareStock1).commit();
  78.                     }
  79.                     if (shareStock1 < 0){
  80.                         trans.buy(stockId1).shares(-shareStock1).commit();
  81.                     }   
  82.                     if (shareStock2 > 0){
  83.                         trans.sell(stockId2).shares(shareStock2).commit();
  84.                     }
  85.                     if (shareStock2 < 0){
  86.                         trans.buy(stockId2).shares(-shareStock2).commit();
  87.                         
  88.                     }
  89.                 }            
  90.             });
  91.         }
  92.     }
复制代码
几个重要的回测结果为:夏普率2.1236, 最大回撤7.450%,回测收益36.050%,同期基准收益为-7.800%.观察交易详情可以看出交易的时间点较为平均的分散在全年各个时间段。
策略优化
我们发现策略的最大回撤为7.450%。为了降低最大回撤,我们可以加入一个止损的方法,即经典的“Cut the lose and let the winning run”。思路为:如果连续亏损达到四天以上,则平仓退场。回测时间为2013年全年 (一定要手选然后选对),初始资金为100000.0
  1. import org.apache.commons.math3.stat.regression.SimpleRegression;
  2. import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
  3. import org.apache.commons.math3.analysis.function.Log;
  4.    
  5. public class PairTrading implements IHStrategy{
  6.         int count = 0;
  7.         double zScore;
  8.         double beta;
  9.         double shareStock1;
  10.         double shareStock2;
  11.         double spread;
  12.         double betShare;
  13.         double buyShare;
  14.         double portfolioValue;
  15.         double dailyReturn;
  16.         double initialCash;
  17.         @Override
  18.         public void init(IHInformer informer, IHInitializers initializers) {
  19.             
  20.    
  21.             String stockId1 = "600815.XSHG";
  22.             String stockId2 = "601002.XSHG";
  23.             double closePrice[][] = new double[200][2];
  24.             // 这些参数值是在研究部分获取的
  25.             double beta = 0.418142479833;
  26.             double mean=7.27385228021;
  27.             double std  = 0.41596412236;
  28.    
  29.    
  30.    
  31.             int numRows = closePrice.length;
  32.             int numCols = closePrice[0].length;
  33.             int period = 199;
  34.         
  35.    
  36.    
  37.             initializers.instruments((universe) -> universe.add(stockId1, stockId2));
  38.             initializers.shortsell().allow();
  39.             initializers.events().statistics((stats, info, trans) -> {
  40.                 //获取两只股票的日线数据
  41.                 double[] closePxInStockId1 = stats.get(stockId1).history(period + 1, HPeriod.Day).getClosingPrice();
  42.                 double[] closePxInStockId2 = stats.get(stockId2).history(period + 1, HPeriod.Day).getClosingPrice();
  43.                 //每次对冲的多头头寸控制为当前持有现金的0.6
  44.                 betShare = info.portfolio().getAvailableCash()*0.6/closePxInStockId2[199];   
  45.                 portfolioValue = info.portfolio().getPortfolioValue();
  46.                 dailyReturn = info.portfolio().getDailyReturn();
  47.                 initialCash = info.portfolio().getInitialCash();
  48.                 buyShare = beta*betShare;
  49.                 //此处为引入的止损。当每天的收益连续四天以上为负的时候则止损
  50.                 if (dailyReturn < 0){
  51.                     count = count + 1;
  52.                 }
  53.                 if (count > 4){
  54.                    if (shareStock1 > 0){
  55.                         trans.sell(stockId1).shares(shareStock1).commit();
  56.                         }
  57.                    if (shareStock1 < 0){
  58.                        trans.buy(stockId1).shares(-shareStock1).commit();
  59.                     }   
  60.                     if (shareStock2 > 0){
  61.                         trans.sell(stockId2).shares(shareStock2).commit();
  62.                     }
  63.                     if (shareStock2 < 0){
  64.                         trans.buy(stockId2).shares(-shareStock2).commit();
  65.                     }
  66.                     count = 0;
  67.                 }
  68.                 if (buyShare < 100){
  69.                     buyShare =100;
  70.                 }
  71.                 shareStock1 = info.position(stockId1).getNonClosedTradeQuantity();
  72.                 shareStock2 = info.position(stockId2).getNonClosedTradeQuantity();
  73.                 //计算两只股票之间的价差
  74.                 spread = closePxInStockId2[199] - beta*closePxInStockId1[199];
  75.                 //计算zScore
  76.                 zScore = (spread - mean)/std;
  77.                 informer.plot("zScore", zScore);
  78.     //当入场信号来的时候,进入市场  
  79.                 if ((zScore > 1.1  ) && (shareStock1 == 0) && (shareStock2 == 0)){               
  80.                     trans.sell(stockId2).shares(betShare).commit();
  81.                     trans.buy(stockId1).shares(buyShare).commit();
  82.                 }
  83.                 if ((zScore < -1.5) && (shareStock2 == 0) && (shareStock1 == 0)){
  84.                     trans.sell(stockId1).shares(buyShare).commit();
  85.                     trans.buy(stockId2).shares(betShare).commit();
  86.                 }
  87.     //当出场信号来的时候,离开市场
  88.                 if ((zScore < 0.8) && (zScore > -1.0) && (shareStock1 != 0) && (shareStock2 != 0) ){
  89.                     if (shareStock1 > 0){
  90.                         trans.sell(stockId1).shares(shareStock1).commit();
  91.                     }
  92.                     if (shareStock1 < 0){
  93.                         trans.buy(stockId1).shares(-shareStock1).commit();
  94.                     }   
  95.                     if (shareStock2 > 0){
  96.                         trans.sell(stockId2).shares(shareStock2).commit();
  97.                     }
  98.                     if (shareStock2 < 0){
  99.                         trans.buy(stockId2).shares(-shareStock2).commit();
  100.                         
  101.                     }
  102.                 }            
  103.             });
  104.         }
  105.     }
复制代码
最大回撤降低为6.650%。最大回测降低的并不多,但是夏普率提高到了2.3372,回测收益也提高到了41.80%。这为大家提供了一个思路,大家可以尝试不同的止损策略来看看效果如何。

本文来自:ricequant

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 免费注册
关注微信