2021 미래에셋자산운용 빅데이터 페스티벌
2021 미래에셋자산운용 빅데이터 페스티벌
- ETF를 활용한 데이터 기반 투자 전략 알고리즘 개발 -
UNIST 산업공학과 박준표
References¶
- Mebean Faber(2006) - A Quantitative Approach to Tactical Asset Allocation
- Xiong and Ibbotson(2015) - Momentum, Acceleration, and Reversal
- AQR Capital Management(2019) - You Can't Always Trend When You Want
- JunPyoPark Blog(Code Reference) - Python - 가속화 모멘텀 전략
Data Processing¶
import pandas as pd
import numpy as np
from magi_module import get_report
import matplotlib.pyplot as plt
%matplotlib inline
from dateutil.relativedelta import relativedelta
기본 제공된 126개의 TIGER ETF 종가 데이터를 사용하였으며 전략의 비교지수로 활용할 코스피 지수 데이터를 외부 데이터로 사용
- 코스피 지수 데이터 출처: FinanceDataReader 모듈을 사용하여 다운로드
import FinanceDataReader as fdr
ks11 = fdr.DataReader('ks11')
ks11 = pd.read_pickle('외부데이터/kospi_price.pkl')
kospi_close = ks11['Close']
etf_data = pd.read_csv('./data/etf_data.csv', encoding = 'euc_kr', parse_dates= ['tdate'])
etf_ohlcv = etf_data.set_index(['tdate', 'etf_code', 'data_name'])['value'].unstack()
# 종가 데이터 (126개의 etf에 대한)
etf_close = etf_ohlcv['종가'].unstack()
전략의 장기적 성과 확인을 위해 target_start_date
를 '2012-12-31'
로 설정
# target_start_date = '2018-12-31'
target_start_date = '2012-12-31'
target_end_date = '2021-05-31'
Accelerated Momentum Scoring¶
- 모멘텀 팩터에 대한 연구는 Jegadeesh(1990), Jegadeesh and Titman(1993)의 연구를 기점으로 폭발적으로 진행되었으며 모멘텀 측정 지표를 개선하기 위한 시도도 꾸준히 이루어짐
- 가장 널리 쓰이는 모멘텀 지표인 직전 1개월 제외 12개월 수익률(PR1YR)에 근거한 전략은 최근 몇 년간 KOSPI 내에서의 성과가 부진한 모습을 보임
- Xiong and Ibbotson(2015)의 가속화 모멘텀 전략은 주가가 끝없이 상승할 수는 없기에 상승의 가속 뒤 주가 성과가 부진할 것이라는 가정을 전제로 함
- 가속화 모멘텀은 KOSPI 대형주 유니버스에서 PR1YR 모멘텀 전략 대비 훨씬 개선된 성과를 기록하였으며 본 전략에서는 가속화 모멘텀(Accelerated Momentum) Scoring 방법론을 사용함
Momentum Weighting Scheme¶
주가 상승의 가속화를 측정하기 위해 직전 1개월 ~ 12개월 수익률에 1에서 -1 사이의 가중치를 부여하여 합산
- Xiong and Ibbotson(2015)의 연구에서는 스코어가 클수록 주가 상승의 가속화가 더 크게 일어났다고 판단하지만 본 전략에서는 가속화가 더욱 크게 일어난 종목에 낮은 점수를 부여해야 하기 때문에 직전 1개월 ~ 6개월 까지는 -1, 7개월 ~ 12개월 까지는 1의 가중치를 부여함
- 본 전략에서는 Step Function 형태의 simple weighting scheme을 사용하였으며 후속 전략연구에서는 매크로 데이터 또는 다른 시계열 데이터와 딥러닝 모델을 통해 해당 가중치를 학습시켜 최적의 weight를 찾으려 함
Accelerated Momentum Scoring¶
# initialize score
acc_score = pd.DataFrame(columns = etf_close.columns, index=etf_close.index).fillna(0)
# 분위 선정 방식이 반대(score가 큰 값이 1분위)이기 때문에 weight의 부호가 바뀜
weight_scheme = [-1] * 6 + [1] * 6
for i in range(1,13):
days = i * 20 # 한달을 20거래일로 가정
acc_score += (etf_close.pct_change(days)) * weight_scheme[i-1]
# 분위 선정 방식이 반대(score가 큰 값이 1분위)이기 때문에 weight의 부호가 바뀜
weight_scheme = [-1] * 6 + [1] * 6
acc_score = pd.DataFrame(columns = etf_close.columns, index=etf_close.index).fillna(0) # initialize score
for i in range(1,13):
days = i * 20 # 한달을 20거래일로 가정
acc_score += (etf_close.pct_change(days)) * weight_scheme[i-1]
- 반복되는 코드를 최적화 하기 위해 magi_module.py의 get_report wrapper 함수를 만듦
- 비교지수인 코스피 지수를 ress DataFrame에 추가함 (그래프의 회색 선)
- 포트폴리오 성과지표로 - CAGR / MDD를 추가함
Portfolio Results and Summaries¶
def get_report_(score, kospi_close):
summary, ress = get_report(score) #magi_module.py의 get_report 모듈
col_kospi = kospi_close[ress.index]
col_kospi /= col_kospi.iloc[0]
ress['KOSPI'] = col_kospi
CAGR_MDD = -summary.loc['CAGR'] / summary.loc['MDD']
CAGR_MDD.name = 'CAGR / MDD'
summary = summary.append(CAGR_MDD)
return summary, ress
def get_ress(score, target_start_date, target_end_date):
# 매일 리밸런싱
daily_score = score.loc[target_start_date: target_end_date]
# 매주(마지막일 기준) 리밸런싱
weekly_score =score.resample('W').last().loc[target_start_date: target_end_date]
# 매달(마지막일 기준) 리밸런싱
monthly_score =score.resample('M').last().loc[target_start_date: target_end_date]
res = {}
res['daily_summary'], res['daily_ress'] = get_report_(daily_score, kospi_close)
res['weekly_summary'], res['weekly_ress'] = get_report_(weekly_score, kospi_close)
res['monthly_summary'], res['monthly_ress'] = get_report_(monthly_score, kospi_close)
return res
# 12-12-31 ~ 21-05-31 까지의 results들 get
res = get_ress(acc_score, target_start_date, target_end_date)
res['daily_summary']
res['daily_ress'].plot(figsize=(12,5));
res['weekly_summary']
res['weekly_ress'].plot(figsize=(12,5));
res['monthly_summary']
res['monthly_ress'].plot(figsize=(12,5));
Disparity - Accelerated Momentum Scoring¶
Disparity Score Concepts¶
- Mebane Faber는 “A Quantitative Approach to Tactical Asset Allocation” (2006)에서 10개월 이동평균선(이하 200일 이평)을 활용한 타이밍 전략을 소개함
- 해당 전략의 성과(수익률, MDD 등)가 단순 보유 전략보다 우수하였고 이를 scoring에 반영하기 위해 200일 이평과 종가의 괴리를 나타내는 지표인 이격도를 사용함
Disparity Score는 위에서 계산한 가속화 모멘텀 스코어에 곱해지게 되며 이는 종가가 200일 이동평균선 위에 위치하고 있다면 score에 가산점을 받게 되고 반대로 종가가 200일 이동평균선 아래에 위치하고 있다면 score에 페널티를 받음을 의미함
Disparity Scoring¶
# 200일 이평
ma_200 = etf_close.rolling(window=200).mean()
disparity_score = etf_close / ma_200 # 이격도
# 200일 이평
ma_200 = etf_close.rolling(window=200).mean()
disparity_score = etf_close / ma_200 # 이격도
위에서 계산한 가속화 모멘텀 스코어에 이격도 스코어를 반영하여 최종 스코어를 산출하였으며 실험 결과 disparity score의 제곱을 곱해주었을 때 포트폴리오의 성과가 더욱 개선됨을 확인
# acc_disparity_score_score = acc_score * disparity_score # 200일 이평에 대한 이격도 반영
# 두번 반영(제곱을 곱)해주면 성과가 더 좋아짐
acc_disparity_score = acc_score * disparity_score * disparity_score # 200일 이평에 대한 이격도 반영
Portfolio Results and Summaries¶
res = get_ress(acc_disparity_score, target_start_date, target_end_date)
res['daily_summary']
res['daily_ress'].plot(figsize=(12,5));
res['weekly_summary']
res['weekly_ress'].plot(figsize=(12,5));
res['monthly_summary']
res['monthly_ress'].plot(figsize=(12,5));
성과 분석¶
월말을 기준으로 리밸런싱을 한 'RANK_L-S' 포트폴리오 결과를 기준으로 전략의 성과 분석을 진행함 이는 대부분의 전략에서 포트폴리오의 성과지표가 가장 우수하였고 turnover도 낮은 수치이기 때문에 거래비용을 줄일 수 있기 때문임
monthly_ress = res['monthly_ress']
위험등급¶
과거 3년간 (로그)수익률 변동성(연환산 표준편차)을 계산하여 전략의 위험등급을 판단함
monthly_logret = np.log(monthly_ress / monthly_ress.shift(1)).dropna()
std_3y = monthly_logret[-252*3:].std() * np.sqrt(252)
std_3y
전략 별 성과 비교¶
성과 지표 산출 방식은 magi_module.py
에서 사용한 방식과 동일하게 적용
def MDD(res):
return (res / res.cummax() -1).min()
def CAGR(res):
return res.values[-1] ** (1 / 36 / 30 * 360) - 1
def sharpe(res):
return np.mean(res.pct_change().dropna()) / np.std(res.pct_change().dropna()) * np.sqrt(12)
def compare_port(port_res, bench_res):
compare_summary = {}
compare_summary['Return'] = port_res.iloc[-1], bench_res.iloc[-1]
compare_summary['CAGR'] = CAGR(port_res), CAGR(bench_res)
compare_summary['MDD'] = MDD(port_res), MDD(bench_res)
compare_summary['Sharpe'] = sharpe(port_res), sharpe(bench_res)
compare_summary['CAGR / MDD'] = -CAGR(port_res) / MDD(port_res), -CAGR(bench_res) / MDD(bench_res)
compare_summary = pd.DataFrame(compare_summary).T
compare_summary.columns = [port_res.name, bench_res.name]
return compare_summary
단순 모멘텀 전략과 가속화 모멘텀 전략의 성과 비교¶
가속화 모멘텀 전략이 단순 모멘텀 전략에 비해 우위가 있는지 확인을 위해 직전 1개월 제외 12개월 수익률(PR1YR)에 근거한 scoring 전략과 성과를 비교함
pr1y_score
: 1개월 제외 12개월 수익률을 기준으로 scoring
pr1y_score = pd.DataFrame(columns = etf_close.columns, index=etf_close.index).fillna(0) # initialize score
for i in range(240,len(pr1y_score)):
pr1y_score.iloc[i] = etf_close.iloc[i] / etf_close.iloc[i-220]
monthly_score =pr1y_score.resample('M').last().loc[target_start_date: target_end_date]
pr1y_summary, pr1y_ress = get_report(monthly_score) # pr1y 포트폴리오 결과물
전체기간(2013-01 ~ 2021-05)의 성과 지표 비교
port_res = monthly_ress['RANK_L-S']
bench_res = pr1y_ress['RANK_L-S']
plt.figure(figsize=(12,5))
port_res.plot(label='Accelerated Momentum');
bench_res.plot(label='PR1YR Momentum');
plt.legend();
compare_results = compare_port(port_res,bench_res)
compare_results.columns = ['Accelerated','PR1YR']
compare_results
전체 기간동안 가속화 모멘텀 전략을 사용한 포트폴리오의 모든 성과지표가 단순 모멘텀 전략보다 우수함을 확인함
설정기간(2019-01 ~ 2021-05)의 성과 지표 비교
port_res = monthly_ress['RANK_L-S']['2018-12-31':'2021-05-31']
port_res = port_res / port_res[0] # 시작일을 1로 설정
bench_res = pr1y_ress['RANK_L-S']['2018-12-31':'2021-05-31']
bench_res = bench_res / bench_res[0] # 시작일을 1로 설정
plt.figure(figsize=(12,5))
port_res.plot(label='Accelerated Momentum');
bench_res.plot(label='PR1YR Momentum');
plt.legend();
compare_results = compare_port(port_res,bench_res)
compare_results.columns = ['Accelerated','PR1YR']
compare_results
설정기간동안 가속화 모멘텀 전략을 사용한 포트폴리오의 모든 성과지표가 단순 모멘텀 전략보다 우수하며 특히 MDD를 절반 이하(31% -> 14%)로 낮춰줌을 확인
Disparity Score의 영향 분석¶
이격도 스코어가 실제 전략에 어떤 영향을 주었는지 파악하기 위하여 단순히 가속화 모멘텀 스코어만 반영한 포트폴리오와 성과 분석을 진행함
전체기간(2013-01 ~ 2021-05)의 성과 지표 비교
monthly_score =acc_score.resample('M').last().loc[target_start_date: target_end_date]
acc_only_summary, acc_only_ress = get_report(monthly_score)
port_res = monthly_ress['RANK_L-S']
bench_res = acc_only_ress['RANK_L-S']
plt.figure(figsize=(12,5))
port_res.plot(label='Disparity-Accelerated Momentum');
bench_res.plot(label='Accelerated Momentum Only');
plt.legend();
compare_results = compare_port(port_res,bench_res)
compare_results.columns = ['Disparity-Accelerated','Accelerated Only']
compare_results
설정기간(2019-01 ~ 2021-05)의 성과 지표 비교
port_res = monthly_ress['RANK_L-S']['2018-12-31':'2021-05-31']
port_res = port_res / port_res[0] # 시작일을 1로 설정
bench_res = acc_only_ress['RANK_L-S']['2018-12-31':'2021-05-31']
bench_res = bench_res / bench_res[0] # 시작일을 1로 설정
plt.figure(figsize=(12,5))
port_res.plot(label='Disparity-Accelerated Momentum');
bench_res.plot(label='Accelerated Momentum Only');
plt.legend();
compare_results = compare_port(port_res,bench_res)
compare_results.columns = ['Disparity-Accelerated','Accelerated Only']
compare_results
- 이격도 지표를 활용한다는 아이디어도 큰 틀에서는 모멘텀과 같은 추세추종 전략임
- 이격도 스코어가 전반적인 포트폴리오 성과 개선에 소폭 도움을 줌을 확인
비교지수(코스피)와의 성과 비교¶
전체기간(2013-01 ~ 2021-05)의 성과 지표 비교
port_res = monthly_ress['RANK_L-S']
bench_res = monthly_ress['KOSPI']
plt.figure(figsize=(12,5))
port_res.plot(label='Acc-Disparity Strategy')
bench_res.plot(label='KOSPI')
plt.legend();
compare_port(port_res,bench_res)
전체기간에서 장기적으로 봤을 때 포트폴리오의 모든 성과지표가 비교지수인 코스피 지수보다 우수함을 확인 가능
설정기간(2019-01 ~ 2021-05)의 성과지표 비교
target_start_date = '2018-12-31'
target_end_date = '2021-05-31'
score = acc_disparity_score.loc[target_start_date: target_end_date]
res = get_ress(score, target_start_date, target_end_date)
port_res = res['monthly_ress']['RANK_L-S']
port_res = port_res / port_res[0] # 시작일을 1로 설정
bench_res = res['monthly_ress']['KOSPI']
bench_res = bench_res / bench_res[0] # 시작일을 1로 설정
plt.figure(figsize=(12,5))
port_res.plot(label='Portfolio');
bench_res.plot(label='KOSPI');
plt.legend();
compare_port(port_res,bench_res)
본 과제의 설정기간에서 전략 포트폴리오의 MDD와 CAGR/MDD는 우수하지만 수익률이 비교지수보다 낮으며 그에 따라 샤프지수도 낮은 결과를 보임
설정기간에서 비교지수에 비해 성과가 부진한 이유 분석¶
설정 기간에서 전략의 성과가 부진한 이유를 크게 2가지 이유로 설명할 수 있음
1. 큰 틀에서 모멘텀 전략과 같은 추세추종 전략이 그 알파를 잃어가고 있음 (2010년대의 전반적인 특성, COVID 이전)
* AQR(2019)에서 나온 논문 You Can't Always Trend When You Want에 따르면 모멘텀과 같은 추세추종 전략이 2010년도 이후 힘을 잃어가는 가장 주요한 이유를 시장의 가격 움직임이 작아졌기 때문이라고 설명하고 있음
2. 본 전략이 COVID 이후 계속되는 주가 상승을 스코어에 정확히 반영하지 못함(COVID 이후의 특성)
* 가속화 모멘텀 전략은 주가가 끝없이 상승할 수는 없기에 상승의 가속 뒤 주가 성과가 부진할 것이라는 가정을 전제로 하여 가중치를 부여함
* 본 전략에서는 6개월을 기준으로 주가 상승에 페널티를 주는 단순한 가중치 부여 방식(Simple Weighting Scheme)을 사용하였는데 이는 COVID 이후 12개월 이상 상승하고 있는 현재의 시장상황을 정확히 반영하지 못한다 판단됨
후속 전략 연구 계획¶
딥러닝 모델을 통한 weighting scheme 재설계¶
위의 설정기간에서 성과가 부진한 이유를 종합하면 변화하는 시장 상황에 맞추어 스코어링 전략이 변화하지 못하기 때문임 특히, 스코어를 산출할 때 직전 1개월에서 직전 12개월에 부여하는 가중치를 본 전략에서는 단순히 -1과 1로 지정하였는데 이 부분을 아래와 같이 딥러닝 모델을 활용하여 개선한다면 더 좋은 성과를 얻을 수 있을것
- 트레이닝 기간에 ETF 가격 데이터를 바탕으로 적당한 최적 가중치를 미리 만들어(forward looking) label로 사용 (12차원 벡터)
- Input 데이터는 ETF 가격 데이터 뿐만 아니라 다른 매크로나 시계열 데이터 또한 사용 가능
- 딥러닝 모델을 통해 위에서 설정한 시계열 Input 데이터와 forward looking을 통해 만들어 놓은 최적 가중치를 mapping 시키는 함수를 학습
- 현재 GAN, LSTM, GRU 등의 딥러닝 모델을 활용하여 다변량 시계열의 joint distribution과 conditional distribution을 모델링 하는 연구를 진행하고 있는데 해당 모델들을 활용하여 가속화 모멘텀의 weighting scheme을 재설계 하는데 도움을 받을 수 있음
스코어 기반 섹터 로테이션 전략으로의 활용¶
본 전략은 섹터 로테이션에도 적용이 가능함 예를 들어 WICS 26개 업종 중 상위 n개를 편입하는 전략 또는 상위 n개를 매수, n개를 매도 하는 롱-숏 전략 등을 만들어 볼 수 있음
포트폴리오 최적화 과정, 위험관리 방법의 적용¶
포트폴리오 구성 단계에서 최적화 프로세스(robust optimization 등)를 집어넣거나 위험관리 방법(특정 종목, 업종에 대한 비중 제한 등)을 적용하면 더욱 우수한 성과가 나올 것
최종 Score 결과물 저장¶
submission = score.stack()
submission = submission.reset_index()
submission.columns = ['tdate', 'code', 'score']
submission.to_csv('2021 빅페_미래에셋자산운용_score.csv')
pd.read_csv('2021 빅페_미래에셋자산운용_score.csv')