MQL5 explained: OnInit, OnDeinit, OnTick – The core of every Expert Advisor

If you understand OnInit, OnDeinit and OnTick, you understand the heart of every MQL5 Expert Advisor.
Everything else – indicators, money management, signals – lives inside or around these three functions.

In this article we’ll go deep:

  • What each function really does and when it is called
  • What belongs in which function (and what absolutely doesn’t)
  • Professional patterns for resource management, order logic and debugging
  • Full code examples, including run-once-per-bar logic and clean shutdown

The goal is that, after reading this, you can look at any EA and immediately see whether its lifecycle is designed properly.

1. The Event-Driven Model of MQL5

An MQL5 EA is not a while(true) loop. It’s an event-driven program.
MetaTrader 5 calls your functions whenever something relevant happens.

For now we’ll focus on the three core events:

FunctionTriggered when…FrequencyTypical responsibilities
OnInit()EA is loaded, parameters changed, or recompiledOnce per loadInitialization, validation, resource allocation
OnDeinit()EA is removed, chart is closed, terminal is shut down, etc.Once per unloadCleanup, releasing resources, logging final state
OnTick()A new tick arrives for the chart’s symbolPotentially many times per secondMain trading logic, checking signals, managing open trades

You do not call these functions yourself. MT5 calls them at the right time.

A minimal EA looks like this:

int OnInit()
  {
   // initialization code
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   // cleanup code
  }

void OnTick()
  {
   // trading logic
  }

Now let’s dive into each function and look at professional usage patterns.

2. OnInit: Preparing the EA for Battle

OnInit() is called when:

  • You attach the EA to a chart
  • You change parameters in the Inputs tab
  • You recompile the EA while it’s attached
  • The terminal or account reinitializes the EA for internal reasons

Signature:

int OnInit();

It must return an int indicating success or failure.

2.1. Common return codes

Return valueMeaningEffect
INIT_SUCCEEDEDInitialization OKEA starts running and receives ticks
INIT_FAILEDFatal error, cannot continueEA stops; user sees error in Experts log
INIT_PARAMETERS_INCORRECTInputs are invalid (optional custom use)EA stops; user must fix parameters

In practice, you either return INIT_SUCCEEDED or INIT_FAILED.
INIT_PARAMETERS_INCORRECT is just a constant with specific numeric value; it behaves like failure.

2.2. What should happen in OnInit?

Think of OnInit() as a constructor for your EA:

  • Validate input parameters
  • Initialize indicator handles (e.g. iMA, iRSI)
  • Initialize objects like CTrade, custom classes, or data structures
  • Set up file handles, global variables, timers (EventSetTimer) if needed
  • Log useful information (symbol, timeframe, key inputs)

Example:

#include <Trade\Trade.mqh>

CTrade trade;

input int    InpMAPeriod = 50;
input double InpLots     = 0.10;

int ma_handle;

int OnInit()
  {
   if(InpMAPeriod <= 0)
     {
      Print("Invalid MA period: ", InpMAPeriod);
      return(INIT_PARAMETERS_INCORRECT);
     }

   if(InpLots <= 0.0)
     {
      Print("Invalid lot size: ", DoubleToString(InpLots, 2));
      return(INIT_PARAMETERS_INCORRECT);
     }

   ma_handle = iMA(_Symbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
   if(ma_handle == INVALID_HANDLE)
     {
      Print("Failed to create iMA. Error: ", GetLastError());
      return(INIT_FAILED);
     }

   trade.SetExpertMagicNumber(123456);

   Print("EA initialized on ", _Symbol, " ", EnumToString(_Period),
         ". MA period=", InpMAPeriod, ", Lots=", InpLots);

   return(INIT_SUCCEEDED);
  }

2.3. What should NOT happen in OnInit?

Two classic mistakes:

  1. Sending market orders from OnInit()
    • Initialization is not the right place to trade.
    • If the user attaches the EA accidentally, an order is fired instantly.
  2. Heavy loops or delays
    • Don’t perform long backtests, optimization or thousands of file reads in OnInit().
    • If it takes too long, the EA may look “frozen”.

Rule of thumb:

If it’s not strictly required for the EA to be ready to trade, it probably doesn’t belong in OnInit().

3. OnDeinit: Cleaning Up Gracefully

OnDeinit() is called when your EA is about to be unloaded.

Signature:

void OnDeinit(const int reason);

The reason parameter explains why:

3.1. Deinitialization reasons

Typical constants (values may vary but semantics are fixed):

ConstantExample numeric valueWhen it happens
REASON_PROGRAM0You removed the EA manually from the chart
REASON_REMOVE1EA was removed because chart was closed
REASON_RECOMPILE2EA was recompiled
REASON_CHARTCHANGE3Symbol or timeframe of chart changed
REASON_ACCOUNT4Account changed (e.g. login/logout)
REASON_TEMPLATE5Template with different EA loaded
REASON_INITFAILED6OnInit() returned INIT_FAILED or parameters incorrect
REASON_PARAMETERS7Inputs changed (EA reinitialized)
REASON_CLOSE8Terminal is shutting down

You can log this to understand what’s happening:

void OnDeinit(const int reason)
  {
   Print("Deinitializing EA. Reason: ", reason);
  }

3.2. What belongs in OnDeinit?

  • Release indicator handles with IndicatorRelease()
  • Close file handles (FileClose)
  • Stop timers (EventKillTimer)
  • Optionally close open positions (if your EA policy demands it)
  • Final logging (e.g. performance summary, last equity, custom stats)

Example:

void OnDeinit(const int reason)
  {
   if(ma_handle != INVALID_HANDLE)
     {
      IndicatorRelease(ma_handle);
      ma_handle = INVALID_HANDLE;
     }

   Print("EA deinitialized. Reason=", reason,
         ", last equity=", AccountInfoDouble(ACCOUNT_EQUITY));
  }

3.3. Should we close trades in OnDeinit?

This depends on your design philosophy.

  • Conservative approach: always close trades when EA is removed.
    Good for simple strategies that are not meant to run without the EA.
  • Infrastructure approach: leave trades open.
    Broker/platform should not assume the EA always runs (e.g. VPS failure) and strategy should survive temporary EA downtime.

If you decide to close, do it explicitly:

void CloseAllPositions()
  {
   CTrade trade;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetInteger(POSITION_MAGIC) == 123456 &&
            PositionGetString(POSITION_SYMBOL) == _Symbol)
           {
            trade.PositionClose(ticket);
           }
        }
     }
  }

void OnDeinit(const int reason)
  {
   if(reason == REASON_CLOSE || reason == REASON_REMOVE)
      CloseAllPositions();
  }

Note the check on reason – you might not want to liquidate positions on simple parameter changes.

4. OnTick: Where the Trading Logic Lives

OnTick() is called every time a new tick arrives for the chart’s symbol.

Signature:

void OnTick();

Inside OnTick() you typically:

  • Read market data (price, indicators, tick volume, etc.)
  • Update signals
  • Manage open positions (move SL, take partial profits, etc.)
  • Open/close orders according to your strategy rules
  • Apply filters (spread, time of day, news, etc.)

4.1. Basic OnTick template

A clean pattern is:

void OnTick()
  {
   // 1. Basic guards
   if(!IsTradingAllowed())
      return;

   if(!RefreshRates())
      return;

   // 2. Optional: run once per bar
   if(!IsNewBar())
      return;

   // 3. Update indicators / read values
   if(!UpdateIndicators())
      return;

   // 4. Generate signals
   int signal = GetSignal();

   // 5. Manage existing positions
   ManageOpenPositions();

   // 6. Open new positions if appropriate
   ExecuteSignal(signal);
  }

We’ll break these steps down.

5. Guards and Environment Checks in OnTick

5.1. Trading allowed?

You may want to skip trading if:

  • AutoTrading is disabled
  • Account is not tradeable
  • Symbol is in “close only” mode
  • Market is closed

Example helper:

bool IsTradingAllowed()
  {
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
      return(false);

   if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED))
      return(false);

   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_MODE_DISABLED)
      return(false);

   return(true);
  }

5.2. RefreshRates

RefreshRates() forces an update of bid/ask quotes.

if(!RefreshRates())
  {
   Print("RefreshRates failed. Error: ", GetLastError());
   return;
  }

double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

6. Running Logic Once Per Bar (Not Every Tick)

If you calculate indicators on the current timeframe, you often want to run main logic once per new bar, not every tick.
This reduces noise and CPU usage.

6.1. Using static last-bar time

bool IsNewBar()
  {
   static datetime last_bar_time = 0;
   datetime current_bar_time = iTime(_Symbol, _Period, 0);

   if(current_bar_time == 0)
      return(false);

   if(current_bar_time != last_bar_time)
     {
      last_bar_time = current_bar_time;
      return(true);
     }

   return(false);
  }

Now in OnTick():

void OnTick()
  {
   if(!IsNewBar())
      return;

   // main logic here executes once per bar
  }

This pattern is extremely common in professional EAs.

7. Indicators and Signals inside OnTick

You usually don’t create indicators in OnTick() – that belongs to OnInit().
But you read their values here.

7.1. Example: Reading MA and RSI

In OnInit:

int ma_handle;
int rsi_handle;

int OnInit()
  {
   ma_handle  = iMA(_Symbol, _Period, 50, 0, MODE_EMA, PRICE_CLOSE);
   rsi_handle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE);

   if(ma_handle == INVALID_HANDLE || rsi_handle == INVALID_HANDLE)
      return(INIT_FAILED);

   return(INIT_SUCCEEDED);
  }

Helper to get values:

bool UpdateIndicators(double &ma, double &rsi)
  {
   double buf_ma[1], buf_rsi[1];

   if(CopyBuffer(ma_handle,  0, 0, 1, buf_ma)  != 1) return(false);
   if(CopyBuffer(rsi_handle, 0, 0, 1, buf_rsi) != 1) return(false);

   ma  = buf_ma[0];
   rsi = buf_rsi[0];

   return(true);
  }

Usage in OnTick:

void OnTick()
  {
   if(!IsNewBar())
      return;

   double ma, rsi;
   if(!UpdateIndicators(ma, rsi))
      return;

   int signal = GetSignal(ma, rsi);
   ExecuteSignal(signal);
  }

7.2. Signal function example

enum SignalType
  {
   SIGNAL_NONE = 0,
   SIGNAL_BUY  = 1,
   SIGNAL_SELL = -1
  };

SignalType GetSignal(double ma, double rsi)
  {
   double price = SymbolInfoDouble(_Symbol, SYMBOL_CLOSE);

   // Simple example:
   // price above MA and RSI crossing above 50 -> BUY
   // price below MA and RSI crossing below 50 -> SELL
   static double last_rsi = 50.0;

   SignalType signal = SIGNAL_NONE;

   if(price > ma && last_rsi <= 50.0 && rsi > 50.0)
      signal = SIGNAL_BUY;
   else if(price < ma && last_rsi >= 50.0 && rsi < 50.0)
      signal = SIGNAL_SELL;

   last_rsi = rsi;
   return(signal);
  }

8. Position Management inside OnTick

Good EAs separate signal generation from order execution.

8.1. Checking current positions

int GetPositionDirection()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC)  == 123456)
           {
            long type = PositionGetInteger(POSITION_TYPE);
            if(type == POSITION_TYPE_BUY)  return(1);
            if(type == POSITION_TYPE_SELL) return(-1);
           }
        }
     }
   return(0); // no position
  }

8.2. Executing signals

CTrade trade;

void ExecuteSignal(SignalType signal)
  {
   int pos_dir = GetPositionDirection();

   // no signal or same direction -> nothing to do
   if(signal == SIGNAL_NONE || (signal == SIGNAL_BUY && pos_dir == 1) ||
      (signal == SIGNAL_SELL && pos_dir == -1))
      return;

   // if opposite position exists, close it
   if((signal == SIGNAL_BUY && pos_dir == -1) ||
      (signal == SIGNAL_SELL && pos_dir == 1))
     {
      ClosePositions();
     }

   // open new position
   OpenPosition(signal);
  }

void ClosePositions()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC)  == 123456)
           {
            trade.PositionClose(ticket);
           }
        }
     }
  }

void OpenPosition(SignalType signal)
  {
   double sl, tp;
   double lots = 0.10;

   // simple example: fixed SL/TP of 100/200 pips
   if(!CalculateSLTP(signal, 100, 200, sl, tp))
      return;

   trade.SetExpertMagicNumber(123456);

   if(signal == SIGNAL_BUY)
      trade.Buy(lots, _Symbol, 0.0, sl, tp, "EA Buy");
   else if(signal == SIGNAL_SELL)
      trade.Sell(lots, _Symbol, 0.0, sl, tp, "EA Sell");
  }

Helper to compute SL/TP:

bool CalculateSLTP(SignalType signal, int sl_pips, int tp_pips,
                   double &sl, double &tp)
  {
   double price = (signal == SIGNAL_BUY)
                  ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                  : SymbolInfoDouble(_Symbol, SYMBOL_BID);

   double point     = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int    digits    = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double pip       = (digits == 3 || digits == 5) ? 10 * point : point;
   double sl_points = sl_pips * pip;
   double tp_points = tp_pips * pip;

   if(signal == SIGNAL_BUY)
     {
      sl = price - sl_points;
      tp = price + tp_points;
     }
   else if(signal == SIGNAL_SELL)
     {
      sl = price + sl_points;
      tp = price - tp_points;
     }
   else
      return(false);

   return(true);
  }

This demonstrates how OnTick orchestrates everything: signal, position state, and order execution.

9. Putting It Together: A Lifecycle Skeleton EA

Here’s a compact “skeleton” that shows how OnInit, OnDeinit, and OnTick work together.
It doesn’t implement a full strategy, but it is structurally correct.

//+------------------------------------------------------------------+
//|   LifecycleSkeletonEA.mq5                                        |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>

CTrade trade;

//--- inputs
input int      InpMagic     = 123456;
input double   InpLots      = 0.10;
input int      InpMAPeriod  = 50;
input int      InpSlPips    = 100;
input int      InpTpPips    = 200;

//--- globals
int ma_handle;

//--- signals
enum SignalType { SIGNAL_NONE = 0, SIGNAL_BUY = 1, SIGNAL_SELL = -1 };

//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpMAPeriod <= 0)
     {
      Print("Invalid MA period.");
      return(INIT_PARAMETERS_INCORRECT);
     }

   ma_handle = iMA(_Symbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
   if(ma_handle == INVALID_HANDLE)
     {
      Print("Failed to create iMA. Error: ", GetLastError());
      return(INIT_FAILED);
     }

   trade.SetExpertMagicNumber(InpMagic);
   Print("EA initialized on ", _Symbol, " ", EnumToString(_Period));
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(ma_handle != INVALID_HANDLE)
      IndicatorRelease(ma_handle);

   Print("EA deinitialized. Reason=", reason);
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   if(!IsTradingAllowed())
      return;

   if(!IsNewBar())
      return;

   double price = SymbolInfoDouble(_Symbol, SYMBOL_CLOSE);
   double ma;

   if(!GetMA(ma))
      return;

   SignalType sig = GetSignal(price, ma);
   ExecuteSignal(sig);
  }
//+------------------------------------------------------------------+
bool IsTradingAllowed()
  {
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return(false);
   if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED))   return(false);
   return(true);
  }
//+------------------------------------------------------------------+
bool IsNewBar()
  {
   static datetime last_bar_time = 0;
   datetime current_bar_time = iTime(_Symbol, _Period, 0);

   if(current_bar_time != last_bar_time)
     {
      last_bar_time = current_bar_time;
      return(true);
     }
   return(false);
  }
//+------------------------------------------------------------------+
bool GetMA(double &ma)
  {
   double buf[1];
   if(CopyBuffer(ma_handle, 0, 0, 1, buf) != 1)
      return(false);
   ma = buf[0];
   return(true);
  }
//+------------------------------------------------------------------+
SignalType GetSignal(double price, double ma)
  {
   if(price > ma) return(SIGNAL_BUY);
   if(price < ma) return(SIGNAL_SELL);
   return(SIGNAL_NONE);
  }
//+------------------------------------------------------------------+
int GetPositionDirection()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC)  == InpMagic)
           {
            long type = PositionGetInteger(POSITION_TYPE);
            if(type == POSITION_TYPE_BUY)  return(1);
            if(type == POSITION_TYPE_SELL) return(-1);
           }
        }
     }
   return(0);
  }
//+------------------------------------------------------------------+
void ExecuteSignal(SignalType sig)
  {
   int pos_dir = GetPositionDirection();

   if(sig == SIGNAL_NONE) return;

   if((sig == SIGNAL_BUY  && pos_dir == 1) ||
      (sig == SIGNAL_SELL && pos_dir == -1))
      return; // already aligned

   if(pos_dir != 0)
      ClosePositions();

   OpenPosition(sig);
  }
//+------------------------------------------------------------------+
void ClosePositions()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC)  == InpMagic)
           {
            trade.PositionClose(ticket);
           }
        }
     }
  }
//+------------------------------------------------------------------+
bool CalculateSLTP(SignalType sig, double &sl, double &tp)
  {
   double price = (sig == SIGNAL_BUY)
                  ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                  : SymbolInfoDouble(_Symbol, SYMBOL_BID);

   double point  = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int    digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double pip    = (digits == 3 || digits == 5) ? 10 * point : point;

   double sl_dist = InpSlPips * pip;
   double tp_dist = InpTpPips * pip;

   if(sig == SIGNAL_BUY)
     {
      sl = price - sl_dist;
      tp = price + tp_dist;
     }
   else if(sig == SIGNAL_SELL)
     {
      sl = price + sl_dist;
      tp = price - tp_dist;
     }
   else
      return(false);

   return(true);
  }
//+------------------------------------------------------------------+
void OpenPosition(SignalType sig)
  {
   double sl, tp;
   if(!CalculateSLTP(sig, sl, tp))
      return;

   if(sig == SIGNAL_BUY)
      trade.Buy(InpLots, _Symbol, 0.0, sl, tp, "LifecycleEA Buy");
   else if(sig == SIGNAL_SELL)
      trade.Sell(InpLots, _Symbol, 0.0, sl, tp, "LifecycleEA Sell");
  }
//+------------------------------------------------------------------+

This EA isn’t meant as a profitable system; it’s a reference implementation showing how OnInit, OnDeinit and OnTick cooperate.

10. Debugging and Logging Best Practices

These lifecycle functions are also where you plug in logging and diagnostics.

10.1. Helpful debug patterns

  • Log initialization with inputs:
PrintFormat("Init EA on %s %s | MA=%d Lots=%.2f",
            _Symbol, EnumToString(_Period), InpMAPeriod, InpLots);
  • Log deinit reason and last known stats:
PrintFormat("Deinit reason=%d | equity=%.2f | balance=%.2f",
            reason,
            AccountInfoDouble(ACCOUNT_EQUITY),
            AccountInfoDouble(ACCOUNT_BALANCE));
  • Use Print(__FUNCTION__,": message") inside helpers to see which function logged the message.

10.2. Avoid spamming the log

Remember that OnTick() may run thousands of times per day.
Only log important events (e.g. new bar, order opened/closed, errors).

Bad pattern:

void OnTick()
  {
   Print("OnTick called"); // will spam log
  }

Good pattern: log only when conditions change (e.g. new signal or new bar).


11. Typical Lifecycle Pitfalls (and How to Avoid Them)

11.1. Creating indicators in OnTick

Some beginners do:

void OnTick()
  {
   int ma_handle = iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE);
   // ...
  }

This creates a new indicator instance on every tick, quickly exhausting resources.

Fix: create handles once in OnInit(), reuse them, release in OnDeinit().

11.2. Using global state without reset

Remember that OnInit() may be called multiple times (e.g. when parameters change).
If you use static or global variables to store state, make sure they are coherent after reinit.

Pattern:

double g_last_equity = 0.0;

int OnInit()
  {
   g_last_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   // ...
  }

11.3. Heavy logic in OnTick

Complex calculations, disk I/O or long loops in OnTick() can:

  • Slow down the terminal
  • Cause missed ticks
  • Make backtests extremely slow

Strategies:

  • Move rare tasks to OnTimer() instead of OnTick()
  • Cache indicator results
  • Use once-per-bar logic for anything that doesn’t need tick-level granularity

12. Conclusion

OnInit, OnDeinit, and OnTick are more than just required boilerplate.
They define the lifecycle of your Expert Advisor:

  • OnInit() is the constructor – validate parameters, allocate resources, prepare to trade.
  • OnDeinit() is the destructor – clean up, possibly close trades, log final state.
  • OnTick() is the engine – called every time the market moves, orchestrating signal logic and order management.

When these three are designed cleanly:

  • Your EA is reliable (no hidden resource leaks, predictable behaviour)
  • Your codebase is maintainable (clear separation of concerns)
  • Debugging and performance tuning become much easier

From here, any additional sophistication – multi-symbol portfolios, advanced money management, news filters, machine learning – still hangs on this same lifecycle.

If you’d like next, I can:

  • Take one of your existing EAs and refactor its lifecycle using the patterns above, or
  • Design a template EA specifically tailored to your coding style and risk management rules, ready to be reused across multiple strategies.
By Forex Real Trader

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like