Complete MQL5 guide & code

MT5 Strategy Tester Guide: How to Backtest Expert Advisors (2026)

A step-by-step guide to using the MetaTrader 5 Strategy Tester to backtest Expert Advisors across multiple symbols and timeframes. Learn how to configure tick data quality, choose modelling modes, interpret equity curves, and avoid common pitfalls that produce misleading backtest results.

Record a reproducible test setup

Compile the final source, select the EA in Strategy Tester, and choose the main symbol, timeframe, dates and inputs. Record deposit currency, leverage, modeling mode, delay setting, broker history and code version. Check the Journal for shifted start dates or missing data.

  • Trace a specific crossover and compare expected order direction with the log.
  • Inspect rejected requests before interpreting an empty or profitable report.
  • Keep optimization data separate from the later evaluation period.

Choose a price model that fits the question

Real-tick mode uses broker tick history where available; generated Every Tick reconstructs ticks from minute records. Open Prices Only calls the EA at bar opens and has timeframe-access restrictions. Stops can still trigger in simplified modes, but their execution model differs.

  • Compare sensitive stop or intrabar behavior with a detailed tick mode.
  • Missing tick minutes can fall back to generated ticks; a selected mode is not proof of complete history.
  • This example reads completed bars, but its orders can still be affected by intrabar SL/TP execution.

Read the report as evidence with limits

History Quality assesses the consistency and gaps of minute data; it is not a percentage of real ticks or a live-performance score. Read balance and equity drawdowns separately. Profit factor compares gross profits with gross losses, while trade count and exposure help you see how much evidence the result contains.

  • Inspect individual deals and the equity path, not only a headline ratio.
  • Save settings and logs with the report, including costs and data limitations.
  • A profitable development sample or a high quality percentage does not establish future profitability.

Implementation details & mechanics

Core implementation pattern

The companion EA uses two completed bars on the selected chart timeframe to identify an MA crossover. Its static two-element arrays place the older copied bar at index 0 and the newer bar at index 1. Use this small example to trace decisions in the visual tester, not as a validated trading system.

Error handling & safety rules

The EA requests closure on an opposite crossover and initial SL/TP in broker points; zero disables the corresponding level. Failed closes block reversal and are retried on later ticks. It uses fixed lots and FOK requests; there is no portfolio limit, and its pending-exit state is not persisted across restart.

MQL5 Code Implementation

//+------------------------------------------------------------------+
//|  StrategyTesterDemo.mq5                                          |
//|  Teaching example: dual-MA crossover EA for MT5 Strategy Tester  |
//|  Demonstrates OnInit / OnTick / OnDeinit patterns and trade ops   |
//+------------------------------------------------------------------+
#property copyright "Pineify Demo"
#property version   "1.00"
#property strict

//--- Input parameters
input int    InpFastPeriod  = 10;      // Fast MA period
input int    InpSlowPeriod  = 30;      // Slow MA period
input ENUM_MA_METHOD InpMaMethod = MODE_EMA; // MA method (0=SMA,1=EMA,2=SMMA,3=LWMA)
input double InpLotSize     = 0.10;    // Trade volume (lots)
input int    InpSlippage    = 10;      // Max slippage in points
input double InpStopLoss    = 50.0;    // Stop loss in points (0 = disabled)
input double InpTakeProfit  = 100.0;   // Take profit in points (0 = disabled)
input ulong  InpMagicNumber = 20260101;// Magic number to identify EA trades

//--- Indicator handles
int g_fastHandle = INVALID_HANDLE;
int g_slowHandle = INVALID_HANDLE;

//--- State
bool g_initOk = false;
bool closeBuys=false, closeSells=false;

//+------------------------------------------------------------------+
//| Expert initialisation                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Validate periods
   if(InpFastPeriod<=0 || InpSlowPeriod<=0 || InpFastPeriod >= InpSlowPeriod
      || InpLotSize<=0 || InpSlippage<0 || InpStopLoss<0 || InpTakeProfit<0 || InpMagicNumber==0)
     {
      Print("ERROR: Fast period must be less than Slow period.");
      return INIT_PARAMETERS_INCORRECT;
     }

   //--- Create MA handles
   g_fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0,
                      InpMaMethod, PRICE_CLOSE);
   g_slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0,
                      InpMaMethod, PRICE_CLOSE);

   if(g_fastHandle == INVALID_HANDLE || g_slowHandle == INVALID_HANDLE)
     {
      Print("ERROR: Failed to create MA indicator handles.");
      return INIT_FAILED;
     }

   g_initOk = true;
   PrintFormat("StrategyTesterDemo initialised on %s / %s | Fast=%d Slow=%d",
               _Symbol, EnumToString(Period()), InpFastPeriod, InpSlowPeriod);
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Release indicator handles to free memory
   if(g_fastHandle != INVALID_HANDLE)
     {
      IndicatorRelease(g_fastHandle);
      g_fastHandle = INVALID_HANDLE;
     }
   if(g_slowHandle != INVALID_HANDLE)
     {
      IndicatorRelease(g_slowHandle);
      g_slowHandle = INVALID_HANDLE;
     }

   PrintFormat("StrategyTesterDemo deinitialised. Reason code: %d", reason);
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(!g_initOk) return;
   if(closeBuys) { CloseAllPositions(POSITION_TYPE_BUY); closeBuys=CountPositions(POSITION_TYPE_BUY)>0; }
   if(closeSells) { CloseAllPositions(POSITION_TYPE_SELL); closeSells=CountPositions(POSITION_TYPE_SELL)>0; }
   if(closeBuys || closeSells) return;

   //--- Only act on a new bar to avoid multiple entries per bar
   static datetime s_lastBarTime = 0;
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(currentBarTime == s_lastBarTime) return;
   if(currentBarTime<=0) return;

   //--- Copy two completed bars; physical index 0=older, 1=newer
   double fastBuf[2], slowBuf[2];
   if(CopyBuffer(g_fastHandle, 0, 1, 2, fastBuf) < 2) return;
   if(CopyBuffer(g_slowHandle, 0, 1, 2, slowBuf) < 2) return;

   s_lastBarTime=currentBarTime;
   double fastPrev = fastBuf[0];  // bar-2 (older)
   double fastCurr = fastBuf[1];  // bar-1 (most recent closed)
   double slowPrev = slowBuf[0];
   double slowCurr = slowBuf[1];

   //--- Determine crossover signals
   bool crossUp   = fastPrev < slowPrev && fastCurr > slowCurr;
   bool crossDown = fastPrev > slowPrev && fastCurr < slowCurr;

   //--- Count open positions for this EA
   int buyCount  = CountPositions(POSITION_TYPE_BUY);
   int sellCount = CountPositions(POSITION_TYPE_SELL);

   //--- Buy signal: fast MA crosses above slow MA
   if(crossUp && buyCount == 0)
     {
      CloseAllPositions(POSITION_TYPE_SELL);
      closeSells=CountPositions(POSITION_TYPE_SELL)>0;
      if(!closeSells) OpenTrade(ORDER_TYPE_BUY);
     }

   //--- Sell signal: fast MA crosses below slow MA
   if(crossDown && sellCount == 0)
     {
      CloseAllPositions(POSITION_TYPE_BUY);
      closeBuys=CountPositions(POSITION_TYPE_BUY)>0;
      if(!closeBuys) OpenTrade(ORDER_TYPE_SELL);
     }
  }

//+------------------------------------------------------------------+
//| Count open positions for this EA's magic number and direction    |
//+------------------------------------------------------------------+
int CountPositions(ENUM_POSITION_TYPE direction)
  {
   int count = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL)          != _Symbol)         continue;
      if(PositionGetInteger(POSITION_MAGIC)          != (long)InpMagicNumber) continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != direction)  continue;
      count++;
     }
   return count;
  }

//+------------------------------------------------------------------+
//| Close all positions of a given type for this EA                  |
//+------------------------------------------------------------------+
void CloseAllPositions(ENUM_POSITION_TYPE direction)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL)          != _Symbol)              continue;
      if(PositionGetInteger(POSITION_MAGIC)          != (long)InpMagicNumber) continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != direction)  continue;

      MqlTradeRequest req = {};
      MqlTradeResult  res = {};
      req.action    = TRADE_ACTION_DEAL;
      req.symbol    = _Symbol;
      req.volume    = PositionGetDouble(POSITION_VOLUME);
      req.type      = (direction == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
      req.price     = (req.type == ORDER_TYPE_SELL) ? SymbolInfoDouble(_Symbol, SYMBOL_BID)
                                                    : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      req.deviation = InpSlippage;
      req.magic     = InpMagicNumber;
      req.position  = ticket;
      req.comment   = "StrategyTesterDemo close";

      if(!OrderSend(req, res) || res.retcode!=TRADE_RETCODE_DONE)
         PrintFormat("CloseAllPositions error: %d retcode=%u", GetLastError(),res.retcode);
     }
  }

//+------------------------------------------------------------------+
//| Open a new trade in the specified direction                      |
//+------------------------------------------------------------------+
void OpenTrade(ENUM_ORDER_TYPE orderType)
  {
   double price    = (orderType == ORDER_TYPE_BUY)
                     ? SymbolInfoDouble(_Symbol, SYMBOL_ASK)
                     : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double point    = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int    digits   = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   double sl = 0.0, tp = 0.0;
   if(InpStopLoss > 0)
      sl = (orderType == ORDER_TYPE_BUY)
           ? NormalizeDouble(price - InpStopLoss * point, digits)
           : NormalizeDouble(price + InpStopLoss * point, digits);
   if(InpTakeProfit > 0)
      tp = (orderType == ORDER_TYPE_BUY)
           ? NormalizeDouble(price + InpTakeProfit * point, digits)
           : NormalizeDouble(price - InpTakeProfit * point, digits);

   MqlTradeRequest req = {};
   MqlTradeResult  res = {};
   req.action    = TRADE_ACTION_DEAL;
   req.symbol    = _Symbol;
   req.volume    = InpLotSize;
   req.type      = orderType;
   req.price     = price;
   req.sl        = sl;
   req.tp        = tp;
   req.deviation = InpSlippage;
   req.magic     = InpMagicNumber;
   req.comment   = "StrategyTesterDemo";

   if(!OrderSend(req, res) || res.retcode!=TRADE_RETCODE_DONE)
      PrintFormat("OpenTrade error: %d  retcode: %d", GetLastError(), res.retcode);
   else
      PrintFormat("Trade opened: %s %s vol=%.2f price=%.5f sl=%.5f tp=%.5f",
                  EnumToString(orderType), _Symbol, InpLotSize, price, sl, tp);
  }
//+------------------------------------------------------------------+

Copy this code into MetaEditor, save it in your MQL5 folder (Experts, Scripts, or Include), and compile with F7.

Generate custom MQL5 code with Pineify AI

Describe the trade execution, indicator calculations, risk management, or data processing logic you need. Pineify generates editable MQL5 source code. Review its logic, request a syntax check, and compile and test the final file in MetaEditor.

Pine Script vs MQL5: Same Strategy, Different Platforms

AspectPine Script (TradingView)MQL5 (MetaTrader 5)
ExecutionSeries evaluated on chart updatesEvent handlers with explicit buffers
DeploymentRuns in TradingView with alertsRuns in the MT5 terminal or on a VPS
Broker accessStrategy orders are simulated; live automation needs a separate integrationDirect broker connectivity
BacktestingStrategy Tester for strategy scriptsStrategy Tester for Expert Advisors
Code complexitySimpler, functional syntaxC++-like, more powerful

The calculation can be implemented on either platform, but the runtime model and buffer APIs differ.

Frequently Asked Questions

Related MQL5 pages

Technical references

Risk and testing note

These educational examples require independent compilation and testing. A compile pass or an illustrative setting does not establish execution correctness or profitability. Any reported performance must be supported by reproducible data and test settings; past results do not predict future returns. Trading involves significant risk of loss. This content is not financial advice.