ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬 / 모멘텀(momentum) 전략
    Python/파이썬과 주식 2020. 11. 2. 08:28
    반응형

    * 이 글은 저의 개인적인 정리물일 뿐입니다. 
    * 투자 권유, 투자 참고의 목적이 아닙니다. 

    모멘텀(momentum) 이란?

    모멘텀 = 현재 가격(종가) - n 기간 전 가격(종가)

    오늘 코스피 지수가 2000이고
    한 달 전 코스피 지수가 1900이라면
    모멘텀은 2000-1900=100입니다. 

     

    모멘텀 전략이란?

    모멘텀이 큰 종목에 투자하는 추세 추종형 전략입니다. 
    '오르는 주식이 오르더라'라고 할 수 있겠죠?

     

    제시 리버모어(Jesse Lauriston Livermore)

    1877년 7월 26일 - 1940년 11월 28일

    추세 매매의 창시자입니다. 
    가격이 오르면 사고, 내리면 공매도를 하는 방법으로 엄청난 돈을 벌었습니다. 
    몇 차례 파산을 했었고, 권총 자살로 생을 마무리했지만...

    어떤 인물인지 한번 검색해보시면 좋은 말씀도 많고...

    • 쉬는 것도 투자다. 
    • 월스트리트에 새로운 것은 없다.
    • 오르는 주식을 사고, 떨어지는 주식을 팔아라.
    • 기업의 내부 정보라며 떠도는 얘기들은 믿지 말라. 
    • 수익을 내는 거래를 이어가고, 손실을 내는 거래는 끝내라.
    • 시장은 절대로 틀리지 않는다. 우리의 생각은 종종 틀린다.

     

    오늘 공매도까지 동원한 극단적인 추세매매를 정리하려는 것은 아니고...
    추세매매와 관련한 퀀트적인 내용을 한번 정리해 보겠습니다.  

     

    제가디쉬(Jegadeesh)와 티트만(Titman)

    1993년 제가디쉬와 티트만의 공저로 'Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency'라는 논문이 발표됩니다. 번역하면 '위너(모멘텀이 높은 주식)를 사고, 루저(모멘텀이 낮은 주식)를 팔았을 때의 수익: 주식 시장의 효율성에 대한 영향' 정도 되려나요? 

    논문의 내용은 1965년부터 1989년까지의 미국 주식 중에서 3~12개월간 모멘텀이 가장 높은 종목들의 평균 수익률이 가장 낮은 종목들의 평균 수익률을 1년 이상 초과했다는 내용입니다. 

    쉽게 말씀드리면 '한번 바람(상승)을 제대로 탄 종목(상위 종목)들은 1년 이상 가더라.'

    제가디쉬 전까지는 '효율적인 시장 가설(EMH, Efficient Market Hypothesis)'에 의해서 
    '장기적으로 시장 수익률을 넘을 수 없다'는 것이 대세였습니다. 

    이런 상황에서 제가디쉬의 논문은 상당히 충격적인 논문이었고, 
    이 논문을 시작으로 수많은 논문들이 쏟아졌고, 
    주식 외의 시장에서도 모멘텀의 효과가 확인이 되었습니다. 

     

    효율적 시장 가설

    상품에 대해 "얻을 수 있는 모든 정보"(all available information)가 아주 빠르게 가격에 반영된다면, 
    (그 정보들을 이용해서는 거래를 한다고 해도) 장기적으론 시장 수익률 이상을 얻을 수 없다는 가설입니다.
    노벨경제학상을 수상한 유진 파마가 처음 주창한 이론으로 알려져 있습니다.

    위의 설명이 조금 까다롭더라도 아래의 예를 보면 쉽게 이해가 갑니다. 

    '장기적으로 투자 전문가들 대다수는 인덱스 펀드보다 못한 수익률을 기록하더라'라는 기사가 있죠. 

    버핏, 헤지펀드와 10년 '투자 내기'서 압승…
    버핏이 선택한 인덱스펀드는 2016년 말까지 연평균 7.1%에 달하는 높은 수익을 낸 데 반해
    프로테제가 선택한 헤지펀드 묶음의 수익률은 2.2%에 그쳤다. 
    www.chosun.com/site/data/html_dir/2018/01/01/2018010101041.html  

     

    1988년 WSJ가 전문 애널리스트 4명과 눈을 가린 원숭이가 다트를 던져 구성한 포트폴리오를 대결시킨 적이 있었다.
    14년간의 실험 결과는 애널리스트의 참패였다.
    애널리스트들은 1.2%의 수익률을 올린 반면 원숭이는 2.3%의 수익률을 달성했다.

     

    참고로 전에 글 올린 '인덱스 펀드 : 채권 = 1 : 1' 전략은 효율적 시장 가설과 잘 맞는 전략임을 알 수 있습니다. 
    장기적으로 지수 이상의 수익률을 불가능하다. 마음이라도 편하게 투자하자. 정도의 전략인 것이죠. 

    효율적인 시장 가설을 부정하는 증거로는 
    오늘 다룰 모멘텀 효과, 저 PBR 효과, 저변동성 효과 등이 있습니다. 

    반대 방향의 이야기긴 하지만, 둘 다 근거가 있으니 알아두는 게 좋을 것 같습니다.

     

    백 테스트

    오늘 제가 만들 전략은 '인덱스 펀드'와 '국고채 10년'의 모멘텀을 비교해서 우세인 쪽에 투자합니다. 
    기존 코드를 조금만 수정해도 되니까... 의 이유로...

    일반적으로 모멘텀을 측정하는 기간은 6~12개월이 가장 적당한 것으로 알려져 있습니다. 

    KOSEF 국고채 10년의 역사가 2012년부터라 좀 더 긴 데이터를 백 테스트 못함이 아쉽습니다. 

    슬리피지 + 수수료는 0.2%를 넣었는데 퀀트를 해본 적이 없어 어느 정도가 적당한 지 모르겠네요. 

    1. 

    2012년 6월 1일부터
    천만원 투자, 
    4개월에 한 번씩,
    8개월 전 주가와 비교
    '모멘텀이 큰 쪽 : 작은 쪽  = 6 : 4'의 비율로 
    리밸런싱 했습니다.

    etf1: TIGER 200, etf2: KOSEF 국고채10년, etf3: 코스피 지수

    2020-10-05 1.5295924

    검은 선을 봅시다. 
    오 시장 수익률을 상회하는 구간이 보입니다. 
    이것이 모멘텀의 힘인가요? 

     

    2.

    모멘텀의 힘을 더 느껴보고 싶습니다. 
    '큰 쪽 : 작은 쪽  = 10 : 0'의 극단적인 비율로 갑니다.
    (수수료 어쩔?)
    다른 조건은 같습니다. 

    2020-10-05  1.7216555

    오 수익률이 하늘을 찌르는군요.. 
    아 이제 부자가 되는 방법을 찾은 것 같습니다. 

     

    3.

    이게 다른 상황에서도 유지될까요?
    시작 날짜를 좀 바꿔 봅니다. 
    2012-09-01

    2020-09-01 13921891
    ㅎㅎㅎ 여윽시... 부자가 되는 게 쉬운 게 아니군요. 
    다행히 지수와 큰 차이가 나진 않습니다. 

     

    4.

    2012년 6월 1일부터 3개월 단위로 리밸런싱 했습니다. 
    모멘텀은 6개월..

    2020-09-01 14746402
    시장 수익률과 비슷하네요

     

    5.

    2012년 6월 1일부터 5개월 단위로 리밸런싱했습니다. 
    모멘텀은 10개월..

    2020-10-05 1.7674259
    날아가긴 합니다만...
    시작일을 다르게 주면
    저렇게 날지는 못할 거라는 걸
    이젠 알고 있습니다. 

     

    6.

    2012-08-01일 시작, 리밸런싱 5개월, 모멘텀 10개월

    생각보다 결과가 좋지 못하네요. 

     

    결론

    주의 : 주식과 채권 모두 빠지는 시기를 주의해야 합니다.
    이런 경우를 대비해서 현금을 포트폴리오에 포함하여도 합니다. 

    대상을 아주 협소하게 kodex200과 국고채 10년으로 골라서
    시행한 모멘텀 전략이라 한계가 분명히 있었을 겁니다. 

    (원래 모멘텀은 넓은 종목 군에서 여러 개를 골라내죠... )

    그래도 성과가 나쁘진 않았는데요...

    위 백테스트 기간 안에서는 
    운이 좋을 때는 모멘텀 전략이 시장 수익률 이상의 수익을 얻을 수 있었습니다. 
    좋지 않을 때도 시장 수익률보다 내려가는 상황은 드물었습니다. 
    국고채 10년과 코스피 지수와의 상호보완적인 관계 때문인 것 같습니다. 

    운이 좋으면 본전 이상, 운이 나빠도 본전이면 괜찮은 거 아닌가....

    따라서 1:1 동일 비중 전략보다는 좀 더 효과적인 전략임은 맞는 것 같습니다만.. 

    위의 기간이 짧아 충분한 백 테스트를 포함하지 않았을 수도 있습니다. 
    다양한 상황에 대한 시뮬레이션이 필요합니다. 이 점은 조심해야 할 것 같습니다. 

     

    파이썬 코드

    # 모멘텀
    # 2020-10-25~26
    
    from datetime import datetime, timedelta  # 사용법: https://dojang.io/mod/page/view.php?id=2463
    
    import FinanceDataReader as fdr
    import matplotlib.pyplot as plt
    import pandas as pd
    from dateutil.relativedelta import relativedelta  # month는 timedelta 사용불가 relativedelta
    
    
    def load_data(code, start_date):
        data = fdr.DataReader(code, start_date)  # , '2016-01-01'
        return data['Close']  # 종가만 남김.
    
    
    def buy_etf(money, etf_price, last_etf_num, fee_rate, etf_rate):
        etf_num = money * etf_rate // etf_price
        etf_money = etf_num * etf_price
        etf_fee = (last_etf_num - etf_num) * etf_price * fee_rate if last_etf_num > etf_num else 0
        while etf_num > 0 and money < (etf_money + etf_fee):
            etf_num -= 1
            etf_money = etf_num * etf_price
            etf_fee = (last_etf_num - etf_num) * etf_price * fee_rate if last_etf_num > etf_num else 0
        money -= etf_money + etf_fee
        return money, etf_num, etf_money
    
    
    def back_test(money: int, fee_rate: float, interval: int, ratio: float, code1: str, code2: str, code3: str,
                  start_date: str):
        start_date = datetime.strptime(start_date, '%Y-%m-%d')  # 조회시작일
    
        # 데이터를 받습니다.
        etf1 = load_data(code1, start_date)
        etf2 = load_data(code2, start_date)
        etf3 = load_data(code3, start_date)
    
        # 3종류의 종가 데이터를 하나의 데이터프레임으로 합칩니다.
        df = pd.concat([etf1, etf2, etf3], axis=1, keys=['etf1', 'etf2', 'etf3'])
    
        # 리밸런싱 날짜의 데이터만 new_df에 남깁니다.
        new_df = pd.DataFrame()
        while start_date <= df.index[-1]:
            temp_date = start_date
            while temp_date not in df.index and temp_date < df.index[-1]:
                temp_date += timedelta(days=1)  # 영업일이 아닐 경우 1일씩 증가.
            new_df = new_df.append(df.loc[temp_date])
            start_date += relativedelta(months=interval)  # interval 개월씩 증가.
    
        new_df["etf1_shift"] = new_df["etf1"].shift(2)
        new_df["etf2_shift"] = new_df["etf2"].shift(2)
    
        #     print(new_df)
    
        etf1_num = etf2_num = etf3_num = 0  # 구매한 ETF 개수
    
        backtest_df = pd.DataFrame()  # 백테스트를 위한 데이터프레임
    
        for each in new_df.index:
            etf1_price = new_df['etf1'][each]
            etf2_price = new_df['etf2'][each]
    
            # 모멘텀 계산
            etf1_shift = new_df["etf1_shift"][each]
            etf2_shift = new_df["etf2_shift"][each]
    
            moment1 = (etf1_price - etf1_shift) / etf1_shift
            moment2 = (etf2_price - etf2_shift) / etf2_shift
    
            if moment1 > moment2:
                rate = ratio
            elif moment1 < moment2:
                rate = 1 - ratio
            else:
                rate = 0.5
    
            # 보유 ETF 매도
            money += etf1_num * etf1_price
            money += etf2_num * etf2_price
    
            # ETF 매입
            money, etf1_num, etf1_money = buy_etf(money, etf1_price, etf1_num, fee_rate, rate)
            money, etf2_num, etf2_money = buy_etf(money, etf2_price, etf2_num, fee_rate, 1)
    
            total = money + etf1_money + etf2_money
            backtest_df[each] = [int(total)]
            # print(f'{each}: {total//1}, etf1:{etf1_num}, etf2:{etf2_num}')
    
        # 행열을 바꿈
        backtest_df = backtest_df.transpose()
        backtest_df.columns = ['backtest', ]
    
        # 백테스트 결과 출력
        # print(backtest_df)
        print(backtest_df.tail())
    
        # 최종 데이터 프레임, 3개의 지표와 백테스트 결과
        final_df = pd.concat([new_df, backtest_df], axis=1)
    
        # 시작점을 1로 통일함.
        final_df['etf1'] = final_df['etf1'] / final_df['etf1'][0]
        final_df['etf2'] = final_df['etf2'] / final_df['etf2'][0]
        final_df['etf3'] = final_df['etf3'] / final_df['etf3'][0]
        final_df['backtest'] = final_df['backtest'] / final_df['backtest'][0]
    
        # 그래프 출력
        plt.plot(final_df['etf1'].index, final_df['etf1'], label='etf1', color='r')
        plt.plot(final_df['etf2'].index, final_df['etf2'], label='etf2', color='g')
        plt.plot(final_df['etf3'].index, final_df['etf3'], label='etf3', color='b')
        plt.plot(final_df['backtest'].index, final_df['backtest'], label='back_test', color='black')
        plt.legend(loc='upper left')
        plt.show()

    조건문을 조금 고치면 모멘텀의 강도에 따라 매수 비율을 조절해서 슬리피지를 줄이는 전략도 가능합니다. 

    반응형
Designed by Tistory.