ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [파이썬] Mean-Variance Frontier 계산하기 (최적 포트폴리오 계산하기)
    코딩/파이썬 2021. 10. 14. 17:27
    반응형

    주식 관련 공부를 하다 보면 최적 포트폴리오 이론(Optimal Portfolio Theory)이라는 것을 한 번쯤 들어보셨을 것입니다. 간단히 말하면 포트폴리오의 수익률과 위험만을 고려하는 합리적인 투자자가 자신의 효용을 극대화하는 포트폴리오를 선택하는 원리에 대한 이론이라고 할 수 있습니다. 이번 포스트에서는 최적 포트폴리오 이론에서 말하는 Mean-Variance Frontier(수익률-분산 곡선)를 파이썬으로 계산하는 방법에 대해서 알아보도록 하겠습니다. 

     

    Mean-Variance Frontier 계산하기 (최적 포트폴리오 계산하기)

     

    시작하기에 앞서 용어 정리를 먼저 하자면 Minimum Variance Frontier(최소 분산 곡선)는 위험자산만 존재하는 경우 목표 수익률을 달성하면서 가장 위험이 작은 포트폴리오의 수익률과 분산을 모아 놓은 곡선입니다. (참고로, Global Minimum Variance Portfolio는 최소 분산 포트폴리오들 중에서 가장 분산이 작은 포트폴리오를 가리킵니다.) 만일 risk free asset(무위험 자산)이 존재하지 않으면 Minimum Variance Frontier가 Efficient frontier(=Mean-Variance Frontier)가 됩니다.

     

    반면에 무위험 자산이 존재하는 경우 위험자산으로만 만들어진 Minimum Variance Frontier는 더 이상 Efficient frontier가 아닙니다. 무위험 자산을 포함하여 다시 Efficient frontier를 만들어 줘야 하는데, Tangency Portfolio와 무위험 자산을 통과하는 직선이 Efficient frontier가 됩니다. Tangency Portfolio는 무위험자산으로 구성된 포트폴리오 중에서 가장 높은 Sharpe ratio를 가진 포트폴리오입니다. (참고로, Sharpe ratio는 위험 대비 위험 프리미엄으로 1 단위 위험당 얼마만큼의 초과 수익률을 주는지를 측정해 줍니다.)

     

     

    1. 사용된 패키지와 데이터 생성 (Used Packages and Generating data)

     

    다음과 같은 패키지를 사용하여 최적 포트폴리오를 계산해 보도록 하겠습니다. 주식 수익률 계산 포스트에서 사용된 패키지 이외에 cvxopt라는 패키지가 추가되었습니다. 이 패키지는 행렬로 되어 있는 목적함수를 최적화시켜주는 데 사용되는 패키지입니다. 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #Packages used (사용된 패키지)
    import yfinance as yf
     
    import numpy as np
    import matplotlib.pyplot as plt
    import cvxopt as opt
    from cvxopt import solvers
    import pandas as pd
     
    # Turn off progress printing(solvers의 진행 상태 표시 없애기)
    solvers.options['show_progress'= False
    cs

     

    아래와 같이 데이터를 생성해 주도록 하겠습니다. 사용될 주식은 Apple, Google, Amazon 3개의 주식입니다. 원칙적으로는 모든 주식을 다 포함해야 하지만 연습해보는 것이기 때문에 간단하게 3개만 사용해 보도록 하겠습니다. 자세한 사항은 수익률 계산 포스트에서 확인하실 수 있습니다.

    2021.10.09 - [코딩/파이썬] - [파이썬] 개별 주식 수익률 계산 (Calculate individual stock returns)

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #Download price data from Yahoo finance (가격 데이터 다운로드)
    p_apple = yf.download('AAPL',start = '2019-01-01')
    p_google = yf.download('GOOGL',start = '2019-01-01')
    p_amazon = yf.download('AMZN',start = '2019-01-01')
     
    #Merge the tow tables above (표 병합)
    p_apple = p_apple[['Adj Close']].rename(columns = {'Adj Close':'Close_Apple'})
    p_google = p_google[['Adj Close']].rename(columns = {'Adj Close':'Close_Google'})
    p_amazon = p_amazon[['Adj Close']].rename(columns = {'Adj Close':'Close_Amazon'})
     
    price = pd.concat([p_apple,p_google,p_amazon],axis=1)
     
    #Calculate the daily return of individual stock (개별 주식 일별 수익률 계산)
    #daily returns = (today price - previous day price)/(previous day price) - 1
    ptc_ret = price.pct_change(1).dropna()
    ptc_ret = ptc_ret.rename(columns={'Close_Apple':'Apple','Close_Google':'Google','Close_Amazon':'Amazon'})
    cs

     

    이후 최적 포트폴리오를 계산함에 있어서 행렬 연산이 사용되기 때문에 데이터를 numpy array로 변환하여 주도록 하겠습니다. 행렬 연산은 주로 numpy 패키지를 통하여 이루어집니다. 변수명은 ret_matrix라고 하겠습니다.

     

    1
    2
    #Convert dataframe to numpy array and transpose it (데이타 프레임에서 numpy array로 변경)
    ret_matrix = ptc_ret.values.T
    cs

     

    결과는 다음과 같습니다.

     

    2. 무작위 포트폴리오 생성 (Generate random portfolios)

    최적 포트폴리오 계산은 그 결과를 그래프를 통하여 살펴보는 것이 보통이기 때문에 그래프에 표시할 점을 먼저 만들어 보도록 하겠습니다. 다음과 같은 함수를 만들어 무작위 가중치를 계산할 수 있습니다.

     

    1
    2
    3
    4
    5
    #Generate random weights (무작위 가중치 생성)
    def rand_weights(n):
        ''' Produces n random weights that sum to 1 '''
        k = np.random.rand(n)
        return k / sum(k)
    cs

     

    함수를 돌려 본 예시를 보도록 하겠습니다. 3개의 가중치를 무작위로 만들어 낸 결과입니다. 합쳐보면 1이 나옵니다.

     

    다음으로 무작위 포트폴리오를 생성할 수 있는 함수를 작성해 보겠습니다. 위에서 만든 무작위 가중치 함수를 이용하여 수익률 데이터를 받아서 포트폴리오 수익률, 표준편차, 비중을 계산해 줍니다. 각 주식의 평균 수익률은 일별 수익률의 평균으로 계산해 주었고 공분산 행렬은 일별 수익률의 공분산을 계산해 주었습니다(연습을 위해서 가장 간단하게 계산할 수 있는 방법을 선택하였습니다, 실제로 평균 수익률과 공분산을 계산해 주는 것은 더 복잡하게 고려해야할 사항이 있을 수 있습니다). 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #Generate a random portfolio (무작위 포트폴리오 생성)
    def random_portfolio(returns):
        ''
        Returns the mean and standard deviation of returns for a random portfolio
        '''
     
        p = np.asmatrix(np.mean(returns, axis=1))
        w = np.asmatrix(rand_weights(returns.shape[0]))
        C = np.asmatrix(np.cov(returns))
        
        mu = w * p.T
        sigma = np.sqrt(w * C * w.T)
        
        # This recursion reduces outliers to keep plots pretty
        if sigma > 2:
            return random_portfolio(returns)
        return mu, sigma, w
    cs

     

    이 함수를 이용하여 실제로 3개의 주식을 사용하여 500개의 무작위 포트폴리오를 생성해 보겠습니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #Generate 500 random portfolio (500개의 무작위 포트폴리오 생성)
    output = []
    for i in range(0,500):
      port_mean, port_std, weights = random_portfolio(ret_matrix)
      mean_std_array = np.array([port_mean, port_std])
      weights_array = np.array(weights)
      merged_array = np.concatenate((mean_std_array,weights_array),axis=None)
      merged_list = merged_array.tolist()
      output.append(merged_list)
     
    col_list = list(ptc_ret.columns)
    col_list = [i + '_W' for i in col_list]
     
    df_col = ['Port_ret','Port_std'+ col_list
    port_ret_var = pd.DataFrame(output,columns = df_col)  
    cs

     

    결과는 다음과 같습니다.

     

    3. 최소분산 포트폴리오 생성 (Generate random portfolios)

    다음으로 위험자산(Apple, Google, Amazon 주식)으로만 만들어진 최소분산 포트폴리오를 생성하는 함수를 다음과 같이 작성해 보겠습니다. 먼저 목표 수익률(mus 변수)을 만들어 준 후 각 목표 수익률에서 최소 분산을 만들어주는 가중치를 계산해 줍니다. 목표 수익률은 기본적으로 자유롭게 만들 수 있지만 저는 0에서 0.003까지 0.0001 간격으로 리스트를 작성해 주었습니다. 최소화에 쓰이는 함수는 solvers.qp()이며, 가장 중요한 것이 solvers.qp()에서 정의하는 목적 함수 및 제약조건에 맞게 행렬 및 백터를 만들어 주어야 합니다. 여기에서는 Q, p, A, b 변수에 해당됩니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #Minimum variance portfolio (최소 분산 포트폴리오)
    '''
    min 1/2w'COVw
    s.t. mu'w = rp and 1'= 1
    '''
    def minimum_port_weight(returns):
      mean_ret = np.mean(returns,axis=1)
      cov_ret = np.cov(returns)
      n = mean_ret.shape[0]
      one_array = np.ones(n)
     
      #List of target portfolio returns (목표 포트폴리오 수익률 리스트)
      mus = list(np.arange(0,0.003,0.0001))
        
      # Convert to cvxopt matrices (cvxopt matrix로 변환)
      Q = opt.matrix(cov_ret)
      p = opt.matrix(0.0, (n, 1))
        
      # Create constraint matrices (최적화 문제 제약조건 설정)
      A = opt.matrix(np.stack((mean_ret,one_array)))
      weights_list = []
      mean_std_list = []
      for mu in mus:
        b = opt.matrix(np.array([mu,1]))
        weights = solvers.qp(Q, p, A = A, b = b)['x']
        weight_array = np.array(list(weights))
        port_ret = np.matrix(weight_array).dot(np.matrix(mean_ret).T)
        port_std = np.sqrt(np.matrix(weight_array).dot(np.matrix(cov_ret)).dot(np.matrix(weight_array).T))
        
        weights_list.append(list(weights))
        mean_std_list.append([port_ret[0,0],port_std[0,0]])
      
      return weights_list,mean_std_list
    cs

     

    다음 코드를 통하여 함수를 실행시켜 보겠습니다. 3번 라인은 dataframe에 쓸 column 이름을 주식 이름 + _Weight으로 바꿔 주기 위해서 작성하였습니다.

     

    1
    2
    3
    4
    5
    weights,mean_stds = minimum_port_weight(ret_matrix)
    #To dataframe (데이타 프레임으로 변경)
    col_name = ptc_ret.columns + '_Weight'
    weights_df = pd.DataFrame(weights,columns=col_name)
    mean_stds_df = pd.DataFrame(mean_stds,columns=['Port_ret','Port_std'])
    cs

     

    결과는 다음과 같습니다. 가중치를 보면 음수가 나오는 것을 볼 수 있는데, 이는 Short selling(공매도)을 가정했기 때문에 가능합니다. 예를 들어 100달러의 재산이 있고 세 주식의 가중치가 -0.5, 0.5, 1이라고 해보겠습니다. 첫 번째 주식과 두 번째 주식의 절댓값이 같기 때문에 첫 번째 주식을 공매도하는 금액이 두 번째 주식의 구매 금액과 같습니다. 그리고 두 번째 주식과 세 번째 주식의 비중 차이가 2배이기 때문에 첫 번째 주식의 구매 금액은 두 번째 주식의 2배가 됩니다. 즉, 첫 번째 주식을 50달러어치 공매도하고, 두 번째 주식을 50달러어치 매수하고, 세 번째 주식을 100달러(남은 50달러 + 공매도하여 받은 50달러) 어치 구입하면 됩니다.

     

    지금까지 계산한 결과를 그래프로 그려서 살펴보도록 하겠습니다. 코드는 다음과 같습니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #Plot minimum variance frontier (최소 분산 곡선 그래프)
    #Random portfolios (무작위 포트폴리오)
    means = port_ret_var['Port_ret'].values
    stds = port_ret_var['Port_std'].values
     
    #Minimum varaince portfolios (최소분산 포트폴리오)
    opt_returns = mean_stds_df['Port_ret']
    opt_stds = mean_stds_df['Port_std']
     
    fig = plt.figure(figsize=(15,8))
    plt.plot(stds, means, 'o')
    plt.ylabel('mean',fontsize=12)
    plt.xlabel('std',fontsize=12)
    plt.plot(opt_stds, opt_returns, 'y-o')
    plt.xlim(0.013,0.025)
    plt.ylim(0.001,0.0024)
    plt.title('Minimum variance frontier for risky assets',fontsize=15)
    cs

     

    결과는 다음과 같습니다.

     

    파란색 점이 500개의 무작위 포트폴리오를 점찍은 것이고 황색 선이 Minimum Variance Frontier를 나타냅니다. 최소 분산 포트폴리오를 계산하는 함수에서 목표 수익률을 더 촘촘하게 만들어 주면 더 곡선 같은 선이 나옵니다.

     

     

    4. 무위험 자산이 존재할 때 Tangency Portfolio와 Efficient Frontier 생성 (Generate Efficient Frontier with a risk free asset)

    먼저 Tangency portfolio를 만들어 주도록 하겠습니다. Tangency portfolio를 계산하는 것은 이미 공식이 존재하기 때문에 그것을 그대로 코드로 만들어 주면 됩니다. 코드는 아래와 같습니다. 무위험 수익률(rf 변수), 평균 수익률 백터(mu 변수), 공분산 행렬(cov 변수)과 그 역행렬(cov_inv 변수)을 이용하여 Tangency portfolio 비중 (tan_weight 변수)를 계산합니다. 그 후에 Tangency portfolio 수익률 (tan_port_ret 변수), 위험 (tan_port_std), Sharpe ratio (tan_sharpe 변수)를 구합니다. 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    '''
    Tangency portfolio = highest Sharpe ratio with only risky assets
    w'1 = 1 needed
     
    max (w'mu - rf) / sqrt(w'COVw) -> Sharpe ratio
    s.t. w'1 = 1
     
    Solution
    w_tp = inv(COV)mu / 1'inv(COV)mu 
    '''
     
    '''
    Risk free rate, mean vector, covariance matrix, inverse matrix
    (무위험 수익률, 평균 수익률 벡터, 공분산 행렬, 역행렬)
    '''
    rf = 0.0001
    mu = np.matrix(np.mean(ret_matrix,axis=1)).T
    cov = np.matrix(np.cov(ret_matrix))
    cov_inv = np.linalg.inv(cov)
     
    top_no = cov_inv.dot(mu)
    one_matrix = np.matrix(np.ones(mu.shape[0]))
    bottom = one_matrix.dot(cov_inv).dot(mu)
     
    tan_weight = top_no/bottom[0,0]
     
    #Calculate Shape ratio (Sharpe ratio 계산)
    tan_port_ret = tan_weight.T.dot(mu)
    tan_port_std = np.sqrt(tan_weight.T.dot(cov).dot(tan_weight))
    tan_sharpe = (tan_port_ret[0,0- rf) / tan_port_std[0,0]
    cs

     

    결과는 다음과 같습니다. 

     

    마지막으로 위에서 계산한 Tangency portfolio를 이용하여 Efficient frontier를 만들어 보도록 하겠습니다. Efficient frontier는 무위험 자산과 Tangency portfolio를 잇는 직선으로 그래프에 예쁘게 나타내기 위해서 Efiicient portfolio 직선 위에 있는 제3의 점을 하나 더 만든 후에 세 점을 잇는 선으로 그려보도록 하겠습니다. 코드는 다음과 같습니다. (위에서 그렸던 그림 위에 더해서 그려지도록 만들었습니다.)

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #Generate the third point in the efficient frontier (EFT위의 제3의 점 생성)
    tird_eft_std = 0.026
    tird_eft_return = tan_sharpe * 0.026 + rf
     
    #Random portfolios for Risky assets (위험 자산 랜덤 포트폴리오) 
    stds = port_ret_var['Port_std'].values
    means = port_ret_var['Port_ret'].values
     
    #Minimum varaince portfolios for risky assets (위험 자산 최소 분산 포트폴리오)
    opt_returns = mean_stds_df['Port_ret']
    opt_stds = mean_stds_df['Port_std']
     
    #Efficient frontier with a risk free asset (무위험 자산 존재 시 efficient frontier) 
    rf_tan_ret = np.array([rf,tan_port_ret[0,0],tird_eft_return])
    rf_tan_std = np.array([0,tan_port_std[0,0],tird_eft_std])
     
    fig = plt.figure(figsize=(15,8))
    plt.plot(stds, means, 'o')
    plt.ylabel('mean',fontsize=12)
    plt.xlabel('std',fontsize=12)
    plt.plot(opt_stds, opt_returns, 'y-o')
    plt.plot(rf_tan_std, rf_tan_ret, 'y-o',color="red")
    plt.xlim(0.013,0.025)
    plt.ylim(0.001,0.0024)
    plt.title('Mean-Variance Frontier with a risk free asset',fontsize=15)
     
    cs

     

    결과는 다음과 같습니다.

    빨간색 직선이 Efficient Frontier이며 빨간색 점이 Tangency portfolio입니다. Tangent가 접하다라는 뜻이라는 것과 빨간색 선이 황색선에 접해 있다는 것이 연관되어 있다고 느껴지지 않으신가요? 맞습니다. Tangency portfolio는 Minimum Variance Frontier와 Efficient Frontier가 접하는 점으로 가장 높은 Sharpe ratio를 갖는 위험 자산으로 구성된 포트폴리오를 의미합니다.

     

     

     

    지금까지의 모든 파이썬 코드는 아래의 Google Colab 링크에서 연습해 보실 수 있습니다.

    Go to Google Colab Link

     

    Mean_Variance_Frontier_Jaseup_TStoryBlog.ipynb

    Colaboratory notebook

    colab.research.google.com

     

    반응형

    댓글

Designed by Tistory.