美文网首页
一个运用SPY与IWM之间均值回归的日内交易策略

一个运用SPY与IWM之间均值回归的日内交易策略

作者: 发明者量化 | 来源:发表于2019-10-15 15:02 被阅读0次

    在本文中,我们将编写一个日内交易策略。它将使用经典的交易理念,即“均值回归交易对”。在这个例子中,我们将利用两只交易型开放式指数基金(ETF),SPY和IWM,它们在纽约证券交易所(NYSE)交易,并试图代表美国股市指数,分别是“S&P500和“Russell 2000"。

    该策略通过做多一种ETF和做空另一种ETF来创造一个“利差”。多空比可以用许多方式定义,例如利用统计协整时间序列的方法。在这种场景中,我们将通过滚动线性回归计算SPY和IWM之间的对冲比率。这将允许我们在SPY和IWM之间创建“利差”,其被标准化为z-score。当z-score超过某一阈值时,将产生交易信号,因为我们相信这个“利差”将恢复到均值。

    该策略的基本原理是,SPY和IWM都大致代表了同样的市场情况,即一组大型和小型美国公司的股价表现。前提是,如果接受价格的“均值回归”理论,那么它总有回归的时候,因为“事件”可能会在很短的时间内分别影响S&P500和Russell 2000,但他们之间的“利差”总会回归到正常的均值,且两者长期的价格序列总是协整(cointegrated)的。

    策略

    该策略按以下步骤执行:

    数据 - 从2007年4月到2014年2月,分别获得SPY和IWM的1分钟k线图。

    处理 - 把数据正确对齐,并且删除相互有缺少的k线。(只要有一边缺少,则两边都删除)

    差价 - 两只ETF之间的对冲比率采用滚动线性回归计算。定义为使用回溯窗口的β回归系数,该回溯窗口向前移动1根k线并重新计算回归系数。因此,对冲比例βi,bi根K线是通过计算跨越点从bi-1-k到bi-1,以用来回溯k线。

    Z-Score - 标准利差的值是以通常的方式来计算。这意味着减去利差的均值(样本)并除以利差的标准差(样本)。这样做的理由是使阈值参数更容易理解,因为Z-Score是一个无量纲的量(dimensionless quantity)。我有意在计算中引入了“前视偏差”,以显示它会有多么微妙。试试看吧!

    交易 - 当负z-score值下降到低于预定(或后优化)阈值时产生做多信号,而做空信号则与此相反。当z-score的绝对值降至额外阈值以下时,将生成平仓信号。对于这个策略,我(有点随意)选择了|z| = 2作为开仓阈值,|z| = 1作为平仓阈值。假设均值回归在利差中发挥作用,以上这些将有望捕捉到这种套利关系并提供不错的利润。

    也许深入理解策略的最佳方式是实际实现它。以下部分详细介绍了用于实现此均值回归策略的完整Python代码(单个文件)。我已经添加了详细的代码注释,以帮助你更好的理解。

    Python实现

    与所有Python/pandas教程一样,必须按照本教程中描述的Python环境来设置。设置完成后,第一项任务是导入必要的Python库。这个对于使用matplotlib和pandas是必需的。

    我使用的特定库版本如下:

    Python - 2.7.3
    NumPy - 1.8.0
    pandas - 0.12.0
    matplotlib - 1.1.0

    让我们继续并导入这些库:

    # mr_spy_iwm.py
    
    import matplotlib.pyplot as plt
    import numpy as np
    import os, os.path
    import pandas as pd
    

    以下函数create_pairs_dataframe导入两个包含两个symbol的日内k线的CSV文件。在我们的例子中,这将是SPY和IWM。然后它会创建一个单独的“数据帧对”,这个“数据帧对”将使用两个原始文件的索引。由于错过的交易和错误,它们的时间戳可能会有所不同。这是使用像pandas这样的数据分析库的主要好处之一。我们以非常有效的方式处理“样板”代码。

    # mr_spy_iwm.py
    def create_pairs_dataframe(datadir, symbols):
        """Creates a pandas DataFrame containing the closing price
        of a pair of symbols based on CSV files containing a datetime
        stamp and OHLCV data."""
     
        # Open the individual CSV files and read into pandas DataFrames
        print "Importing CSV data..."
        sym1 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[0]),
                                      header=0, index_col=0, 
                                      names=['datetime','open','high','low','close','volume','na'])
        sym2 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[1]),
                                      header=0, index_col=0, 
                                      names=['datetime','open','high','low','close','volume','na'])
     
        # Create a pandas DataFrame with the close prices of each symbol
        # correctly aligned and dropping missing entries
        print "Constructing dual matrix for %s and %s..." % symbols    
        pairs = pd.DataFrame(index=sym1.index)
        pairs['%s_close' % symbols[0].lower()] = sym1['close']
        pairs['%s_close' % symbols[1].lower()] = sym2['close']
        pairs = pairs.dropna()
        return pairs
    

    下一步是在SPY和IWM之间进行滚动线性回归。在这个场景中,IWM是预测器('x'),SPY是响应('y')。我设置了一个100根k线的默认回溯窗口。如上所述,这是策略的参数。为了使策略被认为是稳健的,我们理想地希望能看到一份返回报告在回溯期呈现出凸函数状态(或其他性能度量表现)。因此,在代码的后期阶段,我们将通过改变范围内的回顾期来进行灵敏度分析。

    在计算完SPY-IWM的线性回归模型中滚动β系数,将其添加到DataFrame对并删除空行。这就构建好了第一组K线,其等于回溯长度的修剪度量。然后,我们创建了两只ETF的利差,分别为SPY的单位和IWM的-βi的单位。显然,这不是现实的情况,因为我们正在采用少量的IWM,这在实际实施中是不可能的。

    最后,我们创建利差的z-score,通过减去利差的平均值并通过标准化利差的标准差来计算。需要注意的是这里有一个相当微妙的“前视偏差”存在。我故意将其留在代码中,因为我想强调在研究中犯这样的错误是多么容易。计算整个利差时间序列的平均值和标准差。如果这是为了反映真实的历史准确性,那么这些信息将无法获得,因为它隐含地利用了未来的信息。因此,我们应该使用滚动均值和stdev来计算z-score。

    # mr_spy_iwm.py
     
    def calculate_spread_zscore(pairs, symbols, lookback=100):
        """Creates a hedge ratio between the two symbols by calculating
        a rolling linear regression with a defined lookback period. This
        is then used to create a z-score of the 'spread' between the two
        symbols based on a linear combination of the two."""
         
        # Use the pandas Ordinary Least Squares method to fit a rolling
        # linear regression between the two closing price time series
        print "Fitting the rolling Linear Regression..."
        model = pd.ols(y=pairs['%s_close' % symbols[0].lower()], 
                       x=pairs['%s_close' % symbols[1].lower()],
                       window=lookback)
     
        # Construct the hedge ratio and eliminate the first 
        # lookback-length empty/NaN period
        pairs['hedge_ratio'] = model.beta['x']
        pairs = pairs.dropna()
     
        # Create the spread and then a z-score of the spread
        print "Creating the spread/zscore columns..."
        pairs['spread'] = pairs['spy_close'] - pairs['hedge_ratio']*pairs['iwm_close']
        pairs['zscore'] = (pairs['spread'] - np.mean(pairs['spread']))/np.std(pairs['spread'])
        return pairs
    

    在create_long_short_market_signals中,创建交易信号。这些是通过z-score的值超过阈值来计算的。当z-score的绝对值小于或等于另一个(幅度较小)阈值时,给出平仓信号。

    为了实现这种情况,有必要为每根k线确立交易策略是“开仓”还是“平仓”。 long_market和short_market是定义的两个变量,用于跟踪多头和空头头寸。不幸的是,与向量化方法相比,以迭代方式编程更加简单,因此计算起来很慢。尽管1分钟的k线图每个CSV文件需要约700,000个数据点,但在我的旧台式机上计算它仍然相对较快!

    要迭代一个pandas DataFrame(这无疑是一个不常见的操作),有必要使用iterrows方法,它提供了一个迭代的生成器:

    # mr_spy_iwm.py
     
    def create_long_short_market_signals(pairs, symbols, 
                                         z_entry_threshold=2.0, 
                                         z_exit_threshold=1.0):
        """Create the entry/exit signals based on the exceeding of 
        z_enter_threshold for entering a position and falling below
        z_exit_threshold for exiting a position."""
     
        # Calculate when to be long, short and when to exit
        pairs['longs'] = (pairs['zscore'] <= -z_entry_threshold)*1.0
        pairs['shorts'] = (pairs['zscore'] >= z_entry_threshold)*1.0
        pairs['exits'] = (np.abs(pairs['zscore']) <= z_exit_threshold)*1.0
     
        # These signals are needed because we need to propagate a
        # position forward, i.e. we need to stay long if the zscore
        # threshold is less than z_entry_threshold by still greater
        # than z_exit_threshold, and vice versa for shorts.
        pairs['long_market'] = 0.0
        pairs['short_market'] = 0.0
     
        # These variables track whether to be long or short while
        # iterating through the bars
        long_market = 0
        short_market = 0
     
        # Calculates when to actually be "in" the market, i.e. to have a
        # long or short position, as well as when not to be.
        # Since this is using iterrows to loop over a dataframe, it will
        # be significantly less efficient than a vectorised operation,
        # i.e. slow!
        print "Calculating when to be in the market (long and short)..."
        for i, b in enumerate(pairs.iterrows()):
            # Calculate longs
            if b[1]['longs'] == 1.0:
                long_market = 1            
            # Calculate shorts
            if b[1]['shorts'] == 1.0:
                short_market = 1
            # Calculate exists
            if b[1]['exits'] == 1.0:
                long_market = 0
                short_market = 0
            # This directly assigns a 1 or 0 to the long_market/short_market
            # columns, such that the strategy knows when to actually stay in!
            pairs.ix[i]['long_market'] = long_market
            pairs.ix[i]['short_market'] = short_market
        return pairs
    

    在这个阶段,我们更新了pairs以包含实际的多,空信号,这使我们能够确定我们是否需要开仓。现在我们需要创建一个投资组合来跟踪头寸的市场价值。第一项任务是创建一个结合了多头信号和空头信号的位置列。这将包含一列元素从(1,0,-1),其中1表示多头仓位,0代表无仓位(应该平仓),以及-1代表空头仓位。sym1和sym2列表示每根k线结束时SPY和IWM位置的市场价值。

    一旦创建了ETF市场价值,我们将它们相加以在每根k线结束时产出总市值。然后通过该对象的pct_change方法将其转换为返回值。后续的代码行清除了错误的条目(NaN和inf元素),最后计算了完整的权益曲线。

    # mr_spy_iwm.py
     
    def create_portfolio_returns(pairs, symbols):
        """Creates a portfolio pandas DataFrame which keeps track of
        the account equity and ultimately generates an equity curve.
        This can be used to generate drawdown and risk/reward ratios."""
         
        # Convenience variables for symbols
        sym1 = symbols[0].lower()
        sym2 = symbols[1].lower()
     
        # Construct the portfolio object with positions information
        # Note that minuses to keep track of shorts!
        print "Constructing a portfolio..."
        portfolio = pd.DataFrame(index=pairs.index)
        portfolio['positions'] = pairs['long_market'] - pairs['short_market']
        portfolio[sym1] = -1.0 * pairs['%s_close' % sym1] * portfolio['positions']
        portfolio[sym2] = pairs['%s_close' % sym2] * portfolio['positions']
        portfolio['total'] = portfolio[sym1] + portfolio[sym2]
     
        # Construct a percentage returns stream and eliminate all 
        # of the NaN and -inf/+inf cells
        print "Constructing the equity curve..."
        portfolio['returns'] = portfolio['total'].pct_change()
        portfolio['returns'].fillna(0.0, inplace=True)
        portfolio['returns'].replace([np.inf, -np.inf], 0.0, inplace=True)
        portfolio['returns'].replace(-1.0, 0.0, inplace=True)
     
        # Calculate the full equity curve
        portfolio['returns'] = (portfolio['returns'] + 1.0).cumprod()
        return portfolio
    

    主函数将它们结合在一起。日内CSV文件位于datadir路径。请务必修改以下代码以指向你的特定目录。

    为了确定策略对lookback周期的敏感程度,有必要计算一系列lookback的性能指标。我选择了投资组合的最终总回报百分比作为绩效指标和lookback范围[50,200],增量为10.你可以在下面的代码中看到,之前的函数包含在此范围内的for循环中,其他阈值保持不变。· 最后的任务是使用matplotlib创建lookbacks对比returns的折线图:

    # mr_spy_iwm.py
     
    if __name__ == "__main__":
        datadir = '/your/path/to/data/'  # Change this to reflect your data path!
        symbols = ('SPY', 'IWM')
     
        lookbacks = range(50, 210, 10)
        returns = []
     
        # Adjust lookback period from 50 to 200 in increments
        # of 10 in order to produce sensitivities
        for lb in lookbacks: 
            print "Calculating lookback=%s..." % lb
            pairs = create_pairs_dataframe(datadir, symbols)
            pairs = calculate_spread_zscore(pairs, symbols, lookback=lb)
            pairs = create_long_short_market_signals(pairs, symbols, 
                                                    z_entry_threshold=2.0, 
                                                    z_exit_threshold=1.0)
     
            portfolio = create_portfolio_returns(pairs, symbols)
            returns.append(portfolio.ix[-1]['returns'])
     
        print "Plot the lookback-performance scatterchart..."
        plt.plot(lookbacks, returns, '-o')
        plt.show()
    

    现在可以看到lookbacks与returns的图表。请注意,lookback有一个“全局”最大值,等于110根k线。如果我们看到lookbacks与returns无关的情况,这是因为:

    SPY-IWM线性回归对冲比lookback期敏感性分析

    没有向上倾斜的利润曲线,任何回测文章都是不完整的!因此,如果你希望绘制累积利润回报与时间的曲线,则可以使用以下代码。它将绘制从lookback参数研究中生成的最终投资组合。因此,有必要根据你希望可视化的图表选择lookback。该图表还绘制了同期SPY的回报以帮助比较:

    # mr_spy_iwm.py
     
        # This is still within the main function
        print "Plotting the performance charts..."
        fig = plt.figure()
        fig.patch.set_facecolor('white')
     
        ax1 = fig.add_subplot(211,  ylabel='%s growth (%%)' % symbols[0])
        (pairs['%s_close' % symbols[0].lower()].pct_change()+1.0).cumprod().plot(ax=ax1, color='r', lw=2.)
     
        ax2 = fig.add_subplot(212, ylabel='Portfolio value growth (%%)')
        portfolio['returns'].plot(ax=ax2, lw=2.)
     
        fig.show()
    

    以下权益曲线图表的lookback期为100天:

    SPY-IWM线性回归对冲比lookback期敏感性分析

    请注意,在金融危机期间,2009年SPY的缩减幅度很大。该策略在此阶段也处于动荡期。另请注意,由于SPY在此期间的强烈趋势性质反映了标准普尔500指数,去年业绩有所恶化。

    请注意,在计算z-score的利差时,我们仍然需要考虑“前视偏差”。此外,所有这些计算都是在没​​有交易成本的情况下进行的。一旦考虑到这些因素,这种策略肯定会表现得很差。手续费和滑点目前都是未确定的。此外,该策略是以ETF的小数单位进行交易,这也是非常不现实的。

    在以后的文章中,我们将创造一个更为复杂的事件驱动backtester将会把以上这些因素考虑在内,使我们在资金曲线和性能指标表现出更多的信心。

    相关文章

      网友评论

          本文标题:一个运用SPY与IWM之间均值回归的日内交易策略

          本文链接:https://www.haomeiwen.com/subject/uwdsmctx.html