From Novice to Expert: Trading the RSI with Market Structure Awareness
Contents
Introduction
The journey toward consistent trading performance is often hindered by a reliance on fragmented, lagging signals. Conventional education emphasizes tools like channels for breakout trades or the RSI oscillator for overbought/oversold reversals. While foundational, these methods present well-documented pitfalls: breakouts often fail, retests may never occur, and the RSI can remain in extreme territories far longer than a trader’s capital can endure. This reactive approach leaves traders vulnerable to false signals and missed opportunities, perpetually entering after the optimal momentum has begun.
This article introduces a structured methodology designed to overcome these limitations by synergizing price action awareness with early momentum confirmation. We move beyond viewing market structure—specifically, trend channels—as mere boundaries for breakouts. Instead, we treat it as the foundational context that informs the strength and validity of momentum signals.
Our focus is on a precise technique: using the RSI not as a standalone reversal oracle, but as a confirmation engine within the framework of established market structure. By algorithmically detecting price channels and seeking RSI-based early confirmation at key structural levels, we aim to identify higher-probability entries that are both earlier and safer than traditional breakout-retest models.
Furthermore, we bridge the gap between conceptual strategy and executable edge by leveraging the MQL5 programming language. The manual identification of structures and signals is not only time-consuming but also subjective. Therefore, we will detail the development of an automated system that:
- Algorithmically identifies valid trend channels.
- Intelligently interprets RSI behavior in the context of these channels.
- Executes and manages trades based on predefined, rule-based logic.
We will begin with a thorough analysis of common manual channel trading and its common patterns, establishing the core price action principles. We then progress to building a robust channel-detection algorithm. The cornerstone of our discussion is the innovative integration of this structural analysis with filtered RSI dynamics to generate high-confidence signals. Finally, we encapsulate this entire logic into a prototype automated trading system in MQL5, demonstrating a practical path from market theory to algorithmic execution.
Understanding the concept and common structures
Bullish and bearish flags represent a consolidation of momentum within a strong trend. While traditionally viewed as simple continuation patterns, their true value lies in the specific momentum behavior exhibited within their channel boundaries. The conventional breakout-and-retest approach, though logical, systematically surrenders a substantial portion of the ensuing move.
Our methodology refines this by targeting the precise moment within the consolidation where the trend's underlying strength reasserts itself, using a powerful confluence of structure and momentum.
The Anatomy of a Reliable Flag
Bullish Flag: Forms in a strong uptrend. It consists of a sharp, near-vertical flagpole (the initial impulsive move), followed by a downward-sloping or rectangular consolidation channel. The pattern is valid only if the consolidation does not retrace beyond the start of the flagpole.

A Bullish flag setup
Bearish Flag: The mirror image within a downtrend. A steep flagpole downward is followed by an upward-sloping or rectangular consolidation channel.

A bearish flag setup
Beyond the Breakout: The Divergence Edge
The standard breakout entry waits for price to exit the channel and then retest its boundary. Our approach seeks a higher-probability, earlier entry by identifying loss of momentum in the counter-trend move before the breakout even occurs. This is achieved through regular RSI divergence at the pattern's critical level.
For a bullish flag:
The Structural Context: Price is making a series of lower lows (LL) as it descends within the downward-sloping channel.
The Divergence Signal (Our Entry Confluence): As price makes its 3rd or 4th lower low (LL) near the channel's lower support boundary, the RSI forms a higher low (HL). This divergence indicates that the downward selling momentum within the consolidation is exhausting precisely at the logical support area. The larger uptrend is poised to resume.
For a bearish flag:
The structural context: Price is making a series of higher highs (HH) as it ascends within the upward-sloping channel.
The Divergence Signal (Our Entry Confluence): As price makes a new high (HH) near the channel's upper resistance boundary, the RSI forms a lower high (LH). This signals that the upward buying momentum within the pullback is failing exactly at the logical resistance. The larger downtrend is ready to continue.

Showing divergence channel confluence on a bearish flag setup
Why This Confluence Transforms the Trade:
This divergence-based logic directly addresses the core flaw of both simple breakout rules and basic overbought/oversold RSI signals:
- Prevents "Catching the Falling Knife": An oversold RSI at channel support is common, but not a reliable buy signal. A bullish RSI divergence at that same support, however, provides evidence that the selling pressure is deteriorating, turning a mere "bounce level" into a high-probability "reversal point."
- Offers Superior Risk Management: An entry triggered by divergence at a channel boundary allows for a logically tight stop-loss (placed just beyond the most recent swing low/high in the divergence). The reward-to-risk ratio is significantly improved compared to a breakout entry.
- Seeks Earlier, High-Confirmation Entries: Instead of waiting for price action to complete the breakout (and often the best part of the move), we act on the momentum warning that precedes and predicts that breakout.
In essence, we are no longer just trading a geometric pattern. We are trading the proof of momentum exhaustion within that pattern, using divergence as our definitive evidence.
This precise, rule-based concept is perfectly suited for algorithmic translation. In the next section, we will define the exact computational rules to detect these flag structures and identify the critical RSI divergence at their boundaries, forming the core logic of our MQL5 expert advisor.
Join us as we deconstruct the market's blueprint and build a systematic process to trade it with greater clarity, timing, and discipline.
Implementation
Successfully transforming a trading concept into a robust automated system requires a methodical, modular approach. While RSI divergence detection combined with channel-based structure confirmation sounds theoretically straightforward, the practical implementation presents significant technical complexity. To manage this complexity effectively, I've architected the solution as two independent yet complementary modules: - RSI Divergence Indicator: A standalone technical indicator that identifies and visually marks divergence patterns on price charts.
- Equidistant Channel Expert Advisor: An automated trading assistant that detects and draws channel structures in real-time.
This modular architecture offers several strategic advantages. Each component can be developed, tested, and optimized independently, ensuring reliability before integration. Traders can use either tool separately based on their strategy, or combine them for enhanced conviction. Ultimately, these modules can be seamlessly integrated into a unified trading system that provides structure detection, signal confirmation, and automated execution.
The implementation follows a systematic progression. First, I'll detail the step-by-step construction of the RSI Divergence indicator, explaining the logic for pivot detection, divergence validation, and visual signal presentation. Next, I'll demonstrate the Channel Placer EA's algorithm for identifying and drawing equidistant channels on price structures. When these tools operate in concert, traders receive timely divergence alerts within confirmed channel contexts, transforming subjective technical analysis into a mechanical, rules-based trading methodology backed by algorithmic precision.
These next steps will guide you through the complete implementation journey, from individual component development to integrated system deployment, providing you with a professional-grade trading toolkit built on solid algorithmic foundations.
RSI Divergence Detector Indicator
Section 1: Indicator Framework Setup and Configuration
The foundation of our RSI Divergence Detector begins with establishing the proper MetaTrader 5 indicator framework. We start with the mandatory indicator properties that define how the tool interacts with the trading platform. The #property directives at lines 1-22 configure essential metadata: copyright information, version tracking, and, most importantly, the visual presentation settings.
The indicator_separate_window declaration ensures our RSI calculations appear in their own dedicated sub-window below the main price chart, preventing visual clutter. We allocate 7 data buffers for storing various calculation results while defining only 3 actual plots for display—this separation between calculation buffers and visual outputs allows for efficient memory management while maintaining clean visualization.
The color and style configurations (lines 11-17) establish an intuitive visual language: DodgerBlue for the RSI line, orange for high pivots, and DeepSkyBlue for low pivots, creating immediate visual distinction between different types of price structure points.
//+------------------------------------------------------------------+ //| RSIDivergenceDetector.mq5 | //| Copyright 2025, MetaQuotes Ltd| //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property description "RSI Divergence Detector with Main Chart Arrows" #property description "Shows divergence arrows on main chart, stores pivot values" #property indicator_separate_window #property indicator_buffers 7 #property indicator_plots 3 #property indicator_color1 clrDodgerBlue // RSI line #property indicator_color2 clrOrange // RSI High Pivots #property indicator_color3 clrDeepSkyBlue // RSI Low Pivots #property indicator_width1 2 #property indicator_width2 3 #property indicator_width3 3 #property indicator_label1 "RSI" #property indicator_label2 "RSI High Pivot" #property indicator_label3 "RSI Low Pivot"
Section 2: User Configuration and Input Parameters
Professional trading tools must balance automation with user control, which we achieve through the comprehensive input parameter system (lines 25-42). The input keyword creates user-modifiable variables that appear in the indicator's settings dialog, allowing traders to customize the detection algorithm to their specific trading style.
We provide control over the core RSI calculation (period and price source), pivot detection sensitivity through InpPivotStrength, and divergence filtering parameters. The boolean flags InpShowRegular and InpShowHidden give traders the ability to toggle between different divergence types based on their trading strategy.
Particularly important is the InpRequireRSIBreak parameter (line 40), which implements the confirmation logic you requested—ensuring the RSI actually breaks through the previous pivot level before signaling a divergence. This adds a layer of validation that prevents premature signals and increases reliability.
//--- Input parameters input int InpRSIPeriod = 14; // RSI Period input ENUM_APPLIED_PRICE InpRSIPrice = PRICE_CLOSE; // RSI Applied Price input int InpPivotStrength = 3; // Pivot Strength (bars on each side) input double InpOverbought = 70.0; // Overbought Level input double InpOversold = 30.0; // Oversold Level input bool InpShowRegular = true; // Show Regular Divergences input bool InpShowHidden = true; // Show Hidden Divergences input color InpBullishColor = clrLimeGreen; // Bullish divergence arrow color input color InpBearishColor = clrRed; // Bearish divergence arrow color input int InpArrowSize = 3; // Arrow size on chart input bool InpAlertOnDivergence = true; // Alert on divergence input bool InpSendNotification = false; // Send notification input bool InpRequireRSIBreak = true; // Require RSI to break pivot line input double InpMinDivergenceStrength = 2.0; // Minimum RSI divergence strength input int InpMaxPivotDistance = 100; // Max bars between pivots for divergence input double InpArrowOffsetPct = 0.3; // Arrow offset percentage (0.3 = 30%)
Section 3: Data Structure Design and Memory Management
At lines 44-67, we implement the core data architecture using a custom RSI_PIVOT structure. This structure represents a critical design decision: rather than relying on simple arrays that mix different types of data, we create a dedicated data type that encapsulates all relevant information about each pivot point.
Each RSI_PIVOT instance stores the bar index, RSI value, corresponding price level, timestamp, pivot type (high/low), detection strength, and confirmation status.
The constructor (lines 59-62) ensures proper initialization, preventing common programming errors with uninitialized variables. We maintain a dynamic array rsiPivots[] to store these structures, with pivotCount tracking the current number of valid entries. This approach provides both data integrity and efficient memory usage, as we can easily add, remove, or search through pivot data without complex index management.
//--- Indicator buffers double BufferRSI[]; double BufferRSIHighPivot[]; double BufferRSILowPivot[]; double BufferRSIHigh[]; double BufferRSILow[]; double BufferPivotHigh[]; double BufferPivotLow[]; //--- Global variables int rsiHandle; datetime lastAlertTime; string indicatorPrefix = "RSI_DIV_"; //--- Structures for storing pivot data struct RSI_PIVOT { int barIndex; double value; double price; datetime time; bool isHigh; int strength; bool isConfirmed; // Constructor to initialize values RSI_PIVOT() : barIndex(-1), value(0.0), price(0.0), time(0), isHigh(false), strength(0), isConfirmed(false) {} }; RSI_PIVOT rsiPivots[]; int pivotCount = 0;
Section 4: Initialization and Resource Setup
The OnInit() function (lines 70-125) handles the critical setup phase when the indicator loads. Lines 74-80 establish the connection between our calculation buffers and the indicator's display system using SetIndexBuffer().
Each buffer receives a specific role: BufferRSI stores the calculated RSI values, while BufferRSIHighPivot and BufferRSILowPivot hold the visual markers for pivot points.
Lines 83-94 configure the plotting behavior: the main RSI line uses DRAW_LINE, while pivot points use DRAW_ARROW with character code 159 (a square symbol). We create a handle to the built-in RSI indicator at line 103 using iRSI(), which provides efficient access to pre-calculated RSI values without reinventing the wheel.
Lines 112-115 set up visual reference lines for overbought/oversold levels, and line 121 calls CleanChartObjects() to remove any residual graphical elements from previous indicator runs, ensuring a clean starting state.
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, BufferRSI, INDICATOR_DATA); SetIndexBuffer(1, BufferRSIHighPivot, INDICATOR_DATA); SetIndexBuffer(2, BufferRSILowPivot, INDICATOR_DATA); SetIndexBuffer(3, BufferRSIHigh, INDICATOR_DATA); SetIndexBuffer(4, BufferRSILow, INDICATOR_DATA); SetIndexBuffer(5, BufferPivotHigh, INDICATOR_DATA); SetIndexBuffer(6, BufferPivotLow, INDICATOR_DATA); //--- Set plot properties PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_ARROW); //--- Set arrow codes for RSI pivot points PlotIndexSetInteger(1, PLOT_ARROW, 159); // Square dot for high pivots PlotIndexSetInteger(2, PLOT_ARROW, 159); // Square dot for low pivots //--- Set empty values for arrow buffers PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE); //--- Set indicator labels IndicatorSetString(INDICATOR_SHORTNAME, "RSI Divergence (" + string(InpRSIPeriod) + ")"); IndicatorSetInteger(INDICATOR_DIGITS, 2); //--- Create RSI handle rsiHandle = iRSI(_Symbol, _Period, InpRSIPeriod, InpRSIPrice); if(rsiHandle == INVALID_HANDLE) { Print("Failed to create RSI handle"); return(INIT_FAILED); } //--- Set overbought/oversold levels IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, InpOversold); IndicatorSetDouble(INDICATOR_LEVELVALUE, 1, InpOverbought); IndicatorSetInteger(INDICATOR_LEVELCOLOR, 0, clrSilver); IndicatorSetInteger(INDICATOR_LEVELCOLOR, 1, clrSilver); IndicatorSetInteger(INDICATOR_LEVELSTYLE, 0, STYLE_DOT); IndicatorSetInteger(INDICATOR_LEVELSTYLE, 1, STYLE_DOT); lastAlertTime = 0; //--- Clean any existing objects from previous runs CleanChartObjects(); return(INIT_SUCCEEDED); }
Section 5: Core Calculation Engine and Data Processing
The OnCalculate() function (lines 128-169) serves as the main processing loop, called on every tick and bar update. We begin with validation at line 130, ensuring sufficient historical data exists for meaningful calculations.
Lines 133-142 implement intelligent recalculation logic: if this is the first run (prev_calculated == 0), we initialize our pivot array and reset counters; otherwise, we continue from where we left off, optimizing performance by avoiding redundant calculations. At line 145, we retrieve RSI values using CopyBuffer() from our RSI handle—this demonstrates proper use of MetaTrader 5's technical indicator API for data access.
The function then orchestrates the three main processing steps: finding RSI pivots, detecting divergences, and cleaning old data. This modular approach separates concerns while maintaining efficient data flow.
//+------------------------------------------------------------------+ //| Custom indicator iteration 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[]) { if(rates_total < InpRSIPeriod + 10) return(0); int start; if(prev_calculated == 0) { start = InpRSIPeriod + InpPivotStrength; ArrayResize(rsiPivots, rates_total); pivotCount = 0; CleanChartObjects(); } else { start = prev_calculated - 1; } //--- Get RSI values if(CopyBuffer(rsiHandle, 0, 0, rates_total, BufferRSI) <= 0) return(0); //--- Find RSI pivots FindRSIPivots(rates_total, start, high, low, time); //--- Detect divergences and draw arrows on main chart DetectDivergences(rates_total, start, high, low, close, time); //--- Clean old pivot data CleanOldPivots(rates_total); return(rates_total); }
Section 6: Pivot Detection Algorithm Implementation
The FindRSIPivots() function (lines 172-218) implements the core swing point identification logic. Lines 175-176 clear previous pivot markers from the display buffers, ensuring we only show current, relevant pivots.
The loop at lines 178-216 iterates through bars, applying the pivot detection criteria. The IsHighPivot() and IsLowPivot() helper functions (lines 221-272) contain the actual detection logic: for a bar to qualify as a high pivot, its RSI value must be greater than a specified number of bars on both sides (controlled by InpPivotStrength).
When a valid pivot is found (lines 183-215), we populate a new RSI_PIVOT structure with all relevant data: the bar index for reference, RSI value for comparison, price level for divergence analysis, timestamp for tracking, and type classification. The isConfirmedflag is initially set to false, allowing our divergence detection logic to mark pivots once they've contributed to a valid signal.
//+------------------------------------------------------------------+ //| Find RSI pivots function | //+------------------------------------------------------------------+ void FindRSIPivots(int rates_total, int start, const double &high[], const double &low[], const datetime &time[]) { // Clear pivot buffers ArrayFill(BufferRSIHighPivot, start, rates_total - start, EMPTY_VALUE); ArrayFill(BufferRSILowPivot, start, rates_total - start, EMPTY_VALUE); for(int i = start; i < rates_total - InpPivotStrength; i++) { //--- Check for RSI high pivot if(IsHighPivot(i, BufferRSI, InpPivotStrength)) { if(pivotCount < ArraySize(rsiPivots)) { rsiPivots[pivotCount].barIndex = i; rsiPivots[pivotCount].value = BufferRSI[i]; rsiPivots[pivotCount].price = high[i]; rsiPivots[pivotCount].time = time[i]; rsiPivots[pivotCount].isHigh = true; rsiPivots[pivotCount].strength = InpPivotStrength; rsiPivots[pivotCount].isConfirmed = false; pivotCount++; BufferRSIHighPivot[i] = BufferRSI[i]; } } //--- Check for RSI low pivot if(IsLowPivot(i, BufferRSI, InpPivotStrength)) { if(pivotCount < ArraySize(rsiPivots)) { rsiPivots[pivotCount].barIndex = i; rsiPivots[pivotCount].value = BufferRSI[i]; rsiPivots[pivotCount].price = low[i]; rsiPivots[pivotCount].time = time[i]; rsiPivots[pivotCount].isHigh = false; rsiPivots[pivotCount].strength = InpPivotStrength; rsiPivots[pivotCount].isConfirmed = false; pivotCount++; BufferRSILowPivot[i] = BufferRSI[i]; } } } }
Section 7: Helper Functions for Pivot Detection
The IsHighPivot() and IsLowPivot() functions contain the precise logic for identifying swing points in RSI values. These functions implement what traders commonly refer to as "pivot strength" detection.
For a bar to qualify as a high pivot (lines 221-246), its RSI value must be greater than a specified number of preceding bars (looking left) AND greater than a specified number of following bars (looking right).
This bilateral verification ensures we're detecting true local maxima/minima rather than temporary fluctuations. The strength parameter controls sensitivity: higher values require more confirmation bars, resulting in fewer but more reliable pivots. These functions return simple boolean results that feed into our main pivot detection loop, demonstrating clean separation of detection logic from data management.
//+------------------------------------------------------------------+ //| Check if bar is a high pivot | //+------------------------------------------------------------------+ bool IsHighPivot(int index, double &buffer[], int strength) { if(index < strength || index >= ArraySize(buffer) - strength) return false; double pivotValue = buffer[index]; // Check left side for(int i = 1; i <= strength; i++) { if(buffer[index - i] > pivotValue) return false; } // Check right side for(int i = 1; i <= strength; i++) { if(buffer[index + i] > pivotValue) return false; } return true; } //+------------------------------------------------------------------+ //| Check if bar is a low pivot | //+------------------------------------------------------------------+ bool IsLowPivot(int index, double &buffer[], int strength) { if(index < strength || index >= ArraySize(buffer) - strength) return false; double pivotValue = buffer[index]; // Check left side for(int i = 1; i <= strength; i++) { if(buffer[index - i] < pivotValue) return false; } // Check right side for(int i = 1; i <= strength; i++) { if(buffer[index + i] < pivotValue) return false; } return true; }
Section 8: Divergence Detection Logic and Signal Generation
The DetectDivergences() function (lines 275-373) represents the intelligence core of our indicator. After validating we have sufficient pivot data at line 277, we implement a nested loop structure (lines 280-372) that compares all possible pivot pairs.
Line 283 applies a practical filter: pivots too far apart (exceeding InpMaxPivotDistance) are ignored, as distant correlations often represent different market phases. The logic then separates into two main branches: high pivot comparisons for bearish divergences (lines 287-322) and low pivot comparisons for bullish divergences (lines 323-372).
For each divergence type, we check both regular and hidden varieties, applying the specific Check...Divergence() functions that encapsulate the exact price/RSI relationship criteria. When a valid divergence is found, lines 294-298 (for bearish) and 330-334 (for bullish) calculate appropriate arrow placement using a percentage-based offset from the current price range, ensuring arrows are clearly visible without obscuring price action.
//+------------------------------------------------------------------+ //| Detect divergences between price and RSI | //+------------------------------------------------------------------+ void DetectDivergences(int rates_total, int start, const double &high[], const double &low[], const double &close[], const datetime &time[]) { if(pivotCount < 4) return; // Check all pivot pairs for divergence for(int i = pivotCount - 1; i >= 0; i--) { for(int j = i - 1; j >= 0; j--) { // Skip if pivots are too far apart if(rsiPivots[i].barIndex - rsiPivots[j].barIndex > InpMaxPivotDistance) continue; // Check if both are high pivots if(rsiPivots[i].isHigh && rsiPivots[j].isHigh) { // Check for regular bearish divergence if(InpShowRegular && CheckBearishDivergence(rsiPivots[i], rsiPivots[j], rates_total)) { // Check RSI break confirmation if required if(!InpRequireRSIBreak || CheckRSIBreak(rsiPivots[j], rsiPivots[i], rates_total, true)) { double arrowPrice = high[rsiPivots[i].barIndex]; double range = high[rsiPivots[i].barIndex] - low[rsiPivots[i].barIndex]; double offset = range * InpArrowOffsetPct; DrawChartArrow(rsiPivots[i].barIndex, time[rsiPivots[i].barIndex], arrowPrice + offset, false, "Bearish"); TriggerAlert("Regular Bearish Divergence detected!", time[rsiPivots[i].barIndex]); // Mark as confirmed to avoid duplicate signals rsiPivots[i].isConfirmed = true; rsiPivots[j].isConfirmed = true; } } // Check for hidden bearish divergence else if(InpShowHidden && CheckHiddenBearishDivergence(rsiPivots[i], rsiPivots[j])) { if(!InpRequireRSIBreak || CheckRSIBreak(rsiPivots[j], rsiPivots[i], rates_total, true)) { double arrowPrice = high[rsiPivots[i].barIndex]; double range = high[rsiPivots[i].barIndex] - low[rsiPivots[i].barIndex]; double offset = range * InpArrowOffsetPct; DrawChartArrow(rsiPivots[i].barIndex, time[rsiPivots[i].barIndex], arrowPrice + offset, false, "Bearish(H)"); TriggerAlert("Hidden Bearish Divergence detected!", time[rsiPivots[i].barIndex]); rsiPivots[i].isConfirmed = true; rsiPivots[j].isConfirmed = true; } } } // Check if both are low pivots else if(!rsiPivots[i].isHigh && !rsiPivots[j].isHigh) { // Check for regular bullish divergence if(InpShowRegular && CheckBullishDivergence(rsiPivots[i], rsiPivots[j], rates_total)) { // Check RSI break confirmation if required if(!InpRequireRSIBreak || CheckRSIBreak(rsiPivots[j], rsiPivots[i], rates_total, false)) { double arrowPrice = low[rsiPivots[i].barIndex]; double range = high[rsiPivots[i].barIndex] - low[rsiPivots[i].barIndex]; double offset = range * InpArrowOffsetPct; DrawChartArrow(rsiPivots[i].barIndex, time[rsiPivots[i].barIndex], arrowPrice - offset, true, "Bullish"); TriggerAlert("Regular Bullish Divergence detected!", time[rsiPivots[i].barIndex]); rsiPivots[i].isConfirmed = true; rsiPivots[j].isConfirmed = true; } } // Check for hidden bullish divergence else if(InpShowHidden && CheckHiddenBullishDivergence(rsiPivots[i], rsiPivots[j])) { if(!InpRequireRSIBreak || CheckRSIBreak(rsiPivots[j], rsiPivots[i], rates_total, false)) { double arrowPrice = low[rsiPivots[i].barIndex]; double range = high[rsiPivots[i].barIndex] - low[rsiPivots[i].barIndex]; double offset = range * InpArrowOffsetPct; DrawChartArrow(rsiPivots[i].barIndex, time[rsiPivots[i].barIndex], arrowPrice - offset, true, "Bullish(H)"); TriggerAlert("Hidden Bullish Divergence detected!", time[rsiPivots[i].barIndex]); rsiPivots[i].isConfirmed = true; rsiPivots[j].isConfirmed = true; } } } } } }
Section 9: Divergence Validation and Confirmation Functions
The validation functions CheckBearishDivergence(), CheckBullishDivergence(), CheckHiddenBearishDivergence(), and CheckHiddenBullishDivergence() (lines 376-445) implement the precise mathematical definitions of each divergence type.
Each function follows a consistent pattern: parameter validation, price relationship check, RSI relationship check, strength validation, and confirmation status verification.
For example, CheckBearishDivergence() at lines 376-395 requires: 1) the newer pivot has a higher price than the older pivot, 2) the newer pivot has a lower RSI value (by at least InpMinDivergenceStrength), 3) the absolute difference meets minimum strength requirements, and 4) neither pivot has been previously confirmed in another divergence. These strict criteria prevent false signals and ensure only meaningful divergences trigger alerts.
//+------------------------------------------------------------------+ //| Check for regular bearish divergence | //+------------------------------------------------------------------+ bool CheckBearishDivergence(RSI_PIVOT &pivot1, RSI_PIVOT &pivot2, int rates_total) { // pivot1 is newer, pivot2 is older if(pivot1.barIndex <= pivot2.barIndex) return false; if(pivot1.barIndex >= rates_total - 1 || pivot2.barIndex >= rates_total - 1) return false; // Price makes higher high bool priceHigher = pivot1.price > pivot2.price; // RSI makes lower high bool rsiLower = pivot1.value < pivot2.value - InpMinDivergenceStrength; // Ensure there's enough divergence strength bool enoughStrength = MathAbs(pivot2.value - pivot1.value) >= InpMinDivergenceStrength; // Not already confirmed bool notConfirmed = !pivot1.isConfirmed && !pivot2.isConfirmed; return priceHigher && rsiLower && enoughStrength && notConfirmed; } //+------------------------------------------------------------------+ //| Check for regular bullish divergence | //+------------------------------------------------------------------+ bool CheckBullishDivergence(RSI_PIVOT &pivot1, RSI_PIVOT &pivot2, int rates_total) { // pivot1 is newer, pivot2 is older if(pivot1.barIndex <= pivot2.barIndex) return false; if(pivot1.barIndex >= rates_total - 1 || pivot2.barIndex >= rates_total - 1) return false; // Price makes lower low bool priceLower = pivot1.price < pivot2.price; // RSI makes higher low bool rsiHigher = pivot1.value > pivot2.value + InpMinDivergenceStrength; // Ensure there's enough divergence strength bool enoughStrength = MathAbs(pivot1.value - pivot2.value) >= InpMinDivergenceStrength; // Not already confirmed bool notConfirmed = !pivot1.isConfirmed && !pivot2.isConfirmed; return priceLower && rsiHigher && enoughStrength && notConfirmed; } //+------------------------------------------------------------------+ //| Check for hidden bearish divergence | //+------------------------------------------------------------------+ bool CheckHiddenBearishDivergence(RSI_PIVOT &pivot1, RSI_PIVOT &pivot2) { // Price makes lower high bool priceLower = pivot1.price < pivot2.price; // RSI makes higher high bool rsiHigher = pivot1.value > pivot2.value + InpMinDivergenceStrength; // Not already confirmed bool notConfirmed = !pivot1.isConfirmed && !pivot2.isConfirmed; return priceLower && rsiHigher && notConfirmed; } //+------------------------------------------------------------------+ //| Check for hidden bullish divergence | //+------------------------------------------------------------------+ bool CheckHiddenBullishDivergence(RSI_PIVOT &pivot1, RSI_PIVOT &pivot2) { // Price makes higher low bool priceHigher = pivot1.price > pivot2.price; // RSI makes lower low bool rsiLower = pivot1.value < pivot2.value - InpMinDivergenceStrength; // Not already confirmed bool notConfirmed = !pivot1.isConfirmed && !pivot2.isConfirmed; return priceHigher && rsiLower && notConfirmed; }
Section 10: RSI Break Confirmation and Signal Validation
The CheckRSIBreak() function (lines 448-478) implements the additional validation layer you specified—ensuring the RSI actually breaks through the previous pivot level before considering a divergence confirmed.
This function addresses the common problem of premature divergence signals that occur before momentum actually shifts. Lines 456-458 establish a lookback window (10 bars) after the newer pivot, during which we monitor for the break condition.
For bearish divergences (lines 464-467), we check if RSI falls below the older pivot's value; for bullish divergences (lines 469-472), we check if RSI rises above the older pivot's value. This confirmation step transforms the indicator from a simple pattern detector into a momentum-shift validator, significantly increasing signal reliability.
//+------------------------------------------------------------------+ //| Check if RSI has broken the previous pivot level | //+------------------------------------------------------------------+ bool CheckRSIBreak(RSI_PIVOT &olderPivot, RSI_PIVOT &newerPivot, int rates_total, bool isBearish) { // For bearish: Check if RSI has broken below the older pivot's value // For bullish: Check if RSI has broken above the older pivot's value if(newerPivot.barIndex >= rates_total - 1) return false; // Look for break in recent bars after newer pivot int lookbackBars = 10; int startBar = newerPivot.barIndex + 1; int endBar = MathMin(rates_total - 1, newerPivot.barIndex + lookbackBars); for(int i = startBar; i <= endBar; i++) { if(isBearish) { // Bearish: RSI should break below older pivot value if(BufferRSI[i] < olderPivot.value) return true; } else { // Bullish: RSI should break above older pivot value if(BufferRSI[i] > olderPivot.value) return true; } } return false; }
Section 11: Visual Signal Presentation and Chart Management
The DrawChartArrow() function (lines 481-529) handles the visual presentation of divergence signals on the main price chart. Lines 483-484 generate unique object names using the timestamp, ensuring we can manage individual arrow/label pairs independently. Lines 487-488 clean up any existing objects with the same names, preventing duplication when the indicator recalculates.
The function then branches based on signal direction: bullish signals use OBJ_ARROW_BUY objects (lines 491-505) while bearish signals use OBJ_ARROW_SELL objects (lines 507-522). Each arrow receives configurable styling (color from InpBullishColor/InpBearishColor, size from InpArrowSize) and includes a text label showing the divergence type.
The CleanChartObjects() function (lines 532-542) provides systematic cleanup of all indicator-created objects, maintaining chart cleanliness.
//+------------------------------------------------------------------+ //| Draw arrow on main chart | //+------------------------------------------------------------------+ void DrawChartArrow(int barIndex, datetime time, double price, bool isBullish, string labelText) { string arrowName = indicatorPrefix + "Arrow_" + IntegerToString(time); string labelName = indicatorPrefix + "Label_" + IntegerToString(time); // Remove existing objects with same name ObjectDelete(0, arrowName); ObjectDelete(0, labelName); if(isBullish) { // Draw bullish arrow (up arrow) if(!ObjectCreate(0, arrowName, OBJ_ARROW_BUY, 0, time, price)) { Print("Failed to create arrow object: ", GetLastError()); return; } ObjectSetInteger(0, arrowName, OBJPROP_COLOR, InpBullishColor); ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, InpArrowSize); ObjectSetInteger(0, arrowName, OBJPROP_BACK, false); // Draw label if(!ObjectCreate(0, labelName, OBJ_TEXT, 0, time, price)) return; ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); ObjectSetInteger(0, labelName, OBJPROP_COLOR, InpBullishColor); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); } else { // Draw bearish arrow (down arrow) if(!ObjectCreate(0, arrowName, OBJ_ARROW_SELL, 0, time, price)) { Print("Failed to create arrow object: ", GetLastError()); return; } ObjectSetInteger(0, arrowName, OBJPROP_COLOR, InpBearishColor); ObjectSetInteger(0, arrowName, OBJPROP_WIDTH, InpArrowSize); ObjectSetInteger(0, arrowName, OBJPROP_BACK, false); // Draw label if(!ObjectCreate(0, labelName, OBJ_TEXT, 0, time, price)) return; ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); ObjectSetInteger(0, labelName, OBJPROP_COLOR, InpBearishColor); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); } ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_CENTER); ObjectSetInteger(0, labelName, OBJPROP_BACK, false); }
Section 12: Cleanup Functions and Alert System
The CleanChartObjects() function (lines 532-542) and OnDeinit() function (lines 572-582) handle critical resource management. The pivot cleanup logic maintains a rolling window of the most recent 500 pivots, discarding older ones to prevent memory bloat during extended chart analysis. When the indicator is removed, OnDeinit() releases the RSI indicator handle using IndicatorRelease() and cleans up all chart objects.
The TriggerAlert() function implements a sophisticated notification system that balances signal awareness with notification fatigue management. Line 554 respects the user's preference setting for alerts.
Line 557 implements time-based filtering using lastAlertTime, preventing repeated alerts for the same bar—a common annoyance in trading indicators. When conditions are met, line 562 triggers a standard MetaTrader 5 alert with sound, and lines 565-568 optionally send platform notifications if enabled.
//+------------------------------------------------------------------+ //| Clean chart objects | //+------------------------------------------------------------------+ void CleanChartObjects() { int total = ObjectsTotal(0, 0, -1); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i, 0, -1); if(StringFind(name, indicatorPrefix, 0) != -1) { ObjectDelete(0, name); } } } //+------------------------------------------------------------------+ //| Clean old pivot data | //+------------------------------------------------------------------+ void CleanOldPivots(int rates_total) { if(pivotCount > 500) // Keep last 500 pivots maximum { int newCount = 250; for(int i = 0; i < newCount; i++) { rsiPivots[i] = rsiPivots[pivotCount - newCount + i]; } pivotCount = newCount; } } //+------------------------------------------------------------------+ //| Trigger alert function | //+------------------------------------------------------------------+ void TriggerAlert(string message, datetime time) { if(!InpAlertOnDivergence) return; // Avoid repeated alerts for same bar if(time <= lastAlertTime) return; lastAlertTime = time; // Play sound Alert(message + " at ", TimeToString(time, TIME_DATE|TIME_MINUTES)); // Send notification if enabled if(InpSendNotification) SendNotification("RSI Divergence: " + Symbol() + " " + StringSubstr(EnumToString(_Period), 7) + " - " + message + " at " + TimeToString(time)); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Delete RSI handle if(rsiHandle != INVALID_HANDLE) IndicatorRelease(rsiHandle); // Clean up chart objects CleanChartObjects(); }
Having thoroughly examined the RSI Divergence Detector's architecture and implementation, the complete source code is available attached below the article. With our technical indicator foundation established, we now progress to the second component of our trading system: developing the Equidistant Channel Auto-Placement Expert Advisor. This automated tool will complement our divergence detection by identifying structural price patterns, ultimately creating an integrated algorithmic trading solution. Following implementation, we will share comprehensive test results for both projects and establish a strategic roadmap for continued development and system integration.
Equidistant Channel Auto-Placement Expert Advisor
The Equidistant Channel Auto-Placement EA establishes a robust foundation through a carefully designed enumeration system and comprehensive user-configurable parameters. The ENUM_CHANNEL_TYPE enumeration provides clear semantic distinction between rising and falling channels, replacing traditional bullish/bearish terminology with more descriptive terms that align with trading setups: rising channels (higher lows) indicate sell opportunities, while falling channels (lower highs) suggest buy opportunities. This nomenclature shift represents a critical design decision that aligns channel terminology with actual trading behavior rather than abstract technical concepts.
Section 1: Architectural Foundation and User Configuration Framework
The input parameter system implements a sophisticated configuration interface that balances automation with user control. Each parameter serves a specific purpose in the channel detection algorithm: LookbackBars defines the historical data scope, SwingStrength controls sensitivity of swing point detection, while MinTouchesPerLine introduces the novel validation requirement of multiple price touches per channel line—a feature that significantly reduces false signals. The parameter TouchTolerancePips demonstrates attention to real-world trading conditions by accounting for minor price deviations that might otherwise invalidate genuine channel structures.
enum ENUM_CHANNEL_TYPE { CHANNEL_NONE, CHANNEL_RISING, // Higher lows - Sell setups (formerly bullish) CHANNEL_FALLING // Lower highs - Buy setups (formerly bearish) }; input bool EnableRisingChannels = true; input bool EnableFallingChannels = true; input int LookbackBars = 150; input int SwingStrength = 2; input bool ShowChannel = true; input color RisingChannelColor = clrRed; input color FallingChannelColor = clrLimeGreen; input int ChannelWidth = 1; input int MinChannelLengthBars = 15; input double MinChannelHeightPct = 0.5; input bool AlertOnNewChannel = true; input int MinTouchesPerLine = 2; input double TouchTolerancePips = 5.0; input int MaxExtensionBars = 50; input bool ExtendLeft = true; input bool ExtendRight = false;
Section 2: State Management and Initialization Architecture
The EA implements a sophisticated state management system through global variables that track channel persistence and alert timing, addressing critical issues from previous implementations. The lastAlertTime variable introduces intelligent alert throttling, preventing the continuous alert problem identified during testing. The channelStartBar and channelEndBar variables create a memory system that enables the EA to distinguish between new channel formations and existing channel persistence, a crucial feature for maintaining single-channel display behavior.
The initialization function demonstrates professional resource management practices by cleaning existing chart objects during startup. This proactive approach prevents object accumulation that could clutter the chart interface. The deinitialization function follows the principle of minimal intervention by leaving drawn channels intact for user analysis, while providing commented options for automatic cleanup—a design that balances automation with user control.
datetime lastBarTime = 0; string currentChannelName = ""; string channelPrefix = "SmartCh_"; bool channelFound = false; ENUM_CHANNEL_TYPE currentChannelType = CHANNEL_NONE; datetime lastAlertTime = 0; int channelStartBar = -1; int channelEndBar = -1; int OnInit() { DeleteAllChannels(); Print("Smart Single Channel EA initialized"); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { // Optional: Delete channel on exit // DeleteAllChannels(); }
Section 3: Intelligent Event-Driven Processing
The OnTick() function implements an optimized event-driven architecture that balances responsiveness with computational efficiency. The function establishes a new-bar detection mechanism using iTime() to ensure processing occurs only when meaningful price action updates are available, preventing wasteful computation during price consolidation. The innovative throttling logic introduces controlled processing frequency by checking channels only every third bar, a strategic design decision that dramatically reduces alert frequency while maintaining timely channel detection.
This throttling approach directly addresses the continuous alerting problem identified during testing, transforming the EA from a noisy, over-reactive system into a professional trading tool that provides signals only when meaningful channel developments occur. The modular architecture that delegates core processing to FindAndDrawSingleChannel() demonstrates adherence to the Single Responsibility Principle, separating event handling from business logic implementation.
void OnTick() { datetime currentBarTime = iTime(_Symbol, _Period, 0); if(currentBarTime <= lastBarTime) return; lastBarTime = currentBarTime; int barShift = iBarShift(_Symbol, _Period, currentBarTime); if(barShift % 3 != 0) return; FindAndDrawSingleChannel(); }
Section 4: Core Channel Detection and Validation Logic
The FindAndDrawSingleChannel() function implements the EA's sophisticated decision-making engine, coordinating multiple detection algorithms and applying advanced validation criteria. The function demonstrates a parallel processing approach that simultaneously evaluates both rising and falling channel possibilities, with conditional execution based on user preferences. This architecture enables comprehensive market analysis while maintaining computational efficiency through early termination of disabled detection paths.
The channel selection logic implements a multi-criteria decision algorithm that considers both recency and validation strength. The scoring system prioritizes channels based on three factors: proximity to current price, validation robustness, and minimum touch requirements. This multi-dimensional evaluation ensures only high-quality, well-validated channels receive display priority.
The new channel detection logic introduces sophisticated state comparison that prevents redundant alerts through four validation criteria: channel existence, type changes, structural shifts, and temporal cooldown. This comprehensive approach eliminates the continuous alerting problem while maintaining sensitivity to genuine market structure changes.
void FindAndDrawSingleChannel() { bool risingFound = false; int risePoint1 = -1, risePoint2 = -1; double riseSlope = 0; int riseTouchCount = 0; if(EnableRisingChannels) risingFound = FindRisingChannel(risePoint1, risePoint2, riseSlope, riseTouchCount); bool fallingFound = false; int fallPoint1 = -1, fallPoint2 = -1; double fallSlope = 0; int fallTouchCount = 0; if(EnableFallingChannels) fallingFound = FindFallingChannel(fallPoint1, fallPoint2, fallSlope, fallTouchCount); bool drawChannel = false; int point1 = -1, point2 = -1; double slope = 0; ENUM_CHANNEL_TYPE newType = CHANNEL_NONE; int touchCount = 0; if(risingFound && fallingFound) { if(risePoint1 > fallPoint1 || riseTouchCount > fallTouchCount + 1) { drawChannel = true; point1 = risePoint1; point2 = risePoint2; slope = riseSlope; newType = CHANNEL_RISING; touchCount = riseTouchCount; } else { drawChannel = true; point1 = fallPoint1; point2 = fallPoint2; slope = fallSlope; newType = CHANNEL_FALLING; touchCount = fallTouchCount; } } else if(risingFound && riseTouchCount >= MinTouchesPerLine * 2) { drawChannel = true; point1 = risePoint1; point2 = risePoint2; slope = riseSlope; newType = CHANNEL_RISING; touchCount = riseTouchCount; } else if(fallingFound && fallTouchCount >= MinTouchesPerLine * 2) { drawChannel = true; point1 = fallPoint1; point2 = fallPoint2; slope = fallSlope; newType = CHANNEL_FALLING; touchCount = fallTouchCount; } if(drawChannel && touchCount >= MinTouchesPerLine * 2) { bool isNewChannel = (!channelFound) || (currentChannelType != newType) || (MathAbs(point1 - channelStartBar) > 10) || (TimeCurrent() - lastAlertTime > 3600); if(isNewChannel) { DeleteAllChannels(); if(DrawChannel(point1, point2, slope, newType)) { channelFound = true; currentChannelType = newType; channelStartBar = point1; channelEndBar = point2; if(AlertOnNewChannel && (TimeCurrent() - lastAlertTime > 3600)) { string typeStr = (newType == CHANNEL_RISING) ? "Rising (Sell Setup)" : "Falling (Buy Setup)"; string message = StringFormat("%s channel detected on %s %s. %d touches confirmed.", typeStr, Symbol(), PeriodToString(_Period), touchCount); Alert(message); lastAlertTime = TimeCurrent(); } } } } else { if(channelFound && !IsChannelStillValid()) { DeleteAllChannels(); channelFound = false; currentChannelType = CHANNEL_NONE; } } }
Section 5: Advanced Channel Detection with Touch Validation
The FindRisingChannel() and FindFallingChannel() functions implement sophisticated pattern recognition algorithms that go beyond simple swing point detection. These functions employ a dual-loop architecture that evaluates all possible swing point combinations within the lookback period, applying multiple validation filters to identify genuine channel structures.
The validation criteria demonstrate professional attention to market microstructure by ensuring proper channel slope direction and enforcing minimum structural integrity through channel length requirements. The slope validation prevents detection of near-horizontal structures that lack meaningful trend information.
The innovative touch validation system implemented through CountChannelTouches() represents the EA's most significant advancement over traditional channel detectors. By requiring multiple price interactions with both channel boundaries, this system ensures detected channels have been tested and validated by market action rather than being mathematical artifacts. The scoring algorithm combines recency, touch frequency, and channel height into a composite score that objectively identifies the most significant channel structure.
bool FindRisingChannel(int &point1, int &point2, double &slope, int &touchCount) { int swingLows[]; FindSwingLows(swingLows, SwingStrength, LookbackBars); if(ArraySize(swingLows) < 2) return false; int bestPoint1 = -1, bestPoint2 = -1; double bestScore = -1; int bestTouches = 0; for(int i = 0; i < ArraySize(swingLows) - 1; i++) { for(int j = i + 1; j < ArraySize(swingLows); j++) { int low1 = swingLows[i]; int low2 = swingLows[j]; if(iLow(NULL, 0, low1) <= iLow(NULL, 0, low2)) continue; if(MathAbs(low1 - low2) < MinChannelLengthBars) continue; double low1Price = iLow(NULL, 0, low1); double low2Price = iLow(NULL, 0, low2); double barDiff = MathAbs(low1 - low2); slope = (low1Price - low2Price) / barDiff; if(slope < 0.00005) continue; double channelHeight = CalculateChannelHeight(low1, low2, true); int touches = CountChannelTouches(low1, low2, slope, low2Price, channelHeight, true); double recencyScore = 100.0 - (low1 * 100.0 / LookbackBars); double touchScore = touches * 25.0; double heightScore = (channelHeight / SymbolInfoDouble(_Symbol, SYMBOL_POINT)) / 100.0; double totalScore = recencyScore + touchScore + heightScore; if(totalScore > bestScore && touches >= MinTouchesPerLine * 2) { bestScore = totalScore; bestPoint1 = low1; bestPoint2 = low2; bestTouches = touches; } } } if(bestScore > 0) { point1 = bestPoint1; point2 = bestPoint2; slope = (iLow(NULL, 0, bestPoint1) - iLow(NULL, 0, bestPoint2)) / MathAbs(bestPoint1 - bestPoint2); touchCount = bestTouches; return true; } return false; }
Section 6: Mathematical Foundation for Channel Geometry
The CalculateChannelHeight() and CountChannelTouches() functions implement the mathematical core that transforms price data into validated channel structures. These functions demonstrate sophisticated algorithmic thinking by addressing the geometric challenges of channel detection in financial time series.
CalculateChannelHeight() implements a dual-purpose algorithm that performs empirical height calculation by measuring maximum price deviation from the calculated baseline, while enforcing minimum structural requirements through percentage-based thresholds. This approach balances empirical observation with theoretical requirements, ensuring channels have meaningful trading dimensions.
CountChannelTouches() introduces tolerance-based validation that accounts for real-world price behavior where exact line touches are rare. The tolerance calculation demonstrates professional attention to instrument-specific scaling, ensuring consistent behavior across different symbols and pip values. The dual-loop structure separately validates touches on both channel boundaries, providing detailed diagnostic information about channel integrity.
double CalculateChannelHeight(int point1, int point2, bool isRising) { double maxHeight = 0; int startBar = MathMin(point1, point2); int endBar = MathMax(point1, point2); double price1 = (isRising) ? iLow(NULL, 0, point1) : iHigh(NULL, 0, point1); double price2 = (isRising) ? iLow(NULL, 0, point2) : iHigh(NULL, 0, point2); double slope = (price1 - price2) / (point1 - point2); double intercept = price1 - slope * point1; for(int bar = startBar; bar <= endBar; bar++) { double currentPrice = (isRising) ? iHigh(NULL, 0, bar) : iLow(NULL, 0, bar); double baseLinePrice = slope * bar + intercept; double deviation = MathAbs(currentPrice - baseLinePrice); if(deviation > maxHeight) maxHeight = deviation; } double minHeight = (isRising) ? iLow(NULL, 0, startBar) * MinChannelHeightPct / 100.0 : iHigh(NULL, 0, startBar) * MinChannelHeightPct / 100.0; return MathMax(maxHeight, minHeight); } int CountChannelTouches(int point1, int point2, double slope, double basePrice, double height, bool isRising) { int touches = 0; int startBar = MathMin(point1, point2); int endBar = MathMax(point1, point2); double tolerance = TouchTolerancePips * SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 10; for(int bar = startBar; bar <= endBar; bar++) { double currentPrice = (isRising) ? iLow(NULL, 0, bar) : iHigh(NULL, 0, bar); double baseLinePrice = slope * (bar - point2) + basePrice; if(MathAbs(currentPrice - baseLinePrice) <= tolerance) touches++; } for(int bar = startBar; bar <= endBar; bar++) { double currentPrice = (isRising) ? iHigh(NULL, 0, bar) : iLow(NULL, 0, bar); double parallelLinePrice = slope * (bar - point2) + basePrice + (isRising ? height : -height); if(MathAbs(currentPrice - parallelLinePrice) <= tolerance) touches++; } return touches; }
Section 7: Swing Point Detection Engine
The FindSwingLows() and FindSwingHighs() functions implement robust swing point detection using a symmetrical validation approach that examines both left and right price action. These functions form the foundational layer upon which channel detection operates, requiring careful implementation to ensure reliable structural analysis.
Both functions employ identical algorithmic patterns with reversed comparison operators, demonstrating code reuse while maintaining logical clarity. The validation loops implement a rigorous "peak and trough" detection algorithm that requires a swing point to be higher (or lower) than a specified number of bars on both sides, controlled by the SwingStrength parameter. This bilateral verification ensures detected swing points represent genuine local extrema rather than temporary fluctuations.
The array management pattern demonstrates professional memory handling in MQL5, dynamically resizing arrays as valid swing points are identified. The final ArraySort() operations ensure chronological ordering from most recent to oldest, facilitating efficient subsequent processing in channel detection algorithms.
void FindSwingLows(int &swingPoints[], int strength, int lookback) { ArrayResize(swingPoints, 0); for(int i = strength; i < MathMin(lookback, Bars(NULL, 0) - strength); i++) { bool isSwingLow = true; double currentLow = iLow(NULL, 0, i); for(int left = 1; left <= strength && isSwingLow; left++) if(iLow(NULL, 0, i - left) < currentLow) isSwingLow = false; if(isSwingLow) for(int right = 1; right <= strength && isSwingLow; right++) if(iLow(NULL, 0, i + right) < currentLow) isSwingLow = false; if(isSwingLow) { int size = ArraySize(swingPoints); ArrayResize(swingPoints, size + 1); swingPoints[size] = i; } } ArraySort(swingPoints); }
Section 8: Professional Channel Visualization with Controlled Extension
The DrawChannel()function implements sophisticated graphical representation with intelligent extension management, directly addressing the excessive channel drawing issues identified in testing. The function employs a structured approach to channel rendering that balances visual clarity with chart space management.
The function demonstrates professional coordinate calculation that translates mathematical channel parameters into visual elements. The extension logic implements controlled boundary management: ExtendLeft provides historical context while ExtendRight is deliberately disabled to prevent channels from projecting excessively into empty chart space—a direct response to the testing feedback about channels being drawn "too far from current price."
The channel drawing implementation employs separate trend line objects for base and parallel lines rather than using MT5's built-in channel object. This architectural decision provides finer control over visual properties and extension behavior. The label placement logic demonstrates attention to visual hierarchy by positioning descriptive text at appropriate price levels relative to channel boundaries.
bool DrawChannel(int point1, int point2, double slope, ENUM_CHANNEL_TYPE type) { if(!ShowChannel) return false; DeleteAllChannels(); datetime time1 = iTime(NULL, 0, point1); datetime time2 = iTime(NULL, 0, point2); double price1, price2; color channelColor; string channelLabel; bool isRising = (type == CHANNEL_RISING); if(isRising) { price1 = iLow(NULL, 0, point1); price2 = iLow(NULL, 0, point2); channelColor = RisingChannelColor; channelLabel = "Rising Channel (Sell)"; } else { price1 = iHigh(NULL, 0, point1); price2 = iHigh(NULL, 0, point2); channelColor = FallingChannelColor; channelLabel = "Falling Channel (Buy)"; } double channelHeight = CalculateChannelHeight(point1, point2, isRising); int extensionBars = MathMin(MaxExtensionBars, MathAbs(point1 - point2) / 2); datetime extendedTime1 = time1; datetime extendedTime2 = time2; if(ExtendLeft) { int extendBack = MathMin(extensionBars, point2); extendedTime2 = iTime(NULL, 0, point2 - extendBack); } if(ExtendRight) { int extendForward = MathMin(extensionBars, Bars(NULL, 0) - point1 - 1); extendedTime1 = iTime(NULL, 0, point1 + extendForward); } currentChannelName = channelPrefix + "Base"; ObjectCreate(0, currentChannelName, OBJ_TREND, 0, extendedTime2, price2, time1, price1); ObjectSetInteger(0, currentChannelName, OBJPROP_COLOR, channelColor); ObjectSetInteger(0, currentChannelName, OBJPROP_WIDTH, ChannelWidth); ObjectSetInteger(0, currentChannelName, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, currentChannelName, OBJPROP_RAY_LEFT, ExtendLeft); ObjectSetInteger(0, currentChannelName, OBJPROP_BACK, true); string parallelName = channelPrefix + "Parallel"; double parallelPrice1 = price1 + (isRising ? channelHeight : -channelHeight); double parallelPrice2 = price2 + (isRising ? channelHeight : -channelHeight); ObjectCreate(0, parallelName, OBJ_TREND, 0, extendedTime2, parallelPrice2, time1, parallelPrice1); ObjectSetInteger(0, parallelName, OBJPROP_COLOR, channelColor); ObjectSetInteger(0, parallelName, OBJPROP_WIDTH, ChannelWidth); ObjectSetInteger(0, parallelName, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, parallelName, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, parallelName, OBJPROP_RAY_LEFT, ExtendLeft); ObjectSetInteger(0, parallelName, OBJPROP_BACK, true); string labelName = channelPrefix + "Label"; ObjectCreate(0, labelName, OBJ_TEXT, 0, time1, isRising ? price1 + channelHeight * 1.1 : price1 - channelHeight * 1.1); ObjectSetString(0, labelName, OBJPROP_TEXT, channelLabel); ObjectSetInteger(0, labelName, OBJPROP_COLOR, channelColor); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, labelName, OBJPROP_BACK, true); return true; }
Section 9: Channel Persistence Validation and Resource Management
The IsChannelStillValid()and DeleteAllChannels() functions implement critical system maintenance and validation logic that ensures the EA operates reliably during extended market sessions. These functions address two key operational concerns: channel relevance over time and resource management.
IsChannelStillValid() implements a simplified but effective channel break detection algorithm that monitors recent price action for significant deviations from established channel boundaries. The function uses percentage-based thresholds to identify potential channel breaks, providing a pragmatic balance between sensitivity and robustness. The function's conservative approach reduces false invalidation during normal price fluctuation while maintaining responsiveness to genuine structural breaks.
DeleteAllChannels() demonstrates professional object management through systematic traversal and deletion of chart objects with the EA's naming prefix. The reverse iteration pattern ensures safe object deletion during iteration, a critical detail when modifying collections during traversal. The prefix-based filtering prevents interference with other chart objects, demonstrating consideration for multi-tool trading environments.
bool IsChannelStillValid() { if(!channelFound) return false; int recentBars = 10; bool isRising = (currentChannelType == CHANNEL_RISING); for(int i = 0; i < recentBars; i++) { double high = iHigh(NULL, 0, i); double low = iLow(NULL, 0, i); if(isRising) { if(low > iLow(NULL, 0, channelStartBar) * 1.01) return false; } else { if(high < iHigh(NULL, 0, channelStartBar) * 0.99) return false; } } return true; } void DeleteAllChannels() { int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, channelPrefix) == 0) ObjectDelete(0, name); } currentChannelName = ""; }
Testing
Our testing methodology involved direct deployment and observation of the indicator's performance on live charts. The system architecture successfully separates the RSI oscillator into its own dedicated window while maintaining clear signal visualization across both the main price chart and indicator interface.
Upon receiving divergence alerts, traders can perform immediate structural analysis to identify supporting channel formations, creating a multi-factor decision framework for manual trade execution. This approach combines automated pattern detection with discretionary structural validation.
The comprehensive input parameter system allows for extensive customization, enabling users to optimize detection algorithms, adjust alert thresholds, and modify visual displays to align with their individual trading methodologies and market conditions.

Deploying the RSIDivergenceDetector
The screen capture below illustrates the Equidistant Channel Auto-Placement EA being tested in the Strategy Tester, demonstrating its effectiveness in accurately identifying and drawing channel structures. When used with the RSI Divergence Detector, trading signals can be validated against these structural formations, providing a multi-confirmation framework for higher-confidence execution decisions.

Testing the EquidistantChannel Auto-Placement EA in Strategy Tester
Conclusion
The confluence of RSI divergence signals with established market structure, particularly channel boundaries, creates a powerful framework for identifying higher-probability trading setups. In this comprehensive discussion, we have successfully automated two complementary technical analysis components: RSI divergence pattern detection and intelligent equidistant channel placement.
These tools operate synergistically on the same chart without conflict—the RSI Divergence Detector functions as a custom indicator, while the Equidistant Channel Auto-Placement operates as an Expert Advisor. Our modular development approach enabled focused implementation and testing of each component while maintaining clear separation of concerns.
While the current independent modules provide immediate utility, the natural progression involves developing a unified trading system that integrates these capabilities with automated execution logic—an exciting prospect for future development.
The detailed code explanations and implementation walkthroughs have provided practical insights into professional MQL5 programming, trading system architecture, and algorithmic validation techniques. Complete source files for both projects are available in the attachments for further study and customization.
We welcome continued discussion, questions, and constructive feedback in the comments section below.
Key Lessons
| Key Lessons | Description |
|---|---|
| 1. Modular System Architecture | Separate complex systems into independent, testable modules (indicator + EA) that can be integrated later. |
| 2. State Management for Alert Control | Implement time-based alert throttling (lastAlertTime) to prevent continuous alerts and notification fatigue. |
| 3. Validation-First Design Pattern | Require multiple price touches and confirmation breaks before signaling patterns, prioritizing reliability. |
| 4. Smart Object Lifecycle Management | Use unique naming prefixes and systematic cleanup (OnDeinit) to prevent chart object accumulation. |
| 5. Performance-Optimized Event Handling | Implement new-bar detection and processing throttling to balance responsiveness with computational efficiency. |
Attachments
| Source Filename | Version | Description |
|---|---|---|
| RSIDivergenceDetector.mq5 | 1.00 | Custom RSI divergence indicator that detects regular/hidden divergences, stores pivot values, and displays clear buy/sell arrows on the main chart with configurable alerts and RSI break confirmation. |
| EquidistantChannelAuto-Placement.mq5 | 1.00 | Intelligent Expert Advisor that automatically detects and draws a single valid equidistant channel with touch validation, controlled extensions, and smart alert throttling for rising (sell) and falling (buy) setups. |
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.
The View and Controller components for tables in the MQL5 MVC paradigm: Resizable elements
Introduction to MQL5 (Part 31): Mastering API and WebRequest Function in MQL5 (V)
Codex Pipelines, from Python to MQL5, for Indicator Selection: A Multi-Quarter Analysis of the XLF ETF with Machine Learning
Automating Trading Strategies in MQL5 (Part 46): Liquidity Sweep on Break of Structure (BoS)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use