利用均值回归进行价差套利

Table of Contents

什么是均值回归?

均值回归,起初是金融学的一个重要概念。均值回归是指股票价格、房产价格等社会现象、自然现象(气温、降水),无论高于或低于价值中枢(或均值)都会以很高的概率向价值中枢回归的趋势。根据这个理论,一种上涨或者下跌的趋势不管其延续的时间多长都不能永远持续下去,最终均值回归的规律一定会出现:涨得太多了,就会向平均值移动下跌;跌得太多了,就会向平均值移动上升。

均值回归是一种数学方法,通常用于投资股票使用,但它可以适用于其他进程。笼统的概念,无论是股票的高和低价格暂时的,股票的价格往往会在一段时间内的平均价格。国内的期货市场日渐活跃起来,成交量的增长以及T+0交易的优势,使得期货交易也可以应用均值回归来进行套利。

怎么判断是否均值回归?

均值回归的趋势是价格围绕着一个固定的均值以某种关系运动,因此首先我们必须要确定我们选定的合约必须有稳定的均值,且价格波动要围绕着均值。期货合约一段时间的价格可以认为是一个时间序列,那么如果能证明时间序列的平稳性,也就证明了残差的平稳。这时候我们可以认为价格时间序列是平稳的,并且会围绕残差的平均值回归。

那么为什么我们要去做均值回归的套利策略而不针对某一个合约去单独进行均值回归套利呢?原因在于单独的某个期货合约价格的时间序列大部分情况下并不平稳,但是相关性较强的两个产品价格之差比较容易呈现稳定的均值回归现象。举个例子来说,豆油和豆粕的价格本身可能并没有很强的均值回归现象,但是如果豆油和豆粕的价格之差呢?由于他们两者的相关性很强,所以很有可能呈现出较强的均值回归现象,如果他们的价差符合均值回归,那么对于两者进行跨品种套利就可行了。为了验证价差是否具有均值回归,我们需要用到ADF检验(Augmented Dickey-Fuller test),以及协整检验Cointegration Test,因此我们需要额外安装的Statesmodel和numpy。

如何在一堆期货合约中取得最佳的一组套利合约?

首先我们选取一系列的目标期货合约,为了计算方便,示例选择了同种标的物在不同时期的合约,为什么这么做呢?原因是因为同种标的物的相关系数较强,我们认为系数近似于1,不然需要进行OLS(最小二乘法)来获取两种合约的交易比列,并不是本策略展示的最简方式,也就是进行相同手数不同方向同时交易的套利方式。利用statemodel模块的功能,我们能非常简单的就对数列进行ADF检验和协整检验。

遍历函数列表,对每个期货合约时间序列进行ADF检验

这一步的目的是筛选出可以做协整测试的期货合约组合,因为协整检验的前提条件是两个时间序列必须是平稳的。由于大部分的金融资产比如期货本身的价格很难是平稳的,所以我们直接对合约价格时间序列的一阶差分做ADF检验,因为如果两个时间序列是同阶单整的,则可以进行协整检验。对于ADF检验,我们需要做以下3步:

1. 一阶差分我们需要引用numpy库中的diff函数:

np.diff(k1),k1为第一个期货合约1分钟kline最新价的序列。

2. 之后引用ADF检验函数即可做检测了:

adf1 = st.adfuller(np.diff(k1)),st.adfuller为statemodel模块里引用的功能

3. 最后判断是否通过ADF检验:

判断方法为t-value是否小于1%置信度下的t-value,如果通过检测则可以让此期货合约去做协整检验

协整检验,并且找出最佳组合:

对列表中的所有合约做完ADF一阶差分检验后,我们将他们两两组合来做协整检测(注意是组合并不是排列,不考虑排序,默认放在序列前面的合约为第一个合约)。在协整检验完成后我们再从中取得最佳的合约组合,整个过程分为以下几步:

1. 对于ADF检验的通过的期货合约组合进行协整检验:

cov = st.coint(k1, k2)

2. 验证协整检验是否通过:

判断方法为t-value是否小于1%置信度下的t-value,如果通过检测则证明两个期货合约有协整关系,即他们的残差是符合均值回归的。

3. 找出p-value最小的一组期货合约:

协整检验中,p-value越低,代表着两个时间序列的协整性越强。换句话说以p-value最低的两个期货合约作为套利组合,呈现出的结果更加符合我们的预期,也更适合我们做的简单模型,即两时间序列之间的系数为1, 不需要去额外用OLS取系数。

策略交易逻辑

做完以上检验后,我们知道两个期货合约的差值(即残差)是符合均值回归的,因此我们可以取一个残差的序列,用第一个合约1分钟Kline的最新价序列K1减去第二个合约1分钟kline的最新价,得到一个价差序列diff,那么我们可以认为两个合约的价差应该是围绕着diff序列的均值以某种程度回归的。

之后我们算取一些理论价差作为交易信号。取两个极值作为开仓判断信号,如99%和1%分为点的两个价差作为开仓判断价差,同时将最接近均值的两个值作为平仓信号,如50%和40%分位点的两个价差作为平仓判断价格,交易的操作与之前的跨期套利相似。

将实际价差和理论价差进行对比,假设有均值回归现象,那么当实际价差突破开仓信号上线或跌破开仓信号下线时,可以进行开仓操作;同理当实际价差处于平仓信号区间内时,可以进行平仓操作。

当实际价差突破开仓信号上线时,我们认为实际价差被高估,可以多头价差,实际操作为卖出第一个合约同时买入相同手数的第二个合约。

当实际价差跌破开仓信号下线时,我们认为实际价差被低估,可以空头价差,实际操作为买入第一个合约同时卖出相同手数的第二个合约。

当实际价差处于平仓信号区间内时,我们认为实际价差已经回归均值,可以平仓操作,实际操作为同时平仓两个期货合约。

由于同时进入两种合约的相反交易方向,且同种标的物的两个合约相关性较高,盈利并不会被个别合约的涨跌影响,所以风险敞口只需要看整体价差即可,敞口较低。

策略代码

#!/usr/bin/env python
#  -*- coding: utf-8 -*-
__author__ = "Lisiheng"


# 导入模块
import numpy as np
import statsmodels.tsa.stattools as st
from tqsdk import TqApi, TqAuth, TargetPosTask

api = TqApi(web_gui=True, auth=TqAuth("信易账号", "信易密码"))
labels = ["SHFE.cu2003", "SHFE.cu2004", "SHFE.cu2005", "SHFE.cu2006", "SHFE.cu2007", "SHFE.cu2008", "SHFE.cu2009",
              "SHFE.cu2010"]


# 判断两个期货合约是否有均值回归的关系
def portfolio(labels: list, api):
    # 定义变量
    mincov = 1000000
    ins_x = ""
    ins_y = ""
    klines1 = None
    klines2 = None
    # 循环下面操作找出相关性最强的组合合约
    for i, x in enumerate(labels):
        for j, y in enumerate(labels[i + 1:]):
            klines1 = api.get_kline_serial(x, 60, data_length=4000)  # 取第一个合约的1分钟K线
            klines2 = api.get_kline_serial(y, 60, data_length=4000)  # 取第二个合约的1分钟K线
            k1 = klines1['close']  # 取第一个合约K线收盘价的序列
            k2 = klines2['close']  # 取第一个合约K线收盘价的序列
            adf1 = st.adfuller(np.diff(k1))  # 对一个合约取到的最新价做一阶差分,并且做ADF在1%置信度的检测
            adf2 = st.adfuller(np.diff(k2))  # 对一个合约取到的最新价做一阶差分,并且做ADF在1%置信度的检测
            if adf1[0] < adf1[4]['1%'] and adf2[0] < adf2[4]['1%']:  # 判断两个合约的差分在1%置信度是否平稳
                cov = st.coint(k1, k2)  # 如果平稳做协整测试
                cov_t = cov[0]  # 取协整测试的t-value
                cov_p = cov[1]  # 取协整测试的p-value
                if cov_t < cov[2][0] and cov_p < mincov:  # 协整测试在1%置信度通过后并取P-value最小的一组合约
                    mincov = cov_p
                    ins_x = x
                    ins_y = y
                    return x, y, k1, k2


#  获取关于交易信号的各种参数
symbol1, symbol2, kline1_new, kline2_new = portfolio(labels, api)
diff_theory = kline1_new - kline2_new  # 通过测试后得到的新合约组kline的收盘价序列取价差序列
up_open = np.percentile(diff_theory, 99)  # 取99%分为点的价差作为上限开仓线
up_close = np.percentile(diff_theory, 50)  # 取99%分为点的价差作为上限平仓线
down_open = np.percentile(diff_theory, 1)  # 取1%分为点的价差作为下限开仓线
down_close = np.percentile(diff_theory, 40)  # 取99%分为点的价差作为下限平仓线
target1 = TargetPosTask(api, symbol1)  # 设置第一个合约的TargetPosTask
target2 = TargetPosTask(api, symbol2)  # 设置第二个合约的TargetPosTask
pos1 = api.get_position(symbol1)  # 获取第一个合约的仓位情况
pos2 = api.get_position(symbol2)  # 获取第二个合约的仓位情况
quote1 = api.get_quote(symbol1)  # 获取第一个合约的最新价格
quote2 = api.get_quote(symbol2)  # 获取第二个合约的最新价格
#  通过信号判断进行交易
while True:
    api.wait_update()
    if api.is_changing(quote1, "last_price") or api.is_changing(quote2, "last_price"):
        diff_real = kline1_new.iloc[-1] - kline2_new.iloc[-1]  # 实际的价差,当第一个和第二个合约最新价变化时,更新函数
        if (pos1.pos_long == 0 and pos1.pos_short == 0) and (pos2.pos_long == 0 and pos2.pos_short == 0) and \
                diff_real > up_open:  # 判断是否仓位为0且实际价差大于上限开仓价
            target1.set_target_volume(-20)  # 第一个合约开仓空头20手
            target2.set_target_volume(20)  # 第二个合约同时开仓多头20手
        elif (pos1.pos_long == 0 and pos1.pos_short == 0) and (pos2.pos_long == 0 and pos2.pos_short == 0) and \
                diff_real < down_open:  # 判断是否仓位为0且实际价差小于下限开仓价
            target1.set_target_volume(20)  # 第一个合约开仓多头20手
            target2.set_target_volume(-20)  # 第二个合约同时开仓空头20手
        elif pos1.pos_long != 0 or pos1.pos_short != 0 or pos2.pos_long != 0 or pos2.pos_short != 0 and \
                down_close < diff_real < up_close:  # 判断是否有持仓,且价差是否均值回归到指定值
            target1.set_target_volume(0)  # 第一个合约平仓
            target2.set_target_volume(0)  # 第二个合约同时平仓

3 回复

发表评论