Introduction
So, you've finally created your first
Strategy, but it's not as profitable as you had hoped? Or maybe you already have a profitable
Strategy, but are wondering whether or not you could improve its performance even more? Or perhaps you are trying to tweak other aspects of your
Strategy, such as entry / exit points, but can't be bothered to fine-tune parameters for hours on end?
These are the problems that Naxbot R's integrated
Optimizer aims to solve. By leveraging a metaheuristic optimization algorithm, Naxbot R is capable of finding optimum parameters for any strategy you throw at it.
In this tutorial, we will be optimizing the strategy created in "
Create Your First Strategy", but you are free to follow along with any strategy you may already have.
Strategy Preparation
First, we will need to prepare our
strategy.lua
file to allow the
Optimizer to change some of its parameters. As a reminder, we are working with the following strategy:
function process()
local fast_length = 5;
local slow_length = 17;
local fast_rsi = rsi(close, fast_length);
local slow_rsi = rsi(close, slow_length);
local divergence = fast_rsi - slow_rsi;
-- we declare the variable "zero", so we don't need
-- to calculate the constant multiple times
local zero = constant(0);
local long_entry_condition = crossover(divergence, zero);
local short_entry_condition = crossunder(divergence, zero);
-- again we declare a variable to avoid
-- multiple future calculations
local distance = atr(21);
-- we now calculate both long & short stop loss,
-- and then later decide which one to use based
-- on whether the signal is long or not.
local stop_loss_distance = distance * constant(1.5);
local long_stop_loss = close - stop_loss_distance;
local short_stop_loss = close + stop_loss_distance;
local stop_loss = lif(long_entry_condition, long_stop_loss, short_stop_loss);
-- we do the same for our take profit target
local tp_1_distance = distance * constant(3);
local long_tp_1 = close + tp_1_distance;
local short_tp_1 = close - tp_1_distance;
local tp_1 = lif(long_entry_condition, long_tp_1, short_tp_1);
return {
long_entry_condition = long_entry_condition,
short_entry_condition = short_entry_condition,
stop_loss = stop_loss,
tp_1 = tp_1,
fast_rsi = fast_rsi,
slow_rsi = slow_rsi,
divergence = divergence,
}
end
In our "
Create Your First Strategy" tutorial, this particular strategy was actually not profitable, so let's see if the
Optimizer can turn it into a profitable one. First, we'll need to define which parameters we want the optimizer to change. In this case, the two RSI lengths seem like fine candidates. We'll change the
process
function to take in actual parameters, and use the current lengths as default values:
function process(fast_length, slow_length)
fast_length = fast_length or 5
slow_length = slow_length or 17
local fast_rsi = rsi(close, fast_length);
local slow_rsi = rsi(close, slow_length);
local divergence = fast_rsi - slow_rsi;
-- we declare the variable "zero", so we don't need
-- to calculate the constant multiple times
local zero = constant(0);
local long_entry_condition = crossover(divergence, zero);
local short_entry_condition = crossunder(divergence, zero);
-- again we declare a variable to avoid
-- multiple future calculations
local distance = atr(21);
-- we now calculate both long & short stop loss,
-- and then later decide which one to use based
-- on whether the signal is long or not.
local stop_loss_distance = distance * constant(1.5);
local long_stop_loss = close - stop_loss_distance;
local short_stop_loss = close + stop_loss_distance;
local stop_loss = lif(long_entry_condition, long_stop_loss, short_stop_loss);
-- we do the same for our take profit target
local tp_1_distance = distance * constant(3);
local long_tp_1 = close + tp_1_distance;
local short_tp_1 = close - tp_1_distance;
local tp_1 = lif(long_entry_condition, long_tp_1, short_tp_1);
return {
long_entry_condition = long_entry_condition,
short_entry_condition = short_entry_condition,
stop_loss = stop_loss,
tp_1 = tp_1,
fast_rsi = fast_rsi,
slow_rsi = slow_rsi,
divergence = divergence,
}
end
And that's all it takes to prepare a strategy for the optimizer. Painless, wasn't it? Next, we'll set up our
config.json
Config Preparation
In our
config.json
, we'll need to tell the optimizer about the parameters we want it to optimize. Also, since we don't have a lot of parameters to begin with, we'll tune down the generations & population size. You may need to adjust your threads as well, depending on your CPU. Furthermore, we'll give it some extra trading pairs, so it has more data to optimize on:
{
"optimizer": {
"generations_per_thread": 50,
"population_size_per_thread": 50,
"trading_pairs": [
"BTC-PERP",
"ETH-PERP",
"ADA-PERP",
"LTC-PERP",
"DOGE-PERP"
],
"scoring": {
"hit_rate_weight": 50,
"target_trades_per_kline": 0.1,
"total_profit_weight": 10,
"trade_amount_weight": 40
},
"parameters": [
{
"Integer": {
"min": 1,
"max": 100
}
},
{
"Integer": {
"min": 1,
"max": 100
}
}
]
}
}
NOTE: It is generally considered bad practice to optimize on the same trading pairs that you plan to actively trade, due to the risk of overfitting on the training data.
The above config will tell our optimizer that our strategy takes in two parameters, both of type
Integer
, and for both parameters it should only try out values between 1 and 100. All that's left now is to set the bot mode to
Optimize
and enjoy the show.
Optimization
After the optimizer finishes, you should see something like this:
Found new best config with a test score of 0.59:
(10, 53)
Total Trades: 661
Successful Trades: 243
Total Non-Compounded Profit: 138.00%
Longest Losing Streak: 12
[INFO] Optimizer is done. Best config: (10, 53)
Alright! Looks like the optimizer has found a configuration that turns our unprofitable strategy from earlier into a profitable one. We can backtest this properly by first inserting the parameters returned by the optimizer into our strategy like so:
function process(fast_length, slow_length)
fast_length = fast_length or 10;
slow_length = slow_length or 53;
local fast_rsi = rsi(close, fast_length);
local slow_rsi = rsi(close, slow_length);
local divergence = fast_rsi - slow_rsi;
-- we declare the variable "zero", so we don't need
-- to calculate the constant multiple times
local zero = constant(0);
local long_entry_condition = crossover(divergence, zero);
local short_entry_condition = crossunder(divergence, zero);
-- again we declare a variable to avoid
-- multiple future calculations
local distance = atr(21);
-- we now calculate both long & short stop loss,
-- and then later decide which one to use based
-- on whether the signal is long or not.
local stop_loss_distance = distance * constant(1.5);
local long_stop_loss = close - stop_loss_distance;
local short_stop_loss = close + stop_loss_distance;
local stop_loss = lif(long_entry_condition, long_stop_loss, short_stop_loss);
-- we do the same for our take profit target
local tp_1_distance = distance * constant(3);
local long_tp_1 = close + tp_1_distance;
local short_tp_1 = close - tp_1_distance;
local tp_1 = lif(long_entry_condition, long_tp_1, short_tp_1);
return {
long_entry_condition = long_entry_condition,
short_entry_condition = short_entry_condition,
stop_loss = stop_loss,
tp_1 = tp_1,
fast_rsi = fast_rsi,
slow_rsi = slow_rsi,
divergence = divergence,
}
end
And then set the bot to
Backtest
mode before restarting it. Here are the results:
[INFO] --- BACKTEST FINISHED ---
[INFO] (this backtest is using your configured crawler settings to try to be as realistic as possible, unlike optimization mode)
[INFO] Results are in:
[INFO] Total Trades: 138
[INFO] Successful Trades: 48
[INFO] Longest Losing Streak: 8
[INFO] Total Non-Compounded Profit: 12.00%
[INFO] Total Compounded Profit: 6.65%
[INFO] Hit Rate: 34.78%
[INFO] Max Drawdown: 14.92% (theoretical, based on longest losing streak)
[INFO] Total Klines in Test: 1088
[INFO] Your observed R is: 2.00 (mean win / mean loss)
[INFO] Based on the observed R, your strategy will break even at a 33.33% win rate.
This is already much better than what we previously had, but the profit still isn't too overwhelming. Perhaps there is a way to optimize this even more...
Further Optimization
Now that we know how to operate the optimizer, we can take things to the next level. In the previous section, we used the optimizer to optimize the lengths of both of our RSI indicators. But what if we used it to optimize stop loss & take profit targets?
Well, we can do that too, of course! We simply add two new parameters to the function and use those parameters in our take profit & stop loss calculations. The resulting script will look a little like this:
function process(fast_length, slow_length, stop_loss_multiplier, tp_1_multiplier)
fast_length = fast_length or 10;
slow_length = slow_length or 53;
stop_loss_multiplier = stop_loss_multiplier or 1.5;
tp_1_multiplier = tp_1_multiplier or 3;
local fast_rsi = rsi(close, fast_length);
local slow_rsi = rsi(close, slow_length);
local divergence = fast_rsi - slow_rsi;
-- we declare the variable "zero", so we don't need
-- to calculate the constant multiple times
local zero = constant(0);
local long_entry_condition = crossover(divergence, zero);
local short_entry_condition = crossunder(divergence, zero);
-- again we declare a variable to avoid
-- multiple future calculations
local distance = atr(21);
-- we now calculate both long & short stop loss,
-- and then later decide which one to use based
-- on whether the signal is long or not.
local stop_loss_distance = distance * constant(stop_loss_multiplier);
local long_stop_loss = close - stop_loss_distance;
local short_stop_loss = close + stop_loss_distance;
local stop_loss = lif(long_entry_condition, long_stop_loss, short_stop_loss);
-- we do the same for our take profit target
local tp_1_distance = distance * constant(tp_1_multiplier);
local long_tp_1 = close + tp_1_distance;
local short_tp_1 = close - tp_1_distance;
local tp_1 = lif(long_entry_condition, long_tp_1, short_tp_1);
return {
long_entry_condition = long_entry_condition,
short_entry_condition = short_entry_condition,
stop_loss = stop_loss,
tp_1 = tp_1,
fast_rsi = fast_rsi,
slow_rsi = slow_rsi,
divergence = divergence,
}
end
And of course, we will also need to add these new parameters to our
config.json
. This time, we want both of them to be floating point numbers between 1 and 10, so as to not move too far away from our entry price:
{
"optimizer": {
"generations_per_thread": 50,
"population_size_per_thread": 50,
"trading_pairs": [
"BTC-PERP",
"ETH-PERP",
"ADA-PERP",
"LTC-PERP",
"DOGE-PERP"
],
"scoring": {
"hit_rate_weight": 50,
"target_trades_per_kline": 0.1,
"total_profit_weight": 10,
"trade_amount_weight": 40
},
"parameters": [
{
"Integer": {
"min": 1,
"max": 100
}
},
{
"Integer": {
"min": 1,
"max": 100
}
},
{
"Float": {
"min": 1,
"max": 10
}
},
{
"Float": {
"min": 1,
"max": 10
}
}
]
}
}
With our config & strategy prepared, let's run the optimizer once again and check out the results:
Found new best config with a test score of 0.81:
(21, 26, 8.705214468972487, 1.0023625218981158)
Total Trades: 562
Successful Trades: 465
Total Non-Compounded Profit: -86.92%
Longest Losing Streak: 9
[INFO] Optimizer is done. Best config: (21, 26, 8.705214468972487, 1.0023625218981158)
That's strange, the optimizer has produced an unprofitable configuration. But why?
To understand this, it is important to understand how the optimizer picks its configuration. You see, when it comes to evaluating trading strategies, there isn't a single end-all-be-all metric that can be used to rank them. Instead, what's important is usually a
combination of hit rate, overall profitability, risk-reward ratio, and trade frequency.
Naxbot R's optimizer takes all of these into account, but the
weight at which these are evaluated is defined by the user. As we can see from our
config.json
file above, our
hit_rate_weight
is set to
50
and our
total_profit_weight
is set to
10
. That's the reason why the optimizer has picked a config with a fairly high hit rate of around 82%, but an abysmal overall profit.
To mitigate this, all we need to do is to tell the optimizer that we care more about profit, than we do about hit rate:
{
"optimizer": {
"generations_per_thread": 50,
"population_size_per_thread": 50,
"trading_pairs": [
"BTC-PERP",
"ETH-PERP",
"ADA-PERP",
"LTC-PERP",
"DOGE-PERP"
],
"scoring": {
"hit_rate_weight": 20,
"target_trades_per_kline": 0.1,
"total_profit_weight": 40,
"trade_amount_weight": 40
},
"parameters": [
{
"Integer": {
"min": 1,
"max": 100
}
},
{
"Integer": {
"min": 1,
"max": 100
}
},
{
"Float": {
"min": 1,
"max": 10
}
},
{
"Float": {
"min": 1,
"max": 10
}
}
]
}
}
Now that we've adjusted the scoring weights, let's run the optimizer again and see what we get:
Found new best config with a test score of 0.74:
(86, 50, 1.0785053556746964, 9.738447085766838)
Total Trades: 305
Successful Trades: 66
Total Non-Compounded Profit: 713.90%
Longest Losing Streak: 30
[INFO] Optimizer is done. Best config: (86, 50, 1.0785053556746964, 9.738447085766838)
That already looks much more promising. Let's backtest it on
BTC-PERP
to get some more details:
[INFO] --- BACKTEST FINISHED ---
[INFO] (this backtest is using your configured crawler settings to try to be as realistic as possible, unlike optimization mode)
[INFO] Results are in:
[INFO] Total Trades: 61
[INFO] Successful Trades: 16
[INFO] Longest Losing Streak: 9
[INFO] Total Non-Compounded Profit: 198.95%
[INFO] Total Compounded Profit: 473.81%
[INFO] Hit Rate: 26.23%
[INFO] Max Drawdown: 16.63% (theoretical, based on longest losing streak)
[INFO] Total Klines in Test: 1088
[INFO] Your observed R is: 9.03 (mean win / mean loss)
[INFO] Based on the observed R, your strategy will break even at a 9.97% win rate.
These results are much better than what we started with. However, there are still a few problems with this configuration, mainly the longest losing streak. As it stands, you need some pretty big cojones to be trading this strategy. This can be adjusted by limiting the range of the stop loss & tp target parameters the optimizer may pick from, or by adjusting the hit rate vs. total profit scoring weights.
Considering we started with a very simple strategy however, these results are pretty phenomenal. You can imagine the kind of results you can achieve with something more sophisticated.
Now you know how to optimize your strategy with Naxbot R. If you have any further questions, don't hesitate to ask in the proper forums!