preview
Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template

Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template

MetaTrader 5Trading |
272 0
Tola Moses Hector
Tola Moses Hector

Introduction

In Part 3 of this series, we automated Stan Weinstein's Stage Analysis—a framework that classifies markets into four lifecycle stages and trades only Stage 2 advances. Weinstein asks one question: what phase is this market in? Mark Minervini asks a harder one: does this market meet each one of eight specific structural requirements simultaneously?

Minervini is one of the most studied traders alive. He won the U.S. Investing Championship in 1997 with an audited 155% return and again in 2021 with an audited 334.8% return in the million-dollar category. His method, which he calls SEPA—Specific Entry Point Analysis—begins with the Trend Template: a checklist of eight technical conditions that a market must satisfy before it even qualifies for chart analysis. If any single condition fails, the instrument is eliminated. No exceptions.

The power of the trend template is not in any individual condition. Each condition, taken alone, is a basic technical filter. The power comes from requiring all eight conditions simultaneously. This combination selects markets that trend in a specific, structured way: moving averages are stacked correctly, price is above them, distance from the annual low confirms momentum, proximity to the yearly high confirms leadership, and the 200-day moving average is rising, suggesting sustained institutional support.

The trend template was designed for stocks, where Minervini also applies fundamental filters and a relative strength ranking from Investor's Business Daily. This article adapts seven technical conditions for forex pairs on the daily timeframe. The eighth condition (RS ranking) cannot be replicated in MQL5 without an external data feed, so it is replaced with a 14-period RSI momentum filter. The entry signal is the Volatility Contraction Pattern breakout—a series of progressively tighter price swings ending in a high-volume breakout through resistance—simplified here to a 20-bar high breakout on expanding volume.

We will cover the following topics:

  1. The Eight Conditions and What They Measure
  2. Adapting the Template for Forex
  3. Implementation in MQL5
  4. Backtesting
  5. Known Limitations
  6. Conclusion


The Eight Conditions and What They Measure

Minervini's eight conditions are listed below exactly as he specified them in “Think and Trade Like a Champion.” Understanding what each one measures is necessary before coding it.

Condition 1: Price above the 150-day and 200-day moving averages. Price must be trading above both the 150-day simple moving average and the 200-day simple moving average simultaneously. These are the long-term trend filters. A market trading below either of these levels is not in a confirmed uptrend regardless of recent momentum.

Condition 2: The 150-day moving average is above the 200-day moving average. This confirms that the intermediate trend has been above the long-term trend for a meaningful period. When the 150-day crosses above the 200-day, it signals that the intermediate trend is now stronger than the long-term baseline.

Condition 3: The 200-day moving average is trending upward for at least one month. The 200-day moving average must be higher today than it was 20 trading days ago. This eliminates markets where the long-term moving average is flat or declining—ensuring that institutional buying has been persistent, not just recent.

Condition 4: The 50-day moving average is above both the 150-day and the 200-day. The short-term trend must be above both longer-term trends. This stacking confirms that momentum has been accelerating—the most recent 50 days are stronger than the prior 150 and 200.

Condition 5: Price is above the 50-day moving average. The most recent trend must support the current price. A market trading below its 50-day moving average is pulling back in a way that undermines the momentum argument.

Condition 6: Price is at least 25% above its 52-week low. This confirms that genuine accumulation has occurred. A market that has only moved 5% from its yearly low has not built meaningful momentum. Minervini specifies 25% in "Think and Trade Like a Champion" (30% in an earlier book)—we use 25%.

Condition 7: Price is within 25% of its 52-week high. This confirms leadership. A market that is 60% below its yearly high has serious overhead resistance. Being within 25% of the high means the market is near breakout territory—it is a leader, not a laggard.

Condition 8: Relative Strength rating of 70 or higher. In Minervini's original method, this is the Investor's Business Daily RS ranking—a proprietary measure of a stock's price performance relative to all other stocks over 12 months. This is not available for forex pairs in MetaTrader 5. We substitute the 14-period RSI being above 50 as a directional momentum confirmation. This is a simplification that preserves the intent of the condition—momentum must be directionally positive—while remaining calculable from price data alone.

All eight conditions must pass simultaneously. Minervini is explicit: a market failing any single condition is not a candidate, regardless of how strongly it passes the other seven.


Adapting the Template for Forex

Three adaptations are needed to apply this method to forex pairs in MetaTrader 5.

  1. Timeframe. Minervini's conditions are specified in trading days. The daily timeframe in MetaTrader 5 maps directly: the 50-day, 150-day, and 200-day moving averages are available as standard indicators. The 52-week high and low span the last 252 trading days—we use 250 bars as a close approximation.
  2. No relative strength ranking. The IBD RS ranking requires a database of all stocks ranked by 12-month price performance. There is no equivalent for forex pairs in MetaTrader 5. The 14-period RSI above 50 serves as the momentum substitute for condition 8. This is a known simplification that is disclosed in the Known Limitations section.
  3. Entry trigger. Minervini enters on VCP breakouts—Volatility Contraction Pattern—which require identifying a series of three to four progressively tighter price swings within a base. Full VCP detection is a complete article in itself. For this EA, the entry trigger is simplified to a close above the 20-bar high with volume at least 1.5× the 20-bar average. This captures the breakout aspect of a VCP without swing-count detection. If all eight conditions pass and the breakout criteria are met, the EA enters the trade.


Implementation in MQL5

We build the EA section by section, explaining each component before showing the code.

Includes and Input Parameters

We begin by including the standard trade library and defining the EA's input parameters.

//+------------------------------------------------------------------+
//|                                              TrendTemplateEA.mq5 |
//|                                Copyright 2026, Tola Moses Hector |
//|                                          https://t.me/tolahector |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Tola Moses Hector"
#property link      "https://t.me/tolahector"
#property version   "1.00"
#property description "Automating Classic Market Methods in MQL5 Part 4"
#property description "Mark Minervini Trend Template EA"
#property description "All 8 conditions must pass simultaneously before entry"
#property description "Daily timeframe recommended"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Trend Template MAs ==="
input int    InpMA50Period       = 50;    // 50-day moving average period
input int    InpMA150Period      = 150;   // 150-day moving average period
input int    InpMA200Period      = 200;   // 200-day moving average period
input int    InpMA200SlopeBars   = 20;    // Bars to confirm 200-day MA uptrend

input group "=== 52-Week Range ==="
input int    InpYearBars         = 250;   // Bars representing one year
input double InpMinAboveLowPct   = 25.0;  // Minimum percent above 52-week low
input double InpMaxBelowHighPct  = 25.0;  // Maximum percent below 52-week high

input group "=== Entry Trigger ==="
input int    InpBreakoutBars     = 20;    // Bars for breakout high calculation
input double InpVolBreakoutMult  = 1.5;   // Volume multiplier required at breakout
input int    InpVolAvgPeriod     = 20;    // Bars for average volume calculation
input int    InpRSIPeriod        = 14;    // RSI period for condition 8
input double InpRSIMin           = 50.0;  // Minimum RSI for entry

input group "=== Entry and Risk ==="
input double InpRiskPercent      = 1.0;   // Risk per trade as percent of balance
input double InpRR               = 3.0;   // Risk-reward ratio
input int    InpATRPeriod        = 14;    // ATR period for stop placement
input double InpATRMult          = 2.0;   // ATR multiplier for stop distance

input group "=== Exit ==="
input bool   InpExitBelowMA50   = true;   // Exit if close below 50-day MA
input bool   InpTradeShorts      = false;  // Trade short setups (mirror conditions)

input group "=== General ==="
input int    InpMagicNumber      = 111001; // Magic number
input int    InpSlippage         = 10;     // Slippage in points
input bool   InpShowLabels       = true;   // Draw labels on chart

The inputs are organized to mirror the structure of the trend template itself. The first group covers the three moving averages and the slope confirmation period. The second group covers the 52-week range conditions. The third group covers the entry trigger—the breakout signal that fires after all eight template conditions are satisfied. Note that "InpTradeShorts" defaults to false. The trend template is fundamentally a bull market filter. Short trading using mirror conditions is available but requires careful validation—the inverse conditions are less cleanly defined than the original eight.

Global Variables and Indicator Handles

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade g_trade;                                    // Trade execution object
int    g_ma50_handle  = INVALID_HANDLE;            // 50-day MA handle
int    g_ma150_handle = INVALID_HANDLE;            // 150-day MA handle
int    g_ma200_handle = INVALID_HANDLE;            // 200-day MA handle
int    g_atr_handle   = INVALID_HANDLE;            // ATR handle
int    g_rsi_handle   = INVALID_HANDLE;            // RSI handle
datetime g_last_bar   = 0;                         // Last processed bar time
bool   g_in_trade     = false;                     // Position tracking flag

Five indicator handles cover all needs: three moving averages for the stack conditions, ATR for stop sizing, and RSI for condition 8. All five are created once in "OnInit()" and released in "OnDeinit()."

Utility Functions

"PipSize()" and "CalcLots()" are identical to those in Parts 1 through 3 of this series. They are included in full for completeness.

//+------------------------------------------------------------------+
//| Returns pip size for the current symbol                          |
//+------------------------------------------------------------------+
double PipSize()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); // Get symbol digits
   return (digits == 3 || digits == 5) ? _Point * 10.0 : _Point; // Return pip size
  }

//+------------------------------------------------------------------+
//| Calculates lot size from risk percent and stop distance in pips  |
//+------------------------------------------------------------------+
double CalcLots(double sl_pips)
  {
   double balance    = AccountInfoDouble(ACCOUNT_BALANCE);                  // Get balance
   double risk_money = balance * InpRiskPercent / 100.0;                    // Monetary risk
   double tick_val   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);  // Tick value
   double tick_size  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);   // Tick size
   double pip_size   = PipSize();                                           // Get pip size
   if(tick_size <= 0 || tick_val <= 0 || sl_pips <= 0)
      return 0;                                                             // Validate inputs
   double pip_value  = (pip_size / tick_size) * tick_val;                   // Pip monetary value
   double lots       = risk_money / (sl_pips * pip_value);                  // Raw lot size
   double step       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);       // Volume step
   double min_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);        // Minimum lot
   double max_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);        // Maximum lot
   lots = MathFloor(lots / step) * step;                                    // Normalize to step
   return MathMax(min_lot, MathMin(max_lot, lots));                         // Clamp to limits
  }

Chart Drawing

//+------------------------------------------------------------------+
//| Places a text label at the specified time and price              |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr)
  {
   if(!InpShowLabels)
      return;                                                               // Check flag
   string obj = "TTM_" + name;                                              // Build object name
   ObjectDelete(0, obj);                                                    // Remove existing
   ObjectCreate(0, obj, OBJ_TEXT, 0, time, price);                          // Create text object
   ObjectSetString(0, obj, OBJPROP_TEXT, text);                             // Set text content
   ObjectSetInteger(0, obj, OBJPROP_COLOR, clr);                            // Set color
   ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9);                           // Set font size
   ChartRedraw(0);                                                          // Redraw chart
  }

//+------------------------------------------------------------------+
//| Removes all chart objects created by this EA                     |
//+------------------------------------------------------------------+
void ClearLabels()
  {
   int total = ObjectsTotal(0);                                             // Get object count
   for(int i = total - 1; i >= 0; i--)                                      // Iterate backwards
     {
      string name = ObjectName(0, i);                                       // Get object name
      if(StringFind(name, "TTM_") == 0)
         ObjectDelete(0, name);                                             // Delete if EA's
     }
   ChartRedraw(0);                                                          // Redraw chart
  }

The Eight Condition Check

This is the heart of the EA. The "CheckTrendTemplate()" function evaluates all eight conditions and returns true only if each one passes. If any condition fails, the function returns false immediately and logs which condition blocked the entry. This logging is important during testing—it tells you exactly which condition is filtering out potential trades on any given bar.

//+------------------------------------------------------------------+
//| Evaluates all 8 Trend Template conditions                        |
//| Returns true only if every condition passes simultaneously       |
//+------------------------------------------------------------------+
bool CheckTrendTemplate()
  {
//--- Copy all indicator values needed
   double ma50_buf[], ma150_buf[], ma200_buf[], rsi_buf[];
   ArraySetAsSeries(ma50_buf,  true);                                                 // Set as series
   ArraySetAsSeries(ma150_buf, true);                                                 // Set as series
   ArraySetAsSeries(ma200_buf, true);                                                 // Set as series
   ArraySetAsSeries(rsi_buf,   true);                                                 // Set as series
   int bars_needed = InpMA200SlopeBars + 2;                                           // Bars needed
   if(CopyBuffer(g_ma50_handle,  0, 1, bars_needed, ma50_buf)  < bars_needed)
      return false;                                                                   // Copy MA50
   if(CopyBuffer(g_ma150_handle, 0, 1, bars_needed, ma150_buf) < bars_needed)
      return false;                                                                   // Copy MA150
   if(CopyBuffer(g_ma200_handle, 0, 1, bars_needed, ma200_buf) < bars_needed)
      return false;                                                                   // Copy MA200
   if(CopyBuffer(g_rsi_handle,   0, 1, 1, rsi_buf)             < 1)
      return false;                                                                   // Copy RSI
   double price   = iClose(_Symbol, PERIOD_CURRENT, 1);                               // Last close
   double ma50    = ma50_buf[0];                                                      // Current MA50
   double ma150   = ma150_buf[0];                                                     // Current MA150
   double ma200   = ma200_buf[0];                                                     // Current MA200
   double ma200_past = ma200_buf[InpMA200SlopeBars];                                  // MA200 one month ago
   double rsi     = rsi_buf[0];                                                       // Current RSI
//--- Get 52-week high and low
   double high_buf[], low_buf[];                                                      // Year high/low buffers
   ArraySetAsSeries(high_buf, true);                                                  // Set as series
   ArraySetAsSeries(low_buf,  true);                                                  // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, InpYearBars, high_buf) < InpYearBars)
      return false;                                                                   // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, 1, InpYearBars, low_buf)  < InpYearBars)
      return false;                                                                   // Copy lows
   double year_high = 0, year_low = DBL_MAX;                                          // Init year range
   for(int i = 0; i < InpYearBars; i++)                                               // Scan year bars
     {
      year_high = MathMax(year_high, high_buf[i]);                                    // Update year high
      year_low  = MathMin(year_low,  low_buf[i]);                                     // Update year low
     }
//--- Condition 1: Price above MA150 and MA200
   if(price <= ma150)
     { Print("TTM: C1 FAIL — price below MA150"); return false; }                     // Condition 1 failed
   if(price <= ma200)
     { Print("TTM: C1 FAIL — price below MA200"); return false; }                     // Condition 1 failed
//--- Condition 2: MA150 above MA200
   if(ma150 <= ma200)
     { Print("TTM: C2 FAIL — MA150 not above MA200"); return false; }                 // Condition 2 failed
//--- Condition 3: MA200 trending up for at least one month
   if(ma200 <= ma200_past)
     { Print("TTM: C3 FAIL — MA200 not rising"); return false; }                      // Condition 3 failed
//--- Condition 4: MA50 above MA150 and MA200
   if(ma50 <= ma150)
     { Print("TTM: C4 FAIL — MA50 not above MA150"); return false; }                  // Condition 4 failed
   if(ma50 <= ma200)
     { Print("TTM: C4 FAIL — MA50 not above MA200"); return false; }                  // Condition 4 failed
//--- Condition 5: Price above MA50
   if(price <= ma50)
     { Print("TTM: C5 FAIL — price below MA50"); return false; }                      // Condition 5 failed
//--- Condition 6: Price at least 25% above 52-week low
   if(year_low <= 0)
      return false;                                                                   // Invalid year low
   double above_low_pct = ((price - year_low) / year_low) * 100.0;                    // Percent above low
   if(above_low_pct < InpMinAboveLowPct)
     {
      Print(StringFormat("TTM: C6 FAIL — %.1f%% above low (need %.1f%%)",
                         above_low_pct, InpMinAboveLowPct));                          // Log failure
      return false;                                                                   // Condition 6 failed
     }
//--- Condition 7: Price within 25% of 52-week high
   if(year_high <= 0)
      return false;                                                                   // Invalid year high
   double below_high_pct = ((year_high - price) / year_high) * 100.0;                 // Percent below high
   if(below_high_pct > InpMaxBelowHighPct)
     {
      Print(StringFormat("TTM: C7 FAIL — %.1f%% below high (max %.1f%%)",
                         below_high_pct, InpMaxBelowHighPct));                        // Log failure
      return false;                                                                   // Condition 7 failed
     }
//--- Condition 8: RSI above minimum (momentum substitute for RS rating)
   if(rsi < InpRSIMin)
     {
      Print(StringFormat("TTM: C8 FAIL — RSI %.1f below %.1f", rsi, InpRSIMin));      // Log failure
      return false;                                                                   // Condition 8 failed
     }
   Print(StringFormat("TTM: ALL PASS | Price:%.5f | MA50:%.5f | MA150:%.5f | MA200:%.5f | AboveLow:%.1f%% | BelowHigh:%.1f%% | RSI:%.1f",
                      price, ma50, ma150, ma200, above_low_pct, below_high_pct, rsi));// Log pass
   return true;                                                                       // All conditions passed
  }

 In this function, each condition is checked in sequence, numbered exactly as Minervini specified them. When a condition fails, a specific message is printed identifying which condition blocked the entry and why—this is diagnostic output that makes the EA testable and transparent. When all eight pass, the function logs the key values and returns true. The calling code then checks the breakout trigger.

The Breakout Entry Trigger

Passing the trend template confirms that the market is in the right structural position. The breakout trigger is what tells us when to enter.

//+------------------------------------------------------------------+
//| Returns true if last bar closed above the 20-bar high on volume  |
//+------------------------------------------------------------------+
bool IsBreakout(bool is_long)
  {
   double close1 = iClose(_Symbol, PERIOD_CURRENT, 1);                     // Last bar close
//--- Get breakout reference level
   if(is_long)
     {
      double high_buf[];                                                    // High buffer
      ArraySetAsSeries(high_buf, true);                                     // Set as series
      if(CopyHigh(_Symbol, PERIOD_CURRENT, 2, InpBreakoutBars, high_buf) < InpBreakoutBars)
         return false;                                                      // Copy highs
      double ref_high = 0;                                                  // Reference high
      for(int i = 0; i < InpBreakoutBars; i++)
         ref_high = MathMax(ref_high, high_buf[i]);                         // Find period high
      if(close1 <= ref_high)
         return false;                                                      // No breakout
     }
   else
     {
      double low_buf[];                                                     // Low buffer
      ArraySetAsSeries(low_buf, true);                                      // Set as series
      if(CopyLow(_Symbol, PERIOD_CURRENT, 2, InpBreakoutBars, low_buf) < InpBreakoutBars)
         return false;                                                      // Copy lows
      double ref_low = DBL_MAX;                                             // Reference low
      for(int i = 0; i < InpBreakoutBars; i++)
         ref_low = MathMin(ref_low, low_buf[i]);                            // Find period low
      if(close1 >= ref_low)
         return false;                                                      // No breakout
     }
//--- Check volume confirmation
   long vol_buf[];                                                          // Volume buffer
   ArraySetAsSeries(vol_buf, true);                                         // Set as series
   if(CopyTickVolume(_Symbol, PERIOD_CURRENT, 1, InpVolAvgPeriod + 1, vol_buf) < InpVolAvgPeriod + 1)
      return false;                                                         // Copy volumes
   long   breakout_vol = vol_buf[0];                                        // Breakout bar volume
   double avg_vol      = 0;                                                 // Average accumulator
   for(int i = 1; i <= InpVolAvgPeriod; i++)
      avg_vol += (double)vol_buf[i];                                        // Sum prior volumes
   avg_vol /= InpVolAvgPeriod;                                              // Compute average
   bool vol_ok = ((double)breakout_vol > avg_vol * InpVolBreakoutMult);     // Volume check
   Print(StringFormat("TTM: Breakout | BreakoutVol:%I64d | AvgVol:%.0f | Ratio:%.2f | VolOK:%s",
                      breakout_vol, avg_vol,
                      (double)breakout_vol / avg_vol, vol_ok ? "YES" : "NO")); // Log result
   return vol_ok;                                                           // Return result
  }

The breakout reference is taken from bars 2 onward—excluding the current bar—so the breakout bar's own high does not contaminate the reference level. This prevents the common coding mistake where a bar always appears to break out above its own high. The volume confirmation follows the same pattern used in Parts 1 through 3: the current bar's volume must exceed 1.5 times the prior 20-bar average.

Trade Execution

//+------------------------------------------------------------------+
//| Opens a long trade on Trend Template + breakout confirmation     |
//+------------------------------------------------------------------+
void OpenLong()
  {
   double atr_buf[];                                                        // ATR buffer
   ArraySetAsSeries(atr_buf, true);                                         // Set as series
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;                                                               // Copy ATR
   double atr    = atr_buf[0];                                              // ATR value
   double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);                   // Current ask
   double sl     = ask - atr * InpATRMult;                                  // Stop loss price
   double sl_pip = (ask - sl) / PipSize();                                  // Stop in pips
   double tp     = ask + sl_pip * InpRR * PipSize();                        // Take profit price
   double lots   = CalcLots(sl_pip);                                        // Calculate lot size
   if(lots <= 0)
      return;                                                               // Invalid lot size
   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);  // Broker stop level
   double min_dist = stop_lv * _Point;                                      // Minimum distance
   if(ask - sl < min_dist)
      sl = ask - min_dist - _Point;                                         // Adjust SL if needed
   if(tp - ask < min_dist)
      tp = ask + min_dist + _Point;                                         // Adjust TP if needed
   if(g_trade.Buy(lots, _Symbol, ask, sl, tp, "Minervini TT Long"))         // Open long
     {
      datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                      // Get bar time
      DrawLabel("ENTRY", t1,
                iLow(_Symbol, PERIOD_CURRENT, 1) - PipSize() * 5,
                "TT LONG", clrDodgerBlue);                                  // Draw label
      Print(StringFormat("TTM: LONG opened | Lots:%.2f | Ask:%.5f | SL:%.5f | TP:%.5f",
                         lots, ask, sl, tp));                               // Log trade
      g_in_trade = true;                                                    // Update flag
     }
  }
//+------------------------------------------------------------------+
//| Opens a short trade on inverted Trend Template + breakdown       |
//+------------------------------------------------------------------+
void OpenShort()
  {
   if(!InpTradeShorts)
      return;                                                               // Shorts disabled
   double atr_buf[];                                                        // ATR buffer
   ArraySetAsSeries(atr_buf, true);                                         // Set as series
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1)
      return;                                                               // Copy ATR
   double atr    = atr_buf[0];                                              // ATR value
   double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);                   // Current bid
   double sl     = bid + atr * InpATRMult;                                  // Stop loss price
   double sl_pip = (sl - bid) / PipSize();                                  // Stop in pips
   double tp     = bid - sl_pip * InpRR * PipSize();                        // Take profit price
   double lots   = CalcLots(sl_pip);                                        // Calculate lot size
   if(lots <= 0)
      return;                                                               // Invalid lot size
   long   stop_lv  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);  // Broker stop level
   double min_dist = stop_lv * _Point;                                      // Minimum distance
   if(sl - bid < min_dist)
      sl = bid + min_dist + _Point;                                         // Adjust SL if needed
   if(bid - tp < min_dist)
      tp = bid - min_dist - _Point;                                         // Adjust TP if needed
   if(g_trade.Sell(lots, _Symbol, bid, sl, tp, "Minervini TT Short"))       // Open short
     {
      datetime t1 = iTime(_Symbol, PERIOD_CURRENT, 1);                      // Get bar time
      DrawLabel("ENTRY", t1,
                iHigh(_Symbol, PERIOD_CURRENT, 1) + PipSize() * 5,
                "TT SHORT", clrCrimson);                                    // Draw label
      Print(StringFormat("TTM: SHORT opened | Lots:%.2f | Bid:%.5f | SL:%.5f | TP:%.5f",
                         lots, bid, sl, tp));                               // Log trade
      g_in_trade = true;                                                    // Update flag
     }
  }

Exit Management

Minervini's exit rules are precise. For longs, he exits when price closes below the 50-day moving average on meaningful volume—this signals that the institutional sponsorship underlying the Stage 2 advance is faltering. The "CheckExit()" function checks this condition on each new bar for any open position.

//+------------------------------------------------------------------+
//| Checks whether open positions should be exited                   |
//+------------------------------------------------------------------+
void CheckExit()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)                                            // Iterate positions
     {
      ulong ticket = PositionGetTicket(i);                                                   // Get ticket
      if(!PositionSelectByTicket(ticket))
         continue;                                                                           // Select position
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)
         continue;                                                                           // Check symbol
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber)
         continue;                                                                           // Check magic
      long   pos_type = PositionGetInteger(POSITION_TYPE);                                   // Position type
      double close1   = iClose(_Symbol, PERIOD_CURRENT, 1);                                  // Last close
      double ma50_buf[];                                                                     // MA50 buffer
      ArraySetAsSeries(ma50_buf, true);                                                      // Set as series
      if(CopyBuffer(g_ma50_handle, 0, 1, 1, ma50_buf) < 1)
         continue;                                                                           // Copy MA50
      double ma50 = ma50_buf[0];                                                             // Current MA50
      bool exit_long  = InpExitBelowMA50 && pos_type == POSITION_TYPE_BUY  && close1 < ma50; // Long exit
      bool exit_short = InpExitBelowMA50 && pos_type == POSITION_TYPE_SELL && close1 > ma50; // Short exit
      if(exit_long || exit_short)                                                            // Exit condition met
        {
         if(g_trade.PositionClose(ticket))                                                   // Close position
           {
            string dir = (pos_type == POSITION_TYPE_BUY) ? "LONG" : "SHORT";                 // Direction
            Print(StringFormat("TTM: %s closed — MA50 breach. Ticket:%I64u", dir, ticket));  // Log exit
            g_in_trade = false;                                                              // Update flag
            ClearLabels();                                                                   // Clear chart
           }
        }
     }
  }

OnInit, OnDeinit, and OnTick

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_ma50_handle  = iMA(_Symbol, PERIOD_CURRENT, InpMA50Period,  0, MODE_SMA, PRICE_CLOSE); // Create MA50
   g_ma150_handle = iMA(_Symbol, PERIOD_CURRENT, InpMA150Period, 0, MODE_SMA, PRICE_CLOSE); // Create MA150
   g_ma200_handle = iMA(_Symbol, PERIOD_CURRENT, InpMA200Period, 0, MODE_SMA, PRICE_CLOSE); // Create MA200
   g_atr_handle   = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);                            // Create ATR
   g_rsi_handle   = iRSI(_Symbol, PERIOD_CURRENT, InpRSIPeriod, PRICE_CLOSE);               // Create RSI
   if(g_ma50_handle  == INVALID_HANDLE ||                                                   // Check MA50
      g_ma150_handle == INVALID_HANDLE ||                                                   // Check MA150
      g_ma200_handle == INVALID_HANDLE ||                                                   // Check MA200
      g_atr_handle   == INVALID_HANDLE ||                                                   // Check ATR
      g_rsi_handle   == INVALID_HANDLE)                                                     // Check RSI
     {
      Print("TrendTemplateEA: Indicator handle creation failed.");                          // Log error
      return INIT_FAILED;                                                                   // Return failure
     }
   g_trade.SetExpertMagicNumber(InpMagicNumber);                                            // Set magic number
   g_trade.SetDeviationInPoints(InpSlippage);                                               // Set slippage
   g_in_trade = false;                                                                      // No initial trade
   g_last_bar = 0;                                                                          // Initialize bar time
   Print("TrendTemplateEA initialized | Symbol:", _Symbol,
         " | TF:", EnumToString(Period()),
         " | Magic:", InpMagicNumber);                                                      // Log initialization
   return INIT_SUCCEEDED;                                                                   // Return success
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_ma50_handle);                                                         // Release MA50
   IndicatorRelease(g_ma150_handle);                                                        // Release MA150
   IndicatorRelease(g_ma200_handle);                                                        // Release MA200
   IndicatorRelease(g_atr_handle);                                                          // Release ATR
   IndicatorRelease(g_rsi_handle);                                                          // Release RSI
   ClearLabels();                                                                           // Remove chart objects
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0);                                // Current bar open time
   if(current_bar == g_last_bar)
      return;                                                                               // Skip if same bar
   g_last_bar = current_bar;                                                                // Update last bar time
//--- Check exit conditions for any open position
   CheckExit();                                                                             // Check exit signals
   if(g_in_trade)
      return;                                                                               // No new entry while in trade
//--- Evaluate Trend Template for long setup
   if(CheckTrendTemplate())                                                                 // All 8 conditions pass
     {
      if(IsBreakout(true))                                                                  // Breakout confirmed
        {
         OpenLong();                                                                        // Enter long
         return;                                                                            // Exit tick
        }
     }
//--- Evaluate inverse template for short setup
   if(InpTradeShorts && CheckInverseTemplate())                                             // Inverse conditions pass
     {
      if(IsBreakout(false))                                                                 // Breakdown confirmed
         OpenShort();                                                                       // Enter short
     }
  }

"OnTick()" is deliberately simple. It runs the exit check first, then evaluates the trend template, then checks the breakout trigger. The short-side "CheckInverseTemplate()" function—which mirrors all eight conditions in reverse—is called only when "InpTradeShorts" is enabled.

The Inverse Template for Short Trades

When "InpTradeShorts" is enabled, the EA applies the mirror of all eight conditions to detect markets in confirmed Stage 4 declines.

//+------------------------------------------------------------------+
//| Evaluates the inverse of all 8 conditions for short setups       |
//+------------------------------------------------------------------+
bool CheckInverseTemplate()
  {
   double ma50_buf[], ma150_buf[], ma200_buf[], rsi_buf[];
   ArraySetAsSeries(ma50_buf,  true);                                       // Set as series
   ArraySetAsSeries(ma150_buf, true);                                       // Set as series
   ArraySetAsSeries(ma200_buf, true);                                       // Set as series
   ArraySetAsSeries(rsi_buf,   true);                                       // Set as series
   int bars_needed = InpMA200SlopeBars + 2;                                 // Bars needed
   if(CopyBuffer(g_ma50_handle,  0, 1, bars_needed, ma50_buf)  < bars_needed)
      return false;                                                         // Copy MA50
   if(CopyBuffer(g_ma150_handle, 0, 1, bars_needed, ma150_buf) < bars_needed)
      return false;                                                         // Copy MA150
   if(CopyBuffer(g_ma200_handle, 0, 1, bars_needed, ma200_buf) < bars_needed)
      return false;                                                         // Copy MA200
   if(CopyBuffer(g_rsi_handle,   0, 1, 1, rsi_buf)             < 1)
      return false;                                                         // Copy RSI
   double price      = iClose(_Symbol, PERIOD_CURRENT, 1);                  // Last close
   double ma50       = ma50_buf[0];                                         // Current MA50
   double ma150      = ma150_buf[0];                                        // Current MA150
   double ma200      = ma200_buf[0];                                        // Current MA200
   double ma200_past = ma200_buf[InpMA200SlopeBars];                        // MA200 one month ago
   double rsi        = rsi_buf[0];                                          // Current RSI
   double high_buf[], low_buf[];
   ArraySetAsSeries(high_buf, true);                                        // Set as series
   ArraySetAsSeries(low_buf,  true);                                        // Set as series
   if(CopyHigh(_Symbol, PERIOD_CURRENT, 1, InpYearBars, high_buf) < InpYearBars)
      return false;                                                         // Copy highs
   if(CopyLow(_Symbol, PERIOD_CURRENT, 1, InpYearBars, low_buf)  < InpYearBars)
      return false;                                                         // Copy lows
   double year_high = 0, year_low = DBL_MAX;                                // Init year range
   for(int i = 0; i < InpYearBars; i++)
     {
      year_high = MathMax(year_high, high_buf[i]);                          // Update year high
      year_low  = MathMin(year_low,  low_buf[i]);                           // Update year low
     }
//--- Inverse conditions: price below MAs, MAs stacked downward, MA200 declining
   if(price >= ma150)
      return false;                                                         // Price above MA150 — skip
   if(price >= ma200)
      return false;                                                         // Price above MA200 — skip
   if(ma150 >= ma200)
      return false;                                                         // MA150 not below MA200 — skip
   if(ma200 >= ma200_past)
      return false;                                                         // MA200 not declining — skip
   if(ma50  >= ma150)
      return false;                                                         // MA50 not below MA150 — skip
   if(ma50  >= ma200)
      return false;                                                         // MA50 not below MA200 — skip
   if(price >= ma50)
      return false;                                                         // Price above MA50 — skip
   if(year_high <= 0)
      return false;                                                         // Invalid year high
   double below_high_pct = ((year_high - price) / year_high) * 100.0;       // Percent below high
   if(below_high_pct < InpMinAboveLowPct)
      return false;                                                         // Not far enough below high
   if(year_low <= 0)
      return false;                                                         // Invalid year low
   double above_low_pct = ((price - year_low) / year_low) * 100.0;          // Percent above low
   if(above_low_pct > InpMaxBelowHighPct)
      return false;                                                         // Too far above low
   if(rsi > (100.0 - InpRSIMin))
      return false;                                                         // RSI too high for short
   Print("TTM: INVERSE ALL PASS — short setup confirmed");                  // Log pass
   return true;                                                             // All inverse conditions passed
  }
//+------------------------------------------------------------------+

The inverse template mirrors each condition exactly: price must be below both longer-term moving averages, the moving averages must be stacked downward rather than upward, the 200-day must be declining rather than rising, and RSI must be below the inverse threshold rather than above it.


Backtesting

To test the EA, open the MetaTrader 5 Strategy Tester and configure the following settings:

Test Settings

  • Symbol: EURUSD
  • Timeframe: Daily (D1)
  • Modeling mode: Every tick based on real ticks
  • Initial deposit: $10,000
  • Test period: January 1, 2015–December 31, 2024

The extended test period is recommended because the Daily timeframe produces fewer trading signals than H4. A ten-year period also provides a more representative sample covering different market conditions.

EA Input Parameters

Moving Averages

  • InpMA50Period = 50
  • InpMA150Period = 150
  • InpMA200Period = 200
  • InpMA200SlopeBars = 20

Price Range and Breakout Filters

  • InpYearBars = 250
  • InpMinAboveLowPct = 25
  • InpMaxBelowHighPct = 25
  • InpBreakoutBars = 20
  • InpVolBreakoutMult = 1.5

RSI Filter

  • InpRSIPeriod = 14
  • InpRSIMin = 50

Risk Management

  • InpRiskPercent = 1.0
  • InpRR = 3.0

ATR-Based Stop-Loss Settings

  • InpATRPeriod = 14
  • InpATRMult = 2.0

What to Expect

The trend template is an extremely selective filter. On EURUSD daily, the combination of all eight conditions passing simultaneously will be rare—perhaps 5 to 15 qualifying setups over a ten-year test. This is correct behavior. Minervini applies this filter to thousands of stocks simultaneously to find the handful that qualify. Applied to a single forex pair, you will see long periods with no entries followed by occasional sustained trends that satisfy all conditions.

The journal will log which condition blocks entry on each bar. Over a long test, this output reveals the pattern: conditions 6 and 7 together—price at least 25% above the yearly low and within 25% of the yearly high simultaneously—are the most restrictive pair. They together require a market that has moved substantially from its low but is still near its high, which describes a relatively narrow window within a sustained trend.

Test Results

Demonstration input parameters

Fig. 1. Demonstration input parameters


Demonstration

Fig. 2. Demonstration

Balance and equity graph

Fig. 3. Balance and equity graph

Test results

Fig. 4. Test results

Test results entries

Fig. 5. Test results—entries


Known Limitations

The RS rating cannot be replicated in MQL5. Minervini's eighth condition requires the Investor's Business Daily RS ranking, a proprietary score that measures a stock's price performance relative to all other stocks over the past 12 months. There is no equivalent for forex pairs. The RSI above 50 substitution preserves the directional momentum intent of the condition but is a simplification. This is the most significant departure from Minervini's original specification.

The method was designed for stocks with strong earnings growth. Minervini applies fundamental filters before the technical template—companies must have accelerating earnings and revenue growth before chart analysis begins. These fundamental conditions cannot be applied to forex pairs. The trend template conditions alone, without fundamental backing, will produce fewer high-quality setups than Minervini's complete SEPA methodology.

The 200-day moving average requires substantial history. The EA needs at least 250 bars before "OnInit()" to calculate the 200-day moving average reliably. In the Strategy Tester, ensure the test start date is preceded by sufficient historical data. The "Warm-up period" setting in the tester can be used to add history before the test start date.

The VCP entry trigger is simplified. Minervini's actual entry is on the breakout from a Volatility Contraction Pattern—a series of three to four progressively tighter price swings within a base. The 20-bar high breakout used here captures the breakout character but does not verify the contraction pattern. Implementing full VCP detection would be the logical next step for a production version of this EA.

The short template is less well defined. Minervini's original method is explicitly long-only in its public form. The inverse conditions used here for short setups are a logical extension but have not been validated by Minervini himself. Use with additional caution.


Conclusion

The trend template's power is not in any individual condition. A market trading above its 50-day moving average is a common observation. A market where the 150-day is above the 200-day is also common. A market where price is within 25% of its 52-week high while also being 25% above its 52-week low is less common. A market satisfying all eight conditions simultaneously is rare—and that rarity is the point.

Minervini spent decades studying markets that moved explosively and looking for what they had in common before the move. The trend template is his answer. Every condition on the list was there, in the data, before every major move he studied. Requiring all eight simultaneously does not add eight independent filters—it selects for a specific structural condition that only a fraction of markets satisfy at any given time.

The EA in this article implements that checklist mechanically. Every condition is checked on every bar. Every failure is logged. An entry is triggered only when the checklist is clean. No manual judgment is required, and none is permitted—the code either passes all eight or it does not enter.

All code was compiled and tested in MetaTrader 5. The complete "TrendTemplateEA.mq5" source file is attached to this article. Copy to “MQL5\Experts\” and compile in MetaEditor with no additional dependencies. Always test on a demo account before live deployment.

Attached files |
TrendTemplateEA.mq5 (34.04 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor
We are going to improve the usability of the automated optimization conveyor: we will explore the process from creating an optimization project to testing the final EA. For clarity, let us walk through the entire process step by step creating the final EA, while stopping to make any desired corrections.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
How to Research a Trading Idea: A Range Breakout Strategy Case Study How to Research a Trading Idea: A Range Breakout Strategy Case Study
This article demonstrates a practical approach to researching trading ideas using a range breakout strategy as an example. We will go through the entire process, from formalizing trading rules and building a baseline model to parameter optimization, forward testing, and evaluating the robustness of the results. The main goal of the article is to develop an understanding of how statistics and testing can be used to identify, validate, and evaluate trading hypotheses.