Automating Classic Market Methods in MQL5 (Part 4): Mark Minervini's Trend Template
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:
- The Eight Conditions and What They Measure
- Adapting the Template for Forex
- Implementation in MQL5
- Backtesting
- Known Limitations
- 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.
- 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.
- 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.
- 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

Fig. 1. Demonstration input parameters

Fig. 2. Demonstration

Fig. 3. Balance and equity graph

Fig. 4. Test results

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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Developing a Multi-Currency Expert Advisor (Part 29): Improving the Conveyor
Features of Experts Advisors
How to Research a Trading Idea: A Range Breakout Strategy Case Study
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use