DEV Community

Omais Ali
Omais Ali

Posted on

Limiting an MQL5 EA to a few trades per day using the deal history

A common request for an Expert Advisor is a daily trade limit: no more than three new entries per day, and a pause after a losing trade before the next one. It sounds like a counter that goes up by one on every entry and resets at midnight.

A plain counter in a global variable breaks easily. It goes back to zero when the terminal restarts, when the EA is reattached, when the inputs change and when the VPS reboots. After any of those the EA happily takes three more trades on a day that already had three.

The account history already knows how many trades were opened today. This tutorial reads it instead of keeping a counter.

Where the day starts

Deal times in the history are trade server times, so the day must start at midnight on the server clock, not on your local clock.

#include <Trade\Trade.mqh>

input int  InpMaxTradesPerDay = 3;     // new entries allowed per server day
input int  InpCooldownSeconds = 3600;  // pause after a losing trade closes
input long InpMagic           = 24031;

CTrade trade;

datetime ServerDayStart(const datetime t)
{
   MqlDateTime dt;
   TimeToStruct(t, dt);
   dt.hour = 0;
   dt.min  = 0;
   dt.sec  = 0;
   return StructToTime(dt);
}
Enter fullscreen mode Exit fullscreen mode

Reading today's deals

HistorySelect() loads the deals and orders in a time range. The range has to cover two things: everything since the start of the server day, for the count, and the last cooldown period, for the losing trade check. A loss at five to midnight should still block a trade at five past.

// Returns the number of entries opened today by this EA, or -1 on failure.
// lastLossClose receives the time of the most recent losing exit.
int ReadTodayHistory(datetime &lastLossClose)
{
   lastLossClose = 0;
   datetime now      = TimeTradeServer();
   datetime dayStart = ServerDayStart(now);
   datetime from     = dayStart;
   if(now - InpCooldownSeconds < from)
      from = now - InpCooldownSeconds;

   if(!HistorySelect(from, now + 60))
      return -1;

   int entries = 0;
   int total   = HistoryDealsTotal();
   for(int i = 0; i < total; i++)
   {
      ulong deal = HistoryDealGetTicket(i);
      if(deal == 0)
         continue;
      if(HistoryDealGetInteger(deal, DEAL_MAGIC) != InpMagic)
         continue;
      if(HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol)
         continue;

      datetime time  = (datetime)HistoryDealGetInteger(deal, DEAL_TIME);
      long     entry = HistoryDealGetInteger(deal, DEAL_ENTRY);

      if((entry == DEAL_ENTRY_IN || entry == DEAL_ENTRY_INOUT) && time >= dayStart)
         entries++;

      if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT || entry == DEAL_ENTRY_OUT_BY)
      {
         double result = HistoryDealGetDouble(deal, DEAL_PROFIT)
                       + HistoryDealGetDouble(deal, DEAL_SWAP)
                       + HistoryDealGetDouble(deal, DEAL_COMMISSION);
         if(result < 0.0 && time > lastLossClose)
            lastLossClose = time;
      }
   }
   return entries;
}
Enter fullscreen mode Exit fullscreen mode

A few details in there are easy to miss.

  1. The end of the range is now plus a minute. TimeTradeServer() is the terminal's estimate of server time, and a deal that just happened can be stamped a moment later than that estimate.
  2. DEAL_ENTRY_INOUT is a reversal on a netting account. It closes one position and opens the opposite one in a single deal, so it counts as an entry and can also be the losing exit.
  3. The loss check adds swap and commission to the profit. A trade that closed slightly in profit but paid more than that in commission is a losing trade for the account.
  4. Exits caused by a stop loss or take profit carry the position's magic number on a normal MT5 server, so they pass the magic filter. If you ever see a broker where they do not, match exits to entries with DEAL_POSITION_ID instead.

Using it before every entry

bool EntryAllowed()
{
   datetime lastLoss;
   int done = ReadTodayHistory(lastLoss);

   if(done < 0)
      return false;                       // history not available, fail closed
   if(done >= InpMaxTradesPerDay)
      return false;
   if(lastLoss > 0 && TimeTradeServer() - lastLoss < InpCooldownSeconds)
      return false;
   if(PositionSelect(_Symbol))
      return false;                       // one position at a time in this example
   return true;
}

void OnTick()
{
   // your entry signal goes here
   bool signal = false;

   if(signal && EntryAllowed())
   {
      trade.SetExpertMagicNumber(InpMagic);
      trade.Buy(0.10, _Symbol);
   }
}
Enter fullscreen mode Exit fullscreen mode

Note that PositionSelect(_Symbol) only checks that some position on the symbol exists. On a hedging account with several EAs on one symbol, loop over the positions and compare the magic number instead.

The function returns false when the history cannot be loaded. For a limit like this, failing closed is the right choice: a missed trade costs less than an unwanted fourth one.

Pending orders

If the EA enters with pending orders, a limit order placed today may fill tomorrow, and it counts on the day it fills. If the rule is meant to count placed orders instead, read HistoryOrdersTotal() and the open orders with OrdersTotal() as well, and decide which one the trader actually wants. Write that decision into the input description so nobody has to guess.

Testing it

The Strategy Tester keeps a full deal history, so this logic can be tested there.

  1. Run a visual test with InpMaxTradesPerDay set to 1 and confirm there is never more than one entry per server day in the results.
  2. Set a long cooldown and check that no entry appears within that time after a losing exit, including across midnight.
  3. On a demo account, let the EA trade, then restart the terminal and confirm it does not open extra trades on the same day.
  4. Change the inputs while the EA is running. The count must survive that too, because it comes from the history and not from memory.

Reading the history costs a little more than incrementing a variable, but the answer is always correct, whatever happened to the terminal in between.

Top comments (0)