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

Now we’re going to combine the candle pattern recognition with the actual target/stop outcomes and see how well the neural net performs.

Because predicting the next candle is irrelevant if the entire price (over many candles) goes in a different direction.

Instead, now the plan is to take 3 by 3 candles and see where the price went for them. And then we’ll see whether we can predict the price based on candle patterns.

NOTE: it may well be possible that we can’t predict the price from these patterns because those simply do not work. Or they don’t work often enough to be profitable.

But the good news is that all the code is still reusable to test any other theory imaginable. You just need to plug in different numbers.

For example, rain amount or outside temperature. Or prices of some other asset like ETHUSDT which may very well have the predictive power when it comes to BTCUSDT.

The possibilities are endless…

The Code

The only real change is this function that calculates what is the actual outcome in our training dataset.

Previously, this was easily calculated (just by looking at the next candle).

Now we’re looking across many candles until we realize our target price was reached or the stop loss was hit:

double getCorrectOutput(int idx, double targetPrice, double stopPrice) {
	// return 1 if target hit
	// return 0 if stop loss hit
	// return -1 if nothing was reached (end of data)
	int returnValue = -1;
	foreach(c; _allCandles[c.BTCUSDT][(idx+1)..$]) {
		if(c.low <= stopPrice) {
			returnValue = 0;
			break;
		}
		if(c.high >= targetPrice) {
			returnValue = 1;
			break;
		}
	}
	return returnValue;
}

Basically, wherever we are in our data, we’re looking froward from that point and checking prices to see whether one of the (target/stop) conditions was reached.

Here is the full code:

module bbot.bot011;

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-04-16T00: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
	}

	// TARGET/STOP PRICE 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 = 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(int idx, double targetPrice, double stopPrice) {
		// return 1 if target hit
		// return 0 if stop loss hit
		// return -1 if nothing was reached (end of data)
		int returnValue = -1;
		foreach(c; _allCandles[c.BTCUSDT][(idx+1)..$]) {
			if(c.low <= stopPrice) {
				returnValue = 0;
				break;
			}
			if(c.high >= targetPrice) {
				returnValue = 1;
				break;
			}
		}
		return returnValue;
	}

	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 < 1000; n++) { // 1000 times through the data
			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(i,
												 trainingCandles[i].close * 1.003, // +0.3% target price
												 trainingCandles[i].close * 0.995);// -0.5% stop price
				double error = prediction1Layer2 - output;
				if (output == -1) error = 0; // end or close-to-end of data => skipping
				allDataError += error * error;
				//printing ~= to!string(inputs) ~ " : " ~ to!string(prediction1Layer1) ~ " --> " ~to!string(output) ~ "\n";
			}
			if (allDataError < maxError) {
				maxError = allDataError;
				pp();
				pp("ERROR: ", maxError, " --> n : ", n);
				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 past 2 weeks
			// from 2024-04-16 to 2024-04-30
			// see ==> this.backtestPeriod
			// 2 weeks is plenty when trading minute data
			trainNetwork(this._allCandles[c.BTCUSDT]);
			isAIrained = true;
		}

		//return false;

		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;
	}
}

TEST RESULTS

The results are slightly better than before but we still blew up our account:

----------------------------
| PERFORMANCE    |
----------------------------
- Initial capital: 100000.00
- Final capital::: 23759.41
- Total trades:::: 708

- ROI:::::: -76.24 %
- Min. ROI: -76.24 %
- Max. ROI: 0.10 %

- Drawdown: -76.26 %
- Pushup::: 1.61 %
----------------------------

And when we set our trading fees to zero, our model shows no edge. It just dances around zero:

----------------------------
| PERFORMANCE    |
----------------------------
- Initial capital: 100000.00
- Final capital::: 98044.51
- Total trades:::: 708

- ROI:::::: -1.96 %
- Min. ROI: -7.70 %
- Max. ROI: 4.02 %

- Drawdown: -9.19 %
- Pushup::: 12.70 %
----------------------------

CONCLUSION(?)

Based on the results of these predictions we can conclude that the candle patterns don’t have a lot of predictive power.

Even though a lot of people praise them, and our model converged pretty quickly:

ERROR: 8780.04 --> n : 0
ERROR: 5129.96 --> n : 42
ERROR: 5128.40 --> n : 782

The results show that our ROI is just dancing around zero (i.e. purely random play with no edge).

However, there are still some techniques which you will see in our last lesson that can turn around a no-edge model into a decent model… so don’t despair (yet) 😁

But for now, let’s just base our conclusions on the current results…

YOUR ADVANTAGE

Now just stop and appreciate how lucky you are.

Some people spend years trying to learn candlestick patterns not realizing they’re learning noise. There is nothing to learn.

And on the other side, here’s you… testing thousands (or millions of trades) and quickly realizing there is no edge in candlestick patterns.

Instead, you re-focus your efforts and time on finding something that will work.

That is how you win.

However, there are still two lessons you need to cover before getting serious with AI trading…

NEXT LESSON:
Separating Training And Testing Sets