preview
Developing Smart Chart Objects in MQL5 (Part 2): Automating Trendline Discovery and Lifecycle Management

Developing Smart Chart Objects in MQL5 (Part 2): Automating Trendline Discovery and Lifecycle Management

MetaTrader 5 — Examples |
270 2
Francis Nyoike Thumbi
Francis Nyoike Thumbi

Introduction

You already treat trendlines as “live” objects with states (touch, bounce, break), but adding automatic generation risks fragmenting that architecture: generators begin depending on state, managers on swings, and the chart fills with duplicates and orphaned objects. This part addresses that problem by introducing automatic trendline construction while preserving a single, consistent lifecycle. The Builder (CTrendlineBuilder) turns market structure into candidates; the Manager discovers and registers chart objects; and CManagedTrendline remains the single authority for lifecycle, confirmation, resurrection, expiration, and visual state. Success is measured by observable behavior: auto-generated lines appear, are discovered and registered, and then follow the same state machine, update ordering, and visual rules as manual lines—without generator-specific logic leaking into the lifecycle engine.

A trendline can now enter the framework in two ways. It can be drawn manually by the user, or it can be generated automatically from market structure. Although the entry paths are different, both eventually converge into the same management system. This distinction introduces two separate responsibilities. Registration and ownership handle the transition from a chart object to a managed object. Lifecycle management then takes over. It handles proximity, touches, bounces, breaks, resurrection, expiration, and the related state transitions.

The architecture can be viewed through a simple flow.

Fig. 1. Flow diagram

Fig. 1. Architectural flow diagram

The important principle is that creation and management remain separate. The component that creates a trendline should not also become responsible for managing its entire lifecycle. A manually drawn trendline and an automatically generated trendline may therefore follow different paths at the beginning. Once they reach registration, they converge into the same CManagedTrendline lifecycle.

This separation allows the automatic generation system to evolve without turning the lifecycle manager into a collection of generation-specific rules.

Revisiting the Managed Trendline Foundation

Before introducing automatic generation, it is useful to revisit the role of CManagedTrendline. The class remains the lifecycle authority for an individual trendline. Part 2 does not replace the model introduced in Part 1. It gives that model another source of objects.

The distinction is straightforward:

  • The builder creates candidates.
  • The managed trendline manages those candidates afterward.

The builder identifies and constructs a geometric relationship.

The managed trendline handles everything that happens after the chart object exists. This includes proximity, touches, bounce confirmation, breaks, resurrection, expiration, and visual state.

Keeping these responsibilities separate prevents generation logic from becoming part of the lifecycle engine.

//+------------------------------------------------------------------+
//| Encapsulates a single chart trendline object and its state.      |
//| Inherits from CObject to enable CArrayObj collection management. |
//+------------------------------------------------------------------+
class CManagedTrendline : public CObject
  {
private:
   //--- Object Identity
   string                m_name;                         // Chart object name
   long                  m_chart_id;                     // Target chart ID

   //--- Time & Price Coordinates
   datetime              m_time1;                        // Anchor 1 timestamp
   double                m_price1;                       // Anchor 1 price
   datetime              m_time2;                        // Anchor 2 timestamp
   double                m_price2;                       // Anchor 2 price

   //--- State & Tracking
   ENUM_TRENDLINE_STATE m_state;                         // Current lifecycle state
   int                  m_touch_count;                   // Total touches recorded
   int                  m_bounce_count;                  // Total confirmed bounces recorded
   datetime             m_last_touch_time;               // Timestamp of last touch
   datetime             m_break_time;                    // Timestamp of breakout
   bool                 m_was_recently_moved;            // Flag indicating direct user drag/modification
   bool                 m_needs_initial_proximity_check; // Line just entered management; initial live check pending

   //--- Pending Resolution Tracking
   int                  m_pending_close_count;           // Number of completed candles observed in pending state
   int                  m_break_close_count;             // Accumulated breakout confirmation closes
   int                  m_bounce_close_count;            // Accumulated bounce confirmation closes
   bool                 m_support_context;               // Locked interaction context (true = support, false = resistance)

   //--- Part 2: Post-Break Tracking (Resurrection & Expiration)
   int                  m_bars_since_break;              // Bars elapsed since breakout confirmation
   bool                 m_enable_resurrection;           // Enable reclamation check after break
   int                  m_resurrection_bars;             // Window of bars eligible for resurrection
   int                  m_expiration_bars;               // Window of bars before broken line expires
   int                  m_resurrection_count;            // Total resurrection events recorded

   //--- Settings & Volatility Thresholds
   double               m_proximity_pts;                 // Touch tolerance in points
   double               m_break_atr_mult;                // ATR multiplier for BROKEN confirmation
   int                  m_break_confirm_closes;          // Required closes for breakout confirmation
   double               m_bounce_atr_mult;               // ATR multiplier for BOUNCED confirmation
   int                  m_bounce_confirm_closes;         // Required closes for bounce confirmation
   double               m_touch_atr_mult;                // ATR multiplier for TOUCH_PENDING trigger

   //--- Part 2: Directional Trendline Colors
   color                m_bullish_color;                 // Active color for rising (bullish) trendlines
   color                m_bearish_color;                 // Active color for falling (bearish) trendlines

Expanding the Configuration Surface

Automatic generation and the expanded lifecycle introduce a broader configuration surface to the framework. The new inputs define the thresholds and behavioral limits used throughout the trendline lifecycle. They cover proximity detection, break confirmation, bounce confirmation, resurrection, expiration, and directional visualization.

//--- Directional Trendline Colors (Part 2)
input color InpBullishTrendlineColor = clrLime;           // Bullish trendline color (rising lines)
input color InpBearishTrendlineColor = clrBlue;           // Bearish trendline color (falling lines)

//--- Automatic Trendline Generation (Part 2)
input bool   InpEnableAutoTrendlines  = true;             // Master toggle for automatic trendline generation
input int    InpLookbackCandles       = 100;              // Recent historical bars to scan for swings
input int    InpSwingDepth            = 5;                // Bars on each side required for swing pivot
input int    InpMinTrendlineSwings    = 2;                // Minimum qualifying swings to form trendline
input int    InpMinBarsBetweenSwings  = 5;                // Minimum bars between qualifying swings
input int    InpSwingConfirmationBars = 3;                // Minimum bars elapsed to confirm swing
input bool   InpAutoBullishTrendlines = true;             // Enable automatic bullish trendlines (HL -> HL)
input bool   InpAutoBearishTrendlines = true;             // Enable automatic bearish trendlines (LH -> LH)

//--- Broken Trendline Lifecycle (Part 2)
input bool   InpEnableResurrection    = true;             // Enable resurrection on level reclaim
input int    InpResurrectionBars      = 10;               // Max bars after break eligible for resurrection
input int    InpExpirationBars        = 20;               // Max bars after break before line is purged

With these additions, the manager can now configure the complete set of conditions required to evaluate a trendline from its initial proximity through touch, bounce, break, possible resurrection, and eventual expiration.

The visual configuration also distinguishes bullish and bearish trendlines, allowing their directional character to be reflected directly on the chart.

The Trendline Builder

The first major addition is CTrendlineBuilder. Its role is to turn market structure into concrete trendline candidates. The builder scans the price structure and identifies relevant swing points. It then forms point relationships and validates whether they produce a meaningful directional line.

Once a candidate satisfies the required structural conditions, the builder constructs the corresponding chart trendline and assigns it the properties required for registration.

Its responsibilities can therefore be summarized as:

  • inspect available price structure;

  • identify relevant swing points;

  • establish valid point pairs;

  • validate their directional relationship;

  • construct valid trendline candidates;

  • create the corresponding chart objects.

The implementation is organized around these operations, with the builder retaining the structural information required to scan, validate, and construct candidates consistently.

//+------------------------------------------------------------------+
//| Automatic Trendline Builder Engine                               |
//| Discovers market swings and constructs candidate trendlines.     |
//+------------------------------------------------------------------+
class CTrendlineBuilder
  {
private:
   long              m_chart_id;
   string            m_prefix;

   //--- Structural Detection Parameters
   int               m_lookback_bars;
   int               m_depth;
   int               m_min_swings;
   int               m_min_bars_between;
   int               m_confirmation_bars;
   bool              m_auto_bullish;
   bool              m_auto_bearish;

   //--- Internal Swing Buffer
   BuilderSwingPt    m_swings[];

private:
   bool              IsSwingHigh(const int i, const double &high[], const int total_bars);
   bool              IsSwingLow(const int i, const double &low[], const int total_bars);
   int               ExtractSwings(const datetime &time[], const double &high[], const double &low[], const int total_bars);

   bool              BuildBullishTrendline(const datetime &time[]);
   bool              BuildBearishTrendline(const datetime &time[]);

   string            GenerateObjectName(const string dir, const datetime t1, const datetime t2);
   bool              CreateChartTrendline(const string name, const datetime t1, const double p1, const datetime t2, const double p2);

public:
                     CTrendlineBuilder(const string prefix = DEFAULT_TRENDLINE_PREFIX,
                     int lookback_bars = 100,
                     int depth = 5,
                     int min_swings = 2,
                     int min_bars_between = 5,
                     int confirmation_bars = 3,
                     bool auto_bullish = true,
                     bool auto_bearish = true,
                     long chart_id = 0);
                    ~CTrendlineBuilder();

   //--- Configuration Setter
   void              Configure(int lookback_bars, int depth, int min_swings, int min_bars_between,
                               int confirmation_bars, bool auto_bullish, bool auto_bearish);

   //--- Core Scan & Build Routine (Returns number of created lines)
   int               ScanAndBuild(const datetime &time[], const double &high[], const double &low[], const int rates_total);
  };

From Swing Points to Trendline Candidates

The builder works from swing information derived from the supplied market data, using those structural points as the basis for forming potential trendlines. The candidate-generation process follows a simple progression.

Relevant swing points are first identified within the configured lookback window.

Candidate point pairs are then formed from those swings and evaluated for a valid geometric relationship. The resulting relationship is checked for directional validity, with unsuitable combinations rejected before any chart object is constructed.

Only candidates that satisfy these structural conditions proceed to trendline construction.

//+------------------------------------------------------------------+
//| Bilateral Swing High Detection                                   |
//+------------------------------------------------------------------+
bool CTrendlineBuilder::IsSwingHigh(const int i, const double &high[], const int total_bars)
  {
   if(i - m_depth < 0 || i + m_depth >= total_bars)
      return false;

   for(int k = 1; k <= m_depth; k++)
     {
      if(high[i] <= high[i - k] || high[i] <= high[i + k])
         return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Bilateral Swing Low Detection                                    |
//+------------------------------------------------------------------+
bool CTrendlineBuilder::IsSwingLow(const int i, const double &low[], const int total_bars)
  {
   if(i - m_depth < 0 || i + m_depth >= total_bars)
      return false;

   for(int k = 1; k <= m_depth; k++)
     {
      if(low[i] >= low[i - k] || low[i] >= low[i + k])
         return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Scans lookback range and extracts classified swing structure     |
//+------------------------------------------------------------------+
int CTrendlineBuilder::ExtractSwings(const datetime &time[], const double &high[], const double &low[], const int total_bars)
  {
   ArrayFree(m_swings);
   BuilderSwingPt temp[];
   int scan_limit = MathMin(m_lookback_bars, total_bars - m_depth - 1);
   ArrayResize(temp, scan_limit);
   int count = 0;

   double last_high_price = 0.0;
   double last_low_price  = 0.0;
   bool has_high = false;
   bool has_low  = false;

   int start_bar = scan_limit;
   int end_bar   = MathMax(m_depth, m_confirmation_bars);

   for(int i = start_bar; i >= end_bar; i--)
     {
      if(IsSwingHigh(i, high, total_bars))
        {
         double price = high[i];
         string label = (!has_high || price > last_high_price) ? "HH" : "LH";
         last_high_price = price;
         has_high = true;

         temp[count].t         = time[i];
         temp[count].price     = price;
         temp[count].type      = 1;
         temp[count].label     = label;
         temp[count].bar_index = i;
         count++;
        }

      if(IsSwingLow(i, low, total_bars))
        {
         double price = low[i];
         string label = (!has_low || price > last_low_price) ? "HL" : "LL";
         last_low_price = price;
         has_low = true;

         temp[count].t         = time[i];
         temp[count].price     = price;
         temp[count].type      = -1;
         temp[count].label     = label;
         temp[count].bar_index = i;
         count++;
        }
     }

   if(count == 0)
      return 0;

   ArrayResize(m_swings, count);
   for(int s = 0; s < count; s++)
      m_swings[s] = temp[s];

   return count;
  }
Building Bullish Trendlines

A bullish candidate is built from a valid sequence of rising lows. The builder evaluates the selected swing points against the required structural distance and verifies that the second anchor establishes a higher low than the first. Once the relationship satisfies the bullish geometry, the builder uses the two anchors to construct the trendline and create the corresponding chart object.

//+------------------------------------------------------------------+
//| Identifies qualifying HL sequence and creates Bullish trendline  |
//+------------------------------------------------------------------+
bool CTrendlineBuilder::BuildBullishTrendline(const datetime &time[])
  {
   int total = ArraySize(m_swings);
   if(total < m_min_swings)
      return false;

   int hl_indices[];
   int hl_count = 0;

   for(int i = 0; i < total; i++)
     {
      if(m_swings[i].label == "HL")
        {
         ArrayResize(hl_indices, hl_count + 1);
         hl_indices[hl_count] = i;
         hl_count++;
        }
     }

   if(hl_count < m_min_swings)
      return false;

   int idx_anchor2 = hl_indices[hl_count - 1];
   int idx_anchor1 = hl_indices[hl_count - m_min_swings];

   BuilderSwingPt p1 = m_swings[idx_anchor1];
   BuilderSwingPt p2 = m_swings[idx_anchor2];

   if(MathAbs(p1.bar_index - p2.bar_index) < m_min_bars_between)
      return false;

   if(p2.price <= p1.price)
      return false;

   string line_name = GenerateObjectName("BULL", p1.t, p2.t);
   return CreateChartTrendline(line_name, p1.t, p1.price, p2.t, p2.price);
  }
Building Bearish Trendlines

The bearish path applies the corresponding geometry in the opposite direction. The selected swing points must form a valid sequence of lower highs, satisfy the required structural distance, and place the second anchor below the first. Once the relationship satisfies the bearish geometry, the builder constructs the trendline from those anchors and creates the corresponding chart object.

//+------------------------------------------------------------------+
//| Identifies qualifying LH sequence and creates Bearish trendline  |
//+------------------------------------------------------------------+
bool CTrendlineBuilder::BuildBearishTrendline(const datetime &time[])
  {
   int total = ArraySize(m_swings);
   if(total < m_min_swings)
      return false;

   int lh_indices[];
   int lh_count = 0;

   for(int i = 0; i < total; i++)
     {
      if(m_swings[i].label == "LH")
        {
         ArrayResize(lh_indices, lh_count + 1);
         lh_indices[lh_count] = i;
         lh_count++;
        }
     }

   if(lh_count < m_min_swings)
      return false;

   int idx_anchor2 = lh_indices[lh_count - 1];
   int idx_anchor1 = lh_indices[lh_count - m_min_swings];

   BuilderSwingPt p1 = m_swings[idx_anchor1];
   BuilderSwingPt p2 = m_swings[idx_anchor2];

   if(MathAbs(p1.bar_index - p2.bar_index) < m_min_bars_between)
      return false;

   if(p2.price >= p1.price)
      return false;

   string line_name = GenerateObjectName("BEAR", p1.t, p2.t);
   return CreateChartTrendline(line_name, p1.t, p1.price, p2.t, p2.price);
  }

The two construction paths therefore differ only in the structural relationship they validate: bullish candidates are formed from higher lows, while bearish candidates are formed from lower highs.

Creating the Chart Object

Once a candidate has passed structural validation, the builder converts it into an actual MetaTrader chart object.

The construction stage establishes the properties required by the chart representation:

  • object name;

  • anchor times;

  • anchor prices;

  • ray configuration;

  • initial visual properties;

  • successful object creation.

The builder also checks the existing chart state before creating a new object, preventing the same structural relationship from being recreated during subsequent scans.

At this point, the structural candidate has become a concrete trendline on the chart.

//+------------------------------------------------------------------+
//| Creates OBJ_TREND on chart if duplicate does not exist           |
//+------------------------------------------------------------------+
bool CTrendlineBuilder::CreateChartTrendline(const string name, const datetime t1, const double p1, const datetime t2, const double p2)
  {
   if(ObjectFind(m_chart_id, name) >= 0)
      return false;

   if(!ObjectCreate(m_chart_id, name, OBJ_TREND, 0, t1, p1, t2, p2))
      return false;

   ObjectSetInteger(m_chart_id, name, OBJPROP_RAY_RIGHT, true);
   ObjectSetInteger(m_chart_id, name, OBJPROP_BACK, true);
   ObjectSetInteger(m_chart_id, name, OBJPROP_WIDTH, TrendlineDefaults::DefaultWidth);
   ObjectSetInteger(m_chart_id, name, OBJPROP_COLOR, TrendlineDefaults::ActiveColor);
   ObjectSetInteger(m_chart_id, name, OBJPROP_SELECTABLE, true);

   PrintFormat("//--- [TrendlineBuilder] Created auto trendline: '%s' (P1: %.5f @ %s | P2: %.5f @ %s)",
               name, p1, TimeToString(t1), p2, TimeToString(t2));

   return true;
  }

Scanning and Building

ScanAndBuild() provides the operational entry point for automatic trendline generation. The manager supplies the current market data and structural context, while the builder performs the candidate scan and construction process.

The scan evaluates the available structure, processes qualifying candidates, and attempts to create the corresponding chart objects.

This provides a single entry point through which the automatic-generation process can be invoked.

//+------------------------------------------------------------------+
//| Master Execution Method                                          |
//+------------------------------------------------------------------+
int CTrendlineBuilder::ScanAndBuild(const datetime &time[], const double &high[], const double &low[], const int rates_total)
  {
   if(rates_total < m_lookback_bars || rates_total < (m_depth * 2 + 1))
      return 0;

   int swing_count = ExtractSwings(time, high, low, rates_total);
   if(swing_count < m_min_swings)
      return 0;

   int created = 0;

   if(m_auto_bullish && BuildBullishTrendline(time))
      created++;

   if(m_auto_bearish && BuildBearishTrendline(time))
      created++;

   return created;
  }

Discovering and Registering Trendlines

Creating a chart object does not automatically place it under the manager's control. The manager must first discover relevant trendline objects on the chart and then register those objects in its internal collection.

The transition is therefore:

Chart object → discovery → registration → managed object

DiscoverTrendlines() identifies trendlines that are not yet managed, while RegisterTrendline() establishes their CManagedTrendline representation and adds them to the lifecycle system. Registration also performs the initial evaluation when a trendline first enters the manager, which is critical for auto-generated lines that may be created near the current price.

The initial evaluation uses the existing live evaluation path rather than introducing a separate detection mechanism.

//+------------------------------------------------------------------+
//| Scans chart for objects matching prefix and registers them.      |
//+------------------------------------------------------------------+
int CTrendlineManager::DiscoverTrendlines()
  {
   int registered_count = 0;
   int total_objects = ObjectsTotal(m_chart_id, -1, OBJ_TREND);

   for(int i = total_objects - 1; i >= 0; i--)
     {
      string name = ObjectName(m_chart_id, i, -1, OBJ_TREND);

      //--- Prefix filter match
      if(StringFind(name, m_prefix) == 0)
        {
         if(!IsManaged(name))
           {
            if(RegisterTrendline(name))
               registered_count++;
           }
        }
     }

   if(registered_count > 0)
     {
      PrintFormat("//--- [SmartTrendline] Discovery complete: %d new trendlines registered.", registered_count);
     }

   return registered_count;
  }

//+------------------------------------------------------------------+
//| Registers a new trendline object into the collection.            |
//+------------------------------------------------------------------+
bool CTrendlineManager::RegisterTrendline(const string name)
  {
   if(IsManaged(name))
      return false;

   CManagedTrendline *new_line = new CManagedTrendline(name,
         m_chart_id,
         m_proximity_pts,
         m_break_atr_mult,
         m_break_confirm_closes,
         m_bounce_atr_mult,
         m_bounce_confirm_closes,
         m_enable_resurrection,
         m_resurrection_bars,
         m_expiration_bars,
         m_bullish_color,
         m_bearish_color);
   if(new_line == NULL)
      return false;

   if(m_lines.Add(new_line))
     {
      //--- Overwrite visual properties directly on managed chart object
      ObjectSetInteger(m_chart_id, name, OBJPROP_RAY_RIGHT, true);
      ObjectSetInteger(m_chart_id, name, OBJPROP_BACK, true);
      ObjectSetInteger(m_chart_id, name, OBJPROP_WIDTH, TrendlineDefaults::DefaultWidth);
      //--- Part 2: Directional color is owned by CManagedTrendline::UpdateVisualState() - not overwritten here

      //--- Immediately evaluate initial attachment proximity.
      //--- This is NOT a user drag and must not set m_was_recently_moved.
      new_line.EvaluateLiveDrag(false);

      PrintFormat("//--- [SmartTrendline] Registered trendline: '%s'", name);
      return true;
     }

   delete new_line;
   return false;
  }
Architectural Note: Swing Point Reuse

The current automatic trendline generation process does not permanently mark a swing point as consumed after it participates in a generated trendline. Consequently, the same swing point may participate in subsequent candidate pairs when the builder performs later structure scans, potentially resulting in multiple trendlines originating from the same market structure point.

This behavior is intentional within the current generation model and does not affect the lifecycle management described in this article. Controlling swing-point reuse and reducing redundant trendline candidates represents a separate refinement of the automatic generation layer rather than a responsibility of the trendline lifecycle manager.

One Management Path for Manual and Automatic Trendlines

Manual and automatic trendlines enter the framework through different paths. A manually created trendline is discovered and registered directly, while an automatically generated trendline passes through the builder before its chart object is discovered and registered.

After registration, however, the distinction disappears. Both become CManagedTrendline objects and enter the same lifecycle management path. This convergence keeps the origin of a trendline separate from its subsequent behavior, allowing automatic generation to evolve without fragmenting the management system.

The Central Update Cycle

CTrendlineManager::Update() is the orchestration point for the framework. Each update begins with the manager establishing the conditions required for processing the current market data. Immediate interactions are handled first. The normal lifecycle is processed only on closed bars.

The manager then updates the trendlines already under management before continuing with automatic generation, discovery, and registration of newly created objects. This ordering is important. Existing managed objects complete their current lifecycle processing before new candidates are introduced into the system.

//+------------------------------------------------------------------+
//| Orchestrates lifecycle updates across all managed trendlines.    |
//+------------------------------------------------------------------+
void CTrendlineManager::Update(const double &open[], const double &high[], const double &low[],
                               const double &close[], const datetime &time[], const double atr,
                               const int rates_total)
  {
   if(ArraySize(time) < 3)
      return;

//--- Enforce time-series indexing (Index 0 = current live bar, Index 1 = last closed bar)
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(time, true);

//--- 1. Live Drag Branch: process immediate touch evaluation if user moved lines
   bool has_moved_lines = false;
   int total = m_lines.Total();

   for(int i = 0; i < total; i++)
     {
      CManagedTrendline *line = (CManagedTrendline*)m_lines.At(i);

      if(line != NULL && line.WasRecentlyMoved())
        {
         has_moved_lines = true;
         line.EvaluateLiveDrag();
        }
     }

//--- 2. Closed-Bar Gate: skip unless a new bar completed
   bool is_new_bar = (time[1] != m_last_processed_bar);

   if(!is_new_bar && !has_moved_lines)
      return;

   if(is_new_bar)
     {
      m_last_processed_bar = time[1];

      //--- Standard closed-bar state machine processing
      for(int i = m_lines.Total() - 1; i >= 0; i--)
        {
         CManagedTrendline *line = (CManagedTrendline*)m_lines.At(i);

         if(line == NULL)
           {
            m_lines.Delete(i);
            continue;
           }

         line.Update(open, high, low, close, time, atr);

         //--- Expired lines removed their chart object internally; drop from collection
         if(line.IsExpired())
           {
            string name = line.GetName();
            m_lines.Delete(i);

            PrintFormat("[SmartTrendline] Expired line dropped from manager: '%s'.",
                        name);
           }
        }

      //--- 3. Automatic trendline generation on fresh bars
      if(m_builder_ready && m_builder != NULL)
        {
         int created = m_builder.ScanAndBuild(time, high, low, rates_total);

         //--- Immediately discover and register newly created automatic lines
         if(created > 0)
           {
            DiscoverTrendlines();
           }
        }
     }
  }

Immediate Interaction and Closed-Bar Processing

The update cycle operates across two timing contexts. Some interactions require an immediate response. A user moving a trendline should not have to wait for another completed candle before the line is evaluated. The same applies when a newly registered automatic trendline is first introduced into the manager and requires its initial proximity evaluation.

The normal lifecycle follows a different boundary. Touch confirmation, bounce confirmation, break confirmation, and the resulting state transitions depend on completed market data. The manager therefore uses the last processed closed bar to ensure that each completed candle enters the lifecycle evaluation once. The two paths work together without duplicating the evaluation logic:

Live interaction is immediate. Lifecycle progression is bar aware.

The live path continues to use the existing EvaluateLiveDrag() mechanism, while the closed-bar path proceeds through the managed trendline's normal update cycle.

//+------------------------------------------------------------------+
//| Immediate On-Tick Proximity Evaluation Routine.                  |
//| Handles both user-drag and initial attachment evaluation.        |
//+------------------------------------------------------------------+
void CManagedTrendline::EvaluateLiveDrag(const bool force_evaluation)
  {
   if(!force_evaluation && !m_was_recently_moved && !m_needs_initial_proximity_check)
      return;

//--- One-shot flag consumption: capture initial-check reason before clearing
   bool was_initial_check = m_needs_initial_proximity_check;

   m_was_recently_moved            = false;
   m_needs_initial_proximity_check = false;

//--- Only evaluate active lines
   if(m_state != TRENDLINE_STATE_ACTIVE)
      return;

   double live_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   Point2D p1, p2;
   p1.x = (double)m_time1;
   p1.y = m_price1;
   p2.x = (double)m_time2;
   p2.y = m_price2;

   double line_price = CGeometry::GetYAtX(p1, p2, (double)TimeCurrent());

   if(line_price == 0.0)
      return;

   double proximity_margin = m_proximity_pts * _Point;

//--- Check if live bid is within proximity threshold
   if(MathAbs(live_bid - line_price) <= proximity_margin)
     {
      ENUM_TRENDLINE_STATE previous_state = m_state;

      m_touch_count++;
      m_last_touch_time = TimeCurrent();
      m_pending_close_count = 0;
      m_break_close_count = 0;
      m_bounce_close_count = 0;
      m_support_context = (live_bid >= line_price);
      m_state            = TRENDLINE_STATE_TOUCH_PENDING;

      //--- Distinguish initial attachment evaluation from genuine user drag
      if(force_evaluation || was_initial_check)
        {
         PrintFormat("//--- [SmartTrendline] Object '%s' Initial Proximity Touch: %s -> %s | Total Touches: %d",
                     m_name, EnumToString(previous_state), EnumToString(m_state), m_touch_count);
        }
      else
        {
         PrintFormat("//--- [SmartTrendline] Object '%s' Live Drag Touch Intercept: %s -> %s | Total Touches: %d",
                     m_name, EnumToString(previous_state), EnumToString(m_state), m_touch_count);
        }

      UpdateVisualState();
     }
  }

Updating the Managed Trendline

Once the manager reaches the normal lifecycle stage, responsibility moves to the individual CManagedTrendline.

CManagedTrendline::Update() implements the object-level lifecycle evaluation. It refreshes the trendline properties, evaluates the current relationship between price and the line, processes the conditions associated with its current state, updates confirmation information, performs state transitions, and reconciles the visual representation.

This is where the lifecycle becomes concrete. A proximity event can begin a touch interaction. Continued evidence can confirm a bounce, while sufficient penetration and confirmation can establish a break. A broken relationship can subsequently be reconsidered during its configured reclaim window, while expiration provides the final lifetime boundary.

The manager therefore remains the coordinator of the update cycle, while the managed trendline carries the state-dependent evaluation.

//+------------------------------------------------------------------+
//| Core State Machine & Pending Resolution Engine Routine.          |
//+------------------------------------------------------------------+
bool CManagedTrendline::Update(const double &open[], const double &high[], const double &low[], const double &close[], const datetime &time[], const double atr)
  {
   if(ArraySize(close) < 3)
      return false;

//--- Evaluate latest closed bar (Bar 1)
   int bar_idx = 1;
   double expected_price = GetPriceAtBarIndex(bar_idx, time);
   if(expected_price == 0.0)
      return false;

   double current_close = close[bar_idx];

//--- Signed Difference: Positive = Above line (Support context), Negative = Below line (Resistance context)
   double signed_diff  = current_close - expected_price;
   double dist_to_line = MathAbs(signed_diff);

   ENUM_TRENDLINE_STATE previous_state = m_state;

//--- Convert point tolerance input to price units
   double proximity_price = m_proximity_pts * _Point;

//--- STATE 1: ACTIVE -> Shift to TOUCH_PENDING on Volatility/Proximity
   if(m_state == TRENDLINE_STATE_ACTIVE)
     {
      //--- Calculate dynamic touch threshold (ATR multiplier + static point proximity)
      double touch_threshold = (atr * m_touch_atr_mult) + proximity_price;

      if(dist_to_line <= touch_threshold ||
         MathAbs(high[bar_idx] - expected_price) <= touch_threshold ||
         MathAbs(low[bar_idx] - expected_price) <= touch_threshold)
        {
         if(m_last_touch_time != time[bar_idx])
           {
            m_touch_count++;
            m_last_touch_time = time[bar_idx];
            m_pending_close_count = 0;
            m_break_close_count = 0;
            m_bounce_close_count = 0;
            m_support_context = (current_close >= expected_price);
            m_state            = TRENDLINE_STATE_TOUCH_PENDING;
           }
        }
     }
//--- STATE 2: TOUCH_PENDING -> Deterministic Completed-Candle Resolution
   else
      if(m_state == TRENDLINE_STATE_TOUCH_PENDING)
        {
         m_pending_close_count++;

         double break_threshold  = (atr * m_break_atr_mult) + proximity_price;
         double bounce_threshold = (atr * m_bounce_atr_mult) + proximity_price;
         // --- SUPPORT INTERACTION CONTEXT ---
         if(m_support_context)
           {
            if(signed_diff <= -break_threshold)
              {
               m_break_close_count++;
               m_bounce_close_count = 0;
              }
            else
               if(signed_diff >= bounce_threshold)
                 {
                  m_bounce_close_count++;
                  m_break_close_count = 0;
                 }
               else
                 {
                  m_break_close_count = 0;
                  m_bounce_close_count = 0;
                 }
           }
         // --- RESISTANCE INTERACTION CONTEXT ---
         else
           {
            if(signed_diff >= break_threshold)
              {
               m_break_close_count++;
               m_bounce_close_count = 0;
              }
            else
               if(signed_diff <= -bounce_threshold)
                 {
                  m_bounce_close_count++;
                  m_break_close_count = 0;
                 }
               else
                 {
                  m_break_close_count = 0;
                  m_bounce_close_count = 0;
                 }
           }

         PrintFormat("//--- [SmartTrendline] '%s' Pending | Candles=%d | Break=%d | Bounce=%d",
                     m_name, m_pending_close_count, m_break_close_count, m_bounce_close_count);

         if(m_break_close_count >= m_break_confirm_closes)
           {
            m_state = TRENDLINE_STATE_BROKEN;
            m_break_time = time[bar_idx];
            m_bars_since_break = 0; // Reset post-break elapsed bar counter (Part 2)
           }
         else
            if(m_bounce_close_count >= m_bounce_confirm_closes)
              {
               m_state = TRENDLINE_STATE_BOUNCED;
              }
        }
      //--- STATE 3: BOUNCED -> Record bounce count telemetry and reset to ACTIVE for subsequent retests
      else
         if(m_state == TRENDLINE_STATE_BOUNCED)
           {
            m_bounce_count++;
            PrintFormat("//--- [SmartTrendline] Object '%s' Bounce Confirmed! Total Bounces: %d", m_name, m_bounce_count);
            m_state = TRENDLINE_STATE_ACTIVE;
           }
         //--- STATE 4: BROKEN -> Evaluate post-break resurrection or timeout expiration (Part 2)
         else
            if(m_state == TRENDLINE_STATE_BROKEN)
              {
               m_bars_since_break++;

               //--- 4A: Check Resurrection / Reclamation condition
               if(m_enable_resurrection && m_bars_since_break <= m_resurrection_bars)
                 {
                  bool reclaimed = false;

                  //--- If broken as support, price reclaiming above line indicates resurrection
                  if(m_support_context && signed_diff > 0.0)
                     reclaimed = true;
                  //--- If broken as resistance, price reclaiming below line indicates resurrection
                  else
                     if(!m_support_context && signed_diff < 0.0)
                        reclaimed = true;

                  if(reclaimed)
                    {
                     m_state = TRENDLINE_STATE_ACTIVE;
                     m_bars_since_break = 0;
                     m_resurrection_count++;
                     PrintFormat("[SmartTrendline] Line '%s' RESURRECTED after %d bars | Total Resurrections: %d",
                                 m_name, m_bars_since_break, m_resurrection_count);
                    }
                 }

               //--- 4B: Check Expiration condition if not resurrected -> purge chart object
               if(m_state == TRENDLINE_STATE_BROKEN && m_bars_since_break >= m_expiration_bars)
                 {
                  m_state = TRENDLINE_STATE_EXPIRED;
                  if(ObjectDelete(m_chart_id, m_name))
                    {
                     PrintFormat("[SmartTrendline] Line '%s' EXPIRED after %d bars post-break - chart object purged.",
                                 m_name, m_bars_since_break);
                    }
                  else
                    {
                     PrintFormat("[SmartTrendline] Line '%s' EXPIRED after %d bars - object delete failed (err=%d).",
                                 m_name, m_bars_since_break, GetLastError());
                    }
                 }
              }

//--- Handle state transition log & visual update
   if(m_state != previous_state)
     {
      PrintFormat("//--- [SmartTrendline] Object '%s' State Changed: %s -> %s | Total Touches: %d | Total Bounces: %d",
                  m_name, EnumToString(previous_state), EnumToString(m_state), m_touch_count, m_bounce_count);

      UpdateVisualState();
     }

   return true;
  }

The Lifecycle Progression

With the managed update cycle in place, automatically generated trendlines follow the same states already established for manually created trendlines. A newly registered trendline begins in the active state. Interaction with price can move it into touch-pending, where subsequent market evidence determines whether the interaction becomes a bounce or develops into a break.

A broken trendline may remain eligible for reclamation during the configured resurrection window. If the required reclaim conditions are met, it can return to the active lifecycle. If not, the trendline eventually reaches its configured expiration boundary.

The important distinction is between the market relationship and the lifetime of the object.

A break describes what price has done relative to the trendline. Resurrection determines whether that broken relationship can be recovered. Expiration determines whether the managed object has remained relevant for too long.

This gives the lifecycle a complete progression from active interaction through confirmation, failure, possible recovery, and eventual retirement.

Cleaning Up Orphaned Trendline Objects

Automatic generation introduces another ownership concern. The manager maintains its own collection of managed trendlines, while MetaTrader 5 maintains the corresponding chart objects separately. These two representations must therefore remain synchronized.

A managed trendline can leave the lifecycle while its chart object remains on the chart. Likewise, a chart object can remain without a corresponding managed representation.

The result is an orphaned chart object.

To curb this, we introduce explicit cleanup and reconciliation to remove chart objects that no longer have a legitimate relationship with the manager.

The distinction keeps lifecycle retirement and chart-object ownership as separate concerns.

//+------------------------------------------------------------------+
//| Full teardown used by the destructor / deinit. Auto lines are    |
//| always deleted from the chart. Manual lines are deleted only if  |
//| wipe_manual is true AND they match the scoped prefix (InpPrefix).|
//+------------------------------------------------------------------+
void CTrendlineManager::ReleaseAllManaged(const bool wipe_manual)
  {
//--- First release/delete every object currently tracked by manager
   for(int i = m_lines.Total() - 1; i >= 0; i--)
     {
      CManagedTrendline *line = (CManagedTrendline*)m_lines.At(i);

      if(line == NULL)
        {
         m_lines.Delete(i);
         continue;
        }

      string name = line.GetName();

      //--- During full teardown, delete every tracked line.
      //--- During partial cleanup, only auto-generated lines are deleted.
      bool is_auto = line.IsAutoGenerated();

      if(wipe_manual || is_auto)
        {
         if(ObjectFind(m_chart_id, name) >= 0)
           {
            if(ObjectDelete(m_chart_id, name))
              {
               PrintFormat("[SmartTrendline] Cleanup removed managed object: '%s'.",
                           name);
              }
            else
              {
               PrintFormat("[SmartTrendline] Cleanup FAILED to remove managed object: '%s' | Error=%d",
                           name,
                           GetLastError());
              }
           }
        }
      else
        {
         PrintFormat("[SmartTrendline] Cleanup released tracking only: '%s'.",
                     name);
        }

      //--- Free CManagedTrendline object
      //--- m_lines uses FreeMode(true)
      m_lines.Delete(i);
     }

//--- Sweep the actual chart as the final authority.
   if(wipe_manual)
     {
      int total_objects = ObjectsTotal(m_chart_id, -1, OBJ_TREND);

      for(int i = total_objects - 1; i >= 0; i--)
        {
         string name = ObjectName(m_chart_id, i, -1, OBJ_TREND);

         //--- Only remove objects belonging to this manager's scope.
         if(StringFind(name, m_prefix) != 0)
            continue;

         if(ObjectDelete(m_chart_id, name))
           {
            PrintFormat("[SmartTrendline] Cleanup sweep removed chart object: '%s'.",
                        name);
           }
         else
           {
            PrintFormat("[SmartTrendline] Cleanup sweep FAILED: '%s' | Error=%d",
                        name,
                        GetLastError());
           }
        }
     }

   ChartRedraw(m_chart_id);
  }

Direction-Aware Visualization

Automatic generation also extends the managed visual representation with directional awareness. The direction of a trendline is derived directly from its geometry. A rising relationship is treated as bullish, while a falling relationship is treated as bearish. This determination is independent of how the trendline entered the framework.

UpdateVisualState() remains the rendering authority for the managed trendline. It combines the trendline's geometric direction with its current lifecycle state to determine the appropriate color, style, and width.

This keeps rendering centralized and ensures that directional appearance and lifecycle appearance are reconciled through the same visual update path.

//+------------------------------------------------------------------+
//| Updates line visual appearance based on internal state.          |
//+------------------------------------------------------------------+
void CManagedTrendline::UpdateVisualState()
  {
//--- Part 2: Determine active trendline color from geometric direction
   Point2D p1, p2;
   p1.x = (double)m_time1;
   p1.y = m_price1;
   p2.x = (double)m_time2;
   p2.y = m_price2;

   double slope = CGeometry::CalculateSlope(p1, p2);

//--- Rising = bullish, falling = bearish, flat = neutral default color
   color active_color = TrendlineDefaults::ActiveColor;
   if(slope > 0.0)
      active_color = m_bullish_color;
   else
      if(slope < 0.0)
         active_color = m_bearish_color;

   color target_color = active_color;
   ENUM_LINE_STYLE target_style = TrendlineDefaults::ActiveStyle;
   int target_width = TrendlineDefaults::DefaultWidth;

   switch(m_state)
     {
      case TRENDLINE_STATE_ACTIVE:
      case TRENDLINE_STATE_BOUNCED:
         target_color = active_color;
         target_style = TrendlineDefaults::ActiveStyle;
         break;

      case TRENDLINE_STATE_TOUCH_PENDING:
         target_color = TrendlineDefaults::TouchedColor;
         target_style = TrendlineDefaults::ActiveStyle;
         target_width = TrendlineDefaults::DefaultWidth + 1;
         break;

      case TRENDLINE_STATE_BROKEN:
         target_color = TrendlineDefaults::BrokenColor;
         target_style = TrendlineDefaults::BrokenStyle;
         break;

      case TRENDLINE_STATE_RETESTED:
         target_color = TrendlineDefaults::RetestedColor;
         target_style = TrendlineDefaults::ActiveStyle;
         break;

      default:
         break;
     }

//--- Apply visual modifications to chart object
   ObjectSetInteger(m_chart_id, m_name, OBJPROP_COLOR, target_color);
   ObjectSetInteger(m_chart_id, m_name, OBJPROP_STYLE, target_style);
   ObjectSetInteger(m_chart_id, m_name, OBJPROP_WIDTH, target_width);
   ChartRedraw(m_chart_id);
  }

Operational Walkthrough

After attaching the trendline management system, these were the resulting behaviors for both automatically detected and manually drawn trendlines.

Automatic Trendline

Fig. 2. Automatically Generated Trendlines

The builder identifies the qualifying market structure and creates the trendline automatically. The resulting object is then discovered and registered by the manager, allowing it to enter the same managed lifecycle as any other trendline.

Manual Trendline

Fig. 3.

Fig. 3. Manual Trendline Registration and Framework Interpretation

The manually drawn trendline is detected and registered by the framework immediately. Its original chart appearance is then replaced by the managed visual state, with the line interpreted from its geometry and rendered according to its bullish or bearish direction.

Lifecycle Output

Fig. 4. Action message

Fig. 4. Minimal Runtime Telemetry of the Trendline Lifecycle

The runtime printouts trace the lifecycle: object registration and state transitions during execution.

Conclusion

With the trendline lifecycle now fully managed, both automatically generated and manually drawn trendlines can be discovered, registered, interpreted, and tracked through a consistent state-driven framework. The result is a reusable management layer that separates trendline lifecycle handling from the logic responsible for creating or drawing the objects themselves.

This establishes the foundation for the next stage of the framework. In the following phase, the managed trendline states will become actionable through alert integration, allowing significant lifecycle events such as touches, bounces, and breaks to be communicated as they occur.

The framework can therefore move beyond simply managing what happens to a trendline and begin communicating when something important happens to it.

File Name
Description
SmartTrendlineManager2.mq5
The main indicator entry point. It connects MetaTrader 5 events and market data with the internal framework by creating the trendline manager, forwarding chart interactions, and passing market updates for lifecycle evaluation.
TrendlineManager.mqh
Implements the orchestration layer of the framework. It discovers existing trendlines, registers managed entities, handles chart events, maintains the collection of active trendlines, and coordinates lifecycle updates across multiple objects.
ManagedTrendline.mqh
Implements the behavior of an individual managed trendline. It encapsulates trendline geometry, lifecycle states, interaction tracking, confirmation logic, and visual state updates while remaining independent from the manager.
Common.mqh
Contains shared definitions used throughout the framework, including common constants, enumerations, default configuration values, and supporting declarations required by multiple components.
Geometry.mqh
Provides the geometric calculations required by the framework, including trendline projection and price evaluation at specific chart positions. This module remains independent from lifecycle logic to keep calculations reusable and isolated.
 TrendlineBuilder.mqh Implements the automatic trendline generation layer. It analyzes market structure to identify qualifying trendline candidates, constructs the corresponding chart objects, and supplies them to the management layer for discovery and lifecycle tracking.
 MQL5.zip An archive containing the complete MQL5 folder structure. Extract it into the MetaTrader 5 terminal directory so that SmartTrendlineManager2.mq5 is placed under MQL5\Indicators\SmartTrendlineManager2\ and all supporting .mqh files are placed under MQL5\Include\SmartTrendline2\.
Attached files |
MQL5.zip (19.15 KB)
Last comments | Go to discussion (2)
Avery Horton
Avery Horton | 7 Oct 2026 at 10:03
How do YOU define touch, bounce and break?  Thank you.
Francis Nyoike Thumbi
Francis Nyoike Thumbi | 7 Oct 2026 at 11:47
Avery Horton #:
How do YOU define touch, bounce and break?  Thank you.
 A touch occurs when price enters the configured ATR/proximity threshold around the trendline. A bounce is confirmed after the required consecutive closes move away from the line by the bounce threshold, while a break requires consecutive closes beyond the line by the break threshold. Part 1 covers the lifecycle logic and implementation.
Swarm Optimizer with Hierarchical Sub-Flocks — Flock by Leader Swarm Optimizer with Hierarchical Sub-Flocks — Flock by Leader
We are developing and implementing the Flock by Leader algorithm in MQL5: sub-flocks are formed based on the ARF metric, and the leader is determined by the highest personal best rather than by the centroid's position. We present the update formulas for the swarm roles and the separation mechanism. The C_AO_FBL class is compatible with the test bench and has been tested on the Hilly, Forest, and Megacity functions with dimensionalities from 10 to 1000 coordinates, which simplifies reproduction and comparison.
Meta-Labeling the Classics (Part 4): Filtering and Sizing MACD Trades Meta-Labeling the Classics (Part 4): Filtering and Sizing MACD Trades
MACD signal-line crossovers often reflect range noise rather than true momentum shifts, producing whipsaws. We apply a two-layer meta-labeling pipeline with an Optuna-optimized regime gate to filter entries on EURUSD H1, turning a gross-losing rule into a positive but not statistically significant track. A secondary Random Forest adds no lift due to too few gated samples, clarifying when filtering helps and when the ML layer is data-starved.
Monthly Profit and Loss Calendar Heatmap Renderer in MQL5 Monthly Profit and Loss Calendar Heatmap Renderer in MQL5
The article presents an MQL5 script that reads closed deals, groups them by date, and renders a calendar grid for daily P&L and trade count. It explains the data model, normalization to midnight, robust week/weekday mapping, diverging and single-hue color scales, and CCanvas-based drawing, plus a verification script and weekday summary. This helps you quickly locate clusters and quiet periods and cross-check findings with logged totals.
Consecutive Loss Streak Analyzer and Risk-of-Ruin Calculator in MQL5 Consecutive Loss Streak Analyzer and Risk-of-Ruin Calculator in MQL5
This article presents an MQL5 script that extracts closed trade history, computes empirical win rate and payoff, and evaluates consecutive-loss probabilities using the geometric tail, plus risk of ruin from edge and position size. It plots a CCanvas ruin curve with a live marker at the chosen risk and prints a probability summary, including the account's worst historical streak in theoretical context.