preview
How to Detect and Normalize Chart Objects in MQL5 (Part 6): Manipulating Chart Objects with MetaTrader 5 Agentic AI Assistant

How to Detect and Normalize Chart Objects in MQL5 (Part 6): Manipulating Chart Objects with MetaTrader 5 Agentic AI Assistant

MetaTrader 5 — Examples |
92 0
Clemence Benjamin
Clemence Benjamin

Contents

  1. Introduction
  2. Accessibility Workflow and Capability Boundary
  3. The Chart Object as the AI-to-EA Contract
  4. Implementing the Producer Programs
  5. Implementing AIObjectTrader
  6. Implementing AIObjectProtocol.mqh
  7. Implementing AIObjectGeometry.mqh
  8. Testing and Runtime Evidence
  9. Current Boundaries and Key Lessons
  10. Conclusion
  11. Attachments


Introduction

You can describe chart analysis in natural language — “place a resistance level”, “draw a pitchfork from the last three pivots”, “add Fibonacci Time Zones to the swing” — but turning that analytical intent into something an Expert Advisor can reliably interpret is a different problem. A manually drawn object may be unusable for automated monitoring, while code-created drawings often lack consistent naming, identity, or an update lifecycle. As a result, an EA cannot reliably distinguish “my signal” from ordinary annotations, determine whether to monitor TOUCH/BREAK/ZONE, or connect an analyst’s intent to deterministic execution.

In this article, we connect natural-language analysis to chart-object evaluation in MQL5. The Assistant creates indicators and scripts that select anchors and place native objects. We use a structured naming format to carry the intended direction, event, and analytical role into AIObjectTrader. The current EA provides preliminary name recognition and selected geometry helpers; complete protocol validation and end-to-end trade verification remain outside the demonstrated result.

This approach extends the workflow introduced in Part 5, Fibonacci in Focus, where manually drawn Fibonacci objects were normalized so an Expert Advisor could evaluate them. Part 6 moves the starting point earlier: instead of beginning with an object already placed on the chart, we begin with the analyst’s intention expressed in natural language.

We follow the object from its creation to its recognition by the EA. This lets you inspect each stage separately before considering automated execution.

Evolution from manual and MQL5-created objects to AI-built MQL5 producer programs

Fig. 1. The series progresses from trader-drawn objects and automated placement toward a natural-language entry point where the Assistant builds MQL5 indicators or scripts that produce the chart objects.


Accessibility Workflow and Capability Boundary

The accessibility problem is not object availability. It is the knowledge needed to select the correct tool, anchors, parameters, and interpretation. Natural language reduces that entry barrier, while the generated MQL5 program preserves a visible and testable result.

Indicator deployment and script creation routes from an AI prompt to chart objects

Fig. 2. The Assistant does not directly place graphical objects in the tested environment. It creates MQL5 programs: indicators can be compiled and deployed automatically, while scripts can be created and compiled but still require manual launch.

The Pitchfork request demonstrated both routes. The first attempt stopped after compilation, while the later Assistant attached a Pitchfork indicator to GBPUSD H1. The official MQL5 reference confirmed that Andrews' Pitchfork is the native OBJ_PITCHFORK object, correcting the earlier Assistant statement that no built-in Pitchfork existed.

 Observed Pitchfork, Fibonacci Arcs, and Fibonacci Time Zones program workflows

Fig. 3. The two Pitchfork tests expose an evolving execution boundary: the first stopped after compilation, while the later test progressed through indicator attachment and visible chart placement.

Andrews Pitchfork attached to GBPUSD H1 through the AI-assisted workflow

Fig. 4. The Andrews Pitchfork indicator attached through the AI Assistant produces the visible Pitchfork structure on GBPUSD H1. The next integration requirement is to ensure that the native OBJ_PITCHFORK receives the structured unique name expected by AIObjectTrader.

Fibonacci Arcs followed the persistent-indicator route, while Fibonacci Time Zones used a one-shot script that required manual launch. Our workflow begins with a description, continues through program creation and object placement, and then passes the object to the EA for recognition and supported evaluation. Trading is an optional later stage and was not demonstrated in the recorded test.


The Chart Object as the AI-to-EA Contract

The producer program and AIObjectTrader communicate through the chart object. We define the following naming format to carry the analytical intent:

The format defines eight fields. The current parser recognizes the prefix, direction, event, numeric suffix, and Pitchfork line selection, but does not validate every field. The name carries an instruction for evaluation; it does not by itself establish that the object is suitable for trading.

AI_<SYMBOL>_<TIMEFRAME>_<DIRECTION>_<EVENT>_<OBJECT>_<ROLE>_<ID>

The final numeric ID distinguishes names on the chart checked by the producer. Allocation is chart-local, and the producers use different limits. Pitchfork initialization deletes candidate names sharing InpObjectBase before allocating a new name, so its identity is retained during an active run but not recovered across reinitialization. Cleanup is based on the shared prefix rather than a separate instance-ownership key.

Focused Prompt Demonstration

We use three object examples to explore two producer lifecycles: one-shot scripts for a horizontal level and Fibonacci Time Zones, and a persistent indicator for a Pitchfork.

Place a horizontal resistance line on EURUSD H1 at the most relevant resistance. Name it AI_EURUSD_H1_SELL_BREAK_HLINE_RESISTANCE_001, incrementing the ID if necessary. Do not trade.

horizontal resistance line

Fig. 5. The horizontal-resistance demonstration produces a one-shot MQL5 script for EURUSD H1. After execution, the script creates a uniquely named OBJ_HLINE for SELL_BREAK monitoring without submitting a trade.

Create and deploy a persistent GBPUSD H1 indicator that maintains native OBJ_PITCHFORK geometry. Name it AI_GBPUSD_H1_BUY_TOUCH_PITCHFORK_MEDIAN_001 and preserve the name while anchors update. Do not trade.

Pitchfork

Fig. 6. The persistent Pitchfork demonstration creates and deploys an indicator that maintains a native OBJ_PITCHFORK on GBPUSD H1 while preserving one AIObjectTrader-compatible object identity during anchor updates.

Create a one-shot GBPUSD H1 Fibonacci Time Zones script. Name it AI_GBPUSD_H1_BUY_TOUCH_FIBOTIME_SWING_001, incrementing the ID if necessary. Compile it and leave it ready for manual Navigator launch. Do not trade.

Fibonacci Time Zones Script by AI Assistant

Fig. 7. The Fibonacci Time Zones demonstration creates and compiles a one-shot script. Manual launch remains necessary; once executed, the script places a uniquely named native OBJ_FIBOTIMES object on the target GBPUSD H1 chart.


Implementing the Producer Programs

The producer examples show one-shot placement and persistent updates. Their code explains how a natural-language request becomes a native object that the EA can identify.

Horizontal Resistance: Unique One-Shot Identity

The horizontal-line script searches IDs from 1 through 9999 on the current chart. StringFormat() pads IDs to at least three digits, so values above 999 use four digits. If every candidate is occupied, the function returns prefix plus 000 without checking that fallback. This exhaustion path does not guarantee a free name and needs an explicit failure policy before trading use.

BuildUniqueName() separates name allocation from event interpretation. The resistance price is provided through InpResistance; the script does not search market history for the most relevant resistance. InpSymbol and InpTimeframe do not redirect object creation: the script places the line on the chart from which you launch it. Use the intended EURUSD H1 chart and keep the name aligned with that context.

//+------------------------------------------------------------------+
//| BuildUniqueName - finds an unused chart-local object name        |
//| Returns prefix+"000" if IDs 1 through 9999 are occupied.         |
//+------------------------------------------------------------------+
string BuildUniqueName(const string prefix)
  {
   const long chart_id=ChartID();

   for(int id=1; id<=9999; id++)
     {
      string candidate=prefix+StringFormat("%03d",id);

      if(ObjectFind(chart_id,candidate)<0)
         return(candidate);
     }

   return(prefix+"000");
  }

The SELL_BREAK name requests a downward crossing of the selected level, as defined by AIIsBreak(). It does not encode a sell merely because the object is labelled resistance, nor does it request an upward breakout. The direction and event fields define the monitored relationship; the role field describes the analytical use of the level.

Pitchfork: Persistent Identity with Updating Geometry

The Pitchfork indicator retains g_obj_name while updating anchors during an active run. OnInit() first deletes candidates sharing InpObjectBase, then allocates a free name. OnDeinit() deliberately leaves the object on the chart. A later initialization can therefore delete and recreate that object, and instances sharing the prefix can affect each other.

During one uninterrupted run, a BUY_TOUCH_PITCHFORK_MEDIAN object can retain its name as the anchors change. Across reinitialization, this behavior is not yet guaranteed by the shown source.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   g_chart_id = ChartID();
   DeleteAllPrefixObjects(InpObjectBase);
   g_obj_name = BuildUniqueName(InpObjectBase);
   PrintFormat("DeployBuyTouchPitchfork: monitoring object '%s'", g_obj_name);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Remove every remaining pitchfork object of this indicator so the |
//| target always starts from the _001 name with clean geometry.     |
//+------------------------------------------------------------------+
void DeleteAllPrefixObjects(const string prefix)
{
   for(int id = 1; id <= 9999; id++)
   {
      string candidate = prefix + StringFormat("%03d", id);
      if(ObjectFind(g_chart_id, candidate) >= 0)
         ObjectDelete(g_chart_id, candidate);
   }
}
//+------------------------------------------------------------------+
//| Set/refresh the pitchfork anchors (same object name each time)   |
//+------------------------------------------------------------------+
void UpdatePitchfork(const datetime t0, const double p0,
                     const datetime t1, const double p1,
                     const datetime t2, const double p2)
{
   //--- a degenerate (zero-anchor) stale object cannot be updated in
   //--- place, so drop it and let EnsurePitchfork recreate it properly
   if(ObjectFind(g_chart_id, g_obj_name) >= 0)
   {
      datetime cur0 = (datetime)ObjectGetInteger(g_chart_id, g_obj_name, OBJPROP_TIME, 0);
      if(cur0 <= D'1970.01.01 00:00')
         ObjectDelete(g_chart_id, g_obj_name);
   }

   EnsurePitchfork(t0, p0, t1, p1, t2, p2);
   if(ObjectFind(g_chart_id, g_obj_name) < 0)
      return;

   ObjectSetInteger(g_chart_id, g_obj_name, OBJPROP_TIME,  0, (long)t0);
   ObjectSetInteger(g_chart_id, g_obj_name, OBJPROP_TIME,  1, (long)t1);
   ObjectSetInteger(g_chart_id, g_obj_name, OBJPROP_TIME,  2, (long)t2);
   ObjectSetDouble  (g_chart_id, g_obj_name, OBJPROP_PRICE, 0, p0);
   ObjectSetDouble  (g_chart_id, g_obj_name, OBJPROP_PRICE, 1, p1);
   ObjectSetDouble  (g_chart_id, g_obj_name, OBJPROP_PRICE, 2, p2);

   //--- keep the fork projecting to the right even when refreshed
   ObjectSetInteger(g_chart_id, g_obj_name, OBJPROP_RAY_RIGHT, true);

   ChartRedraw(g_chart_id);
}

Fibonacci Time Zones: One-Shot Script

The Time Zones script resolves the target swing and chart before object creation. It then allocates the first free structured name, creates OBJ_FIBOTIMES, redraws the chart, and exits. Unlike the Pitchfork indicator, it has no persistent recalculation cycle. A later launch can create another independently named Time Zones object while the previous one remains available for inspection. The Assistant can prepare the script, but the tested environment still needs manual Navigator launch.

This gives the script route a different implementation role from the indicator route. The script performs all required preparation in one OnStart() execution: it decides where the object belongs, confirms that its name is free, creates the native object, and then hands control back to the chart. From that point onward, the script is no longer involved. Any further interpretation is the responsibility of AIObjectTrader, which is exactly why the object name must carry enough information for the EA to understand the intended direction and event.

The allocation and creation stage below is an exact excerpt from OnStart(). It searches the destination chart for IDs from 001 through 999, stops if allocation fails, and creates the native object using the selected swing and time-unit anchors. CleanExisting is false by default; enabling it removes objects sharing the generated base name before allocation. When no swing meets the amplitude threshold, the script can use the latest detected pivot as a fallback.

   //--- object name: base_NNN, increment the numeric ID until the name is free
   string objName = "";
   int    id      = 1;
   while(id <= 999)
     {
      string candidate = "AI_" + symbol + "_" + TimeframeToString(tf) + "_BUY_TOUCH_FIBOTIME_SWING" +
                         StringFormat("_%03d", id);
      if(ObjectFind(chartId, candidate) < 0)
        {
         objName = candidate;
         break;
        }
      id++;
     }

   if(objName == "")
     {
      Print("Could not allocate a free object name (001..999 all taken on the target chart).");
      return;
     }

   //--- anchors: swing point and one fibo unit to the right (same price level)
   datetime anchorTime = iTime(symbol, tf, anchorIdx);
   int      unit       = MathMax(1, UnitBars);
   datetime t2         = anchorTime + (datetime)unit * (datetime)PeriodSeconds(tf);

   if(!ObjectCreate(chartId, objName, OBJ_FIBOTIMES, 0, anchorTime, anchorPrice, t2, anchorPrice))
     {
      PrintFormat("Failed to create Fibonacci Time Zones '%s' on chart %I64d, error %d.",
                  objName, chartId, GetLastError());
      return;
     }

Organizing the Source Files

MQL5\
├── Experts\Article24080\AIObjectTrader.mq5
├── Indicators\Article24080\DeployBuyTouchPitchfork.mq5
├── Scripts\Article24080\PlaceSellBreakResistanceHLine.mq5
├── Scripts\Article24080\AI_GBPUSD_H1_BUY_TOUCH_FIBOTIME_SWING_001.mq5
└── Include\Article24080\Core\
    ├── AIObjectGeometry.mqh
    └── AIObjectProtocol.mqh


Implementing AIObjectTrader

AIObjectTrader separates object production from evaluation. It scans existing chart objects, performs preliminary name recognition, and passes recognized objects to the evaluation layer. The architecture includes event tracking and optional CTrade requests, but the recorded test establishes recognition and unsupported-type handling only.

Once an object is available, the EA applies programmed evaluation rules. The producer still influences the result through its anchor-selection method. We therefore keep production, recognition, geometry evaluation, event tracking, and order requests as separate stages.

Program producer layer feeding native chart objects into AIObjectTrader

Fig. 8. AI-generated indicators and scripts form the producer layer. Their uniquely named native chart objects feed the same scanner, geometry adapters, interaction rules, risk controls, and execution logic in AIObjectTrader.

Initialization and Tick Flow

Initialization assigns the magic number and symbol filling mode. The magic number later separates EA-owned positions from unrelated trades, while SetTypeFillingBySymbol() adapts CTrade to the active symbol. On every tick, the EA scans first and stores bid/ask afterward. That order preserves the previous market observation for live crossing tests; writing the new values first would remove the historical side of the comparison.

These two functions establish the EA's runtime rhythm. OnInit() performs the one-time setup, while OnTick() repeatedly feeds the current market state into the object scanner. The EA does not attempt to trade at either stage directly. Instead, OnTick() delegates the decision to ScanAIObjects(), which keeps the event loop small and makes the later protocol and geometry stages easier to test independently.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//| Sets the magic number, the filling mode for the current symbol   |
//| and logs a short summary of the supported object types.          |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_trade.SetExpertMagicNumber(InpMagicNumber);
   g_trade.SetTypeFillingBySymbol(_Symbol);
   Print("AIObjectTrader initialized. Structured unique names required; supported: HLINE, TREND, RECTANGLE, PITCHFORK.");
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//| Refreshes the tick and runs the object scan on every incoming    |
//| price update.                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol,tick)) return;
   ScanAIObjects(tick);
   g_last_bid=tick.bid; g_last_ask=tick.ask;
  }

Protocol-Aware Chart Scan

The scanner currently performs a preliminary filter: AI_ prefix, at least eight name segments, a numeric final token, and substring-based direction and event extraction. It does not yet enforce the complete eight-field contract, exact token positions, symbol/timeframe agreement, unique direction/event tokens, declared-versus-native type agreement, or role compatibility. Therefore, a malformed AI-prefixed name may pass this preliminary gate.

Complete validation requires exactly eight fields in their defined positions, an allowed ID width and range, symbol and timeframe agreement, a matching native object type, and compatible event and role values. These checks are not implemented by the scanner excerpt. Until they are added, preliminary recognition must not be treated as approval to trade.

//+------------------------------------------------------------------+
//| ScanAIObjects - walks all objects on the current chart           |
//|                                                                  |
//| Filters out any object that is not part of the AI_ protocol,     |
//| does not carry a numeric instance ID, or has an unknown          |
//| direction/event. Valid candidates are forwarded to               |
//| EvaluateObject().                                                |
//+------------------------------------------------------------------+
void ScanAIObjects(const MqlTick &tick)
  {
   long chart_id=ChartID();
   for(int i=0;i<ObjectsTotal(chart_id,-1,-1);i++)
     {
      string name=ObjectName(chart_id,i,-1,-1);

      //--- only objects that obey the AI_ naming protocol are managed
      if(name=="" || !AIIsProtocolObject(name))
         continue;

      //--- an AI object must end with a numeric instance ID
      if(!AIHasNumericId(name))
        {
         if(InpDebug)
            PrintFormat("IGNORED | %s | missing structured numeric ID",name);

         continue;
        }

      //--- decode direction and event; skip objects we cannot interpret
      ENUM_AI_DIRECTION direction=AIParseDirection(name);
      ENUM_AI_EVENT event_type=AIParseEvent(name);
      if(direction==AI_DIRECTION_NONE || event_type==AI_EVENT_NONE) continue;

      //--- retrieve the concrete object type and evaluate it
      ENUM_OBJECT type=(ENUM_OBJECT)ObjectGetInteger(chart_id,name,OBJPROP_TYPE);
      EvaluateObject(chart_id,name,type,direction,event_type,tick);
     }
  }

Type Dispatch and Safe Failure

EvaluateObject() combines resolved market observations with each supported geometry. HLINE, TREND, and PITCHFORK accept TOUCH or BREAK, while RECTANGLE accepts ZONE and tests fresh entry into its price bounds. The complete function below includes those branches, signal logging, per-object tracking, and optional execution.

FIBOTIMES and FIBOARC reach the unsupported branch. Pitchfork evaluation obtains the selected line from AIParsePitchforkLine(): UPPER takes precedence over LOWER, and absence of both defaults to MEDIAN. The helper does not require an explicit valid role token, so complete role validation remains a separate requirement.

//+------------------------------------------------------------------+
//| EvaluateObject - evaluates a single managed object               |
//|                                                                  |
//| Depending on the object type and the decoded event, computes     |
//| whether a fresh signal has occurred between the previous and     |
//| the current price. A signal is dispatched only once per bar      |
//| (deduplicated via WasSignaledOnBar / MarkSignaled).              |
//+------------------------------------------------------------------+
void EvaluateObject(const long chart_id,const string name,const ENUM_OBJECT type,
                    const ENUM_AI_DIRECTION direction,const ENUM_AI_EVENT event_type,
                    const MqlTick &tick)
  {
   double previous_price=0.0,current_price=0.0,trigger=0.0;
   datetime previous_time=0,current_time=0,signal_bar=0;
   if(!GetEvaluationPrices(direction,tick,previous_price,current_price,
                           previous_time,current_time,signal_bar)) return;

   bool signal=false;

   //--- HORIZONTAL LINE: touch or break of a flat level
   if(type==OBJ_HLINE)
     {
      if(event_type==AI_EVENT_ZONE || !AIGetHorizontalPrice(chart_id,name,trigger)) return;
      signal=(event_type==AI_EVENT_TOUCH)
             ? AIIsTouch(current_price,trigger,InpTouchTolerance)
             : AIIsBreak((int)direction,previous_price,current_price,trigger,trigger);
     }
   //--- TRENDLINE: touch or break of a moving (sloped) level
   else if(type==OBJ_TREND)
     {
      if(event_type==AI_EVENT_ZONE) return;
      double p0=0.0,p1=0.0;
      if(!AIGetTrendPrice(chart_id,name,previous_time,p0) ||
         !AIGetTrendPrice(chart_id,name,current_time,p1)) return;
      trigger=p1;
      signal=(event_type==AI_EVENT_TOUCH)
             ? AIIsTouch(current_price,p1,InpTouchTolerance)
             : AIIsBreak((int)direction,previous_price,current_price,p0,p1);
     }
   //--- RECTANGLE: entering a price zone
   else if(type==OBJ_RECTANGLE)
     {
      if(event_type!=AI_EVENT_ZONE) return;
      double lower=0.0,upper=0.0;
      if(!AIGetRectangleZone(chart_id,name,lower,upper)) return;
      bool now=(current_price>=lower && current_price<=upper);
      bool before=(previous_price>=lower && previous_price<=upper);
      signal=now && !before;                       // fresh entry into the zone
      trigger=(direction==AI_DIRECTION_BUY ? upper : lower);
     }
   //--- PITCHFORK: touch or break of a selected line
   else if(type==OBJ_PITCHFORK)
     {
      if(event_type==AI_EVENT_ZONE) return;
      int line=(int)AIParsePitchforkLine(name);
      double p0=0.0,p1=0.0;
      if(!AIGetPitchforkPrice(chart_id,name,previous_time,line,p0) ||
         !AIGetPitchforkPrice(chart_id,name,current_time,line,p1)) return;
      trigger=p1;
      signal=(event_type==AI_EVENT_TOUCH)
             ? AIIsTouch(current_price,p1,InpTouchTolerance)
             : AIIsBreak((int)direction,previous_price,current_price,p0,p1);
     }
   else
     {
      if(InpDebug) PrintFormat("UNSUPPORTED | %s | type=%d",name,(int)type);
      return;
     }

   //--- discard duplicates: a given object fires only once per bar
   if(!signal || WasSignaledOnBar(name,signal_bar)) return;
   MarkSignaled(name,signal_bar);

   PrintFormat("SIGNAL | %s | %s | %s | market=%.*f | trigger=%.*f",
               name,AIDirectionToString(direction),AIEventToString(event_type),
               _Digits,current_price,_Digits,trigger);

   //--- trading is optional; default behaviour is signal-only mode
   if(!InpEnableTrading)
     {
      PrintFormat("TEST MODE | %s | trade request suppressed",name);
      return;
     }

   ExecuteSignal(name,direction,tick);
  }

Closed-Bar and Live Evaluation

In closed-bar mode, GetEvaluationPrices() enables series indexing on the dynamic rates array before calling CopyRates(). With ArraySetAsSeries(), rates[1] is the latest completed candle and rates[2] is the preceding candle. The function assigns both prices and timestamps, and uses rates[1].time as the signal-bar identifier.

The timestamp in MqlRates is the period start time. This implementation therefore compares closing prices with sloped geometry evaluated at the corresponding bar opening times. Live mode uses the stored previous bid or ask, but approximates its timestamp as one second before TimeCurrent() rather than retaining the actual previous tick time. Both timing choices matter for moving levels. Symbol and timeframe validation are not enforced by the current scanner.

//+------------------------------------------------------------------+
//| GetEvaluationPrices - resolves previous/current evaluation       |
//|                                                                  |
//| Two modes are supported:                                         |
//|  - Closed-bar mode (default): uses the last two finished bars,   |
//|    which removes intra-bar noise and makes break detection       |
//|    stable and reproducible.                                      |
//|  - Live mode: uses the current tick against the previously       |
//|    stored tick, for the fastest possible reaction.               |
//|                                                                  |
//| @return  true when both prices could be resolved.                |
//+------------------------------------------------------------------+
bool GetEvaluationPrices(const ENUM_AI_DIRECTION direction,const MqlTick &tick,
                         double &previous_price,double &current_price,
                         datetime &previous_time,datetime &current_time,
                         datetime &signal_bar)
  {
   if(InpRequireClosedBar)
     {
      MqlRates rates[];
      ArraySetAsSeries(rates,true);
      if(CopyRates(_Symbol,_Period,0,3,rates)<3) return(false);
      current_price=rates[1].close; previous_price=rates[2].close;
      current_time=rates[1].time; previous_time=rates[2].time;
      signal_bar=rates[1].time;
      return(true);
     }

   //--- live mode: pick the ask for buys and the bid for sells
   current_price=(direction==AI_DIRECTION_BUY ? tick.ask : tick.bid);
   previous_price=(direction==AI_DIRECTION_BUY ? g_last_ask : g_last_bid);
   if(previous_price<=0.0) return(false);
   current_time=TimeCurrent(); previous_time=current_time-1;
   signal_bar=iTime(_Symbol,_Period,0);
   return(signal_bar>0);
  }

WasSignaledOnBar() and MarkSignaled() track the most recent signal bar for each full object name in memory. This limits repeated signals during the EA run, but does not persist across reattachment or restart. Repeated TOUCH proximity can therefore produce a signal on another bar. Opposing objects are processed in scan order without a separate conflict-resolution policy.

Execution Guards

ExecuteSignal() checks spread, owned positions, and normalized volume before sending a market-order request. It supports zero stop-loss or take-profit inputs as disabled distances and normalizes nonzero levels to the symbol digits. The function captures the CTrade return value and logs ResultRetcode() and its description. Those diagnostics are implemented, but the article contains no recorded successful trade result.

Execution follows event evaluation. With trading disabled, you can investigate recognition and signal behavior before allowing order requests. EvaluateObject() marks an event before calling ExecuteSignal(), so an execution guard or rejected request may still consume that event for the bar. This behavior needs to be defined and tested alongside duplicate tracking and opposing signals.

//+------------------------------------------------------------------+
//| ExecuteSignal - applies safety filters and sends the order       |
//|                                                                  |
//| Guards applied before trading:                                   |
//|  - max spread filter (when enabled),                             |
//|  - one-position-only filter (when enabled),                      |
//|  - volume normalization to the symbol's lot step.                |
//|                                                                  |
//| The resulting market order is sent via CTrade and its retcode    |
//| is logged.                                                       |
//+------------------------------------------------------------------+
void ExecuteSignal(const string object_name,const ENUM_AI_DIRECTION direction,const MqlTick &tick)
  {
   if(InpMaxSpreadPoints>0 && (tick.ask-tick.bid)/_Point>InpMaxSpreadPoints) return;
   if(InpOnePositionOnly && HasOwnedPosition()) return;

   double volume=NormalizeVolume(InpVolume);
   if(volume<=0.0) return;

   bool sent=false;
   if(direction==AI_DIRECTION_BUY)
     {
      //--- long: SL below and TP above the ask
      double sl=InpStopLossPoints>0 ? NormalizeDouble(tick.ask-InpStopLossPoints*_Point,_Digits):0.0;
      double tp=InpTakeProfitPoints>0 ? NormalizeDouble(tick.ask+InpTakeProfitPoints*_Point,_Digits):0.0;
      sent=g_trade.Buy(volume,_Symbol,0.0,sl,tp,object_name);
     }
   else
     {
      //--- short: SL above and TP below the bid
      double sl=InpStopLossPoints>0 ? NormalizeDouble(tick.bid+InpStopLossPoints*_Point,_Digits):0.0;
      double tp=InpTakeProfitPoints>0 ? NormalizeDouble(tick.bid-InpTakeProfitPoints*_Point,_Digits):0.0;
      sent=g_trade.Sell(volume,_Symbol,0.0,sl,tp,object_name);
     }

   PrintFormat("%s | %s | retcode=%u | %s",
               sent ? "TRADE REQUEST" : "TRADE FAILED",
               object_name,g_trade.ResultRetcode(),g_trade.ResultRetcodeDescription());
  }


Implementing AIObjectProtocol.mqh

AIObjectProtocol.mqh provides preliminary prefix, direction, event, numeric-suffix, and Pitchfork-line recognition. Complete contract validation additionally requires positional parsing, context checks, native-type agreement, and rejection of ambiguous roles.

We keep recognition helpers in a separate header so the scanner does not repeat string searches. AIHasNumericId() requires at least eight underscore-separated segments and a nonempty numeric final segment. It does not require exactly eight fields, validate the ID range, or verify the symbol and timeframe. AIParsePitchforkLine() provides a default selection rather than strict role validation.

Ownership and Direction

//+------------------------------------------------------------------+
//| AIIsProtocolObject - validates the AI_ name prefix               |
//|                                                                  |
//| An object belongs to the AI system only when its name starts     |
//| with the reserved "AI_" prefix. Objects with any other prefix    |
//| are ignored by the scanner.                                      |
//|                                                                  |
//| @param name  Full chart object name.                             |
//| @return      true if the name begins with "AI_".                 |
//+------------------------------------------------------------------+
bool AIIsProtocolObject(const string name)
  {
   return(StringFind(name,"AI_")==0);
  }
//+------------------------------------------------------------------+
//| AIParseDirection - extracts the trade direction token            |
//|                                                                  |
//| Looks for the "_BUY_" / "_SELL_" markers used inside the         |
//| structured object name.                                          |
//|                                                                  |
//| @param name  Structured object name (e.g. AI_..._BUY_...).       |
//| @return      ENUM_AI_DIRECTION, or AI_DIRECTION_NONE if neither  |
//|              token is present.                                   |
//+------------------------------------------------------------------+
ENUM_AI_DIRECTION AIParseDirection(const string name)
  {
   if(StringFind(name,"_BUY_")>=0)
      return(AI_DIRECTION_BUY);

   if(StringFind(name,"_SELL_")>=0)
      return(AI_DIRECTION_SELL);

   return(AI_DIRECTION_NONE);
  }

The AI_ prefix is an ownership hint, not proof that a name follows the contract. The current substring search can accept misplaced or contradictory tokens; a positional parser should reject them rather than choose the first matching direction.

If neither direction substring is present, the scanner ignores the object. If both are present, the current parser returns BUY because it checks that substring first. A complete parser must reject that ambiguity. Visual styling does not change the encoded direction.

Recognizing Events, Pitchfork Lines, and Instance IDs

//+------------------------------------------------------------------+
//| AIParseEvent - extracts the event type token                     |
//|                                                                  |
//| Looks for the "_TOUCH_", "_BREAK_" or "_ZONE_" markers used      |
//| inside the structured object name.                               |
//|                                                                  |
//| @param name  Structured object name (e.g. AI_..._TOUCH_...).     |
//| @return      ENUM_AI_EVENT, or AI_EVENT_NONE if none matches.    |
//+------------------------------------------------------------------+
ENUM_AI_EVENT AIParseEvent(const string name)
  {
   if(StringFind(name,"_TOUCH_")>=0)
      return(AI_EVENT_TOUCH);

   if(StringFind(name,"_BREAK_")>=0)
      return(AI_EVENT_BREAK);

   if(StringFind(name,"_ZONE_")>=0)
      return(AI_EVENT_ZONE);

   return(AI_EVENT_NONE);
  }

//+------------------------------------------------------------------+
//| AIParsePitchforkLine - extracts the pitchfork line reference     |
//|                                                                  |
//| Pitchfork objects can carry an optional "_UPPER" / "_LOWER"      |
//| suffix in the topic portion of the name. Absence of both         |
//| suffixes means the median line is intended.                      |
//|                                                                  |
//| @param name  Structured object name.                             |
//| @return      ENUM_AI_PITCHFORK_LINE (MEDIAN by default).         |
//+------------------------------------------------------------------+
ENUM_AI_PITCHFORK_LINE AIParsePitchforkLine(const string name)
  {
   if(StringFind(name,"_UPPER")>=0) return(AI_PITCHFORK_UPPER);
   if(StringFind(name,"_LOWER")>=0) return(AI_PITCHFORK_LOWER);
   return(AI_PITCHFORK_MEDIAN);
  }
//+------------------------------------------------------------------+
//| AIHasNumericId - verifies the trailing instance ID               |
//|                                                                  |
//| A well-formed AI object name ends with a numeric instance ID     |
//| (separated by an underscore) which guarantees that each object   |
//| is uniquely identifiable, e.g. "..._SWING_001".                  |
//|                                                                  |
//| @param name  Structured object name.                             |
//| @return      true only if the last underscore-delimited          |
//|              segment is at least 1 digit and contains only       |
//|              digits (and the name has >= 8 segments).            |
//+------------------------------------------------------------------+
bool AIHasNumericId(const string name)
  {
   int pos=StringFind(name,"_");

   if(pos<0)
      return(false);

   string parts[];
   int count=StringSplit(name,'_',parts);

   if(count<8)
      return(false);

   string id=parts[count-1];

   if(StringLen(id)<1)
      return(false);

   for(int i=0;i<StringLen(id);i++)
     {
      ushort ch=StringGetCharacter(id,i);

      if(ch<'0' || ch>'9')
         return(false);
     }

   return(true);
  }

The evaluator restricts TOUCH and BREAK to its supported line geometries and ZONE to rectangles. TOUCH means proximity within the configured tolerance; it does not require a new transition into that tolerance. Rectangle evaluation checks price bounds without using the rectangle's time span. Numeric ID width and range are not enforced by the protocol helper.

Separating object type from event makes the naming format extensible. New geometry adapters can share existing event vocabulary where the interpretation is compatible. Duplicate tracking can use the complete object name within its defined scope; the numeric suffix alone does not establish uniqueness across charts or restarts.


Implementing AIObjectGeometry.mqh

AIObjectGeometry.mqh converts chart objects into levels and price bounds. The horizontal, trend, rectangle, and Pitchfork helpers check native object type. The trend helper also checks the API error state; the Pitchfork helper rejects a zero time denominator, invalid line index, and a nonpositive result. Complete property-read and anchor validation, and comparison with native display settings, still need dedicated tests.

This is where graphical objects are normalized into values the EA can compare with market data. The main EA never needs to know which OBJPROP fields belong to a rectangle or how a Pitchfork line is reconstructed. It requests a level or zone through a dedicated helper and then applies the generic event logic. That keeps object-specific calculations in one place and makes future geometry extensions easier to isolate.

Fig. 9. Different graphical objects can be reduced to reusable geometric relationships: fixed levels, time-varying lines, zones, indexed levels, parallel boundaries, and Pitchfork median/outer lines.

Horizontal, Trend, and Rectangle Geometry

//+------------------------------------------------------------------+
//| AIGetHorizontalPrice - reads the price of an OBJ_HLINE           |
//|                                                                  |
//| @param chart_id  Target chart (ChartID()).                       |
//| @param name      Object name (must be an OBJ_HLINE).             |
//| @param price     [out] Horizontal level price.                   |
//| @return          true on success, false if the object is not     |
//|                  an horizontal line or price is not positive.    |
//+------------------------------------------------------------------+
bool AIGetHorizontalPrice(const long chart_id,const string name,double &price)
  {
   if((ENUM_OBJECT)ObjectGetInteger(chart_id,name,OBJPROP_TYPE)!=OBJ_HLINE)
      return(false);
   price=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,0);
   return(price>0.0);
  }
//+-----------------------------------------------------------------+
//| AIGetTrendPrice - interpolates an OBJ_TREND at a given time     |
//|                                                                 |
//| Evaluates the analytic value of a sloped trendline at the       |
//| requested bar time using ObjectGetValueByTime.                  |
//|                                                                 |
//| @param chart_id  Target chart.                                  |
//| @param name      Object name (must be an OBJ_TREND).            |
//| @param time      Bar time at which the line is evaluated.       |
//| @param price     [out] Interpolated level price.                |
//| @return          true on success.                               |
//+-----------------------------------------------------------------+
bool AIGetTrendPrice(const long chart_id,const string name,const datetime time,double &price)
  {
   if((ENUM_OBJECT)ObjectGetInteger(chart_id,name,OBJPROP_TYPE)!=OBJ_TREND)
      return(false);
   ResetLastError();
   price=ObjectGetValueByTime(chart_id,name,time,0);
   return(GetLastError()==0 && price!=0.0);
  }
//+------------------------------------------------------------------+
//| AIGetRectangleZone - reads the two bounds of an OBJ_RECTANGLE    |
//|                                                                  |
//| The two anchor prices are converted into a normalized lower /    |
//| upper envelope so that the zone order does not matter.           |
//|                                                                  |
//| @param chart_id  Target chart.                                   |
//| @param name      Object name (must be an OBJ_RECTANGLE).         |
//| @param lower     [out] Lower zone bound.                         |
//| @param upper     [out] Upper zone bound.                         |
//| @return          true on success with a valid positive range.    |
//+------------------------------------------------------------------+
bool AIGetRectangleZone(const long chart_id,const string name,double &lower,double &upper)
  {
   if((ENUM_OBJECT)ObjectGetInteger(chart_id,name,OBJPROP_TYPE)!=OBJ_RECTANGLE)
      return(false);
   double p1=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,0);
   double p2=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,1);
   lower=MathMin(p1,p2);
   upper=MathMax(p1,p2);
   return(lower>0.0 && upper>lower);
  }

OBJ_HLINE returns one fixed level, so the same trigger can be used for previous and current comparisons. OBJ_TREND is evaluated at a requested time because its active price changes along the slope. Rectangle anchors are normalized with MathMin() and MathMax() into lower and upper boundaries. These helpers hide object-specific property handling from the main EA.

The normalization also gives the event layer a consistent interface. TOUCH and BREAK do not care whether a value came from an object property or from a time-based interpolation, and ZONE does not care which rectangle anchor was originally higher. The geometry header converts each native object into the simplest deterministic form required by the event rule.

Pitchfork and Generic Events

//+------------------------------------------------------------------+
//| AIGetPitchforkPrice - evaluates a Pitchfork line at a time       |
//|                                                                  |
//| Reconstructs the geometric model of a standard Andrews           |
//| pitchfork:                                                       |
//|   - anchors P0 (origin), P1, P2 (the two base pivots);           |
//|   - median point  M = (P1 + P2) / 2 at time  tM = (t1 + t2)/2;   |
//|   - a line through P0 and M defines the slope of all three       |
//|     parallel lines.                                              |
//|                                                                  |
//| The requested line (median=0, upper=1, lower=2) is extrapolated  |
//| to the given time using its own anchor point.                    |
//|                                                                  |
//| @param chart_id   Target chart.                                  |
//| @param name       Object name (must be an OBJ_PITCHFORK).        |
//| @param time       Bar time at which the line is evaluated.       |
//| @param line_index ENUM_AI_PITCHFORK_LINE (0/1/2).                |
//| @param price      [out] Evaluated line price.                    |
//| @return           true on success, false for invalid objects,    |
//|                   zero-length time span or bad line index.       |
//+------------------------------------------------------------------+
bool AIGetPitchforkPrice(const long chart_id,
                         const string name,
                         const datetime time,
                         const int line_index,
                         double &price)
  {
   if((ENUM_OBJECT)ObjectGetInteger(chart_id,name,OBJPROP_TYPE)!=OBJ_PITCHFORK)
      return(false);

   //--- read the three pitchfork anchors (time, price)
   datetime t0=(datetime)ObjectGetInteger(chart_id,name,OBJPROP_TIME,0);
   datetime t1=(datetime)ObjectGetInteger(chart_id,name,OBJPROP_TIME,1);
   datetime t2=(datetime)ObjectGetInteger(chart_id,name,OBJPROP_TIME,2);
   double p0=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,0);
   double p1=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,1);
   double p2=ObjectGetDouble(chart_id,name,OBJPROP_PRICE,2);

   //--- median anchor: average of the two base pivots
   datetime tm=(datetime)(((long)t1+(long)t2)/2);
   double pm=(p1+p2)/2.0;

   //--- the time span from the origin to the median must be positive
   if(tm==t0)
      return(false);

   //--- common slope carried by all pitchfork lines
   double slope=(pm-p0)/(double)(tm-t0);

   //--- extrapolate the requested line to the target time
   if(line_index==0)
      price=p0+slope*(double)(time-t0);
   else if(line_index==1)
      price=p1+slope*(double)(time-t1);
   else if(line_index==2)
      price=p2+slope*(double)(time-t2);
   else
      return(false);

   return(price>0.0);
  }
//+------------------------------------------------------------------+
//| AIIsTouch - tolerance-based touch detection                      |
//|                                                                  |
//| A touch is considered valid when the current market price is     |
//| within 'tolerance_points' of the object level.                   |
//|                                                                  |
//| @param market_price      Current bid/ask (or bar close).         |
//| @param object_price      Level price of the object.              |
//| @param tolerance_points  Allowed distance in points.             |
//| @return                  true when |market - object| <= tol.     |
//+------------------------------------------------------------------+
bool AIIsTouch(const double market_price,const double object_price,const int tolerance_points)
  {
   return(MathAbs(market_price-object_price)<=tolerance_points*_Point);
  }

//+------------------------------------------------------------------+
//| AIIsBreak - one-directional level break detection                |
//|                                                                  |
//| Detects a fresh crossing of a level between two consecutive      |
//| evaluations. The level may itself move (e.g. a sloped trend-     |
//| line), so both the previous and the current level price are      |
//| supplied.                                                        |
//|                                                                  |
//| @param direction        1 = BUY (cross above), 2 = SELL (below). |
//| @param previous_price   Previous market price.                   |
//| @param current_price    Current market price.                    |
//| @param previous_level   Previous level price.                    |
//| @param current_level    Current level price.                     |
//| @return                 true only on a fresh valid crossing.     |
//+------------------------------------------------------------------+
bool AIIsBreak(const int direction,
               const double previous_price,const double current_price,
               const double previous_level,const double current_level)
  {
   if(direction==1)
      return(previous_price<=previous_level && current_price>current_level);
   if(direction==2)
      return(previous_price>=previous_level && current_price<current_level);
   return(false);
  }

The Pitchfork helper derives a common slope from the origin and the midpoint of the two remaining anchors. Line 0 passes through the origin, line 1 through anchor 1, and line 2 through anchor 2. UPPER and LOWER select the latter two lines; their spatial order depends on the anchor arrangement. A zero denominator or unsupported line index returns false. Native-display equivalence and malformed-anchor cases remain unverified.

This is the main reason we keep the event functions generic. AIIsTouch() only needs a market value, an object value, and a tolerance. AIIsBreak() only needs two market observations and two geometry observations. The same event code can therefore work with several native objects once AIObjectGeometry.mqh has converted them into the required values.

Testing and Runtime Evidence

Interpreting the Recorded Result

The demonstrations show producer creation and selected visible chart objects. The Journal excerpt below records a Fibonacci Time Zones object reaching the EA's unsupported-type branch. This test does not establish geometry or event correctness, duplicate handling, or successful order execution.

Planning Further Tests

For further testing, use malformed names, conflicting direction or event tokens, symbol and timeframe mismatches, declared/native type mismatches, invalid Pitchfork roles, degenerate anchors, and exhausted name allocation. Then test repeated events, opposing signals, and accepted and rejected order requests on a demo account. Keep the observed results separate from expected behavior.

 End-to-end validation of indicator and script routes into AIObjectTrader

Fig. 10. Validation follows the actual capability boundary: prompt, program type selection, source creation, compilation, indicator deployment or manual script launch, stable object naming, chart verification, AIObjectTrader detection, and deterministic execution.

Testing is performed in layers: producer creation, object naming, EA recognition, geometry support, event detection, and finally trading. This separation became visible when AIObjectTrader was attached to the chart containing the Fibonacci Time Zones object:

2026.09.02 12:01:04.369  AIObjectTrader (GBPUSD,M5)
UNSUPPORTED | AI_GBPUSD_H1_BUY_TOUCH_FIBOTIME_SWING_001 | type=13

The Journal line shows that AIObjectTrader encountered the named object and logged its unsupported native type. The EA was attached to GBPUSD M5 while the name encoded H1. The preliminary scanner did not reject that mismatch. This result confirms discovery and type dispatch, while also showing why context validation belongs before geometry evaluation.

Current Boundaries and Key Lessons

FindingMeaning
Program type mattersIndicators can be attached automatically; scripts still require manual launch in the tested environment.
The chart is the contractAI-generated programs and AIObjectTrader communicate through visible, uniquely named native objects.
Protocol and geometry are separateAn object can be recognized successfully even when its native geometry is not implemented.
AI claims require verificationThe Pitchfork assumption was corrected through the MQL5 reference and runtime evidence.
Rule-based execution is a design goalProducer heuristics select anchors; the current EA implements partial recognition and selected geometry rules. Complete validation and trade results remain unverified.


Conclusion

In this article, we demonstrated how natural-language prompts can produce MQL5 indicators and scripts that place native chart objects. We organized the connection to AIObjectTrader through a structured naming format, preliminary recognition, and geometry helpers. The producer selects the anchors; the EA applies programmed rules to the resulting object.

The Fibonacci Time Zones test confirmed object discovery followed by rejection of an unsupported type. Complete protocol validation, context checks, event tracking, and trade-result verification remain necessary before enabling automated execution. You can use the same separation of responsibilities to develop and test additional object adapters.

Our demonstrated path reaches program creation, object placement, and preliminary EA recognition. Supported evaluation and optional trading require their own verification.


Attachments

DrawBuySupportTrendline.mq5 is an additional script in the package. It draws a line from input anchor coordinates rather than detecting fresh swing lows. The three prompt demonstrations above remain focused on the horizontal line, Pitchfork, and Time Zones.

The source files use the Article24080 folder layout shown above. The Origin column identifies the programs generated from the demonstrated prompts and the EA and headers used for object recognition and evaluation. Their descriptions reflect the scope explained in this article.

FileTypeOriginFolder structureDescription
AIObjectTrader.mq5Expert AdvisorArticle trading frameworkMQL5\Experts\Article24080\AIObjectTrader.mq5Scans AI-prefixed chart objects using preliminary name recognition and provides the framework for geometry evaluation and optional order requests. End-to-end trading is not demonstrated.
DeployBuyTouchPitchfork.mq5IndicatorGenerated from the Agentic AI Pitchfork promptMQL5\Indicators\Article24080\DeployBuyTouchPitchfork.mq5Maintains a native GBPUSD H1 OBJ_PITCHFORK and retains its structured name during an active indicator run.
PlaceSellBreakResistanceHLine.mq5ScriptGenerated from the Agentic AI horizontal-resistance promptMQL5\Scripts\Article24080\PlaceSellBreakResistanceHLine.mq5Creates a uniquely named EURUSD H1 OBJ_HLINE resistance level for SELL_BREAK monitoring without placing trades.
AI_GBPUSD_H1_BUY_TOUCH_FIBOTIME_SWING_001.mq5ScriptGenerated from the Agentic AI Fibonacci Time Zones promptMQL5\Scripts\Article24080\AI_GBPUSD_H1_BUY_TOUCH_FIBOTIME_SWING_001.mq5Finds a qualifying GBPUSD H1 swing or the latest pivot fallback and places a uniquely named native OBJ_FIBOTIMES object after manual script launch.
AIObjectGeometry.mqhIncludeArticle support header for deterministic geometryMQL5\Include\Article24080\Core\AIObjectGeometry.mqhProvides reusable geometry adapters for supported graphical objects, including Pitchfork median and outer-line calculations.
AIObjectProtocol.mqhIncludeArticle support header for the structured naming contractMQL5\Include\Article24080\Core\AIObjectProtocol.mqhProvides preliminary prefix, direction, event, numeric-suffix, and Pitchfork-line recognition. Complete positional, context, and role validation is not implemented.
Attached files |
MOL5.zip (23.92 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Recovering Option-Implied Probability Distributions Across Four Markets in MQL5 Recovering Option-Implied Probability Distributions Across Four Markets in MQL5
A practical route from the option chain to probability levels inside MetaTrader 5. For GLD, SLV, SPY, and USO, the risk-neutral density implied by each ETF's options is recovered with the Breeden-Litzenberger identity and checked for static arbitrage, after the method itself is validated against an analytic lognormal. A Python feed publishes the percentiles; an MQL5 service rescales them onto a related symbol, draws levels, and alerts when price leaves the risk-neutral 5 to 95 percent band. Every reading carries its own diagnostics, and full source is included.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Implementing an LLM Agent with SQL Memory in MetaTrader 5 Implementing an LLM Agent with SQL Memory in MetaTrader 5
An LLM agent with SQL memory in MetaTrader 5 — an architecture that eliminates the main limitation of traditional LLM Expert Advisors (EAs): the loss of context between queries. Each agent decision is recorded in SQLite along with the market conditions at the time of the signal — RSI, MA alignment, volatility — and after the position is closed, the outcome is written to the database. When the next request comes in, the agent reviews its own history — similar past RSI conditions and the last three agent decisions — and only then analyzes the current market. The result is a system that remembers its errors between restarts and adapts its behavior based on accumulated statistics, rather than starting from scratch with each new bar.