Automating Trading Strategies in MQL5 (Part 53): Double Top and Double Bottom Reversal Model
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:
- Understanding the Double Top and Double Bottom Reversal Model
- Implementation in MQL5
- Backtesting
- 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.

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.

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.

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.

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.

The backtest report is shown below.

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
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.
Building a Swing-Based Volume Profile Indicator in MQL5
Did Your Scale Outs Actually Help? A Scale Out Value Analyzer in MQL5
Uncertainty as a Model (Part 1): Random Variables — The Language of Uncertainty
MetaTrader 5 Machine Learning Blueprint (Part 21): Feature Importance Analysis
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use