ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 효율적 투자선(Efficient Frontier) 포트폴리오 최적화(feat.파이썬)(1)
    빅데이터 2021. 10. 10. 02:07

    투자할 종목군을 선정한 다음 고민할 것은 종목당 최적 투자비율일 것이다. 특히 나처럼 분산투자를 지향하는 투자자라면 더욱 고민되는 부분이다. 먼 옛날 공부했던 '재무관리'의 이론과 비교적 최근에 공부한 '파이썬'을 활용하여 이런 고민을 조금이나마 덜 수 있는 툴을 만들어보고자 한다.

    ​효율적투자선(Efficient Frontier)
    여러 투자 포트폴리오 중에서 동일한 위험을 가진 것들 중 가장 기대 수익률이 높은 포트폴리오를 나타내거나, 동일한 기대 수익률을 가진 것들 중에서 가장 위험이 낮은 포트폴리오를 나타내는 그래프.
    출처 : 우리말샘

    간단하게 말해서 위험 최소화, 수익 최대화 포트폴리오 집합을 나타낸 선이다.

    재무관리 이론은 이쯤하고 파이썬으로 구현해보자.


    데이터 전처리

    라이브러리를 임포트한다.

    # 라이브러리 임포트
    import pandas as pd
    import numpy as np
    import os
    from pykrx import stock
    from datetime import datetime
    from platform import python_version
    print(python_version())
    print(pd.__version__)

     

    인라인 데이터시각화 라이브러리도 임포트한다.

    # 데이터 시각화 라이브러리 임포트
    %matplotlib inline
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    import seaborn as sns
    sns.set_theme(style="white")
    mpl.rc('font', family='Malgun Gothic')
    mpl.__version__

     

    코드네임 불러오기 경로를 지정한다.

    file_path = './data/codename.xlsx'
    pre = pd.read_excel(file_path, header=3)

     

    코넥스 종목을 삭제한다.

    pre = pre.dropna()
    idx_del = pre[pre['시장'] == 'KONEX'].index
    pre = pre.drop(idx_del)
    pre.reset_index(drop=True, inplace=False)

     

    데이터프레임에 2265개 종목이 잘 들어온 것을 확인할 수 있다. 그런데 종목코드가 엉망이라 약간의 전처리가 필요하다. 간단하게 함수를 만들어서 종목코드를 6자리로 변환한다.

    def six_digit(x):
        return '%06d' % x
    
    pre['종목코드'] = pre['종목코드'].apply(six_digit)

     

    종목명을 입력하면 종목코드를 반환하는 함수도 설정한다.

    def code(x):
        y = pre[pre['종목명'].isin([x])].iloc[0,0]
        return y

    A. 시가총액 상위 10개 종목 과거 5년 데이터 활용

    기간을 설정한다.

    start = '20161010'
    end = '20211008'
    year = 5

     

    pykrx라이브러리 활용을 위한 종목코드를 반환한다. 삼성바이오로직스는 상장 5년이 넘지않아 제외했다.

    name_1 = ['삼성전자', 'SK하이닉스', 'NAVER', 'LG화학', '카카오',
             '삼성SDI', '현대차', '기아', '셀트리온', 'POSCO']
    code_1 =[]
    
    for x in name_1:
        code_1.append(code(x))
        
    code_1

     

    종목별 가중치를 설정한다.

    weight_1 = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])

     

    전체기간 수익률을 계산하는 함수를 설정한다.

    def ret(list):
        l = len(list)
        y = []
        for x in range(0, l):
            re = stock.get_market_ohlcv_by_date(start, end, list[x]).iloc[-1,-2] / \
            stock.get_market_ohlcv_by_date(start, end, list[x]).iloc[0,0]
            y.append(re)
        return y

     

    종가 기준 데이터프레임을 반환하는 함수를 설정한다.

    def chart(name, code):
        l = len(code)
        y = pd.DataFrame(columns=name)
        for x in range(0, l):
            y[name[x]] = stock.get_market_ohlcv_by_date(start, end, code[x]).iloc[:,-2]
        return y

     

    종목별 차트를 플로팅한다.

    df = chart(name_1, code_1)
    
    title = 'Portfolio Close Price History'
    my_stocks = df
    plt.figure(figsize=(15, 10))
    
    for c in my_stocks.columns.values:
        plt.plot(my_stocks[c], label=c)
        
    plt.title(title)
    plt.xlabel('Date', fontsize=18)
    plt.ylabel('Close Price KRW', fontsize=18)
    plt.legend(my_stocks.columns.values, loc='upper left')
    plt.show()

     

    공분산 행렬을 계산한다. 연간기준 환산을 위하여 최근 6년 평균 연간 거래일로 247일을 활용한다.

     

    상관계수 히트맵을 플로팅한다. POSCO가 다른 종목들과 상관계수가 낮고, 타 종목들간에는 상관계수가 비교적 매우 높은 것을 확인할 수 있다.

    corrmat = df_new.corr()
    plt.figure(figsize=(10,10))
    cm = np.corrcoef(df.values.T)
    hm = sns.heatmap(cm, cmap='Blues', cbar=True, annot=True, square=True, fmt='.2f',
                     linewidths=0.01, xticklabels=df.columns, yticklabels=df.columns)
    plt.show()

    동일비중 포트폴리오의 퍼포먼스를 확인한다.

    port_var = np.dot(weight_1.T, np.dot(cov, weight_1))
    port_std = np.sqrt(port_var)
    port_SAR = (np.mean(ret(code_1))**(1/year))-1
    # port_SAR = np.sum(df_new.mean()*weight_1) * 247
    
    per_var = str(round(port_var, 4) * 100) +'%'
    per_vols = str(round(port_std, 4) * 100) +'%'
    per_ret = str(round(port_SAR, 4) * 100) +'%'
    sr = str(round(port_SAR/port_std, 2))
    
    print('Expected annual return:'+ per_ret)
    print('Annual volatility:' + per_vols)
    print('Sharpe Ratio:' + sr)

    Expected annual return:25.77%

    Annual volatility:20.97%

    Sharpe Ratio:1.23

     

    효율적투자선 계산을 위하여 pypfopt 라이브러리를 임포트한다. (세상은 넓고 천재는 많다. 멋진 패키지를 만들어준 Robert Andrew Martin에게 무한 감사를!)

    from pypfopt.efficient_frontier import EfficientFrontier
    from pypfopt import risk_models
    from pypfopt import expected_returns

     

    효율적투자선 적용 포트폴리오의 퍼포먼스 및 종목별 투자비율을 확인한다.

    mu = expected_returns.mean_historical_return(df)
    S = risk_models.sample_cov(df)
    
    ef = EfficientFrontier(mu, S)
    weights = ef.max_sharpe()
    cleaned_weights = ef.clean_weights()
    print(cleaned_weights)
    ef.portfolio_performance(verbose=True)

     

    <동일비중>

    Expected annual return:25.77%

    Annual volatility:20.97%

    Sharpe Ratio:1.23

    <효율적투자선>

    Expected annual return: 49.9%

    Annual volatility: 30.9%

    Sharpe Ratio: 1.55

    효율적투자선 적용결과 위험 한단위 당 수익률을 나타내는 지표인 샤프지수가 개선되었음을 알 수 있다.

    OrderedDict([('삼성전자', 0.0), ('SK하이닉스', 0.0), ('NAVER', 0.0), ('LG화학', 0.0), ('카카오', 0.55287), ('삼성SDI', 0.44382), ('현대차', 0.0), ('기아', 0.00331), ('셀트리온', 0.0), ('POSCO', 0.0)])

    효율적투자선은 카카오 55%, 삼성SDI 44% 기아 0.3%의 투자비율을 나타냈다. 아마도 이 3종목이 변동성은 낮고, 수익률을 좋았을 것으로 보인다. 그래도 심리적으로 10개 종목으로 구성된 포트폴리오에서 3개 종목만 집중투자하는 것에 거부감이 앞선다. 따라서 기간을 조정해서 다시 검증해보고자 한다.


    B. 시가총액 상위 10개 종목 과거 1년 데이터 활용

    코드동일 생략

    <동일비중>

    Expected annual return:29.41%

    Annual volatility:22.24%

    Sharpe Ratio:1.32

    <효율적투자선>

    Expected annual return: 61.0%

    Annual volatility: 28.1%

    Sharpe Ratio: 2.10

    OrderedDict([('삼성전자', 0.0), ('SK하이닉스', 0.0), ('NAVER', 0.0), ('LG화학', 0.0), ('삼성바이오로직스', 0.0), ('카카오', 0.35923), ('삼성SDI', 0.34007), ('현대차', 0.0), ('기아', 0.3007), ('셀트리온', 0.0)])

    지난 1년도 마찬가지다. 10년으로 기간을 늘려보자.


    C. 시가총액 상위 10개 종목 과거 10년 데이터 활용

    <동일비중>

    Expected annual return:13.3%

    Annual volatility:19.2%

    Sharpe Ratio:0.69

    <효율적투자선>

    Expected annual return: 17.7%

    Annual volatility: 21.3%

    Sharpe Ratio: 0.74

    OrderedDict([('삼성전자', 0.31324), ('SK하이닉스', 0.07058), ('NAVER', 0.23236), ('LG화학', 0.0), ('카카오', 0.13379), ('삼성SDI', 0.16915), ('현대차', 0.0), ('기아', 0.0), ('셀트리온', 0.08088), ('POSCO', 0.0)])

    기간을 10년으로 늘려보니 어느정도 분산된 효율적 투자선 포트폴리오를 얻을 수 있었다. 종목별 차트와 수익률을 보면 새삼 2020년의 폭발적은 주가 상승이 얼마나 대단한 일이었는지도 알 수 있다.

    이왕 하는 김에 20년 데이터도 확인해보자.


    D. 당시 시가총액 상위 10개 종목 20년 데이터 활용

    한국거래소 정보데이터시스템에 접속하면 시점별 시가총액 상하위 종목을 검색할 수 있다.

    http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC03020103 

     

    KRX 정보데이터시스템

    증권·파생상품의 시장정보(Marketdata), 공매도정보, 투자분석정보(SMILE) 등 한국거래소의 정보데이터를 통합하여 제공 서비스

    data.krx.co.kr

    name_4 = ['삼성전자', 'SK텔레콤', 'KT', '한국전력', 'POSCO',
             '현대차', 'KT&G', '신한지주', '기아', 'S-Oil']
    
    #과거시점 시가총액 순위 확인 페이지
    #http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0302
    
    code_4 =[]
    
    for x in name_4:
        code_4.append(code(x))
        
    code_4

     

    시가총액 TOP10 종목이 참 낯설다. 데이터가 없는 종목은 차순위 종목으로 대체하였다.

    당시 종목명 그대로 시가총액 TOP10
    삼성전자, SK텔레콤, 한국통신공사, 한국전력, 포항제철,
    KTF, 국민은행, 현대차, 담배인삼공사, 주택은행
    출처 : 한국거래소 정보데이터시스템

    <동일비중>

    Expected annual return:9.88%

    Annual volatility:19.90%

    Sharpe Ratio:0.50

    <효율적투자선>

    Expected annual return: 14.8%

    Annual volatility: 24.9%

    Sharpe Ratio: 0.51

    OrderedDict([('삼성전자', 0.66334), ('SK텔레콤', 0.0), ('KT', 0.0), ('한국전력', 0.0), ('POSCO', 0.0), ('현대차', 0.0495), ('KT&G', 0.22479), ('신한지주', 0.0), ('기아', 0.06237), ('S-Oil', 0.0)])

    삼성전자 66%, KT&G 22% 기아 6%, 현대차 5% 로 구성된 삼성전자 몰빵형 포트폴리오가 나왔다. 20년이란 기간은 별볼일 없는 기업에게는 꾸준한 성장은 커녕 생존조차 담보할 수 없는 기간이다. 따라서 20년 동안 꾸준한 성장을 해온 종목으로 효율적투자선 포트폴리오가 구성된 것은 자연스러워 보인다.


    Memo

    1. 효율적투자선을 포함한 CAPM, 자본시장선, 증권특성선, 증권시장선 이론의 한계는 명확하다. 종목별 과거의 위험-수익 간의 관계가 미래에도 지속된다는 가정을 한다는 점이다. 효율적투자선을 맹신하지 말고 참고자료로 활용하자.

    2. 코스닥 종목도 적용해보자.

    3. 동일비중 분산투자하고 있는 포트폴리오 투자비율을 조정하자. 샤프지수 개선이 가능할 것으로 보인다.

    4. 파이썬은 정말 파워풀한 툴이다. 엑셀로 이 작업을 했다고 상상해보면 끔찍하다.

     

     

    네이버블로그

    https://blog.naver.com/gurwjdz

     

    Marathon Investment : 네이버 블로그

    흔들리지 않는 것이 강한 것, 투자는 마라톤

    blog.naver.com

     

    텔레그램

    https://t.me/marathoninvest

     

    마라톤투자

    페이스를 유지하기 위한 최소한의 공부

    t.me

     

    티스토리

    https://inv42195.tistory.com

     

    마라톤투자

    흔들리지 않는 것이 강한 것, 투자는 마라톤

    inv42195.tistory.com

     

Designed by Tistory.