How to detect a new bar in MQL5 (the correct, tick-safe way)
Every EA and every indicator eventually needs to answer one question: has a new bar just opened? Get it wrong and your strategy fires several times inside the same candle, or skips bars entirely. The fix is small, but it has to be done properly.
Why the obvious approaches break
The two things beginners reach for are Bars(_Symbol, _Period) and Time[0].
Counting bars looks reasonable — if the count went up, a new bar appeared. But Bars() can jump by more than one when the terminal pulls in missing history, and it can return 0 while a symbol is still being synchronised. A weekend gap or a backfill can make it look like ten new bars arrived at once.
Time[0] gives you the open time of the current bar, which is the right ingredient, but on its own it tells you nothing. You need to remember what it was on the previous tick. That memory is the whole trick.
You should also avoid comparing against TimeCurrent(). In the tester, and occasionally live, a tick's timestamp can sit slightly before the bar it belongs to, and you end up with off-by-one-bar logic that is painful to debug.
The tick-safe pattern
Store the open time of the last bar you acted on. On every tick, read the current bar's open time with iTime. If it differs from your stored value, a new bar has opened — update the stored value and return true. If it matches, return false.
Do it once per tick, at the top of OnTick, and let everything downstream assume "one call per bar".
Step by step
1. Hold state in a variable that survives between ticks. A static local, a global, or a class member all work. A plain local variable does not.
2. Read the bar time with iTime, not Time[], not Bars(). iTime takes the symbol and timeframe explicitly, which matters the moment you go multi-symbol.
3. Guard against zero. iTime returns 0 when the series is not ready yet. Return false and wait for the next tick rather than treating 0 as a new bar.
4. Compare, update, return. Update the stored value *before* returning true, so the same bar cannot trigger twice.
5. Keep separate state per timeframe. If your EA watches M1 and H1, one shared variable will make them interfere with each other. Wrap it in a small class and create one instance per timeframe.
6. Decide what the first call should do. With the variable initialised to 0, the first tick always counts as a new bar. That is usually what you want — it lets the EA act on the current candle immediately after attaching. If you would rather wait for the next bar, prime the variable in OnInit with iTime(...).
A complete, compiling example
//+------------------------------------------------------------------+
//| NewBarDemo.mq5 |
//| Tick-safe new-bar detection in MQL5 |
//+------------------------------------------------------------------+
#property version "1.00"
#property description "Demonstrates tick-safe new-bar detection"
//--- Tracks the last seen bar open time for one symbol/timeframe pair.
class CNewBar
{
private:
string m_symbol;
ENUM_TIMEFRAMES m_period;
datetime m_last_bar_time;
public:
CNewBar() : m_symbol(""), m_period(PERIOD_CURRENT), m_last_bar_time(0) {}
//--- prime = true -> first tick is not treated as a new bar
//--- prime = false -> first tick triggers once for the current bar
bool Init(const string symbol, const ENUM_TIMEFRAMES period, const bool prime)
{
m_symbol = symbol;
m_period = period;
m_last_bar_time = prime ? iTime(m_symbol, m_period, 0) : 0;
return(true);
}
//--- true exactly once per bar, on the first tick of that bar
bool IsNewBar()
{
const datetime t = iTime(m_symbol, m_period, 0);
if(t == 0)
return(false); // series not ready yet
if(t == m_last_bar_time)
return(false); // still the same bar
m_last_bar_time = t; // remember before returning
return(true);
}
};
CNewBar M1Bars;
CNewBar H1Bars;
int OnInit()
{
M1Bars.Init(_Symbol, PERIOD_M1, false);
H1Bars.Init(_Symbol, PERIOD_H1, false);
return(INIT_SUCCEEDED);
}
void OnTick()
{
if(M1Bars.IsNewBar())
{
const datetime t = iTime(_Symbol, PERIOD_M1, 0);
Print("New M1 bar: ", TimeToString(t, TIME_DATE | TIME_MINUTES));
//--- put your once-per-M1-bar logic here
}
if(H1Bars.IsNewBar())
{
const datetime t = iTime(_Symbol, PERIOD_H1, 0);
Print("New H1 bar: ", TimeToString(t, TIME_DATE | TIME_MINUTES));
//--- put your once-per-H1-bar logic here
}
}
//+------------------------------------------------------------------+
Things that still catch people out
Indicators are different. OnTick does not exist in an indicator. In OnCalculate, the equivalent test is whether rates_total has grown since the last call, using prev_calculated. The bar-time trick still works if you need it, but the standard loop over prev_calculated to rates_total - 1 is normally enough.
History synchronisation fires the pattern. When the terminal downloads missing bars, iTime jumps and your code sees "a new bar". It is one call, not hundreds, so it rarely matters — but if you are doing something expensive like scanning thousands of bars, check the time gap before committing.
Multi-symbol EAs need one instance per symbol. The class above handles this: construct one per symbol/timeframe pair rather than sharing a single global.
The tester behaves the same way. Tick generation is modelled, but bar boundaries are honoured, so logic that works live will work in backtests. If you want to see how bar-level decisions translate into an actual trade list, [see a real backtest](https://mql.ranartech.com/r/EBjGC6HYYxvE) — a GBPUSD H1 EA, 359 trades, 46% win rate, -$3,953. Useful precisely because it is not a flattering result: it shows how a plausible per-bar rule set still loses money once spread and regime shifts are applied.
Once new-bar detection is solid, everything above it gets easier: no duplicate entries, no half-formed signals from a candle that has not closed, and no mystery about why a backtest and a live account disagree.
Build & backtest your own EA free →