O risco para cada nível de retorno
O tópico anterior achou, para um retorno-alvo, os pesos de menor variância. Faça isso para cada alvo e coloque a volatilidade no eixo horizontal e o retorno no vertical: os pontos formam uma curva, a fronteira eficiente. Toda carteira desses ativos está sobre ela ou dentro, à direita. Uma carteira dentro é desperdício, pois outra dá o mesmo retorno com menos risco, ou mais retorno com o mesmo risco. A ideia de Markowitz: o investidor só deve olhar a curva.
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
def target_return_weights(mu, cov, target):
ones = np.ones(len(mu))
s1, sm = np.linalg.solve(cov, ones), np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
d = a * c - b ** 2
return ((c - b * target) * s1 + (a * target - b) * sm) / d
for target in (0.05, 0.06, 0.07, 0.08, 0.09, 0.10):
w = target_return_weights(mu, cov, target)
print(target, round(float(np.sqrt(w @ cov @ w)), 4))Sua forma e sua ponta
A fronteira é uma parábola em (retorno, variância), e uma hipérbole se se traça a volatilidade. A variância para um retorno-alvo t é (a * t**2 - 2 * b * t + c) / (a * c - b**2), com os mesmos números a, b, c de antes. Seu ponto mais baixo é a carteira de variância mínima global, com retorno b / a: a ponta da curva, que é a carteira de variância mínima de dois tópicos atrás. Só a parte da ponta para cima é a fronteira eficiente; o ramo de baixo é a imagem espelhada, com menos retorno para o mesmo risco.
# The tip of the curve: the minimum-variance portfolio and its return
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
ones = np.ones(3)
x = np.linalg.solve(cov, ones)
w = x / x.sum()
print(np.round(w, 4), round(float(w @ mu), 4), round(float(np.sqrt(w @ cov @ w)), 4)) # the tip: return, volatility
def closed_form_vol(target):
s1, sm = x, np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
return float(np.sqrt((a * target ** 2 - 2 * b * target + c) / (a * c - b ** 2)))
for target in (0.05, 0.0642, 0.07):
print(target, round(closed_form_vol(target), 4))Pontos eficientes e ineficientes
O mesmo risco aparece em dois retornos na curva, um em cada ramo, e só o maior vale a pena manter. Logo um alvo é eficiente exatamente quando é pelo menos o retorno de variância mínima. O teste é uma única comparação, mas decide quais alvos pertencem a um gráfico de escolhas. Buscar o melhor equilíbrio na fronteira vira escolher um ponto, e uma resposta comum é o que rende mais por unidade de risco acima da taxa livre de risco.
# The point with the highest Sharpe ratio among candidate targets
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
rf = 0.02
ones = np.ones(3)
s1, sm = np.linalg.solve(cov, ones), np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
d = a * c - b ** 2
targets = np.linspace(0.05, 0.10, 11)
vols = np.sqrt((a * targets ** 2 - 2 * b * targets + c) / d)
sharpe = (targets - rf) / vols
for t, v, s in zip(targets, vols, sharpe):
print(round(float(t), 3), round(float(v), 4), round(float(s), 4))
print("best target:", round(float(targets[np.argmax(sharpe)]), 3))# Efficient or not: compare a target with the return of the minimum-variance portfolio
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
x = np.linalg.solve(cov, np.ones(3))
tip = float((x / x.sum()) @ mu)
for target in (0.05, 0.06, 0.07, 0.08):
print(target, "efficient" if target >= tip else "inefficient")mu e cov, ambos estimados, então ela mesma é uma estimativa, e lisonjeira: o otimizador escolheu, entre todas as misturas, as que por acaso têm retornos estimados altos e riscos baixos. Os resultados reais caem à direita e abaixo da linha visível. Reamostrar os dados, ou comparar a fronteira da primeira metade de uma amostra com o desempenho na segunda, mostra quanto da curva foi sorte. Trate a fronteira como um mapa de equilíbrios, não uma promessa.- A fronteira eficiente dá, para cada retorno-alvo, a menor volatilidade:
sqrt(w @ cov @ w)comw = target_return_weights(...); as carteiras dentro dela são desperdício. - Sua ponta é a carteira de variância mínima global, com retorno
b / a; só alvos iguais ou acima desse retorno são eficientes. - Escolha um ponto por um critério como o maior índice de Sharpe
(target - rf) / volatility; lembre que a curva vem de estimativas e é otimista.
A seguir: um backtester vetorizado, testar uma regra de negociação com preços históricos.
Lab: o que é avaliado
- Traçar a fronteira com
frontier_vols25 pts - Achar a ponta da curva com
min_variance_return25 pts - Distinguir a metade boa da ruim com
is_efficient25 pts - Escolher o melhor ponto com
max_sharpe_target25 pts