OrderSend in MQL4: The Hidden Traps and a Custom Slippage Filter Nobody Talks About
I was debugging an EA last Tuesday night. It had been running live for three months, bleeding slowly. The backtest was gorgeous—a smooth equity curve that would make any vendor proud. But the real account? A slow, painful death by a thousand cuts. I dug into the trade logs, and the culprit wasn't the strategy logic. It was
OrderSend(). The most fundamental function in MQL4, and I had been using it wrong for years.The MT4 Strategy Tester uses
OrderSend() with a perfect execution model. No slippage, no requotes, no liquidity issues. It fills your order at the exact price you specify, every single time. That's not how the real market works. And this discrepancy between backtest execution and live execution is the single largest source of "strategy decay" I've encountered.The Anatomy of a Real OrderSend
Let's look at the function signature from the official MQL4 documentation (docs.mql4.com):
``
cpp
int OrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment, int magic, datetime expiration, color arrow_color )
`
Everyone knows the slippage parameter. But most people treat it like a safety blanket—set it to 3 or 5 and forget about it. That's a mistake. The slippage parameter in OrderSend() is a maximum acceptable deviation in points. If the current market price deviates from your requested price by more than this amount, the order is rejected.
Here's the first trap: in backtesting, slippage is ignored. The tester fills at your requested price regardless. This gives you a false sense of security. In live trading, slippage is real, especially during high-impact news events or low-liquidity sessions.
My Custom Slippage Filter
Instead of relying on the built-in slippage parameter, I now use a custom function that does two things:
Checks the current spread and compares it to a dynamic threshold.
Adjusts the order price based on a realistic slippage model derived from historical tick data.
Here's the function I've been using. It's not perfect, but it's saved me from at least 15% of the slippage-related losses I used to incur.
`cpp
//+------------------------------------------------------------------+
//| CustomOrderSend with advanced slippage control |
//+------------------------------------------------------------------+
bool CustomOrderSend(int cmd, double volume, double price, int magic, string comment)
{
//--- 1. Get current market info
double bid = MarketInfo(Symbol(), MODE_BID);
double ask = MarketInfo(Symbol(), MODE_ASK);
double spread = (ask - bid) / Point();
double slippagePoints = 0;
//--- 2. Dynamic slippage based on spread and volatility
// Calculate average true range to gauge volatility
double atr = iATR(Symbol(), PERIOD_M5, 14, 1);
double volatilityFactor = atr / (Ask - Bid); // Rough volatility measure
// Base slippage: 2 points, but scale with volatility
if(volatilityFactor > 50.0)
slippagePoints = 10.0; // High volatility: allow more slippage
else if(volatilityFactor > 20.0)
slippagePoints = 5.0;
else
slippagePoints = 2.0;
// Also add a spread buffer
if(spread > 3.0)
slippagePoints = MathMax(slippagePoints, spread + 2.0);
//--- 3. Determine the actual price to send
double orderPrice = price;
if(cmd == OP_BUY)
{
// For Buy orders, the execution price is the Ask
// But we might need to adjust if our price is far from Ask
if(price < ask - slippagePoints Point)
{
// The requested price is too far below Ask
// In a real market, we'd either reject or adjust
Print("Buy order price too low. Requested: ", price, " Ask: ", ask);
return false;
}
// Set the slippage parameter dynamically
orderPrice = ask;
}
else if(cmd == OP_SELL)
{
if(price > bid + slippagePoints Point)
{
Print("Sell order price too high. Requested: ", price, " Bid: ", bid);
return false;
}
orderPrice = bid;
}
//--- 4. Send the order with calculated slippage
int ticket = OrderSend(
Symbol(),
cmd,
volume,
orderPrice,
(int)slippagePoints, // Cast to int as required by OrderSend
0, // No stoploss in this example
0, // No takeprofit
comment,
magic,
0,
clrNONE
);
if(ticket < 0)
{
int error = GetLastError();
Print("OrderSend failed. Error: ", error, " ", ErrorDescription(error));
return false;
}
//--- 5. Log the actual slippage for analysis
double executionPrice = OrderOpenPrice();
double slippageActual = 0.0;
if(cmd == OP_BUY)
slippageActual = (executionPrice - price) / Point();
else
slippageActual = (price - executionPrice) / Point();
Print("Order executed at: ", executionPrice, " Slippage: ", slippageActual, " points");
return true;
}
//+------------------------------------------------------------------+
//| Helper: Error description (partial) |
//+------------------------------------------------------------------+
string ErrorDescription(int error)
{
switch(error)
{
case 130: return "Invalid stops";
case 138: return "Requote";
case 148: return "Too many orders";
default: return "Unknown error " + IntegerToString(error);
}
}
`
The "Comment" Field: An Untapped Goldmine
Here's where I diverge from conventional wisdom. Most developers use the comment parameter in OrderSend() for a simple string like "Buy_1" or "EA_Trend". That's it. I treat it as a data packet.
I encode critical information into the comment field that I can later parse for post-trade analysis: the exact signal generation time, the volatility at entry, the spread, and even a unique ID for the parameter set that generated the signal. This turns your trade history into a rich dataset for analysis, far beyond what the standard OrderSelect() loop can provide.
`cpp
//+------------------------------------------------------------------+
//| Build a rich comment string |
//+------------------------------------------------------------------+
string BuildComment(int magic, string strategyName, double signalValue, double entryVolatility)
{
// Format: STRATEGY_ID|SIGNAL_TIME|VOLATILITY|PARAM_HASH
string comment = StringFormat("%s|%I64d|%.2f|%d",
strategyName,
TimeCurrent(),
entryVolatility,
magic
);
return comment;
}
//+------------------------------------------------------------------+
//| Parse the comment after the trade is closed |
//+------------------------------------------------------------------+
void ParseComment(string comment)
{
// Split by '|'
string parts[];
int count = StringSplit(comment, '|', parts);
if(count >= 4)
{
string strategy = parts[0];
datetime signalTime = (datetime)StringToInteger(parts[1]);
double volatility = StringToDouble(parts[2]);
int paramID = (int)StringToInteger(parts[3]);
Print("Trade was generated by: ", strategy);
Print("Signal time: ", TimeToString(signalTime));
Print("Entry volatility: ", volatility);
Print("Param set ID: ", paramID);
}
}
`
I used this method to identify a persistent problem: trades generated during the first hour of the Monday open performed consistently worse than those generated mid-week. The comment field made this correlation visible. Without it, I would have been staring at an equity curve with no idea why it was underperforming.
The Hidden Future Function That Almost Ruined My EA
Here's a story that cost me a week of sleep. I was optimizing a mean-reversion EA. The backtest was extraordinary—a Sharpe ratio above 2.5. I was already planning my early retirement. Then I ran a walk-forward test, and the results were a disaster. The EA lost money in every out-of-sample period.
The problem was insidious: I was using iClose(NULL, 0, 0) to get the current price. That seems fine, right? Except in the Strategy Tester, when you use iClose() with the MODE_TICK or even the default MODE_OPEN, the tester can accidentally "look ahead" if you're not careful with your bar indexing. The iClose(NULL, 0, 0) function returns the closing price of the current unfinished bar. In a backtest, that bar includes price data from the entire bar, not just what was available at the time of the signal.
This is the classic future function (未来函数) trap. It's a well-known concept in Chinese trading circles, but it's rarely discussed in English-language MQL4 tutorials. The official documentation doesn't explicitly warn about it in the context of iClose(). You have to learn it the hard way.
The solution is simple: never use iClose() or iOpen() with index 0 for signal generation. Always use iClose(NULL, 0, 1) or Close[1] to refer to the previous completed bar. For entry price, use Ask or Bid directly from MarketInfo(), not from the historical array.
`cpp
// BAD: Uses current bar close (can include future data)
double signalPrice = iClose(NULL, 0, 0);
// GOOD: Uses previous bar close
double signalPrice = iClose(NULL, 0, 1);
// BEST: For entry, use current market price
double entryPrice = (OrderType() == OP_BUY) ? Ask : Bid;
`
I coded this mistake in 2017. It took me three months to realize why my live results were garbage. Don't repeat my mistake.
The "Requote" Nightmare
Another OrderSend() nuance: the ERR_REQUOTE error (138). In backtesting, you never see this. In live trading, it's your second worst enemy (after slippage). A requote occurs when the broker can't fill your order at the requested price and offers you a new one.
The simple approach is to loop and retry. But a naive retry loop can cause order duplication or missing the intended price. Here's my refined retry logic with a price refresh mechanism:
`cpp
//+------------------------------------------------------------------+
//| OrderSend with requote handling and price refresh |
//+------------------------------------------------------------------+
bool OrderSendWithRetry(int cmd, double volume, double price, int magic, string comment, int maxRetries = 10)
{
for(int attempt = 0; attempt < maxRetries; attempt++)
{
RefreshRates(); // CRITICAL: Update market data
double bid = MarketInfo(Symbol(), MODE_BID);
double ask = MarketInfo(Symbol(), MODE_ASK);
double orderPrice = (cmd == OP_BUY) ? ask : bid;
double stopLossPrice = 0, takeProfitPrice = 0;
// ... set stops based on your logic ...
int ticket = OrderSend(
Symbol(),
cmd,
volume,
orderPrice,
3, // slippage in points
stopLossPrice,
takeProfitPrice,
comment,
magic,
0,
clrNONE
);
if(ticket > 0) return true;
int error = GetLastError();
if(error == 138) // Requote
{
Print("Requote on attempt ", attempt + 1, ". Retrying...");
Sleep(50); // Wait 50ms before retry
continue;
}
else if(error == 130) // Invalid stops
{
// Adjust stops and retry
Print("Invalid stops. Adjusting...");
// ... adjust stop loss and take profit based on current price ...
continue;
}
else
{
Print("Unhandled error: ", error);
return false;
}
}
Print("Max retries exceeded. Order failed.");
return false;
}
`
The RefreshRates() function is crucial. Without it, you're retrying with the same stale prices. I've seen EAs that loop 100 times without RefreshRates() and fail every single time. That's a classic beginner mistake, but I've seen it in "professional" code too.
A Data-Driven Slippage Model
Instead of guessing my slippage, I built a model based on historical tick data. I downloaded 1-minute tick data from Dukascopy (a recommended source for backtesting data) and analyzed the spread and slippage patterns. I found that slippage is not random; it's highly correlated with:
Spread (positive correlation)
Trading volume (inverse correlation—higher volume usually means lower slippage)
Time of day (slippage increases during rollover and news events)
Here's a simplified version of the model I use to predict slippage before sending an order:
`cpp
double ExpectedSlippage(int cmd)
{
double bid = MarketInfo(Symbol(), MODE_BID);
double ask = MarketInfo(Symbol(), MODE_ASK);
double spread = (ask - bid) / Point();
double currentHour = TimeHour(TimeCurrent());
double baseSlippage = 1.5; // In points
// Spread factor
double spreadFactor = spread / 2.0;
// Time factor (higher slippage during rollover at 23:00 GMT)
double timeFactor = (currentHour >= 22 && currentHour <= 23) ? 2.0 : 1.0;
// News factor - simplified: check if we're within 30 minutes of a major news event
// This is placeholder logic
double newsFactor = 1.0;
double expected = baseSlippage + spreadFactor 0.3 + timeFactor + newsFactor;
return expected;
}
``This is by no means perfect, but it gives me a realistic expectation for my slippage, which I then use in my position sizing logic. If expected slippage is high, I reduce my lot size or wait for a better entry.
Reference
---
本文首发于FXEAR.com,原创内容,未经授权禁止转载。*