Preprocessing and Backtesting For Your Neural Net Trading Bot

Is all the hype around using deep learning for algorithmic trading justified? In this article, we at lemon.markets try it out ourselves to find out.

Eric Tang
lemon.markets
10 min readNov 1, 2022

--

You’ve probably heard of deep learning in the context of image recognition/generation and natural language processing. Prime examples of each: DALL-E and GPT-3. Outside of these, another field where interest in deep learning solutions is emerging is finance.

In this article, we at lemon.markets (a Berlin-based fintech start-up offering a trading and market data API), will try and build a few simple neural networks with TensorFlow and run them in the stock market. More specifically, we’ll focus on how you can obtain, store, and prepare historical stock data for your own projects and also backtest your automated strategy with the backtesting.py Python package.

As always, the projects we build are meant to be examples for learning and should not be deployed as final products to trade with real money. Unless, of course, you really enjoy gambling with random number generators (which initialize the values of the neural network before training) and have an incredible amount of trust in us. 🎲🤔

Throughout the article I will be displaying some key functions with GitHub gists and going through them line by line. However, if you wish to see the code for yourself, check out our GitHub repo here!

What is Deep Learning? 🤷

If you aren’t familiar with deep learning or machine learning in general, don’t worry. Check out our article: “Dummies Guide to Machine Learning in Trading” to learn more. It even has an accompanying YouTube video!

To put it briefly, however, deep learning is simply machine learning with a neural network in which many individual “neuron-like” units perform mathematical operations on an input. By updating the weights and biases of these operations after going through countless training examples, the neurons learn to map the input to desired outputs. From a very high-level, this resembles a human brain learning through trial and error. 🤯 See this article or the Wikipedia for a more detailed breakdown of how deep learning works.

Neural Networks We’ll be Building 🥅🔨

Now that you know what deep learning is all about, let’s narrow down what we’ll be building for this article.

The first model we’ll build is a dense neural network, simply several layers of neurons which are “deeply connected” to the preceding layer, meaning every neuron in one layer is connected to every other neuron in its preceding layer. This is the most commonly used layer in neural networks and serves as a simple, easy-to-comprehend example.

The second model we’ll build also uses a few dense layers, but will also include a few LSTM (Long Short Term Memory) layers. LSTMs are a fancy type of Recurrent Neural Network (RNN). Their name comes from the fact that they attempt to simulate some notion of memory by including forget, input, and output gates which affect what is “remembered” from the previous cell state.

Image Credit: “LSTM : What’s the fuss about?” (an informative article on LSTMs)

Due to this, they are specially suited to working with time series data such as historical stock market prices in which current stock prices are the result of past stock prices. The advantage they have over regular, vanilla RNNs is that they attempt to solve the Vanishing Gradient Problem, an issue during back-propagation where the gradient we calculate to change the weights in each cell of the network becomes smaller and smaller until the model no longer learns. Simply put, without the jargon, as we repeatedly multiply the number we are updating our network by with values close to zero, the number quickly approaches zero such that we end up updating our network extremely slowly or not at all. For a more detailed explanation, you can also check out this article.

Having covered both models, let’s now dive into the code!

Getting Data and Preprocessing 📊📈📉

Let’s first grab some data to train our models. To do so, we make requests to the lemon.markets ‘ohlc’ endpoint. Setting up our client with the lemon.markets Python SDK, we grab hourly OHLC data for each day, then loop through to write it into the csv file. While for training we will only be using the close data, gathering everything now and formatting it this way will be helpful for backtesting later.

With our data all written into a csv, this saves us from having to make constant calls to the API for data throughout the rest of the project. We can also now read the data from the file and process it for our uses. Using get_data(), we read the csv and reformat the data into several numpy arrays to use for training our models.

Inside get_data:

[Line 11–12] Read the csv with pandas to generate a DataFrame.
[Line 14–18] Slice the DataFrame to get close prices and then split the close prices into training and testing data (80–20 split).
[Line 20–30] Create training inputs (x) and training outputs (y) numpy arrays. Inputs are arrays of length num_hours and outputs are a single value. Essentially, we will be using num_hours amount of closing prices to predict the closing price for the next hour.
[Line 32–42] Create testing inputs/outputs with correctly shaped matrices the same way as with training.
[Line 44] Create a DatFrame for testing that we will use for backtesting the strategy later.

Dense Model

With our data fully processed, we’re ready to start building our models. With the dense_model function we create the model with tf.keras.Sequential() and proceed to train and test it. As we are not planning anything crazy or fancy with this project, Sequential is sufficient for demo purposes. With more advanced projects, a Sequential model may be limiting, as each layer can only have one input and output.

Inside the Dense Model:

[Line 14–15] Reshape the training and testing input matrices so that they fit the model.
[Line 16–20] Create the model with tf.keras.Sequential, a type of model where each layer only has one input and output. For Sequential, we simply define individual layers in a list. In this case we’re using 'leaky-relu' as the activation function for our layers, again, because it helps with the Vanishing Gradient Problem.
[Line 21] Now we define the optimizer and type of loss we calculate with compile().
[Line 22] With fit() we train the model with our training input/output matrices using batch_size and epochs that we ourselves can specify. 🏋️
[Line 24–27] We can now make predictions with predict.🔮 These predictions can then be used to calculate the root mean square deviation (RMSE), telling us how far off our predictions are.
[Line 28] Return the trained model to use for backtesting and the predictions made here to use for plotting them against the actual values later.

Again, this is an extremely simple implementation of a neural network model. A better formatted model would have separate functions for calling, calculating loss, and calculating accuracy. The train and test functions would also ideally be separate.

LSTM Model

Before we check our results, let’s also build the LSTM model. Similar to the Dense model, we use Sequential to create a model which we train and test.

Inside the LSTM Model:

[Line 13–18] Again, create the model with tf.keras.Sequential. This time we set up a combination of LSTM and Dense layers to make a prediction.
[Line 19] Prints out a summary of the LSTM model — showing layers and trainable params.
[Line 20–22] As with above, compile the model and use fit to train it before making predictions.
[Line 24–25] Calculate the RMSE once more and print it out for some idea of the model’s predictive capabilities.
[Line 27] Just like the Dense model, we return the trained model, so that we can demo backtesting and also the predictions to plot them against the actual stock prices.

Validation ✅

Using our validation data, we can check if our model is learning. Let’s plot the predictions of our models against the actual OHLC data and see if they actually learned anything. 🧐

Dense and LSTM predications vs Actual Data, Y-axis: Stock Price, X-axis: Hours

From what we can see, this looks promising! However, it may also just be overfitting or attributable to look ahead bias. Not familiar with those terms? Check out the ‘Mistakes’ terms in our Dummies Guide to ML! Or check out these specific parts of our YouTube video explaining both: overfitting and look ahead bias. We should now check for mistakes in our code and tune our hyperparameters, so that our models end up learning more and earning more.

Backtesting 📉📈

Having confirmed our models are learning something, let’s now try some backtesting to see whether they can make money. To do so we can use backtesting.py, one of the many backtesting solutions out there. We have settled with using this package as our neural network bot is already written in Python, so integrating it with backtesting.py is fairly easy.

Once we from backtesting import Backtest, Strategy, we can then create a class LSTMNeuralNetTrader which uses the abstract class, Strategy, provided by backtesting.py:

Inside the LSTMNeuralNetTrader Class:

[Line 3–22] init functions for the class that set up the model.
[Line 24] The next() function is called after each candle during backtesting to determine buy/sell decisions.
[Line 25–30] We set up a conditional to make sure there are enough hours being used to make a prediction, then format the data into matrices whose shapes will work.
[Line 31–37] Using the nn_trader_decision() function we decide whether to buy or sell. Since backtesting.py permits shorting but lemon.markets currently does not offer it, I’ve gone ahead and also set up a conditional that makes sure to avoid shorting in line 37 (note that self.position.size will be negative if your position is short).
The current logic of our strategy is that we buy 2 shares whenever the neural net predicts the price to rise, then sell everything when it predicts a drop.

After you’ve created the class representing the logic behind your strategy, simply instantiate a Backtest. To do so you’ll need the DataFrame from preprocessing we set aside for backtesting, the Strategy class we made above, and to set the values for cash and commission fees you’ll be testing with. Afterwards, use bt.run() to run the backtest.

Plot of historical performance of the Dense Model.
Plot of historical performance of the LSTM Model.

Well, that’s not particularly profitable. While it appears both graphs are increasing, this doesn’t factor in trading fees. It’s also barely any gain for a portfolio that is worth €100k. It appears that these artificial brains still have a ways to go, and are only incurring a bunch of trading fees currently. However, let’s step away from the performance of our models and take a closer look at all the performance indicators backtesting.py gives us. From the HTML charts and visuals, we get to see when trades happened and the historical value of our portfolio.

Candle chart for LSTM Model Backtest. It is possible to zoom in on the candles in the HTML.

The output of bt.run() also provides common metrics like maximum drawdown, the Sharpe Ratio, number of trades, and our expected gain from simply buying and holding. The results of our LSTM model are shown below.

Start                     2021-08-06 06:00...
End 2021-10-13 08:00...
Duration 68 days 02:00:00
Exposure Time [%] 21.185185
Equity Final [$] 100182.2
Equity Peak [$] 100182.8
Return [%] 0.1822
Buy & Hold Return [%] -2.061192
Return (Ann.) [%] 0.0
Volatility (Ann.) [%] NaN
Sharpe Ratio 0.0
Sortino Ratio 0.0
Calmar Ratio 0.0
Max. Drawdown [%] -0.043789
Avg. Drawdown [%] -0.009085
Max. Drawdown Duration 47.0
Avg. Drawdown Duration 11.352941
# Trades 118.0
Win Rate [%] 81.355932
Best Trade [%] 2.060647
Worst Trade [%] -0.560718
Avg. Trade [%] 0.629714
Max. Trade Duration 23.0
Avg. Trade Duration 5.542373
Profit Factor 19.734933
Expectancy [%] 0.631437
SQN 11.640299
_strategy LSTMNeuralNetTrader

While we aren’t gaining or losing any money since it’s all paper, we’ve learned quite a bit about how to build a neural network strategy. From here, we can continue to tune the hyperparameters to boost performance or consider scrapping what we have and changing the architecture of our model if nothing changes.

Conclusions 📝

Judging from our results, it appears that making money with machine learning in the market is possible, but difficult. Nevertheless, there may still be potential here, as we did not tune our hyperparameters very much, nor go for more complex models that may have better fit the noisiness of the stock market. Furthermore, we hope this project has been a good learning opportunity especially for those of you inexperienced with backtesting, deep learning, or retrieving historical OHLC data. There are still endless possibilities out there to learn about and explore, so good luck with your own deep learning projects!

I hope you enjoyed this article. The goal was to explore the possibility of using neural networks to trade stocks, while introducing methods for preprocessing historical data and backtesting your algorithmic trading strategy along the way. Once more, if you want to play around with the code yourself, check out our GitHub repository here!

Don’t forget to sign up to lemon.markets to start building your own algorithmic trading project. If you have any questions, make sure to contact us via support@lemon.markets or join our Slack community.

We are looking forward to your projects with lemon.markets :)

🍋 Eric

--

--