210708 Python - 가속화 모멘텀 전략

210708 Python - 가속화 모멘텀 전략

2021, Jul 08    
acc_momentum

가속화 모멘텀 전략

Coded by JunPyo Park

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]:
AJ네트웍스 AK홀딩스 BGF BGF리테일 BNK금융지주 BYC CJ CJ CGV CJ대한통운 CJ씨푸드 ... 효성티앤씨 효성화학 후성 휠라홀딩스 휴니드 휴비스 휴스틸 휴켐스 흥국화재 흥아해운
Date
2004-01-02 NaN 8297.0 NaN NaN NaN 39600.0 31419.0 NaN 14400.0 475.0 ... NaN NaN NaN NaN 4100.0 NaN 4625.0 1984.0 4723.0 963.0
2004-01-05 NaN 8109.0 NaN NaN NaN 39700.0 32122.0 NaN 14325.0 480.0 ... NaN NaN NaN NaN 4150.0 NaN 4680.0 2036.0 4644.0 1012.0
2004-01-06 NaN 7858.0 NaN NaN NaN 39500.0 32272.0 NaN 14250.0 470.0 ... NaN NaN NaN NaN 4050.0 NaN 4575.0 2129.0 4654.0 1162.0
2004-01-07 NaN 7670.0 NaN NaN NaN 39200.0 32674.0 NaN 15075.0 480.0 ... NaN NaN NaN NaN 4000.0 NaN 4655.0 2114.0 4605.0 1333.0
2004-01-08 NaN 7576.0 NaN NaN NaN 39400.0 32674.0 NaN 15000.0 510.0 ... NaN NaN NaN NaN 4050.0 NaN 4560.0 2033.0 4428.0 1532.0

5 rows × 770 columns

  • 정확한 분석을 위해서는 상폐된 종목, 해당 시점 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]:
Timestamp('2010-01-31 00:00:00')
In [9]:
current_dt
Out[9]:
Timestamp('2011-01-31 00:00:00')
In [10]:
end_dt # K = 3
Out[10]:
Timestamp('2011-04-30 00:00:00')
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]:
인스코비      -0.777863
티웨이홀딩스    -0.741223
웰바이오텍     -0.701642
미래아이앤지    -0.630279
쎌마테라퓨틱스   -0.611176
dtype: float64
In [21]:
Q[2].head()
Out[21]:
한솔PNS   -0.113497
SK가스    -0.110823
세하      -0.110479
대웅제약    -0.110433
GKL     -0.110312
dtype: float64
In [22]:
Q[3].head()
Out[22]:
깨끗한나라     0.061509
한국단자      0.062842
KG동부제철    0.065005
우신시스템     0.066176
녹십자       0.069049
dtype: float64
In [23]:
Q[4].head()
Out[23]:
한일철강      0.255230
KPX케미칼    0.256071
참엔지니어링    0.259259
삼성전자      0.262548
KCTC      0.264706
dtype: float64
In [24]:
Q[5].head()
Out[24]:
녹십자홀딩스    0.628994
대원전선      0.636782
현대글로비스    0.642105
두산        0.645317
현대제철      0.646919
dtype: float64
In [25]:
# Loser Portfolio Return
calc_pct_return(df[Q[1].index], current_dt, end_dt)
Out[25]:
인스코비       0.149485
티웨이홀딩스     0.431579
웰바이오텍     -0.159053
미래아이앤지     0.120690
쎌마테라퓨틱스    0.374011
             ...   
인천도시가스    -0.066513
한농화성       0.052632
에이엔피      -0.026309
DRB동일     -0.051254
SK증권      -0.015842
Length: 121, dtype: float64
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]:
[-0.03691360435891919,
 -0.019899033030225874,
 0.05018993927471566,
 0.04672552079857312,
 0.1079683818630946]

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]:
0.523569568275349
In [38]:
# PR1YR 0.217
simple_logret.mean() / simple_logret.std() * np.sqrt(12)
Out[38]:
0.21721580159360945

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.와 어느정도 비슷한 개형을 확인할 수 있음