Automating Chart Patterns in MQL5 (Part 3): The Head and Shoulders and Inverse Head and Shoulders
Introduction
The Head and Shoulders is widely regarded as one of the most reliable reversal patterns in technical analysis. It describes a market that has made three successive peaks during a distribution phase: a left shoulder, a higher head, and a right shoulder that fails to reach the head. The pattern completes when price breaks below the neckline connecting the two intervening troughs. At that moment, the prior uptrend has been structurally broken at three distinct levels, and the evidence for a reversal is stronger than either a Double Top or a single swing high.
In Part 1 of this series, we built CSwingEngine, a swing detection engine that reads market structure from a higher timeframe and labels confirmed swings as HH, LH, HL, or LL. Part 2 applied that engine to double-top and double-bottom patterns. This article applies the same architecture to a pattern with more moving parts: the Head and Shoulders.
The pattern is well known but poorly automated. Most implementations search for three peaks of approximately the right height without asking whether those peaks formed in the correct structural sequence. A head that is geometrically higher than both shoulders is necessary but not sufficient. The left shoulder must form during an uptrend. The head must represent the final push to a new high. The right shoulder must be a lower high, the market's first failure to continue the trend. The neckline break must occur after the right shoulder has fully formed and been confirmed.
This article enforces all of those requirements in code. The swing engine classifies H4 market structure and assigns HH, LH, HL, and LL labels to each confirmed swing point. The detector reads those labels directly to verify the structural sequence, not as a geometric approximation, but as a structural classification. The neckline break that triggers entry is timed on the EA's own chart timeframe for precision.
A confirmed pattern is traded at most once. Once locked, it is monitored until the neckline breaks or the setup expires. The EA will not re-enter on the same right shoulder even if the underlying higher-timeframe structure remains unchanged after a position closes. This separation between detection, confirmation, and execution makes the trading logic more predictable and prevents repeated entries based on the same formation.
The same architecture also supports the Inverse Head and Shoulders: three troughs with the head forming the lowest low, followed by confirmation through a break above the neckline.
A note on timeframes before going further: every "H4" reference in this article describes the default value of "InpSwingTF." The swing timeframe is a configurable input, not a hardcoded assumption—if you set "InpSwingTF" to "PERIOD_D1" or "PERIOD_H1," every "H4" reference below should be read as "the swing timeframe you configured."
A note on units as well: this article standardizes on raw MetaTrader points as the single unit for every reader-facing distance. Head clearance, shoulder symmetry, and pattern height remain ATR multiples rather than a fixed distance, so they scale with the symbol's own volatility, but everything else, position sizing in "CalcLots()", the broker's minimum stop and freeze distances ("SYMBOL_TRADE_STOPS_LEVEL," "SYMBOL_TRADE_FREEZE_LEVEL"), and the trade deviation ("InpSlippage"), is expressed in points. Points is the unit MetaTrader's own API already returns for the stop, freeze, and deviation values—"SymbolInfoInteger()" and "CTrade::SetDeviationInPoints()" are defined in points, not pips, so standardizing the rest of the code on the same unit removes the need for any conversion at all, rather than adding one.

Fig. 1. Visual demonstration of the system.
The Head and Shoulders: Structure and Measurement
The pattern has seven components.
- The prior uptrend. The head must carry the "HH" label, meaning it exceeded the swing high that came before it, and the left trough between the left shoulder and the head must carry the "HL" label, meaning the pullback before the head still held above the prior low. Neither label alone proves a sustained uptrend reaching all the way back through the left shoulder—"HH" only certifies that this specific high exceeded the one immediately before it, under this engine's own labeling rules. Together, the two labels require the swing sequence immediately preceding the head to still be extending higher, which is the evidence this detector checks for, rather than a claim of a full multi-swing trend history. A separate, real-time trend read is not used as an additional gate here, because by the time a right shoulder is confirmed, it is itself the most recent swing high, and the same swing cannot simultaneously satisfy "this is a fresh uptrend high" and "this is a failed lower high." The head's and left trough's labels carry the available evidence instead.
- The left shoulder. The first of the three swing highs. It forms during the uptrend as a normal high before the market's final push.
- The head. The middle swing high, and the highest of the three. In the engine's labeling system, the head carries the "HH" label—it is the highest confirmed swing high of the sequence, the point where the uptrend reached its maximum extent.
- The right shoulder. The third swing high is lower than the head. The engine labels it "LH," a lower high, the market's first structural failure to continue the uptrend. This label check is what separates a genuine Head and Shoulders from a geometric coincidence.
- The neckline. The neckline connects the two troughs between the three peaks. This implementation uses the lower of the two troughs as the neckline price, drawn as a horizontal line. This is the stricter choice: a close below the lower trough has also closed below the higher one, so it requires a more committed move than a break of the higher trough would.
- The neckline break. The entry trigger. The most recently completed bar on the EA's chart timeframe must close below the neckline. The neckline break is evaluated separately from the pattern detection: first, the pattern is confirmed as fully formed; then, the break is watched for on subsequent bars.
- The measured move target. The distance from the head to the neckline, projected downward from the neckline breakpoint. If the head is at 1.1200 and the neckline is at 1.0900, the pattern height is 3000 points. The target is 1.0600.
The Inverse Head and Shoulders mirrors every component. The head carries the "LL" label instead of "HH." The left shoulder is a swing low. The right shoulder is a higher low, labeled "HL." The neckline is the higher of the two intervening peaks, the stricter choice for the same reason: a close above it has also closed above the lower peak. A break above the neckline triggers a long entry.
Why the Label Sequence Matters
The Head and Shoulders is not just three peaks with the middle one highest. It is a specific structural narrative: a trend pushes to a new high (HH), pulls back, pushes to a lower high (LH), then breaks down. The "HH" followed by "LH" sequence in the engine's output is the structural fingerprint of distribution. A pattern where the right shoulder is higher than the head is not a Head and Shoulders; it is an uptrend continuation. The label check enforces this distinction at the code level with a single string comparison.
What Makes a Valid Pattern in This Implementation?
Before any of the eight numbered conditions below are checked, the detector requires at least five confirmed H4 swing points in the engine's array, the minimum needed for five alternating components. This is a cheap early exit, not a guarantee: the swing array may still contain non-alternating runs that prevent all five components from being collected even with five or more swings available, which the collection loop checks for separately.
- Condition 1—Time ordering. The five pattern components must appear in strict sequence: left shoulder, left trough, head, right trough, right shoulder. This is verified against the `time` field of each swing point.
- Condition 2—Pattern width. The span from left shoulder to right shoulder, measured as an actual swing-timeframe bar count via "iBarShift()" rather than a calendar-time calculation, must not exceed "InpMaxPatternBars." A calendar-time calculation would overstate the true bar count across a weekend or a trading-session gap, since no H4 bars form during those hours; "iBarShift()" looks up each swing's real position in the chart's own bar series, so the count stays correct regardless of gaps. This still relies on the engine's documented contract that each swing's "time" field is a genuine bar open time on "InpSwingTF"—it does not rely on any assumption about how the engine orders its internal array.
- Condition 3—Head label. The head must carry the "HH" label. This confirms it exceeded the confirmed swing high before it.
- Condition 4—Left trough label. The left trough must carry the "HL" label. This confirms the pullback before the head still held above the prior low. Condition 3 alone does not establish an uptrend reaching back through the left shoulder; together, Conditions 3 and 4 require the swing sequence immediately preceding the head to still be extending higher.
- Condition 5—Right shoulder label. The right shoulder must carry the "LH" label. This confirms the market failed to make a new high, the first structural sign that the uptrend is failing.
- Condition 6—Head clearance. The head must exceed both shoulders by at least "InpMinHeadHeight" × H4-ATR. This prevents near-equal triple peaks from qualifying as a Head and Shoulders; that shape is closer to a triple top.
- Condition 7—Shoulder symmetry. The two shoulder prices must be within "InpShoulderSymmetry" × H4-ATR of each other. Highly asymmetric shoulders suggest a different structure.
- Condition 8—Pattern height. The distance from the head to the neckline must clear "InpMinHeadHeight" × H4-ATR, the same minimum used for shoulder clearance.
Passing all eight conditions makes a pattern eligible, not tradable. Entry only occurs once a subsequent chart bar closes below the neckline, the pattern has not already been traded, and it has not aged past its allowed break-delay window—all covered in the state machine below.
A note on ATR: the value returned by the ATR handle is validated before use every time it is read. An indicator that has not yet warmed up, or a symbol with a data gap, can return zero or "EMPTY_VALUE" on a given bar, and every distance check in this article multiplies against ATR—an unchecked zero or invalid value would silently disable every tolerance in the EA rather than fail loudly.
File Structure
The EA uses two files:
- "MQL5\Include\ChartPatterns\SwingEngine.mqh"—the swing engine from Part 1, unchanged.
- "MQL5\Experts\HeadShoulder\HeadShouldersEA.mq5"—the Head and Shoulders expert advisor.
No other dependencies are required.
Implementation in MQL5
Inputs
//+------------------------------------------------------------------+ //| HeadShouldersEA.mq5 | //| Copyright 2026, Tola Moses Hector | //| https://t.me/tolahector | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Tola Moses Hector" #property link "https://t.me/tolahector" #property version "1.00" #property description "Head and Shoulders pattern EA" #property description "Swing structure reads InpSwingTF (default H4) internally" #property description "Requires SwingEngine.mqh from Part 1" #include <Trade\Trade.mqh> #include <ChartPatterns\SwingEngine.mqh> input group "=== Swing Engine ===" input int InpSwingStrength = 3; // H4 bars on each side of a swing input int InpSwingLookback = 300; // Maximum H4 bars to scan input ENUM_TIMEFRAMES InpSwingTF = PERIOD_H4; // Timeframe for swing structure input group "=== Pattern Settings ===" input double InpMinHeadHeight = 1.0; // Minimum head clearance above shoulders (swing-TF ATR) input double InpShoulderSymmetry = 2.0; // Maximum shoulder price difference (swing-TF ATR) input int InpMaxPatternBars = 120; // Maximum swing-TF bars from left to right shoulder input int InpMaxBreakDelayBars = 40; // Maximum CHART bars to wait for the neckline break input int InpATRPeriod = 14; // ATR period, computed on InpSwingTF input group "=== Entry and Risk ===" input double InpRiskPercent = 1.0; // Account risk per trade, percent input double InpSLBufferATR = 0.5; // Stop loss buffer beyond the head, swing-TF ATR input bool InpUseMeasuredMove = true; // Use measured move target (false = 2R fallback) input double InpMaxSlipATR = 0.5; // Skip entry if price is already this many ATR past the neckline input group "=== General ===" input int InpMagicNumber = 777002; // Magic number input int InpSlippage = 10; // Trade deviation in points input bool InpShowLabels = true; // Draw pattern labels on chart input bool InpDebugLog = false; // Log the reason every rejected pattern check fails
"InpMaxBreakDelayBars" caps how long a locked pattern is allowed to wait for its neckline break before the setup is considered stale. "InpMaxSlipATR" stops the EA from chasing a neckline that has already been broken by a wide margin. "InpDebugLog" controls a parallel logging path used throughout the detection functions: every condition that rejects a pattern can optionally print why.
Global State and Pattern State Machine
The EA uses a state machine with three states, and every piece of state needed to avoid re-trading the same pattern or waiting on it forever.
//+------------------------------------------------------------------+ //| Pattern lifecycle | //+------------------------------------------------------------------+ enum ENUM_HS_STATE { HS_STATE_SCANNING, // No pattern locked — evaluating every bar HS_STATE_PATTERN_FORMED, // Pattern locked — watching for neckline break or expiry HS_STATE_IN_TRADE // Position open from this pattern }; //+------------------------------------------------------------------+ //| Holds the confirmed pattern while it is locked in | //+------------------------------------------------------------------+ struct SPatternResult { bool valid; bool is_inverse; // true = Inverse H&S (long trade) double left_shoulder; double head; double right_shoulder; double neckline; double target; double stop; datetime ls_time; datetime head_time; datetime rs_time; // Also this pattern's unique identifier, per type datetime neck_time; }; CSwingEngine g_engine; // Swing engine from Part 1 — reads InpSwingTF internally CTrade g_trade; // Trade execution object int g_atr_handle = INVALID_HANDLE; // ATR handle on InpSwingTF datetime g_last_bar = 0; // Last processed CHART bar time string g_obj_prefix = ""; // Unique per symbol + magic number, set in OnInit() string g_gv_hs_name = ""; // GlobalVariable key for the last traded H&S right shoulder string g_gv_ihs_name = ""; // GlobalVariable key for the last traded Inverse H&S right shoulder ENUM_HS_STATE g_state = HS_STATE_SCANNING; // Current pattern lifecycle state SPatternResult g_active_pattern; // The one pattern currently locked in int g_pattern_age_bars = 0; // Chart bars since the pattern was locked in datetime g_last_traded_hs_rs = 0; // Right-shoulder time of the last H&S actually traded datetime g_last_traded_ihs_rs = 0; // Right-shoulder time of the last Inverse H&S actually traded
The "rs_time" doubles as the pattern's identity, the same way Part 2 used the second point's timestamp for the double top and double bottom. The right shoulder is the last component to be confirmed, so its timestamp uniquely identifies the pattern it belongs to.
The state machine works as follows:
- SCANNING: Every bar, the detector checks for a fully formed pattern whose right shoulder has not already been traded. When one is found, the state advances to "PATTERN_FORMED." No trade has been placed yet.
- PATTERN_FORMED: Every bar, the EA revalidates the locked pattern against current swing data, checks whether it has aged past "InpMaxBreakDelayBars," and checks whether the neckline has broken. A revalidation failure or an expiry both return the EA to "SCANNING" without a trade. A neckline break attempts entry.
- IN_TRADE: Every bar, the EA checks "HasOpenPosition()" against the actual position list. When it reports no matching position, the state returns to "SCANNING."
There is no separate boolean flag tracking "Am I in a trade?" anywhere in the EA; the position list itself is the only source of truth, including in "OnInit()," so an EA reattached to a chart with an existing position resolves to the correct state on the first tick rather than assuming a fresh start.
Utility Functions
//+------------------------------------------------------------------+ //| Prints a message only when debug logging is enabled | //+------------------------------------------------------------------+ void DebugLog(string msg) { if(InpDebugLog) Print("HeadShouldersEA[debug]: ", msg); } //+------------------------------------------------------------------+ //| True if a position matching this symbol and magic number exists | //| The single source of truth for "are we in a trade." | //+------------------------------------------------------------------+ bool HasOpenPosition(const string symbol, const long magic) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != magic) continue; return true; } return false; } //+------------------------------------------------------------------+ //| Computes lot size from risk percent and stop distance in points | //| Skips the trade if the risk-appropriate size rounds below the | //| broker minimum, rather than forcing the minimum lot and quietly | //| exceeding InpRiskPercent. | //+------------------------------------------------------------------+ double CalcLots(double sl_points) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double risk_amt = balance * InpRiskPercent / 100.0; double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size <= 0 || tick_value <= 0 || sl_points <= 0) return 0; double point_value = (_Point / tick_size) * tick_value; // Money value of one point, 1.0 lot double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); if(step <= 0) return 0; double raw_lots = risk_amt / (sl_points * point_value); double steps = MathFloor(raw_lots / step + 0.0000001); // Epsilon guards float rounding double lots = NormalizeDouble(steps * step, 8); if(lots < min_lot) return 0; // Skip rather than over-risk return MathMin(max_lot, lots); } //+------------------------------------------------------------------+ //| True if free margin covers the position this order would open | //+------------------------------------------------------------------+ bool HasSufficientMargin(ENUM_ORDER_TYPE order_type, double lots, double price) { double margin_required = 0; if(!OrderCalcMargin(order_type, _Symbol, lots, price, margin_required)) return false; return (margin_required <= AccountInfoDouble(ACCOUNT_MARGIN_FREE)); }
Chart Drawing Helpers
Objects are updated in place when they already exist, and object names are scoped to this EA instance so two attached copies, or an unrelated tool using a similar prefix, cannot delete each other's drawings.
//+------------------------------------------------------------------+ //| Creates or moves a horizontal line at the specified price level | //+------------------------------------------------------------------+ void DrawHLine(string name, double price, color clr, ENUM_LINE_STYLE style) { if(!InpShowLabels) return; string obj = g_obj_prefix + name; if(ObjectFind(0, obj) < 0) { if(!ObjectCreate(0, obj, OBJ_HLINE, 0, 0, price)) { DebugLog("ObjectCreate failed for " + obj); return; } } ObjectSetDouble(0, obj, OBJPROP_PRICE, price); ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); ObjectSetInteger(0, obj, OBJPROP_STYLE, style); ObjectSetInteger(0, obj, OBJPROP_WIDTH, 1); } //+------------------------------------------------------------------+ //| Creates or moves a text label at the specified time and price | //+------------------------------------------------------------------+ void DrawLabel(string name, datetime t, double price, string text, color clr) { if(!InpShowLabels) return; string obj = g_obj_prefix + name; if(ObjectFind(0, obj) < 0) { if(!ObjectCreate(0, obj, OBJ_TEXT, 0, t, price)) { DebugLog("ObjectCreate failed for " + obj); return; } } ObjectMove(0, obj, 0, t, price); ObjectSetString(0, obj, OBJPROP_TEXT, text); ObjectSetInteger(0, obj, OBJPROP_COLOR, clr); ObjectSetInteger(0, obj, OBJPROP_FONTSIZE, 9); } //+------------------------------------------------------------------+ //| Deletes a single named object, if it exists | //+------------------------------------------------------------------+ void DeleteObject(string name) { string obj = g_obj_prefix + name; if(ObjectFind(0, obj) >= 0) ObjectDelete(0, obj); } //+------------------------------------------------------------------+ //| Removes every chart object created by this EA instance | //+------------------------------------------------------------------+ void ClearLabels() { int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, g_obj_prefix) == 0) ObjectDelete(0, name); } }
Head and Shoulders Detection
"CheckHeadAndShoulders()" scans the engine's labeled swing array and verifies all eight conditions. It returns a result struct. It does not check the neckline break; that is handled separately in "OnTick()" once the pattern is confirmed.
//+------------------------------------------------------------------+ //| Evaluates the current H4 swing structure for a Head and Shoulders| //| atr must be computed on InpSwingTF, not the chart timeframe | //+------------------------------------------------------------------+ SPatternResult CheckHeadAndShoulders(double atr) { SPatternResult result; result.valid = false; result.is_inverse = false; int swing_count = g_engine.GetSwingCount(); // Cached once, not re-read every loop iteration if(swing_count < 5) { DebugLog("H&S reject: fewer than 5 confirmed H4 swings."); return result; } //--- Collect five pattern components from the swing array //--- Engine index 0 = most recent, ascending = older SSwingPoint rs; rs.is_high = true; rs.price = 0; rs.bar_index = 0; rs.time = 0; rs.label = ""; SSwingPoint rt; rt.is_high = false; rt.price = 0; rt.bar_index = 0; rt.time = 0; rt.label = ""; SSwingPoint hd; hd.is_high = true; hd.price = 0; hd.bar_index = 0; hd.time = 0; hd.label = ""; SSwingPoint lt; lt.is_high = false; lt.price = 0; lt.bar_index = 0; lt.time = 0; lt.label = ""; SSwingPoint ls; ls.is_high = true; ls.price = 0; ls.bar_index = 0; ls.time = 0; ls.label = ""; int highs_found = 0; int lows_found = 0; for(int i = 0; i < swing_count; i++) { SSwingPoint sp = g_engine.GetSwing(i); if(sp.is_high && highs_found == 0) { rs = sp; highs_found++; } else if(!sp.is_high && highs_found == 1 && lows_found == 0) { rt = sp; lows_found++; } else if(sp.is_high && highs_found == 1 && lows_found == 1) { hd = sp; highs_found++; } else if(!sp.is_high && highs_found == 2 && lows_found == 1) { lt = sp; lows_found++; } else if(sp.is_high && highs_found == 2 && lows_found == 2) { ls = sp; highs_found++; break; } } if(highs_found < 3 || lows_found < 2) { DebugLog("H&S reject: could not collect all five components."); return result; } //--- Condition 1: strict time ordering LS -> LT -> HD -> RT -> RS if(ls.time >= lt.time || lt.time >= hd.time || hd.time >= rt.time || rt.time >= rs.time) { DebugLog("H&S reject: swing time ordering is invalid."); return result; } //--- Condition 2: pattern width, measured as an actual swing-TF bar count via //--- iBarShift() rather than a calendar-time division, which would overstate //--- the true bar count across a weekend or session gap int bar_ls = iBarShift(_Symbol, InpSwingTF, ls.time, false); int bar_rs = iBarShift(_Symbol, InpSwingTF, rs.time, false); int span = bar_ls - bar_rs; if(bar_ls < 0 || bar_rs < 0 || span <= 0 || span > InpMaxPatternBars) { DebugLog(StringFormat("H&S reject: span=%d outside (0, %d].", span, InpMaxPatternBars)); return result; } //--- Condition 3: head must carry HH label — evidence the head exceeded the //--- confirmed high before it. GetTrend() cannot be used as a separate gate //--- here: at the moment a right shoulder is confirmed, the most recent high //--- IS the right shoulder, and TREND_UP would require that same swing to be //--- labeled HH, not LH, contradicting Condition 5 below. if(hd.label != "HH") { DebugLog("H&S reject: head is not labeled HH."); return result; } //--- Condition 4: left trough must carry HL label — the pullback before the //--- head still held above the prior low. Together with Condition 3 this //--- requires the swing sequence immediately preceding the head to still be //--- extending higher; neither label alone proves a full-depth uptrend back //--- through the left shoulder. if(lt.label != "HL") { DebugLog("H&S reject: left trough is not labeled HL."); return result; } //--- Condition 5: right shoulder must carry LH label if(rs.label != "LH") { DebugLog("H&S reject: right shoulder is not labeled LH."); return result; } //--- Condition 6: head must clear both shoulders by minimum ATR distance if((hd.price - ls.price) < atr * InpMinHeadHeight) { DebugLog("H&S reject: head does not clear left shoulder by minimum ATR."); return result; } if((hd.price - rs.price) < atr * InpMinHeadHeight) { DebugLog("H&S reject: head does not clear right shoulder by minimum ATR."); return result; } //--- Condition 7: shoulders must be within symmetry tolerance if(MathAbs(ls.price - rs.price) > atr * InpShoulderSymmetry) { DebugLog("H&S reject: shoulders exceed symmetry tolerance."); return result; } //--- Condition 8: pattern height must clear the minimum ATR distance double neckline = MathMin(lt.price, rt.price); // The LOWER trough — stricter, and breaking it also breaks the higher one double height = hd.price - neckline; if(height < atr * InpMinHeadHeight) { DebugLog("H&S reject: pattern height below minimum."); return result; } result.valid = true; result.is_inverse = false; result.left_shoulder = ls.price; result.head = hd.price; result.right_shoulder = rs.price; result.neckline = neckline; result.target = neckline - height; result.stop = hd.price + atr * InpSLBufferATR; result.ls_time = ls.time; result.head_time = hd.time; result.rs_time = rs.time; result.neck_time = (lt.price < rt.price) ? lt.time : rt.time; Print(StringFormat( "HeadShouldersEA: H&S found | LS:%.5f | HD:%.5f | RS:%.5f | Neck:%.5f | Target:%.5f", result.left_shoulder, result.head, result.right_shoulder, result.neckline, result.target)); return result; }Inverse Head and Shoulders Detection
//+------------------------------------------------------------------+ //| Evaluates the current H4 swing structure for an Inverse H&S | //+------------------------------------------------------------------+ SPatternResult CheckInverseHS(double atr) { SPatternResult result; result.valid = false; result.is_inverse = true; int swing_count = g_engine.GetSwingCount(); if(swing_count < 5) { DebugLog("IH&S reject: fewer than 5 confirmed H4 swings."); return result; } SSwingPoint rs; rs.is_high = false; rs.price = 0; rs.bar_index = 0; rs.time = 0; rs.label = ""; SSwingPoint rt; rt.is_high = true; rt.price = 0; rt.bar_index = 0; rt.time = 0; rt.label = ""; SSwingPoint hd; hd.is_high = false; hd.price = 0; hd.bar_index = 0; hd.time = 0; hd.label = ""; SSwingPoint lt; lt.is_high = true; lt.price = 0; lt.bar_index = 0; lt.time = 0; lt.label = ""; SSwingPoint ls; ls.is_high = false; ls.price = 0; ls.bar_index = 0; ls.time = 0; ls.label = ""; int lows_found = 0; int highs_found = 0; for(int i = 0; i < swing_count; i++) { SSwingPoint sp = g_engine.GetSwing(i); if(!sp.is_high && lows_found == 0) { rs = sp; lows_found++; } else if(sp.is_high && lows_found == 1 && highs_found == 0) { rt = sp; highs_found++; } else if(!sp.is_high && lows_found == 1 && highs_found == 1) { hd = sp; lows_found++; } else if(sp.is_high && lows_found == 2 && highs_found == 1) { lt = sp; highs_found++; } else if(!sp.is_high && lows_found == 2 && highs_found == 2) { ls = sp; lows_found++; break; } } if(lows_found < 3 || highs_found < 2) { DebugLog("IH&S reject: could not collect all five components."); return result; } //--- Condition 1: strict time ordering LS -> LT -> HD -> RT -> RS if(ls.time >= lt.time || lt.time >= hd.time || hd.time >= rt.time || rt.time >= rs.time) { DebugLog("IH&S reject: swing time ordering is invalid."); return result; } //--- Condition 2: pattern width, measured as an actual swing-TF bar count via //--- iBarShift() rather than a calendar-time division int bar_ls = iBarShift(_Symbol, InpSwingTF, ls.time, false); int bar_rs = iBarShift(_Symbol, InpSwingTF, rs.time, false); int span = bar_ls - bar_rs; if(bar_ls < 0 || bar_rs < 0 || span <= 0 || span > InpMaxPatternBars) { DebugLog(StringFormat("IH&S reject: span=%d outside (0, %d].", span, InpMaxPatternBars)); return result; } //--- Condition 3: head must carry LL label — evidence the head undercut the //--- confirmed low before it. GetTrend() cannot be used as a separate gate //--- here for the same reason it cannot in CheckHeadAndShoulders(): TREND_DOWN //--- would require that same swing to be labeled LL, not HL, contradicting //--- Condition 5 below. if(hd.label != "LL") { DebugLog("IH&S reject: head is not labeled LL."); return result; } //--- Condition 4: left peak must carry LH label — the rally before the head //--- still failed below the prior high. Together with Condition 3 this //--- requires the swing sequence immediately preceding the head to still be //--- extending lower; neither label alone proves a full-depth downtrend back //--- through the left shoulder. if(lt.label != "LH") { DebugLog("IH&S reject: left peak is not labeled LH."); return result; } //--- Condition 5: right shoulder must carry HL label if(rs.label != "HL") { DebugLog("IH&S reject: right shoulder is not labeled HL."); return result; } //--- Condition 6: shoulders must clear the head by minimum ATR distance if((ls.price - hd.price) < atr * InpMinHeadHeight) { DebugLog("IH&S reject: left shoulder does not clear head by minimum ATR."); return result; } if((rs.price - hd.price) < atr * InpMinHeadHeight) { DebugLog("IH&S reject: right shoulder does not clear head by minimum ATR."); return result; } //--- Condition 7: shoulders must be within symmetry tolerance if(MathAbs(ls.price - rs.price) > atr * InpShoulderSymmetry) { DebugLog("IH&S reject: shoulders exceed symmetry tolerance."); return result; } //--- Condition 8: pattern height must clear the minimum ATR distance double neckline = MathMax(lt.price, rt.price); // The HIGHER peak — stricter, and breaking it also breaks the lower one double height = neckline - hd.price; if(height < atr * InpMinHeadHeight) { DebugLog("IH&S reject: pattern height below minimum."); return result; } result.valid = true; result.is_inverse = true; result.left_shoulder = ls.price; result.head = hd.price; result.right_shoulder = rs.price; result.neckline = neckline; result.target = neckline + height; result.stop = hd.price - atr * InpSLBufferATR; result.ls_time = ls.time; result.head_time = hd.time; result.rs_time = rs.time; result.neck_time = (lt.price > rt.price) ? lt.time : rt.time; Print(StringFormat( "HeadShouldersEA: IH&S found | LS:%.5f | HD:%.5f | RS:%.5f | Neck:%.5f | Target:%.5f", result.left_shoulder, result.head, result.right_shoulder, result.neckline, result.target)); return result; }
Trade Entry Functions
Both functions follow the same sequence. Each checks "HasOpenPosition()" a second time immediately before sending an order, then requires that price is still on the breakout side of the neckline, not just within the slip tolerance. The stop is finalized against the broker’s minimum stop distance before anything that depends on it is computed, so the 2R fallback target and the lot size both use the stop that will actually be sent. Margin is validated last. The freeze level is deliberately not used: it governs modifying or closing an already-open position, not placing SL/TP on a new one, so applying it to initial placement would tighten the trade’s parameters for no reason tied to what the value actually means.//+------------------------------------------------------------------+ //| Opens a short trade on a confirmed Head and Shoulders break | //+------------------------------------------------------------------+ void TryEntryShort(const SPatternResult &pat, double atr) { if(HasOpenPosition(_Symbol, InpMagicNumber)) return; // Belt-and-braces position check double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(bid >= pat.neckline) { DebugLog("H&S entry skipped — price has moved back above the neckline since the close."); return; } if(bid < pat.neckline - atr * InpMaxSlipATR) { DebugLog("H&S entry skipped — price already too far past the neckline."); return; } //--- Finalize SL against the broker's minimum stop distance FIRST, so the lot //--- size and any 2R fallback target are both computed from the SL that will //--- actually be sent, not from a pre-adjustment value that understates risk. //--- Only the stop level applies here — the freeze level governs modifying or //--- closing an existing position, not placing SL/TP on a new one. double sl = NormalizeDouble(pat.stop, _Digits); long stops_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); double min_dist = stops_level * _Point; if(sl - bid < min_dist) sl = NormalizeDouble(bid + min_dist + _Point, _Digits); double tp = NormalizeDouble(InpUseMeasuredMove ? pat.target : bid - (sl - bid) * 2.0, _Digits); if(bid - tp < min_dist) tp = NormalizeDouble(bid - min_dist - _Point, _Digits); double sl_points = (sl - bid) / _Point; double lots = CalcLots(sl_points); if(lots <= 0) { DebugLog("H&S entry skipped — risk-appropriate lot size is below the broker minimum."); return; } if(!HasSufficientMargin(ORDER_TYPE_SELL, lots, bid)) { DebugLog("H&S entry skipped — insufficient free margin."); return; } if(g_trade.Sell(lots, _Symbol, 0.0, sl, tp, "H&S")) { DrawHLine("NECKLINE", pat.neckline, clrOrange, STYLE_DASH); DrawHLine("TARGET", tp, clrGold, STYLE_DOT); DrawHLine("STOP", sl, clrCrimson, STYLE_DOT); DrawLabel("HEAD", pat.head_time, pat.head + _Point * 100, "HD", clrCrimson); DrawLabel("LS", pat.ls_time, pat.left_shoulder + _Point * 50, "LS", clrOrangeRed); DrawLabel("RS", pat.rs_time, pat.right_shoulder + _Point * 50, "RS", clrOrangeRed); DrawLabel("ENTRY", iTime(_Symbol, PERIOD_CURRENT, 1), iHigh(_Symbol, PERIOD_CURRENT, 1) + _Point * 50, "H&S", clrCrimson); Print(StringFormat("HeadShouldersEA: SHORT | Lots:%.2f | Bid:%.5f | SL:%.5f | TP:%.5f", lots, bid, sl, tp)); g_last_traded_hs_rs = pat.rs_time; // This exact pattern cannot re-trade GlobalVariableSet(g_gv_hs_name, (double)pat.rs_time); // ...even across a restart g_state = HS_STATE_IN_TRADE; } else Print("HeadShouldersEA: Sell failed | Retcode:", g_trade.ResultRetcode(), " | Error:", GetLastError()); } //+------------------------------------------------------------------+ //| Opens a long trade on a confirmed Inverse H&S break | //+------------------------------------------------------------------+ void TryEntryLong(const SPatternResult &pat, double atr) { if(HasOpenPosition(_Symbol, InpMagicNumber)) return; double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(ask <= pat.neckline) { DebugLog("IH&S entry skipped — price has moved back below the neckline since the close."); return; } if(ask > pat.neckline + atr * InpMaxSlipATR) { DebugLog("IH&S entry skipped — price already too far past the neckline."); return; } //--- Finalize SL against the broker's minimum stop distance FIRST — see the //--- comment in TryEntryShort() for why, and why only the stop level applies. double sl = NormalizeDouble(pat.stop, _Digits); long stops_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); double min_dist = stops_level * _Point; if(ask - sl < min_dist) sl = NormalizeDouble(ask - min_dist - _Point, _Digits); double tp = NormalizeDouble(InpUseMeasuredMove ? pat.target : ask + (ask - sl) * 2.0, _Digits); if(tp - ask < min_dist) tp = NormalizeDouble(ask + min_dist + _Point, _Digits); double sl_points = (ask - sl) / _Point; double lots = CalcLots(sl_points); if(lots <= 0) { DebugLog("IH&S entry skipped — risk-appropriate lot size is below the broker minimum."); return; } if(!HasSufficientMargin(ORDER_TYPE_BUY, lots, ask)) { DebugLog("IH&S entry skipped — insufficient free margin."); return; } if(g_trade.Buy(lots, _Symbol, 0.0, sl, tp, "IH&S")) { DrawHLine("NECKLINE", pat.neckline, clrOrange, STYLE_DASH); DrawHLine("TARGET", tp, clrGold, STYLE_DOT); DrawHLine("STOP", sl, clrDodgerBlue, STYLE_DOT); DrawLabel("HEAD", pat.head_time, pat.head - _Point * 100, "HD", clrDodgerBlue); DrawLabel("LS", pat.ls_time, pat.left_shoulder - _Point * 50, "LS", clrDodgerBlue); DrawLabel("RS", pat.rs_time, pat.right_shoulder - _Point * 50, "RS", clrDodgerBlue); DrawLabel("ENTRY", iTime(_Symbol, PERIOD_CURRENT, 1), iLow(_Symbol, PERIOD_CURRENT, 1) - _Point * 50, "IH&S", clrDodgerBlue); Print(StringFormat("HeadShouldersEA: LONG | Lots:%.2f | Ask:%.5f | SL:%.5f | TP:%.5f", lots, ask, sl, tp)); g_last_traded_ihs_rs = pat.rs_time; GlobalVariableSet(g_gv_ihs_name, (double)pat.rs_time); // ...even across a restart g_state = HS_STATE_IN_TRADE; } else Print("HeadShouldersEA: Buy failed | Retcode:", g_trade.ResultRetcode(), " | Error:", GetLastError()); }
OnInit, OnDeinit, and OnTick
Input parameters are validated up front, the object-name prefix is built from the symbol and magic number, and the starting state is decided by an actual position query.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(InpSwingStrength <= 0 || InpSwingLookback <= 0 || InpATRPeriod <= 0 || InpMinHeadHeight <= 0 || InpShoulderSymmetry <= 0 || InpRiskPercent <= 0 || InpSLBufferATR < 0 || InpMaxPatternBars <= 0 || InpMaxBreakDelayBars <= 0 || InpMaxSlipATR < 0) { Print("HeadShouldersEA: one or more input parameters is out of range."); return INIT_PARAMETERS_INCORRECT; } g_obj_prefix = StringFormat("HS_%s_%d_", _Symbol, InpMagicNumber); g_gv_hs_name = g_obj_prefix + "LastHS"; g_gv_ihs_name = g_obj_prefix + "LastIHS"; if(!g_engine.Init(InpSwingStrength, InpSwingLookback, InpSwingTF)) { Print("HeadShouldersEA: swing engine initialization failed."); return INIT_FAILED; } g_atr_handle = iATR(_Symbol, InpSwingTF, InpATRPeriod); if(g_atr_handle == INVALID_HANDLE) { Print("HeadShouldersEA: ATR handle creation failed."); return INIT_FAILED; } g_trade.SetExpertMagicNumber(InpMagicNumber); g_trade.SetDeviationInPoints(InpSlippage); //--- Decide the starting state from the actual position list, not an assumption g_state = HasOpenPosition(_Symbol, InpMagicNumber) ? HS_STATE_IN_TRADE : HS_STATE_SCANNING; g_pattern_age_bars = 0; g_last_bar = 0; //--- Anti-repeat timestamps are read from persisted GlobalVariables, not reset //--- to zero, so a trade cannot repeat even after the EA restarts g_last_traded_hs_rs = GlobalVariableCheck(g_gv_hs_name) ? (datetime)GlobalVariableGet(g_gv_hs_name) : 0; g_last_traded_ihs_rs = GlobalVariableCheck(g_gv_ihs_name) ? (datetime)GlobalVariableGet(g_gv_ihs_name) : 0; Print(StringFormat( "HeadShouldersEA initialized | Symbol:%s | ChartTF:%s | SwingTF:%s | Magic:%d | StartState:%s", _Symbol, EnumToString(PERIOD_CURRENT), EnumToString(InpSwingTF), InpMagicNumber, (g_state == HS_STATE_IN_TRADE ? "IN_TRADE (recovered)" : "SCANNING"))); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(g_atr_handle != INVALID_HANDLE) IndicatorRelease(g_atr_handle); ClearLabels(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { datetime current_bar = iTime(_Symbol, PERIOD_CURRENT, 0); if(current_bar == g_last_bar) return; // One run per chart bar g_last_bar = current_bar; if(Bars(_Symbol, PERIOD_CURRENT) < 5) { DebugLog("Chart history not ready yet — skipping this bar."); return; } g_engine.Update(); // Self-gated on a new swing-TF bar double atr_buf[]; ArraySetAsSeries(atr_buf, true); if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) < 1) { DebugLog("ATR CopyBuffer failed — skipping this bar."); return; } double atr = atr_buf[0]; if(atr <= 0 || atr == EMPTY_VALUE) { DebugLog("ATR invalid on this bar — skipping."); return; } double close1 = iClose(_Symbol, PERIOD_CURRENT, 1); switch(g_state) { case HS_STATE_IN_TRADE: { if(!HasOpenPosition(_Symbol, InpMagicNumber)) { Print("HeadShouldersEA: position closed — resuming scan."); g_state = HS_STATE_SCANNING; ClearLabels(); } break; } case HS_STATE_PATTERN_FORMED: { g_pattern_age_bars++; //--- Revalidate: does this exact pattern still hold given the latest swing data? SPatternResult recheck = g_active_pattern.is_inverse ? CheckInverseHS(atr) : CheckHeadAndShoulders(atr); bool same_pattern = recheck.valid && recheck.is_inverse == g_active_pattern.is_inverse && recheck.rs_time == g_active_pattern.rs_time; if(!same_pattern) { DebugLog("Locked pattern no longer valid against current swing data — discarding."); DeleteObject("NECKLINE_WATCH"); g_state = HS_STATE_SCANNING; break; } if(g_pattern_age_bars > InpMaxBreakDelayBars) { DebugLog("Locked pattern expired before the neckline broke — discarding."); DeleteObject("NECKLINE_WATCH"); g_state = HS_STATE_SCANNING; break; } DrawHLine("NECKLINE_WATCH", g_active_pattern.neckline, clrOrange, STYLE_DASH); bool broken = g_active_pattern.is_inverse ? (close1 > g_active_pattern.neckline) : (close1 < g_active_pattern.neckline); if(broken) { Print(StringFormat("HeadShouldersEA: %s neckline broken at %.5f", g_active_pattern.is_inverse ? "IH&S" : "H&S", close1)); DeleteObject("NECKLINE_WATCH"); if(g_active_pattern.is_inverse) TryEntryLong(g_active_pattern, atr); else TryEntryShort(g_active_pattern, atr); } break; } case HS_STATE_SCANNING: { if(HasOpenPosition(_Symbol, InpMagicNumber)) // Safety net — should be rare { g_state = HS_STATE_IN_TRADE; break; } SPatternResult hs = CheckHeadAndShoulders(atr); if(hs.valid && hs.rs_time != g_last_traded_hs_rs) { g_active_pattern = hs; g_pattern_age_bars = 0; g_state = HS_STATE_PATTERN_FORMED; break; // H&S takes priority when both qualify } SPatternResult ihs = CheckInverseHS(atr); if(ihs.valid && ihs.rs_time != g_last_traded_ihs_rs) { g_active_pattern = ihs; g_pattern_age_bars = 0; g_state = HS_STATE_PATTERN_FORMED; } break; } } ChartRedraw(0); // One redraw per bar, not per object } //+------------------------------------------------------------------+
Because "HS_STATE_SCANNING" only locks onto one pattern at a time and moves straight to "HS_STATE_PATTERN_FORMED," there is never a moment where a Head and Shoulders watch line and an Inverse Head and Shoulders watch line exist on the chart simultaneously.
The State Machine—Why It Matters
Checking "close1 < hs.neckline" on the same bar that "CheckHeadAndShoulders()" confirms a pattern does not work, and it is worth understanding why before looking at the state machine that avoids the problem.
The pattern is only confirmed once the right shoulder has fully formed. At that moment, the neckline is a level that existed before the right shoulder formed. The current close, the bar that confirmed the right shoulder, is seldom already below the neckline. It is still near the right shoulder's level, above the neckline. The break, if it comes, arrives some number of bars later. A detector with no memory of the pattern it already found has no way to keep watching for that break; it can only ask, "Is there a valid pattern on this exact bar?" and re-running the full eight-condition check from scratch every bar answers a different question than, "Has the neckline I already confirmed now been broken?"
The state machine solves this by storing the confirmed pattern in "g_active_pattern" and watching for the neckline break across subsequent bars, however many bars it takes. It also revalidates the pattern each bar, but not by independently re-checking whether the specific locked combination of swings still satisfies all eight conditions in isolation. Revalidation re-runs the same detector that found the pattern in the first place and compares the right-shoulder timestamp of whatever it returns now against the one that was locked in. If a newer swing has displaced the right shoulder, or the detector no longer returns a valid result at all, the lock is dropped and scanning resumes—even in the rare case where the originally locked combination would still individually pass every condition on its own. This trades an exhaustive search of pattern history for a simple, cheap re-derivation; the known limitations section below states this trade-off explicitly.
Two further safeguards sit alongside revalidation. "InpMaxBreakDelayBars" gives a locked pattern a finite lifetime, so a setup that never breaks its neckline is eventually discarded rather than watched forever. And "g_last_traded_hs_rs"/"g_last_traded_ihs_rs" record the right-shoulder timestamp of the last pattern actually traded of each type, so if a position closes while the underlying H4 structure is unchanged, the same pattern cannot immediately re-qualify and re-enter. These two in-memory variables are backed by MetaTrader global variables ("GlobalVariableSet()"/"GlobalVariableGet()"), so the same protection holds across an EA restart, not only within a single run—though MetaTrader itself will purge a global variable that goes unused for four weeks, which is the outer bound on how long this protection lasts without a fresh trade to refresh it.
What to expect:
Head and Shoulders patterns that satisfy all eight conditions on H4 are rare, and exactly how rare depends on the test period, broker data, spread, and session coverage used—run the configuration above yourself and treat the Journal's own count as the figure to go by, rather than a number quoted here without a specific, reproducible test behind it. With "InpDebugLog" enabled, the Journal logs the reason behind every rejected pattern-structure check: insufficient swings, a missing label, a width violation, insufficient head clearance, an expired pattern, or a discarded revalidation each print a specific reason. A few earlier gates in "OnTick()"—insufficient chart history, a failed ATR read—exit before pattern evaluation even begins and log their own, separate reason rather than going through this per-condition path. This makes it possible to tell the difference between "the market has not produced a qualifying pattern" and "a pattern formed but something specific about the entry conditions filtered it out."
If the journal shows patterns found but no trades, check for "entry skipped" messages: a lot size below the broker minimum, insufficient margin, or price already too far past the neckline are all deliberate rejections, not bugs. If patterns are found but expire before entering, raise "InpMaxBreakDelayBars." If no patterns are found at all, reduce "InpSwingStrength" to 2 or increase "InpSwingLookback" to 400.
Known Limitations
- Rare signals by design. Rare signals by design. All eight conditions must pass simultaneously on the swing-timeframe structure. With the EA attached to a EURUSD H1 chart (chart timeframe H1) and the default “InpSwingTF” of H4 (swing timeframe), genuine signals are infrequent. This is correct behavior, not a code defect.
- Horizontal neckline. The neckline is set to a single fixed price level rather than a slanted line connecting the two troughs at different angles. On patterns with significantly different trough depths, the horizontal neckline approximation means the entry trigger fires at the lower trough level rather than along a slanted line. This is an acceptable simplification for an automated system.
- Stop above the head. The stop loss is placed above the head plus a buffer. On patterns with a tall head, this produces a large stop distance and a small lot size for a given risk percentage.
- Single-candidate scanning. The detector considers only the five most recent qualifying swings in the correct alternating sequence; it does not search every possible combination of swing points for the best-fitting pattern. A wider search across multiple candidate groupings could occasionally find a more structurally significant pattern than the most recent one, at the cost of considerably more complexity.
- Revalidation re-derives rather than re-checks the original combination. Each bar, revalidation re-runs the same detector and compares the right-shoulder timestamp it returns against the locked pattern's identity. It does not independently verify that the specific swing combination originally locked in still satisfies all eight conditions in isolation. If a newer swing changes what the detector currently returns, the lock is dropped even in the rare case where the original combination would still individually pass every condition.
- Swing confirmation lag can precede the lock. A swing is only confirmed after "InpSwingStrength" bars have closed to its right, so the right shoulder, and with it the pattern lock, is necessarily reported several H4 bars after the market actually produced it. In a fast-moving market, the neckline may already have broken by the time the EA locks the pattern in. The state machine does not detect a missed break directly; what actually protects against entering a stale setup is the slip filter, "InpMaxSlipATR," in the entry functions, which will reject the trade as too far past the neckline if the move was large enough to matter.
- Reattach recovery is partial. "HasOpenPosition()" correctly restores whether the EA is in a trade after a restart, and the last-traded right-shoulder timestamps are restored from persisted global variables, so a completed trade cannot be repeated. A pattern that was only locked in and being watched, not yet traded, is not restored: "g_active_pattern" starts empty after a restart, and scanning resumes from "HS_STATE_SCANNING" regardless of what was being tracked before. The pattern is simply rediscovered on a later bar if it is still valid, rather than picked back up where it left off.
- Symmetric shoulder tolerance. "InpShoulderSymmetry" treats a right shoulder that is higher than the left by the tolerance amount the same as one that is lower by the same amount. Some traders prefer an asymmetric rule that penalizes a right shoulder approaching the head more heavily than one that undershoots the left shoulder. This implementation does not distinguish between the two cases.
- Spread is not separately modeled. Position sizing is computed from the structural stop distance and does not separately model spread cost against the measured-move target. On tightly spread majors with a chart timeframe of H1 or above, this is a small effect; on wider-spread symbols or shorter chart timeframes, actual realized reward-to-risk will run partially below the structural figure the pattern implies.
- Alternating swing requirement. The collector loop requires strict alternating high-low-high-low-high sequencing. In markets where two consecutive highs of similar height form without a confirmed intervening low, the loop will not complete the required five components, and the pattern will not be detected. This is correct structural behavior; an alternating sequence is part of the pattern's definition.
- One position at a time. "HS_STATE_IN_TRADE" blocks all scanning until the open position closes. A second pattern that becomes eligible while a position is already open is never locked in; it must re-qualify from scratch once scanning resumes.
Conclusion
The Head and Shoulders is more structurally demanding than the double top in every dimension: more peaks, stricter sequencing, a specific relationship between the middle peak and both flanking peaks, and a shoulder symmetry requirement. These demands are also why the pattern carries more weight when it forms: the structural narrative, a failed push to new highs followed by a lower high followed by a neckline break, is a complete description of distribution.
The implementation in this article enforces all of those demands mechanically and treats a confirmed pattern the same way Part 2 treated the double top and double bottom: as a single event to be traded at most once. The head's "HH" label and the right shoulder's "LH" label together confirm the structural sequence, prior uptrend and failed continuation alike, without relying on a real-time trend read that the pattern's own completion would contradict. The eight-condition evaluation verifies every remaining geometric requirement. The three-state lifecycle separates pattern formation from neckline break, tracks a locked pattern across as many bars as it needs, discards it if the underlying structure changes, expires it if it waits too long, and refuses to trade the same right shoulder twice—a refusal that now survives an EA restart, since the last traded right shoulder is persisted outside the EA's own memory.
File Reference
- Save "SwingEngine.mqh" from Part 1 to "MQL5\Include\ChartPatterns\SwingEngine.mqh."
- Save "HeadShouldersEA.mq5" to "MQL5\Experts\HeadShoulder\HeadShouldersEA.mq5."
| # | Name | Type | Description |
|---|---|---|---|
| 1 | HeadShouldersEA.mq5 | Demo EA | Head and Shoulders demonstration EA. |
| 2 | SwingEngine.mqh | Include File | Swing detection engine from Part 1. |
| 3 | MQL5.zip | Zip Archive | Archive with all files that can be unpacked into the terminal installation directory, and all files will be located in the required places. |
Compile in MetaEditor. No additional dependencies are required beyond the MQL5 standard library. Attach the EA to an H1 chart of EURUSD or GBPUSD (chart timeframe H1) with “InpSwingTF” set to H4 (swing timeframe).
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.
Neural Networks in Trading: Iterative Forecast Refinement Models (RAFT)
Buzzard Optimization Algorithm (BUZOA)
Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 6): Curves and Adaptive Flattening
Building A Candlestick Trend Constraint Model (Part 11): Revealing Lower-Timeframe Triggers Within Higher-Timeframe Candle Relationships
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use