ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [파이썬] 포트폴리오 이론, 리밸런싱, 백테스트
    Python/파이썬과 주식 2020. 10. 31. 23:42
    반응형

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

    finance-datareader라는 재미있는 툴을 얼마 전에 소개해 드렸습니다. 
    재미있는 게 있으니 뭔가를 코딩하고 싶어 손이 근질근질합니다. 

    코딩하기 쉬울 것 같아서,
    가장 초보적인 전략인 포트폴리오 이론(분산투자) + 리밸런싱을 백테스트하는 걸로 결정했습니다.
    귀찮긴 하지만 백테스트 라이브러리를 쓰지 않고 직접 백테스트 코드를 작성해 봤습니다. 

    초보적인 금융지식으로 코딩했기 때문에 백테스트에 문제가 있을 수도 있습니다.

     

    포트폴리오 이론, 리밸런싱

    분산투자(포트폴리오 이론)는 다들 잘 아시겠지만
    성격이 다른(=상호보완적인, 반대 방향에 있는) 자산에 분산해서 투자하는 겁니다. 
    '계란을 한 바구니에 담지 마라'는 말이 있죠? 
    보통 주식과 채권이 되죠. 

    '주식 : 채권 : 현금'을 골고루 '1 : 1 : 1'로 담아 봅시다. 

    정기적으로
    자산을 사고 팔아
    정해진 비율에 맞추는 것을 
    리밸런싱이라고 합니다. 

    자주 매매하면 수수료가 커지니,
    '4개월'마다 한 번씩 자산을 매매해서
    위 비율에 맞추겠습니다.  

    이익이 생기면 회수하게 되고, 손해를 보면 물타기가 됩니다. --;
    물타기 + 반등을 노리는 것과 뭐가 다르냐 라고 할 수도 있습니다만, 
    물타기와 달리 멘탈을 튼튼하게 유지할 수 있다는 것이 가장 큰 장점이죠. --;

    미리 '정해진 작전'에 따라 '일정한 비율(금액)'으로 매수, 매도합니다.
    정해진 작전에 따라 움직이니 고민할 것이 없습니다. 
    비록 과거의 데이터로 실험한 것이고, 과거로 미래를 예측할 수는 없지만, 
    큰 이변이 없다면 대략(?)의 결과까지 예상할 수 있습니다.
    두 가지 이유로 멘탈이 유지됩니다. 
    돈의 관리보다 자기 자신의 관리가 더 중요합니다.

    이 작전에 대해 좀 더 자세히 알고 싶으시다면
    systrader79님의 블로그를 참고하시기 바랍니다. 

     

    전설적인 가치투자자인 벤저민 그레이엄은 '주식 : 채권'을 '1 : 1'로 투자하는 것이 최고의 비율이라고 했습니다. 이를 포뮬러 플랜(Formula plan)이라고도 합니다.

     

    종목선택

    주식, 채권, 현금 ETF 3종을 먼저 골라봅니다. 

    '주식'은 TIGER 200 (102110)을 골랐습니다.
    2008-04-03일 상장에 총보수는 0.050%입니다. 

    총보수가 싸고 거래량이 많아야 좋은 ETF입니다. 
    거래량이 너무 적으면 시세보다 비싸게 사지고, 또 싸야 팔립니다. 

    '채권'은 KOSEF 국고채10년(148070)을 골랐습니다.
    2011-10-20일 상장, 총보수는 0.150%입니다.

    KINDEX 국고채 10년은 수수료가 0.100% 쌉니다. 
    하지만 2020-10-15일 상장이라 데이터가 없습니다.
    자료로는 사용할 수 없습니다. 

    '현금'은 TIGER 단기통안채(157450)를 골랐습니다.
    상장일 2012-05-16 / 수수료 0.090

    실제로는 0.1%라도 더 주는 곳에 입금하는 게 맞겠지만
    우리는 데이터가 필요하기 때문에
    ETF 중에 현금에 가까운 종목을 고른 겁니다. 

     

    수수료, 세금 설정

    커미션을 0.2% 잡았습니다. 

    사고팔 때 수수료는 이렇게 높지 않지만 슬리피지를 고려했습니다.
    슬리피지란 사고자 하는 가격과 실제 구매한 가격의 차이를 의미합니다.

    이 백테스트는 과거 데이터의 종가를 기준으로 계산했습니다.
    만약에 이 종가보다 비싸게 샀다면, (싸게 샀을 수도 있지만)
    그만큼 수익률이 덜 나오겠죠.

    또 거래량이 적은 종목의 경우 슬리피지가 커질 수 있습니다. 

    실제 퀀트를 해본 적이 없어 적당한 슬리피지인지 모르겠습니다.

    국내 ETF는 세금을 내지 않기(?) 때문에 세금은 제외했습니다.

    혹시 틀렸다면 리플 남겨 주십시오.

     

    주의

    TIGER 단기통안채(157450)의 종가 중에 튀는 데이터가 하나 있습니다.
    제가 뽑은 리밸런싱 날짜와 겹치지 않아서 오류 여부는 확인하지 않았습니다.
    통계적으로 전혀 도움이 안 되는 데이터기 때문에 주의하시기 바랍니다.

    데이터는 항상 확인해야 합니다. 

     

    코딩

    러프한 코드입니다. ㅠ,.ㅠ
    어짜피 백테스팅 라이브러리를 쓰....

    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)
        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, 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 개월씩 증가.
    
        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]
            etf3_price = new_df['etf3'][each]
    
            # 보유 ETF 매도
            money += etf1_num * etf1_price
            money += etf2_num * etf2_price
            money += etf3_num * etf3_price
    
            # ETF 매입
            money, etf1_num, etf1_money = buy_etf(money, etf1_price, etf1_num, fee_rate, 1 / 3)
            money, etf2_num, etf2_money = buy_etf(money, etf2_price, etf2_num, fee_rate, 1 / 2)
            money, etf3_num, etf3_money = buy_etf(money, etf3_price, etf3_num, fee_rate, 1)
    
            total = money + etf1_money + etf2_money + etf3_money
            backtest_df[each] = [int(total)]
    
        # 행열을 바꿈
        backtest_df = backtest_df.transpose()
        backtest_df.columns = ['backtest', ]
    
        # 백테스트 결과 출력
        print(backtest_df)
    
        # 최종 데이터 프레임, 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='Stock_ETF(102110)', color='r')
        plt.plot(final_df['etf2'].index, final_df['etf2'], label='Bond_ETF(148070)', color='g')
        plt.plot(final_df['etf3'].index, final_df['etf3'], label='Cash_ETF(157450)', color='b')
        plt.plot(final_df['backtest'].index, final_df['backtest'], label='Backtest', color='violet')
        plt.legend(loc='upper left')
        plt.show()
    
    
    back_test(10_000_000, 0.002, 4, '102110', '148070', '157450', '2012-06-01')
    

     

    결과

    backtest_df
    	Backtest
    2012-06-01	10000000
    2012-10-02	10477441
    2013-02-01	10474710
    2013-06-03	10594818
    2013-10-01	10612170
    2014-02-03	10522882
    2014-06-02	10810790
    2014-10-01	10920536
    2015-02-02	11144051
    2015-06-01	11293699
    2015-10-01	11133826
    2016-02-01	11198948
    2016-06-01	11454667
    2016-10-04	11828874
    2017-02-01	11876814
    2017-06-01	12430441
    2017-10-10	12661687
    2018-02-01	12780462
    2018-06-01	12619890
    2018-10-01	12636166
    2019-02-01	12669980
    2019-06-03	12575255
    2019-10-01	12793402
    2020-02-03	13052923
    2020-06-01	12980683
    2020-10-05	13643940

    백테스트해본 결과 10_000_000원이 8년 4개월 만에 13_643_940원으로 불어나 있었고

    (13_643_940 - 10_000_000) / 10_000_000 / (8 + (1 / 3)) * 100  # 단리 환산
    ((13_643_940 / 10_000_000) ** (1 / (8 + (1 / 3))) - 1) * 100  # 복리 환산

    단리 환산 : 연 4.3727279999999995%
    복리 환산 : 연 3.7989059681043935%
    의 수익을 얻을 수 있음을 알 수 있었습니다. 

     

    주식 : 채권 = 1 : 1

    만약 '주식:채권=1:1'로 '3개월에 1번' 리밸런싱 했다면 어떻게 될까요?
    코드를 약간만 고치면 다음의 결과를 얻을 수 있습니다. 

                Backtest
    2012-06-01  10000000
    2012-09-03  10407676
    2012-12-03  10527259
    2013-03-04  10871035
    2013-06-03  10753460
    2013-09-02  10437387
    2013-12-02  10787838
    2014-03-03  10721930
    2014-06-02  10950614
    2014-09-01  11204162
    2014-12-01  11156486
    2015-03-02  11421703
    2015-06-01  11570138
    2015-09-01  10980110
    2015-12-01  11469662
    2016-03-02  11580719
    2016-06-01  11749905
    2016-09-01  12191793
    2016-12-01  11858622
    2017-03-02  12395128
    2017-06-01  13181106
    2017-09-01  13260945
    2017-12-01  13561580
    2018-03-02  13207858
    2018-06-01  13377148
    2018-09-03  13320822
    2018-12-03  12992369
    2019-03-04  13320689
    2019-06-03  13210948
    2019-09-02  13209743
    2019-12-02  13555825
    2020-03-02  13680825
    2020-06-01  13782504
    2020-09-01  14645080

    8년 3개월 후 14,645,080원 이며,  
    단리 환산: 연 5.574095999999999%
    복리 환산: 연 4.684651068431878%
    입니다.

    '큰 마음 고생 없이' 주식 ETF나 채권 ETF와 거의 동일한 수익률을 얻을 수 있음을 알 수 있습니다. 

    워랜 버핏 - "내 유서에 재산의 10%는 미국 국채를 매입하고, 나머지 90%는 전부 S&P500 인덱스펀드에 투자할 것을 명시했다." www.hankyung.com/economy/article/2019041881446

     

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