Quant500 Academy

6. Case study 1: a neural network for one stock

Level 11: Neural Networks for Trading

Let us build the complete strategy: download prices, construct signals, train a network and see what it would have returned.

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']]

We prepare the input variables the network will use to predict. We define: high minus low, close minus open, 3-, 10- and 30-day moving averages, the 5-day standard deviation, the RSI and 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()

Notice the shift(1) on the moving averages: it makes sure today's average does not include today's close. That said, Std_dev, RSI and Williams %R do not carry that shift, and therefore use the current day's close. If your intention is to decide the trade at today's close about tomorrow's close, that is acceptable; if you meant to decide at the open, you have a leak. This is exactly the kind of detail that separates an honest backtest from an inflated one, and it has to be reviewed column by column.

The target variable is the price rise: it equals 1 when tomorrow's close is higher than today's.

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)

Scaling is compulsory here: the moving averages are in the hundreds and the RSI runs from 0 to 100, so without standardizing, the sigmoids would saturate immediately. And note that the scaler is fitted only on the training data and then applied to the test data: using fit_transform on the whole dataset would leak the mean and standard deviation of the future back into the past.

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)

What each argument means:

The output layer uses a sigmoid precisely because we want the prediction to be a probability that the market goes up.

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='Market')
plt.plot(trade_df['Cumulative Strategy Returns'], color='g', label='Strategy')
plt.legend()
plt.show()

The trading rule is to always be in the market: long if the probability exceeds 0.5, short otherwise. Since the returns are logarithmic, adding them cumulatively is correct. What appears nowhere at all are commissions and slippage, and with a signal that can flip from long to short every single day, that is not a minor detail.

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.