Why Event Handlers Matter in MetaTrader EAs and Indicators
Every Expert Advisor (EA) and custom indicator in MetaTrader 4 and MetaTrader 5 is event-driven. The platform fires specific events—like a new price tick arriving, a timer expiring, or a chart object being clicked—and your code must respond inside the correct handler function. Getting these event handlers right is the difference between an EA that executes reliably and one that misses trades or crashes.
This guide covers the three most essential event handlers you'll use daily: OnTick, OnTimer, and OnChartEvent. You'll learn exactly when each fires, how to implement them in MQL4 and MQL5, and common pitfalls to avoid. By the end, you'll be able to write EAs that respond instantly to market moves, run scheduled tasks without a tick, and handle user interactions on your charts.
Event handlers are the backbone of asynchronous programming in MetaTrader. Unlike traditional linear scripts, EAs and indicators must react to unpredictable external events—price changes, time intervals, and user actions. Each handler runs in its own context, and understanding their lifecycle is critical for building robust trading systems. For example, OnTick may fire hundreds of times per second during volatile markets, while OnTimer might fire only once per minute for scheduled maintenance. Mismatching these can lead to performance bottlenecks or missed trading opportunities.
Prerequisites
- MetaTrader 4 build 1400+ or MetaTrader 5 build 4000+ (both work, but MQL5 has stricter event typing and additional event constants)
- MetaEditor (included with MT4/MT5 installation; accessible via F4 in the platform or from the Tools menu)
- Basic familiarity with MQL4 or MQL5 syntax—variables, functions, and conditional statements
- A demo account for testing (no real money required; use a demo with active market data to verify tick behavior)
- One chart open with the symbol you intend to test (EAs only run on attached charts)
Step-by-Step: Implementing the Three Core Event Handlers
1. OnTick – The Heartbeat of Every EA
OnTick fires every time the platform receives a new price quote for the symbol your EA is attached to. In MQL4, this is the only event handler required for a basic EA. In MQL5, OnTick is also the main entry point but works with ticks from the MqlTick structure, which includes bid, ask, last, volume, and time fields.
MQL4 Example – Simple OnTick Handler:
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
Print("EA started on ", Symbol());
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Access current bid and ask
double bid = Bid;
double ask = Ask;
// Example: log price every 100 ticks
static int tickCount = 0;
tickCount++;
if(tickCount % 100 == 0)
Print("Tick ", tickCount, ": Bid=", bid, " Ask=", ask);
}
Key Points:
- OnTick runs on every tick—do not put heavy calculations here. Use it for time-sensitive decisions like checking entry conditions or managing stop losses.
- In MQL5, use SymbolInfoTick() to get the tick structure inside OnTick. The MqlTick structure provides bid, ask, last, volume, time, and flags to distinguish trade ticks from price ticks.
- If your EA is not attached to a chart, OnTick will never fire. The EA must be attached to a chart with the same symbol you want to trade.
- OnTick is automatically called by the platform; you cannot call it manually. The platform queues ticks and processes them sequentially, so your handler must complete before the next tick is processed.
- In MQL4, Bid and Ask are global variables updated automatically before OnTick runs. In MQL5, you must fetch them explicitly using SymbolInfoDouble() or SymbolInfoTick().
MQL5 Example – OnTick with Tick Structure:
//+------------------------------------------------------------------+
//| Expert tick function in MQL5 |
//+------------------------------------------------------------------+
void OnTick()
{
MqlTick tick;
if(SymbolInfoTick(Symbol(), tick))
{
// Access tick fields
double bid = tick.bid;
double ask = tick.ask;
double last = tick.last;
ulong volume = tick.volume;
datetime time = tick.time;
// Check if this is a trade tick (price change from actual trade)
if(tick.flags & TICK_FLAG_ASK)
Print("New ask price: ", ask);
}
}
Performance Considerations: During high-frequency trading or news events, ticks can arrive at sub-millisecond intervals. Keep your OnTick code lean—avoid file I/O, complex loops, or heavy indicator recalculations. Use RefreshRates() sparingly inside OnTick (it's already called by the platform before each tick). If you need heavy processing, consider using OnTimer to offload work.
2. OnTimer – Scheduled Execution Without Ticks
OnTimer allows your EA or indicator to execute code at a fixed interval, regardless of whether new ticks arrive. This is essential for tasks like recalculating indicators every 5 seconds, checking pending orders every minute, or sending heartbeat logs. OnTimer is particularly useful in MQL5 where tick data may be sparse for less liquid symbols.
MQL4 Example – Setting Up a 1-Second Timer:
int OnInit()
{
// Start a timer that fires every 1000 milliseconds (1 second)
EventSetMillisecondTimer(1000);
return(INIT_SUCCEEDED);
}
void OnTimer()
{
// This runs every second
Print("Timer fired at ", TimeCurrent());
// Example: refresh rates manually if needed
RefreshRates();
// Check if market is open
if(!IsTradeAllowed())
Print("Trading not allowed - market may be closed");
}
void OnDeinit(const int reason)
{
// Always destroy the timer on deinitialization
EventKillTimer();
}
MQL5 Note: Use EventSetTimer(1) for 1-second intervals (integer seconds only). For sub-second precision in MQL5, you must use EventSetMillisecondTimer()—available from build 4000+. Unlike MQL4, MQL5's EventSetTimer accepts only integer seconds, so for 500ms intervals you must use the millisecond version.
Timer Precision and Limitations:
- The timer interval is approximate, not guaranteed. The platform may delay timer events if the main thread is busy processing other events (like ticks or chart redraws).
- In MQL4, the minimum interval for EventSetMillisecondTimer is 1 millisecond, but actual precision is typically around 15-50ms due to Windows timer resolution.
- In MQL5, EventSetMillisecondTimer accepts values from 1 to 2147483647 milliseconds. However, intervals below 10ms may not be reliable on all systems.
- You can only have one timer per EA or indicator. Calling EventSetTimer() again with a new interval replaces the previous timer.
- Timers persist even if the EA is removed from the chart; always call EventKillTimer() in OnDeinit to prevent orphaned timers that can cause memory leaks or errors.
When to Use OnTimer vs OnTick:
| Use Case | Best Handler | Why |
|---|---|---|
| Check entry conditions on new price | OnTick | Immediate response to market movement |
| Recalculate indicator every 5 seconds | OnTimer | Avoids redundant calculations on every tick |
| Cancel pending orders after 10 minutes | OnTimer | Precise interval without tick dependency |
| Log account equity every hour | OnTimer | Low-frequency, no tick required |
| Fetch economic calendar data | OnTimer | Scheduled updates independent of price |
3. OnChartEvent – Responding to User Actions
OnChartEvent fires when a user interacts with a chart—clicking on objects, pressing keyboard keys, or resizing the chart window. This is how you build interactive panels, custom buttons, or keyboard shortcuts into your EA or indicator. In MQL5, this handler also supports mouse move events, enabling drag-and-drop functionality.
MQL4 Example – Handling a Button Click:
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
// CHARTEVENT_OBJECT_CLICK fires when an object is clicked
if(id == CHARTEVENT_OBJECT_CLICK)
{
// sparam contains the name of the clicked object
if(sparam == "MyButton")
{
Print("Button 'MyButton' was clicked!");
// Example: toggle EA trading
if(IsTradeAllowed())
Print("Trading is currently allowed");
else
Print("Trading is disabled");
}
}
// CHARTEVENT_KEYDOWN for keyboard input
if(id == CHARTEVENT_KEYDOWN)
{
// lparam contains the virtual key code
if(lparam == 65) // 'A' key
Print("User pressed 'A' key");
if(lparam == 27) // ESC key
Print("User pressed ESC - consider aborting operation");
}
// CHARTEVENT_CHART_CHANGE for window resize
if(id == CHARTEVENT_CHART_CHANGE)
{
int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
Print("Chart resized to ", width, "x", height);
}
}
MQL5 Difference: MQL5 uses the same function signature but has different event ID constants. Always check the id parameter against CHARTEVENT_OBJECT_CLICK, CHARTEVENT_KEYDOWN, CHARTEVENT_MOUSE_MOVE, etc. MQL5 also supports CHARTEVENT_MOUSE_MOVE which provides mouse coordinates in lparam (x) and dparam (y).
Creating an Interactive Button: To make a button clickable, you must set its properties correctly. Here's how to create a clickable button in OnInit:
int OnInit()
{
// Create a button object
ObjectCreate(0, "MyButton", OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, "MyButton", OBJPROP_XDISTANCE, 100);
ObjectSetInteger(0, "MyButton", OBJPROP_YDISTANCE, 50);
ObjectSetInteger(0, "MyButton", OBJPROP_XSIZE, 120);
ObjectSetInteger(0, "MyButton", OBJPROP_YSIZE, 30);
ObjectSetString(0, "MyButton", OBJPROP_TEXT, "Click Me");
ObjectSetInteger(0, "MyButton", OBJPROP_SELECTABLE, true); // Must be true
ObjectSetInteger(0, "MyButton", OBJPROP_HIDDEN, false); // Must be false
ObjectSetInteger(0, "MyButton", OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, "MyButton", OBJPROP_BGCOLOR, clrBlue);
return(INIT_SUCCEEDED);
}
Common Event IDs You'll Use:
| Event ID | What It Fires On |
|---|






