Why OnCalculate and OnTick Are Not Interchangeable
If you've written even a single custom indicator in MQL5, you've stared at the OnCalculate() function template the MetaEditor generates by default. Most new developers assume it works like the OnTick() handler they use in Expert Advisors — just a different name, same idea. That assumption will cost you repainting indicators, wrong buffer values on historical bars, and silent performance hits that only show up in the Strategy Tester.
I've seen this mistake more times than I can count. A developer posts on the MQL5 forum asking why their indicator shows nothing in the Data Window, or why buffer values flicker between different numbers on the same bar. Nine times out of ten, they're trying to fill indicator buffers from inside OnTick. It's an easy trap to fall into — after all, OnTick runs on every tick in an EA, so why wouldn't it work the same way in an indicator?
This guide walks you through exactly when each event fires, how they interact with indicator buffers, and — more importantly — which one you should use for what. By the end, you'll know why OnCalculate is the only correct entry point for indicator calculations, and when you might still need OnTick inside an indicator (spoiler: rarely, and only for specific edge cases).
Prerequisites
Before we dig into the code, make sure you have these ready:
- MetaTrader 5 build 2000 or newer — older builds handle buffer indexing slightly differently. You can check your build under Help → About. If you're on build 1900 or earlier, some of the array indexing behavior differs, particularly around the
prev_calculatedparameter. Build 2000 introduced theSeriesInfoInteger()function improvements that affect how buffer arrays behave withArraySetAsSeries(). - A demo account with at least one currency pair chart open. You don't need a funded live account to test indicators. I usually use EURUSD on M15 for testing — enough tick activity to see real-time updates without being overwhelming. Avoid exotic pairs like USDZAR for initial testing; their low tick volume makes it harder to verify real-time buffer updates.
- Basic familiarity with the MetaEditor — you should know how to create a new indicator file (File → New → Custom Indicator) and compile it (F7). You should also know how to attach an indicator to a chart: drag the compiled
.ex5file from the Navigator panel onto the chart, or right-click the chart → Indicators List → Custom. The Navigator panel is usually on the left side of the MetaTrader window; if you don't see it, press Ctrl+N. - MQL5 knowledge at the level of understanding arrays, loops, and the
#propertydirective. You don't need to be an expert, but this isn't a "what is a variable" tutorial. If you're comfortable reading and modifying the default indicator template, you're good.
I'm assuming you've already attached a custom indicator to a chart at least once. If not, take five minutes to create the default MA indicator from the wizard and attach it — that'll give you a baseline to compare against. Pay attention to how the MA line updates in real time as new ticks arrive. Open the Data Window (Ctrl+D) and click on the indicator line to see the buffer values change tick by tick.
How OnCalculate Works — The Indicator Workhorse
Every MQL5 custom indicator you create must have an OnCalculate function. That's not optional — the compiler enforces it. The function signature looks intimidating at first, but once you break it down, it's straightforward:
int OnCalculate (const int rates_total, // total bars on chart
const int prev_calculated, // bars processed in previous call
const datetime &time[], // time array
const double &open[], // open array
const double &high[], // high array
const double &low[], // low array
const double &close[], // close array
const long &tick_volume[], // tick volume array
const long &volume[], // real volume array (if available)
const int &spread[]) // spread array
The OnCalculate function fires under these conditions:
- Every new tick that arrives for the symbol/timeframe the indicator is attached to.
- When the chart loads — for example, when you first attach the indicator, or when you switch timeframes. This triggers a full recalculation with
prev_calculated = 0. - When historical data changes — if you download missing history or the broker updates price data. This also sets
prev_calculated = 0.
That sounds a lot like OnTick, right? Here's the critical difference: OnCalculate gives you access to the entire price array for all bars on the chart, indexed from oldest (index 0) to newest (index rates_total - 1). You can read any bar's open, high, low, close — not just the current tick's price. This array access is passed by reference, meaning you're not copying data; you're reading directly from the platform's internal memory.
The prev_calculated Parameter — Your Performance Lever
The prev_calculated parameter tells you how many bars were already processed in the previous call. On the very first call, prev_calculated equals 0. On subsequent calls, it equals whatever value your function returned last time. This is the key to writing efficient indicators that don't recalculate history on every tick.
Here's where most beginners waste CPU cycles: they recalculate every bar on every tick. If you have 10,000 bars loaded and each tick triggers a loop over all 10,000, that's a lot of wasted work. On a fast-moving pair like GBPJPY, you might get 50-100 ticks per second during high volatility. That loop adds up fast.
Instead, use prev_calculated to only compute the new bar:
int OnCalculate(...)
{
// Determine starting index
int start = prev_calculated - 1; // keep last bar for recalc
if(start < 0) start = 0;
for(int i = start; i < rates_total; i++)
{
// Your indicator logic here
Buffer[i] = iMA(..., i);
}
return(rates_total);
}
When prev_calculated is 0 (first run), start becomes 0 and you calculate everything. On subsequent ticks, prev_calculated equals rates_total from the previous call, so start is the last bar — you only recalculate the current forming bar and any new bars that appeared since the last tick. This pattern alone can cut CPU usage by 99% on a chart with thousands of bars.
One nuance: notice I subtract 1 from prev_calculated to get start. This is because the current bar (index rates_total - 1) is still forming — it gets new ticks constantly, so we need to recalculate it every time. By starting at prev_calculated - 1, we include that last bar in the recalculation. If you used prev_calculated directly as the starting index, you'd skip the current bar entirely, and your indicator would only update when a new bar opens.
Let me show you what happens with a real example. Suppose you have a 50-period SMA indicator. On the first call with rates_total = 1000 and prev_calculated = 0, your loop runs from i=0 to i=999. That's 1000 iterations — acceptable for first load. On the next tick, prev_calculated = 1000, so your loop runs from i=999 to i=999. That's exactly 1 iteration. You just saved 999 calculations. Over 100 ticks, that's 99,900 fewer calculations. This matters when you're running multiple indicators on multiple timeframes simultaneously.
What OnCalculate Returns — And Why It Matters
The return value of OnCalculate is critical. You must return rates_total in most cases. This tells the platform: "I processed all bars up to this point." The platform stores this return value and passes it as prev_calculated on the next call.
If you return a value less than rates_total, say 100, then on the next tick prev_calculated will be 100 — and your start index logic will skip bars 0 through 99. That's fine if you only want to process the last N bars, but it can cause issues if your indicator needs to reference older bars for calculations (like a 200-period moving average). In that case, you might return rates_total but still only loop over the last N bars for performance.
The only time you should return 0 is when you can't calculate anything — for example, if rates_total is less than the minimum bars your indicator needs. Returning 0 tells the platform to call OnCalculate again with the same data on the next tick, rather than skipping ahead. I've seen this used in custom indicators that require at least 100 bars for a calculation — if rates_total < 100, return 0 and wait for more data.
Common OnCalculate Pitfalls
One mistake I see often: forgetting to handle the case where prev_calculated is negative. In some edge cases, like when the platform resets history, prev_calculated can be -1. Always clamp it:
int start = MathMax(prev_calculated - 1, 0);
Another pitfall: using ArraySetAsSeries() incorrectly inside OnCalculate. If you set the arrays as series (index 0 = newest bar), you must be consistent. I prefer to leave the default indexing (index 0 = oldest bar) and work forward. But if you do use series indexing, remember that rates_total - 1 becomes the oldest bar, and index 0 is the current bar. Your loop logic flips accordingly.
Here's a quick comparison table for array indexing modes:
| Indexing Mode | Index 0 | Index rates_total-1 | Loop Direction |
|---|---|---|---|
| Default (timeseries) | Oldest bar | Newest (current) bar | Forward (0 to rates_total-1) |
| Series (ArraySetAsSeries=TRUE) | Newest (current) bar | Oldest bar | Backward (0 to rates_total-1) |
How OnTick Works in Indicators — The Misunderstood Stepchild
You can include OnTick() in a custom indicator, but the MetaEditor won't generate it for you. You have to declare it manually. Here's the catch: OnTick in an indicator fires on every tick, just like in an EA, but it has no access to the buffered price arrays that OnCalculate provides. The platform doesn't pass any price data to OnTick — you get nothing but the current symbol and timeframe context.
Inside an indicator's OnTick, you can call CopyRates(), CopyClose(), etc., to manually fetch price data. But that's redundant — OnCalculate already passes you these arrays for free. Using OnTick for indicator buffer updates is almost always a mistake. I've seen developers write code like this:
void OnTick()
{
double close[];
ArraySetAsSeries(close, true);
CopyClose(Symbol(), Period(), 0, 100, close);
for(int i = 0; i < 100; i++)
{
MyBuffer[i] = close[i]; // This works, but it's wrong
}
}
This code "works" in the sense that it fills the buffer with values. But it's wrong because:
- OnTick may not fire for every historical bar in the tester. The tester simulates ticks, but it doesn't guarantee that
OnTickfires for every bar the wayOnCalculatedoes. Your indicator will show empty buffers in the tester. - You're copying data that's already available.
OnCalculatepasses you the close array directly. Why copy it again? - You lose the
prev_calculatedoptimization. Without it, you either recalculate everything or have to implement your own tracking. - Buffer synchronization issues. If
OnTickandOnCalculateboth try to write to the same buffer, you'll get race conditions. The platform doesn't guarantee the order of execution between these handlers.
There is exactly one legitimate use case for OnTick inside an indicator: when you need to react to a tick without recalculating all indicator buffers. For example, updating a label or a line object that shows the current bid/ask spread, without touching the buffer values that drive your main indicator. Or updating a status label that shows "New tick at 12:34






