An Expert Advisor does not see a chart the way a human sees it.
It receives data from the terminal.
That data arrives as ticks, candles, symbol properties, account information, and trade events. If the EA is going to make decisions from price movement, it needs to read that data correctly.
This article focuses on the basic market data that most beginner strategies use first: ticks, candles, and timeframes.
We will look at how an EA reacts to price updates, how candles are indexed in MQL5, how to work with completed candles, and how to avoid one of the most common beginner mistakes: making decisions from a candle that is still forming.
At the end, we will build a simple Expert Advisor that trades based on two completed candles.
Ticks
A tick is a new price update received by the terminal.
When an EA is attached to a chart, the function OnTick() runs whenever a new tick arrives for that chart symbol.
Here is the simplest possible tick counter.
#property strict
int tickCount = 0;
void OnTick()
{
tickCount++;
Comment(
"Ticks received: ", tickCount, "\n",
"Symbol: ", _Symbol
);
}Attach this EA to a chart and watch the number increase.
On an active symbol, the number may change quickly.
On a quiet symbol, it may barely move.
This matters because an EA that runs on every tick may execute its logic many times during a single candle.
That is fine for some systems, but it is not always what we want.
A candle-based strategy usually needs to check conditions once when a new candle begins.
Candles
A candle stores price information for a selected period.
In MQL5, the current chart timeframe is available through _Period.
The current chart symbol is available through _Symbol.
This example displays the current symbol and timeframe.
#property strict
void OnTick()
{
Comment(
"Symbol: ", _Symbol, "\n",
"Timeframe: ", EnumToString(_Period)
);
}The timeframe depends on the chart where the EA is attached.
If the chart is M15, _Period refers to M15.
If the chart is H1, _Period refers to H1.
An EA can use the chart timeframe, or it can read data from another timeframe. For now, we will mostly use the chart timeframe because it keeps the first examples easier to inspect.
Candle indexing
MQL5 reads candles by index. The current candle has index 0. The previous completed candle has index 1. The candle before that has index 2.
This distinction is important.
The current candle is still changing. Its high, low, and close can move before the candle closes.
The previous candle is already complete. Its open, high, low, and close are fixed.
For most beginner systems, completed candles are safer to use for signals.
This example reads the previous completed candle.
#property strict
void OnTick()
{
double openPrice = iOpen(_Symbol, _Period, 1);
double highPrice = iHigh(_Symbol, _Period, 1);
double lowPrice = iLow(_Symbol, _Period, 1);
double closePrice = iClose(_Symbol, _Period, 1);
Comment(
"Previous candle\n",
"Open: ", DoubleToString(openPrice, _Digits), "\n",
"High: ", DoubleToString(highPrice, _Digits), "\n",
"Low: ", DoubleToString(lowPrice, _Digits), "\n",
"Close: ", DoubleToString(closePrice, _Digits)
);
}This code reads one candle directly using iOpen(), iHigh(), iLow(), and iClose().
That is fine for small examples.
When an EA needs several candles, CopyRates() is usually cleaner.
Reading several candles with CopyRates
CopyRates() copies candle data into an array.
The array uses the MqlRates structure.
Each element contains the candle time, open, high, low, close, tick volume, spread, and real volume where available.
Here is a simple example.
#property strict
void OnTick()
{
MqlRates candles[];
ArraySetAsSeries(candles, true);
int copied = CopyRates(_Symbol, _Period, 0, 3, candles);
if(copied < 3)
{
Comment("Not enough candle data.");
return;
}
Comment(
"Current candle open: ", DoubleToString(candles[0].open, _Digits), "\n",
"Previous candle open: ", DoubleToString(candles[1].open, _Digits), "\n",
"Two candles ago open: ", DoubleToString(candles[2].open, _Digits)
);
}ArraySetAsSeries(candles, true) makes the newest candle appear first.
With that setting, candles[0] is the current candle.
candles[1] is the previous completed candle.
candles[2] is the completed candle before that.
This indexing style is useful because it matches how most chart logic is written.
Detecting a new candle
An EA receives ticks. A candle-based strategy often needs to act once per candle.
The usual solution is to store the opening time of the current candle.
When that time changes, a new candle has started.
#property strict
datetime lastBarTime = 0;
bool IsNewBar()
{
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
void OnTick()
{
if(IsNewBar())
{
Print("New candle started.");
}
}This function is small, but it changes the behavior of the EA.
The EA still receives every tick.
The main logic can now run only when a new candle starts.
That is useful for candle strategies because the signal can be based on candles that have fully closed.
Reading candle direction
A candle closes above its open when the close is higher than the open.
A candle closes below its open when the close is lower than the open.
This example checks the previous completed candle.
#property strict
void OnTick()
{
double openPrice = iOpen(_Symbol, _Period, 1);
double closePrice = iClose(_Symbol, _Period, 1);
if(closePrice > openPrice)
{
Comment("Previous candle closed up.");
}
else if(closePrice < openPrice)
{
Comment("Previous candle closed down.");
}
else
{
Comment("Previous candle closed unchanged.");
}
}This is basic information, but many strategies start from this kind of condition.
The important point is that the EA is using candle 1, not candle 0.
Candle 1 is complete.
Candle 0 is still forming.
Avoiding signals from the forming candle
This is a common beginner error.
The current candle may look bullish while it is forming. A few ticks later, it may no longer look bullish.
The same candle can change direction many times before it closes.
This code reads the current candle.
double currentOpen = iOpen(_Symbol, _Period, 0);
double currentClose = iClose(_Symbol, _Period, 0);That can be useful in some systems, but it should be intentional.
For beginner candle strategies, it is usually better to base entries on completed candles.
This code reads the previous completed candle.
double previousOpen = iOpen(_Symbol, _Period, 1);
double previousClose = iClose(_Symbol, _Period, 1);That gives the EA stable data.
The signal will not change after the candle has closed.
Quant Atlas’ indicators are now on Metatrader5!
Go to the MQL marketplace to find our products!
Working with a chosen signal timeframe
An EA does not have to use the chart timeframe.
It can read signals from a separate timeframe.
For example, the EA can be attached to an M5 chart but read candle data from H1.
This version adds an input for the signal timeframe.
#property strict
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_H1;
void OnTick()
{
double previousOpen = iOpen(_Symbol, SignalTimeframe, 1);
double previousClose = iClose(_Symbol, SignalTimeframe, 1);
Comment(
"Signal timeframe: ", EnumToString(SignalTimeframe), "\n",
"Previous open: ", DoubleToString(previousOpen, _Digits), "\n",
"Previous close: ", DoubleToString(previousClose, _Digits)
);
}This makes the EA more flexible.
The chart timeframe and the signal timeframe can be different.
When doing this, the new-candle detection should use the signal timeframe, not necessarily the chart timeframe.
datetime lastSignalBarTime = 0;
bool IsNewSignalBar()
{
datetime currentSignalBarTime = iTime(_Symbol, SignalTimeframe, 0);
if(currentSignalBarTime != lastSignalBarTime)
{
lastSignalBarTime = currentSignalBarTime;
return true;
}
return false;
}This keeps the EA aligned with the candles used for the signal.
A candle data monitor
Before building the strategy, here is a small monitor EA.
It reads the last two completed candles on a selected timeframe and shows their direction.
#property strict
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_CURRENT;
ENUM_TIMEFRAMES GetSignalTimeframe()
{
if(SignalTimeframe == PERIOD_CURRENT)
return _Period;
return SignalTimeframe;
}
void OnTick()
{
ENUM_TIMEFRAMES timeframe = GetSignalTimeframe();
MqlRates candles[];
ArraySetAsSeries(candles, true);
int copied = CopyRates(_Symbol, timeframe, 0, 3, candles);
if(copied < 3)
{
Comment("Not enough candle data.");
return;
}
string previousDirection = GetCandleDirection(candles[1]);
string secondPreviousDirection = GetCandleDirection(candles[2]);
Comment(
"Symbol: ", _Symbol, "\n",
"Signal timeframe: ", EnumToString(timeframe), "\n",
"Previous candle: ", previousDirection, "\n",
"Second previous candle: ", secondPreviousDirection
);
}
string GetCandleDirection(const MqlRates &candle)
{
if(candle.close > candle.open)
return "up";
if(candle.close < candle.open)
return "down";
return "unchanged";
}This code uses a helper function to keep the main logic readable.
GetCandleDirection() receives one candle and returns a text description.
The EA does not trade.
It only displays what it sees.
Simple strategy: two-candle continuation
The strategy at the end of this article uses only completed candles.
It checks the last two completed candles.
If both closed up, the EA opens a buy position at the start of the next candle.
If both closed down, the EA opens a sell position at the start of the next candle.
The EA only checks once per signal candle.
It allows one open position per symbol.
It uses fixed stop loss and take profit values.
It also includes a spread filter.
This is a basic example for learning candle-based execution. It is not a complete trading system.
Complete Expert Advisor
#property strict
#include <Trade/Trade.mqh>
CTrade trade;
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_CURRENT;
input double Lots = 0.10;
input int StopLossPoints = 200;
input int TakeProfitPoints = 400;
input int MaxSpreadPoints = 30;
input int MagicNumber = 10002;
datetime lastSignalBarTime = 0;
int OnInit()
{
trade.SetExpertMagicNumber(MagicNumber);
Print("Two-Candle Continuation EA started.");
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
Comment("");
Print("Two-Candle Continuation EA stopped.");
}
void OnTick()
{
ENUM_TIMEFRAMES timeframe = GetSignalTimeframe();
if(!IsNewSignalBar(timeframe))
return;
if(PositionSelect(_Symbol))
return;
if(!SpreadIsAcceptable())
return;
MqlRates candles[];
ArraySetAsSeries(candles, true);
int copied = CopyRates(_Symbol, timeframe, 0, 3, candles);
if(copied < 3)
{
Print("Not enough candle data.");
return;
}
bool previousCandleUp = candles[1].close > candles[1].open;
bool secondPreviousCandleUp = candles[2].close > candles[2].open;
bool previousCandleDown = candles[1].close < candles[1].open;
bool secondPreviousCandleDown = candles[2].close < candles[2].open;
ShowStatus(timeframe, candles);
if(previousCandleUp && secondPreviousCandleUp)
{
OpenBuy();
return;
}
if(previousCandleDown && secondPreviousCandleDown)
{
OpenSell();
return;
}
Print("No signal on this candle.");
}
ENUM_TIMEFRAMES GetSignalTimeframe()
{
if(SignalTimeframe == PERIOD_CURRENT)
return _Period;
return SignalTimeframe;
}
bool IsNewSignalBar(ENUM_TIMEFRAMES timeframe)
{
datetime currentBarTime = iTime(_Symbol, timeframe, 0);
if(currentBarTime == 0)
return false;
if(currentBarTime == lastSignalBarTime)
return false;
lastSignalBarTime = currentBarTime;
return true;
}
bool SpreadIsAcceptable()
{
int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
if(spread > MaxSpreadPoints)
{
Print("Spread too high. Current spread: ", spread);
return false;
}
return true;
}
void OpenBuy()
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double stopLoss = NormalizeDouble(ask - StopLossPoints * _Point, _Digits);
double takeProfit = NormalizeDouble(ask + TakeProfitPoints * _Point, _Digits);
bool result = trade.Buy(
Lots,
_Symbol,
0.0,
stopLoss,
takeProfit,
"Two completed candles up"
);
if(result)
{
Print("Buy trade opened.");
}
else
{
Print(
"Buy trade failed. Retcode: ",
trade.ResultRetcode(),
" ",
trade.ResultRetcodeDescription()
);
}
}
void OpenSell()
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double stopLoss = NormalizeDouble(bid + StopLossPoints * _Point, _Digits);
double takeProfit = NormalizeDouble(bid - TakeProfitPoints * _Point, _Digits);
bool result = trade.Sell(
Lots,
_Symbol,
0.0,
stopLoss,
takeProfit,
"Two completed candles down"
);
if(result)
{
Print("Sell trade opened.");
}
else
{
Print(
"Sell trade failed. Retcode: ",
trade.ResultRetcode(),
" ",
trade.ResultRetcodeDescription()
);
}
}
void ShowStatus(ENUM_TIMEFRAMES timeframe, const MqlRates &candles[])
{
string previousDirection = GetDirection(candles[1]);
string secondPreviousDirection = GetDirection(candles[2]);
int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
Comment(
"Two-Candle Continuation EA\n",
"Symbol: ", _Symbol, "\n",
"Signal timeframe: ", EnumToString(timeframe), "\n",
"Previous candle: ", previousDirection, "\n",
"Second previous candle: ", secondPreviousDirection, "\n",
"Spread: ", spread, " points\n",
"Open position: ", PositionSelect(_Symbol) ? "yes" : "no"
);
}
string GetDirection(const MqlRates &candle)
{
if(candle.close > candle.open)
return "up";
if(candle.close < candle.open)
return "down";
return "unchanged";
}How the EA works
The EA starts by loading the trade library and creating a trade object.
#include <Trade/Trade.mqh>
CTrade trade;The input SignalTimeframe controls which timeframe supplies the candle data.
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_CURRENT;If the input is set to PERIOD_CURRENT, the EA uses the chart timeframe.
The helper function handles that choice.
ENUM_TIMEFRAMES GetSignalTimeframe()
{
if(SignalTimeframe == PERIOD_CURRENT)
return _Period;
return SignalTimeframe;
}The EA checks for a new candle on the signal timeframe.
if(!IsNewSignalBar(timeframe))
return;This keeps the strategy from evaluating the same candle repeatedly.
The EA then checks whether a position is already open.
if(PositionSelect(_Symbol))
return;It also checks the spread before trading.
if(!SpreadIsAcceptable())
return;After those checks, it copies the last three candles.
MqlRates candles[];
ArraySetAsSeries(candles, true);
int copied = CopyRates(_Symbol, timeframe, 0, 3, candles);The EA uses three candles because it needs the current candle and the last two completed candles.
The signal uses candles[1] and candles[2].
bool previousCandleUp = candles[1].close > candles[1].open;
bool secondPreviousCandleUp = candles[2].close > candles[2].open;
bool previousCandleDown = candles[1].close < candles[1].open;
bool secondPreviousCandleDown = candles[2].close < candles[2].open;If both completed candles closed up, the EA opens a buy position.
if(previousCandleUp && secondPreviousCandleUp)
{
OpenBuy();
return;
}If both completed candles closed down, the EA opens a sell position.
if(previousCandleDown && secondPreviousCandleDown)
{
OpenSell();
return;
}The trade functions set a fixed stop loss and take profit.
double stopLoss = NormalizeDouble(ask - StopLossPoints * _Point, _Digits);
double takeProfit = NormalizeDouble(ask + TakeProfitPoints * _Point, _Digits);The EA also writes trade errors to the log.
Print(
"Buy trade failed. Retcode: ",
trade.ResultRetcode(),
" ",
trade.ResultRetcodeDescription()
);That helps during testing.
If a trade is rejected because of invalid stops, insufficient margin, market closure, or another condition, the log gives a reason.
What to inspect during testing
Run the EA in the Strategy Tester first.
Start with visual mode enabled.
Watch the entries.
The EA should only evaluate once per signal candle.
It should not open a new position if one is already open.
It should ignore candles where the last two completed candles do not agree.
It should skip trades when the spread is above the input limit.
Check the Experts log after the test.
The log should show when the EA starts, when it opens trades, when it skips because of spread, and when no signal is found.
Why this example matters
This EA is intentionally simple.
The value is in the structure.
It separates the signal timeframe from the chart timeframe.
It uses completed candles.
It detects new bars.
It copies candle data with CopyRates().
It avoids repeated entries on every tick.
It checks spread before trading.
It displays the EA state on the chart.
These are common parts of many Expert Advisors.
The strategy can be changed later. The structure can stay.
🚨🚨Get your Quant Atlas Free trial (no credit card required).
Sign up takes ~9 seconds before you have unconditional 10-day access to 100+ markets.


