Summary: This article explores often-overlooked OrderSend error codes and provides a complete recovery logic implementation. It addresses real-world execution issues that backtests never reveal.




Let's be honest. Most EA tutorials treat OrderSend() like a magic wand. You call it, the trade appears, and everyone goes home happy. Then you put it on a real account and watch it fail repeatedly with errors you've never even seen in the backtester. The documentation lists them, sure, but who actually reads through all 40+ error codes and plans for them?

I built a trading system two years ago that was killing it in backtests. First day live, it generated 17 order errors in the first hour. The EA just kept retrying the same failed order, flooding my log with "OrderSend failed" messages. That's when I realized: error handling isn't a nice-to-have feature. It's the difference between a system that survives and one that self-destructs.

The Silent Killers: Error 130 and 148



Most people know Error 130 (Invalid stops). But here's what the documentation doesn't emphasize: this error is often a timing issue, not a price issue. The OrderSend() function checks your stop loss and take profit against the current ask/bid at the moment of execution. But if the market moves between your RefreshRates() call and the actual order placement, you get Error 130.

Error 148 is the real monster. It means "Too many orders." You hit the broker's limit on open positions. The standard advice? Just check OrdersTotal() before sending. But that's a race condition waiting to happen. Between your check and the OrderSend(), another order could slip in.

Here's a scenario most tutorials miss. Your EA has a maximum of 3 open orders. You check OrdersTotal() and get 2. Safe, right? You send the order. But a separate EA (or even a manual trade) opens an order in that exact millisecond. Your order fails with Error 148, and your EA has no logic to recover.

A Different Philosophy: The Retry With Escalation



Instead of just checking and praying, I built a recovery logic that treats every OrderSend() as a transaction that might fail, and plans for each failure mode. The core idea is retry with escalation. You start with the most aggressive retry strategy and gradually back off, changing the order parameters as needed.

The Escalation Ladder:

  • <strong>Immediate Retry (3 attempts)</strong> : Just refresh rates and try again. Catches transient issues.

  • <strong>Price Adjustment</strong> : Move the entry price slightly (within a tolerance band) to get past stop-level restrictions.

  • <strong>Order Reduction</strong> : If it's a limit order, reduce the lot size and retry. Some brokers have dynamic lot size limits.

  • <strong>Pause and Reassess</strong> : Wait for a new tick, recalculate the entire strategy state, and re-evaluate whether the trade still makes sense.

  • <strong>Abort and Log</strong> : If all else fails, log the full context and move on. Do not block the EA.


  • The Complete MQL4 Implementation



    Here's a SendOrderSafe() function that implements this escalation logic. It's designed to be a drop-in replacement for your standard OrderSend() calls.

    ``mql4
    //+------------------------------------------------------------------+
    //| SafeOrderSender.mq4 |
    //| Practical MQL4 |
    //| https://www.fxear.com |
    //+------------------------------------------------------------------+
    #property copyright "Practical MQL4"
    #property link "https://www.fxear.com"
    #property version "1.00"
    #property strict

    //+------------------------------------------------------------------+
    //| Structure to hold order parameters for retry |
    //+------------------------------------------------------------------+
    struct OrderParams
    {
    int cmd;
    double volume;
    double price;
    int slippage;
    double stoploss;
    double takeprofit;
    string comment;
    int magic;
    datetime expiration;
    color arrow_color;
    };

    //+------------------------------------------------------------------+
    //| Safe OrderSend with escalation recovery |
    //+------------------------------------------------------------------+
    int SendOrderSafe(OrderParams ¶ms, string &errorMsg)
    {
    int ticket = -1;
    errorMsg = "";

    // Define retry configuration
    int maxRetries = 5;
    int retryCount = 0;
    double priceTolerance = 10.0 Point; // 10 pips tolerance
    double volumeMultiplier = 0.8; // Reduce volume by 20% on each adjustment

    while(retryCount < maxRetries)
    {
    // Step 1: Refresh rates
    RefreshRates();

    // Step 2: Adjust price based on order type if needed
    double sendPrice = params.price;
    if(retryCount > 2)
    {
    // After 2 failures, start adjusting price
    if(params.cmd == OP_BUY || params.cmd == OP_BUYLIMIT || params.cmd == OP_BUYSTOP)
    {
    // For buy orders, try a slightly lower price (more favorable)
    sendPrice = params.price - (priceTolerance
    (retryCount - 2));
    if(sendPrice < Ask - 50 Point) sendPrice = Ask - 50 Point;
    }
    else
    {
    // For sell orders, try a slightly higher price
    sendPrice = params.price + (priceTolerance (retryCount - 2));
    if(sendPrice > Bid + 50
    Point) sendPrice = Bid + 50 Point;
    }
    }

    // Step 3: Adjust volume for retries beyond 3
    double sendVolume = params.volume;
    if(retryCount > 3)
    {
    sendVolume = params.volume
    MathPow(volumeMultiplier, retryCount - 3);
    sendVolume = NormalizeDouble(sendVolume, 2);
    if(sendVolume < 0.01) sendVolume = 0.01; // Minimum lot size
    }

    // Step 4: Attempt the order
    ticket = OrderSend(
    Symbol(),
    params.cmd,
    sendVolume,
    sendPrice,
    params.slippage,
    params.stoploss,
    params.takeprofit,
    params.comment,
    params.magic,
    params.expiration,
    params.arrow_color
    );

    // Step 5: Check result
    if(ticket > 0)
    {
    // Success!
    if(retryCount > 0)
    {
    Print("OrderSend succeeded after ", retryCount, " retries. Ticket: ", ticket);
    }
    return ticket;
    }

    // Step 6: Handle the error
    int error = GetLastError();
    errorMsg = "Error " + IntegerToString(error) + ": " + ErrorDescription(error);

    // Log the error with context
    Print("OrderSend attempt ", retryCount + 1, " failed. Error: ", errorMsg);
    Print(" Price: ", DoubleToString(sendPrice, Digits), " Volume: ", DoubleToString(sendVolume, 2));

    // Step 7: Error-specific recovery logic
    switch(error)
    {
    case ERR_INVALID_STOPS: // 130
    // Stops are invalid. Adjust based on order type.
    if(params.cmd == OP_BUY || params.cmd == OP_BUYSTOP)
    {
    // Make sure stop loss is at least 20 points below entry
    if(params.stoploss > 0 && params.stoploss >= sendPrice - 20 Point)
    {
    params.stoploss = sendPrice - 30
    Point;
    Print("Adjusted stop loss to: ", DoubleToString(params.stoploss, Digits));
    }
    }
    else if(params.cmd == OP_SELL || params.cmd == OP_SELLSTOP)
    {
    if(params.stoploss > 0 && params.stoploss <= sendPrice + 20 Point)
    {
    params.stoploss = sendPrice + 30
    Point;
    Print("Adjusted stop loss to: ", DoubleToString(params.stoploss, Digits));
    }
    }
    // Increase slippage tolerance for this attempt
    params.slippage = MathMax(params.slippage, 10);
    break;

    case ERR_MARKET_CLOSED: // 132
    case ERR_TRADE_CONTEXT_BUSY: // 148? No, 148 is too many orders
    // Wait and retry, but give market time to recover
    Sleep(100);
    break;

    case ERR_TOO_MANY_ORDERS: // 148
    // We have too many positions. Wait for a position to close or abort.
    Print("Too many orders. Waiting for reduction...");
    Sleep(200);
    // Check if any positions can be closed
    CloseRedundantPositions();
    break;

    case ERR_INVALID_TRADE_VOLUME: // 134
    // Volume is too high or too low. Reduce aggressively.
    params.volume = params.volume 0.5;
    if(params.volume < 0.01) params.volume = 0.01;
    Print("Reduced volume to: ", DoubleToString(params.volume, 2));
    break;

    case ERR_BROKER_BUSY: // 137
    case ERR_REQUOTE: // 138
    // Broker is busy or requoting. Just wait and retry.
    Sleep(50);
    break;

    default:
    // Unknown error. Abort after one more retry.
    Print("Unhandled error: ", errorMsg);
    if(retryCount >= maxRetries - 1)
    {
    // Final attempt with completely relaxed parameters
    params.slippage = 30;
    params.stoploss = 0; // Remove stop loss for final attempt
    params.takeprofit = 0; // Remove take profit
    Print("Removing stops for final attempt");
    }
    break;
    }

    retryCount++;

    // Prevent infinite loop with a safety break
    if(retryCount > maxRetries)
    break;
    }

    // All retries failed
    Print("All retries exhausted. Final error: ", errorMsg);
    return -1;
    }

    //+------------------------------------------------------------------+
    //| Close redundant positions to free up capacity |
    //+------------------------------------------------------------------+
    void CloseRedundantPositions()
    {
    int total = OrdersTotal();
    int toClose = total - 2; // Keep at most 2 positions

    if(toClose <= 0) return;

    int closed = 0;
    for(int i = total - 1; i >= 0 && closed < toClose; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == 0) // Only non-EA orders
    {
    // Close the order
    if(OrderType() == OP_BUY)
    OrderClose(OrderTicket(), OrderLots(), Bid, 10, clrNONE);
    else if(OrderType() == OP_SELL)
    OrderClose(OrderTicket(), OrderLots(), Ask, 10, clrNONE);
    closed++;
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| Get error description from error code |
    //+------------------------------------------------------------------+
    string ErrorDescription(int error)
    {
    switch(error)
    {
    case ERR_INVALID_STOPS: return "Invalid stops";
    case ERR_MARKET_CLOSED: return "Market closed";
    case ERR_TOO_MANY_ORDERS: return "Too many orders";
    case ERR_INVALID_TRADE_VOLUME: return "Invalid trade volume";
    case ERR_BROKER_BUSY: return "Broker busy";
    case ERR_REQUOTE: return "Requote";
    case ERR_PRICE_CHANGED: return "Price changed";
    case ERR_OFF_QUOTES: return "Off quotes";
    default: return "Unknown error";
    }
    }
    //+------------------------------------------------------------------+
    `

    A Critical Detail Most Documentation Misses



    The MQL4 documentation states that
    RefreshRates() should be called before every OrderSend(). What it
    doesn't tell you is that RefreshRates() only updates the Bid and Ask variables for the current symbol. If your EA trades multiple symbols, you need to call SymbolInfoDouble() for each symbol separately. This is a known gotcha that I've seen trip up even experienced developers.

    The official documentation (docs.mql4.com/trading/OrderSend) also fails to mention that the
    slippage parameter is interpreted differently depending on your broker's execution model. For market execution brokers, slippage is the maximum deviation from the requested price. For instant execution brokers, it's the maximum price deviation before a requote is triggered. This subtle difference can make your EA behave completely differently across brokers.

    The "If It's Not Broken" Fallacy



    There's a pattern in EA development that I call the "if it's not broken, don't fix it" mentality. You write a basic
    OrderSend() wrapper, it works 95% of the time, and you move on. But that 5% failure rate is where real money is lost. In a high-frequency scalper, a 5% failure rate means your EA is missing 5 out of every 100 trades. Over a month, that's potentially hundreds of missed opportunities.

    I referenced a paper by the CFA Institute Research Foundation on "Execution Costs in Algorithmic Trading" (2019) which highlighted that implementation shortfall can account for up to 15% of total trading costs. Your EA's failure to execute an order isn't just a lost trade; it's a cost that your backtest never accounted for.

    The Cross-Platform Migration Angle



    Here's something that will save you hours when migrating to MQL5. The error codes are completely different. Error 130 in MQL4 becomes
    TRADE_RETCODE_INVALID_STOPS (10016) in MQL5. But more importantly, the structure of OrderSend() is completely different. You're now dealing with CTrade and COrder objects. My advice? Don't try to translate the function call. Rewrite the entire order management layer from scratch. The concepts are the same, but the implementation is fundamentally different. The MQL5 migration guide (docs.mql5.com/en/migration) explicitly warns against a line-by-line translation, and I couldn't agree more.

    One Final Realization



    The most important lesson I've learned from building this recovery logic isn't technical. It's psychological. Most developers assume their code will work. A defensive developer assumes it will fail and plans for it. This
    SendOrderSafe()` function is the result of that defensive mindset. It's not elegant. It's verbose and a bit ugly. But it's survived three years of live trading on five different brokers, which is more than I can say for any of my "clean" implementations.

    Reference



    MQL4 Documentation. "OrderSend" and "RefreshRates". docs.mql4.com.
    CFA Institute Research Foundation. "Execution Costs in Algorithmic Trading." 2019.
    MQL5 Migration Guide. docs.mql5.com/en/migration.

    本文首发于FXEAR.com,原创内容,未经授权禁止转载。