SPY와 IWM 사이의 Intraday Mean Reversion Pairs 전략을 백테스팅

저자:선함, 2019-03-28 10:51:06, 업데이트:

이 기사에서는 우리의 첫 번째 내일 거래 전략을 고려할 것입니다. 그것은 고전적인 거래 아이디어를 사용하여, 트레이딩 페어. 이 경우 우리는 뉴욕 증권 거래소 (NYSE) 에서 거래되고 미국 증권 시장 지표, S&P500 및 Russell 2000를 대표하려고하는 두 개의 거래 상장 펀드 (ETF) SPY 및 IWM을 사용할 것입니다.

이 전략은 크게 ETF 쌍 사이에 스프레드를 생성하여 하나를 길게하고 다른 금액을 단축합니다. 긴 것과 짧은 비율은 통계적 코인그리테이션 시간 시리즈 기술을 사용하는 것과 같은 여러 가지 방법으로 정의 될 수 있습니다. 이 시나리오에서 우리는 롤링 선형 회귀를 통해 SPY와 IWM 사이의 헤지 비율을 계산할 것입니다. 이것은 SPY와 IWM 사이에 z 스코어로 정상화 된 스프레드를 만들 수 있습니다. z 스코어가 특정 임계치를 초과 할 때 거래 신호가 생성됩니다. 스프레드가 평균으로 돌아갈 것이라는 믿음 아래.

이 전략의 논리는 SPY와 IWM가 대략 같은 상황을 특징으로 하고 있다는 것입니다. 대기업과 소액 주식 미국 기업들의 경제입니다. 가정은 가격의 스프레드를 취하면 평균을 역전해야한다는 것입니다. S&P500 또는 러셀 2000 지수 (small-cap/large-cap 차이, 재균형 날짜 또는 블록 거래와 같은) 에 개별적으로 영향을 줄 수 있는 지역적 사건들 (시간) 이 있지만, 두 가지의 장기 가격 시리즈는 아마도 통합 될 것입니다.

전략

이 전략은 다음 단계로 이루어집니다.

  1. 데이터 - SPY와 IWM의 1분 바는 2007년 4월부터 2014년 2월까지 얻습니다.
  2. 처리 - 데이터가 올바르게 정렬되고 부족한 바가 서로 폐기됩니다.
  3. 스프레드 - 두 ETF 사이의 헤지 비율은 롤링 선형 회귀를 통해 계산됩니다. 이것은 1 바로 앞으로 이동하고 회귀 계수를 재 계산하는 룩백 윈도우를 사용하여 β 회귀 계수로 정의됩니다. 따라서 바 bi의 헤지 비율 βi는 k 바의 룩백을 위해 bi−1−k에서 bi−1까지의 점들을 통해 계산됩니다.
  4. Z 점수 - 스프레드의 표준 점수는 일반적인 방식으로 계산됩니다. 이것은 스프레드의 ( 샘플) 평균을 빼고 스프레드의 ( 샘플) 표준 편차로 나누는 것을 의미합니다. z 점수가 비차원적 인 양이기 때문에 문턱 매개 변수를 해석하는 것이 더 간단하기 때문입니다. 나는 얼마나 미묘 할 수 있는지 보여주기 위해 계산에 의도적으로 룩헤드 편향을 도입했습니다. 시도하고 조심하십시오!
  5. 트레이드 - 마이너스 z 스코어가 미리 결정된 (또는 후 최적화 된) 임계치 이하로 떨어지면 긴 신호가 생성되며, 짧은 신호는 그 반대입니다. 절대 z 스코어가 추가 임계치 이하로 떨어지면 출구 신호가 생성됩니다. 이 전략을 위해 (미미하게 임의로) 나는 절대 입점 임계치 인 쪼즈즈즈즈=2와 출구 임계치 인 쪼즈즈즈즈즈=1을 선택했습니다. 스프레드의 평균 역행 행동을 가정하면, 이것은 그 관계를 포착하고 긍정적 인 성능을 제공할 것으로 기대됩니다.

아마도 전략을 깊이 이해하는 가장 좋은 방법은 실제로 구현하는 것입니다. 다음 섹션에서는이 평균 역전 전략을 구현하기위한 완전한 파이썬 코드 (단 하나의 파일) 를 설명합니다. 이해를 돕기 위해 코드를 자유자재로 언급했습니다.

파이썬 구현

모든 파이썬/판다스 튜토리얼과 마찬가지로 이 튜토리얼에서 설명한 바와 같이 파이썬 연구 환경을 설정하는 것이 필요합니다. 설치가 완료되면 첫 번째 작업은 필요한 파이썬 라이브러리를 수입하는 것입니다. 이 백테스트를 위해 matplotlib과 판다스가 필요합니다.

제가 사용하는 특정 라이브러리 버전은 다음과 같습니다.

  • 파이썬 - 2.7.3
  • 수 - 1.8.0
  • 판다 - 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는 두 개의 기호의 내일 바를 포함하는 두 개의 CSV 파일을 가져옵니다. 우리의 경우 이것은 SPY와 IWM입니다. 그 다음 두 가지 원본 파일의 인덱스를 사용하는 별도의 데이터 프레임 쌍을 만듭니다. 놓친 트레이드 및 오류로 인해 시간표가 다를 가능성이 있기 때문에 이것은 일치하는 데이터를 가질 수 있음을 보장합니다. 이것은 판다와 같은 데이터 분석 라이브러리를 사용하는 주요 이점 중 하나입니다. boilerplate 코드는 우리에게 매우 효율적으로 처리됩니다.

# 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 바의 기본 룩백 창을 설정했습니다. 위에서 논의했듯이 이것은 전략의 매개 변수입니다. 전략이 견고하다고 간주되기 위해서는 이상적으로 룩백 기간의 마주나고 있는 함수로서 반환 프로필 (또는 다른 성능 측정) 을 보고 싶습니다. 따라서 코드의 후기 단계에서 우리는 범위에서 룩백 기간을 변화시킴으로써 민감성 분석을 수행합니다.

SPY-IWM의 선형 회귀 모델에서 롤링 베타 계수가 계산되면, 우리는 그것을 DataFrame 쌍에 추가하고 빈 줄을 떨어 뜨립니다. 이것은 트림 메이저로 룩백의 크기에 해당하는 첫 번째 바 세트를 구성합니다. 그 다음 우리는 SPY의 단위 및 IWM의 −βi 단위로 두 ETF의 스프레드를 만듭니다. 이것은 분명히 현실적인 상황이 아닙니다. 왜냐하면 우리는 IWM의 분수량을 취하고 있기 때문에 실제 구현에서는 불가능합니다.

마지막으로, 우리는 스프레드의 평균을 빼고 스프레드의 표준편차로 정규화하여 계산되는 스프레드의 z 점수를 만듭니다. 여기서 상당히 미묘한 룩헤드 편차가 발생한다는 점에 유의하십시오. 나는 연구에서 그러한 실수를하는 것이 얼마나 쉬운지 강조하기 위해 의도적으로 코드를 남겨두었습니다. 평균과 표준편차는 전체 스프레드 시간 시리즈에 대해 계산됩니다. 이것이 진정한 역사적 정확성을 반영하려면 이러한 정보가 사용할 수 없었을 것입니다. 따라서 z 점수를 계산하기 위해 롤링 평균과 stdev을 사용해야합니다.

# 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 점수가 부정적인 z 점수를 초과 할 때 스프레드를 길게하고 z 점수가 긍정적 인 z 점수를 초과 할 때 스프레드를 짧게 계산하여 계산됩니다. 출구 신호는 z 점수의 절대 값이 다른 (대도적으로 더 작은) 임계보다 작거나 같을 때 제공됩니다.

이 상황을 달성하기 위해서는 각 바에 대해 전략이 시장의 in 또는 out인지 알아야합니다. long_market 및 short_market는 장기 및 단위 시장 위치를 추적하기 위해 정의된 두 변수입니다. 불행히도 이것은 벡터화 된 접근 방식에 비해 반복적으로 코딩하는 것이 훨씬 쉬워 계산이 느립니다. CSV 파일 당 ~ 700,000 데이터 포인트를 필요로하는 1 분 바에도 불구하고 이전 데스크톱 머신에서 여전히 비교적 빠르게 계산됩니다!

판다 데이터 프레임 (공백적으로 일반적인 연산이 아닌) 을 반복하려면 반복 방법을 사용해야합니다. 이 방법은 반복을 할 수있는 생성기를 제공합니다.

# 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

이 단계에서 우리는 실제 장기/단계 신호를 포함하도록 쌍을 업데이트하여 시장에 있어야하는지 여부를 결정할 수 있습니다. 이제 우리는 포지션의 시장 가치를 추적하기 위해 포트폴리오를 만들어야합니다. 첫 번째 작업은 장기 및 단계 신호를 결합하는 포지션 열을 만드는 것입니다. 이것은 (1,0,−1), 1이 장기/시장 위치를 대표하는 1, 0이 위치 (출출출되어야 한다) 를 대표하는 1과 -1이 짧은/시장 위치를 대표하는 요소 목록을 포함합니다. sym1 및 sym2 열은 각 바의 종료 시 SPY 및 IWM 포지션의 시장 가치를 나타냅니다.

일단 ETF 시장 값이 생성되면 각 바의 끝에 총 시장 값을 생성하기 위해 합합니다. 이것은 그 시리즈 객체에 대한 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 경로에 위치합니다. 아래 코드를 수정하여 특정 디렉토리를 가리키십시오.

룩백 기간에 전략이 얼마나 민감한지 결정하기 위해서는 룩백 범위의 성능 메트릭을 계산하는 것이 필요합니다. 나는 성능 척도로 포트폴리오의 최종 전체 비율 수익률을 선택했으며 룩백 범위는 [50,200]에서 10의 인크리먼트로 선택했습니다. 다음 코드에서 이전 함수들이 이 범위 전체에 있는 for 루프에 싸여있는 것을 볼 수 있습니다. 다른 임계값이 고정되어 있습니다. 마지막 작업은 matplotlib을 사용하여 룩백 대 수익률의 라인 차트를 만드는 것입니다.

# 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()

룩백 기간 대 수익률의 차트는 이제 볼 수 있습니다. 110 바에 해당하는 룩백 주위에는 글로벌 최대치가 있음을 유의하십시오. 룩백이 수익률에 독립된 상황을 본다면 이것은 우려의 원인이 될 것입니다.imgSPY-IWM 선형 회귀 헤지 비율 역시각 분석

어떤 백테스팅 기사도 상향 기울기 주식 곡선이 없으면 완성되지 않을 것입니다! 따라서 누적 수익률 대 시간의 곡선을 그려내고 싶다면 다음 코드를 사용할 수 있습니다. 룩백 매개 변수 연구에서 생성된 최종 포트폴리오를 그래프화합니다. 따라서 어떤 차트를 시각화하고 싶은지에 따라 룩백을 선택해야합니다. 차트는 비교를 돕기 위해 같은 기간에 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()

다음 주식 곡선 그래프는 100일 회전 기간을 나타냅니다.imgSPY-IWM 선형 회귀 헤지 비율 역시각 분석

2009년 금융위기 기간 동안 SPY의 마감률이 상당히 높았다는 점에 유의해야 한다. 또한 이 단계에서 전략은 변동적인 기간을 겪었다. 또한 SPY가 S&P500 지수를 반영하는 이 기간에 강한 경향을 보였기 때문에 지난 1년 동안 성과가 다소 악화되었다는 점에 유의해야 한다.

스프레드의 z 점수를 계산할 때 여전히 룩헤드 편향을 고려해야한다는 점에 유의하십시오. 또한, 이러한 모든 계산은 거래 비용없이 수행되었습니다. 이러한 요소가 고려되면이 전략은 확실히 매우 좋지 않을 것입니다. 수수료, 입찰 / 요청 스프레드 및 미끄러짐은 현재 모두 계산되지 않습니다. 또한 전략은 ETF의 분수 단위로 거래되며 이는 또한 매우 비현실적입니다.

후기 기사에서는 이 요소들을 고려하고 주식 곡선과 성과 측정에 대한 신뢰도를 훨씬 높일 수 있는 훨씬 더 정교한 이벤트 기반 백테스터를 만들 것입니다.


더 많은