210708 Python - 가속화 모멘텀 전략
2021, Jul 08
가속화 모멘텀 전략
Coded by JunPyo Park
Paper: Momentum, Acceleration, and Reversal
발표자료(UNIST FE Lab Notion): 210708 - Momentum, Acceleration and Reversal
References
- 모멘텀 200년의 역사
- Momentum, Acceleration, and Reversal(2015)
- 대신증권 2021.05.17. Quant 리포트: 가속화 모멘텀 전략
In [4]:
import pandas as pd
import numpy as np
import datetime
from dateutil.relativedelta import relativedelta
import matplotlib.pyplot as plt
%matplotlib inline
Read Data¶
In [5]:
# FinanceDataReader 통해 받은 데이터
# stocks = fdr.StockListing('KOSPI') # 여기 리스트에 있는 종목의 종가만 사용
df = pd.read_pickle('kospi.pkl')['2004':]
In [6]:
df.head()
Out[6]:
- 정확한 분석을 위해서는 상폐된 종목, 해당 시점 KOSPI 대형주 유니버스(대신증권 리포트 기준) 등을 고려해야 함
JK Portfolio Example¶
In [7]:
# JK Portfolio Example
current_dt = df.loc['2011-01'].index[-1]
holding_period = relativedelta(months=3) # K = 3
lookback_period = relativedelta(months=12) # J = 12
start_dt = current_dt - lookback_period
end_dt = current_dt + holding_period
In [8]:
start_dt # J = 12
Out[8]:
In [9]:
current_dt
Out[9]:
In [10]:
end_dt # K = 3
Out[10]:
In [11]:
def calc_pct_return(price_table, start_dt, end_dt):
price_table = price_table[start_dt:end_dt]
start_price = price_table.iloc[0]
end_price = price_table.iloc[-1]
pct_return = (end_price-start_price) / start_price # pct return
return pct_return
In [13]:
scored_df = calc_pct_return(df, start_dt, current_dt).sort_values(ascending=True).dropna()
In [14]:
def quintile(df): # 5분위 나누기
Q = {}
step_size = len(df) / 5
for i in range(5):
Q[i+1] = df.iloc[int(i*len(df)/5):int((i+1)*len(df)/5)]
return Q
In [15]:
Q = quintile(scored_df) # Q[1] ~ Q[5]
In [16]:
Q[1].head()
Out[16]:
In [21]:
Q[2].head()
Out[21]:
In [22]:
Q[3].head()
Out[22]:
In [23]:
Q[4].head()
Out[23]:
In [24]:
Q[5].head()
Out[24]:
In [25]:
# Loser Portfolio Return
calc_pct_return(df[Q[1].index], current_dt, end_dt)
Out[25]:
In [26]:
# Quintile 포트폴리오 각각의 return 계산
q_returns = []
for i in range(1,6):
q_returns.append(calc_pct_return(df[Q[i].index], current_dt, end_dt).mean()) # Equal-Weighted
In [27]:
# 1~5 분위 포트폴리오의 수익률
q_returns
Out[27]:
Accelerated Momentum¶
In [28]:
class portfolio():
def __init__(self,start_year_month, rebalancing_day, holding_period, strategy_type):
self.start_dt = df[start_year_month].index[rebalancing_day-1] # 0-1 = -1 -> the last day of the month
self.current_dt = self.start_dt
self.holding_period = relativedelta(months=holding_period) # months
self.strategy_type = strategy_type
self.stock_list = {}
self.returns = {}
self.current_wealth = 1
self.wealth = {}
self.end_dt = datetime.datetime.today()
def filter_stocks(self):
# 편의상 해당 시점에 가격이 존재하는 종목만 filtering
st = self.current_dt - relativedelta(months=12)
et = self.current_dt + self.holding_period
available_stocks = df[st:et].dropna(axis=1).columns
return df[available_stocks][st:self.current_dt]
def calc_pct_return(self, price_table, st, et):
start_price = price_table[st:et].iloc[0]
end_price = price_table[st:et].iloc[-1]
pct_return = (end_price-start_price) / start_price # pct return
return pct_return
def momentum_scoring(self,price_table): # PR1YR
# 12_1 Momentum
st = self.current_dt - relativedelta(months=12)
et = self.current_dt - relativedelta(months=1)
scored_df = self.calc_pct_return(price_table, st, et)
return scored_df.sort_values(ascending=True) # 값이 큰게 5분위로 가도록
def acc_momentum_scoring(self,price_table): # Accelerated Momentum
# Acceleration Momentum
weight_scheme = [1] * 6 + [-1] * 6 # use simple weight scheme
scored_array = np.zeros(len(price_table.columns))
et = self.current_dt # end dt
for i in range(1,13):
st = self.current_dt - relativedelta(months=i) # start dt
r_i = self.calc_pct_return(price_table, st, et)
r_i = (1+r_i) ** (1/i) - 1 # monthly average
scored_array += weight_scheme[i-1] * r_i
scored_df = pd.Series(scored_array, index=price_table.columns)
return scored_df.sort_values(ascending=False) # 값이 작은게 5분위로 가도록
def quintile(self,df): # 5분위 나누기
Q = {}
step_size = len(df) / 5
for i in range(5):
Q[i+1] = df.iloc[int(i*len(df)/5):int((i+1)*len(df)/5)]
return Q
def calc_port_return(self,buy_stocks,sell_stocks):
st = self.current_dt
et = self.current_dt + self.holding_period
# buy reutrn
buy_return = self.calc_pct_return(df[buy_stocks], st, et)
# sell return
sell_return = self.calc_pct_return(df[sell_stocks], st, et)
# equal-weighted portfolio
# for simplicity
return (buy_return.mean() - sell_return.mean())
def construct_portfolio(self):
while((self.current_dt + self.holding_period) < self.end_dt):
price_table = self.filter_stocks()
if self.strategy_type == 'simple':
scored_df = self.momentum_scoring(price_table)
elif self.strategy_type == 'acc':
scored_df = self.acc_momentum_scoring(price_table)
Q = self.quintile(scored_df) # Q[1] ~ Q[5]
buy_stocks = list(Q[5].index)+list(Q[4].index) # 4,5 분위 매수
sell_stocks = list(Q[2].index)+list(Q[1].index) # 1,2 분위 매도
self.stock_list[self.current_dt] = {'buy':buy_stocks,
'sell':sell_stocks}
port_return = self.calc_port_return(buy_stocks,sell_stocks)
self.returns[self.current_dt] = port_return
self.current_wealth = self.current_wealth * (1+port_return)
self.wealth[self.current_dt] = self.current_wealth
self.current_dt += self.holding_period
return pd.Series(self.wealth)
In [29]:
# 가속화 모멘텀 전략
acc_port = portfolio(start_year_month = '2005-01',
rebalancing_day = 0, # 0 월의 마지막 거래일 리밸런싱
holding_period = 1,
strategy_type = 'acc')
acc_wealth = acc_port.construct_portfolio()
In [30]:
acc_wealth.plot();
In [31]:
# 단순 모멘텀 전략(PR1YR)
simple_momentum_port = portfolio(start_year_month = '2005-01',
rebalancing_day = 0, # 0 월의 마지막 거래일 리밸런싱
holding_period = 1,
strategy_type = 'simple')
simple_wealth = simple_momentum_port.construct_portfolio()
In [32]:
simple_wealth.plot();
Results¶
PR1YR vs ACC 모멘텀 성과 추이 비교¶
In [34]:
plt.figure(figsize=(12,7))
acc_wealth.plot(label='Accelerated Momentum');
simple_wealth.plot(label='PR1YR Momentum');
plt.legend()
plt.show()
- 리포트에서는 KOSPI 대형주 유니버스를 사용해서 결과값이 다르게 나옴 (
아니면 코드를 잘못 만들었거나...)
- Portfolio Weighting 방식을 value-weighted 로 바꾸어 보기 (여기서는 equal-weighted 사용)
변동성 대비 수익률¶
In [35]:
acc_logret = np.log(acc_wealth / acc_wealth.shift(1)).dropna()
In [36]:
simple_logret = np.log(simple_wealth / simple_wealth.shift(1)).dropna()
In [37]:
# 가속화 모멘텀 0.523
acc_logret.mean() / acc_logret.std() * np.sqrt(12) # annualize
Out[37]:
In [38]:
# PR1YR 0.217
simple_logret.mean() / simple_logret.std() * np.sqrt(12)
Out[38]:
Paper의 Figure 2¶
- x축이 J, K=1로 고정한 JK 5분위 포트폴리오의 수익률 평균을 plot
In [65]:
class portfolio():
def __init__(self,start_year_month, rebalancing_day, holding_period, lagging_period):
self.start_dt = df[start_year_month].index[rebalancing_day-1]
self.current_dt = self.start_dt
self.holding_period = relativedelta(months=holding_period) # months
self.returns = {}
self.current_wealth = 1
self.wealth = {}
self.end_dt = datetime.datetime.today()
# self.end_dt = pd.Timestamp('2010')
self.num_lagging = lagging_period
self.lagging_period=relativedelta(months=lagging_period) # months
def filter_stocks(self):
# 편의상 해당 시점에 가격이 존재하는 종목만 filtering
st = self.current_dt - self.lagging_period
et = self.current_dt + self.holding_period
available_stocks = df[st:et].dropna(axis=1).columns
return df[available_stocks][st:self.current_dt]
def calc_pct_return(self, price_table, st, et):
start_price = price_table[st:et].iloc[0]
end_price = price_table[st:et].iloc[-1]
pct_return = (end_price-start_price) / start_price # pct return
return pct_return
def momentum_scoring(self,price_table):
st = self.current_dt - self.lagging_period
et = self.current_dt
scored_df = self.calc_pct_return(price_table, st, et)
return scored_df.sort_values(ascending=True) # 값이 큰게 5분위로 가도록
def quintile(self,df): # 5분위 나누기
Q = {}
step_size = len(df) / 5
for i in range(5):
Q[i+1] = df.iloc[int(i*len(df)/5):int((i+1)*len(df)/5)]
return Q
def calc_port_return(self,buy_stocks):
st = self.current_dt
et = self.current_dt + self.holding_period
# buy reutrn
buy_return = self.calc_pct_return(df[buy_stocks], st, et)
# equal-weighted portfolio
# for simplicity
return buy_return.mean()
def construct_portfolio(self):
while((self.current_dt + self.holding_period) < self.end_dt):
price_table = self.filter_stocks()
scored_df = self.momentum_scoring(price_table)
self.Q = self.quintile(scored_df) # Q[1] ~ Q[5]
quintiles = [2,3,4]
q_returns = []
for q in quintiles:
buy_stocks = list(self.Q[q].index) # 2,3,4 분위
port_return = self.calc_port_return(buy_stocks)
q_returns.append(port_return)
self.returns[self.current_dt] = q_returns
self.current_dt += self.holding_period
q_returns = pd.DataFrame(self.returns).T
q_returns.columns = ['Q2','Q3','Q4']
return ((1+q_returns).cumprod() ** (1/len(q_returns)) - 1).iloc[-1]
Q2, Q3, Q5 Plot¶
In [66]:
q = pd.DataFrame(columns=['Q2','Q3','Q4'])
for i in range(1,13): # lagging months
lag_port = portfolio(start_year_month = '2005-01',
rebalancing_day = 0, # 0, 월의 마지막 거래일에 리밸런싱
holding_period = 1,
lagging_period=i)
q_returns = lag_port.construct_portfolio()
q = q.append(q_returns, ignore_index=True)
In [67]:
q.index = range(1,len(q)+1)
In [68]:
q.plot(figsize=(10,5));
- Paper와 같이 Q 1,3,5로 그려보면 모양이 다르게 나옴 - Q5가 Q1 보다 under perform
- 한국시장에서 모멘텀 전략이 잘 안먹히는 결과와 연관이 있을 듯
- 모멘텀 상위 10분위 포트폴리오의 수익률이 바닥에 있음
- 정확한 KOSPI 구성종목 데이터를 사용하고 전처리를 통해 가벼운 주식을 날리거나(논문에서도 $2 이하 날림) value-weighted 방식으로 하면 개선 되지 않을까?
- Q 2,3,4를 그려보면 Fig 2.와 어느정도 비슷한 개형을 확인할 수 있음