preview
Building A Candlestick Trend Constraint Model (Part 13): Normalizing and Scoring Lower-Timeframe Trigger Quality

Building A Candlestick Trend Constraint Model (Part 13): Normalizing and Scoring Lower-Timeframe Trigger Quality

MetaTrader 5 — Examples |
74 0
Clemence Benjamin
Clemence Benjamin

Trading signals confirmed at an M1 close are easy to detect, but hard to evaluate cleanly: if you let later highs/lows or a recalculated feature creep into the “score,” you introduce look‑ahead bias and contaminate any downstream analysis. This article implements an observational layer that solves that problem by freezing the information that was actually available at confirmation and by recording subsequent price behavior separately.

Concretely, the EA captures three normalized features at the confirming M1 close (body fraction, relative breakout, and elapsed fraction), combines them into an equal‑weight preliminary score, and stamps that snapshot with an availability time and validity flag. All post‑confirmation outcomes (MFE/MAE measured only on later M1 bars) are collected independently. Exports distinguish snapshots from completed observations with a record kind field so you can filter for record kind=completed, state=Triggered, score_valid=1 and obtain a dataset suitable for hypothesis testing without peeking into the future. This is a reproducible diagnostic artifact for investigating signal quality - not a validated trading edge - and requires explicit handling of history coverage, fixed horizons, and run isolation.

Contents

  1. Where we are taking the model
  2. Choosing the Decision Boundary
  3. Defining Transparent Normalized Features
  4. Freezing the Score at Confirmation
  5. Measuring Outcomes After the Signal
  6. Processing Minutes and Finalizing Observations
  7. Keeping Snapshots Distinct from Completed Rows
  8. Operating the Expert Advisor
  9. Reviewing the Results
  10. Key lessons
  11. Conclusion
  12. Attached source


Where we are taking the model

In our first article, I explored a single developing daily candle as a directional constraint. Our goal has evolved into an explainable model that uses adjacent completed candles, A and B, to evaluate lower-timeframe opportunities inside C or D.

In Part 11, we implemented a specific interpretation: A–B constrains direction, the M1 path inside C establishes a setup, and D confirms or invalidates the opportunity. We retain that detector here and add a scoring layer for evaluating its confirmed signals.

The broader model can accommodate confirmation within C, within D, or across both. We aim to identify which combinations improve signal reliability through comparison with the same lower-timeframe conditions without A–B context.

Visual examples of the broader A–B concept

The following illustrations show that broader idea using hypothetical M1 RSI conditions, which are not implemented in this EA. A and B are adjacent higher-timeframe candles; the small candles represent excerpts of M1 activity. The illustrative RSI values are not calculated from the drawn prices.

In the bearish example, B’s body engulfs A’s bullish body. We then examine a rally inside C for M1 RSI at or above 70. This identifies a candidate short setup against the bearish context without requiring a break of B’s low.

AB_Context_Trigger_In_C

Fig. 1. A bearish A–B engulfing pattern provides context for an M1 RSI overbought candidate inside C.

In the bullish example, both A and B close bullish, with B closing above A’s high. After C completes, we examine a pullback inside D for M1 RSI at or below 30. This identifies a candidate long setup without requiring a lower-timeframe break above B’s high.

AB_Context_Trigger_In_D

Fig. 2. A bullish A–B continuation provides context for an M1 RSI oversold candidate inside D.

We evaluate these candidates at an M1 close. An extreme RSI reading does not confirm a reversal: price may continue in the same direction. Before testing this variation, we must specify the RSI settings, eligible window, confirmation rule, and invalidation rule.


Choosing the Decision Boundary

The EA scores a trigger after its confirming M1 candle closes, when its body, closing distance beyond C, and elapsed time within D are known. Later D extremes belong to the outcome record; including them in the score would introduce look-ahead bias.

The score describes how a confirmed trigger matches our feature preferences. It does not predict confirmation itself, which has already occurred.

Fig. 3. Known features lead to a frozen score; later outcomes test its usefulness.

Fig. 3. Known features lead to a frozen score; later outcomes test its usefulness.

The score record retains the trigger candle’s opening time as its identity. The EA adds one M1 period to determine nominal feature availability. Live processing occurs on the next relevant tick, so this boundary is not an execution timestamp.


Defining Transparent Normalized Features

We chose C’s completed range as the fixed price scale. A 20-point breakout has a different relative size against a 40-point range than against a 400-point range. This normalization does not remove differences in spread, liquidity, contract specifications, or volatility regime.

Quantity Definition Interpretation
C range C high − C low Positive price scale known before D.
Body fraction, B abs(trigger close − trigger open)/trigger range How much of the confirming candle is body.
Breakout fraction, X Directional distance beyond C/C range Raw relative breakout; retained uncapped in CSV.
Breakout component, K min(1, max(0, X/saturation)) Saturation defaults to 0.50 of C range.
Elapsed fraction, E clamp((confirmation boundary − D open)/nominal context seconds, 0, 1) Uses elapsed clock time, not a count of observed bars.
Preliminary score, S 100 × (B + K + (1 − E))/3 Equal-weight descriptive score; not a probability.

A larger body fraction, a breakout approaching the cap, and earlier confirmation raise the score by design. Equal weights make these preferences transparent; their predictive value remains untested.

The EA uses PeriodSeconds() to obtain the nominal context duration. Select an explicit M2–D1 context. The trigger timeframe must be M1. Weekly, monthly, and chart-dependent context settings are excluded. Elapsed fraction measures nominal clock time, not active trading time; comparisons must account for weekends, session gaps, and incomplete history.


Freezing the Score at Confirmation

We declare the normalization input and the frozen score record before the functions that use them. InpBreakoutSaturation defaults to 0.50, so a breakout equal to half of C's range receives the full breakout component. STriggerScore stores the feature values, availability time, score, and validity flag; g_score holds the current sequence's record.

input double InpBreakoutSaturation=0.50;     // Breakout fraction saturating strength
//+------------------------------------------------------------------+
//| Frozen feature snapshot of one confirmed D trigger               |
//+------------------------------------------------------------------+
struct STriggerScore
  {
   datetime             available_time;
   double               c_range;
   double               body_fraction;
   double               breakout_fraction;
   double               elapsed_fraction;
   double               value;
   bool                 valid;
  };

CTrade    g_trade;
CCanvas   g_status_canvas;
SSequence     g_sequence;
STriggerScore g_score;

InputsAreValid rejects a nonfinite, zero, or negative InpBreakoutSaturation before the divisor is used. The exact validation block from the attached full EA is shown here.

   if(!MathIsValidNumber(InpBreakoutSaturation) || InpBreakoutSaturation<=0.0)
     {
      Print("Breakout saturation must be finite and positive.");
      return(false);
     }

OnInit calls InputsAreValid before requesting history or replaying the sequence. A failed check returns INIT_PARAMETERS_INCORRECT; the following lines show that call path.

   if(!InputsAreValid())
      return(INIT_PARAMETERS_INCORRECT);

These source checks establish the intended validation path. A recorded invalid-input test remains a separate terminal check.

The validity flag distinguishes a recorded score from an unavailable value. The EA clears the record when initializing a new sequence and populates it only when a completed M1 candle confirms the trigger.

We designed a separate structure for the score so that future outcomes remain outside its calculation. FreezeTriggerScore() reads completed C and the confirming M1 candle. It validates its denominators and derived values before marking the record valid. A missing score is treated as unavailable, not as a weak score of zero.

//+------------------------------------------------------------------+
//| Clamps one finite normalized feature to the unit interval        |
//+------------------------------------------------------------------+
double ClampUnit(const double value)
  {
   return(MathMax(0.0,MathMin(1.0,value)));
  } 
//+------------------------------------------------------------------+
//| Freezes an observational score using confirmation-time data      |
//+------------------------------------------------------------------+
void FreezeTriggerScore(const MqlRates &bar)
  {
   ZeroMemory(g_score);
//--- Use completed C as the price scale, never the final range of D
   double scale=g_sequence.c.high-g_sequence.c.low;
   double range=bar.high-bar.low;
   int duration=PeriodSeconds(InpContextTimeframe);
   if(!MathIsValidNumber(scale) || !MathIsValidNumber(range) ||
      scale<=0.0 || range<=0.0 || duration<=0)
      return;

//--- Bar timestamps identify opens; the feature becomes known at close
   datetime available=bar.time+PeriodSeconds(InpTriggerTimeframe);
   double elapsed=(double)(available-g_sequence.d.time)/(double)duration;
   double breakout=(g_sequence.direction>0 ?
                    bar.close-g_sequence.c.high :
                    g_sequence.c.low-bar.close);
   double body=MathAbs(bar.close-bar.open)/range;
   double fraction=MathMax(0.0,breakout)/scale;
   if(!MathIsValidNumber(body) || !MathIsValidNumber(fraction) ||
      !MathIsValidNumber(elapsed))
      return;

//--- Equal weights define a research hypothesis, not a probability
   g_score.available_time=available;
   g_score.c_range=scale;
   g_score.body_fraction=ClampUnit(body);
   g_score.breakout_fraction=fraction;
   g_score.elapsed_fraction=ClampUnit(elapsed);
   double strength=ClampUnit(fraction/InpBreakoutSaturation);
   double promptness=1.0-g_score.elapsed_fraction;
   g_score.value=100.0*(g_score.body_fraction+strength+promptness)/3.0;
   g_score.valid=true;
  } 

The unclipped breakout fraction is retained for later analysis. The score uses the capped value. This preserves information without allowing one unusually large value to dominate the equal-weight average. The elapsed component uses the nominal close boundary, correcting the one-minute understatement that would result from treating a bar's opening timestamp as its confirmation time.

EvaluateDMinute enforces the half-open nominal window [D open, D open + context duration) before evaluating a trigger or updating excursions. An M1 bar opening at the end boundary is rejected; the final M1 bar ending at that boundary remains eligible. This clock-time guard does not establish history completeness or compensate for server-calendar irregularities.

EvaluateDMinute calls FreezeTriggerScore immediately after assigning the trigger’s state, timestamp, and closing price, before any optional trade request. The EA does not recalculate the score as D develops. Changing later D highs or lows can change outcomes, but cannot improve an already frozen feature record.

//+------------------------------------------------------------------+
//| Evaluates one completed M1 candle inside Candle D                |
//+------------------------------------------------------------------+
void EvaluateDMinute(const MqlRates &bar,const bool allow_trade)
  {
//--- Restrict evaluation and excursions to Candle D's nominal window
   int duration=PeriodSeconds(InpContextTimeframe);
   if(duration<=0 || bar.time<g_sequence.d.time ||
      bar.time>=g_sequence.d.time+duration)
      return;

//--- After the trigger only the excursion extremes keep updating
   if(g_sequence.state==SEQUENCE_TRIGGERED)
     {
      UpdateExcursion(bar);
      return;
     }

   if(g_sequence.state!=SEQUENCE_TRACK_D)
      return;

   double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   double buffer=InpBreakBufferPoints*point;

//--- The protected A-B extreme remains authoritative inside D
   if(g_sequence.direction>0 &&
      bar.close<g_sequence.protected_level-buffer)
     {
      g_sequence.state=SEQUENCE_INVALIDATED;
      PrintFormat("Sequence invalidated at %s by a bullish-side breach.",
                  TimeToString(bar.time,TIME_DATE|TIME_MINUTES));
      return;
     }

   if(g_sequence.direction<0 &&
      bar.close>g_sequence.protected_level+buffer)
     {
      g_sequence.state=SEQUENCE_INVALIDATED;
      PrintFormat("Sequence invalidated at %s by a bearish-side breach.",
                  TimeToString(bar.time,TIME_DATE|TIME_MINUTES));
      return;
     }

//--- A displacement close beyond C completes the trigger
   bool confirmed=false;
   if(g_sequence.direction>0 && IsDisplacement(bar,1) &&
      bar.close>g_sequence.c.high+buffer)
      confirmed=true;

   if(g_sequence.direction<0 && IsDisplacement(bar,-1) &&
      bar.close<g_sequence.c.low-buffer)
      confirmed=true;

   if(!confirmed)
      return;

//--- Record the signal, draw it, and optionally execute the trade
   g_sequence.state=SEQUENCE_TRIGGERED;
   g_sequence.d_signal_time=bar.time;
   g_sequence.signal_price=bar.close;
   FreezeTriggerScore(bar);
   DrawSignal(bar);

   PrintFormat("%s trigger confirmed at %s, close=%.*f",
               (g_sequence.direction>0 ? "BUY" : "SELL"),
               TimeToString(bar.time+PeriodSeconds(InpTriggerTimeframe),TIME_DATE|TIME_MINUTES),
               _Digits,bar.close);

   if(allow_trade)
      ExecuteTrade();

  } 

The existing protected-level and displacement checks remain in charge of detection. The new score does not override them, and ExecuteTrade() does not read the score. This separation allows us to inspect score quality without silently changing the population of accepted signals.


Measuring Outcomes After the Signal

In our preceding implementation, the confirmation routine also passed the signal candle to UpdateExcursion(). That candle's high and low may have occurred before its close, when the signal was not yet known. Counting them as movement after confirmation would overstate the information available to a trader acting on the completed candle.

//+------------------------------------------------------------------+
//| Updates maximum favorable and adverse movement after a signal    |
//+------------------------------------------------------------------+
void UpdateExcursion(const MqlRates &bar)
  {
   if(g_sequence.state!=SEQUENCE_TRIGGERED ||
      bar.time<=g_sequence.d_signal_time)
      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));
  } 

UpdateExcursion ignores the signal candle and begins measurement with subsequent M1 bars. For a bullish signal, favorable movement is the later high above the signal close; adverse movement is the signal close above the later low. The function reverses those comparisons for a bearish signal. Both measurements remain nonnegative maxima over the observed remainder of D.

The EA exports maximum favorable excursion (MFE) and maximum adverse excursion (MAE) in points and relative to C’s range when the score is valid. These are price measurements, not trading returns: they omit execution costs and use the signal close rather than a fill. M1 OHLC cannot establish which of a stop or target was reached first within a minute.

Unequal exposure is a central limitation of this score. Earlier confirmation increases its promptness component and also leaves more time to accumulate excursions before D ends. A score–MFE association could therefore reflect a longer observation window rather than better signal quality. Before evaluating predictive usefulness, measure outcomes over a predefined common horizon, handle truncated horizons explicitly, and compare signals within comparable sessions and contexts. The current exporter does not implement that fixed-horizon measurement. Its end-of-D maxima cannot simply be converted into fixed-horizon outcomes; you must recompute them from the underlying history.

After a trigger, EvaluateDMinute updates excursions and returns. Protected-level invalidation applies before confirmation; it does not model a post-entry stop or exit. A triggered observation remains triggered even if price subsequently crosses the protected level. Its adverse movement is recorded as MAE, and any optional execution path must be evaluated separately.


Processing Minutes and Finalizing Observations

Our event handler previously focused on the newest completed trigger candle. The EA now processes available minutes in chronological order from the last processed timestamp. The same routine supports startup replay and catch-up. During replay, execution is disabled; during normal processing, only the newest completed minute is eligible to request a trade.

Fig. 4. The score, outcome collector, display, and execution gate remain separate.

Fig. 4. The score, outcome collector, display, and execution gate remain separate.

//+------------------------------------------------------------------+
//| Processes available closed minutes in chronological order        |
//+------------------------------------------------------------------+
bool ProcessClosedMinutes(const datetime last_open,const bool allow_latest)
  {
   datetime first=(datetime)MathMax((double)g_sequence.d.time,
                                   (double)(g_last_m1_time+1));
   if(first>last_open)
      return(true);

//--- Request a time interval; an error leaves the cursor unchanged
   MqlRates bars[];
   int copied=CopyRates(_Symbol,InpTriggerTimeframe,first,last_open,bars);
   if(copied<0)
     {
      PrintFormat("M1 catch-up is unavailable: error=%d",GetLastError());
      return(false);
     }

   ArraySetAsSeries(bars,false);
   for(int i=0;i<copied;i++)
     {
      //--- Old minutes restore state; only the newest may request a trade
      EvaluateDMinute(bars[i],allow_latest && bars[i].time==last_open);
      g_last_m1_time=bars[i].time;
     }
   return(true);
  } 

The time-range form of CopyRates() selects bars by their opening timestamps. The processing routine keeps its cursor on the last returned bar, and a failed request leaves it unchanged. At a context transition, the EA processes the remaining available minutes in the old nominal D window without trading. Then it replaces the context. This includes its final available M1 bar, which the earlier rollover order could omit.

This is not a complete history-integrity system. A successful request can return only the data currently available. The routine does not prove that every expected minute exists, and a long disconnection does not reconstruct every intervening higher-timeframe sequence. Exclude incomplete runs from validation until coverage checks and a recovery policy are implemented. The inherited C scanner also accepts the first qualifying setup; it does not subsequently rescan the remainder of C to detect a later cancellation. We preserve that rule here rather than silently redefining the detector.

//+------------------------------------------------------------------+
//| Finalizes one ended D window without counting shutdown snapshots |
//+------------------------------------------------------------------+
void ReportCompletedSequence(void)
  {
   if(g_active_d_time==0 || g_sequence_finalized)
      return;

//--- An armed window without a confirmation expires at the boundary
   if(g_sequence.state==SEQUENCE_TRACK_D)
      g_sequence.state=SEQUENCE_EXPIRED;
   double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   double mfe_points=(point>0.0 ? g_sequence.mfe/point : 0.0);
   double mae_points=(point>0.0 ? g_sequence.mae/point : 0.0);

//--- Memory aggregation and disk persistence are separate outcomes
   AggregateCompletedSequence(mfe_points,mae_points);
   bool exported=ExportObservation(mfe_points,mae_points,true);
   g_sequence_finalized=true;
   PrintFormat("D finalized: %s, state=%s, CSV=%s, samples=%d",
               TimestampText(g_sequence.d.time),StateName(g_sequence.state),
               (InpExportCSV ? (exported ? "written" : "FAILED") : "disabled"),
               g_completed_sequences);
  } 

The EA marks an armed but unconfirmed window as expired when the context advances. Only ended windows enter the in-memory aggregate. We describe the displayed rate as the triggered-window share: ended D windows that were triggered divided by all recorded ended D windows, including unclassified and unarmed windows. It is a diagnostic frequency, not an armed-setup confirmation rate or a measure of detector efficiency. An armed-setup rate would require a separately recorded eligible denominator, which this evidence does not establish. The finalization flag prevents repeat aggregation during a single attachment. It does not guarantee exactly-once CSV delivery across restarts. A failed export can leave the memory count above the stored-row count.


Keeping Snapshots Distinct from Completed Rows

Our new schema includes record_kind, score_valid, the raw normalized features, the preliminary score, and the later excursion measurements. A completed row describes an ended D window as observed by this run. A snapshot describes the state available when the EA is removed or reinitialized. The distinction prevents a short chart demonstration from being counted as a fully observed sample.

//+------------------------------------------------------------------+
//| Appends a typed observation and checks the write result          |
//+------------------------------------------------------------------+
bool ExportObservation(const double mfe_points,const double mae_points,
                       const bool completed)
  {
   if(!InpExportCSV)
      return(true);

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

//--- Use a new file for this version; old schemas are not migrated
   bool ok=true;
   if(FileSize(handle)==0)
      ok=(FileWrite(handle,"schema","record_kind","symbol","context_tf",
                    "trigger_tf","d_time","relation","direction","state",
                    "signal_bar_open","feature_available","score_valid",
                    "c_range","body_fraction","breakout_fraction",
                    "elapsed_fraction","saturation","score",
                    "mfe_points","mae_points","mfe_c_range","mae_c_range",
                    "last_processed_bar")>0);

//--- Missing features remain empty instead of becoming zero scores
   string kind=(completed ? "completed" : "snapshot");
   double scale=g_score.c_range;
   if(ok)
      ok=FileSeek(handle,0,SEEK_END);
   if(ok)
      ok=(FileWrite(handle,"P13_v1",kind,_Symbol,
                    EnumToString(InpContextTimeframe),
                    EnumToString(InpTriggerTimeframe),
                    TimestampText(g_sequence.d.time),
                    RelationName(g_sequence.relation),g_sequence.direction,
                    StateName(g_sequence.state),
                    TimestampText(g_sequence.d_signal_time),
                    TimestampText(g_score.available_time),(int)g_score.valid,
                    (g_score.valid ? DoubleToString(scale,10) : ""),
                    (g_score.valid ? DoubleToString(g_score.body_fraction,8) : ""),
                    (g_score.valid ? DoubleToString(g_score.breakout_fraction,8) : ""),
                    (g_score.valid ? DoubleToString(g_score.elapsed_fraction,8) : ""),
                    DoubleToString(InpBreakoutSaturation,8),
                    (g_score.valid ? DoubleToString(g_score.value,6) : ""),
                    DoubleToString(mfe_points,2),DoubleToString(mae_points,2),
                    (g_score.valid ? DoubleToString(g_sequence.mfe/scale,8) : ""),
                    (g_score.valid ? DoubleToString(g_sequence.mae/scale,8) : ""),
                    TimestampText(g_last_m1_time))>0);

//--- Preserve operation errors and isolate the flush error state
   int operation_error=GetLastError();
   ResetLastError();
   FileFlush(handle);
   int flush_error=GetLastError();
   FileClose(handle);
   if(!ok || operation_error!=0 || flush_error!=0)
     {
      PrintFormat("CSV write failed: %s, operation_error=%d, flush_error=%d",
                  InpCSVFileName,operation_error,flush_error);
      return(false);
     }
   return(true);
  } 

ExportObservation checks file-opening, seeking, and FileWrite results. It preserves the preceding operation error, calls ResetLastError immediately before FileFlush, and captures the flush error separately before closing the handle. The GetLastError documentation states that reading the error does not clear it; ResetLastError performs that reset. Keeping both error values avoids attributing an earlier error to the flush or discarding a preceding failure. This is error reporting, not a transactional or read-back integrity guarantee. Use a new filename for each controlled run; existing schemas are not migrated and concurrent writers are not coordinated.

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Removal preserves a partial snapshot, not a completed sample
   if(g_active_d_time!=0 && !g_sequence_finalized)
     {
      double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
      double mfe=(point>0.0 ? g_sequence.mfe/point : 0.0);
      double mae=(point>0.0 ? g_sequence.mae/point : 0.0);
      bool exported=ExportObservation(mfe,mae,false);
      PrintFormat("Removal snapshot: CSV=%s; completed count unchanged.",
                  (InpExportCSV ? (exported ? "written" : "FAILED") : "disabled"));
     }

//--- Always release our canvas; other owned objects are optional
   PrintStatistics();
   DestroyStatusCanvas();
   if(InpDeleteObjectsOnRemoval && StringLen(g_prefix)>0)
      ObjectsDeleteAll(0,g_prefix);
   ChartRedraw(0);
  } 

When you remove the EA, OnDeinit requests a snapshot export without increasing completed-sample statistics. The handler releases the canvas and, if InpDeleteObjectsOnRemoval is enabled, deletes chart objects carrying the EA’s nonempty prefix. The empty-prefix guard protects unrelated objects if initialization stops before the prefix is assigned.

For score/outcome analysis, accept only record_kind=completed rows with state=Triggered and score_valid=1, then check the timestamps, observation horizon, history coverage, and duplicates. Retain expired, invalidated, and unarmed rows for lifecycle diagnostics; their zero excursions are not post-trigger outcomes. TimestampText returns the literal none for a nonpositive timestamp, so an absent feature_available value is not exported as an epoch date. Treat none as missing when loading the file. Missing numerical score fields remain empty.

The CSV has no durable run ID, configuration hash, or globally unique sequence ID. For now, isolate each run in its own file. Keep the full inputs, source version, symbol, timeframes, and test interval in a run manifest. Do not pool files without assigning run identities and checking duplicates. A candidate duplicate key within an identified run is symbol, context timeframe, trigger timeframe, D opening time, and record kind; repeated keys need review rather than blind deletion. These manual controls do not add restart-safe delivery to the EA.


Operating the Expert Advisor

Compile Candlestick_Trend_Constraint_P13.mq5 in MetaEditor and attach it to a chart on a demo account or run it in the Strategy Tester. Use a demo account for forward testing and start with trading disabled. Our default context is D1 even when the visible chart is H1; the chart period does not redefine the model's context input.

Input or group Default/behavior Purpose
InpContextTimeframe/InpTriggerTimeframe D1/M1 Separate completed context from minute-level detection.
InpBreakoutSaturation 0.50 Relative breakout receiving the full breakout component.
InpMinBodyRangeRatio 0.55 Existing displacement qualification, independent of the new score.
InpBreakBufferPoints/InpRetestTolerancePoints 10/30 Existing detection buffers; broker points, not universal pip units.
InpExportCSV/InpCSVFileName true/CTC_P13_Scores_v1.csv Typed observations in the terminal or tester file sandbox.
InpShowStatusPanel true Text-only CCanvas chart comments, including the frozen score.
InpDeleteObjectsOnRemoval true Delete owned drawings; the canvas resource is always released.
InpEnableTrading false Optional inherited execution path; no score threshold is applied.

The EA uses CCanvas to display its status and observation details on the chart, including the frozen preliminary score and a reminder that it is neither a probability nor an execution filter. The score persists unchanged while later excursion values develop.

To reproduce the diagnostic observation below, use the generated “Every tick” mode with the recorded inputs and history. For a separate test of tick-sensitive execution, prefer “Every tick based on real ticks” where suitable data exists, and inspect history coverage. MetaTrader’s description of real and generated ticks distinguishes broker tick history from ticks constructed from minute data. A generated-tick run can demonstrate completed-M1 logic. It cannot verify historical intraminute ordering, actual fills, or equivalence to a real-tick run. “1 minute OHLC” is suitable only for an initial logic check here. Do not use “Open prices only” for this minute-dependent workflow. Preserve the build, broker, symbol, account mode, interval, mode, full inputs, and CSV filename with each run.


Reviewing the Results

We tested optional execution on EURUSD using build 6230, generated “Every tick” modeling, an H1 chart, D1 context, and M1 triggers. The completed nonvisual run covered the configured interval from 1 January 2024 00:00 to 31 December 2024 00:00. The journal ends with a normal completion message and a final summary at 30 December 2024 23:59:59.

Checking the observation records

The completed journal reports 313 ended windows and 40 triggers, a 12.8% triggered-window share. It also reports a separate shutdown snapshot without increasing that count. The findings below come from this completed run’s journal; a matching full-run CSV and detailed tester report were not included in the new evidence.

What we checked What we found
Completed records and snapshots The journal reports 313 completed windows and a separately written shutdown snapshot. The snapshot does not increase the completed count.
States and trigger frequency The final summary reports 40 triggered, 251 expired, and 22 invalidated windows. The 12.8% share includes unclassified and unarmed windows; it is not a win rate.
Feature availability and scores The journal records 40 confirmations. The score formula and freeze point are visible in the source extracts, but a full-run CSV is needed to verify the exported feature values and arithmetic for this run.
Record consistency The journal documents finalization and order events. Exported-row uniqueness, schema, and values are not independently verified for this completed run without its matching CSV.

The completed count includes one carried-in D window dated 29 December 2023, leaving 312 completed windows dated in 2024. Keep that startup context separate when selecting an in-range analysis population. The final summary includes 120 unclassified windows, which explains why the overall rate differs from an armed-setup conversion rate.

The chart below illustrates the status display from an earlier demonstration. Its sample count is not the total from the completed test.

metatester64_PPsoc9hiRZ.png

Fig. 5. EURUSD tester visualization with a confirmed bullish trigger and its frozen preliminary score.

Separating execution from the diagnostic score

EvaluateDMinute freezes the feature record at confirmation and calls ExecuteTrade only when allow_trade is true. ExecuteTrade and its permission, position, and trade-result helpers do not inspect g_score or apply a score threshold. The initial execution checks below come directly from ExecuteTrade.

//--- Execution is optional and is skipped silently when disabled
   if(!InpEnableTrading)
      return(true);

//--- All three trading permission layers must be enabled
   if(!TradingIsAllowed())
     {
      Print("Signal detected, but trading permissions are unavailable.");
      return(false);
     }

The remaining ExecuteTrade code checks blocking positions, tick availability, spread, volume, stop distances, and trade results. The full path is in ExecuteTrade, TradingIsAllowed, HasBlockingPosition, and TradeResultAccepted in Candlestick_Trend_Constraint_P13.mq5. FreezeTriggerScore records a diagnostic observation; even an unavailable score does not itself veto a confirmed signal.

The score expresses our equal-weight preferences for candle body, normalized breakout, and promptness. It is neither a probability nor a validated predictor. Since execution has no score filter, a trading-enabled balance result cannot establish that a higher score identifies better trades.

Checking optional execution

In the completed nonvisual run, execution was enabled with 0.1-lot volume and a take-profit distance twice the initial stop distance. Fixed volume does not imply fixed monetary risk because the protected-level stop distance varies. The input illustration below is retained from the earlier demonstration; the new journal is the source of the current run settings.

Strategy Tester inputs with optional execution enabled and fixed volume of 0.1 lots

Figure 6. Inputs used for the trading-enabled test.

The completed nonvisual run used build 6230, server-supplied EURUSD history, generated “Every tick” modeling, a simulated USD 10,000 deposit, and leverage 1:100. The journal identifies a demo-server quote source; its exact identifier and full input list are retained with the run evidence. The platform account’s netting/hedging mode was not recorded. The tested EX5 was loaded from Experts\CandlestickTrendConstraint; a compiler summary and an exact source-to-EX5 hash were not included.

Both runs recorded InpEnableTrading=true, volume 0.1, reward/risk multiple 2.0, a 100-point stop buffer, a 50-point spread limit, and 20 points of requested deviation. Classification used a 10-point break buffer, 0.5 recovery fraction, swing depth 3, 30-point retest tolerance, minimum body/range ratio 0.55, and breakout saturation 0.5. Drawing, removal cleanup, chart comments, and CSV export were enabled. The filename was CTC_P13_Scores_v1.csv and the magic number was 24840013. Actual spread conditions, commissions, swap charges, filling policy, delay settings, and realized slippage were not supplied; limits and requests are not measurements of those conditions.

These settings establish part of the execution environment, but actual spread conditions, charges, account mode, and fill assumptions remain necessary for exact reproduction. Retain the full tester report, symbol/account settings, and matching CSV with the run. We do not assume that unreported costs are zero.

The completed nonvisual journal recorded 40 signals and 10 accepted entries; an existing position blocked the other 30. Four positions reached take profit, five reached stop loss, and the tester closed the remaining position at the end. Its final balance was USD 10,438.50, an increase of USD 438.50 (4.385%) from the initial deposit. This is the recorded balance change under that test’s execution environment, not evidence that the score selected profitable signals.

The existing balance/equity illustration below belongs to the earlier demonstration. The new completed run independently reports the same ending balance in its journal; we do not present the old image as a capture of the new run.

Strategy Tester balance and equity graph for the completed trading-enabled run

Figure 7. Balance and equity during the trading-enabled test.

This run exercised entry and exit handling while signal observations continued independently. The score did not filter orders, so the balance gain does not validate its predictive usefulness. Positions can also outlast D, whereas our excursion measurements end with that window.

Only the completed run is used for these results. Its journal reports 10 accepted entries and 30 position-blocked signals. A matching complete CSV is still needed to inspect exported rows; the journal’s CSV=written messages alone do not verify file contents.

The completed journal demonstrates that signal tracking and optional order handling proceeded through the test interval. It does not establish the score’s predictive usefulness, full M1 coverage, controlled replay behavior, or failure recovery. These are separate questions; testing predictive value requires a baseline and equal outcome horizons on unseen data.


Key lessons

These programming and trading concepts extend beyond our particular detector.

Concept What we learn
Higher-timeframe context and lower-timeframe timing A completed A–B relationship defines the directional hypothesis. Examining M1 candles inside C and D reveals the sequence of setup and confirmation events hidden within the larger candles. This organizes signal selection; any improvement in reliability still needs testing.
Finite-state machines Explicit states distinguish tracking, triggering, invalidation, and expiration. The EA uses state-dependent processing to determine which actions are allowed, making event-driven logic easier to trace and test.
Immutable feature snapshots The EA calculates and freezes the feature record when confirmation becomes available. Keeping that record unchanged prevents later price movement from altering the information attributed to the original decision.
Look-ahead bias A completed signal candle becomes usable only after its close. UpdateExcursion excludes its earlier high and low from post-confirmation excursions so that the outcome measurement does not include movement that occurred before the signal was known.
Separation of concerns and side effects Detection, scoring, outcome measurement, display, export, and execution have distinct responsibilities. The replay path can rebuild analytical state while the execution gate prevents historical signals from sending orders.
Price normalization Expressing breakout distance and excursions relative to a completed candle range makes their scale explicit. It helps compare relative price movement, but does not remove differences in spread, liquidity, contract size, or market regime.
Explicit missing-data representation A validity flag distinguishes an unavailable score from a genuine numerical zero. Empty feature fields and absent timestamps preserve that distinction when observations are exported for analysis.
Persistent records and resource lifecycle The EA distinguishes shutdown snapshots from completed observations, checks file-operation failures, and releases chart resources on removal. An in-memory completion flag prevents repeated aggregation during one attachment; durable uniqueness across restarts requires additional record identity and storage logic.
Comparable outcome horizons Early signals have more time to accumulate MFE and MAE before D ends. Comparing signals over a common post-confirmation horizon would help separate signal quality from exposure duration, especially because promptness also contributes to our score.
Signal evaluation versus trade performance A trigger frequency describes how often a condition occurs, while MFE and MAE describe subsequent price excursions. Neither is a win rate or realized return. Evaluating a trading rule also requires entry and exit definitions, execution costs, and testing on unseen data.


Conclusion

We added a transparent, one‑time scoring layer that freezes normalized confirmation‑time features and keeps later outcomes strictly separate. The EA now:

  • records three normalized features and a preliminary score exactly when an M1 candle confirms a signal,
  • measures excursions only from subsequent minutes so pre‑confirmation movement is not counted as post‑entry outcome,
  • exports typed records (completed vs snapshot) so incomplete windows do not pollute analytic samples.

The delivered artifact is a practical framework for reproducible analysis: use the exported CSV and filter for completed, triggered, score_valid rows to study score ↔ outcome relationships on equal footing. It is not a proof of predictive value — do additional work to enforce fixed post‑confirmation horizons, verify M1 coverage, and manage run identities before claiming a validated edge. Compile the EA, run it on demo or in the tester with trading disabled to collect observations, and treat this as the starting point for controlled experiments into candlestick‑context signal quality.


Attached source

The complete EA contains the inherited detector, optional execution path, text-only canvas, and the additions explained above. You can use the complete source with the same inputs to reproduce the observation workflow and inspect the exported records.

File name Purpose
Candlestick_Trend_Constraint_P13.mq5 Complete Part 13 Expert Advisor with revised CSV error reporting. Compilation and a completed observation run are documented; controlled replay, failure, and coverage tests remain outstanding.
CTC_P13_Scores_v1.csv Exported observation records, including the A–B relationship, sequence state, confirmation-time features, diagnostic score, and directional MFE/MAE. Filter records by record_kind and state before analysis, and use score_valid to identify available feature values.
Developing a Quantitative Session Analysis Tool (Part 2): Building Universal Sessions and Analysing Session Relationships Developing a Quantitative Session Analysis Tool (Part 2): Building Universal Sessions and Analysing Session Relationships
The Session Analyzer is upgraded to define sessions by local market time and to translate those windows into broker/server time with date-aware DST logic. We describe the session model, broker offset treatment, and efficient bar selection, then add a relationship layer that measures temporal overlap, shared price range, range breaks, and size ratios. This provides a clear, reusable basis for session-based research and chart inspection
Neural Networks in Practice: ReLU Function Graph Neural Networks in Practice: ReLU Function Graph
We will create an indicator written entirely in MQL5 to display the activation function and its derivative on the chart, using ReLU as an example. We will explain how derivatives affect the choice of activation function and what precautions are needed at nondifferentiable points. Readers will be able to interactively visualize the curves and gain a practical foundation for deciding when to use certain activation functions and when not to.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Basic to Intermediate: Subwindows (III) From Basic to Intermediate: Subwindows (III)
This article takes a detailed look at using subwindows in MQL5 indicators: creating them, detecting instances, and preventing duplication. It also covers INDICATOR_SHORTNAME, querying information about chart windows and indicators, and the difference between displaying indicators in the main chart window and in a separate window. The article also shows how to set the height and boundaries of a subwindow to standardize the interface and simplify the placement of objects.