(this is part 5 of the “AI for Finance Course“)
Now we can apply all we’ve learned so far and train a single neuron (perceptron) to predict real market (Bitcoin) price.
The most simple thing we can do is to use previous candle to predict the next candle. And we’re not going to complicate much with the exact price. We’re just going to try to predict whether the next candle is RED or GREEN.
The output for RED candle is going to be 0 and the output for GREEN is going to be 1.
Preparing The Inputs
First we need to transform the candle into something manageable. Something that the neuron will be able to easily digest.
One idea (which is not the only one, nor best one, perhaps not even good one) is to capture candle high, low, and close as relevant inputs.
The thinking is that this indicates a market movement and might tell us something about what’s going to happen to the next candle.
Entire candle is defined by the relative movement compared to the open price. Here I took the Bitcoin price around 50k for ease of calculation (who knows what’s the price today, could be 5k could be 500k)
INPUT1 = HIGH / OPEN
INPUT2 = LOW / OPEN
INPUT3 = CLOSE / OPEN
And since each individual candle doesn’t move much (especially the minute candles I’m going to use) the input values will be nicely distributed around the value of 1.
EVEN BETTER…
We can distribute it around the value of zero:
INPUT1 = (HIGH - OPEN) / OPEN
INPUT2 = (LOW - OPEN) / OPEN
INPUT3 = (CLOSE - OPEN) / OPEN
INPUT1 = +0.02
INPUT2 = -0.01
INPUT3 = +0.01
Putting This Into Code
As you will see, the code didn’t change much from our previous lesson. The only real difference is now we have 3 inputs (4 if you count the constant input=1) and 4 weights.
double[] getInputs(Candle c) { double[] inputs = [c.high, c.low, c.close]; inputs[] -= c.open; inputs[] /= c.open; return inputs; }
However…
I am using the algo-bot starter code that downloads the data automatically and generates candles for me.
You do NOT NEED this starter code in order to follow along with this course.
But, I will be using it because it allows me to easily…
- get & save the data
- load the data in any candle format
- (from 1-min to 7-day candles)
- make trades and backtest the bot
- deploy the bot live with a single line of code
…which is why I developed it in the first place.
And you can go through a quick starter code tutorial on this link if you want (for better understanding).
But most of the code and functions should be very familiar to you…
module bbot.bot008; import std.stdio : pp = writeln; import std.algorithm : map, sum; import std.algorithm.comparison : min; import std.array : array; import std.algorithm.searching : minElement; import std.range : repeat, take; import std.math.trigonometry : tanh; import std.conv : to; import std.random : Random, choice; import std.range : iota; import c = blib.constants; import bbot.badgerbot : AbstractBadger; import bmodel.chart : Chart; import bmodel.candle : Candle; import bmodel.order : Order; import bmodel.enums : Interval, OrderType, Direction, Action, Color; import blib.utils : getStringFromTimestamp; class BadgerBot : AbstractBadger { override void setup() { this.TRADE_LIVE = false; // ATTENTION !!! // ignored when running LIVE bot (or pulling real values) this.capital = 100_000; this.transactionFee = 0.001; // 0.1% -> approx like this on Binance this.backtestPeriod = ["2024-01-01T00:00:00", "2024-04-30T00:00:00"]; // semi-open interval: [> // backtestPeriod ignored on LIVE - calculated from pastCandlesCount, candleInterval (and current timestamp) // applied always this.candleInterval = Interval._1M; this.pastCandlesCount = 1000; // example: set to 50 for 50-candle moving average (limit 1000 candles total for LIVE - for simplicity) this.tickers = [c.BTCUSDT]; // ETHUSDT //this.chart = Chart(450, 200); // comment out when you want to visualize like on LIVE but backtest will (intentionally) run slower // this.positions; // will be autofilled // this.candles; // will be autofilled super.setup(); // DON'T REMOVE } // SINGLE NEURON MARKET PREDICTIONS bool hasOpenPosition() { return this.positions.length != 0 && this.positions[$-1].action == Action.OPEN; } int trades = 0; bool isAIrained = false; auto rand = Random(1); double[] backupWeights1 = []; // 1st neuron, 1st layer double[] weights1Layer1 = [0.5, 0.5, 0.5, 0.5]; double sigmoid(double input) { return (tanh(input) + 1) / 2.0; } double[] getInputs(Candle c) { double[] inputs = [c.high, c.low, c.close]; inputs[] -= c.open; inputs[] /= c.open; return inputs; } double getCorrectOutput(Candle c) { if (c.color == Color.GREEN) return 1.0; return 0.0; } Color getPredictedColor(double prediction) { return (prediction > 0.5) ? Color.GREEN : Color.RED; } double getPrediction(double[] inputs, double[] weights) { double[] weightedInputs = [1.0] ~ inputs; weightedInputs[] *= weights[]; double weightedSum = weightedInputs.array.sum; double prediction = sigmoid(weightedSum); return prediction; } void backupWeights() { backupWeights1 = weights1Layer1.dup; } void restoreWeights() { weights1Layer1 = backupWeights1.dup; } void updateRandomWeight() { double[] selectedWeights = [weights1Layer1 ].choice(rand); int index = to!int(selectedWeights.length).iota.choice(rand); double value = [-0.01, +0.01].choice(rand); backupWeights(); selectedWeights[index] += value; } void trainNetwork(Candle[] trainingCandles) { double maxError = trainingCandles.length; for (int n = 0; n < 1000; n++) { // 1000 times through the data double allDataError = 0; string printing = ""; updateRandomWeight(); for (int i = 0; i < trainingCandles.length - 1; i++) { double[] inputs = getInputs(trainingCandles[i]); double prediction1Layer1 = getPrediction(inputs, weights1Layer1); double output = getCorrectOutput(trainingCandles[i+1]); double error1Layer1 = prediction1Layer1 - output; allDataError += error1Layer1 * error1Layer1; //printing ~= to!string(inputs) ~ " : " ~ to!string(prediction1Layer1) ~ " --> " ~to!string(output) ~ "\n"; } if (allDataError < maxError) { maxError = allDataError; pp(maxError, " : ", weights1Layer1); //pp(printing); } else { restoreWeights(); } } pp(); pp("final weights:"); pp(weights1Layer1); pp(); pp("TRAINING COMPLETE"); pp(); } override bool trade() { if(!super.trade()) return false; // ALL CODE HERE - BEGIN // if (!isAIrained) { // training on 1000 candles prior to 2024-01-01 // see ==> this.pastCandlesCount trainNetwork(this.candles[c.BTCUSDT][0..$-1]); isAIrained = true; } // stops the bot after one trade (testing purposes) // if (trades >= 1 && !hasOpenPosition()) { // return false; // stop the bot // } Candle[] btcCandles = this.candles[c.BTCUSDT]; double currentPrice = btcCandles[$-1].close; Candle lastCandle = btcCandles[$-2]; double[] inputs = getInputs(lastCandle); double prediction1Layer1 = getPrediction(inputs, weights1Layer1); Color predictedNextCandleColor = getPredictedColor(prediction1Layer1); if ( predictedNextCandleColor == Color.GREEN && !hasOpenPosition() ) { Order marketBuy = Order( c.BTCUSDT, OrderType.MARKET, Direction.BUY, currentPrice * 1.003, // target price = +0.3% currentPrice * 0.995, // stop price = -0.5% ); this.order(marketBuy); trades++; } // ALL CODE HERE - END // return true; } }
Code Explanation
As I mentioned, most of the code should look familiar since it’s the same one we used in previous lesson.
However, we downsized the number of neurons back to one. But we’re still using Monte Carlo weight guessing method.
The only new code is the one in setup() function, but you can find the explanation for that code in the quick walkthrough of the algo-bot starter code.
(but mostly this function contains our simulation definitions / parameters)
The trade() function is the one that loads new data. (minute-by-minute) and from there I can simply make orders.
In this case, when our “AI” predicts next candle is going to be GREEN, the bot makes a market buy and I also specify:
- +0.3% target price
- -0.5% stop price
Now I know this doesn’t make much sense because even if the next candle is GREEN, the overall price might fall and hit the stop loss.
And this is something our neuron was NOT trained for to predict.
I KNOW.
But for now we’re just making small steps so we can have bitesized lessons that are easy to grasp.
In one of the future lessons we’re going to address this issue and make our bot better and better.
Also this code…
// stops the bot after one trade (testing purposes) // if (trades >= 1 && !hasOpenPosition()) { // return false; // stop the bot // }
…allows me to run one trade at a time for debugging purposes and visually inspecting whether everything works properly or not:
Bot Performance
The bot performed remarkably well. In the period of full 4 months…
this.backtestPeriod = [ "2024-01-01T00:00:00", "2024-04-30T00:00:00" ];
…it made 395 trades and “only” lost 55% of the account.
---------------------------- | PERFORMANCE | ---------------------------- - Initial capital: 100000.00 - Final capital::: 44183.90 - Total trades:::: 395 - ROI:::::: -55.82 % - Min. ROI: -55.95 % - Max. ROI: 0.20 % - Drawdown: -56.04 % - Pushup::: 1.00 % ----------------------------
IN WHAT WORLD IS THAT GOOD?
First of all, I’m comparing this bot to popular strategies that many people swear by. I tested those strategies and made posts about them:
- buy and hold strategy
- local minimum (buy low) strategy
- price percentile strategy
- moving-average crossover
- candle (card) counting strategy
- trend following strategy 👈
(last one has surprising results)
REMEMBER: JUST ONE NEURON
On top of that, this bot only uses a single neuron and very limited data. So I’m very excited to see how will it perform in the next lessons as we build this Badger bot up.
TRANSACTION FEES
As part of this backtest, a transaction fee (0.1%) is included for every buy or sell. This imitates current rates on Binance.
(since this is where I’m going to deploy my bot eventually)
// 0.1% transaction fee this.transactionFee = 0.001; // ZERO transaction fee this.transactionFee = 0.000;
However… I always like to see how the bot would perform if it wasn’t encumbered with the transaction fees… and these are the results:
---------------------------- | PERFORMANCE | ---------------------------- - Initial capital: 100000.00 - Final capital::: 97434.52 - Total trades:::: 395 - ROI:::::: -2.57 % - Min. ROI: -6.08 % - Max. ROI: 1.30 % - Drawdown: -7.28 % - Pushup::: 7.54 % ----------------------------
Overall, the bot is almost not losing money at all. So it’s very good considering the simplicity of the bot.
Why Binance & crypto?
Because this is the only logical solution when dealing with trading bot that is expected to have many trades per day.
Due to legal requirements when dealing with stocks and options (one of them being minimum balance of $25.000), crypto is the best trading avenue for beginners.
In case you’re wondering what happens if you don’t have $25k – then you are limited to only 3 trades per week (of the same asset).
Options have additional problem of trades clearing the next trading day.
So using a trading bot for stocks and options (for a beginner with smaller account) would be beyond pointless.
NEXT LESSON:
Neural Network Market Predictions