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() * 100KNN 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.