preview
Automating Chart Patterns in MQL5 (Part 1): The Multi-Timeframe Swing Structure Engine

Automating Chart Patterns in MQL5 (Part 1): The Multi-Timeframe Swing Structure Engine

MetaTrader 5Examples |
622 0
Tola Moses Hector
Tola Moses Hector

Introduction

Most chart pattern implementations share the same limitation. They detect the shape. They do not detect the context. A head and shoulders pattern found at the top of a confirmed uptrend is a high-probability reversal signal. The same shape found in the middle of a sideways range is noise. A double top after three or more consecutive higher highs is a structural warning. A double top after a single rally leg that barely qualifies as a trend is questionable at best. Every experienced technician knows this distinction. Almost no automated implementation enforces it.

There is a second, related problem that is just as common and much less discussed: most swing-based EAs calculate their structure on the same timeframe they trade on. An M15 scalping EA ends up classifying "trend" from M15 swings, which are noisy by nature and reclassify direction dozens of times a day. A trader working from a chart would never do this—they would glance at H4 or daily to establish bias, then drop to a lower timeframe to time the entry. This article's engine enforces that same discipline in code: market structure is always read from H4, independent of the timeframe the EA or indicator is attached to.

This series—"Automating Chart Patterns in MQL5"—builds a complete pattern detection system where every pattern is validated against the market context that gives it meaning. A double top is not accepted unless the swing engine confirms a prior uptrend. A flag is not accepted unless the prior impulse exceeds a minimum strength threshold. A head and shoulders is not accepted unless the swing sequence matches the structural requirements of a distribution pattern. All of that context comes from a higher timeframe than the one being traded.

This article lays the foundation: CSwingEngine, a self-contained, reusable class that any EA or indicator can include with a single line. The engine identifies swing points on H4 using a configurable lookback and strength, labels each swing HH, LH, HL, or LL as it forms, tracks the labeled sequence to determine market structure, and exposes a clean public interface for querying the current trend and retrieving the swing array—all while drawing correctly on whatever lower timeframe chart the calling code is attached to.

The central question CSwingEngine is designed to answer is this:

What is the higher-timeframe market structure, and what does the labeled sequence of swing points tell us about where a valid pattern can form on the timeframe I actually trade?

Before showing what the engine does, this article explains why naive same-timeframe swing detection fails and what goes wrong when pattern detectors skip both the context check and the timeframe check. Every design decision in CSwingEngine exists because of a specific failure mode in simpler approaches.

We will cover the following topics:

  1. Why Pattern Detection Without Context Fails
  2. What Makes a Swing Point Significant
  3. How Swing Sequences Define Market Structure
  4. Architecture—Calculating on H4, Drawing on the Chart Timeframe
  5. Implementation in MQL5
  6. How Subsequent Articles Use the Engine
  7. Backtesting and Validation
  8. Known Limitations
  9. Conclusion


Why Pattern Detection Without Context Fails

Why context matters

Fig. 1. Why context matters.

Consider a typical naive double top detector. It scans recent bars for two highs at approximately the same price level, separated by a trough. When found, it fires a sell signal. This approach has one fundamental problem: it has no idea what happened before those two highs.

If the market was in a sustained uptrend for 40 bars before the two highs formed, the double top represents a genuine exhaustion of buying pressure at a resistance level. The pattern has structural meaning because it forms at the end of a directional move where buyers are running out of momentum.

If the market was ranging sideways for 60 bars and the two highs formed as part of normal oscillation within the range, the double top is not a pattern at all. It is just two bars that happened to reach the same price within a consolidation. Trading it as a reversal signal is a category error.

The naive detector fires in both cases because it never checks the prior trend. Even when it does, checking that trend on the same noisy trading timeframe still causes regular misclassification: minor intraday swings often break the “two consecutive higher highs” test even within a clean higher-timeframe uptrend.

The same problem applies to every continuation pattern. A flag is supposed to form after a strong impulsive move—the flagpole. A flag that forms in a choppy, low-ATR environment with no prior impulse is not a flag. It is random consolidation. A pattern detector that looks only at the shape of the consolidation—without verifying the impulse that preceded it on a timeframe stable enough to show it clearly—will produce false signals continuously in ranging markets.

The state machine architecture used throughout this series solves the first problem by enforcing prerequisite conditions before any pattern evaluation begins. The engine must confirm a trend before a reversal pattern can be evaluated. The engine must confirm an impulse before a continuation pattern can be evaluated.

This article's engine solves the second problem by reading the prerequisite trend from H4, not from the chart's own bars. That keeps the trend condition stable while the pattern shape is evaluated on the trading timeframe.


What Makes a Swing Point Significant

A swing high is a bar whose high is strictly greater than the high of every bar in a symmetric window of N bars to its left and N bars to its right. A swing low is a bar whose low is strictly lower than every bar in the same window. This is the standard definition used in technical analysis, and it is the exact test this engine uses—the same strength-window comparison used for swing detection in Smart Money Concept tooling, adapted here for a dedicated, lightweight swing-only engine.

The parameter N—the "swing strength"—controls the minimum significance of detected swings. A small N produces more swings that are closer together, capturing minor oscillations. A large N produces fewer swings that are farther apart, capturing only the major structural highs and lows.

Two things are different from a same-timeframe swing detector:

  1. N is applied to H4 bars, not to the chart's own bars. A value of 3 on H4 already spans roughly half a trading day on either side of the candidate swing—comparable in significance to a much larger N applied to M15 or H1. This is deliberate. The swing strength parameter should describe how significant a structural point is, and significance is a higher-timeframe property.
  2. Every confirmed swing is labeled the moment it is classified, using the same convention Smart Money Concept indicators use: HH (higher high) or LH (lower high) for swing highs, HL (higher low) or LL (lower low) for swing lows. A swing high is labeled HH if it is higher than the previous confirmed swing high, LH otherwise. A swing low is labeled HL if it is higher than the previous confirmed swing low, LL otherwise. This labeling is not cosmetic—it is what the trend classification in Section 3 reads directly, and it is what makes the chart readable at a glance instead of requiring the viewer to mentally compare price levels.

A swing point can only be confirmed after N bars have passed to its right—on H4, not on the chart timeframe.


How Swing Sequences Define Market Structure

Swing labels and market structure

Fig. 2. Swing labels and market structure.

Once H4 swing highs and swing lows are labeled, market structure follows directly from the two most recent labels, with no separate comparison logic required.

An uptrend is confirmed when the most recently labeled swing high is HH and the most recently labeled swing low is HL. Read literally: the last leg up made a new high, and the last pullback held above the previous low. This is the classic Dow Theory definition of an uptrend, now expressed as a label match rather than a raw price comparison.

A downtrend is the mirror: the most recent swing high is LH, and the most recent swing low is LL.

A ranging market is anything else—an HH paired with an LL, an LH paired with an HL, or insufficient swing history. Mixed labels mean the market made a new extreme in one direction while failing to do so in the other, which is precisely the signature of a transition or a range.

This classification drives everything in the subsequent articles, and it is always read from H4. When Part 2 evaluates a Double Top on, say, the H1 chart, the first check is still whether "CSwingEngine" reports "TREND_UP"—but that answer reflects H4 structure, not H1 noise. If it does not report TREND_UP, the evaluation stops immediately. The shape of the potential pattern, which is still measured using H1 (or whatever chart timeframe the EA runs on) price action, is never examined until the H4 context supports it.


Architecture—Calculating on H4, Drawing on the Chart Timeframe

"CSwingEngine" is distributed across two files.

  1. "SwingEngine.mqh" is the self-contained engine class. It detects swing points on H4, labels them, classifies market structure, and exposes the swing array and trend state through a clean public interface. It knows nothing about specific chart patterns, and its analysis timeframe is independent of the chart it is attached to.
  2. "SwingDemo.mq5" is a demonstration indicator. It attaches "CSwingEngine" to any chart—typically the lower timeframe you intend to trade, such as M15 or H1—and draws every detected H4 swing high and swing low with its HH/LH/HL/LL label, using arrow markers and label colors in the Smart Money Concept style: bright cyan for HH, dim blue for LH, bright green for HL, and dim green for LL. The current trend classification appears as a label in the top-left corner of the chart.

The key architectural point is what does not need to happen: no coordinate translation between timeframes. Every swing point stored by the engine carries the datetime of the H4 bar it was found on. MetaTrader 5 positions chart objects (arrows, labels, trend lines) by the datetime value on the horizontal axis, regardless of the chart period. An H4 swing high with a time of, say, 08:00 on a given day will render at exactly the right horizontal position on an M15 chart, sitting above the cluster of M15 candles that constitute that H4 bar, with no extra code required. This is a native property of how MetaTrader 5 charts work, not something the engine has to implement—but it is worth stating explicitly because it is the reason this architecture is simple rather than fragile.

Swing engine architecture

Fig. 3. Swing engine architecture.

This layering has a practical consequence. Every subsequent article in this series includes "SwingEngine.mqh" and calls the engine to get the current H4 trend and swing array, then evaluates pattern shape against the chart's own lower-timeframe price action for entry timing. None of them reimplement swing detection, and none of them need to reimplement multi-timeframe drawing. The engine is written once, tested once, and serves the entire series.


Implementation in MQL5

The engine is built section by section, explaining each component before showing the code.

The Swing Point Structure and Trend Enumeration

Each detected swing point is stored as a simple structure, now carrying its HH/LH/HL/LL label alongside the original fields. The enumeration defines the three possible market structure states.

Save to "MQL5\Include\ChartPatterns\SwingEngine.mqh."

//+------------------------------------------------------------------+
//|                                                SwingEngine.mqh   |
//|   Multi-timeframe swing detection and trend classification       |
//|   Swings are calculated on a higher timeframe (default H4) and   |
//|   drawn correctly on whatever timeframe the chart is showing.    |
//+------------------------------------------------------------------+
#ifndef SWINGENGINE_MQH
#define SWINGENGINE_MQH

//+------------------------------------------------------------------+
//| Market structure classification                                  |
//+------------------------------------------------------------------+
enum ENUM_SWING_TREND
  {
   TREND_UP,    // Last confirmed swing high is HH and swing low is HL
   TREND_DOWN,  // Last confirmed swing high is LH and swing low is LL
   TREND_RANGE  // Mixed labels or insufficient swing history
  };

//+------------------------------------------------------------------+
//| One detected swing point                                         |
//+------------------------------------------------------------------+
struct SSwingPoint
  {
   bool              is_high;   // true = swing high, false = swing low
   double            price;     // Price of the swing (H4 high or low)
   datetime          time;      // Open time of the H4 swing bar
   int               bar_index; // H4 bars ago at time of detection
   string            label;     // "HH", "LH", "HL", or "LL"
  };

The "time" field is the field the drawing layer relies on. Because it stores the actual H4 bar open time—not a bar index relative to the chart—any object anchored to it lands correctly on any chart timeframe. The "bar_index" field is expressed in H4 bars, not chart bars; this matters if a pattern detector in a later article uses it to measure the distance between two swings, and it is called out again in Section 6.

Infrastructure Helper Functions

"GetPipValue()" and "PipSize()" are the same instrument-agnostic pip helpers used throughout this series.

//+------------------------------------------------------------------+
//| Monetary value of one pip for a given symbol and lot size        |
//+------------------------------------------------------------------+
double GetPipValue(const string symbol, double lots)
  {
   double tick_val  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);   // Tick value
   double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);    // Tick size
   int    digits    = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);       // Symbol digits
   double point     = SymbolInfoDouble(symbol, SYMBOL_POINT);              // Point size
   double pip_size  = (digits == 3 || digits == 5) ? point * 10.0 : point; // Pip size
   if(tick_size <= 0 || tick_val <= 0)
      return 0;                                                            // Validate inputs
   return (pip_size / tick_size) * tick_val * lots;                        // Return pip value
  }

//+------------------------------------------------------------------+
//| Returns pip size in price units for the given symbol             |
//+------------------------------------------------------------------+
double PipSize(const string symbol)
  {
   int    digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);          // Symbol digits
   double point  = SymbolInfoDouble(symbol, SYMBOL_POINT);                 // Point size
   return (digits == 3 || digits == 5) ? point * 10.0 : point;             // Return pip size
  }

The Engine Class

The class is fully encapsulated. All swing states are private. The calling code interacts only through the public interface.

//+------------------------------------------------------------------+
//| CSwingEngine—HTF swing detection, chart-timeframe-agnostic       |
//+------------------------------------------------------------------+
class CSwingEngine
  {
private:
   SSwingPoint       m_swings[];     // Confirmed swing points (newest first)
   int               m_count;        // Number of swings currently stored
   int               m_strength;     // H4 bars required on each side to confirm a swing
   int               m_lookback;     // Maximum H4 bars to scan on each Update() call
   ENUM_SWING_TREND  m_trend;        // Current classified trend
   string            m_symbol;       // Symbol being analyzed
   ENUM_TIMEFRAMES   m_swing_tf;     // Timeframe swings are CALCULATED on (default H4)
   datetime          m_last_htf_bar; // Last H4 bar time processed

   //+------------------------------------------------------------------+
   //| Checks if H4 bar at index is a confirmed swing high              |
   //+------------------------------------------------------------------+
   bool              IsSwingHigh(const double &high[], int idx, int total)
     {
      if(idx - m_strength < 0 || idx + m_strength >= total)
         return false;                                                      // Boundary check
      for(int i = 1; i <= m_strength; i++)
        {
         if(high[idx - i] >= high[idx])
            return false;                                                   // Left side must be lower
         if(high[idx + i] >= high[idx])
            return false;                                                   // Right side must be lower
        }
      return true;                                                          // Confirmed swing high
     }

   //+------------------------------------------------------------------+
   //| Checks if H4 bar at index is a confirmed swing low               |
   //+------------------------------------------------------------------+
   bool              IsSwingLow(const double &low[], int idx, int total)
     {
      if(idx - m_strength < 0 || idx + m_strength >= total)
         return false;                                                      // Boundary check
      for(int i = 1; i <= m_strength; i++)
        {
         if(low[idx - i] <= low[idx])
            return false;                                                   // Left side must be higher
         if(low[idx + i] <= low[idx])
            return false;                                                   // Right side must be higher
        }
      return true;                                                          // Confirmed swing low
     }

   //+------------------------------------------------------------------+
   //| Sorts swings oldest-to-newest, assigns HH/LH/HL/LL labels, then  |
   //| reverses to newest-first so index 0 is always the most recent    |
   //+------------------------------------------------------------------+
   void              LabelSwings()
     {
      //--- Sort ascending by time—labeling must walk oldest to newest
      for(int i = 0; i < m_count - 1; i++)
         for(int j = i + 1; j < m_count; j++)
            if(m_swings[i].time > m_swings[j].time)
              {
               SSwingPoint tmp = m_swings[i];
               m_swings[i] = m_swings[j];
               m_swings[j] = tmp;
              }
      //--- Assign HH/LH/HL/LL by comparing each swing to the last of its kind
      double lastHigh = -1, lastLow = DBL_MAX;
      for(int i = 0; i < m_count; i++)
        {
         if(m_swings[i].is_high)
           {
            m_swings[i].label = (lastHigh < 0 || m_swings[i].price > lastHigh) ? "HH" : "LH";
            lastHigh = m_swings[i].price;
           }
         else
           {
            m_swings[i].label = (lastLow == DBL_MAX || m_swings[i].price > lastLow) ? "HL" : "LL";
            lastLow = m_swings[i].price;
           }
        }
      //--- Reverse to newest-first—matches the public GetSwing(0) contract
      for(int i = 0, j = m_count - 1; i < j; i++, j--)
        {
         SSwingPoint tmp = m_swings[i];
         m_swings[i] = m_swings[j];
         m_swings[j] = tmp;
        }
     }

   //+------------------------------------------------------------------+
   //| Classifies trend from the two most recent labels                 |
   //+------------------------------------------------------------------+
   void              ClassifyTrend()
     {
      if(m_count < 4)
        {
         m_trend = TREND_RANGE;
         return;
        }                                                                   // Need at least 4 swings
      SSwingPoint lastHigh = GetLastSwingHigh();                            // Most recent labeled high
      SSwingPoint lastLow  = GetLastSwingLow();                             // Most recent labeled low
      if(lastHigh.label == "HH" && lastLow.label == "HL")
         m_trend = TREND_UP;                                                // New high + higher low
      else
         if(lastHigh.label == "LH" && lastLow.label == "LL")
            m_trend = TREND_DOWN;                                           // New low + lower high
         else
            m_trend = TREND_RANGE;                                          // Mixed sequence
     }

public:
                     CSwingEngine() : m_count(0), m_strength(3), m_lookback(200),
                     m_trend(TREND_RANGE), m_swing_tf(PERIOD_H4),
                     m_last_htf_bar(0)
     { m_symbol = _Symbol; }

   //+------------------------------------------------------------------+
   //| Initialize the engine                                            |
   //| swing_tf is the timeframe structure is READ from (default H4).   |
   //| It is independent of whatever chart the calling code is on.      |
   //+------------------------------------------------------------------+
   bool              Init(int strength, int lookback,
                          ENUM_TIMEFRAMES swing_tf = PERIOD_H4,
                          const string symbol = "")
     {
      m_strength  = MathMax(1, strength);                                   // Minimum strength of 1
      m_lookback  = MathMax(m_strength * 4, lookback);                      // Minimum sensible lookback
      m_swing_tf  = swing_tf;                                               // Analysis timeframe
      m_symbol    = (symbol == "") ? _Symbol : symbol;                      // Use chart symbol if blank
      m_count     = 0;                                                      // Reset swing count
      m_last_htf_bar = 0;                                                   // Reset HTF bar tracker
      m_trend     = TREND_RANGE;                                            // Start as range
      ArrayResize(m_swings, 0);                                             // Clear swing array
      int bars_available = Bars(m_symbol, m_swing_tf);                      // Check available H4 bars
      if(bars_available < m_lookback + m_strength * 2)                      // Insufficient bars
        {
         Print("CSwingEngine: Insufficient ", EnumToString(m_swing_tf),
               " bars. Available:", bars_available,
               " Required:", m_lookback + m_strength * 2);
         return false;
        }
      Print(StringFormat(
               "CSwingEngine: Initialized | Symbol:%s | SwingTF:%s | Strength:%d | Lookback:%d",
               m_symbol, EnumToString(m_swing_tf), m_strength, m_lookback));
      return true;
     }

   //+------------------------------------------------------------------+
   //| Scans H4 bars and updates swing points and trend classification  |
   //| Gated on a NEW H4 BAR, not a new bar on the calling chart.       |
   //| Safe to call every tick from OnTick() or OnCalculate().          |
   //+------------------------------------------------------------------+
   bool              Update()
     {
      datetime htf_bar = iTime(m_symbol, m_swing_tf, 0);                    // Current H4 bar time
      if(htf_bar == 0)
         return false;                                                      // H4 data not ready
      if(htf_bar == m_last_htf_bar)
         return false;                                                      // Same H4 bar — skip
      m_last_htf_bar = htf_bar;                                             // Update HTF bar tracker
      //--- Copy H4 price data for the lookback window
      int    total = m_lookback + m_strength * 2;                           // Total H4 bars needed
      double high[], low[];
      datetime times[];
      ArraySetAsSeries(high,  true);                                        // Newest first
      ArraySetAsSeries(low,   true);                                        // Newest first
      ArraySetAsSeries(times, true);                                        // Newest first
      if(CopyHigh(m_symbol, m_swing_tf, 0, total, high)  < total)
         return false;                                                      // Copy H4 highs
      if(CopyLow(m_symbol,  m_swing_tf, 0, total, low)   < total)
         return false;                                                      // Copy H4 lows
      if(CopyTime(m_symbol, m_swing_tf, 0, total, times) < total)
         return false;                                                      // Copy H4 times
      //--- Scan for swing points in the confirmed zone
      ArrayResize(m_swings, 0);                                             // Clear and rebuild
      m_count = 0;
      for(int i = m_strength; i < total - m_strength; i++)                  // Scan confirmed zone
        {
         if(IsSwingHigh(high, i, total))                                    // Confirmed swing high
           {
            SSwingPoint sp;
            sp.is_high = true;
            sp.price = high[i];
            sp.time = times[i];
            sp.bar_index = i;
            sp.label = "";
            ArrayResize(m_swings, m_count + 1);
            m_swings[m_count] = sp;
            m_count++;
           }
         if(IsSwingLow(low, i, total))                                      // Confirmed swing low
           {
            SSwingPoint sp;
            sp.is_high = false;
            sp.price = low[i];
            sp.time = times[i];
            sp.bar_index = i;
            sp.label = "";
            ArrayResize(m_swings, m_count + 1);
            m_swings[m_count] = sp;
            m_count++;
           }
        }
      LabelSwings();                                                        // Sort, label, reverse
      ClassifyTrend();                                                      // Classify from labels
      return true;                                                          // New H4 bar processed
     }

   //+------------------------------------------------------------------+
   //| Returns the current classified market trend                      |
   //+------------------------------------------------------------------+
   ENUM_SWING_TREND  GetTrend()       { return m_trend; }

   //+------------------------------------------------------------------+
   //| Returns the timeframe swings are calculated on                   |
   //+------------------------------------------------------------------+
   ENUM_TIMEFRAMES   GetSwingTimeframe() { return m_swing_tf; }

   //+------------------------------------------------------------------+
   //| Returns the total number of confirmed swings currently stored    |
   //+------------------------------------------------------------------+
   int               GetSwingCount()  { return m_count; }

   //+------------------------------------------------------------------+
   //| Returns the swing point at the given index (0 = most recent)     |
   //+------------------------------------------------------------------+
   SSwingPoint       GetSwing(int index)
     {
      SSwingPoint empty = {false, 0, 0, 0, ""};                             // Empty result
      if(index < 0 || index >= m_count)
         return empty;                                                      // Bounds check
      return m_swings[index];                                               // Return swing
     }

   //+------------------------------------------------------------------+
   //| Returns the most recent confirmed swing high                     |
   //+------------------------------------------------------------------+
   SSwingPoint       GetLastSwingHigh()
     {
      SSwingPoint empty = {true, 0, 0, 0, ""};                              // Empty result
      for(int i = 0; i < m_count; i++)                                      // Scan from newest
         if(m_swings[i].is_high)
            return m_swings[i];                                             // Return first high
      return empty;                                                         // None found
     }

   //+------------------------------------------------------------------+
   //| Returns the most recent confirmed swing low                      |
   //+------------------------------------------------------------------+
   SSwingPoint       GetLastSwingLow()
     {
      SSwingPoint empty = {false, 0, 0, 0, ""};                             // Empty result
      for(int i = 0; i < m_count; i++)                                      // Scan from newest
         if(!m_swings[i].is_high)
            return m_swings[i];                                             // Return first low
      return empty;                                                         // None found
     }

   //+------------------------------------------------------------------+
   //| Returns a human-readable string for the current trend            |
   //+------------------------------------------------------------------+
   string            GetTrendString()
     {
      switch(m_trend)
        {
         case TREND_UP:
            return "UPTREND";
         case TREND_DOWN:
            return "DOWNTREND";
         default:
            return "RANGE";
        }
     }
  };

#endif // SWINGENGINE_MQH
//+------------------------------------------------------------------+

"Update()" rebuilds the entire swing array from scratch on every call—this guarantees the array is always consistent with current H4 data and eliminates stale swing points. The important part is the gate at the top: because it checks "iTime(m_symbol, m_swing_tf, 0)" against the last processed H4 bar time, the expensive rebuild only runs once every four hours, no matter how often "Update()" is called from a fast-ticking M15 or M5 chart. This is a useful side effect of the timeframe split, not just a correctness requirement—it makes the engine cheaper to run than a same-timeframe version would be on a busy lower-timeframe chart.

"LabelSwings()": sorts the detected swings into chronological order, walks them from oldest to newest comparing each new high or low to the last one of its kind, and assigns HH/LH or HL/LL accordingly. The final reversal step is specific to this engine—it restores the newest-first ordering that "GetSwing(0)" and the rest of the public interface expect.

The Demo Indicator

Save to "MQL5\Indicators\SwingDemo.mq5." This indicator can be attached to any chart timeframe—M15 and H1 are the timeframes you will actually use for pattern detection later in this series—while the engine underneath always analyzes H4. Swing highs and lows are drawn with the same visual language as Smart Money Concept tooling: a down arrow and HH/LH label above swing highs, an up arrow and HL/LL label below swing lows, bright colors for the higher-timeframe-confirming label (HH, HL), and dimmer colors for the counter-label (LH, LL).

//+------------------------------------------------------------------+
//|                                                    SwingDemo.mq5 |
//|     Visual demonstration of CSwingEngine — H4 structure drawn on |
//|                 whatever timeframe this indicator is attached to |
//|                                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 indicator_chart_window
#property indicator_plots 0

#include <ChartPatterns\SwingEngine.mqh>

input int              InpStrength = 3;                       // Swing strength — H4 bars on each side
input int              InpLookback = 200;                     // Maximum H4 bars to scan
input ENUM_TIMEFRAMES  InpSwingTF  = PERIOD_H4;               // Timeframe swings are calculated on

CSwingEngine g_engine;
int          g_atr_handle = INVALID_HANDLE;                   // ATR on the swing timeframe — sets label offset

//+------------------------------------------------------------------+
//| Indicator initialization function                                |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!g_engine.Init(InpStrength, InpLookback, InpSwingTF))
      return INIT_FAILED;
   g_atr_handle = iATR(_Symbol, InpSwingTF, 14);              // ATR computed on the SAME TF as swings
   if(g_atr_handle == INVALID_HANDLE)
      return INIT_FAILED;
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Removes all objects drawn by this indicator                      |
//+------------------------------------------------------------------+
void ClearObjects()
  {
   int total = ObjectsTotal(0);
   for(int i = total - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "SWING_") == 0)
         ObjectDelete(0, name);
     }
  }

//+------------------------------------------------------------------+
//| Draws all H4 swing points on the current chart timeframe         |
//+------------------------------------------------------------------+
void DrawSwings()
  {
   ClearObjects();
   double atr_buf[];
   ArraySetAsSeries(atr_buf, true);
   double offset = 0;
   if(CopyBuffer(g_atr_handle, 0, 1, 1, atr_buf) >= 1)
      offset = atr_buf[0] * 0.3;                                             // Label/arrow offset

   int count = g_engine.GetSwingCount();
   for(int i = 0; i < count; i++)
     {
      SSwingPoint sp    = g_engine.GetSwing(i);                              // Get swing (time = H4 bar time)
      string      name  = "SWING_" + IntegerToString(i) + "_" +
                          TimeToString(sp.time, TIME_DATE | TIME_MINUTES);   // Unique name
      bool   is_confirm = (sp.label == "HH" || sp.label == "HL");            // Bright vs dim
      color  clr;
      int    arrow;
      double label_price, arrow_price;
      if(sp.is_high)
        {
         clr         = is_confirm ? C'100,200,255' : C'60,130,180';          // Bright = HH, dim = LH
         arrow       = 218;                                                  // Down arrow
         label_price = sp.price + offset;
         arrow_price = sp.price + offset * 0.4;
        }
      else
        {
         clr         = is_confirm ? C'100,255,120' : C'60,160,80';           // Bright = HL, dim = LL
         arrow       = 217;                                                  // Up arrow
         label_price = sp.price - offset;
         arrow_price = sp.price - offset * 0.4;
        }
      //--- sp.time is an H4 bar time. MetaTrader positions time-anchored
      //--- objects at the correct x-coordinate on ANY chart period, so
      //--- these draw correctly here even though they were computed
      //--- entirely from H4 data.
      ObjectCreate(0, name + "_A", OBJ_ARROW, 0, sp.time, arrow_price);
      ObjectSetInteger(0, name + "_A", OBJPROP_ARROWCODE, arrow);
      ObjectSetInteger(0, name + "_A", OBJPROP_COLOR, clr);
      ObjectSetInteger(0, name + "_A", OBJPROP_WIDTH, 1);
      ObjectCreate(0, name + "_L", OBJ_TEXT, 0, sp.time, label_price);
      ObjectSetString(0,  name + "_L", OBJPROP_TEXT, sp.label);
      ObjectSetInteger(0, name + "_L", OBJPROP_COLOR, clr);
      ObjectSetInteger(0, name + "_L", OBJPROP_FONTSIZE, 8);
     }
//--- Draw trend label
   string label_name = "SWING_TREND_LABEL";
   string trend_str  = g_engine.GetTrendString();
   color  trend_clr  = (g_engine.GetTrend() == TREND_UP)   ? clrLime :
                       (g_engine.GetTrend() == TREND_DOWN)  ? clrRed  : clrGray;
   ObjectCreate(0, label_name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, label_name, OBJPROP_XDISTANCE, 10);
   ObjectSetInteger(0, label_name, OBJPROP_YDISTANCE, 30);
   ObjectSetString(0,  label_name, OBJPROP_TEXT,
                   "H4 Trend: " + trend_str);
   ObjectSetInteger(0, label_name, OBJPROP_COLOR, trend_clr);
   ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 12);
   ChartRedraw(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator calculation function                            |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated,
                const datetime &time[], const double &open[],
                const double &high[], const double &low[], const double &close[],
                const long &tick_volume[], const long &volume[], const int &spread[])
  {
//--- Update() is internally gated on a new H4 bar, so calling it on
//--- every OnCalculate() pass—even on a fast chart timeframe—is cheap.
   if(g_engine.Update())
      DrawSwings();
   return rates_total;
  }

//+------------------------------------------------------------------+
//| Indicator deinitialization                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(g_atr_handle);
   ClearObjects();
  }
//+------------------------------------------------------------------+

Notice that "OnCalculate" calls g_engine.Update() unconditionally on every pass—there is no chart-side new-bar gate because the engine's own H4 gate already does that job. Attach this indicator to an M15 chart, and it will redraw at most once every four hours, precisely when a new H4 candle closes and the structure genuinely might have changed.


How Subsequent Articles Use the Engine

Every subsequent article in this series follows the same integration pattern as before, with one addition: "Init()" now takes the analysis timeframe explicitly.

#include <ChartPatterns\SwingEngine.mqh>

CSwingEngine g_engine;

//--- In OnInit(), on an EA attached to M15 or H1:
if(!g_engine.Init(3, 300, PERIOD_H4))
   return INIT_FAILED;

//--- In OnTick(), once per chart bar or every tick—the engine self-gates on H4:
g_engine.Update();

//--- Before evaluating any Double Top pattern:
if(g_engine.GetTrend() != TREND_UP)
   return; // No H4 uptrend—no Double Top possible

This single gate—checking "GetTrend()" before any pattern evaluation—remains the architectural contribution of the engine. What changes is that the gate now reflects H4 structure while the pattern shape itself is still evaluated against the EA's own chart data—its own swing highs, its own neckline breaks, its own entry timing. The engine supplies the context; the chart timeframe supplies the trigger.

One integration detail for later articles: "bar_index" in each "SSwingPoint" is expressed in H4 bars, not chart bars. A pattern detector that measures the distance between two peaks—Part 2's "InpMaxPatternBars," for example—should either be re-expressed in H4 bars or should compare the "time" fields directly and convert to a bar count on its own chart timeframe using "iBarShift()." Comparing "bar_index" values directly across timeframes without this adjustment would silently misjudge how far apart two peaks actually are.


Backtesting and Validation

The "SwingDemo" indicator is the primary tool for validating the engine before any pattern detection is built on top of it.

Attach it to EURUSD on the H1 chart with "InpSwingTF" left at "PERIOD_H4" and examine the following.

Swing point placement: every arrow should land within the span of H4 candles—visually, a swing high arrow on the H1 chart should sit above a cluster of roughly four consecutive H1 candles that together make up the underlying H4 bar, at the price level of that H4 bar's high. If arrows appear to align with individual H1 candle extremes instead, "InpSwingTF" has likely been left at "PERIOD_CURRENT" mistakenly.

Label correctness: an HH label should only appear on a swing high that is priced above the previous HH or LH label. An LL label should only appear on a swing low priced below the previous HL or LL label. Scroll back through a clear H4 uptrend and verify the labels read HL, HH, HL, HH in sequence—any break in that pattern should coincide with a visible structural pause on the H4 chart itself.

Trend classification: the "H4 Trend: UPTREND" label should appear only after the most recent labeled high is HH and the most recent labeled low is HL—check this by switching to the H4 chart directly and confirming the same two swings visually.

Refresh cadence: with the indicator attached to M15, confirm in the terminal's Experts log that "CSwingEngine: Initialized" prints once, and that no further recalculation happens except roughly every four hours. If the chart redraws on every M15 bar, "Update()" is not gating correctly—check that "InpSwingTF" is not accidentally set to "PERIOD_CURRENT."

Test Parameters

Attach "SwingDemo" to the following configurations and verify visually:

Symbol: EURUSD. Chart timeframe: H1. Swing timeframe: H4. Strength: 3. Lookback: 200. Verify H4 trend labels match the clearly visible H4 structure while the chart itself displays H1 candles.

Demonstration on EURUSD

Fig. 4. Demonstration on EURUSD.

Symbol: Gold. Chart timeframe: M15. Swing timeframe: H4. Strength: 3. Lookback: 200. Gold's strong directional moves make it easy to confirm that H4-derived labels remain stable while M15 price chops around inside them.

Demonstration on gold

Fig. 5. Demonstration on gold.

Symbol: GBPUSD. Chart timeframe: H1. Swing timeframe: Daily (D1). Strength: 3. Lookback: 150. Confirms the engine generalizes to any higher-timeframe/lower-timeframe pairing, not just H4-over-M15/H1.

Demonstration on GBPUSD

Fig. 6. Demonstration on GBPUSD.


Known Limitations

The swing classification requires at least two confirmed swing highs and two confirmed swing lows to classify a trend, and because those swings now come from H4, the classification can lag the visual chart-timeframe trend by up to several H4 bars—potentially most of a trading day. This is correct behavior—the engine will not call a trend until H4 evidence meets the minimum structural requirement—but it means the engine is deliberately slower to react than a same-timeframe version would be. That trade-off is the entire point of reading structure from a higher timeframe; it should not be tuned away by lowering "InpSwingTF" back to the chart's own period.

A swing point's displayed position corresponds to the H4 bar's open time, at the H4 bar's high or low price. That price was reached at some point during the four-hour window, not necessarily at the H4 bar's open. On the lower timeframe chart, the arrow will sit at the left edge of the H4 window it belongs to rather than precisely above the specific lower-timeframe candle where the extreme occurred. This is a minor visual offset, not a data error, and it does not affect the trend classification or the swing prices used by pattern detectors.

"Update()" requires enough H4 history to be available for the symbol, independent of how much history the chart's own timeframe has loaded. On a freshly opened chart, or a symbol MetaTrader has not previously loaded H4 data for, the initial "CopyHigh/CopyLow/CopyTime" calls may return fewer bars than requested until the terminal finishes downloading H4 history from the broker. "Init()" checks "Bars()" up front, but a chart opened for the first time on a new symbol may still need a few seconds before "Update()" succeeds.

The engine rebuilds the full H4 swing array on every H4 bar close. For the lookback values used throughout this series (150–300 H4 bars), this is fast and produces no noticeable impact, and it happens far less often than a same-timeframe engine would need to rebuild.

Swing points are defined by their H4 bar's high or low price using bar-level data, not intrabar tick data. On the H4 timeframe, this is standard practice, but it means a brief intra-H4-bar spike that reverses before the H4 candle closes will still register if it produced that bar's extreme.


Conclusion

The pattern detection problem in MQL5 is not only a shape recognition problem or a context recognition problem—it is also, quietly, a timeframe problem. A detector that reads its trend from the same noisy bars it trades on will misclassify structure regularly, no matter how carefully the trend rule itself is written. This article's engine separates those two jobs cleanly: H4 answers, "What is the structure?" The chart's own timeframe answers, "Is the shape here, right now, and is it time to enter?"

"CSwingEngine" is not a trading strategy. It does not generate signals. It does not open positions. Furthermore, it answers one question that every chart pattern detector in this series must answer before it can be trusted: what is the higher-timeframe market structure, labeled the way an experienced technician would label it, and does the current context on the timeframe I trade support the pattern being evaluated?

All code was compiled and tested in MetaTrader 5. Save "SwingEngine.mqh" to "MQL5\Include\ChartPatterns\SwingEngine.mqh" and "SwingDemo.mq5" to "MQL5\Indicators\SwingDemo.mq5." Compile "SwingDemo" in MetaEditor with no additional dependencies. Attach it to a lower timeframe chart such as M15 or H1 with "InpSwingTF" left at H4, and verify H4 swing detection visually.

Attached files |
SwingDemo.mq5 (6.31 KB)
SwingEngine.mqh (17.09 KB)
Does This Entry Filter Really Add Edge? A Block-Permutation Test in MQL5 Does This Entry Filter Really Add Edge? A Block-Permutation Test in MQL5
An MQL5 analyzer reconstructs completed trades, records acceptance labels, and measures the accepted-minus-rejected mean net-profit difference. It benchmarks that statistic against individual permutations, equal-block permutations, and circular shifts while preserving the accepted count. Block-size sensitivity, CSV exports, and coordinated base/filtered passes separate statistical selection evidence from operational effects on profit, drawdown, and efficiency metrics.
Measuring Market Efficiency with Lempel-Ziv Complexity Measuring Market Efficiency with Lempel-Ziv Complexity
This article presents a compact MQL5 library for market-complexity analysis: LZ76 complexity and Normalized Compression Distance built on a SAX symbolizer, exposed through a simple facade and an efficiency indicator. It explains the discretization choices, normalization, and distance formulation, and validates the code with unit checks and an independent cross-check. You get a ready-to-use library and indicator, plus a disciplined way to interpret readings with a shuffle null and a direction check.
Larry Williams Market Secrets (Part 16): Detecting and Trading the Oops Gap Reversal Pattern Larry Williams Market Secrets (Part 16): Detecting and Trading the Oops Gap Reversal Pattern
Learn how to build an MQL5 Expert Advisor that detects and trades Larry Williams’ Oops Gap Reversal pattern using objective gap rules and later-bar confirmation. The EA tracks setup expiration, prepares stop-loss and take-profit levels, supports manual or risk-based position sizing, executes market orders, and is evaluated through historical testing.
Feature Engineering for ML (Part 14): Trend-Scanning Features in MQL5 Feature Engineering for ML (Part 14): Trend-Scanning Features in MQL5
A naive MQL5 port of trend-scanning features recomputes each candidate window per bar at O(H·L) cost. This article introduces CTrendScanningFeatures.mqh, which maintains three running sums per horizon and updates them in O(1) per bar, verified against a Python reference. The indicator exposes four causal buffers - window, slope, t_value, rsquared - at the confirmation bar and corrects a sign inversion present in the original backward labeling mode.