Page 2 of 2
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:25 pm
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;
}
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:26 pm
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.
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:26 pm
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;
}
}
}
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:26 pm
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.
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:27 pm
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.
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:27 pm
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);
}
}
}
Re: Order Retry Logic in EAs: Avoiding Order Storms After Server Errors
Posted: Wed Oct 07, 2026 6:28 pm
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.