[量化学堂-策略开发]基于协整的配对交易

配对交易
统计套利
zscore
协整
标签: #<Tag:0x00007f5bffc59bf0> #<Tag:0x00007f5bffc599e8> #<Tag:0x00007f5bffc59880> #<Tag:0x00007f5bffc596c8>

(iQuant) #1
克隆策略

配对交易

相信很多同学都了解过 Pairs Trading,即配对交易策略。其基本原理就是找出两只走势相关的股票。这两只股票的价格差距从长期来看在一个固定的水平内波动,如果价差暂时性的超过或低于这个水平,就买多价格偏低的股票,卖空价格偏高的股票。等到价差恢复正常水平时,进行平仓操作,赚取这一过程中价差变化所产生的利润。

使用这个策略的关键就是“必须找到一对价格走势高度相关的股票”,而高度相关在这里意味着在长期来看有一个稳定的价差,这就要用到协整关系的检验。

在量化课堂介绍协整关系的文章里,我们知道如果用 $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$ 上涨。所以,要使用配对交易,必须找到一对协整相关的股票。

协整关系的检验

我们想使用协整的特性进行配对交易,那么要怎么样发现协整关系呢?

在 Python 的 Statsmodels 包中,有直接用于协整关系检验的函数 coint,该函数包含于 statsmodels.tsa.stattools 中。 首先,我们构造一个读取股票价格,判断协整关系的函数。该函数返回的两个值分别为协整性检验的 p 值矩阵以及所有传入的参数中协整性较强的股票对。我们不需要在意 $p$ 值具体是什么,可以这么理解它: $p$ 值越低,协整关系就越强;$p$ 值低于 0.05 时,协整关系便非常强。

In [1]:
import numpy as np
import pandas as pd
import statsmodels.api as sm
import seaborn as sns
In [2]:
# 输入是一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$ 值,较为直观地看出他们之间的关系。

我们的测试区间为2015年1月1日至2017年7月18日。热力图画出的是 1 减去 $p$ 值,因此颜色越红的地方表示 $p$ 值越低。

In [3]:
instruments = ["002142.SZA", "600000.SHA", "600015.SHA", "600016.SHA", "600036.SHA", "601009.SHA",
              "601166.SHA", "601169.SHA", "601328.SHA", "601398.SHA", "601988.SHA", "601998.SHA"]

# 确定起始时间
start_date = '2015-01-01' 
# 确定结束时间
end_date = '2017-07-18' 
# 获取股票总市值数据,返回DataFrame数据格式
prices_temp = D.history_data(instruments,start_date,end_date,
              fields=['close'] )
prices_df=pd.pivot_table(prices_temp, values='close', index=['date'], columns=['instrument'])
pvalues, pairs = find_cointegrated_pairs(prices_df)
#画协整检验热度图,输出pvalue < 0.05的股票对
sns.heatmap(1-pvalues, xticklabels=instruments, yticklabels=instruments, cmap='RdYlGn_r', mask = (pvalues == 1))
print(pairs)
[('601328.SHA', '601988.SHA', 0.0050265192277696939), ('601328.SHA', '601998.SHA', 0.0069352163995946518)]
In [4]:
df = pd.DataFrame(pairs, index=range(0,len(pairs)), columns=list(['Name1','Name2','pvalue']))
#pvalue越小表示相关性越大,按pvalue升序排名就是获取相关性从大到小的股票对
df.sort_values(by='pvalue')
Out[4]:
Name1 Name2 pvalue
0 601328.SHA 601988.SHA 0.005027
1 601328.SHA 601998.SHA 0.006935

可以看出,上述10只股票中有3对具有较为显著的协整性关系的股票对(红色表示协整关系显著)。我们选择使用其中 $p$ 值最低(0.004)的交通银行(601328.SHA)和中信银行(601998.SHA)这一对股票来进行研究。首先调取交通银行和中信银行的历史股价,画出两只股票的价格走势。

In [5]:
T.plot(prices_df[['601328.SHA','601998.SHA']], chart_type='line', title='Price')

接下来,我们用这两支股票的价格来进行一次OLS线性回归,以此算出它们是以什么线性组合的系数构成平稳序列的。

In [6]:
# ols
x = prices_df['601328.SHA']
y = prices_df['601998.SHA']
X = sm.add_constant(x)
result = (sm.OLS(y,X)).fit()
print(result.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:             601998.SHA   R-squared:                       0.682
Model:                            OLS   Adj. R-squared:                  0.682
Method:                 Least Squares   F-statistic:                     1323.
Date:                Mon, 23 Apr 2018   Prob (F-statistic):          1.20e-155
Time:                        16:36:53   Log-Likelihood:                -566.43
No. Observations:                 619   AIC:                             1137.
Df Residuals:                     617   BIC:                             1146.
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const          0.3818      0.226      1.687      0.092      -0.063       0.826
601328.SHA     0.8602      0.024     36.378      0.000       0.814       0.907
==============================================================================
Omnibus:                        0.497   Durbin-Watson:                   0.070
Prob(Omnibus):                  0.780   Jarque-Bera (JB):                0.340
Skew:                           0.003   Prob(JB):                        0.844
Kurtosis:                       3.115   Cond. No.                         90.0
==============================================================================

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

系数是 0.8602,画出数据和拟合线。

In [7]:
import matplotlib.pyplot as plt
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')
Out[7]:
<matplotlib.legend.Legend at 0x7fd38dff0da0>

设中信银行的股价为 $Y$,交通银行为 $X$,回归拟合的结果是 $$Y=0.3818 +0.8602⋅X$$ 也就是说 $Y−0.8602⋅X$ 是平稳序列。

依照这个比例,我们画出它们价差的平稳序列。可以看出,虽然价差上下波动,但都会回归中间的均值。

In [8]:
# T.plot(pd.DataFrame({'Stationary Series':0.8602*x-y, 'Mean':[np.mean(0.8602*x-y)]}), chart_type='line')
df = pd.DataFrame({'Stationary Series':y-0.8602*x, 'Mean':np.mean(y-0.8602*x)})
T.plot(df, chart_type='line', title='Stationary Series')

买卖时机的判断

这里,我们先介绍一下, $z-score$ 是对时间序列偏离其均值程度的衡量,表示时间序列偏离了其均值多少倍的标准差。首先,我们定义一个函数来计算 $z-score$:

一个序列在时间 $t$ 的 $z-score$,是它在时间 $t$ 的值,减去序列的均值,再除以序列的标准差后得到的值。

In [11]:
def zscore(series):
    return (series - series.mean()) / np.std(series)
In [10]:
zscore_calcu = zscore(y-0.8602*x)
T.plot(pd.DataFrame({'zscore':zscore_calcu, 'Mean':np.mean(y-0.8602*x), 'upper':1, 'lower':-1}) ,chart_type='line', title='zscore')

策略完整交易系统设计

1.交易标的:中信银行(601998.SHA)和交通银行(601328.SHA)

2.交易信号: 当zscore大于1时,全仓买入交通银行,全仓卖出中信银行→做空价差 当zscore小于-1时,全仓卖出中信银行,全仓买入交通银行→做多价差

3.风险控制:暂时没有风险控制

4.资金管理:暂时没有择时,任何时间保持满仓

策略回测部分

In [12]:
instrument = {'y':'601998.SHA','x':'601328.SHA'}  # 协整股票对
start_date = '2015-01-01' # 起始日期   
end_date = '2017-07-18' # 结束日期
In [13]:
# 初始化账户和传入需要的变量
def initialize(context):
    context.set_commission(PerDollar(0.0015)) # 手续费设置
    context.zscore = zscore_calcu # 交易信号需要根据zscore_calcu的具体数值给出
    context.ins  = instrument # 传入协整股票对
    
# 策略主题函数   
def handle_data(context, data):
    
    date = data.current_dt.strftime('%Y-%m-%d') # 运行到当根k线的日期
    zscore = context.zscore.ix[date]  # 当日的zscore
    stock_1 = context.ins['y'] # 股票y
    stock_2 = context.ins['x'] # 股票x
    
    symbol_1 = context.symbol(stock_1) # 转换成回测引擎所需要的symbol格式
    symbol_2 = context.symbol(stock_2)
    
    # 持仓
    cur_position_1 = context.portfolio.positions[symbol_1].amount
    cur_position_2 = context.portfolio.positions[symbol_2].amount
   
    
    # 交易逻辑
    # 如果zesore大于上轨(>1),则价差会向下回归均值,因此需要买入股票x,卖出股票y
    if zscore > 1 and cur_position_2 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2):  
        context.order_target_percent(symbol_1, 0)
        context.order_target_percent(symbol_2, 1)
        print(date, '全仓买入:交通银行')
        
    # 如果zesore小于下轨(<-1),则价差会向上回归均值,因此需要买入股票y,卖出股票x
    elif zscore < -1 and cur_position_1 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2):  
        context.order_target_percent(symbol_1, 1)
        print(date, '全仓买入:中信银行')
        context.order_target_percent(symbol_2, 0)     
In [14]:
# 回测启动接口
m=M.trade.v2( 
    instruments=list(instrument.values()),# 保证instrument是有字符串的股票代码组合成的列表(list)
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='open',
    order_price_field_sell='open',
    capital_base=10000,
    benchmark='000300.INDX',
)
[2018-04-23 16:39:29.831635] INFO: bigquant: backtest.v7 开始运行..
[2018-04-23 16:39:29.970275] INFO: algo: set price type:backward_adjusted
2015-01-05 全仓买入:交通银行
2015-06-04 全仓买入:中信银行
2015-07-08 全仓买入:交通银行
2015-08-31 全仓买入:中信银行
2015-11-18 全仓买入:交通银行
2016-06-17 全仓买入:中信银行
2016-11-23 全仓买入:交通银行
2017-04-26 全仓买入:中信银行
[2018-04-23 16:39:37.793525] INFO: Performance: Simulated 619 trading days out of 619.
[2018-04-23 16:39:37.795151] INFO: Performance: first open: 2015-01-05 01:30:00+00:00
[2018-04-23 16:39:37.796960] INFO: Performance: last close: 2017-07-18 07:00:00+00:00
  • 收益率70.99%
  • 年化收益率24.41%
  • 基准收益率3.78%
  • 阿尔法0.22
  • 贝塔0.76
  • 夏普比率0.57
  • 胜率0.714
  • 盈亏比0.744
  • 收益波动率35.14%
  • 信息比率0.81
  • 最大回撤40.65%
[2018-04-23 16:39:40.560766] INFO: bigquant: backtest.v7 运行完成[10.729137s].

参考资料


社区干货与精选整理(持续更新中...)
(GQUANT) #3

const 0.3818 0.226 1.687 0.092 -0.063 0.826
601328.SHA 0.8602 0.024 36.378 0.000 0.814 0.907

这个个0.8753的系数是怎么算出来的,应该是0.8602才对啊~~ 求解?!


(youke) #4

我也觉得应该是0.8602,按OLS线性回归结果,Y=0.3818+0.8602*X


(GQUANT) #5

对。跟你想法一样。Y=0.3818+0.8602*X
就是不知道为什么bigquant 的文档要这么写,实在觉得这写文档的到底懂不懂这个了。。


(youke) #6

这个策略,回归分析那些还是用策略里的时间段,
但是,回测时间段我想变成2016-01-01~2018-03-15,最后的代码改成这样,但是出错

回测启动接口

m=M.trade.v3(
instruments=list(instrument.values()),# 保证instrument是有字符串的股票代码组合成的列表(list)
#start_date=start_date,
#end_date=end_date,
start_date=‘2016-01-01’,
end_date=‘2018-03-15’,
initialize=initialize,
handle_data=handle_data,
order_price_field_buy=‘open’,
order_price_field_sell=‘open’,
capital_base=10000,
benchmark=‘000300.INDX’,
)

却提示这样错误
[2018-03-15 21:39:48.226326] INFO: bigquant: backtest.v7 开始运行…
[2018-03-15 21:39:48.566720] INFO: algo: set price type:backward_adjusted

TypeError Traceback (most recent call last)
TypeError: ‘str’ object cannot be interpreted as an integer

During handling of the above exception, another exception occurred:

KeyError Traceback (most recent call last)
KeyError: ‘2017-07-19’

During handling of the above exception, another exception occurred:

KeyError Traceback (most recent call last)
KeyError: 1500422400000000000

During handling of the above exception, another exception occurred:

KeyError Traceback (most recent call last)
KeyError: Timestamp(‘2017-07-19 00:00:00’)

During handling of the above exception, another exception occurred:

KeyError Traceback (most recent call last)
in ()
14 order_price_field_sell=‘open’,
15 capital_base=10000,
—> 16 benchmark=‘000300.INDX’,
17 )

in handle_data(context, data)
9
10 date = data.current_dt.strftime(’%Y-%m-%d’) # 运行到当根k线的日期
—> 11 zscore = context.zscore.ix[date] # 当日的zscore
12 stock_1 = context.ins[‘y’] # 股票y
13 stock_2 = context.ins[‘x’] # 股票x

KeyError: ‘2017-07-19’

想做成像其他AI策略那样,训练一个时间段,回测用另一个时间段,如何修改?