퀀트

24. 게리 안토나치의 듀얼 모멘텀 전략

만 기 2023. 1. 14. 23:50

 

게리 안토나치의 듀얼 모멘텀 전략

게리 안토나치가 제안한 절대 모멘텀과 상대 모멘텀을 결합한 투자 전략

 

전략의 기본 원리

상승장이 아닌 시점에는 현금을 보유함으로써 위험을 최소화하고(절대모멘텀), 상승장인 시점에서는 가장 큰 추세를 가진 자산군을 사용함으로써 최대의 수익을 취함(상대 모멘텀)

  • 모멘텀 : 특정 자산에 대하여 상승하는 가격이 더욱 상승하거나, 하락하는 자산 가격이 더욱 하락하는 경향을 말하며 주가 추세의 가속도를 측정해 주가의 변동 상황을 이해하는 하나의 방법
  • 절대 모멘텀 : 투자 자산의 과거 12개월과 비요하여 절대적 상승세를 평가
  • 상대 모멘텀 : 투자 자산 가운데 상대적으로 강세를 보이는 곳에 투자

 

파라미터

  1. 어떤 자산군(종목)을 편입할 것인가?
  2. 절대 모멘텀의 기준은 무엇인가? (지난 6개월 수익률이 +인지, 예금 금리 이상인지, 벤치마크 대비 outperform 했는지 등)
  3. 모멘텀 판단 기준 주기를 어떻게 잡을 것인가?

 

예시 - 국제 ETF 듀얼모멘텀

모멘텀 측정 주기 : 6개월

사용 자산군

  • KODEX 미국 S&P500 선물 - 미국 지수
  • TIGER 유로스탁스50 - 유럽 지수
  • KODEX 일본 TOPIX100 - 일본 지수
  • KODEX 200 - 한국 지수

매수

  1. 4개 자산군의 최근 6개월 수익률이 모두 - 이면 현금 보유 ⇒ 하락 추세(절대 모멘텀 없음), 하나라도 +이면 절대 모멘텀 있다고 판단하여 상대 모멘텀을 체크한다.
  2. 최근 6개월 수익률이 가장 높은 자산군을 매수 ⇒ 상대 모멘텀

매도

  1. 최근 6개월 수익률이 가장 높은 자산군이 변경되면 보유중인 자산군을 매도하고 해당 자산군을 매수
  2. 4개 자산군의 최근 6개월 수익률이 모두 -되면 매도 후 현금 보유

 

 

국제 ETF 듀얼모멘텀

데이터 준비

# 자산군 4개 종목
stock_list = [
  ["KODEX 미국S&P500선물(H)", "219480"],
  ["TIGER 유로스탁스50(합성 H)", "195930"],
  ["KODEX 일본TOPIX100", "101280"],
  ["KODEX 200", "069500"]
]


# 종가 데이터 가져오기
# 가장 짧은 날짜 종목 기준으로 시작일 설정
df_list = [fdr.DataReader(code, '2015-05-29')['Close'] for name, code in stock_list]

# 병합해서 데이터프레임으로 만들기
df = pd.concat(df_list, axis=1)
df.columns = [name for name, code in stock_list]

# 수익률 데이터 프레임으로 전환
수익률_측정기간 = 120    # 6개월
수익률_개월 = df.pct_change(periods=수익률_측정기간)


# 상대모멘텀, 절대모멘텀 columns 추가하기
df['상대모멘텀'] = 수익률_개월.idxmax(axis='columns')
df['절대모멘텀'] = 수익률_개월.max(axis='columns') > 0    # True & False

# 상대모멘텀과 절대모멘텀을 구할수 없는 앞에 개월간의 데이터(NaN)를 스킵
df = df[수익률_측정기간:]


# 시뮬레이션을 위한 날짜 변수와 날짜 인덱스 변수 만들기
date_list = df.index
date_i = np.arange(len(date_list))
  • idxmax = 최댓값을 가지는 인덱스 레이블을 반환
    • axis="columns" : 행 방향으로 탐색하여 열로 반환

 

듀얼 모멘텀 알고리즘

print(f"듀얼모멘텀 전략 Signal, 모멘텀 측정주기 : {수익률_측정기간}")

# 신호 데이터 수집
signal_i_list = []
signal_date_list = []
signal_return_list = []
#-------------------------------------------

포지션_보유유무 = False   # 투자 상태 : 전체 현금 보유 시작
for i in date_i:
    if i < 1:    # 첫날의 이전날 데이터 없으므로 
        continue
    
		# 전날과 당일 데이터 준비    
    prev_date = date_list[i-1]
    now_date = date_list[i]
    
    직전_상대모멘텀_종목 = df['상대모멘텀'].loc[prev_date]
    당일_상대모멘텀_종목 = df['상대모멘텀'].loc[now_date]
    
    직전_절대모멘텀 = df['절대모멘텀'].loc[prev_date]
    당일_절대모멘텀 = df['절대모멘텀'].loc[now_date]
    # -------------------------------------------------------
    
    # 매수 신호 포착 시점
    if 당일_절대모멘텀 == True and 직전_절대모멘텀 == False:
        매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
        매수_종목 = 당일_상대모멘텀_종목
        포지션_보유유무 = True
        print(f'++++++++++++++++++++++++++++++++++++++{now_date} 절대모멘텀 발생+++++++++++++++++++++++++++++++++++')
    
    # 매도 신호 포착 시점 : 수익률 계산, 신호 데이터 수집
    elif 당일_절대모멘텀 == False and 직전_절대모멘텀 == True:
        매도시_주가 = df[매수_종목].loc[now_date]
        수익률 = (매도시_주가 / 매수시_주가) * 100 - 100
        print(f"  - {now_date}  매도 Signal 발생!  매수매도종목 {매수_종목}  ->  {매수_종목}, 매수주가 {매수시_주가:.0f}  ->  매도주가 {매도시_주가:.0f}  , 수익률 {수익률:.3f} %")
        print(f'--------------------------------------{now_date} 절대모멘텀 상실----------------------------------------------')
        signal_i_list.append(i)
        signal_date_list.append(now_date)
        signal_return_list.append(수익률)
        포지션_보유유무 = False

    # 매수 종목 보유 중에 상대 모멘텀 종목 변경됐을 경우
    elif 당일_절대모멘텀 == True and 직전_상대모멘텀_종목 != 당일_상대모멘텀_종목:
        if 포지션_보유유무 == True:
            매도시_주가 = df[직전_상대모멘텀_종목].loc[now_date]
            매도_종목 = 직전_상대모멘텀_종목
            수익률 = (매도시_주가 / 매수시_주가) * 100 - 100
            print(f"  - {now_date}  매도 Signal 발생!  {매수_종목}  ->  {매도_종목}, 매수주가 {매수시_주가:.0f}  ->  매도주가 {매도시_주가:.0f}  , 수익률 {수익률:.3f} %")
            signal_i_list.append(i)
            signal_date_list.append(now_date)
            signal_return_list.append(수익률)

            매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
            매수_종목 = 당일_상대모멘텀_종목
        elif 포지션_보유유무 == False:
            매수시_주가 = df[당일_상대모멘텀_종목].loc[now_date]
            매수_종목 = 당일_상대모멘텀_종목
            포지션_보유유무 = True

 

 

수익률

# 누적 수익률
듀얼_모멘텀_누적수익률 = np.array(signal_return_list) / 100 + 1
듀얼_모멘텀_누적수익률 = 듀얼_모멘텀_누적수익률.cumprod()

# 듀얼모멘텀 누적수익률 데이터프레임 만들기
듀얼모멘텀_기간_수익률 = pd.DataFrame(듀얼_모멘텀_누적수익률, index = signal_date_list, columns =['듀얼모멘텀'])
듀얼모멘텀_기간_수익률 - 1


# 수익률 비교
수익률_합 = sum(signal_return_list)
평균_수익률 = 수익률_합 / len(signal_return_list)

단순_기간_수익률 = ((df['KODEX 미국S&P500선물(H)'][-1] / df['KODEX 미국S&P500선물(H)'][0] +\
             df['TIGER 유로스탁스50(합성 H)'][-1] / df['TIGER 유로스탁스50(합성 H)'][0] +\
             df['KODEX 일본TOPIX100'][-1] / df['KODEX 일본TOPIX100'][0] + \
             df['KODEX 200'][-1] / df['KODEX 200'][0]) / 4) * 100 - 100

print(f"전체 매매 수익률 합  {수익률_합:.2f}%,  매매 당 평균 수익률  {평균_수익률:.2f}%,  "
      f"단순 기간 수익률 {단순_기간_수익률:.2f}%, 듀얼모멘텀 기간 수익률 {((듀얼모멘텀_기간_수익률.iloc[-1].values[0]-1)*100):.2f}%")
# 전체 매매 수익률 합  -9.19%,  매매 당 평균 수익률  -0.06%,  단순 기간 수익률 62.01%, 듀얼모멘텀 기간 수익률 -17.60%
  • df.cumprod(axis=None, skipna=True, args, kwargs)
    • axis : 누적합/누적곱을 구할 축을 지정합니다.
    • skipna : 결측치를 무시할지 여부 입니다
  • 누적 수익률은 복리를 생각해야하므로 누적곱셈을 한다.

 

 

그래프

# 첫 날을 기준으로 상대 수익률 그려보기
df_norm  = pd.DataFrame(columns = ['KODEX 미국S&P500선물(H)', 'TIGER 유로스탁스50(합성 H)', 'KODEX 일본TOPIX100', 'KODEX200'])
df_norm['KODEX 미국S&P500선물(H)'] = df['KODEX 미국S&P500선물(H)'] / df['KODEX 미국S&P500선물(H)'].iloc[0] - 1
df_norm['TIGER 유로스탁스50(합성 H)'] = df['TIGER 유로스탁스50(합성 H)'] / df['TIGER 유로스탁스50(합성 H)'].iloc[0] - 1
df_norm['KODEX 일본TOPIX100'] = df['KODEX 일본TOPIX100'] / df['KODEX 일본TOPIX100'].iloc[0] - 1
df_norm['KODEX200'] = df['KODEX 200'] / df['KODEX 200'].iloc[0] - 1
df_norm.plot(figsize=(20,6));
plt.title('상대 수익률');


# 듀얼모멘텀 상대수익률
df_dual = 듀얼모멘텀_기간_수익률 - 1
df_dual.plot(figsize=(20,6));
plt.title('상대 수익률');

 

결론

개별 주식을 단순 보유하는 것 만큼 안정적이면서 상승하는 수익률을 보여준다.

파라미터(모멘텀측정주기, 절대모멘텀과 상대모멘텀 정의, 자산군 설정)를 바꿔가며 효율적인 듀얼모멘텀 전략을 찾아가야 한다.

과거데이터가 좋게 나온다고해서 미래에도 그렇다는 보장이 없기때문에 왜 이런 현상이 발생했는지 생각해보는 것이 중요하다.