(this is part 7 of the “AI for Finance Course“)
Some people believe candlestick patterns help predict market movements, so let’s test it and see if they’re correct.
Here are some patterns mentioned on wikipedia:
While these might hold value we’re not going to hardcode them. Instead, we’re going to let our neural net recognize them on its own.
Those patterns might include some or all listed on Wikipedia. But it may also include none.
The neural network might just learn brand new patterns specific to this asset and market conditions.
Key concepts
So far we passed a single candle to the network. But how do we pass multiple candles at once? And how do we tell the network which candle comes first and which one’s the last?
Short answer: we don’t.
We’ll let the network figure this out on its own.
INPUTS
Previously, we increased the number of neurons. Now we’re going to increase the number of inputs.
We had 3+1 inputs so far (3 being for 1 candle).
And now we’re going to have 9+1 to represent 3 candles (each with it’s own high, low, close price):
- IN 0: represents constant input (bias)
- IN 1-3: represent the last candle
- IN 4-6: represent 2nd last
- IN 7-9: represent 3rd last
Now let’s see how this looks like in code…
The code
First we need to change the getInputs() function to accept 3 candles instead of one. We can do this by sending an array of candles or by explicitly defining each parameter.
For simplicity (and ease of understanding) purposes I’ll set 3 separate function arguments:
double[] getInputs(Candle c1, Candle c2, Candle c3) { double[] inputs1 = [c1.high, c1.low, c1.close]; inputs1[] -= c1.open; inputs1[] /= c1.open; double[] inputs2 = [c2.high, c2.low, c2.close]; inputs1[] -= c2.open; inputs1[] /= c2.open; double[] inputs3 = [c3.high, c3.low, c3.close]; inputs1[] -= c3.open; inputs1[] /= c3.open; return inputs1 ~ inputs2 ~ inputs3; }
And in our for() loop we need to start from the third element (so we can pass the previous two to the getInputs() function):
for (int i = 2; i < trainingCandles.length - 1; i++) { double[] inputs = getInputs(trainingCandles[i], trainingCandles[i-1], trainingCandles[i-2]);
And also, we need to increase the number of weights in the first layer so our neurons can get the information from all 3 candles:
// 1st neuron, 1st layer double[] weights1Layer1 = repeat(0.5, 10); // 9+1 // 2nd neuron, 1st layer double[] weights2Layer1 = repeat(0.5, 10); // 9+1 // 1st neuron, 2nd layer double[] weights1Layer2 = repeat(0.5, 3); // 2+1
Also note: once you train the network, you can just print out the weights and replace these random ones. Also, you can train your network during backtest and then just copy the weights for LIVE trading.
Another thing…
Since we now have more weights (23) updating them one by one might be slow. Plus, they can get stuck because the changes are too small to produce favorable outcome.
Therefore I prefer to update a small sample of weights. Usually a square root of number of weights:
√23 ~ 4.8
So we’ll just make a small update to the function that does the updates:
void updateRandomWeights() { // [-3,+3] // square root of all weights (23) is ~ 4.8 // so I'm gonna update sample of 4 each time backupWeights(); for (int i = 0; i < 4; i++) { int idx = uniform(0, 23, rand); if (idx <= 9) { weights1Layer1[idx] = uniform01(rand)*6-3; } if (idx >= 10 && idx <= 19) { weights2Layer1[idx-10] = uniform01(rand)*6-3; } if (idx >= 20 && idx <= 22) { weights1Layer2[idx-20] = uniform01(rand)*6-3; } } }
You can notice that we’re choosing the weights randomly in the range of -3 to +3 which is enough to capture any kind of behavior. The output from the sigmoid function for the value of -3 is very close to zero and for the value of +3 is very close to one.
In short, these are all the changes you need. And you can easily extend this code to include even more neurons, layers, and inputs.
Here is the code in its entirety:
module bbot.bot010; 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, uniform01, uniform; import std.range : iota, repeat, generate, take; 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 } // CANDLE PATTERNS RECOGNITION bool hasOpenPosition() { return this.positions.length != 0 && this.positions[$-1].action == Action.OPEN; } bool isAIrained = false; auto rand = Random(1); double[] backupWeights1 = []; double[] backupWeights2 = []; double[] backupWeights3 = []; // 1st neuron, 1st layer double[] weights1Layer1 = repeat(0.5, 10).array; // 9+1 // 2nd neuron, 1st layer double[] weights2Layer1 = repeat(0.5, 10).array; // 9+1 // 1st neuron, 2nd layer double[] weights1Layer2 = repeat(0.5, 3).array; // 2+1 double sigmoid(double input) { return (tanh(input) + 1) / 2.0; } double[] getInputs(Candle c1, Candle c2, Candle c3) { double[] inputs1 = [c1.high, c1.low, c1.close]; inputs1[] -= c1.open; inputs1[] /= c1.open; double[] inputs2 = [c2.high, c2.low, c2.close]; inputs1[] -= c2.open; inputs1[] /= c2.open; double[] inputs3 = [c3.high, c3.low, c3.close]; inputs1[] -= c3.open; inputs1[] /= c3.open; return inputs1 ~ inputs2 ~ inputs3; } 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; backupWeights2 = weights2Layer1.dup; backupWeights3 = weights1Layer2.dup; } void restoreWeights() { weights1Layer1 = backupWeights1.dup; weights2Layer1 = backupWeights2.dup; weights1Layer2 = backupWeights3.dup; } void updateRandomWeights() { // [-3,+3] // square root of all weights (23) is ~ 4.8 // so I'm gonna update sample of 4 each time backupWeights(); for (int i = 0; i < 4; i++) { int idx = uniform(0, 23, rand); if (idx <= 9) { weights1Layer1[idx] = uniform01(rand)*6-3; } if (idx >= 10 && idx <= 19) { weights2Layer1[idx-10] = uniform01(rand)*6-3; } if (idx >= 20 && idx <= 22) { weights1Layer2[idx-20] = uniform01(rand)*6-3; } } } void trainNetwork(Candle[] trainingCandles) { double maxError = trainingCandles.length; for (int n = 0; n < 5000; n++) { // 5000 times through the data //pp(weights1Layer1); double allDataError = 0; string printing = ""; updateRandomWeights(); for (int i = 2; i < trainingCandles.length - 1; i++) { double[] inputs = getInputs( trainingCandles[i], trainingCandles[i-1], trainingCandles[i-2]); double prediction1Layer1 = getPrediction(inputs, weights1Layer1); double prediction2Layer1 = getPrediction(inputs, weights2Layer1); double prediction1Layer2 = getPrediction([prediction1Layer1, prediction2Layer1], weights1Layer2); double output = getCorrectOutput(trainingCandles[i+1]); double error1Layer1 = prediction1Layer2 - output; allDataError += error1Layer1 * error1Layer1; //printing ~= to!string(inputs) ~ " : " ~ to!string(prediction1Layer1) ~ " --> " ~to!string(output) ~ "\n"; } if (allDataError < maxError) { maxError = allDataError; pp(); pp("ERROR: ", maxError); pp(weights1Layer1); pp(weights2Layer1); pp(weights1Layer2); //pp(printing); } else { restoreWeights(); } //pp(weights1Layer1); } pp(); pp("final weights:"); pp(weights1Layer1); pp(weights2Layer1); pp(weights1Layer2); 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; } Candle[] btcCandles = this.candles[c.BTCUSDT]; double currentPrice = btcCandles[$-1].close; Candle lastCandle = btcCandles[$-2]; Candle secondCandle = btcCandles[$-3]; Candle thirdCandle = btcCandles[$-4]; double[] inputs = getInputs(lastCandle, secondCandle, thirdCandle); double prediction1Layer1 = getPrediction(inputs, weights1Layer1); double prediction2Layer1 = getPrediction(inputs, weights2Layer1); double prediction1Layer2 = getPrediction([prediction1Layer1, prediction2Layer1], weights1Layer2); Color predictedNextCandleColor = getPredictedColor(prediction1Layer2); 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); } // ALL CODE HERE - END // return true; } }
This will also allow you to train even bigger networks. The only difference is that you might need to do more iterations.
Test results
In this example the network converges fairly quickly. Even after 1000 iterations it’s very good. But I set it to 5000 just in case.
For example, at the beginning, you could expect an error of approximately 500 (because we have 1000 training samples and roughly 500 of them will be wrong).
This is because our network is untrained and we can expect just random behavior and pure statistical guessing.
ERROR: 505.657 [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -2.23125, 0.5, 0.5, 2.98311] [0.5, 0.5, 2.59534, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] [0.5, 2.99424, 0.5]
And indeed, the error at the beginning was 505.65 (basically pure random guessing) but it gets better as we train it:
// 1.000 iterations: 249.246 // 5.000 iterations: 249.072 // 30.000 iterations: 248.171
But you can see the biggest improvement happened in the first 1000 iterations when it went from roughly 500 to 250. Cutting the error in half.
So even though this Monte Carlo is just guesswork – you can see it converges pretty fast.
Of course, you can always give it 100.000 data points and iterate 100.000 times and let it run overnight to produce better model.
In the code it boils down to replacing 2 numbers and you’re set.
Also remember, all this “AI” is just a bunch of (simple) number crunching. Remember the story of Dota2 bots that I mentioned earlier? And how they were trained on 128.000 CPU cores? What do you think they were doing?
REMEMBER:
All this “AI” hype really is – are bunch of dumb and overtrained bots.
Nothing more.
And we’re gonna build ourselves one as well… 😉
TRADING RESULTS
The trading results are surprisingly good. We only lost 80% of the account after 853 trades:
---------------------------- | PERFORMANCE | ---------------------------- - Initial capital: 100000.00 - Final capital::: 19495.85 - Total trades:::: 853 - ROI:::::: -80.50 % - Min. ROI: -80.58 % - Max. ROI: 0.30 % - Drawdown: -80.64 % - Pushup::: 1.81 % ----------------------------
And here are the results for trading without any transaction fees:
---------------------------- | PERFORMANCE | ---------------------------- - Initial capital: 100000.00 - Final capital::: 107537.93 - Total trades:::: 853 - ROI:::::: 7.54 % - Min. ROI: -9.84 % - Max. ROI: 11.18 % - Drawdown: -10.65 % - Pushup::: 23.32 % ----------------------------
Everything else considering, this bot was fairly well performing. Overall, the movements to the upside outweigh the movements to the downside. So I’m quite satisfied with it…
WHAT’S THE PROBLEM?
Nothing really, it’s just that our entire setup was wrong (intentionally, to ease the learning process).
Realize…
The next candle has very little to do with the overall price movement. For example, next candle could be going up, but the overall price over the next 10-20 candles goes down and we hit the stop loss.
What we should be teaching our network instead, is recognizing what happens with the price movement after each candle pattern. Regardless of how many candles need to pass until one of two outcomes occurs (target or stop).
And this is why I said we will fix this later… and the good news is… this “later” is in the very next lesson.
NEXT LESSON:
Predicting Target/Stop Prices with Neural Network