iCustom returns empty or zero in MQL5 — causes and fixes
MQL5's iCustom looks like its MQL4 ancestor but behaves nothing like it, and that gap is behind most "my indicator returns nothing" reports. If your buffer reads come back as zero, EMPTY_VALUE, or the handle itself is -1, the cause is almost always one of five things. Here they are, in the order you should check them.
In MQL5, iCustom returns a handle, not a value
The single most common mistake is treating iCustom as if it still returned a price. It does not. It returns an int handle — an identifier the terminal uses to track the indicator instance. The values live behind that handle and only come out through CopyBuffer or CopyBuffer's relatives.
// Wrong: this is an int, not a price. It is -1 or a handle.
double ma = iCustom(_Symbol, PERIOD_CURRENT, "MyInd", 14);
// Right: create once, read many times.
int handle = iCustom(_Symbol, PERIOD_CURRENT, "MyInd", 14);
double buf[];
CopyBuffer(handle, 0, 0, 3, buf);
If you printed the "value" and saw -1, that is INVALID_HANDLE, not an indicator reading.
Symptom: the handle comes back as INVALID_HANDLE
Four things produce this, and GetLastError() straight afterwards tells you which.
The path is wrong. The name is relative to MQL5\Indicators, without the .ex5 extension, using backslashes for subfolders: "MyIndicators\\TrendProbe". An indicator sitting in MQL5\Indicators\TrendProbe.mq5 is just "TrendProbe".
The indicator failed to initialise. If the custom indicator's own OnInit returns INIT_FAILED, or it needs a symbol or timeframe that does not exist yet, iCustom hands you INVALID_HANDLE and no explanation beyond the error code. Compile it, attach it to a chart, and confirm it draws before blaming the EA.
The parameter list does not match. The types and order of the arguments after the name must correspond to the indicator's input declarations. Get one wrong and the handle fails. Get one missing and it does not fail — the indicator quietly runs with the default, which is worse, because your values look plausible and are simply not what the chart shows.
Multi-symbol handles in the tester. A handle is bound to one symbol/timeframe pair. In the Strategy Tester only the tested symbol is reliably available; requesting a second symbol gives you a handle that never calculates.
Symptom: CopyBuffer returns -1 or 0
If the handle is valid but the count is not, the indicator has not calculated that far back yet. BarsCalculated(handle) answers this directly. A return of -1 means the handle is dead; anything less than the bar index you are asking for means "not ready", not "broken".
The usual culprit is reading inside OnInit. At that moment the indicator has been created but not calculated. Read on the first tick instead, and guard it:
if(BarsCalculated(handle) <= shift)
return; // try again next tick, do not treat as an error
Error 4806 (ERR_INDICATOR_DATA_NOT_FOUND) is the same story. Asking for 100 values when 12 exist also returns fewer than requested — check the returned count, not just != -1.
Symptom: values arrive, but they are zero or nonsense
Here the plumbing works and the content is wrong. Three causes dominate.
Wrong buffer index. Buffers are numbered in the order the indicator calls SetIndexBuffer. If TrendProbe sets its signal line as buffer 0 and its histogram as buffer 1, reading buffer 1 for the signal gives you whatever the histogram holds — often 0.0 on quiet bars.
You are looking at EMPTY_VALUE. When an indicator has nothing to plot, the buffer contains EMPTY_VALUE (DBL_MAX), not zero. Cast it to int, compare it to a price, or feed it into arithmetic and you get garbage that looks like a bug. Always validate before use.
Series order. The receiving array's AS_SERIES flag decides whether index 0 is the newest or the oldest bar. Copy three values with the flag set one way, then index them as if it were the other, and every reading is shifted — which traders misread as "the indicator is lagging" or "returns zero in the tester".
A safe read helper
Create the handle once, validate every read, release on unload:
int g_handle = INVALID_HANDLE;
string g_symbol;
ENUM_TIMEFRAMES g_tf;
int OnInit()
{
g_symbol = _Symbol;
g_tf = PERIOD_CURRENT;
g_handle = iCustom(g_symbol, g_tf, "MyIndicators\\TrendProbe",
14, // int period
2.0, // double multiplier
false); // bool useClose
if(g_handle == INVALID_HANDLE)
{
PrintFormat("iCustom failed, error %d", GetLastError());
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(g_handle != INVALID_HANDLE)
IndicatorRelease(g_handle);
}
bool ReadBufferValue(int handle, int buffer, int shift, double &value)
{
value = EMPTY_VALUE;
if(handle == INVALID_HANDLE)
return false;
int calculated = BarsCalculated(handle);
if(calculated <= shift)
{
if(calculated < 0)
PrintFormat("BarsCalculated failed, error %d", GetLastError());
return false;
}
double tmp[];
ArraySetAsSeries(tmp, false);
int copied = CopyBuffer(handle, buffer, shift, 1, tmp);
if(copied != 1)
{
PrintFormat("CopyBuffer(%d,%d) copied %d, error %d",
buffer, shift, copied, GetLastError());
return false;
}
if(!MathIsValidNumber(tmp[0]) || tmp[0] == EMPTY_VALUE)
return false;
value = tmp[0];
return true;
}
void OnTick()
{
double value;
if(!ReadBufferValue(g_handle, 0, 1, value))
return;
// use value
}
Two details worth keeping: call GetLastError() immediately, because any intervening call resets it, and never call iCustom inside OnTick. Each call creates a new instance, and a few thousand ticks later the terminal is tracking handles you have long forgotten.
When it still looks wrong, test it
Wire the helper into a trivial rule and run it in the Strategy Tester over a decent sample. As a reference point for what correctly-wired custom indicator reads look like on a live-ish instrument, [see a real backtest](https://mql.ranartech.com/r/8dlSBBZ7Lm1v) — XAUUSD M15 EA (+$3,412, 66% win, 192 trades). If your version trades nothing at all, the handle is fine and your buffer index or EMPTY_VALUE check is not.