What's new

Level up your trading game with Naxbot

Naxbot R is the world's first fully programmable crypto trading bot with an integrated metaheuristic optimization engine. Join us today to automate the mundane aspects of trading, so you can spend more time crafting profitable strategies. Self-hosted with no DRM or licensing, buy once - own forever!

Optimizing Your First Strategy

  • Views Views: 874
  • Last updated Last updated:
  • 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:
    Lua:
    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:
    Lua:
    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:
    JSON:
    {
    "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:
    Code:
    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:
    Lua:
    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:
    Code:
    [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:
    Lua:
    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:
    JSON:
    {
    "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:
    Code:
    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:
    JSON:
    {
    "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:
    Code:
    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:
    Code:
    [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!
Top