Technical revision: 1 October 2026 · Educational material
The event lifecycle determines when an EA allocates resources, reads prices and releases them. A signal rule can be correct while the surrounding program processes stale history or leaks handles. Use a non-trading logger to learn these boundaries before adding orders.
OnInit: validate inputs and own your handles
Create indicator handles once during initialization. Check for INVALID_HANDLE and return INIT_FAILED if a required resource cannot be created. If a later initialization step fails, release resources already acquired. Invalid input combinations can return INIT_PARAMETERS_INCORRECT.
A handle is a reference to an indicator calculation, not an indicator value. Creating it successfully does not mean its history is immediately available.
OnTick: postpone incomplete reads
A new-bar gate compares the current bar opening time with the last processed time. Only update that saved time after the required rates and indicator values have been copied successfully. Otherwise an initial data delay can cause the entire bar to be skipped.
The example initializes the saved time to the current bar during attachment. It waits for the next bar instead of retroactively acting on a signal whose timing the EA did not observe. This is a policy decision that should be recorded in a test.
A complete logger with no orders
#property strict
#property version "1.00"
// Non-trading EA: logs a completed bar and its EMA.
input int InpPeriod=20;
int ma_handle=INVALID_HANDLE;
datetime last_bar=0;
int OnInit()
{
if(InpPeriod<2) return INIT_PARAMETERS_INCORRECT;
ma_handle=iMA(_Symbol,_Period,InpPeriod,0,MODE_EMA,PRICE_CLOSE);
if(ma_handle==INVALID_HANDLE) return INIT_FAILED;
last_bar=iTime(_Symbol,_Period,0);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(ma_handle!=INVALID_HANDLE) IndicatorRelease(ma_handle);
Print("Lifecycle demo stopped. Reason=",reason);
}
void OnTick()
{
datetime current=iTime(_Symbol,_Period,0);
if(current<=0 || current==last_bar) return;
if(BarsCalculated(ma_handle)<InpPeriod+2) return;
double ma[1];
MqlRates closed[1];
if(CopyBuffer(ma_handle,0,1,1,ma)!=1 ||
CopyRates(_Symbol,_Period,1,1,closed)!=1) return;
if(!MathIsValidNumber(ma[0]) || ma[0]==EMPTY_VALUE) return;
last_bar=current;
Print("Closed bar ",TimeToString(closed[0].time),
" close=",DoubleToString(closed[0].close,_Digits),
" EMA=",DoubleToString(ma[0],_Digits));
}
The price close comes from CopyRates at shift 1. CopyBuffer also starts at shift 1, so the printed EMA and price refer to a completed candle. Comparing a closed price to an unfinished indicator value would make the observation inconsistent.
OnDeinit: release and explain
Changing inputs, changing chart period or removing the EA can trigger deinitialization. Release each valid handle and log the reason while debugging. Do not rely on removal to undo submitted orders: a trading position is server state, separate from the lifetime of a chart program.
What a lifecycle example leaves to the next lesson
OnTick is not a promise to receive every individual market tick. The platform’s event queue affects delivery, so heavy calculations or blocking work can change observation timing. A new-bar strategy and a tick-sensitive execution strategy need different timing designs.
A trading EA also needs account-mode awareness, broker constraints, server result handling and transaction reconciliation. For an initial implementation follow the demo EA tutorial. Then verify missing-history behavior, reattachment and timeframe changes before evaluating trading results.