/ Proyectos / Finanzas / Markowitz con ML

Markowitz con ML para carteras de inversión: LSTM + CAPM + Optimización

Pipeline cuantitativo completo: predicción de precios de activos con redes LSTM, selección de acciones con CAPM y construcción de la Frontera Eficiente con la Teoría Moderna de Carteras de Markowitz.

25 min de lectura Python · TensorFlow · Keras · yfinance · scipy Datos: precios históricos reales vía Yahoo Finance API Aplicable a tesis de finanzas, economía e ingeniería financiera

Resumen del proyectoWiki

¿Qué encontrarás en este proyecto?

  • Descarga de precios históricos de 10 acciones tecnológicas y financieras vía Yahoo Finance API.
  • Predicción del precio futuro de cada activo con redes LSTM entrenadas con indicadores técnicos (SMA, Bollinger, RSI, autocorrelación).
  • Filtrado de activos con el modelo CAPM: beta, costo de equity y selección de acciones con rendimiento esperado atractivo.
  • Optimización del portafolio con la Teoría Moderna de Carteras de Markowitz: Frontera Eficiente y portafolio de mínima volatilidad.

Este proyecto integra tres pilares de las finanzas cuantitativas modernas en un solo pipeline: predicción de series temporales con Deep Learning, selección de activos basada en riesgo sistemático y optimización matemática de carteras. El resultado es un portafolio con pesos óptimos que maximiza el retorno ajustado al riesgo según las predicciones del modelo LSTM.

El universo de inversión son 10 acciones de alta capitalización bursátil (AAPL, MSFT, GOOGL, AMZN, TSLA, META, NFLX, NVDA, JPM, JNJ), con datos históricos desde el 1 de enero de 2020. La red neuronal predice el precio para los próximos 30 días usando los precios de cierre ajustados — un precio que incorpora dividendos, splits y consolidaciones, permitiendo comparaciones históricas precisas.

¿Por qué este proyecto es relevante para una tesis? Combina tres áreas con bibliografía sólida: redes neuronales recurrentes (LSTM), teoría financiera clásica (CAPM y Markowitz) y optimización numérica (SLSQP). La contribución es tangible: usar predicciones ML como entrada de rendimientos esperados en lugar de medias históricas puras — un enfoque que se puede comparar empíricamente contra el baseline clásico.

Arquitectura del pipeline

  • Obtención de datos: descarga de precios ajustados de cierre con yfinance para 10 acciones desde 2020 a la fecha actual.
  • Predicción LSTM: entrenamiento de un modelo por acción con indicadores técnicos como features. Los últimos 30 días son el conjunto de prueba; el modelo predice los precios futuros que se usarán como rendimientos esperados.
  • Selección CAPM: cálculo de betas con regresión lineal sobre el S&P 500, aplicación del CAPM y filtrado de acciones cuyo retorno esperado ajustado por riesgo supera el exceso de retorno de mercado.
  • Optimización Markowitz: cálculo de la matriz de covarianza, trazado de la Frontera Eficiente y obtención de los pesos óptimos que minimizan la volatilidad del portafolio.

Stack tecnológico

  • Python 3.10+ — lenguaje principal
  • yfinance — descarga de datos históricos de Yahoo Finance
  • TensorFlow / Keras — arquitectura LSTM con capas de Dropout y regularización L2
  • scikit-learn — MinMaxScaler, StandardScaler, mean_squared_error
  • scipy — optimización numérica con minimize (SLSQP) y regresión lineal (stats.linregress)
  • pandas / numpy — manipulación de datos y álgebra matricial
  • matplotlib — visualización de precios, curvas de pérdida y Frontera Eficiente

Setup del entornoConfiguración del sistema

El proyecto puede ejecutarse en Google Colab o localmente. Se recomienda Colab por el acceso a GPU gratuita para acelerar el entrenamiento de los modelos LSTM, especialmente cuando se entrena un modelo por acción.

Instalación de dependencias

Terminal — instalar dependencias
pip install yfinance tensorflow scikit-learn scipy pandas numpy matplotlib

Estructura del proyecto

Estructura de carpetas recomendada
proyecto-markowitz-lstm/
├── notebooks/
│   ├── 01_obtencion_datos.ipynb
│   ├── 02_prediccion_lstm.ipynb
│   ├── 03_capm_seleccion.ipynb
│   └── 04_markowitz_optimizacion.ipynb
├── models/
│   ├── lstm_AAPL.h5
│   ├── lstm_MSFT.h5
│   └── ...                        # un modelo por acción
├── data/
│   └── closing_prices.csv         # precios descargados y cacheados
└── requirements.txt

Obtención de datos con yfinance

Se descarga el histórico de precios de cierre ajustados para 10 acciones desde el 1 de enero de 2020 hasta la fecha actual. El precio de cierre ajustado incorpora dividendos, splits y consolidaciones, lo que permite comparaciones históricas precisas entre períodos.

Python — descarga de precios con yfinance
from datetime import datetime
import yfinance as yf
import pandas as pd

# Universo de inversión: 10 acciones de alta capitalización
symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA',
           'META', 'NFLX', 'NVDA', 'JPM', 'JNJ']

fecha_inicial = "2020-01-01"
fecha_final   = datetime.today().strftime('%Y-%m-%d')

# Descargar datos históricos
main_data = yf.download(symbols, start=fecha_inicial, end=fecha_final)

# Extraer precios de cierre ajustados
closing_prices = main_data['Adj Close']

print(f"Período: {fecha_inicial} → {fecha_final}")
print(f"Shape: {closing_prices.shape}")
print(closing_prices.tail(3))
Python — visualización de precios históricos
import matplotlib.pyplot as plt

closing_prices.plot(figsize=(12, 6))
plt.title('Precios de Cierre Ajustados — 10 Acciones (2020 a la fecha)')
plt.xlabel('Fecha')
plt.ylabel('Precio de Cierre Ajustado (USD)')
plt.legend(loc='upper left', fontsize=8)
plt.tight_layout()
plt.show()

Deep Learning para series temporalesPredicción de precios con LSTM

Las redes LSTM (Long Short-Term Memory) son especialmente adecuadas para series temporales financieras porque pueden capturar dependencias de largo plazo entre observaciones — algo que las redes neuronales estándar no logran. Se entrena un modelo LSTM independiente por cada acción del universo.

División de datos e indicadores técnicos

Los datos de entrenamiento incluyen todos los registros excepto los últimos 30 días. Los últimos 30 días forman el conjunto de prueba. Sobre cada serie se calculan cuatro indicadores técnicos que enriquecen las features del modelo:

  • SMA de 20 días — suaviza las fluctuaciones e identifica la tendencia general. Un valor alto o bajo puede indicar niveles de soporte o resistencia.
  • Bandas de Bollinger (20 días, 2σ) — miden la volatilidad del mercado. Precios que tocan las bandas sugieren condiciones de sobrecompra o sobreventa.
  • RSI de 14 días — oscila entre 0 y 100. RSI > 70 indica sobrecompra; RSI < 30 indica sobreventa. Útil para anticipar cambios de tendencia.
  • Autocorrelación lag-1 — mide si el precio de hoy está correlacionado con el de ayer. Detecta patrones repetitivos y ciclos.
Python — cálculo de indicadores técnicos
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler

def calculate_technical_indicators(data, stock_name):
    """Calcula SMA, Bandas de Bollinger, RSI y autocorrelación."""
    data = pd.DataFrame(data.copy())

    # SMA de 20 días
    data['SMA_20'] = data[stock_name].rolling(window=20).mean()

    # Bandas de Bollinger (20 días, desviación estándar de 2)
    rolling_mean = data[stock_name].rolling(window=20).mean()
    rolling_std  = data[stock_name].rolling(window=20).std()
    data['BB_upper'] = rolling_mean + 2 * rolling_std
    data['BB_lower'] = rolling_mean - 2 * rolling_std

    # RSI de 14 días
    delta = data[stock_name].diff()
    gain  = (delta.where(delta > 0, 0)).rolling(window=14).mean()
    loss  = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
    RS    = gain / loss
    data['RSI_14'] = 100 - (100 / (1 + RS))

    # Autocorrelación lag-1 (valor escalar — se usa como referencia)
    autocorr_value = data[stock_name].autocorr()

    return data.dropna()

def preprocess_data(data):
    """Elimina NaNs y normaliza con MinMaxScaler al rango [0, 1]."""
    data.dropna(inplace=True)
    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(data)
    return scaled_data, scaler

Arquitectura de la red LSTM

La red usa un modelo secuencial de Keras con tres capas LSTM apiladas y capas de Dropout para reducir el sobreajuste. La capa densa de salida tiene regularización L2 para penalizar pesos grandes.

Python — definición y entrenamiento del modelo LSTM
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.regularizers import L2

def build_lstm_model(input_shape):
    model = Sequential([
        # Primera capa LSTM — devuelve secuencias para la siguiente capa
        LSTM(units=50, return_sequences=True, input_shape=input_shape),

        # Segunda capa LSTM con Dropout del 20%
        LSTM(units=50, return_sequences=True),
        Dropout(0.2),

        # Tercera capa LSTM — solo devuelve la última salida
        LSTM(units=50, return_sequences=False),
        Dropout(0.1),

        # Capa densa de salida con regularización L2
        Dense(units=1, kernel_regularizer=L2(0.01))
    ])
    model.compile(optimizer='adam', loss='mean_squared_error')
    return model

# Ejemplo: entrenamiento para AAPL
stock = 'AAPL'
test_data  = closing_prices[stock].iloc[-30:]
train_data = closing_prices[stock].iloc[:-30]

train_with_ind = calculate_technical_indicators(train_data, stock)
train_processed, scaler = preprocess_data(train_with_ind)

X_train = train_processed[:, :-1]
y_train = train_processed[:, -1]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))

model = build_lstm_model(input_shape=(X_train.shape[1], 1))
history = model.fit(
    X_train, y_train,
    epochs=50,
    batch_size=32,
    validation_split=0.1,
    verbose=1
)
Parámetros clave del entrenamiento: 50 épocas permiten que ambas pérdidas (entrenamiento y validación) desciendan consistentemente sin sobreajuste severo. La pérdida de entrenamiento cae de ~0.143 a ~0.029; la de validación de ~0.154 a ~0.047, con fluctuaciones normales en una serie financiera. El batch_size de 32 balancea velocidad de entrenamiento con estabilidad del gradiente.

Curvas de aprendizaje

Python — visualización de curvas de pérdida
import matplotlib.pyplot as plt

plt.figure(figsize=(14, 5))
plt.plot(history.history['loss'],     label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.title('Curvas de Aprendizaje — LSTM')
plt.xlabel('Épocas')
plt.ylabel('Pérdida (MSE)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Entrenamiento en lote: un modelo por acción

Para el pipeline completo se entrena un modelo LSTM por cada acción del universo y se almacenan las predicciones de los últimos 30 días para usarlas como rendimientos esperados en el CAPM y Markowitz.

Python — loop de entrenamiento para todas las acciones
from sklearn.metrics import mean_squared_error
import numpy as np

models      = {}
predictions = {}
errors      = {}

for column in closing_prices.columns:
    test_data  = closing_prices[column].iloc[-30:]
    train_data = closing_prices[column].iloc[:-30]

    # Indicadores técnicos y normalización
    train_with_ind = calculate_technical_indicators(train_data, column)
    train_processed, scaler = preprocess_data(train_with_ind)

    X_train = train_processed[:, :-1].reshape(-1, train_processed.shape[1]-1, 1)
    y_train = train_processed[:, -1]

    # Construir y entrenar el modelo
    model = build_lstm_model(input_shape=(X_train.shape[1], 1))
    model.fit(X_train, y_train, epochs=50, batch_size=32, verbose=0)

    # Predicción en test
    test_with_ind = calculate_technical_indicators(test_data, column)
    test_processed, _ = preprocess_data(test_with_ind)
    X_test = test_processed[:, :-1].reshape(-1, test_processed.shape[1]-1, 1)

    pred_scaled = model.predict(X_test)
    pred = scaler.inverse_transform(
        np.concatenate((X_test[:, :, 0], pred_scaled), axis=1))[:, -1]

    models[column]      = model
    predictions[column] = pred
    errors[column]      = np.sqrt(mean_squared_error(test_data[:len(pred)], pred))
    print(f"{column}: RMSE = {errors[column]:.4f}")

print("\nEntrenamiento completado para todas las acciones.")

Filtrado de activos por riesgo sistemáticoSelección con CAPM

Antes de optimizar el portafolio, se filtra el universo de acciones usando el Modelo de Valoración de Activos de Capital (CAPM). La idea central es seleccionar solo las acciones cuyo rendimiento esperado — estimado por las predicciones LSTM — justifica el riesgo sistemático que añaden al portafolio.

Rendimientos esperados desde las predicciones LSTM

Los rendimientos esperados se calculan como el cambio porcentual entre el último precio predicho y el primero: (precio_final - precio_inicial) / precio_inicial. Esto usa la predicción del modelo como estimador del rendimiento futuro en lugar de promedios históricos.

Python — rendimientos esperados desde predicciones LSTM
import pandas as pd

er = {}  # expected returns
for stock, prices in predictions.items():
    er[stock] = (prices[-1] - prices[0]) / prices[0]

# Convertir a Serie de pandas
expected_returns_ = pd.Series(er, name='expected_returns')

print("Rendimientos esperados (basados en predicción LSTM):")
print(expected_returns_.sort_values(ascending=False))

Cálculo de betas y aplicación del CAPM

El beta de cada acción se estima mediante regresión lineal de los rendimientos diarios de la acción contra los rendimientos del S&P 500 (proxy del mercado). Con el beta y una tasa libre de riesgo del 2%, se aplica la ecuación del CAPM para obtener el costo de capital propio de cada acción. Se seleccionan las acciones cuyo costo de equity supera el exceso de retorno del mercado.

Python — CAPM: betas, costo de equity y selección de activos
import yfinance as yf
import pandas as pd
import numpy as np
from scipy import stats

# Rendimientos históricos de las acciones
data    = yf.download(symbols, start=fecha_inicial, end=fecha_final)['Adj Close']
returns = data.pct_change().dropna()

# S&P 500 como proxy del mercado
market_index = yf.download('^GSPC', start=fecha_inicial, end=fecha_final)['Adj Close']
market_index = market_index.pct_change().dropna()

# Parámetros CAPM
rf            = 0.02  # Tasa libre de riesgo (supuesto anualizado)
market_return = market_index.mean() * 252  # Rendimiento medio anualizado del mercado

# Calcular beta por acción usando regresión lineal
betas = {}
for ticker in symbols:
    slope, intercept, r_value, p_value, std_err = stats.linregress(
        market_index, returns[ticker])
    betas[ticker] = slope

# Costo de capital propio: CAPM → E(Ri) = Rf + β * (E(Rm) - Rf)
cost_of_equity = {}
for ticker in symbols:
    cost_of_equity[ticker] = rf + betas[ticker] * (market_return - rf)

# Seleccionar acciones donde el costo de equity supera el exceso de retorno de mercado
selected_stocks = [t for t in symbols if cost_of_equity[t] > market_return - rf]

print("Betas calculados:")
for t, b in betas.items():
    print(f"  {t}: β = {b:.4f}")

print(f"\nAcciones seleccionadas por CAPM ({len(selected_stocks)}):")
print(selected_stocks)

Interpretación del CAPM

¿Por qué usar CAPM como filtro? En lugar de incluir todas las acciones en el portafolio — lo que puede diluir el rendimiento con activos que no compensan su riesgo sistemático —, el CAPM actúa como un primer filtro cuantitativo. Las acciones seleccionadas son aquellas donde el mercado paga suficientemente por el riesgo adicional (beta alto) que representan. Sobre este subconjunto, Markowitz optimiza los pesos.

Teoría Moderna de CarterasOptimización con Markowitz

La Teoría Moderna de Carteras de Harry Markowitz (1952) postula que existe un conjunto de portafolios óptimos que maximizan el rendimiento esperado para cada nivel de riesgo — la Frontera Eficiente. El problema de optimización consiste en encontrar los pesos de asignación que minimizan la volatilidad del portafolio sujeto a un retorno objetivo.

Preparación: rendimientos esperados y matriz de covarianza

Python — rendimientos esperados filtrados y matriz de covarianza
import numpy as np
from scipy.optimize import minimize
import yfinance as yf

# Rendimientos esperados solo para las acciones seleccionadas por CAPM
er = {}
for stock, prices in predictions.items():
    if stock in selected_stocks:
        er[stock] = (prices[-1] - prices[0]) / prices[0]

expected_returns_ = pd.Series(er, name='expected_returns')
print("Rendimientos esperados (acciones seleccionadas):")
print(expected_returns_)

# Matriz de covarianza sobre rendimientos diarios históricos
data_sel      = yf.download(selected_stocks, start=fecha_inicial, end=fecha_final)['Adj Close']
daily_returns = data_sel.pct_change().dropna()
cov_matrix    = daily_returns.cov()

print(f"\nMatriz de covarianza ({cov_matrix.shape}):")
print(cov_matrix.round(6))

Optimización del portafolio de mínima volatilidad

La función objetivo es la volatilidad del portafolio: √(wT · Σ · w). Se minimiza con SLSQP (Sequential Least Squares Programming) sujeto a que los pesos sumen 1 y sean no negativos (sin posiciones cortas).

Python — optimización de mínima volatilidad con scipy
import numpy as np
from scipy.optimize import minimize

num_assets = len(selected_stocks)

# Función objetivo: volatilidad del portafolio
def objective(weights):
    return np.sqrt(weights.T @ cov_matrix @ weights)

# Restricciones: pesos suman a 1 y son no negativos
constraints = (
    {'type': 'eq',   'fun': lambda w: np.sum(w) - 1},
    {'type': 'ineq', 'fun': lambda w: w}
)
bounds        = tuple((0, None) for _ in range(num_assets))
initial_guess = [1.0 / num_assets] * num_assets

# Optimización
optimized = minimize(objective, initial_guess,
                     method='SLSQP', bounds=bounds,
                     constraints=constraints)

w_optimal = optimized.x

print("Pesos óptimos del portafolio (mínima volatilidad):")
for i, symbol in enumerate(selected_stocks):
    print(f"  {symbol}: {w_optimal[i]:.4f} ({w_optimal[i]*100:.2f}%)")

Funciones auxiliares para la Frontera Eficiente

Python — cálculo y graficado de la Frontera Eficiente
import pandas as pd
import matplotlib.pyplot as plt

def portfolio_return(weights, returns):
    """Retorno esperado del portafolio: w^T · μ"""
    return weights.T @ returns

def portfolio_vol(weights, covmat):
    """Volatilidad del portafolio: √(w^T · Σ · w)"""
    return (weights.T @ covmat @ weights) ** 0.5

def minimize_vol(target_return, er, cov):
    """Encuentra los pesos que minimizan volatilidad para un retorno objetivo."""
    n = er.shape[0]
    init_guess = np.repeat(1/n, n)
    bounds = ((0.0, 1.0),) * n
    constraints = (
        {'type': 'eq', 'fun': lambda w: np.sum(w) - 1},
        {'type': 'eq', 'args': (er,),
         'fun': lambda w, er: target_return - portfolio_return(w, er)}
    )
    result = minimize(portfolio_vol, init_guess, args=(cov,),
                      method='SLSQP', constraints=constraints,
                      bounds=bounds, options={'disp': False})
    return result.x

def plot_ef(n_points, er, cov):
    """Traza la Frontera Eficiente con n_points portafolios óptimos."""
    min_ret = max(0, er.min())
    target_rs = np.linspace(min_ret, er.max(), n_points)
    weights   = [minimize_vol(tr, er, cov) for tr in target_rs]
    rets = [portfolio_return(w, er) for w in weights]
    vols = [portfolio_vol(w, cov)   for w in weights]

    ef = pd.DataFrame({"Returns": rets, "Volatility": vols})
    return rets, ef.plot.line(x="Volatility", y="Returns", style='.-')

# Graficar la Frontera Eficiente
er_array  = expected_returns_.values
rets, ax  = plot_ef(100, er_array, cov_matrix)

# Calcular y mostrar el portafolio óptimo
retorno_optimo = portfolio_return(w_optimal, er_array)
riesgo_optimo  = portfolio_vol(w_optimal, cov_matrix)

ax.scatter(riesgo_optimo, retorno_optimo,
           color='red', marker='*', s=200, label='Portafolio Óptimo', zorder=5)
ax.set_xlabel('Volatilidad (Desviación Estándar)')
ax.set_ylabel('Rendimiento Esperado')
ax.set_title('Frontera Eficiente — Teoría de Markowitz')
ax.legend()

print(f"\nPortafolio óptimo:")
print(f"  Retorno esperado: {retorno_optimo:.4f} ({retorno_optimo*100:.2f}%)")
print(f"  Volatilidad:      {riesgo_optimo:.4f} ({riesgo_optimo*100:.2f}%)")
plt.show()

Métricas y resultadosEvaluación

La evaluación se realiza en dos niveles: la calidad de las predicciones LSTM (RMSE por acción) y las métricas financieras del portafolio resultante (retorno esperado, volatilidad y ratio de Sharpe implícito).

Calidad de las predicciones LSTM

Python — tabla de errores por acción
import pandas as pd

# Resumen de errores RMSE por acción
error_df = pd.DataFrame({
    'Acción': list(errors.keys()),
    'RMSE (USD)': [round(v, 4) for v in errors.values()]
}).sort_values('RMSE (USD)')

print(error_df.to_string(index=False))

Tabla comparativa de resultados del portafolio

Componente Descripción Resultado típico Observaciones
LSTM — Training Loss Error cuadrático medio en entrenamiento 0.143 → 0.029 Descenso consistente en 50 épocas
LSTM — Validation Loss Error cuadrático medio en validación 0.154 → 0.047 Fluctuaciones normales; sin sobreajuste severo
CAPM — Acciones seleccionadas Filtrado por costo de equity vs. exceso de mercado 4–7 de 10 Varía según el período y condiciones de mercado
Portafolio — Retorno esperado wT · μ sobre acciones seleccionadas Depende de predicciones LSTM Basado en cambio % de precio predicho en 30 días
Portafolio — Volatilidad óptima √(wT · Σ · w) en el portafolio de mínima vol. Mínimo de la Frontera Eficiente Pesos no negativos — sin posiciones cortas

Visualización del portafolio óptimo

Python — gráfico de pesos del portafolio óptimo
import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Gráfico de barras: pesos por acción
axes[0].bar(selected_stocks, w_optimal * 100, color='steelblue', edgecolor='white')
axes[0].set_title('Asignación del Portafolio Óptimo')
axes[0].set_ylabel('Peso (%)')
axes[0].set_xlabel('Acción')
axes[0].tick_params(axis='x', rotation=45)

# Gráfico de torta: distribución del portafolio
axes[1].pie(w_optimal, labels=selected_stocks, autopct='%1.1f%%',
            startangle=90, pctdistance=0.85)
axes[1].set_title('Distribución del Portafolio')

plt.tight_layout()
plt.show()

Conclusiones y trabajo futuroHallazgos

Hallazgos principales

Hallazgo 1

LSTM captura patrones de corto plazo con precisión razonable

La pérdida de validación desciende de 0.154 a 0.047 en 50 épocas sin sobreajuste significativo. La diferencia entre training loss y validation loss se mantiene pequeña, lo que indica que la arquitectura de tres capas LSTM con Dropout tiene capacidad de generalización adecuada para horizontes de 30 días. Los indicadores técnicos (especialmente RSI y Bandas de Bollinger) enriquecen el poder predictivo respecto a usar solo el precio de cierre.

Hallazgo 2

El CAPM elimina activos con riesgo sistemático no compensado

El filtro CAPM reduce el universo de 10 a 4–7 acciones según las condiciones de mercado. Las acciones con beta muy alto (como TSLA o NVDA en períodos de alta volatilidad) pueden ser excluidas si su exceso de retorno predicho no justifica el riesgo que añaden. Esto protege la cartera de concentrar peso en activos altamente volátiles cuya prima de riesgo no está siendo compensada.

Hallazgo 3

Markowitz concentra el peso en activos de baja covarianza

El optimizador de mínima volatilidad tiende a asignar pesos altos a pares de acciones con baja correlación entre sí — por ejemplo, una acción tecnológica (MSFT o GOOGL) junto con una financiera o defensiva (JPM o JNJ). Esta diversificación matemática es la ventaja central de Markowitz: no se trata de elegir los mejores activos individualmente, sino los que combinados reducen el riesgo global.

Hallazgo 4

Usar predicciones LSTM como rendimientos esperados mejora la relevancia del modelo

El enfoque clásico de Markowitz usa medias históricas de rendimientos como estimadores de retornos esperados futuros — lo cual asume que el pasado predice el futuro directamente. Sustituir esas medias históricas por las predicciones del modelo LSTM introduce una estimación dinámica que reacciona a condiciones recientes del mercado, lo que puede generar portafolios más adaptados al contexto actual.

Limitaciones y trabajo futuro

  • El modelo LSTM predice solo precio, no distribución de probabilidad — una extensión con predicción de intervalo de confianza (Bayesian LSTM o Monte Carlo Dropout) cuantificaría la incertidumbre de los rendimientos esperados.
  • El horizonte de predicción es fijo en 30 días. Una arquitectura multi-step (predicción de múltiples pasos futuros de forma directa) permitiría ajustar el horizonte del portafolio de manera más flexible.
  • No se incorpora rebalanceo periódico del portafolio. En producción, el pipeline debería ejecutarse periódicamente (mensual o trimestralmente) para actualizar predicciones LSTM y pesos óptimos.
  • El universo se limita a 10 acciones. Ampliar a un índice completo (S&P 500) requeriría técnicas de clustering para agrupar acciones y entrenar modelos por cluster, reduciendo el costo computacional.
  • SHAP aplicado sobre el modelo LSTM permitiría explicar qué indicadores técnicos contribuyen más a cada predicción individual, mejorando la interpretabilidad para el analista.
¿Cómo adaptar este proyecto a tu tesis? El pipeline completo es replicable con cualquier universo de acciones disponible en Yahoo Finance. Para una tesis de ingeniería financiera, la contribución original puede ser la comparación empírica entre rendimientos esperados basados en LSTM vs. medias históricas como entrada del modelo de Markowitz, evaluando cuál genera un portafolio con mejor ratio de Sharpe out-of-sample durante un período de backtesting de 12 meses.