Error code 10016 [Invalid stops]

 
Could some review my code which I've attached below and let me know why do I occasionally get Error Code 10016 indicating it's an Invalid stops?

For reference here is one of the many log messages I receive in the MetaTrader 5's Journal
2023.06.02 00:23:36.885 Trades '5013799956': failed modify #50475513619 sell 0.01 AUDUSD sl: 0.65727, tp: 0.00000 -> sl: 0.65722, tp: 0.00000 [Invalid stops]

This seems to be an issue with the trail_sl function which handles the trailing of stop losses... but not exactly sure where to pin-point it...

I am using MetaQuotes-Demo server for testing out and ruling off what could be causing the issue for invalid stops... Perhaps the stop-loss is too small of a change for it to even send?
Documentation on MQL5: Constants, Enumerations and Structures / Codes of Errors and Warnings / Trade Server Return Codes
Documentation on MQL5: Constants, Enumerations and Structures / Codes of Errors and Warnings / Trade Server Return Codes
  • www.mql5.com
Trade Server Return Codes - Codes of Errors and Warnings - Constants, Enumerations and Structures - MQL5 Reference - Reference on algorithmic/automated trading language for MetaTrader 5
Files:
mt5_test-bot.py  11 kb
 
Please don't create topics randomly in any section. It has been moved to the section: Expert Advisors and Automated Trading
MQL5 forum: Expert Advisors and Automated Trading
MQL5 forum: Expert Advisors and Automated Trading
  • www.mql5.com
How to create an Expert Advisor (a trading robot) for Forex trading
 

Are you verifying that the prices are aligned to the tick size? I don't see that in your code.

Are you verifying the Stops Level and Freeze Levels? I also don't see that in your code.

Articles

The checks a trading robot must pass before publication in the Market

MetaQuotes, 2016.08.01 09:30

Before any product is published in the Market, it must undergo compulsory preliminary checks in order to ensure a uniform quality standard. This article considers the most frequent errors made by developers in their technical indicators and trading robots. An also shows how to self-test a product before sending it to the Market.


Forum on trading, automated trading systems and testing trading strategies

Tick size vs Point(), can be a little tricky in Multicurrency EA

Fernando Carreiro, 2022.03.09 12:11

Tick Size and Point Size can be very different especially on stocks and other symbols besides forex.

Always use Tick Size to adjust and align your prices, not the point size. In essence, make sure that your price quotes, are properly aligned to the Tick size (see following examples).

...
double tickSize = SymbolInfoDouble( _Symbol, SYMBOL_TRADE_TICK_SIZE );
...
double normalised_price = round( price / tick_size ) * tick_size;
...
// Or use a function
double Round2Ticksize( double price )
{
   double tick_size = SymbolInfoDouble( _Symbol, SYMBOL_TRADE_TICK_SIZE );
   return( round( price / tick_size ) * tick_size );
};
 
Sorry for the late response!

Should I utilize tick size during trailing stop losses? Freeze Levels, Stop Levels are returning zero for now but thanks for pointing out as it'll be needed when placing orders and trailing stop losses as they change from broker to broker.

I've added in the necessary code snippet and added in a variable called tick_size which updates for every symbol, calculating and storing it under normalised_sl and just assigning that as my stop-loss send request, should I use it anywhere else for comparing? 
def trail_sl(symbol, order_ticket, timeframe, stop_loss_dict):
    while True:

        bars = mt5.copy_rates_from_pos(symbol, timeframe, 0, 4)
        bars_df = pd.DataFrame(bars)
        bar1_high = bars_df['high'].iloc[0]
        bar2_high = bars_df['high'].iloc[1]
        bar3_high = bars_df['high'].iloc[2]
        bar1_low = bars_df['low'].iloc[0]
        bar2_low = bars_df['low'].iloc[1]
        bar3_low = bars_df['low'].iloc[2]

        if mt5.positions_get(ticket=order_ticket):
            position_type = mt5.positions_get(ticket=order_ticket)[0].type
            if position_type == mt5.ORDER_TYPE_SELL:
                stop_loss_value = max(
                    bar1_high, bar2_high, bar3_high)  # Sell order S/L
            else:
                stop_loss_value = min(bar1_low, bar2_low,
                                      bar3_low)  # Buy order S/L

            tick_size = mt5.symbol_info(symbol).trade_tick_size
            normalised_sl = round(stop_loss_value / tick_size) * tick_size

            if normalised_sl != stop_loss_dict[symbol]:
                current_sl = mt5.positions_get(ticket=order_ticket)[0].sl
                if normalised_sl != current_sl:
                    request = {
                        "action": mt5.TRADE_ACTION_SLTP,
                        "position": order_ticket,
                        "symbol": symbol,
                        "magic": 24001,
                        "sl": normalised_sl
                    }
                    result = mt5.order_send(request)
                    if result.retcode == mt5.TRADE_RETCODE_DONE:
                        print(
                            f"[{datetime.now()}] Trailing Stop Loss for Order {order_ticket} updated. New S/L: {normalised_sl}")
                        print()
                        stop_loss_dict[symbol] = normalised_sl
                    elif result.retcode == 10025:  # Ignore error code 10025
                        pass
                    else:
                        print(
                            f"[{datetime.now()}] Failed to update Trailing Stop Loss for Order {order_ticket}: {result.comment}")
                        print(f"Error code: {result.retcode}")
                        print()

        time.sleep(1)  # Wait for 1 second before checking again
 

Root Cause Analysis

The "failed modify #0" errors are NOT caused by tick size alignment issues. They are caused by:

  1. Position already closed: The trailing stop logic attempts to modify positions that have already been closed by take profit or stop loss
  2. Timing issue: The position cache update happens after the position is closed, but the trailing stop logic still tries to modify the old position
  3. Invalid ticket reference: When a position is closed, the ticket becomes invalid, showing as #0 in the error

The Fix

The issue is in the trailing stop logic. We need to validate that the position still exists before attempting to modify it. The ValidatePositionModification() function should catch this, but it's not being called or not working properly.