克隆策略

配对交易

相信很多同学都了解过 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)
[('601009.SHA', '601169.SHA', 0.02365359083397179), ('601328.SHA', '601988.SHA', 0.015969649625933734), ('601328.SHA', '601998.SHA', 0.021157033600204263), ('601988.SHA', '601998.SHA', 0.04749522836337974)]
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
1 601328.SHA 601988.SHA 0.015970
2 601328.SHA 601998.SHA 0.021157
0 601009.SHA 601169.SHA 0.023654
3 601988.SHA 601998.SHA 0.047495

可以看出,上述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:                Thu, 30 Apr 2020   Prob (F-statistic):          1.20e-155
Time:                        15:51:32   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 0x7fbe6544c128>

设中信银行的股价为 $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 [9]:
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 [11]:
instrument = {'y':'601998.SHA','x':'601328.SHA'}  # 协整股票对
start_date = '2015-01-01' # 起始日期   
end_date = '2017-07-18' # 结束日期
In [12]:
# 初始化账户和传入需要的变量
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 [13]:
# 回测启动接口
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',
)
2015-01-05 全仓买入:交通银行
2015-06-04 全仓买入:中信银行
2015-07-08 全仓买入:交通银行
2015-08-31 全仓买入:中信银行
2015-09-01 全仓买入:中信银行
2015-11-18 全仓买入:交通银行
2016-06-17 全仓买入:中信银行
2016-06-20 全仓买入:中信银行
2016-11-23 全仓买入:交通银行
2017-04-26 全仓买入:中信银行
2017-04-27 全仓买入:中信银行
  • 收益率50.71%
  • 年化收益率18.17%
  • 基准收益率3.78%
  • 阿尔法0.17
  • 贝塔0.66
  • 夏普比率0.6
  • 胜率0.71
  • 盈亏比0.65
  • 收益波动率31.21%
  • 信息比率0.04
  • 最大回撤40.63%
bigcharts-data-start/{"__type":"tabs","__id":"bigchart-b4598af2782b457d9ff3d8c5b8bbbe05"}/bigcharts-data-end
In [ ]: