Skip to content

Functions & Event Handlers

OnInit, OnCalculate, OnTick — the event-driven architecture that powers MQL5 programs.

💡
Before you start

A C++ compiler and a terminal. No MetaTrader, no broker, no account and no money. Most Linux machines already have g++; on macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2. Check with g++ --version.

If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.

This runs the MQL5 language, not MetaTrader, and nothing here can place a trade. The position-size function in step 5 is real and worth keeping, but it must be tested on a demo account before it decides anything with money attached.

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
}
💡
Performance matters in OnCalculate

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);
}

Write MQL5 Functions the Way the Platform Expects, in Five Steps

MQL5's own functions almost all share one shape: the return value tells you whether the call worked, and the answer comes back through a parameter marked with &. CopyClose, OrderSend and SymbolInfoDouble are all built this way. Once that clicks, a great deal of MQL5 stops being mysterious — and the most common category of Expert Advisor bug, using data that was never filled in, becomes obvious. In the next half hour you will compile and run that shape, watch what happens when a return value is ignored, and write one function properly enough to keep. Every line of output below came from running these files.

1
Save the header that supplies MetaTrader's built-ins

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as mql5.h. If you already have it from an earlier page, reuse that copy and skip to step 3.

// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::string string;
typedef long long datetime;
#define input static

template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }

inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) Print(rest...);
    else std::cout << std::endl;
}

inline string DoubleToString(double v, int digits = 8) {
    char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v)  { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int    MathRound(double v) { return (int)std::lround(v); }

You should see: nothing — this is a header file. Confirm it saved with ls mql5.h (Windows: dir mql5.h).

This runs the MQL5 language, not MetaTrader. No charts, no prices, no orders — but a function you can compile and call in five seconds.

If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.

2
Save the build script

Go: the same folder.

Do: save this as build.sh. It compiles a .mq5 file and executes the result. Make it executable with chmod +x build.sh, or invoke it as sh build.sh.

#!/bin/sh
# build.sh NAME  ->  compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
    :
elif command -v clang++ >/dev/null 2>&1; then
    clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
    echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
    exit 1
fi
"./$NAME"

You should see: nothing — this is the toolchain. ls should now show both mql5.h and build.sh.

If not: Permission denied means the chmod was skipped; use sh build.sh NAME, which needs no execute bit.

3
Find out which parameters a function can change

Go: the same folder.

Do: save this as byref.mq5 and run sh build.sh byref.

#include "mql5.h"

//+------------------------------------------------------------------+
//| The single most confusing thing about MQL5 functions.             |
//+------------------------------------------------------------------+

void TryToChange(double value)          // a COPY arrives
{
    value = 999.0;
    Print("   inside TryToChange, value is now ", DoubleToString(value, 1));
}

void ReallyChange(double &value)        // the ORIGINAL arrives
{
    value = 999.0;
    Print("   inside ReallyChange, value is now ", DoubleToString(value, 1));
}

bool GetPrice(double &out_price)         // the MQL5 house style
{
    out_price = 1.10250;
    return true;                         // true means "out_price is valid"
}

void OnStart()
{
    double price = 1.0;

    Print("before        : ", DoubleToString(price, 1));
    TryToChange(price);
    Print("after by value: ", DoubleToString(price, 1), "   <- unchanged");

    Print("");
    price = 1.0;
    Print("before        : ", DoubleToString(price, 1));
    ReallyChange(price);
    Print("after by ref  : ", DoubleToString(price, 1), "   <- changed");

    Print("");
    double got = 0.0;
    if(GetPrice(got))
        Print("GetPrice returned true, and out_price is ", DoubleToString(got, 5));
    Print("");
    Print("MQL5 uses this everywhere: the RETURN value says whether it worked,");
    Print("and the answer comes back through a & parameter. CopyClose, OrderSend");
    Print("and SymbolInfoDouble all follow that shape -- which is why ignoring a");
    Print("return value means using data that was never filled in.");
}

int main() { OnStart(); return 0; }

You should see: the same assignment changing the caller's variable only once:

before        : 1.0
   inside TryToChange, value is now 999.0
after by value: 1.0   <- unchanged

before        : 1.0
   inside ReallyChange, value is now 999.0
after by ref  : 999.0   <- changed

GetPrice returned true, and out_price is 1.10250

MQL5 uses this everywhere: the RETURN value says whether it worked,
and the answer comes back through a & parameter. CopyClose, OrderSend
and SymbolInfoDouble all follow that shape -- which is why ignoring a
return value means using data that was never filled in.

Without the &, the function receives a copy: it can change the copy all it likes and the caller never sees it. With the &, the function receives the caller's variable itself.

This is why MQL5 functions are shaped the way they are. A function that must report both a result and whether it succeeded has two things to return and only one return slot, so the platform's convention is: return a bool or a count for success, and hand back the data through a reference parameter. GetPrice above is the pattern in miniature, and recognising it makes the reference documentation far easier to read.

One practical note: passing a large array by value copies the whole thing on every call, which inside OnTick() is real work repeated thousands of times. Arrays are normally passed by reference for that reason as well as for the ability to fill them.

If not: if both cases print 999.0 afterwards, the & was added to both signatures — TryToChange must take a plain double for the contrast to appear.

4
Watch what an ignored return value does

Go: the same folder. This is where most 'my EA behaves strangely' reports end up.

Do: save this as uninit.mq5 and run sh build.sh uninit.

#include "mql5.h"

//+------------------------------------------------------------------+
//| What happens when a function fails and you use the answer anyway. |
//+------------------------------------------------------------------+

bool CopyPrices(std::vector<double> &dest, int wanted, bool pretend_failure)
{
    if(pretend_failure)
        return false;                    // data not ready -- dest untouched
    ArrayResize(dest, wanted);
    for(int i = 0; i < wanted; i++)
        dest[i] = 1.1000 + i * 0.0001;
    return true;
}

double AverageOf(const std::vector<double> &v)
{
    if(ArraySize(v) == 0) return 0.0;
    double s = 0.0;
    for(int i = 0; i < ArraySize(v); i++) s += v[i];
    return s / ArraySize(v);
}

void OnStart()
{
    Print("THE CARELESS VERSION -- return value ignored");
    std::vector<double> a;
    CopyPrices(a, 5, true);                       // fails, and nobody looks
    Print("   ArraySize(a)  : ", ArraySize(a));
    Print("   average       : ", DoubleToString(AverageOf(a), 5));
    Print("   ...and a strategy now compares that 0.00000 with a real price.");

    Print("");
    Print("THE CHECKED VERSION");
    std::vector<double> b;
    if(!CopyPrices(b, 5, true))
        Print("   copy failed -- returning early, doing nothing this tick");
    else
        Print("   average: ", DoubleToString(AverageOf(b), 5));

    Print("");
    Print("AND WHEN IT SUCCEEDS");
    std::vector<double> c;
    if(CopyPrices(c, 5, false))
        Print("   got ", ArraySize(c), " prices, average ",
              DoubleToString(AverageOf(c), 5));

    Print("");
    Print("Price data is genuinely not always available: a fresh chart, a");
    Print("reconnect, a symbol whose history is still downloading. The copy");
    Print("functions return -1 in those cases, and an EA that does not check");
    Print("trades on zeros.");
}

int main() { OnStart(); return 0; }

You should see: an average of zero being computed from data that never arrived:

THE CARELESS VERSION -- return value ignored
   ArraySize(a)  : 0
   average       : 0.00000
   ...and a strategy now compares that 0.00000 with a real price.

THE CHECKED VERSION
   copy failed -- returning early, doing nothing this tick

AND WHEN IT SUCCEEDS
   got 5 prices, average 1.10020

Price data is genuinely not always available: a fresh chart, a
reconnect, a symbol whose history is still downloading. The copy
functions return -1 in those cases, and an EA that does not check
trades on zeros.

The careless version produces 0.00000 and no error. If a strategy then asks “is the price above the average?”, the answer is yes — every price is above zero — so the EA enters a trade on data it never received.

And this is not a hypothetical failure. Price data genuinely is unavailable at times: the first ticks after a chart opens, after a reconnection, while a symbol's history is still downloading, or on a symbol you have not opened before. The real CopyClose/CopyRates functions return -1 in those cases, which is easy to ignore precisely because it looks like an ordinary number.

The habit: every copy call is wrapped in an if, and the failure branch does nothing at all — returns from OnTick() and waits for the next one. Doing nothing is always a valid action for an EA; acting on zeros is not.

If not: if the careless version prints a real average, the pretend_failure argument was passed as false — it must be true for the first case, which is what simulates data not being ready.

5
Write one function properly and keep it

Go: the same folder.

Do: save this as possize.mq5 and run sh build.sh possize.

#include "mql5.h"

//+------------------------------------------------------------------+
//| One function worth writing correctly once, then reusing forever.  |
//+------------------------------------------------------------------+

// Everything a broker would tell you, gathered in one place so the
// arithmetic is visible rather than hidden behind platform calls.
struct SymbolSpec
{
    double tick_size;      // smallest price change
    double tick_value;     // money per tick per 1.0 lot
    double min_lot;
    double max_lot;
    double lot_step;
};

double NormaliseLot(double lot, const SymbolSpec &s)
{
    lot = MathMax(lot, s.min_lot);
    lot = MathMin(lot, s.max_lot);
    double steps = MathRound(lot / s.lot_step);
    return steps * s.lot_step;
}

// Returns 0.0 when the trade cannot be sized safely -- caller must check.
double PositionSize(double balance, double risk_percent,
                    double stop_points, const SymbolSpec &s)
{
    if(balance <= 0.0 || risk_percent <= 0.0 || stop_points <= 0.0) return 0.0;
    if(s.tick_value <= 0.0 || s.tick_size <= 0.0)                   return 0.0;

    double risk_money   = balance * risk_percent / 100.0;
    double loss_per_lot = stop_points * s.tick_value;
    if(loss_per_lot <= 0.0) return 0.0;

    return NormaliseLot(risk_money / loss_per_lot, s);
}

void OnStart()
{
    SymbolSpec eurusd = {0.00001, 1.0, 0.01, 100.0, 0.01};

    Print("balance 10000, tick value 1.00 per point per lot, lot step 0.01");
    Print("");
    Print("risk%  stop(pts)   risk money   lot");
    double cases[4][2] = {{1.0, 200}, {2.0, 200}, {2.0, 50}, {2.0, 1000}};
    for(int i = 0; i < 4; i++)
    {
        double risk = cases[i][0], stop = cases[i][1];
        double lot  = PositionSize(10000.0, risk, stop, eurusd);
        Print("  ", DoubleToString(risk, 1), "      ", (int)stop,
              "        ", DoubleToString(10000.0 * risk / 100.0, 2),
              "      ", DoubleToString(lot, 2));
    }

    Print("");
    Print("the cases that must return 0.00, not a guess:");
    Print("   stop of 0 points : ", DoubleToString(PositionSize(10000, 2, 0, eurusd), 2));
    Print("   negative balance : ", DoubleToString(PositionSize(-1, 2, 200, eurusd), 2));
    Print("");
    Print("Halving the stop distance DOUBLES the lot for the same risk. That");
    Print("is the whole point: risk is decided by you, and the lot size is");
    Print("whatever makes the stop cost exactly that much.");
}

int main() { OnStart(); return 0; }

You should see: the lot size changing with the stop distance, and zero where sizing is impossible:

balance 10000, tick value 1.00 per point per lot, lot step 0.01

risk%  stop(pts)   risk money   lot
  1.0      200        100.00      0.50
  2.0      200        200.00      1.00
  2.0      50        200.00      4.00
  2.0      1000        200.00      0.20

the cases that must return 0.00, not a guess:
   stop of 0 points : 0.00
   negative balance : 0.00

Halving the stop distance DOUBLES the lot for the same risk. That
is the whole point: risk is decided by you, and the lot size is
whatever makes the stop cost exactly that much.

Read the last two lines of output as the definition of risk management. Risking 2% with a 200-point stop gives 1.00 lots; the same 2% with a 50-point stop gives 4.00. You choose the risk; the lot size is then whatever makes the stop cost exactly that. Sizing the other way round — picking a lot size and accepting whatever the stop costs — is how accounts are lost on a single wider-than-usual trade.

Three details in the code are worth copying into your own version. It returns 0.0 rather than guessing when the inputs make sizing impossible, so the caller must check — exactly the convention from step 3. It normalises the result to the broker's lot step, because an order for 0.4763 lots is rejected. And it clamps to the minimum and maximum, because those are real limits that differ per symbol.

In a real EA the SymbolSpec values come from SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE) and its siblings, read once in OnInit(). Hard-coding them is the fourth-most-common cause of an EA that works on one symbol and misbehaves on another.

If not: if every row prints the same lot, the stop distance is not reaching the calculation — loss_per_lot must multiply the stop by the tick value. If the small-stop case prints the maximum lot, the clamp is firing, which is correct behaviour and worth noticing.

🎉
Check yourself before moving on

Without scrolling up: a colleague's EA calls CopyClose into an array and then uses the first element, and it works perfectly except for occasional trades at prices that make no sense, always shortly after they restart the terminal. What is happening, and what would you change? Answer: the copy is failing on the first ticks after a restart, while the terminal is still fetching history, and the return value is being ignored — step 4 showed exactly this shape, with the array left empty and the calculation proceeding on zeros. It works “perfectly” the rest of the time because the data is normally there, which is what makes the bug so hard to catch: it is invisible except in the one condition nobody tests. The change is to wrap the call in an if, treat a failure as “do nothing this tick and return”, and never let the code below it run on an array that was not filled. It is also worth checking the number of elements actually copied rather than only the success flag, because a partial copy is possible; the function returns how many it wrote, and asking for 100 bars and getting 3 is a different situation from getting none.

Now do it without the page: extend possize.mq5 with a second symbol specification — something with a different tick value and a 0.1 lot step, such as a stock index — and run the same four cases through it. The lots will differ substantially for identical risk, which is the point. Then add a fifth guard to PositionSize: refuse to return a lot whose loss at the stop would exceed the risk you asked for, even after normalising to the lot step. Rounding up to the nearest step can push you over, and that guard is missing from most published position-size functions.

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);