Portfolio optimization(포트폴리오 최적화)

2023. 10. 10. 22:02금융 데이터 분석

반응형

이번 포스팅에서는 시뮬레이션과 최적화를 활용하여 포트폴리오의 종목 비중을 최적화 할 수 있는 방법에 대해서 작성해보겠습니다.

Sharpe ratio 개념에서 엿볼 수 있듯이 변동성을 최소화하면서 수익률을 극대화하는 것이 좋은 포트폴리오를 구성하는 방법일 것입니다. 물론 미래를 알 수 있다면 수익률이 가장 좋은 단일 종목을 선택하여 가지고 가는 것이 좋겠지만, 미래는 불확실하기 때문에 우리가 선정한 후보 종목들 중에서 어떻게 비중을 가져가는 것이 좋은 포트폴리오를 구성하는 것인지 선택하는 기준에 대해서 살펴보겠습니다.

[시뮬레이션]

시뮬레이션 방법은 종목을 구성하고자 한 비중들을 무작위로 생성하고, 생성한 비중일 때의 수익률과 변동성을 계산하여 Plotting 해보는 방법입니다. 예를 들어 삼성전자/네이버/SKT 3가지 종목으로 포트폴리오를 구성하고자 하면 [0.1,0.1,0.8] 부터 [0.8, 0.1,0.1] 까지 다양하게 비중을 생성해보고 해당 비중에서의 수익률과 변동성을 구합니다.

import numpy as np
import scipy.optimize as sco
import matplotlib.pyplot as plt
import FinanceDataReader as fdr
import pandas as pd


### DATA LOADING 


samsung = fdr.DataReader('005930','2022-10-01', '2023-10-01')
naver = fdr.DataReader('035420','2022-10-01', '2023-10-01')
skt = fdr.DataReader('017670','2022-10-01', '2023-10-01')


df_stock = pd.concat([samsung["Close"],naver["Close"],skt["Close"]] ,axis = 1)
df_stock.columns = ["samsung", "naver", "skt"]



returns = df_stock.pct_change().iloc[1:]
returns.cov()
returns.var()


## simulation
num_ports = 5000
all_weights = np.zeros((num_ports, len(df_stock.columns)))
ret_arr = np.zeros(num_ports)
vol_arr = np.zeros(num_ports)
sharpe_arr = np.zeros(num_ports)

for ind in range(num_ports): 
    # weights 
    weights = np.array(np.random.random(len(df_stock.columns))) 
    weights = weights/np.sum(weights)  
	
    # save the weights
    all_weights[ind,:] = weights
	
    # expected return 
    ret_arr[ind] = np.sum((returns.mean()*weights)*255)

    # expected volatility 
    vol_arr[ind] = np.sqrt(np.dot(weights.T,np.dot(returns.cov()*255, weights)))
    
    # Sharpe Ratio 
    sharpe_arr[ind] = ret_arr[ind]/vol_arr[ind]

plt.figure(figsize=(12,8))
plt.scatter(vol_arr,ret_arr,c=sharpe_arr,cmap='plasma')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatility')
plt.ylabel('Return')

Code에서 확인할 수 있듯이 np.random.random함수를 이용하여 무작위로 포트폴리오 비중을 생성하고 해당 결과를 Plotting 해본 것입니다. 경계면이 Efficient frontier로 변동성에 따른 최대 기대수익률을 나타냅니다.

그래프를 보면 0.14정도에서 가장 작은 변동성을 가지고 해당 변동성에서 기대수익률은 0.1정도 되는 것을 확인 할 수 있습니다.

[최적화]

시뮬레이션을 통한 그래프를 통해서도 비중을 구해볼 수 있지만 최적화 방법론도 적용해볼 수 있습니다.

 

위의 수식을 보면 수익률 $\mu_p$를 기대할 때 변동성을 최소화하는 포트폴리오 비중 $w$를 구하는 최적화 방정식입니다.

해당 식을 Code로 구현하면 아래와 같습니다.

## optimization

def objective(weights, returns):
    weights = np.array(weights)
    return weights.dot(returns.cov()).dot(weights.T)


# The constraints
cons = (# The weights must sum up to one.
        # 여기서 x는 weights들이다.
        {"type":"eq", "fun": lambda x: np.sum(x)-1}, 
        {"type": "ineq", "fun": lambda x: np.sum(returns.mean()*x)-0.2/255} ## 기대수익률 10% 일때 - 0.1/255
        )

# Every stock can get any weight from 0 to 1
bounds = tuple((0,1) for x in range(returns.shape[1])) 


# Initialize the weights
guess = [1./returns.shape[1] for x in range(returns.shape[1])]
guess = [0.0, 1.0, 0.0]
 
 ## tol이 너무 크면 Local optima에 빠질 수 있음
optimized_results = sco.minimize(objective,  guess, args = returns, method = "SLSQP", bounds=bounds, constraints=cons, tol=0.0000000001)
optimized_results

"""
결과
     fun: 0.00013750245640658268
     jac: array([3.32403799e-04, 2.87882543e-04, 8.13982660e-05])
 message: 'Optimization terminated successfully'
    nfev: 40
     nit: 10
    njev: 10
  status: 0
 success: True
       x: array([0.7642101 , 0.00864806, 0.22714185])
 """


weights = np.array([0.76,0,0.24])
np.sum((returns.mean()*weights)*255)
np.sqrt(np.dot(weights.T,np.dot(returns.cov()*255, weights)))

기대수익률이 20%일 때 비중은 약 [0.76,0,0.24]로 구성되며, 이때 변동성은 0.18정도 됩니다. 만약 기대수익률이 10%라면 [0.32,0,0.68] 정도됩니다. 즉 기대수익률은 삼성전자가 좋지만, 변동성은 SKT가 낮기 때문에 기대수익률이 낮아진다면 SKT의 비중이 높아지게 됩니다.

 

참조: 

https://predictivehacks.com/portfolio-optimization-in-python/

반응형