Quant500 Academy

6. statsmodels en la práctica: de precios a regresión factorial

Nivel 6: Regresión y Modelos de Pricing

Vamos a montar la tubería completa: descargar precios, limpiarlos, convertirlos en retornos, traer los factores de Fama-French y estimar CAPM, FF3 y FF5 de una tacada.

statsmodels ofrece un buen abanico de modelos de corte transversal y también algunos de series temporales. La regresión lineal básica la proporciona OLS. Estimar un modelo requiere primero especificar el modelo y después llamar a un método para estimar los parámetros (llamado fit en la mayoría de modelos). Esa separación entre especificar y estimar es la marca de la casa.

Empezamos por los datos. Se obtiene la lista de componentes del S&P 500 de Wikipedia, se descargan sus precios ajustados y se añade SPY como aproximación del mercado:

data = yf.download(tickers, start=start, end=end)['Adj Close']

cleaned_data = data.copy()
# Quitamos empresas (columnas) sin ningun dato en todo el rango
cleaned_data.dropna(axis='columns', how='all', inplace=True)
# Quitamos dias (filas) sin datos para ninguna empresa
cleaned_data.dropna(axis='index', how='all', inplace=True)
# Y por ultimo las columnas con al menos un NaN
cleaned_data.dropna(axis='columns', how='any', inplace=True)

# Convertimos precios en retornos logaritmicos
return_values = np.log(cleaned_data).diff().dropna()

Un aviso honesto sobre ese último dropna: al eliminar toda columna con al menos un valor faltante te quedas solo con las empresas que existían y cotizaron durante todo el periodo. Eso es sesgo de supervivencia de manual. Para un ejercicio didáctico vale, pero si de ahí sales a construir una estrategia, tus resultados estarán inflados porque has borrado justo a las que quebraron.

Ahora la función que hace el trabajo, tal como la plantea el profesor:

def assetPriceReg(df_stk):
    import pandas_datareader.data as web

    # Datos de factores desde la libreria de Kenneth French
    df_factors = web.DataReader('F-F_Research_Data_5_Factors_2x3_daily', 'famafrench')[0]
    df_factors.rename(columns={'Mkt-RF': 'MKT'}, inplace=True)
    for col in ['MKT', 'SMB', 'HML', 'RMW', 'CMA', 'RF']:
        df_factors[col] = df_factors[col] / 100

    # Unimos retornos de la accion y factores por fecha
    df_stock_factor = pd.merge(df_stk, df_factors,
                               left_index=True, right_index=True)
    # Retorno en exceso sobre el activo libre de riesgo
    df_stock_factor['XsRet'] = df_stock_factor['Returns'] - df_stock_factor['RF']

    CAPM = sm.ols(formula='XsRet ~ MKT', data=df_stock_factor).fit(
        cov_type='HAC', cov_kwds={'maxlags': 1})
    FF3 = sm.ols(formula='XsRet ~ MKT + SMB + HML', data=df_stock_factor).fit(
        cov_type='HAC', cov_kwds={'maxlags': 1})
    FF5 = sm.ols(formula='XsRet ~ MKT + SMB + HML + RMW + CMA',
                 data=df_stock_factor).fit(cov_type='HAC', cov_kwds={'maxlags': 1})
    return CAPM, FF3, FF5

Tres detalles críticos. Primero, la división entre 100: la librería de French publica los factores en porcentaje (0,53 significa 0,53%), mientras que los retornos logarítmicos de yfinance vienen en tanto por uno. Sin esa conversión, el beta sale escalado por 100 y el modelo entero deja de tener sentido. Segundo, el pd.merge por índice hace una unión interna: solo sobreviven las fechas presentes en ambos conjuntos, que es lo que quieres. Tercero, la variable dependiente nunca es el retorno, es el exceso de retorno sobre RF: si regresas el retorno bruto, el intercepto ya no es el alfa.

Sobre la acción concreta se prepara el retorno logarítmico y se lanza todo:

df_stk = yf.download('NVDA', start=start, end=end)
df_stk['Returns'] = np.log(df_stk[['Adj Close']]).diff().dropna()
df_stk['Returns'].describe()

df_regOutput = assetPriceReg(df_stk)

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.