Event-Driven Architecture
MQL5 programs are event-driven. Instead of running from top to bottom like a simple script, indicators and EAs respond to events — a new tick arrives, a new bar forms, the user changes settings, the chart timeframe changes, etc.
You define event handler functions that MT5 calls automatically when each event occurs. This is the core pattern of all MQL5 programming.
Event Handlers by Program Type
Scripts have one event handler:
void OnStart() // Called once when the script is attached to a chart
Indicators have these key handlers:
int OnInit() // Called once when indicator is loaded
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[]) // Called on every new tick
void OnDeinit(const int reason) // Called when indicator is removed
Expert Advisors have these key handlers:
int OnInit() // Called once when EA is loaded
void OnTick() // Called on every new tick (price change)
void OnDeinit(const int reason) // Called when EA is removed
void OnTrade() // Called when a trade event occurs
void OnTimer() // Called on timer events
OnInit — Initialization
OnInit() runs once when your program is first attached to a chart, or when the chart timeframe changes, or when input parameters are modified. Use it to:
int OnInit()
{
// Validate input parameters
if(MAPeriod < 1)
{
Print("Error: MA Period must be >= 1");
return INIT_PARAMETERS_INCORRECT;
}
// Create indicator handles
maHandle = iMA(_Symbol, PERIOD_CURRENT, MAPeriod, 0, MODE_SMA, PRICE_CLOSE);
if(maHandle == INVALID_HANDLE)
{
Print("Error creating MA indicator");
return INIT_FAILED;
}
// Set indicator properties (for custom indicators)
SetIndexBuffer(0, mainBuffer, INDICATOR_DATA);
PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDodgerBlue);
return INIT_SUCCEEDED; // must return this on success
}
OnCalculate — The Indicator Engine
This is the heart of every custom indicator. It runs on every new tick and receives the full price data arrays:
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
// rates_total = total number of bars on the chart
// prev_calculated = bars already processed (0 on first call)
// Only process new bars (efficiency optimization)
int start = (prev_calculated == 0) ? MAPeriod : prev_calculated - 1;
for(int i = start; i < rates_total; i++)
{
// Calculate your indicator value for bar i
double sum = 0;
for(int j = 0; j < MAPeriod; j++)
sum += close[i - j];
mainBuffer[i] = sum / MAPeriod;
}
return rates_total; // tell MT5 how many bars we processed
}
This function runs on every tick — potentially dozens of times per second. Only recalculate bars that are new or changed. The prev_calculated parameter tells you where you left off, so you can skip already-processed bars.
OnTick — The EA Engine
OnTick() fires every time a new price quote arrives for the chart symbol. This is where your EA logic lives:
void OnTick()
{
// Get current prices
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Read indicator values
double maValues[];
CopyBuffer(maHandle, 0, 0, 3, maValues);
// Check for trade signals
if(bid > maValues[0] && !HasOpenPosition())
{
// Price crossed above MA — open buy
OpenBuy(ask);
}
else if(bid < maValues[0] && HasOpenPosition())
{
// Price crossed below MA — close position
CloseAllPositions();
}
}
OnDeinit — Cleanup
Called when your program is removed from the chart. Use it to release resources:
void OnDeinit(const int reason)
{
// Release indicator handles
IndicatorRelease(maHandle);
// Remove chart objects you created
ObjectsDeleteAll(0, "MyIndicator_");
// Log the reason for removal
Print("Removed. Reason: ", reason);
}
Writing Your Own Functions
Break your code into reusable functions:
// Function with return value
double CalculateLotSize(double riskPercent, double stopLossPips)
{
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = accountBalance * riskPercent / 100.0;
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double lotSize = riskAmount / (stopLossPips * tickValue);
return NormalizeDouble(lotSize, 2);
}
// Function with no return value
void LogTradeInfo(string action, double price, double lots)
{
Print(action, " | Price: ", DoubleToString(price, _Digits),
" | Lots: ", DoubleToString(lots, 2),
" | Time: ", TimeToString(TimeCurrent()));
}
// Usage
void OnTick()
{
double lots = CalculateLotSize(1.0, 50);
LogTradeInfo("BUY", SymbolInfoDouble(_Symbol, SYMBOL_ASK), lots);
}
Pass by Reference
Use the & operator to pass variables by reference — the function can modify the original variable:
bool GetPriceData(double &high, double &low, double &close, int shift)
{
high = iHigh(_Symbol, PERIOD_CURRENT, shift);
low = iLow(_Symbol, PERIOD_CURRENT, shift);
close = iClose(_Symbol, PERIOD_CURRENT, shift);
return (high != 0 && low != 0 && close != 0);
}
// Usage — variables are filled by the function
double h, l, c;
if(GetPriceData(h, l, c, 1))
Print("Yesterday: H=", h, " L=", l, " C=", c);