이 기사에서는 우리의 첫 번째 내일 거래 전략을 고려할 것입니다. 그것은 고전적인 거래 아이디어를 사용하여,
이 전략은 크게 ETF 쌍 사이에
이 전략의 논리는 SPY와 IWM가 대략 같은 상황을 특징으로 하고 있다는 것입니다. 대기업과 소액 주식 미국 기업들의 경제입니다. 가정은 가격의 스프레드를 취하면 평균을 역전해야한다는 것입니다. S&P500 또는 러셀 2000 지수 (small-cap/large-cap 차이, 재균형 날짜 또는 블록 거래와 같은) 에 개별적으로 영향을 줄 수 있는 지역적 사건들 (시간) 이 있지만, 두 가지의 장기 가격 시리즈는 아마도 통합 될 것입니다.
이 전략은 다음 단계로 이루어집니다.
아마도 전략을 깊이 이해하는 가장 좋은 방법은 실제로 구현하는 것입니다. 다음 섹션에서는이 평균 역전 전략을 구현하기위한 완전한 파이썬 코드 (단 하나의 파일) 를 설명합니다. 이해를 돕기 위해 코드를 자유자재로 언급했습니다.
모든 파이썬/판다스 튜토리얼과 마찬가지로 이 튜토리얼에서 설명한 바와 같이 파이썬 연구 환경을 설정하는 것이 필요합니다. 설치가 완료되면 첫 번째 작업은 필요한 파이썬 라이브러리를 수입하는 것입니다. 이 백테스트를 위해 matplotlib과 판다스가 필요합니다.
제가 사용하는 특정 라이브러리 버전은 다음과 같습니다.
# 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입니다. 그 다음 두 가지 원본 파일의 인덱스를 사용하는 별도의 데이터 프레임 쌍을 만듭니다. 놓친 트레이드 및 오류로 인해 시간표가 다를 가능성이 있기 때문에 이것은 일치하는 데이터를 가질 수 있음을 보장합니다. 이것은 판다와 같은 데이터 분석 라이브러리를 사용하는 주요 이점 중 하나입니다.
# 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는 예측자 (
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 점수의 절대 값이 다른 (대도적으로 더 작은) 임계보다 작거나 같을 때 제공됩니다.
이 상황을 달성하기 위해서는 각 바에 대해 전략이 시장의
판다 데이터 프레임 (공백적으로 일반적인 연산이 아닌) 을 반복하려면 반복 방법을 사용해야합니다. 이 방법은 반복을 할 수있는 생성기를 제공합니다.
# 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 바에 해당하는 룩백 주위에는
어떤 백테스팅 기사도 상향 기울기 주식 곡선이 없으면 완성되지 않을 것입니다! 따라서 누적 수익률 대 시간의 곡선을 그려내고 싶다면 다음 코드를 사용할 수 있습니다. 룩백 매개 변수 연구에서 생성된 최종 포트폴리오를 그래프화합니다. 따라서 어떤 차트를 시각화하고 싶은지에 따라 룩백을 선택해야합니다. 차트는 비교를 돕기 위해 같은 기간에 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일 회전 기간을 나타냅니다.SPY-IWM 선형 회귀 헤지 비율 역시각 분석
2009년 금융위기 기간 동안 SPY의 마감률이 상당히 높았다는 점에 유의해야 한다. 또한 이 단계에서 전략은 변동적인 기간을 겪었다. 또한 SPY가 S&P500 지수를 반영하는 이 기간에 강한 경향을 보였기 때문에 지난 1년 동안 성과가 다소 악화되었다는 점에 유의해야 한다.
스프레드의 z 점수를 계산할 때 여전히 룩헤드 편향을 고려해야한다는 점에 유의하십시오. 또한, 이러한 모든 계산은 거래 비용없이 수행되었습니다. 이러한 요소가 고려되면이 전략은 확실히 매우 좋지 않을 것입니다. 수수료, 입찰 / 요청 스프레드 및 미끄러짐은 현재 모두 계산되지 않습니다. 또한 전략은 ETF의 분수 단위로 거래되며 이는 또한 매우 비현실적입니다.
후기 기사에서는 이 요소들을 고려하고 주식 곡선과 성과 측정에 대한 신뢰도를 훨씬 높일 수 있는 훨씬 더 정교한 이벤트 기반 백테스터를 만들 것입니다.