preview
Building A Candlestick Trend Constraint Model (Part 12) Measuring Lower-Timeframe Trigger Quality

Building A Candlestick Trend Constraint Model (Part 12) Measuring Lower-Timeframe Trigger Quality

MetaTrader 5 — Examples |
480 0
Clemence Benjamin
Clemence Benjamin

You have already built the A–B–C–D detector: completed Candles A and B set direction, while M1 events inside C and D provide the hidden setup and confirmation. The practical problem now is that a detector can be logically correct yet produce triggers that are too late, too rare, or exposed to excessive adverse movement — and without explicit measurements you cannot decide which A–B relationships merit further study. We therefore add an engineering-grade measurement layer that does not change the pattern rules.

Its purpose is reproducible evidence collection: for every completed D window record C setup time, D confirmation time, breakout distance relative to C, directional MFE and MAE from processed M1 ranges, including the confirmation bar’s full range, and the trigger rate. The EA exports CSV records with run_id and record_kind, maintains grouped counters, and displays a compact status summary on the chart and in the journal. The higher‑timeframe opening after D is the commit boundary; detach snapshots are written separately so incomplete windows never contaminate completed statistics.

Contents

  1. Objective and Scope
  2. Defining Trigger Quality
  3. The Observation Lifecycle
  4. Measurement Records
  5. Capturing Timing and Distance
  6. Finalizing Observations
  7. Grouped Statistics
  8. CSV Export
  9. CCanvas Chart Comments
  10. Operating the Expert Advisor
  11. Testing and Verification
  12. Limitations
  13. Conclusion
  14. Key Lessons
  15. Attachments


Objective and Scope

To make these comparisons useful, we keep recognition, measurement, reporting, and execution separate. In Part 11, we established the recognition rules. Here, the measurement layer follows each accepted sequence through its D window and records the outcome without changing those rules.

We implement this measurement layer in an Expert Advisor (EA). It processes new bars, maintains the active observation, writes CSV records, and displays summaries. Trading is disabled by default; an independent execution gate controls any order requests when you enable it.

Responsibility Our implementation boundary
Classification Completed A and B define the relationship and direction.
Setup Completed M1 bars inside C establish the retest and structure response.
Confirmation Completed M1 bars inside D confirm, invalidate, or leave the sequence to expire.
Measurement Timing, breakout distance, MFE, and MAE describe the observed path.
Execution A separate permission and safety gate decides whether a confirmed signal can become an order.


Defining Trigger Quality

We describe trigger quality through separate measurements rather than a single weighted score. Timing, breakout distance, frequency, and excursions answer different questions. Keeping them separate lets you choose comparison criteria explicitly instead of hiding those choices inside a score.

Measure Definition Interpretation
C setup minutes Elapsed server minutes from C’s opening to the accepted setup bar’s opening timestamp. Shows how early or late the hidden setup forms.
D confirmation minutes Elapsed server minutes from D’s opening to the confirmation bar’s opening timestamp. Measures the delay before confirmation.
Breakout points Directional distance between the signal close and the relevant C extreme. Shows how far the confirming bar closed beyond C.
MFE Maximum favorable distance from the signal close across processed M1 ranges, including the confirmation bar, in points. Describes the largest favorable distance in the observed bars.
MAE Maximum adverse distance from the signal close across processed M1 ranges, including the confirmation bar, in points. Describes the largest adverse distance in the observed bars.
Trigger rate Triggered sequences divided by completed sequences for a relationship. Describes frequency, not profitability.

For a bullish sequence, favorable movement is the M1 high minus the signal close, while adverse movement is the signal close minus the M1 low. The signs reverse for a bearish sequence. We retain the maximum nonnegative value observed in each direction. Commission, swap, slippage, and exit rules are deliberately absent from these path measurements.

 Converting the A-B-C-D sequence into a finalized observation

Fig. 1. Converting the A-B-C-D sequence into a finalized observation

Our Part 11 detector provides the recognition rules. The attached Candlestick_Trend_Constraint_P12.mq5 contains the complete A-B classifier, C scanner, D evaluator, replay, initialization, execution guards, and reporting calls. The extracts below focus on measurement and its lifecycle; read those functions together in the full MQ5 file when tracing a signal end to end.


The Observation Lifecycle

Completed A and B supply the context; M1 bars inside C and D supply setup and confirmation. At the next higher-timeframe opening, D ends and ReportCompletedSequence records its final state. On removal, OnDeinit records a snapshot of the unfinished D window. The snapshot does not enter the completed statistics.

The boundary keeps shutdown snapshots out of completed averages. A snapshot can still show what the EA knew when it was removed, but its excursions cover only the observed portion of D.

RefreshDailySequence compares the current D opening time with the active window. When they differ, it reports the ended window before InitializeSequence replaces its candles and state. During startup, ReplayDHistory restores the M1 path of the new D without placing orders for signals that occurred before attachment.

//+------------------------------------------------------------------+
//| Refreshes the sequence when a new higher-timeframe D candle opens|
//+------------------------------------------------------------------+
bool RefreshDailySequence(const bool replay_history)
  {
//--- The four newest context candles are required for A-B-C-D
   MqlRates daily[];
   ArraySetAsSeries(daily,true);

   ResetLastError();
   int copied=CopyRates(_Symbol,InpContextTimeframe,0,4,daily);
   if(copied!=4)
     {
      PrintFormat("Daily context unavailable: copied=%d, error=%d",
                  copied,GetLastError());
      return(false);
     }

//--- Nothing changes while the current D candle is still forming
   if(g_active_d_time==daily[0].time)
      return(true);

//--- A new D candle closes the previous sequence and opens the next
   ReportCompletedSequence();
   if(!InitializeSequence(daily))
      return(false);

//--- At startup replay the already closed part of D without trading
   if(replay_history)
      ReplayDHistory();

   return(true);
  }

The CopyRates check prevents an incomplete four-candle context from replacing the active sequence. The call order keeps the completed window separate from a snapshot recorded during removal.

Components and safety boundaries of the measurement EA

Fig. 2. Components and safety boundaries of the measurement EA


Measurement Records

The sequence structure stores the active observation: four context candles, relationship, direction, event times, signal price, MFE, and MAE. We use a separate aggregate structure for each A-B relationship so the EA can replace its live context without losing the totals from earlier completed windows.

SRelationStats holds terminal-outcome counters and measurement sums. The EA calculates averages when reporting them. We keep the sums so each reported measure can use its appropriate denominator.

//+------------------------------------------------------------------+
//| Aggregated measurements for one A-B relationship                 |
//+------------------------------------------------------------------+
struct SRelationStats
  {
   int                  sequences;
   int                  triggered;
   int                  invalidated;
   int                  expired;
   double               sum_mfe_points;
   double               sum_mae_points;
   double               sum_confirmation_minutes;
  };

The relationship enumeration acts as the index into the statistics array, so each pattern class receives an independent record. The counters retain all terminal outcomes, while the accumulated measurements use only triggered observations. An expired sequence belongs in the trigger-rate denominator, but it has no post-signal excursion and must not dilute the MFE or MAE average.


Capturing Timing and Distance

The EA joins timeframes by bar timestamps instead of assuming a fixed number of M1 bars inside D. This identifies event order in available history, but it cannot reconstruct missing minutes. Before analyzing a window, verify that the required M1 history is sufficiently complete for the symbol and trading session.

MinutesBetween computes elapsed server-clock minutes between events. It returns 0.0 when timestamps are missing or reversed, and also for a valid zero-minute interval. ElapsedText keeps missing durations empty in the CSV. Neither function measures active trading minutes across session breaks.

//+------------------------------------------------------------------+
//| Returns the number of minutes between two valid timestamps       |
//+------------------------------------------------------------------+
double MinutesBetween(const datetime first,const datetime second)
  {
   if(first<=0 || second<first)
      return(0.0);

   return((double)(second-first)/60.0);
  }

//+------------------------------------------------------------------+
//| Returns a CSV-safe elapsed time or empty for an absent event     |
//+------------------------------------------------------------------+
string ElapsedText(const datetime first,const datetime second)
  {
   if(first<=0 || second<first)
      return("");

   return(DoubleToString(MinutesBetween(first,second),1));
  }

C setup latency begins at the opening time of C, whereas D confirmation latency begins at the opening time of D. Both stored event timestamps identify the M1 bar’s opening; the closing decision becomes available only after that bar completes. These are elapsed server minutes, not counts of traded minutes. Keeping these origins explicit prevents the two waiting periods from being mixed in later analysis. Breakout distance uses the C high for bullish signals and the C low for bearish signals. We divide every distance by SYMBOL_POINT so the CSV, inputs, and journal use the same point-based unit.

UpdateExcursion measures directional movement from the signal close using each processed M1 bar’s high and low. For a BUY, favorable movement is high minus signal price; adverse movement is signal price minus low. The SELL calculations reverse these directions. The maxima remain nonnegative and are stored in price units until finalization converts them to points.

//+------------------------------------------------------------------+
//| Updates maximum favorable and adverse movement after a signal    |
//+------------------------------------------------------------------+
void UpdateExcursion(const MqlRates &bar)
  {
   if(g_sequence.state!=SEQUENCE_TRIGGERED)
      return;

//--- Measure movement relative to the signal price by direction
   double favorable=0.0;
   double adverse=0.0;

   if(g_sequence.direction>0)
     {
      favorable=bar.high-g_sequence.signal_price;
      adverse=g_sequence.signal_price-bar.low;
     }
   else
     {
      favorable=g_sequence.signal_price-bar.low;
      adverse=bar.high-g_sequence.signal_price;
     }

//--- Keep the worst-case favorable and adverse extremes
   g_sequence.mfe=MathMax(g_sequence.mfe,MathMax(0.0,favorable));
   g_sequence.mae=MathMax(g_sequence.mae,MathMax(0.0,adverse));
  }

EvaluateDMinute calls this function on the confirmation bar as well as subsequent processed bars. Consequently, the confirmation bar’s full range can include prices reached before its closing signal. These values are bar-range diagnostics, not strictly post-confirmation tick excursions. Live processing evaluates the latest closed bar; it does not recover every missed minute, and the context rollover can omit the final old-D bar. A completed record means that D has ended, not that history coverage has been proven complete.

BreakoutDistancePoints measures how far the confirmation close lies beyond C’s directional extreme. It uses C high for a BUY and C low for a SELL, divides by the symbol’s point size, and clamps the result to zero. A non-triggered state returns zero internally; the CSV writer leaves the corresponding measurement empty instead of exporting it as a valid zero.

//+------------------------------------------------------------------+
//| Returns the D breakout distance in points                        |
//+------------------------------------------------------------------+
double BreakoutDistancePoints(void)
  {
   if(g_sequence.state!=SEQUENCE_TRIGGERED)
      return(0.0);

   double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   if(point<=0.0)
      return(0.0);

   if(g_sequence.direction>0)
      return(MathMax(0.0,(g_sequence.signal_price-g_sequence.c.high)/point));

   if(g_sequence.direction<0)
      return(MathMax(0.0,(g_sequence.c.low-g_sequence.signal_price)/point));

   return(0.0);
  }


Finalizing Observations

The next higher-timeframe opening is the commit boundary for the previous observation. ReportCompletedSequence checks that a sequence exists and has not already been handled, then converts its stored MFE and MAE from price units to points. Aggregation, export, and the journal receive the same converted measurements; an export failure can still leave the in-memory count ahead of the CSV.

RefreshDailySequence() calls this function before initializing the next A-B-C-D context. The ordering is essential: the previous sequence must be committed while its relationship, signal time, state, and excursions are still available. Only after that commit may the live structure be cleared for the next candle block.

InitializeSequence assigns the new D opening time before handling inactive cases and returns true on every current path. An unclassified relationship or missing C setup becomes EXPIRED; a qualifying setup becomes TRACK_D. At D’s end, TRACK_D becomes EXPIRED. IDLE is a transient initialization state, not an additional completed outcome. Thus the denominator includes all initialized, ended D windows, including inactive ones; it is not the success rate among armed setups. The entry guard below also prevents recommitting an already handled window if this function is called again.

//+------------------------------------------------------------------+
//| Reports the completed sequence before the next D1 cycle begins   |
//+------------------------------------------------------------------+
void ReportCompletedSequence(void)
  {
//--- Report each existing window at most once per attachment
   if(g_active_d_time==0 || g_sequence_flushed)
      return;

//--- An armed but unconfirmed window expires only after D has ended
   if(g_sequence.state==SEQUENCE_TRACK_D)
      g_sequence.state=SEQUENCE_EXPIRED;

   double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
//--- Convert the extremes into points for the journal
   double mfe_points=(point>0.0 ? g_sequence.mfe/point : 0.0);
   double mae_points=(point>0.0 ? g_sequence.mae/point : 0.0);

//--- Export the ended D window before adding it to the report totals
   bool exported=ExportObservation(mfe_points,mae_points,"completed");
   AggregateCompletedSequence(mfe_points,mae_points);
   g_sequence_flushed=true;
   if(!exported)
      Print("Completed window counted in memory; CSV export failed.");

   PrintFormat("ABCD complete: D=%s, relation=%s, state=%s, "
               "MFE=%.1f points, MAE=%.1f points, samples=%d",
               TimeToString(g_sequence.d.time,TIME_DATE),
               RelationName(g_sequence.relation),
               StateName(g_sequence.state),mfe_points,mae_points,
               g_completed_sequences);
  }

ReportCompletedSequence marks the ended D window handled after the export attempt and in-memory aggregation. It logs an export failure because memory totals can differ from stored CSV rows. The flag is checked at entry, preventing repeated processing within one attachment; it does not persist across reattachments.

When you remove the EA during D, OnDeinit writes a row with record_kind=snapshot and leaves completed counts unchanged. It then prints the completed-window summary, releases the canvas and, when enabled, deletes only chart objects with our EA prefix. This keeps reporting and cleanup within the EA’s own lifecycle.

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Preserve the live D state as a partial snapshot without aggregation
   if(g_active_d_time!=0 && !g_sequence_flushed)
     {
      double flush_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
      double flush_mfe=(flush_point>0.0 ? g_sequence.mfe/flush_point : 0.0);
      double flush_mae=(flush_point>0.0 ? g_sequence.mae/flush_point : 0.0);
      bool snapshot_written=ExportObservation(flush_mfe,flush_mae,"snapshot");
      g_sequence_flushed=true;
      PrintFormat("Removal snapshot: CSV=%s, completed samples=%d",
                  (snapshot_written ? "written" : "failed"),
                  g_completed_sequences);
     }

//--- Preserve a final grouped summary after the optional snapshot flush
   PrintStatistics();
//--- Release the canvas first, then the optional owned objects
   DestroyStatusCanvas();

   if(InpDeleteObjectsOnRemoval)
      ObjectsDeleteAll(0,g_prefix);

   ChartRedraw(0);
  }

The prefix limits object cleanup to the EA’s drawings. The in-memory g_sequence_flushed flag avoids repeated processing in one attachment, but reattaching to the same D can write another snapshot. The run_id distinguishes attachments; it does not make rows globally unique. Check D time and record kind within each run, and isolate test files before combining data.


Grouped Statistics

Each ENUM_AB_RELATION value indexes one SRelationStats element. AggregateCompletedSequence() validates that index before changing any counters. This guard prevents an undefined relationship from writing outside the array or contaminating a valid group.

The aggregation function accepts only explicit terminal states: triggered, invalidated or expired. An armed TRACK_D sequence stays out of the totals until D ends. Only triggered records contribute MFE, MAE and confirmation-time sums.

//+------------------------------------------------------------------+
//| Aggregates one completed sequence by its A-B relationship        |
//+------------------------------------------------------------------+
void AggregateCompletedSequence(const double mfe_points,
                                const double mae_points)
  {
   int index=(int)g_sequence.relation;
   if(index<0 || index>=ArraySize(g_stats) ||
      (g_sequence.state!=SEQUENCE_TRIGGERED &&
       g_sequence.state!=SEQUENCE_INVALIDATED &&
       g_sequence.state!=SEQUENCE_EXPIRED))
      return;

//--- Every completed D window contributes one relationship sample
   g_stats[index].sequences++;
   g_completed_sequences++;

   if(g_sequence.state==SEQUENCE_TRIGGERED)
     {
      g_stats[index].triggered++;
      g_stats[index].sum_mfe_points+=mfe_points;
      g_stats[index].sum_mae_points+=mae_points;
      g_stats[index].sum_confirmation_minutes+=
         MinutesBetween(g_sequence.d.time,g_sequence.d_signal_time);
      g_triggered_sequences++;
     }
   else
      if(g_sequence.state==SEQUENCE_INVALIDATED)
         g_stats[index].invalidated++;
      else
         if(g_sequence.state==SEQUENCE_EXPIRED)
            g_stats[index].expired++;
  }

You can now compare two distinct properties: how often an ended window triggered, and what movement the EA recorded for triggered cases. The overall rate includes inactive windows, while excursion and confirmation averages use triggered cases only. These are descriptive measurements; comparing them requires adequate samples and consistent observation conditions.


CSV Export

The FileOpen() function confines MQL5 file access to the terminal file sandbox. ExportObservation() opens the configured file for reading and writing, then checks INVALID_HANDLE before attempting another file operation. A failed open is therefore reported in the Experts journal instead of allowing the export path to continue with an invalid resource.

When the CSV file is empty, ExportObservation writes its schema. It appends typed records with run_id and record_kind, then checks writing and flushing results. The exporter leaves confirmation and excursion fields empty when the state is not triggered. Use a separate file for each test configuration so its observations share the same rules and columns.

//+------------------------------------------------------------------+
//| Appends a typed completed record or a partial shutdown snapshot  |
//+------------------------------------------------------------------+
bool ExportObservation(const double mfe_points,const double mae_points,
                       const string record_kind)
  {
   if(!InpExportCSV)
      return(true);

   ResetLastError();
   int handle=FileOpen(InpCSVFileName,
                       FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI,';');
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("FileOpen() failed for %s. Error=%d",
                  InpCSVFileName,GetLastError());
      return(false);
     }

   bool success=true;
//--- Write the schema when the export file is empty
   if(FileSize(handle)==0)
     {
      ResetLastError();
      if(FileWrite(handle,"run_id","record_kind","symbol",
                   "context_timeframe","trigger_timeframe","d_time",
                   "relation","direction","final_state","c_setup_time",
                   "d_signal_time","c_setup_minutes",
                   "d_confirmation_minutes","breakout_points",
                   "mfe_points","mae_points")==0 || GetLastError()!=0)
         success=false;
     }

//--- Preserve missing measurements as empty CSV fields
   string c_minutes=ElapsedText(g_sequence.c.time,g_sequence.c_setup_time);
   string d_minutes="";
   string breakout="";
   string mfe="";
   string mae="";
   if(g_sequence.state==SEQUENCE_TRIGGERED)
     {
      d_minutes=ElapsedText(g_sequence.d.time,g_sequence.d_signal_time);
      breakout=DoubleToString(BreakoutDistancePoints(),1);
      mfe=DoubleToString(mfe_points,1);
      mae=DoubleToString(mae_points,1);
     }

   if(success)
     {
      ResetLastError();
      if(!FileSeek(handle,0,SEEK_END) || GetLastError()!=0)
         success=false;
     }
   if(success)
     {
      ResetLastError();
      if(FileWrite(handle,g_run_id,record_kind,_Symbol,
                   EnumToString(InpContextTimeframe),
                   EnumToString(InpTriggerTimeframe),
                   TimeToString(g_sequence.d.time,TIME_DATE|TIME_MINUTES),
                   RelationName(g_sequence.relation),g_sequence.direction,
                   StateName(g_sequence.state),
                   TimestampText(g_sequence.c_setup_time),
                   TimestampText(g_sequence.d_signal_time),c_minutes,
                   d_minutes,breakout,mfe,mae)==0 || GetLastError()!=0)
         success=false;
     }

   ResetLastError();
   FileFlush(handle);
   int flush_error=GetLastError();
   FileClose(handle);
   if(!success || flush_error!=0)
     {
      PrintFormat("CSV export failed for %s. Flush error=%d",
                  InpCSVFileName,flush_error);
      return(false);
     }
   return(true);
  }

FileFlush requests a write before the file handle closes. The default file, CTC_P12_TriggerQuality_v2.csv, includes both completed records and partial snapshots, identified by record_kind. Export errors are logged; successful return values are not a substitute for reading the exported file.


CCanvas Chart Comments

The CCanvas class is used only to display chart comments. UpdateStatus() reads the current analytical state and converts it into eight concise lines. These report the relationship, direction, sequence state, C setup time, completed sample count, trigger rate, excursion averages, and CSV status.

The function returns immediately when the bitmap resource is unavailable. This guard keeps a visualization failure outside the detector, measurement, export, and execution paths. The canvas remains an observer rather than a dependency of the trading model.

//+------------------------------------------------------------------+
//| Draws state and measurement comments through CCanvas             |
//+------------------------------------------------------------------+
void UpdateStatus(void)
  {
   if(!g_status_canvas_ready)
      return;

//--- Compose the compact state lines from the runtime sequence
   string direction="Neutral";
   if(g_sequence.direction>0)
      direction="Bullish";
   else
      if(g_sequence.direction<0)
         direction="Bearish";

   string c_setup=(g_sequence.c_setup_time>0 ?
                   TimeToString(g_sequence.c_setup_time,
                                TIME_DATE|TIME_MINUTES) :
                   "none");

   double average_mfe=0.0;
   double average_mae=0.0;
   OverallExcursionAverages(average_mfe,average_mae);

   string lines[8];
   lines[0]="Candlestick Trend Constraint Part 12";
   lines[1]="A-B: "+RelationName(g_sequence.relation);
   lines[2]="Direction: "+direction;
   lines[3]="State: "+StateName(g_sequence.state);
   lines[4]="C setup: "+c_setup;
   lines[5]=StringFormat("Samples: %d | triggered: %d | rate: %.1f%%",
                         g_completed_sequences,g_triggered_sequences,
                         OverallTriggerRate());
   lines[6]=StringFormat("Triggered averages: MFE %.1f | MAE %.1f points",
                         average_mfe,average_mae);
   lines[7]="CSV: "+(InpExportCSV ? InpCSVFileName : "disabled");

//--- Pick a text color that contrasts with the chart background
   color bg=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND);
   uint bg_rgb=(uint)bg;
   double luminance=(0.299*(bg_rgb&0xFF)+
                     0.587*((bg_rgb>>8)&0xFF)+
                     0.114*((bg_rgb>>16)&0xFF));
   color text_rgb=(luminance>128.0 ? clrBlack : clrWhite);
   uint text_color=ColorToARGB(text_rgb,255);

//--- Clear the previous text while keeping the canvas transparent
   g_status_canvas.Erase(0);
   for(int i=0;i<ArraySize(lines);i++)
      g_status_canvas.TextOut(4,4+i*18,lines[i],text_color);

   g_status_canvas.Update();
  }

The display code calculates chart-background luminance and selects black or white text for contrast. Erase clears the previous frame, TextOut draws the status lines, and Update publishes the bitmap. These operations keep the comments readable as the observation changes.

The detector and exporter continue to work when CCanvas is disabled. We keep the display independent so changes to chart comments do not alter recognition, measurement, or export.


Operating the Expert Advisor

Input Default Purpose
InpContextTimeframe D1 Defines the completed A-B-C-D context.
InpTriggerTimeframe M1 Provides the hidden setup and confirmation path.
InpExportCSV true Writes completed D windows and removal-time snapshots as separately labeled CSV records.
InpCSVFileName CTC_P12_TriggerQuality_v2.csv Names the dataset inside the terminal file sandbox.
InpShowStatusPanel true Displays chart comments through CCanvas.
InpDeleteObjectsOnRemoval true Deletes only chart objects owned through our EA prefix when the EA is removed.
InpEnableTrading false Keeps measurement independent from optional execution.

Compile Candlestick_Trend_Constraint_P12.mq5 in MetaEditor, attach it to the symbol you want to study, and begin with trading disabled. Ensure that the D1 and M1 histories are available. The EA processes newly completed trigger bars; if you attach it during D, replay reconstructs the analytical state without executing historical signals.

For a meaningful dataset, keep the relationship and trigger inputs fixed throughout one experiment. Changing thresholds midway causes the same CSV file to mix observations produced by different rules. Use a new filename whenever you change the model configuration materially.


Testing and Verification

We use the Strategy Tester to observe the recognition and measurement paths with execution disabled. The checks below describe the expected behavior; the results that follow identify what the completed run and inspected CSV actually establish.

Test Procedure Expected evidence
Compilation Compile our EA in MetaEditor. Record the exact error and warning counts.
Single finalization Run across several D1 boundaries and compare journal completions with CSV rows. Observed in the completed run: 313 completed CSV rows match the journal, with no repeated run/symbol/D keys. Restart behavior still requires separate testing.
Replay safety Start midway through D with trading enabled on a demo test. State is reconstructed without an order for a historical trigger.
Excursion direction Inspect one bullish and one bearish trigger bar by bar. MFE and MAE use the correct directional extremes.
CSV append Reinitialize without deleting the dataset. The header remains single and new observations append at the end.
Canvas cleanup Remove and reattach our EA. No orphaned bitmap-label resource remains.

Use "Every tick based on real ticks" when available, or "1 minute OHLC" for structural testing. "Open prices only" is unsuitable for verifying the timing of this model’s M1 processing; access to historical bars alone does not establish the required event sequence. Record the MetaTrader 5 build, symbol, data source, account mode, date range, modeling mode, spread assumptions, and complete input set with every experiment.

Observed demonstration results

We ran the EA with trading disabled to examine measurement and export across complete D windows. The completed run produced 313 observations and one separately labeled shutdown snapshot. We opened the CSV and matched every completed row’s D date, relationship, final state, MFE, and MAE to the corresponding journal entry.

Run condition Recorded setting
Build and data MetaTester 5 build 6230; EURUSD history synchronized from the terminal’s demo-server data source.
Timeframes and account H1 tester chart; D1 context; M1 trigger; initial simulated deposit USD 10,000, leverage 1:100. Netting/hedging mode was not recorded.
Configured test interval 1 January 2024 00:00 to 31 December 2024 00:00, server time. The run finished normally at 30 December 2024 23:59:59.
Modeling and spread Every tick (generated ticks), not real ticks. Actual spread conditions were not recorded; InpMaxSpreadPoints=50 is an execution filter, not the modeled spread.
Run isolation The journal contains an earlier interrupted attempt. The figures below use the subsequent completed run and the matching CSV only.
CSV identity One run_id: EURUSD_1704067200_612916875. Completed rows span D openings from 29 December 2023 to 29 December 2024; the first is a carried-in context before the configured start. D=30 December 2024 is a shutdown snapshot.

These settings matter because the results below should be read as an audit of the measurement pipeline under one specific test configuration, not as a universal estimate of pattern behavior. In particular, the use of generated ticks and a single symbol-period means the run is suitable for internal consistency checks, but not for broad performance claims.

We used the input settings listed below.

Input Value
InpContextTimeframe 16408
InpTriggerTimeframe 1
InpBreakBufferPoints 10
InpReversalCloseFraction 0.5
InpSwingDepth 3
InpRetestTolerancePoints 30
InpMinBodyRangeRatio 0.55
InpDrawSequence true
InpDeleteObjectsOnRemoval false
InpShowStatusPanel true
InpExportCSV true
InpCSVFileName CTC_P12_TriggerQuality.csv
InpEnableTrading false
InpVolume 0.1
InpRiskReward 2.0
InpStopBufferPoints 100
InpMaxSpreadPoints 50
InpDeviationPoints 20
InpMagicNumber 24516012

Keeping these inputs fixed is important for interpretation. Because the article’s goal here is observation rather than optimization, the main question is whether the EA measured and exported completed windows consistently under one unchanged rule set.
Measurement Result
Completed observations 313
Triggered / invalidated / expired 40 / 22 / 251
Triggered-window share 40 / 313 = 12.8%
Mean triggered MFE / MAE 249.7 / 234.8 points
Mean confirmation timestamp offset 475.1 server minutes from D opening
Separate shutdown snapshots 1; excluded from completed totals
Duplicate completed keys 0 within this CSV, using run_id, symbol, and D opening

The aggregate figures show that most completed D windows did not trigger, which is exactly why separating frequency from excursion statistics is useful. A low trigger share does not automatically imply poor quality; it simply describes how often the full setup reached confirmation under these rules.
A-B relationship Windows Triggered Invalidated Expired Triggered share Mean MFE, points Mean MAE, points
Bullish continuation 43 10 6 27 23.3% 292.7 184.4
Bearish reversal 30 3 5 22 10.0% 184.0 420.0
Bearish continuation 47 9 5 33 19.1% 209.2 363.7
Inside-bar compression 60 18 4 38 30.0% 257.0 167.4
Unclassified 120 0 0 120 0.0% — —
Bullish reversal 13 0 2 11 0.0% — —

Inside-bar compression triggered in 18 of 60 windows, the highest observed share among these groups. Bullish reversal produced no triggers in 13 windows. These differences identify candidates for further study; unequal sample sizes and a single symbol-period do not establish that one relationship has a trading advantage.

The 12.8% overall rate includes 120 unclassified windows. It measures frequency across all counted D windows, not conversion among armed setups. Excursion averages use only the 40 triggered completed records. Non-triggered rows leave confirmation, breakout, MFE, and MAE fields empty, and the shutdown snapshot does not enter these totals.

This generated-tick run demonstrates the reporting path and agreement between the exported observations and journal. It does not independently validate the detector or M1 coverage. The excursion calculation includes the confirmation bar’s full range, and the documented processing gaps remain relevant. No trades were enabled, so these results are not trading returns. We disabled object cleanup for this run, so cleanup testing remains separate.

The earlier attachment-and-removal illustration below demonstrates replay and chart-object handling; it is separate from the completed tester run.

Attaching the Expert on chart and Comparing some feature improvement with Part 11

Fig. 3. Attaching and removing Parts 11 and 12 while observing trigger reconstruction, CSV flushing, and owned-object cleanup

The additional tester image shows the interrupted attempt at an intermediate checkpoint: 39 samples and a 17.9% share. Use the completed-run CSV and summary above for the final figures.

CTC_P12_Tester

Fig. 4. Intermediate tester checkpoint during the earlier interrupted attempt


Limitations

  • Our statistics exist in memory for the current EA session; the CSV preserves observations across sessions, but our EA does not reload and aggregate earlier rows.
  • Reattachment during the same D can append another partial snapshot. run_id separates sessions but does not prevent duplicate market windows.
  • The D1 boundary follows broker-server time, so different brokers can produce different higher-timeframe candles.
  • Completed-row MFE and MAE end with Candle D; snapshot excursions cover only the observed portion of D.
  • An expired window enters frequency statistics only after D ends; its post-signal excursion fields remain empty.
  • The CSV does not include commission, swap, slippage, or a complete exit policy.
  • A high triggered-window share or favorable average does not establish robustness, causality, or profitability.
  • Reliable comparison requires adequate samples, several instruments, multiple market regimes, and out-of-sample testing.


Conclusion

In this article, we have extended the candlestick-constraint detector into a measurable experiment without altering detection logic. The EA now manages an explicit observation lifecycle: it tracks each A–B relationship, records terminal states (triggered/invalidated/expired), converts excursions into point units, and commits completed observations at the next higher‑timeframe opening. Observations and removal‑time snapshots are exported to a stable CSV schema (including run id and record kind), while grouped in‑memory SRelationStats accumulate per‑relation counters and sums for later averaging.

Visualization via a nonessential CCanvas status panel and a strict object‑prefix lifecycle keep reporting observational and reversible. Crucially, detection remains decoupled from execution: an independent permission gate decides whether a confirmed signal becomes an order. The practical result is a reproducible dataset that lets you compare candidate trigger paths by frequency, latency, breakout magnitude, and MFE/MAE — enabling data-driven selection of promising relationships without conflating movement statistics with claims about profitability. Continued work requires adequate sample sizes, multi‑instrument and out‑of‑sample testing, and explicit inclusion of trading costs when moving from measurement to performance evaluation.

Key Lessons

Lesson Description
Measure a signal before optimizing it Preserving timing, distance, MFE, and MAE separately reveals behavior before arbitrary weights turn them into a score.
Higher-timeframe closed-bar relationships control direction Completed A-B context provides a stable constraint, while the lower timeframe reveals the hidden retest, displacement, and structure path that makes the context actionable.
Separate recognition from outcome evaluation Our detector decides whether the pattern occurred; our measurement layer describes what followed without changing that decision.
Use a natural completion event as the commit boundary The next higher-timeframe opening ends D; shutdown snapshots remain outside completed statistics. Completeness of the processed M1 path must be checked separately.
Join timeframes with timestamps Bar opening times locate C and D across gaps and server-specific boundaries; missing M1 bars still require separate coverage checks.
Restore analytical state without executing historical signals Replay reconstructs context after initialization, while only live processing may authorize execution.
Treat missing outcomes explicitly Expired and invalidated sequences belong in frequency statistics but must not be assigned fictional post-signal excursions.
Keep visualization observational CCanvas reports state and summaries without becoming an input to classification, measurement, or trading.
A signal and an accepted order are different outcomes Analytical confirmation remains separate from permissions, ownership, spread, volume, stops, and server acceptance.
Design exports for reproducibility Stable columns, typed records, explicit timeframes and a run identifier support auditing. Check repeated D windows across runs before comparative analysis.
Own the complete chart-object lifecycle Create objects with a unique prefix and delete that same namespace during deinitialization so removing an EA restores a clean chart without touching unrelated objects.
Make cross-session identity explicit An in-memory one-time flag prevents duplicate writes within one attachment, while persistent datasets still need a run identifier or a stable deduplication key across reattachments.


Attachments

Filename Type Purpose
Candlestick_Trend_Constraint_P12.mq5 Expert Advisor Complete A-B-C-D detector, measurement collector, CSV exporter, CCanvas comments, and optional execution logic.
Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 7): Arcs, Circles and Rounded Rectangles Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 7): Arcs, Circles and Rounded Rectangles
This part finishes the geometry layer: arcs use a sagitta‑based angular step tied to the pixel tolerance, so segment counts are computed, not guessed. The path gains ArcTo, AddCircle, AddEllipse and AddRoundRect with per‑corner radii, enabling common UI shapes such as tabs, pills, badges, rings, gauges and donut wedges.
Optimal Trade Exit in MQL5: Longstaff-Schwartz Least-Squares Monte Carlo for Continuation-Value Estimation Optimal Trade Exit in MQL5: Longstaff-Schwartz Least-Squares Monte Carlo for Continuation-Value Estimation
This article models position exits as an American option and implements the Longstaff–Schwartz least‑squares Monte Carlo in native MQL5. The engine calibrates drift and volatility from recent returns, simulates forward price paths, and uses backward induction to compare immediate exercise to continuation. It integrates into an EA with hard time and price backstops and a live indicator, and is tested on four symbol/timeframe combinations with mixed, reproducible Strategy Tester results.
How to Train an MLP Using Markov Chain Features in MQL5 How to Train an MLP Using Markov Chain Features in MQL5
The article describes the two-level MarkovMLPOscillator indicator: a three-state Markov chain uses historical data to construct a transition matrix and generate 15 probabilistic features for each bar, and an MLP is trained on these features to predict price direction at a specified forecast horizon. The article covers feature generation, the validation procedure using a holdout set, and parameter settings. The result is an interpretable oscillator with a color histogram, a smoothed signal, and a display of the current transition matrix.
Building a Prop-Firm Rule Guard in MQL5 (Part 1): The Observation Layer Building a Prop-Firm Rule Guard in MQL5 (Part 1): The Observation Layer
Prop-firm accounts operate under a second rulebook that MetaTrader 5 does not model. This first part builds the observation layer: a per-pass frozen account snapshot read by every rule, a verdict record that reports without acting, explicit enable flags in the profile, and a session clock aligned to the firm's reset time. The result is a reproducible pass that prevents cascading effects and prepares the ground for safe decisions in Part 2.