V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
thinkingmind
V2EX  ›  推广

配对交易(Paper Version)

  •  
  •   thinkingmind · 2017-03-02 10:35:47 +08:00 · 2231 次点击
    这是一个创建于 2824 天前的主题,其中的信息可能已经有所发展或是发生改变。

    什么是配对交易?

    配对交易是一个有经济意义做基础的理论,因此是一个站得住脚的策略。配对策略利用一些股票对,即两只股票, 它们的价格走势倾向于一致这一性质来进行交易。当股票对之间的价格变化出现异常时,配对交易策略认为这一异常在未来会消失,回归到之前的情况。配对交易背后利用的是证券的相对价值这一概念。我们知道投资的一个原则是买入低估值的股票,卖出高估值的股票。然而股票的真实价值很难得知,从而也让我们无法知道当前股票的价值是被高估还是低估。而配对交易中的两只股票,它们的相对价值是一个平稳的时间序列,因此我们可以在其相对价值偏离均值到一定程度时做空估值高的股票,做多估值低的股票,然后在相对价值回归均值时反向平仓获利,后面我们会用价差(spread)来表示相对价值。

    什么样的股票对适合配对交易策略?

    从之前的阐述中已经可以看出,适合用于配对交易的股票对它们的相对价值一定要是一个平稳的时间序列。接下来我们就来看看为什么会存在两只股票,它们的价差会是一个平稳的时间序列。我们知道股价的对数值的时间序列是一个随机行走过程,也就是一个非平稳的时间序列。简单来说,平稳的时间序列即时间序列。然而计量经济学家 Engle 和 Granger 发现:两个非平稳的时间序列的线性组合是有可能得到一个平稳的时间序列的。

    yt−γxt 为一个平稳的时间序列 yt−γxt 为一个平稳的时间序列 其中 yt,xt 为非平稳的时间序列,γ为一个特定的常数 Engle 和 Granger 也把有这种性质的时间序列称为协整( cointegration )。接下来我们给出价差的表达式:

    spread=log(PBt)−γlog(PAt)spread=log(PtB)−γlog(PtA) PAt 和 PBt 为两只股票 A 和 B 在 t 时刻的股价 PtA 和 PtB 为两只股票 A 和 B 在 t 时刻的股价

    这样我们证明了可以用两只股票价格的对数值的时间序列这两个非平稳时间序列来构造一个平稳时间序列,从而对这一平稳时间序列来用配对交易策略进行交易。因此,具有协整性质的股票对是我们所寻找的适于交易的标的。

    怎样找到适合的配对?

    首先寻找出满足协整的必要条件的股票对。因为如果股票对具有协整的性质,那么它必然满足协整的必要条件。我们首先引入一个共有走向模型来描述时间序列。共有走向模型认为一个时间序列可以表示成一个平稳的时间序列和一个非平稳的时间序列的简单线性叠加叠加。

    yt=nyt+εytyt=nyt+εyt zt=nzt+εztzt=nzt+εzt nyt,nzt 为非平稳的时间序列,即共有走向项。εyt,εzt 为平稳的时间序列,即特有项。 nyt,nzt 为非平稳的时间序列,即共有走向项。εyt,εzt 为平稳的时间序列,即特有项。

    取它们的线性组合: yt−γzt=(nyt−γnzt)+(εyt−γεzt)yt−γzt=(nyt−γnzt)+(εyt−γεzt) 因此若这两个时间序列满足协整,那么一定有: nyt=γnztnyt=γnzt 这是满足协整的一个必要条件,即两个时间序列的共有走向项必须成正比的。 接下来我们来看下对于两只股票扁和扂来说,它在时间扩内的回报为:

    rA=log(PriceAt)−log(PriceAt−i)=nAt−nAt−i+εAt−εAt−i=rc,At+rs,At rB=log(PriceBt)−log(PriceBt−i)=nBt−nBt−i+εBt−εBt−i=rc,Bt+rs,BtrA=log(PricetA)−log(Pricet−iA)=ntA−nt−iA+εtA−εt−iA=rtc,A+rts,A rB=log(PricetB)−log(Pricet−iB)=ntB−nt−iB+εtB−εt−iB=rtc,B+rts,B rct,rst 为共有走向回报和特有回报 rtc,rts 为共有走向回报和特有回报

    从之前我们从协整推出的必要条件可以发现,如果两只股票协整且协整系数为γ,那么可以推出它们的共有走向回报必须成正比关系: rc,Bt=γrc,Atrtc,B=γrtc,A

    这个条件就是两只股票满足协整的一个必要条件,也是我们用来选择适合交易的股票对的一个依据。这个条件就是两只股票满足协整的一个必要条件,也是我们用来选择适合交易的股票对的一个依据。

    两只股票满足这一关系的时候,我们接下来就可以再检验它们的价差是不是平稳时间序列。 我们不直接检验任意两只股票之间的价差是否为平稳的原因是如果直接检验价差的平稳性的话,由于股票数量很多,需要用大量的时间,因此我们先利用协整的必要条件来缩小平稳性检验的股票对的数量。

    我们可以发现上述推出两只股票满足协整时的必要条件的推出引入了一个共有模型理论,现在的问题来了,为什么两只股票会有相似的回报?这背后的支撑即为套利定价理论。我们只简单的介绍一下套利定价理论。在套利定价理论中,如果不同的股票具有相同的风险因子,那么这些股票的共同因子回报是相同的,这里的共同因子回报即之前共有走向模型中的共有走向回报。 有了套利定价理论和共有走向模型之间的这种对应关系,也就保证了我们是可以找到两只具有相同或相似回报的股票对,这也是配对交易策略背后的经济学基础之一。

    我们现在知道了为了减少用于平稳性检验的股票对的数量,我们首先要找出具有相同或相似的回报的股票对,因为这是两只股票协整的必要条戲件。如果两只股票没有相同或相似的回报,那么这两只股票一定不是协整的,也就无法构造出一个平稳的价差时间序列来用于配对交易。我们通过计算不同股票之间的回报的相关性(correlation)来选择可能具有协整性质的股票对。计算方式如下:

    对于两只股票 A 和 B, d(A,B)=|ρ|=|Cov(rA,rB)√Var(rA)Var(rB)|d(A,B)=|ρ|=|Cov(rA,rB)Var(rA)Var(rB)|

    通过以上步骤,我们已经选出了可能具有协整性质的股票对,这就大大减少了我们的计算量。接下来的任务就是验证这些选出的股票对是否真的是具有协整性质。检验的原则为:如果两个时间序列是协整的,那么对这两个时间序列做一个简单的线性回测就可以获得一个很好的线性关系。在这一线性关系中,斜率即为我们所需的协整系数γ,残差即为我们所需的价差。总的来说分两步:

    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 年全年的日线数据。

    import operator import numpy as np import statsmodels.tsa.stattools as sts import matplotlib.pyplot as plt import tushare as ts import pandas as pd from datetime import datetime from scipy.stats.stats import pearsonr

    sector = pd.read_csv('sector.csv', index_col=0) sector_code = sector['code'][100:110] resectorcode = sector_code.reset_index(drop=True) stockPool = [] rank = {} Rank = {} for i in range(10): stockPool.append(str(resectorcode[i])) 以上为策略研究部分的第一部分代码,我们创建了一个股票池,即 stockPool 。 for i in range(10): for j in range(i+1,10): if i != j: # get the price of stock from TuShare price_of_i = ts.get_hist_data(stockPool[i], start='2012-01-01', end='2013-01-01') price_of_j = ts.get_hist_data(stockPool[j], start='2012-01-01', end='2013-01-01') # combine the close price of the two stocks and drop the NaN closePrice_of_ij = pd.concat([price_of_i['close'], price_of_j['close']], axis = 1) closePrice_of_ij = closePrice_of_ij.dropna() # change the column name in the dataFrame closePrice_of_ij.columns = ['close_i', 'close_j'] # calculate the daily return and drop the return of first day cause it is NaN. ret_of_i = ((closePrice_of_ij['close_i'] - closePrice_of_ij['close_i'].shift())/closePrice_of_ij['close_i'].shift()).dropna() ret_of_j = ((closePrice_of_ij['close_j'] - closePrice_of_ij['close_j'].shift())/closePrice_of_ij['close_j'].shift()).dropna() # calculate the correlation and store them in rank1 if len(ret_of_i) == len(ret_of_j): correlation = np.corrcoef(ret_of_i.tolist(), ret_of_j.tolist()) m = stockPool[i] + '+' + stockPool[j] rank[m] = correlation[0,1] rank1 = sorted(rank.items(), key=operator.itemgetter(1)) potentialPair = [list(map(int, item[0].split('+'))) for item in rank1] potentialPair = potentialPair[-5:]

    选出的相关系数最高的五对股票。 比如 ('600815+601177', 0.59753123459010704), 600815+601177 为两只股票的代码, 0.59753123459010704 为它们之间的相关系数。

    [('600815+601177', 0.59753123459010704), ('601100+601106', 0.60006268751560954), ('601106+601177', 0.66441434941650324), ('600815+601100', 0.6792572923561927), ('600815+601106', 0.76303679456471019)] 以上为策略研究部分的第二部分代码。我们从股票池中选取两只股票,计算它们的回报然后算出它们之间的相关系数,最后取相关系数最高的五对股票来进行下一步的协整检验。 for i in range(len(potentialPair)): m = str(potentialPair[i][0]) n = str(potentialPair[i][1]) price_of_1 = ts.get_hist_data(m, start='2012-01-01', end='2013-01-01') price_of_2 = ts.get_hist_data(n, start='2012-01-01', end='2013-01-01')

    closeprice_of_1 = price_of_1['close']
    closeprice_of_2 = price_of_2['close']
    
    if len(closeprice_of_1) != 0 and len(closeprice_of_2) != 0:
        model = pd.ols(y=closeprice_of_2, x=closeprice_of_1, intercept=True)   # perform ols on these two stocks
        spread = closeprice_of_2 - closeprice_of_1*model.beta['x']
        spread = spread.dropna()
        sta = sts.adfuller(spread, 1)
        pair = m + '+' + n
        Rank[pair] = sta[0]
        rank2 = sorted(Rank.items(), key=operator.itemgetter(1))
    

    以上为策略研究部分的第三部分代码。我们对选取出来的相关系数高的股票对进行协整检验,即检验它们的价差是否为稳定序列。 比如的对于股票对 600815 和 601002 ,我们进行 Augumented Dickey-Fuller test 得到结果如下: (-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 来表示,具体计算方式如下: zScore=spread−spreadmeanspreadvariancezScore=spread−spreadmeanspreadvariance 策略执行 通过以上策略研究部分,我们发现最适合做配对交易的股票对为厦工股份(600815), 晋亿实业(601002).接下来我们用 RiceQuant 量化交易平台来执行我们的策略,回测时间为 2013 年全年 (一定要手选然后选对),初始资金为 100000.0 。 import org.apache.commons.math3.stat.regression.SimpleRegression; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.commons.math3.analysis.function.Log;

    public class PairTrading implements IHStrategy{
        int count = 0;
        double zScore;
        double beta;
        double shareStock1;
        double shareStock2;
        double spread;
        double betShare;
        double buyShare;
        double portfolioValue;
        double dailyReturn;
        double initialCash;
        @Override
        public void init(IHInformer informer, IHInitializers initializers) {
            
    
            String stockId1 = "600815.XSHG";
            String stockId2 = "601002.XSHG";
            double closePrice[][] = new double[200][2];
            // 这些参数值是在研究部分获取的
            double beta = 0.418142479833;
            double mean=7.27385228021;
            double std  = 0.41596412236;
    
    
    
            int numRows = closePrice.length;
            int numCols = closePrice[0].length;
            int period = 199;
        
    
    
            initializers.instruments((universe) -> universe.add(stockId1, stockId2));
            initializers.shortsell().allow();
            initializers.events().statistics((stats, info, trans) -> {
                //获取两只股票的日线数据
                double[] closePxInStockId1 = stats.get(stockId1).history(period + 1, HPeriod.Day).getClosingPrice();
                double[] closePxInStockId2 = stats.get(stockId2).history(period + 1, HPeriod.Day).getClosingPrice();
                //每次对冲的多头头寸控制为当前持有现金的 0.6
                betShare = info.portfolio().getAvailableCash()*0.6/closePxInStockId2[199];   
                portfolioValue = info.portfolio().getPortfolioValue();
                dailyReturn = info.portfolio().getDailyReturn();
                initialCash = info.portfolio().getInitialCash();
                buyShare = beta*betShare;
                
                if (dailyReturn < 0){
                    count = count + 1;
                }
                
               
                if (buyShare < 100){
                    buyShare =100;
                }
                shareStock1 = info.position(stockId1).getNonClosedTradeQuantity();
                shareStock2 = info.position(stockId2).getNonClosedTradeQuantity();
                //计算两只股票之间的价差
                spread = closePxInStockId2[199] - beta*closePxInStockId1[199];
                //计算 zScore
                zScore = (spread - mean)/std;
                informer.plot("zScore", zScore);
    //当入场信号来的时候,进入市场  
                if ((zScore > 1.1  ) && (shareStock1 == 0) && (shareStock2 == 0)){               
                    trans.sell(stockId2).shares(betShare).commit();
                    trans.buy(stockId1).shares(buyShare).commit();
                }
                if ((zScore < -1.5) && (shareStock2 == 0) && (shareStock1 == 0)){ 
                    trans.sell(stockId1).shares(buyShare).commit();
                    trans.buy(stockId2).shares(betShare).commit();
                }
    //当出场信号来的时候,离开市场
                if ((zScore < 0.8) && (zScore > -1.0) && (shareStock1 != 0) && (shareStock2 != 0) ){
                    if (shareStock1 > 0){
                        trans.sell(stockId1).shares(shareStock1).commit();
                    }
                    if (shareStock1 < 0){
                        trans.buy(stockId1).shares(-shareStock1).commit();
                    }    
                    if (shareStock2 > 0){
                        trans.sell(stockId2).shares(shareStock2).commit();
                    }
                    if (shareStock2 < 0){
                        trans.buy(stockId2).shares(-shareStock2).commit();
                        
                    }
                }            
            });
        }
    }
    

    几个重要的回测结果为:夏普率 2.1236 , 最大回撤 7.450%,回测收益 36.050%,同期基准收益为-7.800%.观察交易详情可以看出交易的时间点较为平均的分散在全年各个时间段。 策略优化 我们发现策略的最大回撤为 7.450%。为了降低最大回撤,我们可以加入一个止损的方法,即经典的“ Cut the lose and let the winning run ”。思路为:如果连续亏损达到四天以上,则平仓退场。回测时间为 2013 年全年 (一定要手选然后选对),初始资金为 100000.0 import org.apache.commons.math3.stat.regression.SimpleRegression; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.commons.math3.analysis.function.Log;

    public class PairTrading implements IHStrategy{ int count = 0; double zScore; double beta; double shareStock1; double shareStock2; double spread; double betShare; double buyShare; double portfolioValue; double dailyReturn; double initialCash; @Override public void init(IHInformer informer, IHInitializers initializers) {

            String stockId1 = "600815.XSHG";
            String stockId2 = "601002.XSHG";
            double closePrice[][] = new double[200][2];
            // 这些参数值是在研究部分获取的
            double beta = 0.418142479833;
            double mean=7.27385228021;
            double std  = 0.41596412236;
    
    
    
            int numRows = closePrice.length;
            int numCols = closePrice[0].length;
            int period = 199;
        
    
    
            initializers.instruments((universe) -> universe.add(stockId1, stockId2));
            initializers.shortsell().allow();
            initializers.events().statistics((stats, info, trans) -> {
                //获取两只股票的日线数据
                double[] closePxInStockId1 = stats.get(stockId1).history(period + 1, HPeriod.Day).getClosingPrice();
                double[] closePxInStockId2 = stats.get(stockId2).history(period + 1, HPeriod.Day).getClosingPrice();
                //每次对冲的多头头寸控制为当前持有现金的 0.6
                betShare = info.portfolio().getAvailableCash()*0.6/closePxInStockId2[199];   
                portfolioValue = info.portfolio().getPortfolioValue();
                dailyReturn = info.portfolio().getDailyReturn();
                initialCash = info.portfolio().getInitialCash();
                buyShare = beta*betShare;
                //此处为引入的止损。当每天的收益连续四天以上为负的时候则止损
                if (dailyReturn < 0){
                    count = count + 1;
                }
                if (count > 4){
                   if (shareStock1 > 0){
                        trans.sell(stockId1).shares(shareStock1).commit();
                        }
                   if (shareStock1 < 0){
                       trans.buy(stockId1).shares(-shareStock1).commit();
                    }    
                    if (shareStock2 > 0){
                        trans.sell(stockId2).shares(shareStock2).commit();
                    }
                    if (shareStock2 < 0){
                        trans.buy(stockId2).shares(-shareStock2).commit();
                    }
                    count = 0;
                }
                if (buyShare < 100){
                    buyShare =100;
                }
                shareStock1 = info.position(stockId1).getNonClosedTradeQuantity();
                shareStock2 = info.position(stockId2).getNonClosedTradeQuantity();
                //计算两只股票之间的价差
                spread = closePxInStockId2[199] - beta*closePxInStockId1[199];
                //计算 zScore
                zScore = (spread - mean)/std;
                informer.plot("zScore", zScore);
    //当入场信号来的时候,进入市场  
                if ((zScore > 1.1  ) && (shareStock1 == 0) && (shareStock2 == 0)){               
                    trans.sell(stockId2).shares(betShare).commit();
                    trans.buy(stockId1).shares(buyShare).commit();
                }
                if ((zScore < -1.5) && (shareStock2 == 0) && (shareStock1 == 0)){ 
                    trans.sell(stockId1).shares(buyShare).commit();
                    trans.buy(stockId2).shares(betShare).commit();
                }
    //当出场信号来的时候,离开市场
                if ((zScore < 0.8) && (zScore > -1.0) && (shareStock1 != 0) && (shareStock2 != 0) ){
                    if (shareStock1 > 0){
                        trans.sell(stockId1).shares(shareStock1).commit();
                    }
                    if (shareStock1 < 0){
                        trans.buy(stockId1).shares(-shareStock1).commit();
                    }    
                    if (shareStock2 > 0){
                        trans.sell(stockId2).shares(shareStock2).commit();
                    }
                    if (shareStock2 < 0){
                        trans.buy(stockId2).shares(-shareStock2).commit();
                        
                    }
                }            
            });
        }
    }
    

    最大回撤降低为 6.650%。最大回测降低的并不多,但是夏普率提高到了 2.3372 ,回测收益也提高到了 41.80%。这为大家提供了一个思路,大家可以尝试不同的止损策略来看看效果如何。

    米筐量化交易平台: http://www.ricequant.com 量化炒股 QQ 群: 484490463 群内大神每日在线讲解代码,用 Python 自动赚钱!

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5487 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 01:33 · PVG 09:33 · LAX 17:33 · JFK 20:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.