Automating Chart Patterns in MQL5 (Part 1): The Multi-Timeframe Swing Structure Engine
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:
- Why Pattern Detection Without Context Fails
- What Makes a Swing Point Significant
- How Swing Sequences Define Market Structure
- Architecture—Calculating on H4, Drawing on the Chart Timeframe
- Implementation in MQL5
- How Subsequent Articles Use the Engine
- Backtesting and Validation
- Known Limitations
- Conclusion
Why Pattern Detection Without Context Fails

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:
- 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.
- 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

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.
- "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.
- "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.

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.

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.

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.

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.
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.
Does This Entry Filter Really Add Edge? A Block-Permutation Test in MQL5
Measuring Market Efficiency with Lempel-Ziv Complexity
Larry Williams Market Secrets (Part 16): Detecting and Trading the Oops Gap Reversal Pattern
Feature Engineering for ML (Part 14): Trend-Scanning Features in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use