Quant500 Academy

6. Caso práctico 1: red neuronal para una acción

Nivel 11: Redes Neuronales para Trading

Vamos a montar la estrategia completa: descargar precios, construir señales, entrenar una red y ver qué habría rentado.

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import pandas_ta as pta
from sklearn.preprocessing import StandardScaler

df = yf.download('TSLA', start='2018-01-01', end='2023-01-01')
df = df.dropna()
df = df[['Open', 'High', 'Low', 'Close']]

Preparamos las variables de entrada que usará la red para predecir. Definimos: precio máximo menos mínimo, apertura menos cierre, medias móviles de 3, 10 y 30 días, desviación típica de 5 días, el RSI y el Williams %R.

df['O-C'] = df.Open - df.Close
df['H-L'] = df.High - df.Low
df['3day MA'] = df.Close.shift(1).rolling(window=3).mean()
df['10day MA'] = df.Close.shift(1).rolling(window=10).mean()
df['30day MA'] = df.Close.shift(1).rolling(window=30).mean()
df['Std_dev'] = df.Close.rolling(5).std()
df['RSI'] = pta.rsi(df.Close, length=9)
df['Williams %R'] = pta.willr(df.High, df.Low, df.Close, length=7)
df = df.dropna()

Fíjate en el shift(1) de las medias móviles: sirve para que la media del día de hoy no incluya el cierre de hoy. Ahora bien, Std_dev, RSI y Williams %R no llevan ese shift, y por tanto usan el cierre del propio día. Si tu intención es decidir la operación al cierre de hoy sobre el cierre de mañana, eso es admisible; si querías decidir en la apertura, tienes fuga de información. Es exactamente el tipo de detalle que separa un backtest honesto de uno inflado, y hay que revisarlo columna por columna.

La variable objetivo es la subida de precio: vale 1 cuando el cierre de mañana es mayor que el de hoy.

df['Price_Rise'] = np.where(df.Close.shift(-1) > df.Close, 1, 0)
df = df.dropna()

X = df.iloc[:, 4:-1]
y = df.iloc[:, -1]

split = int(len(df) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)

El escalado es obligatorio aquí: las medias móviles valen cientos y el RSI va de 0 a 100, así que sin estandarizar las sigmoides saturarían de inmediato. Y observa que el escalador se ajusta solo con el entrenamiento y luego se aplica al test: usar fit_transform sobre el conjunto completo filtraría la media y la desviación del futuro hacia el pasado.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout

classifier = Sequential()
classifier.add(Dense(units=32, kernel_initializer='uniform',
                     activation='relu', input_dim=X.shape[1]))
classifier.add(Dense(units=32, kernel_initializer='uniform', activation='relu'))
classifier.add(Dense(units=1, kernel_initializer='uniform', activation='sigmoid'))
classifier.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model = classifier.fit(X_train, y_train, batch_size=5, epochs=10)

Qué significa cada argumento:

La capa de salida usa sigmoide precisamente porque queremos que la predicción sea una probabilidad de que el mercado suba.

y_pred = classifier.predict(X_test)
y_pred = (y_pred > 0.5)

df['y_pred'] = np.nan
df.iloc[(len(df) - len(y_pred)):, -1:] = y_pred.astype(np.float64)

trade_df = df.dropna().copy()
trade_df['Tomorrows Returns'] = np.log(trade_df['Close'] / trade_df['Close'].shift(1))
trade_df['Tomorrows Returns'] = trade_df['Tomorrows Returns'].shift(-1)

trade_df['Strategy Returns'] = np.where(trade_df['y_pred'] == True,
                                        trade_df['Tomorrows Returns'],
                                        -trade_df['Tomorrows Returns'])

trade_df['Cumulative Market Returns'] = np.cumsum(trade_df['Tomorrows Returns'])
trade_df['Cumulative Strategy Returns'] = np.cumsum(trade_df['Strategy Returns'])

plt.figure(figsize=(10, 5))
plt.plot(trade_df['Cumulative Market Returns'], color='r', label='Mercado')
plt.plot(trade_df['Cumulative Strategy Returns'], color='g', label='Estrategia')
plt.legend()
plt.show()

La regla de trading es siempre estar dentro: largo si la probabilidad supera 0.5, corto si no. Como los retornos son logarítmicos, sumarlos acumulados es correcto. Lo que no aparece por ninguna parte son las comisiones ni el deslizamiento, y con una señal que puede cambiar de largo a corto cada día eso no es un detalle menor.

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.