preview
How to Detect and Normalize Chart Objects in MQL5 (Part 4): Fully Automated Analytical Objects System

How to Detect and Normalize Chart Objects in MQL5 (Part 4): Fully Automated Analytical Objects System

MetaTrader 5Examples |
86 0
Clemence Benjamin
Clemence Benjamin

Contents

  1. The Need for Automatic Object Placement
  2. Adding an Automatic Placement Layer
  3. Step 1 – Swing Detection Utility (SwingDetector.mqh)
  4. Step 2 – Object Placer Utility (ObjectPlacer.mqh)
  5. Step 3 – Topology Manager (TopologyManager.mqh)
  6. Step 4 – Adaptive Trade Execution (AdaptiveTrade.mqh)
  7. Step 5 – Creating the Main Expert Advisor (AutoObjectTradingSystem.mq5)
  8. Testing and Validation
  9. Conclusion
  10. Attachments and Installation Summary


The Need for Automatic Object Placement

In the previous parts of this series, we built a robust pipeline to detect, normalize, and interact with manually drawn chart objects. Part 1 provided a base detector that enumerates all analytical objects on a chart. Part 2 extended it with complex data extraction for Fibonacci levels, channels, and pitchforks. Part 3 added an interaction layer, an alert manager, and a trade executor, turning static drawings into dynamic signals. However, the entire system still depends on the trader to manually draw every object. The EA can react to a pitchfork, but it cannot draw one itself when it identifies a suitable swing pattern.

Manual drawing is time‑consuming and inconsistent. A trader may spot a swing pattern and draw a pitchfork, but if they are away from the screen, the opportunity is missed. Moreover, the same trader might draw slightly different pitchforks at different times, leading to inconsistent signals. To overcome these limitations, we need the ability to programmatically create analytical objects based on market structure. This part of the series introduces automatic object placement: the EA will scan the chart for swing points and automatically draw pitchforks, trendlines, and support/resistance lines. These objects are then immediately monitored by the same interaction pipeline, generating alerts and trades without any human intervention.

We will also address the latency introduced by timer‑based polling. In Part 3, interactions are evaluated every two seconds. If an object is created or modified by the EA, it would not be detected until the next polling cycle. To solve this, the modular system uses a topology manager to refresh objects on demand. This ensures immediate responsiveness when an object is created, deleted, or modified. The combination of automatic placement and event‑driven updates makes the system fully autonomous and responsive.


Adding an Automatic Placement Layer

As the system grows, maintaining a single monolithic codebase becomes increasingly difficult. To address this, we transition to a modular architecture that separates concerns into distinct, reusable components. This approach not only improves code organization but also makes future enhancements significantly easier to implement.

The updated pipeline now consists of six core modules, organized into a clean modular architecture:

  1. Swing detection – CSwingDetector scans the chart and identifies swing highs and lows.
  2. Object placement – CObjectPlacer uses the swing list to draw pitchforks, trendlines, support/resistance, Fibonacci, and channels.
  3. Market data caching – CMarketDataCache efficiently stores price data to avoid redundant API calls.
  4. Signal evaluation – CSignalEvaluator analyzes price interactions with each analytical object.
  5. Topology management – CTopologyManager orchestrates object scanning, placement, and signal processing.
  6. Adaptive trade execution – ExecuteAdaptiveTrade() places orders with dynamic stop levels that work for any instrument.

This modular design means every object drawn by the EA is treated exactly like a manually drawn object. The trader can step away and let the EA find structures, draw them, and trade their interactions automatically. Each module is self-contained and can be replaced or extended independently.

The architecture separates concerns into clear layers:

  • Common/ – Shared structures and global variables
  • Helpers/ – Utility functions for math and object creation
  • Core/ – Fundamental detection, placement, and evaluation logic
  • Engine/ – Orchestration and management layer
  • Execution/ – Trade execution with adaptive stops


Step 1 – Swing Detection Utility (SwingDetector.mqh)

Create a new file SwingDetector.mqh inside MQL5/Include/ChartObjectsAlgorithms-Part4/Core/. This file provides a static class CSwingDetector with two public methods that check whether a given bar forms a swing point. The SSwingPoint structure is defined in Common/Structures.mqh for reuse across modules.

The swing detection algorithm is simple: a bar is considered a swing high if its high is strictly higher than the highs of the surrounding MinSwingBars bars on both sides. The same logic applies for swing lows. This approach works well on clean charts and can be extended with more sophisticated methods in the future.

//+------------------------------------------------------------------+
//|                                                 SwingDetector.mqh |
//|                               Copyright 2026, Clemence Benjamin |
//|                                            https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"

#include "../Common/Structures.mqh"
#include "../Helpers/MathHelpers.mqh"

//+------------------------------------------------------------------+
//| Class: CSwingDetector                                           |
//| Purpose: Detects swing highs and lows on the chart              |
//+------------------------------------------------------------------+
class CSwingDetector
  {
public:
//--- Checks if a bar forms a swing high
   static bool IsSwingHigh(int bar, int minBars = 3);
   
//--- Checks if a bar forms a swing low
   static bool IsSwingLow(int bar, int minBars = 3);
   
//--- Finds all swing points within a lookback range
   static void FindSwings(SSwingPoint &swings[], int lookback, int minBars);
  };

Understanding the Swing Detection Logic

The CSwingDetector class uses a simple but effective algorithm to identify swing points. The IsSwingHigh method checks if a bar's high price is greater than the highs of minBars bars on both sides. For example, with minBars = 3, a swing high must be higher than the previous 3 bars and the next 3 bars. Similarly, IsSwingLow checks if a bar's low is lower than surrounding bars.

The implementation uses cached price data from MathHelpers.mqh for optimal performance. Instead of calling iHigh() and iLow() for every bar in the lookback window (which would be very slow), the system preloads price data into memory arrays and reuses them for all swing detection checks.

The FindSwings method scans bars from (lookback - 1) down to minBars and stores swing points in an array. Each swing point stores its bar index, time, price, and type (high or low). This array serves as the foundation for all pattern recognition in the system.

//+------------------------------------------------------------------+
//| Checks if bar forms a swing high                                 |
//+------------------------------------------------------------------+
bool CSwingDetector::IsSwingHigh(int bar, int minBars)
  {
//--- Refresh price cache for speed
   RefreshPriceCache();
   if(bar >= ArraySize(g_highCache))
      return(false);
   
   for(int j = 1; j <= minBars; j++)
     {
      if(bar + j >= ArraySize(g_highCache) || bar - j < 0)
         return(false);
      if(g_highCache[bar] <= g_highCache[bar + j])
         return(false);
      if(g_highCache[bar] <= g_highCache[bar - j])
         return(false);
     }
   return(true);
  }

Implementation Details of IsSwingHigh

The IsSwingHigh method first refreshes the price cache to ensure it has the latest data. It then checks whether the bar index is within the cached array bounds. The method loops through j = 1 to minBars, comparing the high of the current bar with the highs of bars at bar+j (future) and bar-j (past). If any surrounding bar has a higher or equal high, the current bar is not a swing high. The method returns true only if all surrounding bars are lower.

The IsSwingLow method follows the same logic but uses low prices instead of highs. Both methods rely on the cached price arrays g_highCache and g_lowCache, which are loaded once per bar and reused for all swing detection checks. This approach is significantly faster than calling iHigh() and iLow() for each comparison.


Step 2 – Object Placer Utility (ObjectPlacer.mqh)

The CObjectPlacer class uses the swing detection to automatically draw analytical objects. It provides methods to place trendlines, support/resistance, Fibonacci, channels, and pitchforks. All drawings are created through helper functions in ObjectHelpers.mqh, ensuring consistency and reuse.

The file ObjectPlacer.mqh is placed in MQL5/Include/ChartObjectsAlgorithms-Part4/Core/.

//+------------------------------------------------------------------+
//|                                                ObjectPlacer.mqh |
//|                               Copyright 2026, Clemence Benjamin |
//|                                            https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"

#include "../Common/Structures.mqh"
#include "../Helpers/ObjectHelpers.mqh"
#include "SwingDetector.mqh"

//+------------------------------------------------------------------+
//| Class: CObjectPlacer                                            |
//| Purpose: Automatically places analytical objects on the chart    |
//+------------------------------------------------------------------+
class CObjectPlacer
  {
private:
   long              m_chartId;      // chart identifier
   string            m_prefix;       // object name prefix
   int               m_maxObjects;   // maximum objects per type
   bool              m_debug;        // debug mode flag
   int               m_maxTotal;     // maximum total objects

public:
//--- Constructor
                    CObjectPlacer(long chartId, string prefix, int maxObjects,
                                  int maxTotal, bool debug);

//--- Places all enabled object types
   void              PlaceAll(int lookback, int minBars,
                              bool placeTrendlines, bool placeSR,
                              bool placeFibonacci, bool placeChannels,
                              bool placePitchforks);
   
//--- Cleans up old objects
   void              Cleanup();

private:
//--- Places trendlines connecting swing points
   void              PlaceTrendlines(int lookback, int minBars);
   
//--- Places support/resistance horizontal lines
   void              PlaceSupportResistance(int lookback, int minBars);
   
//--- Places Fibonacci retracements between swing points
   void              PlaceFibonacci(int lookback, int minBars);
   
//--- Places channels using three swing points
   void              PlaceChannels(int lookback, int minBars);
   
//--- Places pitchforks using three swing points
   void              PlacePitchforks(int lookback, int minBars);
  };

Understanding the Object Placer Architecture

The CObjectPlacer class is responsible for all automatic object placement. It maintains the chart ID, a naming prefix (so objects can be identified as auto-generated), and limits on the number of objects. The constructor initializes these values when the class is created.

The PlaceAll method is the main entry point. It checks whether the total object count is below the maximum, then calls each individual placement method based on the user's settings. This allows the trader to enable or disable specific object types. The Cleanup method removes old objects to prevent chart clutter.

Each placement method uses CSwingDetector::FindSwings to get swing points and then calls the appropriate creation function from ObjectHelpers.mqh. The methods are designed to place only the most significant objects by limiting the count to m_maxObjects.

//+------------------------------------------------------------------+
//| Places support/resistance horizontal lines                       |
//+------------------------------------------------------------------+
void CObjectPlacer::PlaceSupportResistance(int lookback, int minBars)
  {
//--- Get swing points
   SSwingPoint swings[];
   CSwingDetector::FindSwings(swings, lookback, minBars);
   
   if(ArraySize(swings) < 1)
      return;
   
   int count = 0;
   for(int i = 0; i < ArraySize(swings) && count < m_maxObjects; i++)
     {
      if(swings[i].isHigh)
        {
         string name = m_prefix + "Res_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(count);
         if(CreateHorizontalLineObject(m_chartId, name, swings[i].price, clrRed))
            count++;
        }
      else
        {
         string name = m_prefix + "Sup_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(count);
         if(CreateHorizontalLineObject(m_chartId, name, swings[i].price, clrGreen))
            count++;
        }
     }
  }

Implementation Details of Support/Resistance Placement

The PlaceSupportResistance method creates horizontal support and resistance levels at each swing point. It first obtains the list of swing points using CSwingDetector::FindSwings. If no swing points are found, the method returns early.

The method then iterates through the swing points, creating a resistance level (red line) at each swing high and a support level (green line) at each swing low. Each object receives a unique name containing a timestamp to prevent conflicts. The count variable ensures we don't exceed the maximum number of SR levels per type.

The horizontal lines are created using CreateHorizontalLineObject from ObjectHelpers.mqh, which handles the actual object creation with consistent styling. This separation ensures that styling changes only need to be made in one place.


Step 3 – Topology Manager (TopologyManager.mqh)

The CTopologyManager is the central orchestrator that binds all modules together. It manages the object placer, market data cache, and signal evaluators. This class handles the complete workflow:

  1. Place objects using CObjectPlacer
  2. Refresh the object list and create evaluators
  3. Process price interactions and generate signals

The file TopologyManager.mqh is placed in MQL5/Include/ChartObjectsAlgorithms-Part4/Engine/.

//+------------------------------------------------------------------+
//|                                             TopologyManager.mqh |
//|                               Copyright 2026, Clemence Benjamin |
//|                                            https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"

#include "../Common/Structures.mqh"
#include "../Core/MarketCache.mqh"
#include "../Core/ObjectPlacer.mqh"
#include "../Core/SignalEvaluator.mqh"

//+------------------------------------------------------------------+
//| Class: CTopologyManager                                          |
//| Purpose: Orchestrates object placement, scanning, and signals    |
//+------------------------------------------------------------------+
class CTopologyManager
  {
private:
   long                     m_chartId;          // chart identifier
   CSignalEvaluator*       m_evaluators[];     // signal evaluators
   CMarketDataCache*        m_marketCache;      // market data cache
   CObjectPlacer*           m_objectPlacer;     // object placer
   datetime                 m_lastScan;         // last scan timestamp
   datetime                 m_lastPlacement;    // last placement timestamp
   int                      m_scanInterval;     // scan interval in seconds
   int                      m_placementInterval; // placement interval in seconds
   double                   m_touchThreshold;   // touch threshold in pips
   double                   m_slMultiplier;     // stop loss multiplier
   double                   m_tpMultiplier;     // take profit multiplier
   int                      m_evaluatorCount;   // number of active evaluators

public:
//--- Constructor
                    CTopologyManager(long chartId, int scanInterval, int placementInterval,
                                     double threshold, double slMult, double tpMult,
                                     int maxObjects, int maxTotal, bool debug);
                    
//--- Destructor
                   ~CTopologyManager();

//--- Refreshes object list and creates evaluators
   void              RefreshObjects();

//--- Places objects based on swing detection
   void              PlaceObjects(int lookback, int minBars,
                                  bool placeTrendlines, bool placeSR,
                                  bool placeFibonacci, bool placeChannels,
                                  bool placePitchforks);

//--- Processes signals from all evaluators
   int               ProcessSignals(TradeSignal &signals[], int maxSignals);

//--- Prints topology statistics
   void              PrintStatistics();

private:
//--- Checks if object type is analytical
   bool              IsAnalyticalObject(ENUM_OBJECT type);
   
//--- Clears all evaluators
   void              ClearEvaluators();
  };

Understanding the Topology Manager

The CTopologyManager is the brain of the system. It orchestrates all other modules, coordinating their activities in the correct sequence. It maintains arrays of evaluators for all detected objects, and manages the lifecycle of objects and evaluators.

The constructor creates the market data cache and object placer. The destructor ensures all evaluators are properly deleted to prevent memory leaks. The RefreshObjects method scans the chart and creates evaluators for all analytical objects. It first checks whether enough time has elapsed since the last scan (throttled by m_scanInterval) to prevent excessive processing.

For each object on the chart, it checks if the object is analytical (trendline, horizontal line, Fibonacci, channel, or pitchfork). If so, it creates a CSignalEvaluator instance for that object and stores it in the evaluators array. The evaluator will later be used to detect price interactions with that specific object.

The PlaceObjects method throttles object placement to avoid excessive updates. It calls the object placer's Cleanup method to remove old objects, then PlaceAll to create new ones. The placement interval prevents the EA from redrawing objects too frequently, which would cause flickering.

The ProcessSignals method processes all evaluators and generates trading signals. It first refreshes the market data cache, then iterates through all evaluators. Each evaluator's AnalyzeSignal method is called with the current market snapshot. If a signal is generated and its confidence is above the minimum threshold (0.50), it's added to the output array.


Step 4 – Adaptive Trade Execution (AdaptiveTrade.mqh)

The ExecuteAdaptiveTrade function provides intelligent order placement that adapts to any instrument. It automatically calculates minimum stop distances based on the instrument's point value, spread, and user-defined multipliers. This ensures that the "Invalid stops" error is prevented on instruments like Boom 500 Index, Gold, Crypto, and Forex.

The file AdaptiveTrade.mqh is placed in MQL5/Include/ChartObjectsAlgorithms-Part4/Execution/.

//+------------------------------------------------------------------+
//|                                               AdaptiveTrade.mqh |
//|                               Copyright 2026, Clemence Benjamin |
//|                                            https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"

#include "../Common/Structures.mqh"
#include "../Common/Globals.mqh"
#include <Trade/Trade.mqh>

//+------------------------------------------------------------------+
//| Executes adaptive trade - works for any instrument               |
//+------------------------------------------------------------------+
bool ExecuteAdaptiveTrade(TradeSignal &signal, double lotSize, int minStopPoints,
                         double slMultiplier, double tpMultiplier,
                         CTrade &trade, datetime &lastTradeTime, int tradeCooldown,
                         bool debug)
  {
//--- Get instrument properties
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / point;
   
//--- Get current market prices
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   
//--- Calculate minimum stop distance
   int stopPoints = MathMax(minStopPoints, 50);
   
//--- Increase for Boom/Crash indices
   if(StringFind(_Symbol, "Boom") >= 0 || StringFind(_Symbol, "Crash") >= 0)
     {
      stopPoints = MathMax(stopPoints, 150);
     }
   
   double minStopDistance = stopPoints * point;
   minStopDistance += spread * point * 2;
   
//--- Determine entry price
   bool isBuy = (signal.orderType == ORDER_TYPE_BUY);
   double entry = isBuy ? ask : bid;
   
//--- Calculate SL and TP
   double sl = 0;
   double tp = 0;
   
   if(isBuy)
     {
      sl = entry - minStopDistance * slMultiplier;
      tp = entry + minStopDistance * tpMultiplier;
      
      if(sl >= entry)
         sl = entry - minStopDistance * 2;
      if(tp <= entry)
         tp = entry + minStopDistance * 3;
     }
   else
     {
      sl = entry + minStopDistance * slMultiplier;
      tp = entry - minStopDistance * tpMultiplier;
      
      if(sl <= entry)
         sl = entry + minStopDistance * 2;
      if(tp >= entry)
         tp = entry - minStopDistance * 3;
     }
   
//--- Ensure valid prices
   if(sl <= 0)
      sl = entry * 0.95;
   if(tp <= 0)
      tp = entry * 1.05;
   
//--- Debug logging
   if(debug)
     {
      Print("=== ADAPTIVE STOPS ===");
      PrintFormat("Symbol: %s, Point: %.5f", _Symbol, point);
      PrintFormat("Min Stop Points: %d, Min Distance: %.5f", stopPoints, minStopDistance);
      PrintFormat("Entry: %.5f, SL: %.5f, TP: %.5f", entry, sl, tp);
      PrintFormat("SL Dist: %.0f pts, TP Dist: %.0f pts",
                  MathAbs(entry - sl) / point, MathAbs(entry - tp) / point);
      Print("=======================");
     }
   
//--- Place order
   bool result = false;
   string comment = signal.reason;
   
   if(isBuy)
     {
      result = trade.Buy(lotSize, _Symbol, entry, sl, tp, comment);
     }
   else
     {
      result = trade.Sell(lotSize, _Symbol, entry, sl, tp, comment);
     }
   
   if(result)
     {
      lastTradeTime = TimeCurrent();
      if(debug)
         Print("Trade executed. Ticket: ", trade.ResultOrder());
     }
   else
     {
      if(debug)
        {
         Print("Trade failed: ", trade.ResultRetcodeDescription());
         Print("Error code: ", trade.ResultRetcode());
        }
     }
   
   return(result);
  }

Understanding Adaptive Trade Execution

The ExecuteAdaptiveTrade function is the key to making the system work on any instrument. It handles the critical task of placing orders with valid stop loss and take profit levels, adapting to each instrument's unique properties.

The function first retrieves the instrument's point value and calculates the current spread. The point value is the smallest price movement for the instrument (e.g., 0.00001 for EURUSD, 0.001 for USDJPY). The spread is measured in points. These values are essential for calculating valid stop and take profit levels.

The minimum stop distance is calculated based on minStopPoints and the point value. For Boom and Crash indices, it forces a larger minimum distance (150 points) because these instruments require wider stops due to their volatility. The spread is also added to ensure the stop is outside the spread.

For BUY orders, the stop loss is placed below the entry price, and the take profit is placed above. For SELL orders, the opposite applies. The multipliers (slMultiplier and tpMultiplier) control how far the stop and take profit are placed from entry.

The function includes safety checks to ensure the stop loss is always on the correct side of the entry and far enough away. If the calculated stop loss is too close or on the wrong side, it's automatically adjusted. Finally, the function places the order using the CTrade object. If successful, it updates the last trade timestamp. If it fails, it logs the error for debugging.


Step 5 – Creating the Main Expert Advisor (AutoObjectTradingSystem.mq5)

The main Expert Advisor combines all modules and demonstrates the complete workflow. To keep the EA organized and separate from standard MQL5 files, we place it in a dedicated subfolder: MQL5/Experts/ChartObjectsAlgorithms-Part4/.

The EA is broken down into several logical sections, each handling a specific responsibility.

Section 1 – Property Directives and Include Statements

The EA begins with standard property directives that define copyright, version, and compilation behavior. The include statements reference all modular files from the ChartObjectsAlgorithms-Part4 library using relative paths. This ensures the EA has access to all the core modules we built in the previous steps.

//+------------------------------------------------------------------+
//|                                     AutoObjectTradingSystem.mq5 |
//|                               Copyright 2026, Clemence Benjamin |
//|                                            https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Clemence Benjamin"
#property link      "https://www.mql5.com"
#property version   "5.00"
#property strict

//--- Include all modular files from the ChartObjectsAlgorithms-Part4 library
#include <Trade/Trade.mqh>
#include "../Include/ChartObjectsAlgorithms-Part4/Common/Structures.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Common/Globals.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Helpers/MathHelpers.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Helpers/ObjectHelpers.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Core/SwingDetector.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Core/ObjectPlacer.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Core/MarketCache.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Core/SignalEvaluator.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Engine/TopologyManager.mqh"
#include "../Include/ChartObjectsAlgorithms-Part4/Execution/AdaptiveTrade.mqh"

Section 2 – Input Parameters

Input parameters are organized into logical groups: risk management, object placement, trading settings, adaptive stops, and performance. Each input includes a descriptive comment that explains its purpose, making configuration intuitive without requiring source code analysis. The groups help users quickly find and adjust relevant settings.

//--- Input parameters
input group "--- Risk Management ---"
input double   InpLotSize           = 0.1;      // Lot size
input ulong    InpMagicNumber       = 888888;   // Magic number
input int      InpSlippage          = 10;       // Slippage in points

input group "--- Object Placement ---"
input int      InpScanInterval      = 10;       // Scan interval in seconds
input int      InpSwingLookback     = 50;       // Bars to scan for swings
input int      InpMinSwingBars      = 3;        // Bars on each side of swing
input bool     InpPlaceTrendlines   = true;     // Auto-place trendlines
input bool     InpPlaceSR           = true;     // Auto-place support/resistance
input bool     InpPlaceFibonacci    = true;     // Auto-place Fibonacci
input bool     InpPlaceChannels     = true;     // Auto-place channels
input bool     InpPlacePitchforks   = true;     // Auto-place pitchforks
input int      InpMaxObjectsPerType = 2;        // Max objects per type
input int      InpMaxObjectsTotal   = 15;       // Maximum total objects on chart

input group "--- Trading Settings ---"
input bool     InpEnableTrading     = true;     // Enable auto trading
input double   InpTouchThreshold    = 15.0;     // Touch threshold in pips
input int      InpMinConfidence     = 50;       // Minimum confidence (0-100)

input group "--- Adaptive Stops ---"
input double   InpSLMultiplier      = 3.0;      // SL distance multiplier
input double   InpTPMultiplier      = 5.0;      // TP distance multiplier
input int      InpMinStopPoints     = 100;      // Minimum stop distance in points

input group "--- Performance Settings ---"
input bool     InpEnableDebug       = false;    // Enable debug logging
input int      InpPlacementInterval = 30;       // Place objects every N seconds
input int      InpTradeCooldown     = 5;        // Seconds between trades

Section 3 – Global Objects and Variables

Global objects are declared to persist throughout the EA's lifetime. The CTrade object handles order execution, the CTopologyManager orchestrates the entire system, and the m_signals array stores detected trade signals. The m_lastTradeTime variable enforces a cooldown period between trades to prevent over-trading, which is essential for risk management.

//--- Global objects
CTrade                     m_trade;
CTopologyManager*          m_topologyManager = NULL;
TradeSignal                m_signals[];
datetime                   m_lastTradeTime = 0;

Section 4 – OnInit() – System Initialization

The OnInit function sets up the entire trading system. It first initializes cached values like the point value and digits, then caches the EMA values for trend detection. The trade object is configured with the magic number and slippage. The topology manager is created and configured with all parameters. Finally, initial objects are placed, a scan is performed, and a statistics summary is printed to the Experts log.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize cached values
   g_point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   g_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   
//--- Initialize EMA cache
   GetEMA(20);
   GetEMA(50);
   
   m_trade.SetExpertMagicNumber(InpMagicNumber);
   m_trade.SetDeviationInPoints(InpSlippage);
   
   m_topologyManager = new CTopologyManager(
      ChartID(),
      InpScanInterval,
      InpPlacementInterval,
      InpTouchThreshold,
      InpSLMultiplier,
      InpTPMultiplier,
      InpMaxObjectsPerType,
      InpMaxObjectsTotal,
      InpEnableDebug
   );
   
   if(CheckPointer(m_topologyManager) == POINTER_INVALID)
     {
      Print("Failed to initialize topology manager");
      return(INIT_FAILED);
     }
   
//--- Initial placement
   m_topologyManager.PlaceObjects(InpSwingLookback, InpMinSwingBars,
                              InpPlaceTrendlines, InpPlaceSR,
                              InpPlaceFibonacci, InpPlaceChannels,
                              InpPlacePitchforks);
   m_topologyManager.RefreshObjects();
   m_topologyManager.PrintStatistics();
   
   Print("=== Auto Object Trading System v5.00 (MODULAR) ===");
   PrintFormat("Symbol: %s", _Symbol);
   PrintFormat("Point: %.5f, Digits: %d", g_point, g_digits);
   PrintFormat("Lot size: %.2f", InpLotSize);
   PrintFormat("Touch threshold: %.1f pips", InpTouchThreshold);
   PrintFormat("Min stop points: %d", InpMinStopPoints);
   PrintFormat("Scan interval: %d seconds", InpScanInterval);
   PrintFormat("Placement interval: %d seconds", InpPlacementInterval);
   PrintFormat("Max total objects: %d", InpMaxObjectsTotal);
   Print("===================================================");
   
   return(INIT_SUCCEEDED);
  }

Section 5 – OnDeinit() – Cleanup

When the EA is removed from the chart, OnDeinit releases all resources. It prints final statistics and deletes the topology manager to prevent memory leaks. Proper cleanup is essential in MQL5 to avoid memory issues, especially during extended testing sessions.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(CheckPointer(m_topologyManager) == POINTER_DYNAMIC)
     {
      m_topologyManager.PrintStatistics();
      delete m_topologyManager;
     }
   
   Print("Auto Object Trading System deinitialized");
  }

Section 6 – OnChartEvent() – Real-time Object Updates

The OnChartEvent function monitors chart events for object changes. When a user creates, deletes, or modifies an analytical object, the topology manager refreshes its object list. This ensures the system always works with the latest chart state without requiring a full scan on every tick. This event-driven approach eliminates the latency problem identified in Part 3.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CREATE ||
      id == CHARTEVENT_OBJECT_DELETE ||
      id == CHARTEVENT_OBJECT_CHANGE)
     {
      if(CheckPointer(m_topologyManager) != POINTER_INVALID)
         m_topologyManager.RefreshObjects();
     }
  }

Section 7 – OnTick() – Main Execution Loop

The OnTick function is the heart of the EA. It follows a clear sequence of steps: placing objects based on swing detection, refreshing the object list, checking for existing positions, enforcing trade cooldown, and processing signals. When valid signals are found, they are sorted by confidence and the best one is executed using the adaptive trade function. This throttled approach balances performance with responsiveness.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(CheckPointer(m_topologyManager) == POINTER_INVALID)
      return;
   
//--- Place objects
   m_topologyManager.PlaceObjects(InpSwingLookback, InpMinSwingBars,
                              InpPlaceTrendlines, InpPlaceSR,
                              InpPlaceFibonacci, InpPlaceChannels,
                              InpPlacePitchforks);
   
//--- Refresh object list
   m_topologyManager.RefreshObjects();
   
//--- Check for existing positions
   if(PositionsTotal() > 0)
      return;
   
//--- Cooldown between trades
   if(TimeCurrent() - m_lastTradeTime < InpTradeCooldown)
      return;
   
//--- Process signals
   TradeSignal signals[];
   int signalCount = m_topologyManager.ProcessSignals(signals, 5);
   
   if(signalCount > 0 && InpEnableTrading)
     {
      //--- Sort by confidence
      int sortCount = MathMin(signalCount, 3);
      for(int i = 0; i < sortCount - 1; i++)
        {
         for(int j = i + 1; j < sortCount; j++)
           {
            if(signals[i].confidence < signals[j].confidence)
              {
               TradeSignal temp = signals[i];
               signals[i] = signals[j];
               signals[j] = temp;
              }
           }
        }
      
      TradeSignal signal = signals[0];
      if(signal.isValid && signal.confidence >= (double)InpMinConfidence / 100.0)
        {
         if(InpEnableDebug)
           {
            Print("=== SIGNAL GENERATED ===");
            PrintFormat("Object: %s | Type: %s", signal.objectName, ObjectTypeToString(signal.objectType));
            PrintFormat("Order: %s | Conf: %.0f%%",
                        (signal.orderType == ORDER_TYPE_BUY) ? "BUY" : "SELL",
                        signal.confidence * 100);
            PrintFormat("Entry: %.5f, SL: %.5f, TP: %.5f",
                        signal.entryPrice, signal.stopLoss, signal.takeProfit);
            Print("=========================");
           }
         
         //--- Execute with adaptive stops
         ExecuteAdaptiveTrade(signal, InpLotSize, InpMinStopPoints,
                              InpSLMultiplier, InpTPMultiplier,
                              m_trade, m_lastTradeTime, InpTradeCooldown,
                              InpEnableDebug);
        }
     }
  }

Detailed Walkthrough of the OnTick Execution Flow

When the OnTick function runs, it first checks whether the topology manager is valid. If not, it exits immediately to prevent errors. The EA then calls PlaceObjects, which analyzes the current swing structure and updates the chart with fresh analytical objects. This step ensures that the chart always reflects the most recent market structure, even as new swings form.

After placing objects, the EA refreshes the object list through RefreshObjects. This step scans the chart for analytical objects (manual and auto-generated) and creates a signal evaluator for each one. The evaluators are stored in memory and reused until the next scan, providing a balance between responsiveness and performance.

The EA then checks whether any positions are already open. If a position exists, the EA exits immediately to avoid placing multiple concurrent trades. This simple check prevents over-trading and ensures that only one trade is active at any time. The trade cooldown check adds another layer of protection by enforcing a minimum time interval between orders.

Finally, the EA processes signals from all evaluators. It collects up to five signals, sorts them by confidence, and selects the highest-confidence signal for execution. The ExecuteAdaptiveTrade function handles the actual order placement, calculating appropriate stop loss and take profit levels based on the instrument's properties. This entire sequence runs on every tick but is throttled by the scan and placement intervals to conserve CPU resources.


Testing and Validation

Compile all files and attach AutoObjectTradingSystem to a chart. The EA will automatically draw objects based on swing patterns detected on the chart.

With trading enabled, the EA will:

  1. Scan for swing points using CSwingDetector
  2. Place objects using CObjectPlacer
  3. Monitor interactions using CSignalEvaluator
  4. Execute trades using ExecuteAdaptiveTrade

The adaptive trade execution ensures that the system works on any instrument – Forex, Gold, Indices, Crypto, or Boom/Crash indices.

Strategy Tester

Fig. 1. Strategy Tester settings.

AutoObjectTradingSystem

Fig. 2. Trendlines and AutoSR levels drawn by the EA on EURUSD H1.


Conclusion

We have closed the loop between manual and automated chart analysis with a clean, modular architecture. The EA can now discover market structure on its own, draw the appropriate analytical objects, and then use the same interaction pipeline to generate alerts and trades.

The modular design offers several key benefits:

  • Separation of concerns – Each module has a single, well-defined responsibility
  • Code reuse – Modules can be used in other projects
  • Easier maintenance – Changes are isolated to specific modules
  • Better testing – Each module can be tested independently
  • Adaptability – The system works on any instrument
  • Organized structure – Files are placed in logical folders for easy navigation

The topology manager ensures immediate responsiveness, while the auto‑placer leverages the existing normalization and trading infrastructure without any modification. This allows traders to mix manual and automatic drawings seamlessly, turning the entire chart into a dynamic, algorithmically monitored workspace.

Find the attached source code below. Happy trading, until our next article.


Attachments and Installation Summary

The following files are provided with this article. All files are organized in the modular structure described earlier under MQL5/Include/ChartObjectsAlgorithms-Part4/ and MQL5/Experts/ChartObjectsAlgorithms-Part4/.

Important: This modular system represents an advanced, well-organized approach to MQL5 development. It is completely self-contained and improves upon the earlier architecture by separating concerns into logical, reusable components.


File Name Type Description
Structures.mqh Include Core data structures: SSwingPoint, MarketSnapshot, and TradeSignal.
Globals.mqh Include Cached global values for performance optimization (point, digits, EMAs, price cache).
MathHelpers.mqh Include Utility functions: GetEMA, GetLineValueAtTime, RefreshPriceCache, and ObjectTypeToString.
ObjectHelpers.mqh Include Object creation functions and CleanupObjectsWithPrefix for managing chart objects.
SwingDetector.mqh Include CSwingDetector class for identifying swing highs and lows on the chart.
ObjectPlacer.mqh Include CObjectPlacer class for automatic placement of trendlines, SR, Fibonacci, channels, and pitchforks.
MarketCache.mqh Include CMarketDataCache class for efficient price data caching.
SignalEvaluator.mqh Include CSignalEvaluator class for analyzing price interactions with analytical objects.
TopologyManager.mqh Include CTopologyManager class – orchestrates object placement, scanning, and signal processing.
AdaptiveTrade.mqh Include ExecuteAdaptiveTrade function for order placement that works on any instrument.
AutoObjectTradingSystem.mq5 Expert Advisor Main EA that integrates all modules into a complete trading system.
MQL5.zip Archive Complete archive with all files in their correct folder structure. Extract directly into your MQL5 folder.

Installation steps:

  1. Download the MQL5.zip archive and extract it directly into your MetaTrader 5 installation folder (where the MQL5 folder is located). The archive already contains the correct folder structure, so you can simply merge it with your existing MQL5 folder.
  2. If you prefer to manually place the files, follow these sub‑steps: 
    1. Place all .mqh files inside MQL5/Include/ChartObjectsAlgorithms-Part4/ maintaining the folder structure as shown below:
ChartObjectsAlgorithms-Part4/
├── Common/
│   ├── Structures.mqh      // SSwingPoint, MarketSnapshot, TradeSignal
│   └── Globals.mqh         // Global cached values
├── Helpers/
│   ├── MathHelpers.mqh    // GetEMA, GetLineValueAtTime, RefreshPriceCache
│   └── ObjectHelpers.mqh  // CreateObject functions, CleanupObjectsWithPrefix
├── Core/
│   ├── SwingDetector.mqh  // CSwingDetector class
│   ├── ObjectPlacer.mqh    // CObjectPlacer class
│   ├── MarketCache.mqh    // CMarketDataCache class
│   └── SignalEvaluator.mqh // CSignalEvaluator class
├── Engine/
│   └── TopologyManager.mqh // CTopologyManager class
└── Execution/
    └── AdaptiveTrade.mqh  // ExecuteAdaptiveTrade function
  1. Place AutoObjectTradingSystem.mq5 inside MQL5/Experts/ChartObjectsAlgorithms-Part4/.
  2. Compile all files (F7 in MetaEditor).
  3. Attach the EA to any chart. It will automatically draw objects and begin monitoring them.
  4. Observe the Experts tab for interaction logs and trade executions.
Attached files |
Structures.mqh (2.54 KB)
Globals.mqh (1.24 KB)
MathHelpers.mqh (3.58 KB)
ObjectHelpers.mqh (5.23 KB)
MarketCache.mqh (3.1 KB)
SignalEvaluator.mqh (12.28 KB)
AdaptiveTrade.mqh (3.72 KB)
SwingDetector.mqh (3.66 KB)
ObjectPlacer.mqh (8.76 KB)
MQL5.zip (17.53 KB)
Exporting Symbol Tick Data to Binary Files in MQL5 for Offline Analysis Exporting Symbol Tick Data to Binary Files in MQL5 for Offline Analysis
The article delivers a complete, verifiable tick export path from MQL5 to a binary file and into Python. It defines a 64‑byte header, 48‑byte records with millisecond time and flags, an export pipeline using CopyTicksRange(), and a single‑call NumPy loader. Users obtain compact, precision‑preserving files and a reproducible workflow for vectorized analysis.
Neural Networks in Trading: Effective Feature Extraction for Accurate Classification (Building Objects) Neural Networks in Trading: Effective Feature Extraction for Accurate Classification (Building Objects)
Mantis is a versatile tool for in-depth time series analysis that can be flexibly scaled to accommodate any financial scenario. Learn how a combination of patching, local convolutions, and cross-attention enables a highly accurate interpretation of market patterns.
Hierarchical Risk Parity: A Robust Portfolio Allocator and Expert Advisor Hierarchical Risk Parity: A Robust Portfolio Allocator and Expert Advisor
We implement a Hierarchical Risk Parity allocator in MQL5 as a single class, validate each stage against an independent Python reference, and package it in a rebalancing Expert Advisor. The pipeline covers returns, covariance/correlation, clustering, quasi-diagonalization, and recursive bisection, and contrasts HRP with Markowitz on stressed data. You finish with a verified allocator and an EA ready for basket-level testing.
Analysis of the Impact of Solar and Lunar Cycles on Currency Exchange Rates Analysis of the Impact of Solar and Lunar Cycles on Currency Exchange Rates
What if lunar cycles and seasonal patterns influence the foreign exchange markets? This article shows how to translate astrological concepts into the language of mathematics and machine learning. I built a Python system with 88 features based on astronomical cycles, trained CatBoost on 15 years of EURUSD data, and obtained some intriguing results. The code is open-source, the methods are verifiable, and the conclusions are unexpected — ancient wisdom meets gradient boosting.