Русский
preview
Automating Trading Strategies in MQL5 (Part 53): Double Top and Double Bottom Reversal Model

Automating Trading Strategies in MQL5 (Part 53): Double Top and Double Bottom Reversal Model

MetaTrader 5 — Trading |
2 346 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

A double top or double bottom is easy to see after the fact but hard to trade in real time: peaks rarely match exactly, the valley or ridge between them can be shallow, and human eyes tend to impose tidy shapes on noisy price action. For algorithmic developers the problem is worse. Without fixed rules the same pattern appears and disappears depending on who drew the neckline, which bars count as pivots, or how lenient the tolerance is. That non-determinism makes signals unreproducible in the Strategy Tester, encourages post-hoc parameter fitting, and defeats robust automation.

In our previous article (Part 52), we automated the tCISD model, which pairs Quarterly Theory cycles with a correlated-symbol divergence. This article fixes the problem above by translating the pattern into a strict, testable contract. We build a MetaQuotes Language 5 (MQL5) program that formalizes every judgment: confirmed swing pivots, a quantitative peak-matching tolerance scaled by pattern height, a neckline defined from the intervening pivot, configurable spacing and leg-balance filters, and explicit trigger rules (close through the neckline or optional pullback). The program also defines stop placement (stop beyond the paired extreme with a buffer), target rules (measured-move or reward-to-risk), lot sizing modes, and optional trailing stops. The result is a reproducible detection, trigger, and risk-management pipeline you can run in the Strategy Tester and tune without falling back on visual judgment. We will cover the following topics:

  1. Understanding the Double Top and Double Bottom Reversal Model
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end of this article, you will be able to:

  • Detect double tops and double bottoms from confirmed swing pivots and validate each with peak tolerance, leg balance, and spacing filters.
  • Mark the peaks and neckline on the chart and enter a reversal on the break or an optional pullback, with the stop beyond the pattern extreme and a measured-move or reward-to-risk target managed by a trailing stop.


Understanding the Double Top and Double Bottom Reversal Model

A double top and a double bottom are the market's way of failing twice at the same price. In a double top, price rallies to a high, pulls back, and rallies again to roughly the same high before turning down, and the failure of that second push to make new ground signals that buyers are exhausted. The double bottom is the mirror image: two troughs at about the same low, where sellers fail to break lower the second time and control passes back to buyers. The line drawn through the reaction point between the two peaks is the neckline, the level the whole pattern hinges on, because the pattern is only complete once price closes through it.

A rule-based approach outperforms visual judgment because it turns each subjective decision into a parameter. Peak matching becomes a tolerance scaled by pattern height, and leg balance compares bar counts on each leg to reject lopsided shapes. Spacing and an optional prior-trend requirement filter out peaks too close together or patterns that appear mid-range with nothing to reverse. Once a pattern passes the filters, the anchors are explicit: the neckline is the trigger, the paired extreme anchors the stop, and the neckline-to-extreme distance defines the target. The break of the neckline confirms the reversal, and an optional pullback to the retested level offers a second, often better-priced entry. The general strategy architecture is shown below.

DOUBLE TOP AND DOUBLE BOTTOM

When trading the model, wait for the second peak to form and confirm it stalls near the first rather than pushing decisively past it. Treat the neckline as your line in the sand, and let a close through it, not a mere touch, mark the reversal. Decide in advance whether you take the break directly or hold out for the pullback, which fills less often but at a tighter risk. Anchor your stop just beyond the twin peaks so an invalidation is unambiguous, and project the pattern height from the neckline for a first target that reflects the size of the structure you traded. Skip patterns with no clear trend leading into them, because a reversal needs something to reverse. In a nutshell, here is a visualization of what we will create.

DOUBLE TOP AND DOUBLE BOTTOM EXPERT ADVISOR


Implementation in MQL5

Before beginning the implementation, it is worth fixing what the program will count as a pattern, because every function below exists to serve these rules. A swing pivot is confirmed only once a set number of bars have closed on both sides of it with none printing a higher high or lower low. Two peaks pair into a double top or double bottom only when their prices sit within a tolerance expressed as a percentage of the pattern height, so one setting means the same thing on a tall pattern and a shallow one. The neckline is the pivot between the two peaks, and nothing else. A shape is rejected when the peaks sit closer together than the minimum spacing, when the two legs differ in bar count by more than the leg-balance allowance, or when an optional prior trend into the pattern is absent. The trade triggers only on a bar closing through the neckline, with the option to wait for a pullback to that level instead, and it is abandoned if price violates the paired extreme or the confirmation window expires. Everything that follows is that contract expressed in code.

With the contract fixed, we begin the implementation with the foundation the rest of the program builds on: the trade library, the enumerations that expose our choices, the input set, and the global state that carries a pattern from detection through to entry.

//+------------------------------------------------------------------+
//|                                 Double Top and Double Bottom.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

//--- Include the standard library for order execution
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum LotSizingMode
  {
   LOTS_FIXED,        // Fixed lot size
   LOTS_RISK_PERCENT  // Risk percent of balance (auto lot)
  };

enum TakeProfitMode
  {
   TP_MEASURED_MOVE,  // Project the pattern height from the neckline
   TP_REWARD_RISK     // Multiple of the stop distance
  };

enum SetupState
  {
   STATE_IDLE,        // No active pattern
   STATE_ARMED,       // Pattern found, waiting for the neckline break
   STATE_RETEST       // Break confirmed, waiting for the pullback
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "GENERAL"
input long            InpMagicNumber        = 1107;             // Magic number (unique ID for this EA)
input LotSizingMode   InpLotSizingMode      = LOTS_RISK_PERCENT; // Lot sizing mode
input double          InpFixedLots          = 0.01;             // Fixed lot
input double          InpRiskPercent        = 0.5;              // Risk per trade (percent of balance)
input string          InpOrderComment       = "DTB";            // Order comment

input group "PATTERN"
input ENUM_TIMEFRAMES InpEntryTimeframe     = PERIOD_CURRENT;   // Working timeframe
input int             InpSwingLength        = 5;                // Swing pivot length (bars each side)
input double          InpPeakTolerancePercent = 10.0;           // Max peak difference as percent of pattern height
input int             InpMinPatternBars     = 10;               // Minimum bars from first to second peak (0 = off)
input double          InpLegBalancePercent  = 50.0;             // Shorter leg as percent of longer leg, minimum (0 = off)
input bool            InpIncludeBreakLeg    = false;            // Fold the break leg into the balance test at breakout
input bool            InpRequirePriorTrend  = false;             // Require a prior trend into the pattern

input group "STOP LOSS"
input int             InpStopBufferPoints   = 3000;               // Buffer beyond the pattern extreme (points)
input int             InpMinStopPoints      = 0;                // Skip the trade if the stop is closer than this (points, 0 = off)

input group "TAKE PROFIT"
input TakeProfitMode  InpTakeProfitMode     = TP_MEASURED_MOVE; // Target mode
input double          InpRewardRiskRatio    = 1.0;              // Reward to risk ratio (R:R mode)

input group "ENTRY"
input bool            InpTradePullback      = false;             // Wait for the pullback to the neckline after the breakout
input int             InpMaxConfirmBars     = 50;               // Bars to wait for the neckline break
input int             InpMaxRetestBars      = 24;               // Bars to wait for the pullback

input group "TRAILING STOP"
input bool            InpUseTrailingStop    = true;            // Use trailing stop
input int             InpMinProfitPoints    = 1000;             // Minimum profit to activate trailing (points)
input int             InpTrailPoints        = 300;              // Trailing distance (points)

input group "LOGGING"
input bool            InpShowLogs           = true;             // Print messages to the Journal
input string          InpLogPrefix          = "DTB> ";          // Log prefix

input group "VISUALS"
input bool            InpDrawVisuals        = true;             // Draw patterns on the chart
input int             InpMarkerSize         = 10;               // Peak marker size (Wingdings 3)
input color           InpBullColor          = clrDodgerBlue;    // Double bottom color
input color           InpBearColor          = clrRed;           // Double top color
input color           InpNecklineColor      = clrGoldenrod;     // Neckline color
input color           InpTrendColor         = clrGray;          // Leading trend leg color

//+------------------------------------------------------------------+
//| Swing pivot point (high or low)                                  |
//+------------------------------------------------------------------+
struct SwingPoint
  {
   bool     isHigh;  // True for a swing high, false for a swing low
   double   price;   // Pivot price
   datetime time;    // Pivot bar time
  };

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   trade;                        // Trade execution object
int      symbolDigits;                 // Cached symbol digits
double   symbolPoint;                  // Cached symbol point size
datetime lastBarTime = 0;              // Last processed bar time

SwingPoint swings[];                   // Rolling buffer of detected swing pivots

//--- Pattern state machine
int        setupState           = STATE_IDLE; // Current setup stage
int        patternDirection     = 0;          // Pattern direction: -1 top, +1 bottom
double     necklineLevel        = 0.0;        // Neckline price
double     patternExtreme       = 0.0;        // Pattern high (top) or low (bottom)
datetime   peakOneTime          = 0;          // Time of the first peak
datetime   peakTwoTime          = 0;          // Time of the second peak
double     peakOnePrice         = 0.0;        // Price of the first peak
double     peakTwoPrice         = 0.0;        // Price of the second peak
datetime   necklineTime         = 0;          // Time of the neckline pivot
bool       hasLead              = false;      // True when a leading trend leg exists
datetime   leadTime             = 0;          // Time of the leading trend start
double     leadPrice            = 0.0;        // Price of the leading trend start
datetime   necklineStartTime    = 0;          // Time the neckline line starts drawing
datetime   lastPatternEndTime   = 0;          // Second-peak time of the last pattern
datetime   breakoutTime         = 0;          // Time of the neckline breakout bar
datetime   pullbackExtremeTime  = 0;          // Time of the pullback extreme
double     pullbackExtremePrice = 0.0;        // Price of the pullback extreme
int        barsInState          = 0;          // Bars elapsed in the current stage

We start by including the standard "Trade.mqh" library for order execution, then declare three enumerations that shape the program's behavior. The "LotSizingMode" enumeration selects fixed or risk-based lots, the "TakeProfitMode" enumeration chooses between a measured-move target and a reward-to-risk multiple, and the "SetupState" enumeration names the three stages a setup passes through: idle, armed and waiting for the neckline break, and waiting for the pullback. That last enumeration is the backbone of the state machine that drives the whole entry sequence.

The inputs group tunable settings: pattern rules (tolerance, balance, spacing, prior-trend), plus stop, target, entry, trailing, and visual options. We then define the "SwingPoint" structure to hold a single confirmed pivot, its price, time, and whether it is a high or a low, since the whole detection works off a rolling history of these pivots. The global block that follows keeps that pivot buffer and the full state of the active pattern, from the two peaks and the neckline to the extreme we anchor the stop against and the counter that ages each stage, so every step of the setup has a fixed place to read and write as bars arrive.

Drawing the Pattern and Storing Swings

These helpers put the detected structure on the chart and feed the detector. Three of them render the pattern, its extended neckline, and any pullback path, while the fourth appends confirmed pivots to a bounded history.

//+------------------------------------------------------------------+
//| Draw the full double top or bottom pattern                       |
//+------------------------------------------------------------------+
void DrawPattern()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build a per-pattern id from the second peak time
   string id = IntegerToString((int)peakTwoTime);
//--- Pick the color and orientation for the pattern side
   color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor;
   bool topPattern = (patternDirection < 0);
//--- Extend drawing to the current bar
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Draw the leading trend leg when present
   if(hasLead)
      DrawTrend("DTB_Lead_" + id, leadTime, leadPrice, peakOneTime, peakOnePrice, InpTrendColor, STYLE_DOT, 1);
//--- Draw the first leg from peak one to the neckline
   DrawTrend("DTB_LegA_" + id, peakOneTime, peakOnePrice, necklineTime, necklineLevel, patternColor, STYLE_SOLID, 2);
//--- Draw the second leg from the neckline to peak two
   DrawTrend("DTB_LegB_" + id, necklineTime, necklineLevel, peakTwoTime, peakTwoPrice, patternColor, STYLE_SOLID, 2);
//--- Draw the horizontal neckline
   DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1);
//--- Label the neckline
   DrawLabel("DTB_Neckt_" + id, necklineStartTime, necklineLevel, " Neckline", InpNecklineColor,
             topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
//--- Mark the first peak
   DrawMarker("DTB_P1_" + id, peakOneTime, peakOnePrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER);
//--- Mark the second peak
   DrawMarker("DTB_P2_" + id, peakTwoTime, peakTwoPrice, !topPattern, patternColor, topPattern ? ANCHOR_LOWER : ANCHOR_UPPER);
//--- Mark the neckline pivot
   DrawMarker("DTB_N_"  + id, necklineTime, necklineLevel, topPattern, InpNecklineColor, topPattern ? ANCHOR_UPPER : ANCHOR_LOWER);
//--- Label the first peak
   DrawLabel("DTB_P1t_" + id, peakOneTime, peakOnePrice, topPattern ? " First Top" : " First Bottom",
             patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
//--- Label the second peak
   DrawLabel("DTB_P2t_" + id, peakTwoTime, peakTwoPrice, topPattern ? " Second Top" : " Second Bottom",
             patternColor, topPattern ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
  }

//+------------------------------------------------------------------+
//| Extend the neckline line to the current bar                      |
//+------------------------------------------------------------------+
void ExtendNeckline()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build the per-pattern id and current bar time
   string id = IntegerToString((int)peakTwoTime);
   datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Redraw the neckline out to the current bar
   DrawTrend("DTB_Neck_" + id, necklineStartTime, necklineLevel, now, necklineLevel, InpNecklineColor, STYLE_DASH, 1);
  }

//+------------------------------------------------------------------+
//| Draw the pullback path back to the neckline                      |
//+------------------------------------------------------------------+
void DrawPullback()
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build the per-pattern id and the touch bar time
   string id = IntegerToString((int)peakTwoTime);
   datetime touchTime = iTime(_Symbol, InpEntryTimeframe, 1);
//--- Pick the pattern color
   color patternColor = (patternDirection < 0) ? InpBearColor : InpBullColor;
//--- Draw the leg from the breakout to the pullback extreme
   DrawTrend("DTB_PBa_" + id, breakoutTime, necklineLevel, pullbackExtremeTime, pullbackExtremePrice, patternColor, STYLE_DASH, 1);
//--- Draw the leg from the pullback extreme back to the neckline
   DrawTrend("DTB_PBb_" + id, pullbackExtremeTime, pullbackExtremePrice, touchTime, necklineLevel, patternColor, STYLE_DASH, 1);
//--- Label the pullback
   DrawLabel("DTB_PBt_" + id, touchTime, necklineLevel, " Pullback", patternColor,
             patternDirection < 0 ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
  }

//+------------------------------------------------------------------+
//| Append a swing pivot and cap the rolling buffer                  |
//+------------------------------------------------------------------+
void AddSwing(bool isHigh, double price, datetime time)
  {
//--- Skip if this exact pivot is already stored
   int n = ArraySize(swings);
   for(int i = 0; i < n; i++)
      if(swings[i].time == time && swings[i].isHigh == isHigh) return;
//--- Append the new pivot
   ArrayResize(swings, n + 1);
   swings[n].isHigh = isHigh; swings[n].price = price; swings[n].time = time;
//--- Drop the oldest pivot once the buffer exceeds 80 entries
   if(ArraySize(swings) > 80)
     {
      //--- Shift entries down over the gap and shrink the array
      for(int i = 0; i < ArraySize(swings) - 1; i++) swings[i] = swings[i + 1];
      ArrayResize(swings, ArraySize(swings) - 1);
     }
  }

We define the "DrawPattern" function to lay out the whole shape once a pattern is found, drawing the two legs, the horizontal neckline, an optional leading trend leg, and the peak and neckline markers with their labels, all flipped in orientation and color by whether it is a top or a bottom. The "ExtendNeckline" function keeps that neckline stretched to the current bar while we wait, so the trigger level stays visible as price approaches it, and the "DrawPullback" function traces the path from the breakout down to the retest so a pullback entry is easy to read after the fact. The one piece that matters beyond visuals is the "AddSwing" function: it appends each confirmed pivot to the "swings" history, ignores duplicates, and caps the buffer at eighty entries by dropping the oldest, which keeps the pattern search working off a bounded, recent set of pivots rather than growing without limit over a long run.

Detecting Swing Pivots

The pattern search runs entirely on swing pivots, so we define the "DetectSwings" function to confirm and store them one bar at a time.

//+------------------------------------------------------------------+
//| Detect a confirmed swing high or low and store it                |
//+------------------------------------------------------------------+
void DetectSwings()
  {
//--- Center on the candidate bar with length bars each side
   int length = InpSwingLength;
   int candidate = length + 1;
//--- Require enough history to test both sides
   if(iBars(_Symbol, InpEntryTimeframe) < 2 * length + 2) return;
//--- Read the candidate bar high and low
   double candidateHigh = iHigh(_Symbol, InpEntryTimeframe, candidate);
   double candidateLow  = iLow(_Symbol, InpEntryTimeframe, candidate);
//--- Assume both a high and a low pivot until a neighbor breaks it
   bool isHigh = true, isLow = true;
//--- Compare the candidate against length bars on each side
   for(int k = 1; k <= length; k++)
     {
      //--- Reject the high candidate when a neighboring bar breaks it
      if(iHigh(_Symbol, InpEntryTimeframe, candidate - k) >= candidateHigh ||
         iHigh(_Symbol, InpEntryTimeframe, candidate + k) >  candidateHigh) isHigh = false;
      //--- Reject the low candidate when a neighboring bar breaks it
      if(iLow(_Symbol, InpEntryTimeframe, candidate - k) <= candidateLow ||
         iLow(_Symbol, InpEntryTimeframe, candidate + k) <  candidateLow) isLow = false;
     }
//--- Store the confirmed pivot(s)
   datetime candidateTime = iTime(_Symbol, InpEntryTimeframe, candidate);
   if(isHigh) AddSwing(true, candidateHigh, candidateTime);
   if(isLow)  AddSwing(false, candidateLow, candidateTime);
  }

We define the "DetectSwings" function to test a settled candidate bar, one sitting far enough from the right edge to have equal numbers of bars on each side, and confirm it as a pivot only when no neighbor within that window prints a higher high or lower low. A bar can qualify as both a swing high and a swing low, and each confirmation is handed to the "AddSwing" function for storage. Anchoring the candidate away from the live bar is what makes a pivot final rather than provisional, so the detector never marks a swing that a later bar could undo, which is exactly the reliability the pattern matching depends on.

Arming a Detected Pattern

Once the search finds a valid shape, we define the "ArmPattern" function to record every anchor the later stages need and move the setup into its armed state.

//+------------------------------------------------------------------+
//| Store a detected pattern and arm it for the neckline break       |
//+------------------------------------------------------------------+
void ArmPattern(int direction, double neckline, datetime neckTime, double extreme, datetime t1, double p1, datetime t2, double p2, bool lead, datetime lTime, double lPrice)
  {
//--- Store the pattern direction
   patternDirection = direction;
//--- Store the neckline level and its pivot time
   necklineLevel = neckline; necklineTime = neckTime;
//--- Store the pattern extreme used as the stop anchor
   patternExtreme = extreme;
//--- Store the two peak times and prices
   peakOneTime = t1; peakOnePrice = p1;
   peakTwoTime = t2; peakTwoPrice = p2;
//--- Store the leading trend leg details
   hasLead = lead; leadTime = lTime; leadPrice = lPrice;
//--- Remember this pattern's second peak as the last pattern end
   lastPatternEndTime = t2;
//--- Default the neckline drawing to start at the first peak
   necklineStartTime = t1;
//--- Move the neckline start onto the leading leg where it crosses
   if(hasLead && MathAbs(p1 - lPrice) > 0)
     {
      //--- Interpolate the crossing point along the leading leg
      double ratio = (neckline - lPrice) / (p1 - lPrice);
      if(ratio > 0 && ratio < 1)
         necklineStartTime = lTime + (datetime)((double)(t1 - lTime) * ratio);
     }
//--- Arm the setup and reset the bar counter
   setupState = STATE_ARMED; barsInState = 0;
//--- Log the armed pattern
   Log((direction < 0 ? "Double top" : "Double bottom") + " armed | neckline " +
       DoubleToString(neckline, symbolDigits) + " | waiting for the break");
//--- Draw the pattern
   DrawPattern();
  }

We define the "ArmPattern" function to latch a confirmed pattern into the global state: its direction, the neckline, and its pivot time, the paired extreme that will anchor the stop, both peaks, and any leading trend leg. It stamps the second peak as the end of the last pattern so the next search cannot overlap it, flips the state to armed, and draws the shape. The one calculation worth noting is where the drawn neckline should begin: rather than starting it flatly at the first peak, we interpolate the point where the neckline price crosses the leading leg, so the horizontal line meets the trend that led into the pattern cleanly instead of floating in mid-air.

Searching for a Double Top or Bottom

This is the core of the detection, where we define the "TryDetectPattern" function to turn the swing history into a validated pattern.

//+------------------------------------------------------------------+
//| Search the recent swings for a double top or bottom              |
//+------------------------------------------------------------------+
void TryDetectPattern()
  {
//--- Only search while idle
   if(setupState != STATE_IDLE) return;
//--- Require at least three swings to form a pattern
   int n = ArraySize(swings);
   if(n < 3) return;
//--- Take the most recent swing as the second peak
   SwingPoint latest = swings[n - 1];
//--- Convert the peak tolerance percent to a fraction
   double tolerance = InpPeakTolerancePercent / 100.0;
//--- Double top: the latest swing is a high
   if(latest.isHigh)
     {
      //--- Find the previous swing high before the latest
      int priorHigh = -1;
      for(int i = n - 2; i >= 0; i--) if(swings[i].isHigh) { priorHigh = i; break; }
      if(priorHigh < 0) return;
      //--- Reject overlap with the previous pattern
      if(swings[priorHigh].time < lastPatternEndTime) return;
      //--- Take the lowest low between the two highs as the neckline
      double neckline = 0; datetime neckTime = 0; bool found = false;
      for(int i = priorHigh + 1; i < n - 1; i++)
         if(!swings[i].isHigh && (!found || swings[i].price < neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; }
      if(!found) return;
      //--- Measure the pattern height above the neckline
      double peak1 = swings[priorHigh].price, peak2 = latest.price;
      double height = (peak1 + peak2) / 2.0 - neckline;
      if(height <= 0) return;
      //--- Reject when the two peaks differ by more than the tolerance
      if(MathAbs(peak1 - peak2) > tolerance * height) return;
      //--- Resolve the bar shifts of the two peaks and the neckline
      int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorHigh].time, false);
      int shiftNeck    = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false);
      int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false);
      //--- Reject when the two peaks are too close together
      if(InpMinPatternBars > 0 && (shiftPeakOne - shiftPeakTwo) < InpMinPatternBars) return;
      //--- Reject when the two legs are too unbalanced
      if(InpLegBalancePercent > 0)
        {
         //--- Measure the two legs in bars
         double legA = (double)(shiftPeakOne - shiftNeck);
         double legB = (double)(shiftNeck - shiftPeakTwo);
         if(legA <= 0 || legB <= 0) return;
         //--- Require the shorter leg to meet the balance percent
         if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return;
        }
      //--- Find a leading low before the first peak for the trend leg
      bool lead = false; datetime lTime = 0; double lPrice = 0;
      for(int i = priorHigh - 1; i >= 0; i--) if(!swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; }
      //--- Require a prior downtrend into the pattern when enabled
      if(InpRequirePriorTrend && (!lead || lPrice >= neckline)) return;
      //--- Arm the double top
      ArmPattern(-1, neckline, neckTime, MathMax(peak1, peak2), swings[priorHigh].time, peak1, latest.time, peak2, lead, lTime, lPrice);
     }
//--- Double bottom: the latest swing is a low
   else
     {
      //--- Find the previous swing low before the latest
      int priorLow = -1;
      for(int i = n - 2; i >= 0; i--) if(!swings[i].isHigh) { priorLow = i; break; }
      if(priorLow < 0) return;
      //--- Reject overlap with the previous pattern
      if(swings[priorLow].time < lastPatternEndTime) return;
      //--- Take the highest high between the two lows as the neckline
      double neckline = 0; datetime neckTime = 0; bool found = false;
      for(int i = priorLow + 1; i < n - 1; i++)
         if(swings[i].isHigh && (!found || swings[i].price > neckline)) { neckline = swings[i].price; neckTime = swings[i].time; found = true; }
      if(!found) return;
      //--- Measure the pattern height below the neckline
      double trough1 = swings[priorLow].price, trough2 = latest.price;
      double height = neckline - (trough1 + trough2) / 2.0;
      if(height <= 0) return;
      //--- Reject when the two troughs differ by more than the tolerance
      if(MathAbs(trough1 - trough2) > tolerance * height) return;
      //--- Resolve the bar shifts of the two troughs and the neckline
      int shiftTroughOne = iBarShift(_Symbol, InpEntryTimeframe, swings[priorLow].time, false);
      int shiftNeck      = iBarShift(_Symbol, InpEntryTimeframe, neckTime, false);
      int shiftTroughTwo = iBarShift(_Symbol, InpEntryTimeframe, latest.time, false);
      //--- Reject when the two troughs are too close together
      if(InpMinPatternBars > 0 && (shiftTroughOne - shiftTroughTwo) < InpMinPatternBars) return;
      //--- Reject when the two legs are too unbalanced
      if(InpLegBalancePercent > 0)
        {
         //--- Measure the two legs in bars
         double legA = (double)(shiftTroughOne - shiftNeck);
         double legB = (double)(shiftNeck - shiftTroughTwo);
         if(legA <= 0 || legB <= 0) return;
         //--- Require the shorter leg to meet the balance percent
         if(100.0 * MathMin(legA, legB) / MathMax(legA, legB) < InpLegBalancePercent) return;
        }
      //--- Find a leading high before the first trough for the trend leg
      bool lead = false; datetime lTime = 0; double lPrice = 0;
      for(int i = priorLow - 1; i >= 0; i--) if(swings[i].isHigh) { lead = true; lTime = swings[i].time; lPrice = swings[i].price; break; }
      //--- Require a prior uptrend into the pattern when enabled
      if(InpRequirePriorTrend && (!lead || lPrice <= neckline)) return;
      //--- Arm the double bottom
      ArmPattern(1, neckline, neckTime, MathMin(trough1, trough2), swings[priorLow].time, trough1, latest.time, trough2, lead, lTime, lPrice);
     }
  }

The "TryDetectPattern" function searches recent swings only when no setup is active. It treats the newest pivot as the second peak and scans back for a matching pivot of the same type. The neckline is the deepest opposite pivot sitting between the pair, the lowest low between two highs for a top or the highest high between two lows for a bottom, and its distance to the peaks gives the pattern height. That height is what turns loose visual judgment into numbers: the two peaks must agree within the tolerance expressed as a fraction of the height, the two legs must be balanced enough in bar count, the peaks must be far enough apart, and an optional prior-trend check demands a real move led into the shape. The direction is mirrored exactly between the top and bottom cases, and only a pattern that clears every enabled filter is passed to the "ArmPattern" function, with its extreme set to the outer of the two peaks so the stop later sits beyond the whole structure. Let us initialize the system and run the pattern search function in the tick event handler to see the progress. Here is the approach we use to achieve that.

Wiring the Event Handlers

With the detection in place, we add our startup work to the "OnInit" event handler, our cleanup to the "OnDeinit" event handler, and the per-bar sequence to the "OnTick" event handler.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Cache the symbol digits and point size
   symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   symbolPoint  = _Point;
//--- Configure the trade object magic number and slippage
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(20);
//--- Clear the swing buffer
   ArrayResize(swings, 0);
//--- Reset the setup state
   setupState = STATE_IDLE; patternDirection = 0;
//--- Reset the pattern history anchors
   lastPatternEndTime = 0; necklineStartTime = 0;
//--- Seed the bar-time guard
   lastBarTime = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Log a ready banner with the key settings
   Log("Double Top and Double Bottom EA ready on " + _Symbol + " | Magic " + IntegerToString(InpMagicNumber));
//--- Report successful initialization
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete our chart objects on a real removal or chart close
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE)
      ObjectsDeleteAll(0, "DTB_");
//--- Clear any chart comment
   Comment("");
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new bar
   if(IsNewBar())
     {
      //--- Detect new swing pivots
      DetectSwings();
      //--- Try to detect and arm a pattern
      TryDetectPattern();
      if(VisualsAllowed()) ChartRedraw(0);
     }
  }

In the OnInit event handler, we prepare the program before the first tick by caching the symbol digits and point size, configuring the trade object with our magic number and deviation, clearing the swing buffer and pattern state, seeding the bar-time guard, and returning "INIT_SUCCEEDED" once the ready banner is logged. The "OnDeinit" event handler removes only our own chart objects by their shared prefix on a genuine removal or chart close, leaving the chart clean without wiping visuals on a routine recompile. In the OnTick event handler, we gate the work to a fresh bar, detecting new pivots and then searching for a pattern in that order, since a pattern can only be found once the swings feeding it exist. When we run this, we get the following outcome.

DETECTED DOUBLE BOTTOM PATTERN

From the image, we can see that we have detected the double bottom pattern. What remains is validating it in the next bars or discarding it and starting a fresh look for the pattern. To reset the invalidated or filled setups, we use the following logic.

Resetting the Setup

We define the "ResetSetup" function to return the state machine to idle whenever a pattern is abandoned or filled.

//+------------------------------------------------------------------+
//| Reset the setup state machine to idle                            |
//+------------------------------------------------------------------+
void ResetSetup(string reason)
  {
//--- Log the reset with its direction and reason when active
   if(setupState != STATE_IDLE)
      Log((patternDirection < 0 ? "Double top" : "Double bottom") + " reset: " + reason + ".");
//--- Clear the state, direction and bar counter
   setupState = STATE_IDLE;
   patternDirection = 0;
   barsInState = 0;
  }

We define the "ResetSetup" function to clear the state, direction, and bar counter back to their idle values, logging the direction and the reason first whenever a setup was actually active. Routing every abandonment through one place keeps the Journal honest about why each pattern went away, and returning cleanly to idle is what frees the detector to search for the next shape.

Opening the Trade

With a pattern confirmed and triggered, we define the "OpenTrade" function to size, protect, and send the order, alongside a balance check that can fold in the breakout leg.

//+------------------------------------------------------------------+
//| Size, build SL and TP, and open the trade                        |
//+------------------------------------------------------------------+
void OpenTrade(bool isBull)
  {
//--- Compute the stop buffer in price
   double buffer = InpStopBufferPoints * symbolPoint;
//--- Enter at the market on the correct side
   double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), symbolDigits)
                         : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), symbolDigits);
//--- Place the stop beyond the pattern extreme
   double stop = isBull ? patternExtreme - buffer : patternExtreme + buffer;
//--- Reject the trade when the stop is closer than the minimum
   if(InpMinStopPoints > 0 && MathAbs(entry - stop) / symbolPoint < InpMinStopPoints)
     { ResetSetup("stop below minimum"); return; }
//--- Read the broker minimum stop distance (stops level or spread)
   long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   spread     = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   double minPoints  = (double)MathMax(stopsLevel, spread);
   if(minPoints < 1) minPoints = 1;
//--- Convert the minimum distance to price
   double minDistance = minPoints * symbolPoint;
//--- Push the stop out to the broker minimum when too tight
   if(isBull) { if(entry - stop < minDistance) stop = entry - minDistance; }
   else       { if(stop - entry < minDistance) stop = entry + minDistance; }
//--- Normalize the stop and measure the risk distance
   stop = NormalizeDouble(stop, symbolDigits);
   double riskDistance = MathAbs(entry - stop);
//--- Abort on an invalid risk distance
   if(riskDistance <= 0) { ResetSetup("invalid risk distance"); return; }
//--- Measure the pattern height for the measured-move target
   double height = MathAbs(patternExtreme - necklineLevel);
//--- Build the take profit by the selected mode
   double takeProfit;
   if(InpTakeProfitMode == TP_MEASURED_MOVE)
      //--- Measured move: project the height from the neckline
      takeProfit = isBull ? necklineLevel + height : necklineLevel - height;
   else
      //--- Reward-to-risk: multiple of the stop distance
      takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance
                          : entry - InpRewardRiskRatio * riskDistance;
//--- Push the target out to the broker minimum when too tight
   if(isBull) { if(takeProfit - entry < minDistance) takeProfit = entry + minDistance; }
   else       { if(entry - takeProfit < minDistance) takeProfit = entry - minDistance; }
//--- Normalize the target
   takeProfit = NormalizeDouble(takeProfit, symbolDigits);
//--- Resolve the lot size for this trade
   double lots = ResolveLots(entry, stop);
//--- Abort on a lot sizing error
   if(lots <= 0) { ResetSetup("lot calc error"); return; }
//--- Send the market order on the correct side
   bool ok = isBull ? trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment)
                    : trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment);
//--- Annotate and log a successful fill
   if(ok)
     {
      //--- Draw the entry, stop and target levels
      datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
      DrawTradeLevels(isBull, now, entry, stop, takeProfit);
      //--- Log the fill details
      Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, symbolDigits) +
          "  SL=" + DoubleToString(stop, symbolDigits) + "  TP=" + DoubleToString(takeProfit, symbolDigits) +
          "  lots=" + DoubleToString(lots, 2));
     }
   else
      //--- Log the failure reason
      Log("Open failed: " + trade.ResultRetcodeDescription());
//--- Reset the setup after the attempt
   ResetSetup("filled");
  }

//+------------------------------------------------------------------+
//| Test the three legs including the break leg for balance          |
//+------------------------------------------------------------------+
bool BreakLegBalanced()
  {
//--- Resolve the bar shifts of the two peaks and the neckline
   int shiftPeakOne = iBarShift(_Symbol, InpEntryTimeframe, peakOneTime, false);
   int shiftNeck    = iBarShift(_Symbol, InpEntryTimeframe, necklineTime, false);
   int shiftPeakTwo = iBarShift(_Symbol, InpEntryTimeframe, peakTwoTime, false);
//--- Measure the three legs in bars, including the break leg to now
   double legA = (double)(shiftPeakOne - shiftNeck);
   double legB = (double)(shiftNeck - shiftPeakTwo);
   double legC = (double)(shiftPeakTwo - 1);
//--- Fail on any non-positive leg
   if(legA <= 0 || legB <= 0 || legC <= 0) return false;
//--- Compare the shortest and longest legs
   double shortest = MathMin(legA, MathMin(legB, legC));
   double longest  = MathMax(legA, MathMax(legB, legC));
//--- Pass when the shortest leg meets the balance percent
   return (100.0 * shortest / longest >= InpLegBalancePercent);
  }

We define the "OpenTrade" function to enter the market and place the stop beyond the paired extreme by the buffer, rejecting the trade when that stop is tighter than our minimum and then nudging both stop and target out to the broker's own minimum distance so the order is always accepted. The target is where the two modes diverge: a measured move projects the pattern height from the neckline, giving a goal that scales with the structure itself, while the reward-to-risk mode simply multiplies the stop distance. After resolving the lot and sending the order through the Buy or Sell method of the trade object, we draw the trade levels, log the fill, and reset the setup, since the pattern has had its one attempt.

We also define the "BreakLegBalanced" function to extend the leg-balance idea to the breakout itself, measuring the two pattern legs plus the leg from the second peak to the current bar and requiring the shortest of the three to hold up against the longest. This is the optional stricter test that rejects a break arriving too early or too late to sit in proportion with the pattern that produced it.

Progressing the Setup to Entry

We define the "ProgressSetup" function to carry an armed pattern one bar at a time through the neckline break and, when required, the pullback that follows.

//+------------------------------------------------------------------+
//| Advance the setup through the break and pullback to entry        |
//+------------------------------------------------------------------+
void ProgressSetup()
  {
//--- Do nothing while idle
   if(setupState == STATE_IDLE) return;
//--- Count another bar in the current state
   barsInState++;
//--- Read the just-closed bar close, high and low
   double closePrice = iClose(_Symbol, InpEntryTimeframe, 1);
   double highPrice  = iHigh(_Symbol, InpEntryTimeframe, 1);
   double lowPrice   = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Armed stage: wait for the neckline break
   if(setupState == STATE_ARMED)
     {
      //--- Reset if price broke through the pattern extreme first
      if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("price broke above the pattern"); return; }
      if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("price broke below the pattern"); return; }
      //--- Reset if the neckline break never arrived in time
      if(barsInState > InpMaxConfirmBars) { ResetSetup("no neckline break in time"); return; }
      //--- Keep the neckline extended to the current bar
      ExtendNeckline();
      //--- Test for a close through the neckline
      bool confirmed = (patternDirection < 0) ? (closePrice < necklineLevel) : (closePrice > necklineLevel);
      //--- Handle a confirmed break
      if(confirmed)
        {
         //--- Optionally reject when the break leg fails the balance test
         if(InpIncludeBreakLeg && InpLegBalancePercent > 0 && !BreakLegBalanced())
           { ResetSetup("break leg fails balance"); return; }
         //--- Log the confirmed break
         Log((patternDirection < 0 ? "Double top" : "Double bottom") + " confirmed: neckline broken at " +
             DoubleToString(necklineLevel, symbolDigits));
         //--- Record the breakout bar time
         breakoutTime = iTime(_Symbol, InpEntryTimeframe, 1);
         //--- Draw the breakout leg and label
         if(VisualsAllowed())
           {
            //--- Draw the leg from peak two to the breakout
            string bid = IntegerToString((int)peakTwoTime);
            color bcolor = (patternDirection < 0) ? InpBearColor : InpBullColor;
            DrawTrend("DTB_Break_" + bid, peakTwoTime, peakTwoPrice, breakoutTime, necklineLevel, bcolor, STYLE_SOLID, 2);
            DrawLabel("DTB_Breakt_" + bid, breakoutTime, necklineLevel, " Breakout", bcolor,
                      patternDirection < 0 ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER);
           }
         //--- Enter immediately unless a pullback is required
         if(!InpTradePullback) { OpenTrade(patternDirection > 0); return; }
         //--- Seed the pullback extreme and wait for the retest
         pullbackExtremeTime  = breakoutTime;
         pullbackExtremePrice = (patternDirection < 0) ? lowPrice : highPrice;
         setupState = STATE_RETEST; barsInState = 0;
        }
      //--- Stop after the armed stage
      return;
     }
//--- Retest stage: wait for the pullback to the neckline
   if(setupState == STATE_RETEST)
     {
      //--- Reset if the pattern extreme is violated before the pullback
      if(patternDirection < 0 && highPrice > patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; }
      if(patternDirection > 0 && lowPrice < patternExtreme) { ResetSetup("pattern extreme violated before pullback"); return; }
      //--- Reset if the pullback never arrived in time
      if(barsInState > InpMaxRetestBars) { ResetSetup("no pullback in time"); return; }
      //--- Keep the neckline extended to the current bar
      ExtendNeckline();
      //--- Track the deepest pullback extreme for a top
      if(patternDirection < 0 && lowPrice < pullbackExtremePrice)
        { pullbackExtremePrice = lowPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); }
      //--- Track the highest pullback extreme for a bottom
      if(patternDirection > 0 && highPrice > pullbackExtremePrice)
        { pullbackExtremePrice = highPrice; pullbackExtremeTime = iTime(_Symbol, InpEntryTimeframe, 1); }
      //--- Enter short once price pulls back up to the neckline
      if(patternDirection < 0 && highPrice >= necklineLevel) { DrawPullback(); OpenTrade(false); }
      //--- Enter long once price pulls back down to the neckline
      else if(patternDirection > 0 && lowPrice <= necklineLevel) { DrawPullback(); OpenTrade(true); }
     }
  }

We define the "ProgressSetup" function to advance whichever stage the setup is in. In the armed stage, we wait for a close through the neckline, but abandon the pattern first if price instead violates the paired extreme, meaning the shape has failed, or if too many bars pass with no break. A confirmed break can face the optional break-leg balance test before we accept it, after which the program either enters immediately or, when a pullback is required, seeds the retest stage and waits. In the retest stage, we hold until price returns to the neckline, tracking the deepest pullback extreme for the drawn path and entering on the touch, while the same extreme-violation and timeout guards keep a stale setup from lingering. This two-stage split is what lets one pattern serve both the direct break entry and the more patient pullback entry from a single detection. When we call this function in the tick event handler, we get the following outcome.

CONFIRMED DOUBLE BOTTOM SETUP

From the image, we can see that the setups are confirmed and positions are opened. What remains is trailing the positions once they are in our favor, and we use the following logic to achieve that.

Trailing the Open Position

We define the "ManageTrailing" function to advance the stop behind price on our open positions as a trade runs in our favor.

//+------------------------------------------------------------------+
//| Trail the stop on this EA's open positions                       |
//+------------------------------------------------------------------+
void ManageTrailing()
  {
//--- Do nothing when trailing is disabled
   if(!InpUseTrailingStop) return;
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Read the position side, entry, stop and target
      bool   isBull  = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      double entry   = PositionGetDouble(POSITION_PRICE_OPEN);
      double curStop = PositionGetDouble(POSITION_SL);
      double curTP   = PositionGetDouble(POSITION_TP);
      //--- Read the current bid and ask
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      //--- Measure open profit in points
      double profitPoints = isBull ? (bid - entry) / symbolPoint : (entry - ask) / symbolPoint;
      //--- Trail only past the activation threshold
      if(profitPoints >= InpMinProfitPoints + InpTrailPoints)
        {
         //--- Compute the trailed stop behind price
         double newStop = isBull ? bid - InpTrailPoints * symbolPoint : ask + InpTrailPoints * symbolPoint;
         newStop = NormalizeDouble(newStop, symbolDigits);
         //--- Move the stop only when it improves protection
         bool improves = isBull ? (newStop > curStop) : (curStop == 0 || newStop < curStop);
         if(improves) trade.PositionModify(ticket, newStop, curTP);
        }
     }
  }

We define the "ManageTrailing" function to walk the open positions and act only on the ones matching our magic number and symbol, leaving any other program's trades untouched. For each of ours, we measure the open profit in points and, only once it clears the activation threshold of the minimum profit plus the trail distance, compute a stop trailing the set distance behind price. We apply it through the PositionModify method of the trade object only when it tightens protection, never when it would loosen, which is what lets the stop ratchet forward through a run without ever giving ground. We call the function per tick in the event handler. What remains is testing the program, and that is handled in the next section.


Backtesting

We tested the program on XAUUSDm on the five-minute chart from 2025.01.01 to 2025.07.22, on every tick, starting from a ten thousand dollar deposit. The set file attached to this article holds the exact inputs, so the run can be repeated as it stands. Two of those settings matter more than the rest. The prior-trend requirement is off, so a double bottom in the middle of a range is accepted on the same terms as one that ends a downtrend. The pullback option is also off, so every entry lands on the bar that closes through the neckline and the retest stage never engages.

The run produced 53 trades, of which 50 closed in profit, for a profit factor of 2.90 and a net profit of 147.08 dollars, against a maximal equity drawdown of 37.11 dollars, or 0.37 percent. The shape behind those numbers is more useful than the win rate. The average win returned 4.49 dollars while the average loss cost 25.81 dollars, which is the trailing stop at work: it arms one dollar into profit and then follows thirty cents behind price, so most trades close early with a small gain rather than running to the two-to-one target.

The backtest graph is shown below.

GRAPH

The backtest report is shown below.

REPORT


Conclusion

What we delivered is a formal, testable implementation of the double top / double bottom reversal pattern in MQL5. Instead of relying on subjective, on-the-fly decisions, the program implements a complete, reproducible pipeline:

  • Detection from confirmed swing pivots and pairing of two peaks within a tolerance scaled by pattern height.
  • Neckline defined from the opposite pivot and drawn consistently for visibility.
  • Shape validation via spacing, leg-balance, and optional prior-trend filters to reject lopsided or ambiguous structures.
  • Trigger rules that require a bar close through the neckline to confirm the reversal, with an option to instead wait for a pullback retest.
  • Risk management: stop anchored beyond the paired extreme (with configurable buffer and broker-minimum adjustments), TP either as a measured move from the neckline or a reward-to-risk multiple, and optional trailing-stop management.
  • Full program scaffolding: state machine (idle to armed to retest), input parameters for every rule, visual marking of peaks/neckline/entries, and trade execution with a Magic Number so results are reproducible in the Strategy Tester.

This design turns subjective pattern calls into a contract: given the same inputs and market data, the program will make the same detection and trading decisions every time. That reproducibility lets you perform honest backtests, stress tests, and parameter sensitivity studies instead of implicit curve-fitting.

Limitations and next steps: no pattern model removes risk, and thorough backtesting across instruments and timeframes is essential. Use the Strategy Tester to validate robustness, adjust tolerances and timeouts to your market and timeframe, and apply sound position sizing and risk controls before going live.

Disclaimer: educational purposes only. Trading carries significant financial risk and past backtests do not guarantee future profitability.


    The project is supported in Algo Forge: https://forge.mql5.io/29210372/Article-23576-Automating-Trading-Strategies-MQL5-Double-Top-Double-Bottom

    Building a Swing-Based Volume Profile Indicator in MQL5 Building a Swing-Based Volume Profile Indicator in MQL5
    This article presents a swing-based volume profile indicator in MQL5 that analyzes platform tick volume within completed high-to-low and low-to-high legs. It confirms swing points, partitions each leg's range into ATR(200)‑adaptive price bins, distributes tick volume, and marks the Point of Control. The indicator draws a ZigZag and renders the profile on chart rectangles, helping you study volume concentration within each swing relative to price structure.
    Did Your Scale Outs Actually Help? A Scale Out Value Analyzer in MQL5 Did Your Scale Outs Actually Help? A Scale Out Value Analyzer in MQL5
    The article presents an MQL5 tool that tests whether scaling out improved results rather than only appearing disciplined. It reconstructs positions from closing-deal history and reprices the full volume at the first, last, and best exit rates actually achieved, producing a Value-Add Ratio, a Scale-Out Win Rate, and an Efficiency measure. A single-trade dependence check and a configurable A+ to F grade turn these into clear, decision-ready feedback.
    Uncertainty as a Model (Part 1): Random Variables — The Language of Uncertainty Uncertainty as a Model (Part 1): Random Variables — The Language of Uncertainty
    The article provides a systematic overview of the theory of random variables, which serves as the basis for analyzing and modeling uncertainty in financial markets. The article covers the definitions and properties of univariate random variables, cumulative distribution functions (CDFs) and probability density functions (PDFs), as well as the differences between discrete, continuous, and mixed models. The theoretical material is based on intuitive analogies with mass and density. The appendix to this article contains practical examples of using the standard MQL5 library to calculate probabilities, quantiles, and moments of distributions. It also demonstrates the graphical capabilities of the MetaTrader 5 platform for visual data analysis by plotting PDF and CDF curves and QQ plots.
    MetaTrader 5 Machine Learning Blueprint (Part 21): Feature Importance Analysis MetaTrader 5 Machine Learning Blueprint (Part 21): Feature Importance Analysis
    Feature importance often understates correlated predictors by spreading one signal across many engineered copies, while unrelated noise can appear higher. We measure this effect against a known ground truth and compare four remedies: permutation importance with purged cross-validation, single-feature models, and clustered impurity versus clustered accuracy. The results include per-method rankings and a per-cluster dilution ratio that help identify true signals and avoid deleting valuable features.