Optimización algorítmica de carteras con Markowitz

Gerard Sánchez - 27 de Diciembre de 2020 a las 18:20 - Modelos




El modelo de Markowitz ha sido un referente teórico fundamental a lo largo de los años en la selección de portafolios dentro del marco de la MPT o teoría moderna de gestión de carteras. 

Con el modelo de Markowitz se introduce la pregunta: ¿Cuál es el riesgo de mi inversión?, dejando atrás la percepción única de buscar retornos. La generación de retornos ajustados por riesgo es en gran medida lo que determina la calidad de un gestor o inversor.

Consiste en encontrar la cartera más adecuada, respetando la elección de activos, para cada inversor en función del riesgo que esté dispuesto a asumir. El modelo de Markowitz permite obtener los pesos óptimos de esos valores en base a maximizar individualmente una serie de variables como la volatilidad, el retorno, o el ratio sharpe. La generación de retornos ajustados por riesgo es en gran medida lo que determina la calidad de un gestor o inversor.

Como siempre, importamos las librerías que vamos a necesitar. En este caso la novedad es Scipy.Optimize, esta librería nos ayudará más adelante a minimizar variables de una función objetivo para poder dejar un valor limpio.

import pandas as pd
import numpy as np
from pandas_datareader import data as wb
import matplotlib.pyplot as plt
import scipy.optimize as optimize

Para el ejemplo tendremos una cartera con 3 valores: Visa, Home Depot y JD.com. Debemos descargar los resultados de cierre de estos 3 valores y guardarlos en una variable llamada log_returns en este caso.

assets = ['V','HD','JD']        

data = pd.DataFrame()
for t in assets:
    data[t] = wb.DataReader(t, data_source='yahoo', start='2015-1-1')['Adj Close']

log_returns = np.log(1+data.pct_change())

Con num_assets definiremos el número de nuestros activos con len que escogimos anteriormente. 

Weights genera una matriz aleatoria de pesos con np.random.dirichlet y np.ones con el número de la variable num_assets que suman 1.0En la cartera, uno de los supuestos es que todos los fondos se destinarán a los activos de la cartera de acuerdo con alguna ponderación, en este ejemplo no se considera liquidez alguna.

Una vez generadas nuestras variables, procedemos a calcular los rendimientos ponderados y la volatilidad de la cartera para cada una de las iteraciones del rango, que añadiremos a las variables port_returns y port_vols con append.

port_returns = []
port_vols = []

for i in range (10000):
    num_assets = len(assets)
    weights = np.random.random(num_assets)
    weights /= np.sum(weights)  
    port_ret = np.sum(log_returns.mean() * weights) * 252
    port_var = np.sqrt(np.dot(weights.T, np.dot(log_returns.cov()*252, weights)))       
    port_returns.append(port_ret)
    port_vols.append(port_var)

Con esto tenemos las volatilidades y retornos ponderados por nuestros pesos aleatorios para cada una de las iteraciones. Ya podemos ser capaces de dibujar el scatter plot de Markowitz o "Markowitz bullets", pero le vamos a añadir algunas cosas antes.

Vamos a definir una función con def llamada portfolio_stats con los argumentos weights y log_returns con los cálculos de retornos, volatilidad y sharpe para poder llamar a las variables con 'Return', 'Volatility' y 'Sharpe' que pasaremos a continuación a una función de cara a optimizar ese ratio. Con esto conseguiremos la cartera con mayor rentabilidad ajustada al riesgo. 

Para la fórmula de Sharpe en este ejemplo asumiremos un .

También se pueden optimizar las variables de volatilidad y retornos, minimizando y maximizando respectivamente, creando las carteras con la menor varianza y el mayor retorno independiente del riesgo, pero no será en este ejemplo.

def portfolio_stats(weights, log_returns):
    port_ret = np.sum(log_returns.mean() * weights) * 252
    port_var = np.sqrt(np.dot(weights.T, np.dot(log_returns.cov() * 252, weights)))
    sharpe = port_ret/port_var
    return {'Return': port_ret, 'Volatility': port_var, 'Sharpe': sharpe}

Para optimizar el ratio Sharpe necesitamos una función que nos devuelva ese ratio.

Scipy ofrece una función de "minimizar", pero no una función de "maximizar". Esto puede parecer un problema a primera vista, pero se resuelve fácilmente al darte cuenta de que la maximización de la relación de Sharpe es análoga a minimizar la ratio Sharpe negativa, o incluso podemos minimizar su inversa. Nos decantamos por la vía rápida que es con un signo negativo en portfolio_stats.

def minimize_sharpe(weights, log_returns): 
    return -portfolio_stats(weights, log_returns)['Sharpe'] 

Transformamos en matriz los resultados de la cartera y las volatilidades y redefinimos la variable sharpe para poder usar argmax() y maximizar los valores de port_returns y port_vols con el mayor Sharpe.

port_returns = np.array(port_returns)
port_vols = np.array(port_vols)
sharpe = port_returns/port_vols

max_sr_vol = port_vols[sharpe.argmax()]
max_sr_ret = port_returns[sharpe.argmax()]

Para la función optimize.minimize debemos especificar una serie de parámetros que explicaremos a continuación:

constraints son restricciones, en este caso nuestra restricción clave es que todas las ponderaciones de la cartera deben sumar 1.0.No se permite un % en liquidez como hemos comentado antes.

bounds se refiere a la cantidad de nuestra cartera que puede ocupar un activo, de 0.0 a 1.0 en tanto por uno. 0.0 es una posición de 0% y 1.0 es una posición de 100%. Si un activo cuenta con el 100% del efectivo no tenemos diversificación alguna, podríamos cambiar esto por una cantidad fija máxima por activo para limitar la exposición.

initializer es el punto de partida con los primeros pesos. Aquí los configuraremos para que cada acción ocupe un porcentaje igual de la cartera, que en este caso, al tener 3, será de 0,3 periódico cada uno.

El método SLSQP es el método utilizado para la mayoría de problemas de minimización genéricos, significa Sequential Least Squares Programming

constraints = ({'type' : 'eq', 'fun': lambda x: np.sum(x) -1})
bounds = tuple((0,1) for x in range(num_assets))
initializer = num_assets * [1./num_assets,]

Observamos la variable optimal_sharpe definida por la función de optimize.minimize con los parámetros: minimize_sharpe definidos en una función anterior, initializer, método SLSQP, como argumento los log_returns en forma de tupla, bounds y contstrains.

Pesos óptimos

La matriz x nos muestra los pesos óptimos, creamos la variable optimal_sharpe_weights y optimal_stats con la función portfolio_stats, esta vez redefinida con los pesos óptimos según ratio Sharpe.

optimal_sharpe = optimize.minimize(minimize_sharpe,
                                   initializer,
                                   method = 'SLSQP',
                                   args = (log_returns,),
                                   bounds = bounds,
                                   constraints = constraints)

optimal_sharpe_weights = optimal_sharpe['x'].round(4)
optimal_stats = portfolio_stats(optimal_sharpe_weights, log_returns)

Con print sacamos por pantalla los resultados obtenidos con los activos y los pesos óptimos que lograron el mayor ratio sharpe, en forma de lista, y con optimal_stats devolvemos el retorno, la volatilidad y el sharpe óptimos redondeado a 4 decimales.

print("Pesos óptimos de la cartera: ", list(zip(assets, list(optimal_sharpe_weights*100))))
print("Retorno óptimo de la cartera: ", round(optimal_stats['Return']*100,4))
print("Volatilidad óptima de la cartera: ", round(optimal_stats['Volatility']*100,4))
print("Ratio Sharpe óptimo de la cartera: ", round(optimal_stats['Sharpe'],4))

Para una cartera con el máximo ratio sharpe, deberíamos tener un 49,07% de la inversión en Visa, un 39,31% en HD y un 11,62% en JD. Estos pesos son exactos pero la posición en acciones que podamos tomar para representar esos pesos no lo será, salvo que podamos fraccionar las acciones y comprar por una cantidad determinada.

Ratio Sharpe óptimo

Tenemos todo lo que necesitamos para trazar un gráfico con matplotlib que compare todas las combinaciones en términos de volatilidad (o riesgo) y rendimiento, coloreado por el índice de Sharpe

El punto rojo se obtiene del cálculo anterior y representa el rendimiento(y) y la volatilidad(x) de la simulación con el máximo ratio de Sharpe.

plt.figure(figsize = (12,6))
plt.scatter(port_vols,port_returns,c = (port_returns/port_vols))
plt.scatter(max_sr_vol, max_sr_ret,c='red', s=30)
plt.colorbar(label = 'Ratio Sharpe (rf=0)')
plt.xlabel('Volatilidad de la cartera')
plt.ylabel('Retorno de la cartera')
plt.show()

Se genera un gráfico de 3 ejes siendo un punto cada cartera con pesos aleatorios con el número de iteraciones especificadas. Los puntos más importantes son el máximo ratio sharpe (marcado en rojo), el de volatilidad mínima que lo encontrariamos lo más pegado a la izquierda posible (en este caso particular ambos están cerca pero no siempre va a ser así), y por último el de retorno máximo.

Markowitz Sharpe

Con 1M de iteraciones se nos dibuja la parábola a la perfección y disipamos cualquier atisbo de duda. La Frontera Eficiente es el conjunto de las carteras más eficientes del universo de carteras que se puede encontrar con una combinación de activos determinada, empieza en el punto óptimo de mínima varianza y sube a través de la curva.

Capital marketm line

Aquí tenéis el script entero: 

import pandas as pd
import numpy as np
from pandas_datareader import data as wb
import matplotlib.pyplot as plt
import scipy.optimize as optimize

assets = ['V','HD','JD']        

data = pd.DataFrame()
for t in assets:
    data[t] = wb.DataReader(t, data_source='yahoo', start='2015-1-1')['Adj Close']

log_returns = np.log(1+data.pct_change())

port_returns = []
port_vols = []

for i in range (10000):
    num_assets = len(assets)
    weights = np.random.random(num_assets)
    weights /= np.sum(weights) 
    port_ret = np.sum(log_returns.mean() * weights) * 252
    port_var = np.sqrt(np.dot(weights.T, np.dot(log_returns.cov()*252, weights)))       
    port_returns.append(port_ret)
    port_vols.append(port_var)

def portfolio_stats(weights, log_returns):
    port_ret = np.sum(log_returns.mean() * weights) * 252
    port_var = np.sqrt(np.dot(weights.T, np.dot(log_returns.cov() * 252, weights)))
    sharpe = port_ret/port_var    
    return {'Return': port_ret, 'Volatility': port_var, 'Sharpe': sharpe}

def minimize_sharpe(weights, log_returns): 
    return -portfolio_stats(weights, log_returns)['Sharpe'] 

port_returns = np.array(port_returns)
port_vols = np.array(port_vols)
sharpe = port_returns/port_vols

max_sr_vol = port_vols[sharpe.argmax()]
max_sr_ret = port_returns[sharpe.argmax()]

constraints = ({'type' : 'eq', 'fun': lambda x: np.sum(x) -1})
bounds = tuple((0,1) for x in range(num_assets))
initializer = num_assets * [1./num_assets,]

optimal_sharpe = optimize.minimize(minimize_sharpe, initializer, method = 'SLSQP', args = (log_returns,) ,bounds = bounds, constraints = constraints)
optimal_sharpe_weights = optimal_sharpe['x'].round(4)
optimal_stats = portfolio_stats(optimal_sharpe_weights, log_returns)

print("Pesos óptimos de la cartera: ", list(zip(assets, list(optimal_sharpe_weights*100))))
print("Retorno óptimo de la cartera: ", round(optimal_stats['Return']*100,4))
print("Volatilidad óptima de la cartera: ", round(optimal_stats['Volatility']*100,4))
print("Ratio Sharpe óptimo de la cartera: ", round(optimal_stats['Sharpe'],4))

plt.figure(figsize = (12,6))
plt.scatter(port_vols,port_returns,c = (port_returns/port_vols))
plt.scatter(max_sr_vol, max_sr_ret,c='red', s=30)
plt.colorbar(label = 'Ratio Sharpe (rf=0)')
plt.xlabel('Volatilidad de la cartera')
plt.ylabel('Retorno de la cartera')
plt.show()


1879 visitas
4    Login to like
Categorías:
 Estrategias   Estadísticas   Random   Gestión pasiva   Análisis técnico   Modelos   CEO   Mapas mentales   Liberalismo   Python   Growth   Niusleta   Ahorro   Recursos humanos   Inmobiliario   Fiscalidad   Value investing   Dividendos   Contabilidad   Marketing   Riesgo   IF   Cursos   Opciones   Bolsa