The code
A script with inputs: pick the order type, its distance from the market and its lifetime. It places a real pending order, so run it on a demo account first.
Save it as MQL5/Scripts/pending-orders-with-expiration.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.
#property script_show_inputs
#include <Trade/Trade.mqh>
enum ENUM_PENDING
{
PENDING_BUY_LIMIT, // Buy limit (below the Ask)
PENDING_BUY_STOP, // Buy stop (above the Ask)
PENDING_SELL_LIMIT, // Sell limit (above the Bid)
PENDING_SELL_STOP // Sell stop (below the Bid)
};
input ENUM_PENDING InpType = PENDING_BUY_LIMIT; // Order type
input int InpDistance = 200; // Distance from the market, points
input double InpLots = 0.01; // Volume, lots
input int InpStopPoints = 300; // Stop-loss from the order price, points (0 = none)
input int InpTakePoints = 600; // Take-profit from the order price, points (0 = none)
input int InpMinutes = 240; // Expires after, minutes (0 = good till cancelled)
input ulong InpMagic = 20261112; // Magic number
double RoundToTick(const double price)
{
double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick <= 0.0)
return NormalizeDouble(price, _Digits);
return NormalizeDouble(MathRound(price / tick) * tick, _Digits);
}
void OnStart()
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick) || tick.ask <= 0.0)
return;
bool buy = (InpType == PENDING_BUY_LIMIT || InpType == PENDING_BUY_STOP);
bool limit = (InpType == PENDING_BUY_LIMIT || InpType == PENDING_SELL_LIMIT);
double ref = buy ? tick.ask : tick.bid; // a buy order triggers on the Ask, a sell order on the Bid
// Limits wait for a better price (buy below, sell above), stops for a breakout (buy above, sell below)
double dir = (buy == limit) ? -1.0 : 1.0;
double price = RoundToTick(ref + dir * InpDistance * _Point);
double stops = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
if(MathAbs(price - ref) < MathMax(stops, _Point))
{
PrintFormat("The order must be at least %d points from the market", (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL));
return;
}
// Stops of a pending order are measured from the order's own price
double sl = InpStopPoints > 0 ? RoundToTick(buy ? price - InpStopPoints * _Point : price + InpStopPoints * _Point) : 0.0;
double tp = InpTakePoints > 0 ? RoundToTick(buy ? price + InpTakePoints * _Point : price - InpTakePoints * _Point) : 0.0;
// Expiry: use a specified time only if the symbol allows it. Expiration is in SERVER time.
ENUM_ORDER_TYPE_TIME typeTime = ORDER_TIME_GTC;
datetime expiration = 0;
int modes = (int)SymbolInfoInteger(_Symbol, SYMBOL_EXPIRATION_MODE);
if(InpMinutes > 0)
{
if((modes & SYMBOL_EXPIRATION_SPECIFIED) == SYMBOL_EXPIRATION_SPECIFIED)
{
typeTime = ORDER_TIME_SPECIFIED;
expiration = TimeTradeServer() + InpMinutes * 60;
}
else
Print("This symbol doesn't accept an expiry time: the order is good till cancelled, delete it yourself");
}
CTrade trade;
trade.SetExpertMagicNumber(InpMagic);
bool ok = false;
switch(InpType)
{
case PENDING_BUY_LIMIT:
ok = trade.BuyLimit(InpLots, price, _Symbol, sl, tp, typeTime, expiration, "example");
break;
case PENDING_BUY_STOP:
ok = trade.BuyStop(InpLots, price, _Symbol, sl, tp, typeTime, expiration, "example");
break;
case PENDING_SELL_LIMIT:
ok = trade.SellLimit(InpLots, price, _Symbol, sl, tp, typeTime, expiration, "example");
break;
case PENDING_SELL_STOP:
ok = trade.SellStop(InpLots, price, _Symbol, sl, tp, typeTime, expiration, "example");
break;
}
// A pending order that was accepted returns 10008 (placed), not 10009 (done)
if(!ok || trade.ResultRetcode() != TRADE_RETCODE_PLACED)
{
PrintFormat("Order refused: retcode %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription());
return;
}
PrintFormat("Order #%I64u placed at %s, expires %s", trade.ResultOrder(), DoubleToString(price, _Digits),
expiration > 0 ? TimeToString(expiration) : "never (good till cancelled)");
}How it works
dir. Limit orders wait for a better price (a buy below the market, a sell above), stop orders for a breakout (a buy above, a sell below).buy == limitis true for a buy limit and a sell stop, the two orders placed below the market.ref. A buy order triggers when the Ask reaches its price, a sell order when the Bid does, so each is placed and checked against that price.- The stops level check. The order's price must be at least
SYMBOL_TRADE_STOPS_LEVELpoints fromref. Its own stop-loss and take-profit are measured from the order's price, not from the market. SYMBOL_EXPIRATION_MODE. A set of flags: 1 good till cancelled, 2 end of day, 4 specified time, 8 specified day. When the symbol doesn't accept a specified time, the script falls back to good till cancelled and says so.TimeTradeServer(). The expiration is compared with the server's clock.TimeTradeServer()keeps running between ticks, unlikeTimeCurrent(), which is the time of the last tick.- 10008. An accepted pending order returns
TRADE_RETCODE_PLACED;ResultOrder()is its ticket. It becomes a position only when price reaches it.
Pitfalls
- The wrong side of the market. A buy limit above the Ask, or a buy stop below it, is refused with 10015 invalid price. Mixing up limit and stop is the usual cause.
- Too close to the market, or stops too close to the order. Inside the stops level, the order price is refused, and a stop-loss or take-profit too close to the order's price fails with 10016 invalid stops.
- An expiration in local time.
TimeLocal()is your computer's clock, often hours away from the server's: the order expires at the wrong time, or is refused with 10022 (TRADE_RETCODE_INVALID_EXPIRATION) when that time is already past on the server. - An expiry type the symbol doesn't allow. Sending
ORDER_TIME_SPECIFIEDwhere only good till cancelled is accepted also gives 10022;CTradeprints "Invalid expiration type" and doesn't send. - A price off the tick grid. On symbols whose tick is larger than a point, a price rounded only to
_Digitsis refused with 10015. Round to the tick size as the script does.
Variations
- Until the end of the day.
ORDER_TIME_DAYwith an expiration of 0 cancels the order when the trading day ends, where the symbol allows it (flag 2). - Buy stop limit.
trade.OrderOpen(_Symbol, ORDER_TYPE_BUY_STOP_LIMIT, lots, limitPrice, stopPrice, sl, tp, ORDER_TIME_GTC, 0)places a buy limit atlimitPriceonce the Ask reachesstopPrice. - Expiry handled by the EA. Where the broker accepts no expiry time, an EA can delete its own orders: loop over
OrdersTotal(), compareORDER_TIME_SETUPplus the lifetime withTimeTradeServer(), and calltrade.OrderDelete(ticket). - One cancels the other. MetaTrader 5 has no OCO order. An EA places both orders and, when a deal from one arrives in
OnTradeTransaction(), deletes the other. - Moving an order.
trade.OrderModify(ticket, price, sl, tp, ORDER_TIME_SPECIFIED, expiration)changes it, subject to the same distances and to the freeze level near the market.
Questions
What is the difference between a buy limit and a buy stop in MT5?
A buy limit is placed below the current Ask and buys if price falls to it: you expect a pullback. A buy stop is placed above the Ask and buys if price rises to it: you expect a breakout. Sell limits go above the Bid and sell stops below it.
Is a pending order's expiration in server time or my local time?
Server time. Compute it from TimeTradeServer() (or TimeCurrent() while ticks are flowing), never from TimeLocal(), unless your computer happens to be in the server's time zone.
What happens to an expired pending order?
The server cancels it at the expiration time and it moves to the account history with the state expired (ORDER_STATE_EXPIRED). No position is opened and nothing else changes.
This program is a minimal example written to show one technique, not a trading system, and none of it is a recommendation to trade. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.