DEV Community

Omais Ali
Omais Ali

Posted on

Why your MQL5 EA reads zero or garbage from indicators on the first ticks

A bug I see in many MT5 Expert Advisors looks like a strategy problem at first. The EA opens a trade right after it is attached to a chart, or right after the terminal restarts, and the entry makes no sense. The journal shows a moving average of 0, or a value like 1.7976931348623157e+308. A few minutes later everything works normally.

The cause is almost always the way indicator data is read. In MQL5, indicators are not functions that return a value. They are separate calculation objects that you create once and read from later, and they need time to calculate. Here is how I structure indicator access in every EA now.

1. Create handles once, in OnInit

Functions like iMA, iRSI and iATR return a handle, not a value. The terminal starts calculating the indicator in the background when you create it.

If you call iMA inside OnTick, the terminal usually hands back the same handle for identical parameters, so it may look harmless, but you pay a lookup on every tick, you make it easy to leak handles when parameters change, and you hide the fact that the first read after creation can fail. Create every handle in OnInit, check it, and fail the initialization if it is invalid. An EA that cannot build its indicators should not run at all.

2. Check that the data is ready before reading it

Right after creation, the indicator may not have calculated any bars yet. BarsCalculated(handle) returns how many bars are ready, or a negative value while it is still working. CopyBuffer will return a negative value or fewer values than you asked for, and GetLastError often reports 4806 (ERR_INDICATOR_DATA_NOT_FOUND).

The wrong reaction is to ignore the return value and use whatever is in the array. The array then holds zeros from initialization or leftovers from a previous tick, and your crossover logic happily compares them.

The right reaction is to treat "not ready" as a normal state: skip this tick and try again on the next one.

3. Read closed bars, and know your array direction

Bar 0 is the bar that is still forming. Its indicator value changes with every tick, so a signal on bar 0 can appear and vanish within the same minute. For most strategies, read from bar 1, the last closed bar.

CopyBuffer fills the array oldest first unless the array is set as a series. I always call ArraySetAsSeries(array, true) before copying, so index 0 means the most recent value I requested. Mixing the two conventions is a classic source of signals that fire one bar late or in the wrong direction.

4. Handle empty values

Some indicators, especially custom ones that draw arrows or gaps, write EMPTY_VALUE on bars where they have nothing to say. That is the huge number from the journal above. Compare against EMPTY_VALUE before using a value, otherwise a stop loss or a threshold check can be calculated from it.

5. Release handles in OnDeinit

Call IndicatorRelease for every handle when the EA is removed or reinitialized. It keeps the terminal clean when you switch timeframes or change inputs during testing.

Putting it together

input int InpFastPeriod = 20;
input int InpSlowPeriod = 50;

int fastHandle = INVALID_HANDLE;
int slowHandle = INVALID_HANDLE;

int OnInit()
{
   fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
   {
      Print("Could not create indicator handles, error ", GetLastError());
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   if(fastHandle != INVALID_HANDLE) IndicatorRelease(fastHandle);
   if(slowHandle != INVALID_HANDLE) IndicatorRelease(slowHandle);
}

// Copies 'count' closed bar values (starting at bar 1) into out[].
// out[0] is the last closed bar. Returns false if data is not ready.
bool ReadClosedBars(const int handle, const int count, double &out[])
{
   int ready = BarsCalculated(handle);
   if(ready < count + 1)
      return false;

   ArraySetAsSeries(out, true);
   int copied = CopyBuffer(handle, 0, 1, count, out);
   if(copied != count)
   {
      PrintFormat("CopyBuffer returned %d of %d values, error %d",
                  copied, count, GetLastError());
      return false;
   }

   for(int i = 0; i < count; i++)
      if(out[i] == EMPTY_VALUE || out[i] == 0.0)
         return false;

   return true;
}

void OnTick()
{
   double fast[], slow[];

   if(!ReadClosedBars(fastHandle, 2, fast) ||
      !ReadClosedBars(slowHandle, 2, slow))
      return;   // indicator not ready yet, try on the next tick

   bool crossUp   = fast[1] <= slow[1] && fast[0] > slow[0];
   bool crossDown = fast[1] >= slow[1] && fast[0] < slow[0];

   if(crossUp)
      Print("Fast EMA crossed above slow EMA on the last closed bar");
   if(crossDown)
      Print("Fast EMA crossed below slow EMA on the last closed bar");
}
Enter fullscreen mode Exit fullscreen mode

The zero check in the loop is a deliberate choice for a moving average, which can never be zero on a real price series. For an oscillator where zero is a legitimate value, remove it and rely on BarsCalculated and the copied count.

A note on other timeframes and symbols

If the handle is created on another timeframe or symbol, that history may not be loaded yet when the EA starts. BarsCalculated still protects you, but it can take longer. I also check SeriesInfoInteger(symbol, timeframe, SERIES_SYNCHRONIZED) before trusting a higher timeframe value, and log when the EA is waiting, so "no trades for the first two minutes" is explained in the journal instead of looking like a bug.

Testing it

The Strategy Tester rarely shows this problem, because history is prepared before the test starts. To reproduce it, attach the EA to a chart of a symbol you have not opened recently, or restart the terminal with the EA running, and watch the first lines of the Experts tab. With the guard in place you should see the EA wait rather than act on empty data.

Most "random bad trades after a restart" I have traced came down to one unchecked CopyBuffer call. It is a few lines of code to fix, and it removes a whole class of bugs that only appear on live charts.

Top comments (2)

Collapse
 
stratcorealpha profile image
Arnold Holm •

One extra guard before using this example for orders: crossUp or crossDown stays true on every tick while those two closed bars stay the same, so the current OnTick can print the same crossing repeatedly.

I'd keep the last processed closed-bar timestamp and update it only after both buffer reads succeed. Updating it before the reads would lose that bar if the first tick arrives before the indicators are ready.

That gives two useful checks: repeated ticks produce one decision, and a failed first read still gets another chance on the next tick. Restart deduplication needs a separate decision too.

Collapse
 
omaisali profile image
Omais Ali •

Good catch, and you are right that the example only shows the read side. In a real EA I gate the whole block on the closed bar time, something like this:

datetime barTime = iTime(_Symbol, _Period, 1);
if(barTime == 0 || barTime == lastProcessedBar)
   return;
// read both buffers here, return early if either read fails
lastProcessedBar = barTime;
Enter fullscreen mode Exit fullscreen mode

The order matters for exactly the reason you gave. The timestamp only moves forward after both reads succeed, so a first tick that arrives before BarsCalculated is ready just tries again on the next tick instead of skipping the bar.

For restarts I treat it as a separate question, as you said. lastProcessedBar starts at 0, so after a restart the EA would evaluate the last closed bar again. I usually decide that per strategy: either initialise it to the current closed bar in OnInit so nothing fires until a new bar forms, or let it evaluate once but check open positions and recent deals with the EA's magic number before acting, so it cannot enter the same signal twice.