(this is part 6 of the “AI for Finance Course“)

Now that we can see how a single neuron performs when dealing with real market data, it’s time to test a multi-neuron network.

Everything is the same as before, we just now have 3 neurons and 2 layers:

And obviously we have 3 inputs that we’re passing to our network (i.e. candle data: high, low, close)

The Code

The code is not that different from our previous lesson. We just need to introduce more weights since we now have 3 neurons and 2 layers:

// 1st neuron, 1st layer
double[] weights1Layer1 = [0.5, 0.5, 0.5, 0.5];
// 2nd neuron, 1st layer
double[] weights2Layer1 = [0.5, 0.5, 0.5, 0.5];
// 1st neuron, 2nd layer
double[] weights1Layer2 = [0.5, 0.5, 0.5];

And obviously additional weight backup variables:

double[] backupWeights1 = [];
double[] backupWeights2 = [];
double[] backupWeights3 = [];

After that all we have to do is calculate 2 extra neuron outputs (the last being our entire network output)

double prediction1Layer1 = getPrediction(inputs, weights1Layer1);
double prediction2Layer1 = getPrediction(inputs[i], weights2Layer1);
double prediction1Layer2 = getPrediction([prediction1Layer1, prediction2Layer1], weights1Layer2);

And here is the code in its entirety:

module bbot.bot009;

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
	}

	// NEURAL NETWORK MARKET PREDICTIONS

	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 = [0.5, 0.5, 0.5, 0.5];
	// 2nd neuron, 1st layer
	double[] weights2Layer1 = [0.5, 0.5, 0.5, 0.5];
	// 1st neuron, 2nd layer
	double[] weights1Layer2 = [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;
		backupWeights2 = weights2Layer1.dup;
		backupWeights3 = weights1Layer2.dup;
	}

	void restoreWeights() {
		weights1Layer1 = backupWeights1.dup;
		weights2Layer1 = backupWeights2.dup;
		weights1Layer2 = backupWeights3.dup;
	}

	void updateRandomWeight() {
		double[] selectedWeights = [weights1Layer1,
					    weights2Layer1,
					    weights1Layer2
					    ].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 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("ERROR: ", maxError);
				pp(weights1Layer1);
				pp(weights2Layer1);
				pp(weights1Layer2);
				//pp(printing);
			}
			else {
				restoreWeights();
			}
		}
		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];
		double[] inputs = getInputs(lastCandle);
		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;
	}
}

Test results

With neural network we got to some interesting situation:

----------------------------
| PERFORMANCE    |
----------------------------
- Initial capital: 100000.00
- Final capital::: 100000.00
- Total trades:::: 0

- ROI:::::: 0.00 %
- Min. ROI: 0.00 %
- Max. ROI: 0.00 %

- Drawdown: 0.00 %
- Pushup::: 0.00 %
----------------------------

The bot didn’t make any trades at all 😱 and to make things worse, this is our best performing bot 😅

The reason is that ALL of our predictions fell (roughly) between 0.492 and 0.494 and since we interpret everything below 0.5 as a RED candle we never got the signal to BUY.

This might also be caused due to slightly bigger number of the RED candles in the training set (while a single candle not having significant predicting power).

So the model just chose the statistically best solution (favor the RED outcome) which is not bad conclusion at all.

However, by using several candles (and recognizing candle patterns) we might predict future behavior more accurately.

So that’s what we’re going to do next…

NEXT LESSON:
Recognizing Candle Patterns Using Neural Network