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, FF5Tres 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)