Edgoro
Script · MQL5

MQL5 buy limit and buy stop orders with expiration

Short answer

Use trade.BuyLimit() below the Ask or trade.BuyStop() above it (a sell limit goes above the Bid, a sell stop below it), with ORDER_TIME_SPECIFIED and an expiration in server time, such as TimeTradeServer() + 4 * 3600. Check SYMBOL_EXPIRATION_MODE first, and expect retcode 10008 (TRADE_RETCODE_PLACED), not 10009, when the order is accepted.

UsesCTrade::BuyLimitCTrade::BuyStopCTrade::SellLimitCTrade::SellStopSYMBOL_EXPIRATION_MODETimeTradeServer

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.

MQL5
#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 == limit is 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_LEVEL points from ref. 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, unlike TimeCurrent(), 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_SPECIFIED where only good till cancelled is accepted also gives 10022; CTrade prints "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 _Digits is refused with 10015. Round to the tick size as the script does.

Variations

  • Until the end of the day. ORDER_TIME_DAY with 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 at limitPrice once the Ask reaches stopPrice.
  • Expiry handled by the EA. Where the broker accepts no expiry time, an EA can delete its own orders: loop over OrdersTotal(), compare ORDER_TIME_SETUP plus the lifetime with TimeTradeServer(), and call trade.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.

All MQL5 errors

Want it inside a complete EA?

Breakout and pullback entries with pending orders are a common request to Edgoro's MT5 EA generator: it places the order on the right side of the market, with an expiry the symbol accepts. A clean build proves the code compiles, not that a strategy works: test it on a demo account.