Quant500 Academy

7. Case studies 2 and 3: SVM for regimes and a KNN strategy

Level 11: Neural Networks for Trading

Case 2: support vector machines to predict regime and trend. The plan is this: first, create an unsupervised algorithm that predicts market regimes; second, plot them to visualize them; third, train a support vector classifier using the regime as one of the features; fourth, use that classifier to predict the day's trend at the market open; and fifth, visualize the strategy's performance on the test data.

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           # 10-day window
t = 0.8          # 80% training, 20% test
split = int(t * len(df))

We shift High, Low and Close by one day so as to access past data only. We then build technical indicators: RSI, simple moving average, ADX, correlation, parabolic SAR and the previous day's return computed open to open.

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()

Now the unsupervised algorithm. A Gaussian mixture splits the market into 4 regimes according to a criterion of its own choosing: we give it no labeled set whatsoever. Note that it is fitted on the training data only and predicts on the later stretch.

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('Regime %i mean: ' % i, unsup.means_[i][0])
    print('Regime %i covariance: ' % i, unsup.covariances_[i])

Without even looking at the factors used for the classification, some things can be concluded from the chart: there is a low-volatility or sideways zone, a high-volatility or panic zone, and a breakout zone. The data are read as follows: regime 0 has a low mean and high covariance; regime 1, a high mean and high covariance; regime 2, a high mean and low covariance; regime 3, a low mean and low covariance.

We scale the regimes DataFrame excluding the Date and Regime columns, without losing any feature, and we create the signal column that the algorithm will learn to predict.

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

# the scaler is fitted ONLY on the training slice and then applied to everything:
# fitting it on the whole set would leak the future mean and standard deviation into the test
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)

The 'rbf' kernel is what makes the SVM non-linear: it implicitly projects the data into a very high-dimensional space where a linear boundary separates classes that were not separable in the original space. C controls the trade-off between a wide margin and classification errors.

Case 3: a trading strategy with KNN. The nearest neighbor is the simplest possible model and serves as the benchmark against which to measure everything above.

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('Train accuracy: %.2f' % accuracy_score(Y_train, knn.predict(X_train)))
print('Test accuracy:  %.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 learns no parameters: it stores the data and classifies each new day by a vote of its 15 closest neighbors. A small k memorizes the noise; a large one smooths until the signal is gone. And pay attention to the shift(1) on Predicted_Signal: yesterday's signal is applied to today's return, which is the only thing you can actually execute.

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.