Advertisement IC Markets

Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

MT5 (MQL5): The Asynchronous State Machine

MT5 introduces a highly dangerous trap for developers: OrderSend() returning true only means the server successfully verified your request structure, not that the trade was actually executed. You must query result.retcode to understand the actual outcome.

If you receive TRADE_RETCODE_TIMEOUT (10022), it means the trade server did not respond. The order may have perfectly executed, but the acknowledgment was lost. Retrying blindly here is exactly how double-fills happen.

Code: Select all

bool ExecuteSafeOrder_MT5(MqlTradeRequest &request, int magic) {
    int maxRetries = 3;
    int delayMs = 500;
    
    request.magic = magic; // Embed the unique signal ID for idempotency
    MqlTradeResult result;
    
    for(int attempt = 1; attempt <= maxRetries; attempt++) {
        // 1. Idempotency Check: Scan active positions on the broker server
        for(int i = 0; i < PositionsTotal(); i++) {
            ulong posTicket = PositionGetTicket(i);
            if(posTicket > 0) {
                if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetString(POSITION_SYMBOL) == request.symbol) {
                    PrintFormat("ABORT RETRY: Position %d verified open for Magic %d", posTicket, magic);
                    return true; 
                }
            }
        }
        
        // 2. Re-sync market quotes before retrying to prevent off-quote rejections
        MqlTick lastTick;
        SymbolInfoTick(request.symbol, lastTick);
        request.price = (request.type == ORDER_TYPE_BUY) ? lastTick.ask : lastTick.bid;
        
        // 3. Execution Attempt
        ResetLastError();
        ZeroMemory(result);
        bool sent = OrderSend(request, result);
        
        // 4. Verify actual execution, not just structure acceptance
        if(sent && (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED)) {
            return true; // Actual execution success
        }
        
        // 5. Error Routing via server return codes
        uint ret = result.retcode;
        if(ret == TRADE_RETCODE_REQUOTE || ret == TRADE_RETCODE_CONNECTION || ret == TRADE_RETCODE_TIMEOUT || ret == TRADE_RETCODE_PRICE_CHANGED) {
            PrintFormat("Transient Code %u - Attempt %d/%d. Retrying...", ret, attempt, maxRetries);
            Sleep(delayMs + (MathRand() % 250)); // Exponential backoff with Jitter
            delayMs *= 2;
        } else {
            PrintFormat("Terminal Code %u (System Err: %d) - Aborting.", ret, GetLastError());
            break; 
        }
    }
    return false;
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

Because cTrader’s cAlgo API is built natively on .NET/C#, architecting this execution pipeline is much cleaner than in MetaTrader. You are no longer dealing with obscure integer return codes; cTrader provides a strongly typed TradeResult object and an ErrorCode enum.

In cTrader, the equivalent of MT4's Magic Number is the Label parameter (a string). This allows you to generate highly specific, human-readable signal signatures (e.g., "Long_PA_15m_EURUSD_Tick123456") to enforce idempotency.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

Here is the C# state machine wrapper for cAlgo.

Code: Select all

using System;
using System.Threading;
using cAlgo.API;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class IdempotentExecutionBot : Robot
    {
        // Use a standard Random instance for execution jitter
        private readonly Random _random = new Random();

        /// <summary>
        /// Executes a market order with exponential backoff, state synchronization, and strict error routing.
        /// </summary>
        public TradeResult ExecuteSafeOrder_cTrader(TradeType tradeType, string symbol, double volume, string signalLabel, double? slPips, double? tpPips)
        {
            int maxRetries = 3;
            int delayMs = 500;
            TradeResult result = null;

            for (int attempt = 1; attempt <= maxRetries; attempt++)
            {
                // 1. Idempotency Check: Scan active broker positions for this exact Label.
                // If a previous timeout actually filled on the broker side, this catches it.
                var existingPosition = Positions.Find(signalLabel, SymbolName, tradeType);
                if (existingPosition != null)
                {
                    Print("ABORT RETRY: Position already open for Label {0} (Position ID: {1})", signalLabel, existingPosition.Id);
                    return result; // Loop aborted to prevent duplicate fill
                }

                // 2. Execution Attempt
                // cTrader handles current Bid/Ask automatically for MarketOrders,
                // but if using Limit orders, you would recalculate the target price here.
                result = ExecuteMarketOrder(tradeType, symbol, volume, signalLabel, slPips, tpPips);

                // 3. Verify Execution
                if (result.IsSuccessful)
                {
                    Print("SUCCESS: Order Executed | Position ID: {0} | Slippage: {1}", result.Position.Id, result.Position.EntryPrice);
                    return result;
                }

                // 4. Error Routing via cTrader ErrorCodes
                switch (result.Error)
                {
                    // Transient Errors: Network drops, server lag, or extreme spread widening (requote equivalent)
                    case ErrorCode.Timeout:
                    case ErrorCode.Disconnected:
                    case ErrorCode.TechnicalError:
                    case ErrorCode.PriceHasChanged: 
                        Print("Transient Error '{0}' - Attempt {1}/{2}. Retrying...", result.Error, attempt, maxRetries);
                        
                        // Exponential backoff with millisecond Jitter
                        int jitter = _random.Next(50, 250);
                        Thread.Sleep(delayMs + jitter);
                        delayMs *= 2;
                        break;

                    // Terminal Errors: Margin issues, invalid stops, or market closure
                    case ErrorCode.NoMoney:
                    case ErrorCode.BadVolume:
                    case ErrorCode.MarketClosed:
                    case ErrorCode.InvalidRequest:
                    default:
                        Print("Terminal Error '{0}' - Hard Abort.", result.Error);
                        return result; // Instantly break the loop
                }
            }

            Print("CRITICAL: Max retries reached for Label {0}. State machine terminating.", signalLabel);
            return result;
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

Key Differences in the .NET cAlgo Environment

Synchronous Thread Blocking (Thread.Sleep): In standard cAlgo OnTick or OnBar methods, calling Thread.Sleep() blocks the specific robot instance thread. Unlike MT4, this does not lock the entire cTrader platform UI or other robots, making it perfectly safe to use for a 500ms–2000ms delay.

The Asynchronous Alternative: If your architecture requires the robot thread to remain entirely unblocked (e.g., you are tracking complex multidimensional arrays or tick data while the order is pending), you would switch ExecuteMarketOrder to ExecuteMarketOrderAsync. However, the asynchronous callback structure fundamentally changes the retry loop into a recursive method, which requires careful memory management to prevent memory leaks if the connection is continuously dropping. For 99% of price action scalping logic, the synchronous wrapper above is mathematically safer and guarantees sequential evaluation.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

To elevate this from a standard trading script to enterprise-grade C# infrastructure, we must eliminate Thread.Sleep() (which is a thread-blocking anti-pattern), implement fully asynchronous non-blocking execution, decouple the logic for unit testing, and structure the telemetry for external ingestion.

Given modern cTrader runs on .NET 6, you can utilize standard C# asynchronous paradigms (Task, async/await), dependency injection interfaces, and structured data transfer objects (DTOs) for your database.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

Here is the professional-grade, fully asynchronous execution engine.

Code: Select all

using System;
using System.Text.Json;
using System.Threading.Tasks;
using cAlgo.API;

namespace cAlgo.Robots
{
    // 1. Data Transfer Object for Centralized Logging
    public class RejectionTelemetry
    {
        public string SignalId { get; set; }
        public string Symbol { get; set; }
        public string ErrorCode { get; set; }
        public int Attempt { get; set; }
        public double EntryPriceTarget { get; set; }
        public double ActualPrice { get; set; }
        public DateTime TimestampUtc { get; set; }
    }

    // 2. Interface for Dependency Injection & Chaos Mocking
    public interface IExecutionEngine
    {
        Task<TradeResult> ExecuteIdempotentAsync(TradeType tradeType, string symbol, double volume, string signalId, double targetPrice, double maxSlippagePips);
    }

    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FullAccess)]
    public class EnterpriseExecutionBot : Robot, IExecutionEngine
    {
        private readonly Random _random = new Random();

        protected override void OnStart()
        {
            // Initialization, API connections, etc.
        }

        /// <summary>
        /// Fully asynchronous, non-blocking order execution with price validation and telemetry.
        /// </summary>
        public async Task<TradeResult> ExecuteIdempotentAsync(TradeType tradeType, string symbol, double volume, string signalId, double targetPrice, double maxSlippagePips)
        {
            int maxRetries = 3;
            int delayMs = 500;
            TradeResult result = null;

            for (int attempt = 1; attempt <= maxRetries; attempt++)
            {
                // 1. Idempotency Check (Server-Side)
                var existingPosition = Positions.Find(signalId, symbol, tradeType);
                if (existingPosition != null)
                {
                    Print($"ABORT: Position already open for {signalId}. Race condition prevented.");
                    return result; 
                }

                // 2. Price Action Validation (The Circuit Breaker)
                // For scalping, if the retry loop delays execution by 2 seconds, 
                // the price may have moved beyond the acceptable risk/reward ratio.
                double currentPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
                double priceDeltaPips = Math.Abs(currentPrice - targetPrice) / Symbol.PipSize;

                if (priceDeltaPips > maxSlippagePips)
                {
                    Print($"ABORT: Price ran away during retry loop. Delta: {priceDeltaPips} pips.");
                    LogTelemetry(signalId, symbol, "PRICE_SLIPPAGE_EXCEEDED", attempt, targetPrice, currentPrice);
                    return result;
                }

                // 3. Asynchronous Execution Attempt (Does not block the main OnTick/OnBar thread)
                result = await ExecuteMarketOrderAsync(tradeType, symbol, volume, signalId, null, null);

                // 4. State Evaluation
                if (result.IsSuccessful)
                {
                    Print($"SUCCESS: {signalId} filled at {result.Position.EntryPrice}");
                    return result;
                }

                // 5. Advanced Error Routing
                switch (result.Error)
                {
                    case ErrorCode.Timeout:
                    case ErrorCode.Disconnected:
                    case ErrorCode.TechnicalError:
                    case ErrorCode.PriceHasChanged:
                        // Transient Error -> Execute Async Backoff
                        int jitter = _random.Next(50, 250);
                        LogTelemetry(signalId, symbol, result.Error.ToString(), attempt, targetPrice, currentPrice);
                        
                        // Task.Delay yields the thread back to the runtime, keeping the bot highly responsive
                        await Task.Delay(delayMs + jitter); 
                        delayMs *= 2;
                        break;

                    default:
                        // Terminal Error -> Hard Abort
                        LogTelemetry(signalId, symbol, $"TERMINAL_{result.Error}", attempt, targetPrice, currentPrice);
                        return result; 
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes rejection data for ingestion by external microservices.
        /// </summary>
        private void LogTelemetry(string signalId, string symbol, string error, int attempt, double target, double actual)
        {
            var telemetry = new RejectionTelemetry
            {
                SignalId = signalId,
                Symbol = symbol,
                ErrorCode = error,
                Attempt = attempt,
                EntryPriceTarget = target,
                ActualPrice = actual,
                TimestampUtc = DateTime.UtcNow
            };

            string jsonPayload = JsonSerializer.Serialize(telemetry);
            
            // Write locally for terminal diagnostics
            Print($"TELEMETRY: {jsonPayload}");

            // In a production environment, this is where you dispatch the JSON to your SQL DB via REST/RabbitMQ
            // e.g., _httpClient.PostAsync("https://api.yourdomain.com/v1/telemetry/rejections", content);
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors

Post by PTScalper »

Architectural Upgrades Justified

Task<TradeResult> and await ExecuteMarketOrderAsync:
Standard Thread.Sleep() freezes the robot's execution thread. If you are tracking multiple symbols, managing trailing stops, or reading indicator values on other charts concurrently, a synchronous 1500ms delay pauses everything. await Task.Delay() yields the thread back to cTrader's execution pool, keeping the rest of the bot responsive while the specific order routine waits out the broker timeout.

Price Delta Circuit Breaker:
When trading raw price action structures, the setup is invalidated if the price sweeps liquidity and runs away during a network timeout. The maxSlippagePips parameter calculates the real-time delta between your original targetPrice and the current spread. If the delay causes the price to move beyond your acceptable threshold, it aborts the trade rather than forcing a late entry.

Dependency Injection (IExecutionEngine):
By abstracting the logic behind an interface, you can write automated unit tests using a mocking framework (like Moq) to deliberately throw ErrorCode.Timeout and assert that your state machine correctly handles the backoff without executing duplicate simulated orders.

JSON Telemetry Serialization (RejectionTelemetry):
Instead of unstructured print statements, errors are mapped to a DTO and serialized using System.Text.Json. This exact payload can be pushed asynchronously via an HttpClient straight to your centralized enterprise logging pipeline.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply