평균회귀 모델
이 모델의 기본 가정
- 시계열 데이터는 과거의 평균값으로 회귀하려는 경향이 있습니다.
- 어떤 변수가 정규 분포를 따른다고 가정합니다. 이러한 경우, 평균에 가까이 갈 확률이 높고, 반대로 멀어질수록, 확률이 낮아진다고 할 수 있습니다.
주가로 생각해보겠습니다. 만약 평균 주가보다 낮으면 다시 올라올 것이고, 평균 주가보다 높으면 다시 내려올 것이라고 가정하는 것이 평균회귀 모델의 특징입니다.
이러한 평균회귀 모델은 이해하기 쉬우며, 우리 주변에서도 어렵지 않게 활용하는 경우를 볼 수 있습니다.
하지만 실제 주가에 적용하기에는 주가 데이터는 랜덤워크로 무작위로 움직이는 경향이 있습니다. 따라서 단순하게 평균회귀 모델을 적용하기에는 큰 어려움이 있습니다.
평균 회귀 테스트
평균 회귀 모델을 적용하기 위해서는, 현재 주가 데이터가 랜덤 워크인지 판별해야 합니다. 랜덤워크는 다음 데이터가 이전 데이터에 영향을 받지않는 독립적인 사건이라고 생각하시면 됩니다.
주가 데이터가 랜덤워크를 보인다는 것은 어떤 의미일까요? 결국 평균 회귀 모델을 적용하기 어렵다는 것입니다. 왜냐하면 주가의 흐름이 이전의 데이터와 상관이 없기 때문입니다.
따라서 평균 회귀 모델을 적용할 수 있는 시계열인지 아닌지를 판별을 해야 합니다. 평균 회귀 모델을 판별하기 위한 방법으로 대표적으로 ADF 테스트와 허스트 지수가 있습니다.
ADF 테스트
ADF 테스트는 시계열 데이터가 랜덤워크를 따른다고 가정을 합니다. 그 다음, 이 사실을 검증해, 랜덤워크 여부를 판별합니다. 파이썬을 활용해 직접 모비스 주가를 테스트해보겠습니다.
import pandas as pd
import pandas_datareader.data as web
import datetime
import matplotlib.pyplot as plt
from pandas.plotting import scatter_matrix
from pandas.tools.plotting import scatter_matrix, autocorrelation_plot
from pandas.compat import range, lrange, lmap, map, zip
import numpy as np
import statsmodels.tsa.stattools as ts
import pprint
# file_name 다운로드한 주가 데이터를 저장할 파일 이름
# company_code 종목 코드
# year1/month1/date1 데이터를 다운로드할 시작일
# year2/month2/date2 데이터를 다운로드할 마감일
def download_stock_data(file_name, company_code, year1, month1, date1, year2, month2, date2):
start = datetime.datetime(year1, month1, date1)
end = datetime.datetime(year2, month2, date2)
df = web.DataReader("%s.KS" % (company_code), "yahoo", start, end)
df.to_pickle(file_name)
return df
def load_stock_data(file_name):
df = pd.read_pickle(file_name)
return df
download_stock_data('mobis.data', '012330', 2018, 1, 1, 2018, 12, 31)
df_mobis = load_stock_data('mobis.data')
adf_result = ts.adfuller(df_mobis['Close'])
pprint.pprint(adf_result)
# (-1.6455469329073344,
# 0.45936385333903723,
# 0,
# 243,
# {'1%': -3.4575505077947746,
# '10%': -2.573148434859185,
# '5%': -2.8735087323013526},
# 4508.956331290399)
첫 번째 값은 검정 통계량입니다. 두번째 값은 p-value입니다. 네번째값은 데이터 수를 의미합니다. 다섯번째는 가설검정을 위한 기각값을 나타냅니다. 가설검정을 기각하기 위해서는 검정 통계량 값이 기가값 중 어느 하나보다 작아야 합니다. 하지만 주석의 결과를 참고하면 모비스의 주식은 평균 회귀 모델을 적용할 수 없다는 것을 알 수 있습니다.
허스트 지수
허스트 지수의 핵심은 분산을 확산 속도로 치환하는 것입니다. 그리고 그값을 GBM의 확산 속도와 비교합니다. 그리고 비교를 통해 랜덤워크인지 정상과정인지를 파악하는 것입니다.
보통 허스트 지수의 값이 0.5보다 작으면 평균회귀, 크면 추세 성향이 있다고 할 수 있습니다. 즉, H값이 0에 가까울수록 평균회귀 성향이 강합니다. 또한 1에 가까울수록 추세성향이 강합니다. 이번에는 파이썬을 이용해 모비스 주가와 만도의 주가에 허스트 지수를 알아보겠습니다.
import pandas as pd
import pandas_datareader.data as web
import datetime
import matplotlib.pyplot as plt
import numpy as np
from pandas.plotting import scatter_matrix
# file_name 다운로드한 주가 데이터를 저장할 파일 이름
# company_code 종목 코드
# year1/month1/date1 데이터를 다운로드할 시작일
# year2/month2/date2 데이터를 다운로드할 마감일
def download_stock_data(file_name, company_code, year1, month1, date1, year2, month2, date2):
start = datetime.datetime(year1, month1, date1)
end = datetime.datetime(year2, month2, date2)
df = web.DataReader("%s.KS" % (company_code), "yahoo", start, end)
df.to_pickle(file_name)
return df
def load_stock_data(file_name):
df = pd.read_pickle(file_name)
return df
def get_hurst_exponent(df):
lags = range(2, 100)
ts = np.log(df)
tau = [np.sqrt(np.std(np.subtract(ts[lag:], ts[:-lag]))) for lag in lags]
poly = np.polyfit(np.log(lags), np.log(tau), 1)
result = poly[0]*2.0
return result
download_stock_data('mobis.data', '012330', 2018, 1, 1, 2018, 12, 31)
df_mobis = load_stock_data('mobis.data')
download_stock_data('mando.data', '204320', 2018, 1, 1, 2018, 12, 31)
df_mando = load_stock_data('mando.data')
hurst_mobis = get_hurst_exponent(df_mobis['Close'])
hurst_mando = get_hurst_exponent(df_mando['Close'])
print( "Hurst : mobis=%s, mando=%s" % (hurst_mobis, hurst_mando))
# Hurst : mobis=0.11736167485553504, mando=0.05045704451869849
모비스와 만도의 허스트 지수는 각각 0.1, 0.05로 평균회귀 성향이 있다는 것을 알 수 있습니다.
평균회귀의 Half-life
두가지의 테스트를 모두 통과하는 주식 종목은 매우 드물다고 할 수 있습니다. 하지만 통과하지 못한 주식 종목에 평균회귀 모델을 적용해 고수익을 내는 모델이 종종 있습니다. 과연 테스트를 통과하지 못해도 평균회귀 모델을 적용하는 방법이 있을까요?
Half-life는 평균으로 회귀하는 데 걸리는 시간을 의미합니다. 이 값을 활용해 평균회귀 모델을 적용할 수 있는 주식 종목을 발견할 수 있습니다.
Half-life에서 의미하는 수치는 계산에 사용한 시간 단위입니다. 그러므로 계산에 사용한 데이터가 초 단위의 데이터인 경우는 어떨까요? 초 단위의 평균회귀 시간을 나타냅니다. 시간 단위라면 시간 단위의 평균 회귀 시간을 나타냅니다. 따라서 시간 단위를 유의해서 값을 구해야 합니다.
Half-life의 값이 크고 작음은 알고리즘 트레이딩에 적용하는 전략에 따라 좋을 수도 나쁠 수도 있습니다. 왜 그럴까요? 값이 크다면 장기간 지속하는 경향이 있기 때문입니다. 반면에 작다면, 그만큼 주식 값이 변동이 잦기 때문입니다. 알고리즘 모델에 전략에 따라 주식 종목을 선정할 때 유의해야 합니다.
import pandas as pd
import pandas_datareader.data as web
import datetime
import matplotlib.pyplot as plt
import numpy as np
from pandas.plotting import scatter_matrix
# file_name 다운로드한 주가 데이터를 저장할 파일 이름
# company_code 종목 코드
# year1/month1/date1 데이터를 다운로드할 시작일
# year2/month2/date2 데이터를 다운로드할 마감일
def download_stock_data(file_name, company_code, year1, month1, date1, year2, month2, date2):
start = datetime.datetime(year1, month1, date1)
end = datetime.datetime(year2, month2, date2)
df = web.DataReader("%s.KS" % (company_code), "yahoo", start, end)
df.to_pickle(file_name)
return df
def load_stock_data(file_name):
df = pd.read_pickle(file_name)
return df
def get_half_life(df):
price = pd.Series(df)
lagged_price = price.shift(1).fillna(method="bfill")
delta = price - lagged_price
beta = np.polyfit(lagged_price, delta, 1)[0]
half_life = (-1*np.log(2)/beta)
return half_life
download_stock_data('mobis.data', '012330', 2018, 1, 1, 2018, 12, 31)
df_mobis = load_stock_data('mobis.data')
download_stock_data('mando.data', '204320', 2018, 1, 1, 2018, 12, 31)
df_mando = load_stock_data('mando.data')
half_mobis = get_half_life(df_mobis['Close'])
half_mando = get_half_life(df_mando['Close'])
print( "half_life : mobis=%s, mando=%s" % (half_mobis, half_mando))
# half_life : mobis=32.319422876264746, mando=34.58971061473641
만도와 보미스의 Half-Life의 값은 각각 32, 34입니다. 두 주식 모두 비슷한 속도로 평균으로 회귀함을 알 수 있습니다. 만약 값이 매우 큰 주식 종목의 경우에는 평균회귀 성향이 다른 주식 종목보다 희박하다고 할 수 있습니다 .