相信很多同学都了解过 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 时,协整关系便非常强。
import numpy as np
import pandas as pd
import statsmodels.api as sm
import seaborn as sns
# 输入是一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$ 值越低。
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)
df = pd.DataFrame(pairs, index=range(0,len(pairs)), columns=list(['Name1','Name2','pvalue']))
#pvalue越小表示相关性越大,按pvalue升序排名就是获取相关性从大到小的股票对
df.sort_values(by='pvalue')
可以看出,上述10只股票中有3对具有较为显著的协整性关系的股票对(红色表示协整关系显著)。我们选择使用其中 $p$ 值最低(0.004)的交通银行(601328.SHA)和中信银行(601998.SHA)这一对股票来进行研究。首先调取交通银行和中信银行的历史股价,画出两只股票的价格走势。
T.plot(prices_df[['601328.SHA','601998.SHA']], chart_type='line', title='Price')
接下来,我们用这两支股票的价格来进行一次OLS线性回归,以此算出它们是以什么线性组合的系数构成平稳序列的。
# 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())
系数是 0.8602,画出数据和拟合线。
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')
设中信银行的股价为 $Y$,交通银行为 $X$,回归拟合的结果是 $$Y=0.3818 +0.8602⋅X$$ 也就是说 $Y−0.8602⋅X$ 是平稳序列。
依照这个比例,我们画出它们价差的平稳序列。可以看出,虽然价差上下波动,但都会回归中间的均值。
# 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$ 的值,减去序列的均值,再除以序列的标准差后得到的值。
def zscore(series):
return (series - series.mean()) / np.std(series)
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.资金管理:暂时没有择时,任何时间保持满仓
instrument = {'y':'601998.SHA','x':'601328.SHA'} # 协整股票对
start_date = '2015-01-01' # 起始日期
end_date = '2017-07-18' # 结束日期
# 初始化账户和传入需要的变量
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)
# 回测启动接口
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',
)
[2020-04-30 15:52:15.778295] INFO: moduleinvoker: backtest.v7 开始运行..
[2020-04-30 15:52:15.787217] INFO: backtest: biglearning backtest:V7.2.0
[2020-04-30 15:52:15.866100] INFO: moduleinvoker: cached.v2 开始运行..
[2020-04-30 15:52:16.104086] INFO: moduleinvoker: cached.v2 运行完成[0.237963s].
[2020-04-30 15:52:16.113377] INFO: algo: TradingAlgorithm V1.6.7
[2020-04-30 15:52:16.293405] INFO: algo: trading transform...
[2020-04-30 15:52:17.550459] INFO: Performance: Simulated 619 trading days out of 619.
[2020-04-30 15:52:17.551886] INFO: Performance: first open: 2015-01-05 09:30:00+00:00
[2020-04-30 15:52:17.552817] INFO: Performance: last close: 2017-07-18 15:00:00+00:00
[2020-04-30 15:52:18.850597] INFO: moduleinvoker: backtest.v7 运行完成[3.072303s].
[2020-04-30 15:52:18.852144] INFO: moduleinvoker: trade.v2 运行完成[3.103096s].