Solução Teórica
Após selecionar os ativos que poderão compor a carteira, devemos obter seus retornos médios e a matriz de covariância desses retornos. Tanto o vetor de retorno quanto a matriz de covariância devem ser anualizados, considerando que o ano possui 252 dias de negociação (dias úteis).
Para calcular a variância e o retorno esperado da carteira, utilizamos as seguintes equações:
\[
\sigma^2_{c} = \omega^\prime\Sigma\omega \\
R_{c} = \omega^\prime\mu
\]
Onde temos:
\[\omega\text{: Vetor de proporção de cada ativo na carteira, com }\sum_{i=1}^{N}\omega_i=1 \\
\Sigma\text{: Matriz de covariância dos ativos anualizada} \\
\mu\text{: Vetor do retorno médio anualizado dos ativos}
\]
Para solucionar o problema, devemos fixar um valor mínimo para o retorno da carteira. Com esse valor fixado, devemos minimizar a variância da carteira sujeito à restrição de que o retorno não seja inferior ao mínimo fixado.
\[min\space\sigma^2_{c} \\ s.t.\space R_{c} \geq R \]
A solução do problema acima, para cada \(R\) escolhido, nos dará um vetor \(\omega^*\) pertencente a fronteira. Assim, com as equações apresentadas acima, conseguimos calcular o retorno e a variância da carteira formada por \(\omega^*\).
O lugar geométrico formado pelos pares (\(\sigma_c,R_c\)), calculado para \(\omega^*\), para diversos valores escolhidos de \(R\), forma a fronteira média variância.
Implementação em Python
A simulação foi implementada em Python 3.5.1 e disponibilizada em um arquivo do tipo Jupyter Notebook para melhor visualização.
\(\checkmark\)Foram selecionados 5 ativos negociados na bolsa brasileira.
\(\checkmark\)Foi feita a captura dos preços desses ativos do período de 04/Jan/16 a 07/Dez/16.
first_price_date = dt.datetime(2016, 1, 1)
last_price_date = dt.datetime(2016, 12, 7)
symbols = ['VALE5', 'PETR4', 'BVMF3', 'ITUB4', 'BBAS3']
data = pd.DataFrame()
for sym in symbols:
data[sym] = web.DataReader(sym, 'google', first_price_date, last_price_date)['Close']
data = data.dropna()
\(\checkmark\)Com os preços, foi feito o cálculo dos retornos diários e, em seguida, o retorno médio \(\mu\) e a matrix de covariância \(\Sigma\), ambos anualizados.
daily_returns = np.log(data / data.shift(1))
_252_DAYS = 252
covariance_matrix = np.asarray(_252_DAYS * daily_returns.cov())
annualized_returns = np.asarray(_252_DAYS * daily_returns.mean())
\(\checkmark\)Foram definidas as equações para calcular a variância e o retorno da carteira, dado \(\omega\).
def portfolio_variance(w):
_w = np.asarray(w)
return _w.dot(covariance_matrix).dot(_w)
def portfolio_return(w):
_w = np.asarray(w)
return _w.dot(annualized_returns)
\(\checkmark\)Foi definido um método que, dado um retorno mínimo, minimiza a variância da carteira e retorna o vetor com a proporção de cada ativo \(\omega^*\).
def minimize_portfolio_variance(min_return, short_sale_allowed=True):
assets_number = len(symbols)
bnds = None if short_sale_allowed \
else [(0, None) for i in range(assets_number)] # If short sale not allowed, lower bound = 0
initial_guess = [1 / assets_number for i in range(assets_number)]
cons = ({'type': 'eq', 'fun': lambda w: w.sum() - 1},
{'type': 'ineq', 'fun': lambda w: portfolio_return(w) - min_return})
return minimize(portfolio_variance, initial_guess,
constraints=cons,
bounds=bnds,
options={'disp': False},
method='SLSQP',
jac=portfolio_variance_gradient)
\(\checkmark\)Foi definido um método que calcula a fronteira eficiente, executando o método anterior para diversos valores de retorno mínimo. Assim, para cada iteração, temos um ponto da fronteira desejada.
def calculate_mv_frontier(short_sale_allowed=True):
frontier = pd.Series()
weights = pd.DataFrame()
for min_return in np.linspace(0, 1.2, num=2000):
result = minimize_portfolio_variance(min_return, short_sale_allowed=short_sale_allowed)
if not result.success:
continue
w_frontier = result.x
port_sigma = np.sqrt(portfolio_variance(w_frontier))
port_return = portfolio_return(w_frontier)
frontier.set_value(port_sigma, port_return)
for i, symbol in enumerate(symbols):
weights.set_value(port_sigma, symbol, w_frontier[i])
return frontier, weights
\(\checkmark\)Foi feito o cálculo das fronteiras eficientes: Uma calculada sem vendas a descoberto e a outra com vendas a descoberto.
frontier_ss_false, weights_ss_false = calculate_mv_frontier(short_sale_allowed=False)
frontier_ss_true, weights_ss_true = calculate_mv_frontier(short_sale_allowed=True)
\(\checkmark\)As fronteiras foram apresentadas, bem como as proporções dos ativos pra cada nível de risco. As figuras geradas na simulação são apresentadas a seguir:



É interessante notar que a fronteira que permite vendas a descoberto é maior, por possibilitar mais combinações de ativos. Porém, como esperado, na região em que a carteira ótima possui apenas proporções positivas, as duas fronteiras se confundem.
Outra característica importante que deve ser notada é que o retorno máximo da carteira quando não é permitido venda a descoberto é o maior retorno dentre os ativos, alcançado quando a carteira está totalmente alocada neste ativo (proporção de 100%).
Quando a venda a descoberto é permitida, alcançamos o mesmo retorno com um risco inferior e com outros ativos na carteira. É claro que pelo menos um ativo deve estar vendido.
Arquivo com a Implementação: Link