Quant500 Academy

7. Casos prácticos 2 y 3: SVM para regímenes y estrategia con KNN

Nivel 11: Redes Neuronales para Trading

Caso 2: máquinas de vectores soporte para predecir el régimen y la tendencia. El plan es este: primero, crear un algoritmo no supervisado que prediga los regímenes de mercado; segundo, representarlos gráficamente para visualizarlos; tercero, entrenar un clasificador de vectores soporte usando el régimen como una de las características; cuarto, usar ese clasificador para predecir la tendencia del día en la apertura del mercado; y quinto, visualizar el rendimiento de la estrategia sobre los datos de test.

import numpy as np
import pandas as pd
import yfinance as yf
import seaborn as sns
import matplotlib.pyplot as plt
import talib as ta
from sklearn import mixture as mix
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

df = yf.download('SPY', start='2000-01-01', end='2012-01-01')
df = df.dropna()[['Open', 'High', 'Low', 'Close']]

n = 10           # ventana de 10 dias
t = 0.8          # 80% entrenamiento, 20% test
split = int(t * len(df))

Desplazamos High, Low y Close un día para acceder solo a datos pasados. Después construimos indicadores técnicos: RSI, media móvil simple, ADX, correlación, SAR parabólico y el retorno del día anterior calculado de apertura a apertura.

df['high'] = df['High'].shift(1)
df['low'] = df['Low'].shift(1)
df['close'] = df['Close'].shift(1)
df['RSI'] = ta.RSI(np.array(df['close']), timeperiod=n)
df['SMA'] = df['close'].rolling(window=n).mean()
df['Corr'] = df['SMA'].rolling(window=n).corr(df['close'])
df['SAR'] = ta.SAR(np.array(df['high']), np.array(df['low']), 0.2, 0.2)
df['ADX'] = ta.ADX(np.array(df['high']), np.array(df['low']),
                   np.array(df['close']), timeperiod=n)
df.loc[df.Corr > 1, 'Corr'] = 1
df.loc[df.Corr < -1, 'Corr'] = -1
df['Return'] = np.log(df['Open'] / df['Open'].shift(1))
df = df.dropna()

Ahora el algoritmo no supervisado. Una mezcla de gaussianas divide el mercado en 4 regímenes según un criterio de su propia elección: no le damos ningún conjunto etiquetado. Nótese que se ajusta solo con los datos de entrenamiento y se predice sobre el tramo posterior.

ss = StandardScaler()
unsup = mix.GaussianMixture(n_components=4, covariance_type='spherical',
                            n_init=100, random_state=42)
df = df.drop(['High', 'Low', 'Close'], axis=1)
unsup.fit(np.reshape(ss.fit_transform(df[:split]), (-1, df.shape[1])))
regime = unsup.predict(np.reshape(ss.transform(df[split:]), (-1, df.shape[1])))

Regimes = (pd.DataFrame(regime, columns=['Regime'], index=df[split:].index)
           .join(df[split:], how='inner')
           .assign(market_cu_return=df[split:].Return.cumsum())
           .reset_index(drop=False).rename(columns={'index': 'Date'}))

for i in [0, 1, 2, 3]:
    print('Media del regimen %i: ' % i, unsup.means_[i][0])
    print('Covarianza del regimen %i: ' % i, unsup.covariances_[i])

Sin mirar siquiera los factores con los que se hizo la clasificación, del gráfico se pueden concluir cosas: hay una zona de baja volatilidad o lateral, una zona de alta volatilidad o pánico, y una zona de ruptura. Los datos se interpretan así: el régimen 0 tiene media baja y covarianza alta; el 1, media alta y covarianza alta; el 2, media alta y covarianza baja; el 3, media baja y covarianza baja.

Escalamos el DataFrame de regímenes excluyendo las columnas Date y Regime, sin perder ninguna característica, y creamos la columna de señal que el algoritmo aprenderá a predecir.

ss1 = StandardScaler()
columns = Regimes.columns.drop(['Regime', 'Date'])
Regimes['Signal'] = 0
Regimes.loc[Regimes['Return'] > 0, 'Signal'] = 1
Regimes.loc[Regimes['Return'] < 0, 'Signal'] = -1
Regimes['return'] = Regimes['Return'].shift(1)
Regimes = Regimes.dropna()

cls = SVC(C=1.0, kernel='rbf', gamma='auto', degree=3, tol=0.001,
          decision_function_shape='ovr', shrinking=True)

split2 = int(0.8 * len(Regimes))

# el escalador se ajusta SOLO con el tramo de entrenamiento y despues se aplica a todo:
# ajustarlo con el conjunto entero le filtraria al test la media y la desviacion del futuro
ss1.fit(Regimes[columns].iloc[:split2])
Regimes[columns] = ss1.transform(Regimes[columns])

X = Regimes.drop(['Signal', 'Return', 'market_cu_return', 'Date'], axis=1)
y = Regimes['Signal']
cls.fit(X[:split2], y[:split2])

p_data = len(X) - split2
df['Pred_Signal'] = 0
df.iloc[-p_data:, df.columns.get_loc('Pred_Signal')] = cls.predict(X[split2:])
df['str_ret'] = df['Pred_Signal'] * df['Return'].shift(-1)

El núcleo 'rbf' es el que hace no lineal al SVM: proyecta implícitamente los datos a un espacio de dimensión muy alta donde una frontera lineal separa clases que en el espacio original no eran separables. C controla el compromiso entre margen amplio y errores de clasificación.

Caso 3: estrategia de trading con KNN. El vecino más cercano es el modelo más simple posible y sirve de referencia contra la que medir todo lo anterior.

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

df = yf.download('SPY', start='2012-01-01', end='2021-01-01').dropna()
df = df[['Open', 'High', 'Low', 'Close']]

df['Open-Close'] = df.Open - df.Close
df['High-Low'] = df.High - df.Low
df = df.dropna()
X = df[['Open-Close', 'High-Low']]
Y = np.where(df['Close'].shift(-1) > df['Close'], 1, -1)

split = int(0.7 * len(df))
X_train, X_test = X[:split], X[split:]
Y_train, Y_test = Y[:split], Y[split:]

knn = KNeighborsClassifier(n_neighbors=15)
knn.fit(X_train, Y_train)
print('Exactitud en train: %.2f' % accuracy_score(Y_train, knn.predict(X_train)))
print('Exactitud en test:  %.2f' % accuracy_score(Y_test, knn.predict(X_test)))

df['Predicted_Signal'] = knn.predict(X)
df['SPY_returns'] = np.log(df['Close'] / df['Close'].shift(1))
df['Strategy_returns'] = df['SPY_returns'] * df['Predicted_Signal'].shift(1)
Cumulative_SPY = df[split:]['SPY_returns'].cumsum() * 100
Cumulative_Strategy = df[split:]['Strategy_returns'].cumsum() * 100

KNN no aprende parámetros: guarda los datos y clasifica cada día nuevo por votación de sus 15 vecinos más próximos. Un k pequeño memoriza el ruido; uno grande suaviza hasta perder la señal. Y presta atención al shift(1) sobre Predicted_Signal: la señal de ayer se aplica al retorno de hoy, que es lo único ejecutable en la práctica.

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.