preview
Does Your EA Depend on Candle Boundaries? A Shifted-Bar Stress Test in MQL5

Does Your EA Depend on Candle Boundaries? A Shifted-Bar Stress Test in MQL5

MetaTrader 5 — Statistics and analysis |
223 0
Roberto Danilo Riccio
Roberto Danilo Riccio

Introduction

A profitable backtest can depend on the bar-opening grid. This test rebuilds shifted bars from a common M1 history and measures the resulting changes in indicators, signals, entries, and exits. Initialization and end-of-test clipping can also change the observations supplied to the strategy.

A boundary-sensitivity test enumerates every integer-minute boundary offset for a fixed target timeframe. The M15 case study compares boundary offsets 0 through 14 minutes. Sub-minute shifts are outside this M1-based method. For each boundary offset, the EA rebuilds shifted bars from completed M1 source bars and executes one otherwise identical Strategy Tester pass. The purpose is not to find the most profitable boundary offset, but to observe how strongly the result depends on the bar-opening grid. The boundary-sensitivity test complements other robustness diagnostics. A previous article tests whether an entry filter's apparent edge survives dependent-data placebo models and block-size sensitivity. This test measures how an EA's backtest changes with the bar-opening grid.

The reader receives ShiftedBarBuilder.mqh, BoundarySensitivityEA.mq5, ShiftedBarBuilderSelfTest.mq5, and SourceGapAudit.mq5. They provide the shifted-bar implementation, an auditable 15-pass M15 boundary-sensitivity profile, and a complete 15-pass modeling-mode comparison. Separate control series assess warm-up and end-of-test clipping. SourceGapAudit exports individual gaps and a weekly session snapshot.


Experimental rules, inputs, and assumptions

The main test series uses 1 minute OHLC; the validation test series uses Every tick based on real ticks. The warm-up control series and common-end control series use 1 minute OHLC.

A gap is an interval between consecutive observed M1 source bar openings longer than one minute. An absent minute is a minute within a gap for which no M1 source bar is observed.

A weekly session snapshot contains the quote sessions and trade sessions exposed by the Strategy Tester. The weekly session snapshot is labeled WEEKLY_SNAPSHOT_UNVERIFIED because its historical validity is not established.

Boundary offset is the only input that varies within the main test series. The validation test series covers all 15 boundary offsets with Every tick based on real ticks. The warm-up control series and common-end control series change only initialization and evaluation-window rules.

A boundary offset is an integer-minute displacement of the target-timeframe bar-opening grid. Boundary offset 0 is the baseline offset. A bucket is one interval on that grid; an aggregate is its mutable OHLC state. A shifted bar is an aggregate finalized at a causal boundary and published to the strategy. Finalization does not imply that all 15 M1 source bars exist.

The boundary-sensitivity profile is descriptive, not a binary verdict or an independence-based significance test. The declared summaries are profitable boundary offsets, median net profit, net profit IQR (interquartile range) and range, baseline net profit rank, Profit Factor range, maximum relative equity drawdown range, and total trades range.

Decision Fixed rule
Variable under test Boundary offset from 0 through 14; boundary offset 0 is the baseline offset and all other inputs remain fixed
Bar construction Completed M1 source bars are aggregated into shifted M15 bars; signals use the closes of completed shifted M15 bars only
Strategy Fixed 10/30 SMA crossover, long and short, with close-and-reverse execution
Trade execution One 0.10-lot market position; no SL, TP, trailing stop, pending orders, or time filter
Main test series All 15 boundary offsets tested with 1 minute OHLC
Validation test series All 15 boundary offsets with Every tick based on real ticks
Pass-level metrics Net profit, Profit Factor, maximum relative equity drawdown, and total trades
Interpretation boundary Describe dispersion across all boundary offsets; do not treat the passes as independent samples or select the boundary offset with the highest net profit for deployment

The 15 passes reuse almost all M1 source bars and therefore cannot be treated as independent samples.

EA inputs that implement the contract

InpTargetTimeframe remains fixed within each test series, and only InpBoundaryOffset is optimized. Evidence inputs record declared Strategy Tester dates, modeling mode, and execution delay. InpEvaluationStart is the trading gate; InpEvaluationEnd is the liquidation gate. Both gates are optional and used only in the control series.

Inputs are grouped into Research configuration, Fixed strategy configuration, Trade configuration, and Evidence export.

input group "Research configuration"
input ENUM_TIMEFRAMES InpTargetTimeframe = PERIOD_M15; // Target timeframe
input int      InpBoundaryOffset = 0;          // Boundary offset (minutes)
input group "Fixed strategy configuration"
input int      InpFastPeriod     = 10;         // Fast SMA period (shifted bars)
input int      InpSlowPeriod     = 30;         // Slow SMA period (shifted bars)
input group "Trade configuration"
input double   InpVolume         = 0.10;       // Order volume (lots)
input ulong    InpMagic          = 26081115;   // Magic number
input ulong    InpDeviationPoints= 20;         // Max deviation (points)
input group "Evidence export"
input bool     InpExportEvidence = false;      // Export Strategy Tester CSV
input string   InpEvidenceTag    = "manual";   // CSV evidence tag
input datetime InpTestStart      = 0;          // Declared Strategy Tester start (server time)
input datetime InpTestEnd        = 0;          // Declared exclusive Strategy Tester end
input string   InpModelingMode   = "unspecified"; // Declared Strategy Tester modeling mode
input int      InpExecutionDelayMs= 0;        // Declared Strategy Tester execution delay
input datetime InpEvaluationStart= 0;          // Trading gate; zero retains cold start
input datetime InpEvaluationEnd  = 0;          // Common liquidation gate; zero uses Strategy Tester end

Evidence export defaults to false. Every reported pass enables it and supplies explicit metadata. Separate tags identify the main test series, validation test series, warm-up control series, and common-end control series. Modeling mode and execution delay are declared inputs: they must match the external Strategy Tester configuration, which remains the authority for those settings.

InpTargetTimeframe must resolve to a fixed duration in whole minutes. Standard timeframes from M1 through W1 are accepted; PERIOD_MN1 is rejected because calendar months do not have one fixed minute length. For a target duration of P minutes, InpBoundaryOffset must remain in the complete range 0 through P − 1.

Tests use a retail netting account and allow at most one EA-owned position per symbol and magic number. Completed M1 source bars are processed before a pending signal is attempted. A market-closed rejection is retried at most once per new M1 opening, without a fixed retry count. A newer signal replaces the pending signal; disagreement with the latest completed SMA state cancels it. Other trade failures or invariant failures invalidate the pass.

The long condition is exact: the previous fast SMA is less than or equal to the previous slow SMA, and the current fast SMA exceeds the current slow SMA.

Long signal: previous fast SMA ≤ previous slow SMA and current fast SMA > current slow SMA

The short condition uses the reverse operators:

Short signal: previous fast SMA ≥ previous slow SMA and current fast SMA < current slow SMA

The builder discards its first finalized aggregate because its bucket may begin before initialization. The Strategy Tester can expose the preceding available M1 source bar at startup, including across a weekend. The export records the first accepted M1 source bar opening time and the number of M1 source bars in the discarded initialization aggregate.

At the first available M1 opening after a gap, the builder finalizes the preceding bucket. It neither creates empty bars nor merges different buckets. A published shifted bar may contain fewer than the target duration in M1 source bars.

Gap counters record absent minutes without assigning a cause. SourceGapAudit compares absent minutes with quote sessions and trade sessions in the weekly session snapshot. The weekly session snapshot does not establish a historical session calendar.

At the Strategy Tester end, the active aggregate remains unpublished unless a later available M1 opening has confirmed its boundary. The export records the last published shifted bar and M1 source bars in the unpublished tail. These end-of-test clipping differences belong to the main test series; they do not demonstrate a pure regrouping effect. At the end of a valid pass, g_d.aggregate_sources is exported as unpublished_tail_sources: it counts accepted M1 source bars remaining in the current unpublished aggregate.

The case study uses MetaQuotes-Demo XAUUSD from 2025-06-01 inclusive through 2026-01-01 exclusive. The tests use BoundarySensitivityEA version 1.12 in MetaTrader 5, build 6230. Account and trading inputs are fixed across the main test series and validation test series.


From M1 source bars to shifted M15 bars

Let M(t) denote the integer minute index of an M1 source bar in broker server time. Let P denote the fixed target duration in minutes. Let A denote the baseline anchor—the integer minute index of a standard target-timeframe bar opening in broker server time. Let o denote the boundary offset in minutes.

The bucket opening is:

B_{P,o,A}(t) = A + P × floor((M(t) − A − o) / P) + o

B_{P,o,A}(t) is the bucket-opening minute. The complete boundary-offset set is o ∈ {0, …, P − 1}.

For the XAUUSD M15 case study, P = 15 and A is any standard M15 opening. The resulting boundary offsets are exactly 0 through 14.

Boundary offset Consecutive shifted M15 bars
0 10:00–10:14, 10:15–10:29
1 10:01–10:15, 10:16–10:30
7 09:52–10:06, 10:07–10:21
14 09:59–10:13, 10:14–10:28

Each shifted M15 bar uses four deterministic aggregation rules:

Open  = open of the first included M1 source bar
High  = maximum high across included M1 source bars
Low   = minimum low across included M1 source bars
Close = close of the final included M1 source bar

Consider a three-bar example with opens 100, 101, and 102. Its highs are 102, 105, and 104; lows are 99, 100, and 101; closes are 101, 103, and 102.5.

The aggregate is O = 100, H = 105, L = 99, and C = 102.5. These values are illustrative and are not historical test results.

Aggregation must be stateful because an in-progress aggregate becomes an immutable shifted bar only when the first tick of a later M1 source bar confirms that the bucket has ended.


Architecture of the boundary-sensitivity test

The implementation separates aggregation, strategy logic, deterministic verification, and evidence analysis.

  • ShiftedBarBuilder.mqh: bucket assignment, OHLC state, one-time publication, and rolling SMA windows.
  • BoundarySensitivityEA.mq5: new-M1 detection, signal state, position handling, and custom optimization criterion.
  • ShiftedBarBuilderSelfTest.mq5: synthetic sequences and explicit deterministic assertions.
  • SourceGapAudit.mq5: gap CSV and weekly session snapshot CSV export.

Image 1 shows the flow from M1 source bars through aggregation and strategy state to trade operations.

Causal flow

Image 1: Causal flow from one completed M1 source bar to a possible trade operation.

The builder owns target duration, baseline anchor, current bucket identifier, and one immutable publication slot. It does not read EA inputs, account data, positions, or platform target-timeframe bars.

The EA owns configuration, rolling crossover state, and trade operations. The self-test owns synthetic prices and assertions, so it requires no broker history.


Implementing CShiftedBarBuilder

State and initialization

The complete CShiftedBarBuilder declaration appears below. Its runtime state is private, and its methods are defined outside the class body.

class CShiftedBarBuilder
  {
private:
   int               m_period_minutes;      // Fixed target duration in minutes
   int               m_boundary_offset;     // Configured boundary offset in minutes
   long              m_anchor_minute;       // Opening minute of a standard target-timeframe bar
   long              m_current_bucket_id;   // Identifier of the aggregate being built
   datetime          m_last_source_time;    // Last accepted M1 source bar opening time
   bool              m_configured;          // True after successful initialization
   bool              m_has_current;         // True while an aggregate is in progress
   bool              m_discard_current;     // True until the initialization aggregate ends
   bool              m_completed_available; // True while one shifted bar awaits retrieval
   SShiftedBar       m_current_bar;          // Aggregate currently receiving M1 source bars
   SShiftedBar       m_completed_bar;        // Immutable shifted bar awaiting retrieval

   long              FloorDivide(const long numerator,const int denominator);
   long              BucketId(const datetime source_time);
   void              BeginCurrent(const MqlRates &source_bar,const long bucket_id);
   void              UpdateCurrent(const MqlRates &source_bar);
   void              FinishCurrent(void);

public:
                     CShiftedBarBuilder(void);
   bool              Initialize(const int period_minutes,
                                const int boundary_offset,
                                const datetime baseline_anchor);
   void              Reset(void);
   bool              Process(const MqlRates &source_bar,const datetime next_source_time);
   bool              CompletedAvailable(void);
   bool              PopCompleted(SShiftedBar &completed_bar);
   datetime          BucketOpenTime(const datetime source_time);
  };

Initialize() accepts a positive whole-minute target duration, a boundary offset below that duration, and a minute-aligned baseline anchor. Reset() preserves this configuration while clearing aggregation and publication state.

Calculating the bucket identifier

MQL5 datetime values are converted to integer minutes. Source times can precede the selected anchor, so FloorDivide() corrects MQL5 integer division for negative remainders.

//+------------------------------------------------------------------+
//|Divide signed minute distances with mathematical floor behavior   |
//+------------------------------------------------------------------+
   long CShiftedBarBuilder::FloorDivide(const long numerator,
                                        const int denominator)
     {
//--- MQL5 truncates integer division toward zero, so correct negatives
      long quotient=numerator/denominator; // Truncated integer quotient
      long remainder=numerator%denominator; // Signed division remainder
      if(remainder<0)
         quotient--;
      return(quotient);
     }

//+------------------------------------------------------------------+
//|Convert a broker-time minute into a stable shifted-bucket ID      |
//+------------------------------------------------------------------+
   long CShiftedBarBuilder::BucketId(const datetime source_time)
     {
//--- Measure from a known standard-bar anchor before applying the boundary offset
      long minute_index=(long)source_time/60; // Broker-time minute index
      long relative_minute=minute_index-m_anchor_minute-m_boundary_offset; // Relative minute
      return(FloorDivide(relative_minute,m_period_minutes));
     }

Updating OHLC values

The first M1 source bar supplies all four starting prices. Later M1 source bars can extend the high or low, while every accepted M1 source bar replaces the current close.

The standard MqlRates structure supplies the M1 source bar opening time, OHLC values, volumes, and spread. The builder reads only time and OHLC because those are the fields required by this strategy.

Finalizing a bar without look-ahead

Process() receives the completed M1 source bar and the opening time of the next available M1 source bar. This pairing reveals a boundary without reading any future price.

//+------------------------------------------------------------------+
//|Process a completed M1 source bar at the next M1 opening          |
//+------------------------------------------------------------------+
   bool CShiftedBarBuilder::Process(const MqlRates &source_bar,
                                    const datetime next_source_time)
     {
//--- Protect an unread shifted bar from accidental replacement
      if(!m_configured || m_completed_available)
         return(false);

//--- Enforce causal timestamps and strictly increasing source order
      if(source_bar.time<=0 || next_source_time<=source_bar.time)
         return(false);
      if(m_last_source_time>0 && source_bar.time<=m_last_source_time)
         return(false);

//--- A correct caller must announce every boundary through next time
      long source_bucket_id=BucketId(source_bar.time); // M1 source bar bucket
      if(m_has_current && source_bucket_id!=m_current_bucket_id)
         return(false);

//--- Initialize or extend the aggregate with the completed M1 source bar
      if(!m_has_current)
         BeginCurrent(source_bar,source_bucket_id);
      else
         UpdateCurrent(source_bar);
      m_last_source_time=source_bar.time;

//--- Finalize when the newly opened M1 source bar belongs to another bucket
      long next_bucket_id=BucketId(next_source_time); // Next M1 source bar bucket
      if(next_bucket_id!=source_bucket_id)
         FinishCurrent();

      return(true);
     }

A shifted bar occupies one publication slot until PopCompleted() retrieves it. This guard prevents a later boundary from silently replacing unread data.

Handling initialization and gaps

FinishCurrent() suppresses the first completed aggregate and publishes each later one once as a shifted bar. After any boundary, the current state is cleared until another completed M1 source bar arrives.

If the next available M1 opening skips several buckets, the source and next identifiers still differ. The current aggregate is finalized, but the implementation creates no zero-volume placeholders.


Verifying the builder with synthetic data

The self-test constructs MqlRates values directly. It does not request price history, place trades, or depend on the chart symbol.

The assertions cover M15 assignment and OHLC behavior, one-time publication, initialization, gaps, and reset. Additional checks verify H1 boundary offsets, signed floor behavior before the anchor, and generic range validation.

The self-test recorded 14 passes and no failures.

=== ShiftedBarBuilder deterministic self-test ===
PASS | Offset 0 assigns minutes 00:00 through 00:14 to one bucket
PASS | Offset 7 assigns minutes 00:07 through 00:21 to one bucket
PASS | First initialization aggregate is not supplied to the strategy
PASS | Aggregate open comes from the first M1 source bar
PASS | Aggregate high is the maximum source high
PASS | Aggregate low is the minimum source low
PASS | Aggregate close comes from the final M1 source bar
PASS | Completed shifted bar is published once
PASS | Gap across multiple buckets does not merge two buckets
PASS | Reset reproduces identical output for the same sequence
PASS | Generic H1 offset 0 assigns sixty source minutes correctly
PASS | Generic H1 offset 15 assigns sixty source minutes correctly
PASS | Signed floor preserves buckets before the baseline anchor
PASS | Offset equal to target duration is rejected
SELF_TEST_SUMMARY | passed=14 | failed=0 | total=14


Integrating the shifted bars into the Expert Advisor

Integration follows a linear lifecycle: include the module, initialize pass-specific state, process completed M1 source bars in OnTick(), and export the pass in OnTester().

Include the module and initialize the pass

The archive installs the builder under MQL5\Include\BoundarySensitivity. The following blocks are exact source slices from the attached EA. The first includes the trading interface and shifted-bar module:

#include <Trade\Trade.mqh>
#include <BoundarySensitivity\ShiftedBarBuilder.mqh>

The EA owns one builder and two rolling-SMA objects:

CTrade                 g_trade;                  // Synchronous trade-operation interface
CShiftedBarBuilder     g_builder;                // Causal shifted-bar aggregation state
CRollingSMA            g_fast_sma;               // Fast rolling average of shifted closes
CRollingSMA            g_slow_sma;               // Slow rolling average of shifted closes

Inside OnInit(), input validation and anchor resolution precede this exact initialization block. The complete function in the attached EA also configures CTrade, initializes new-M1 detection, resets signal state, records the account mode, and prints the resolved configuration.

//--- Configure independent aggregation and rolling-average objects
   if(!g_builder.Initialize(target_minutes,InpBoundaryOffset,baseline_anchor) ||
      !g_fast_sma.Initialize(InpFastPeriod) ||
      !g_slow_sma.Initialize(InpSlowPeriod))
     {
      ::Print("Unable to initialize shifted-bar or SMA state");
      return(INIT_FAILED);
     }

New-M1 detection and causal signal timing

OnTick() ignores ticks inside the active M1 source bar. At the first tick of a later M1 source bar, CopyRates() retrieves only completed M1 source bars.

The official CopyRates documentation states that the oldest copied element occupies the start of physical memory. The EA processes that order directly.

//+------------------------------------------------------------------+
//|Process completed data on a new M1 source bar's first tick        |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//--- Stop after the first invariant or non-retryable execution failure
   if(g_runtime_failed)
      return;
   datetime new_m1_open=::iTime(_Symbol,PERIOD_M1,0); // Active M1 source bar opening
   if(new_m1_open<=0 || new_m1_open==g_current_m1_open)
      return;

//--- Liquidate at the first available M1 tick at or after the liquidation gate
   if(InpEvaluationEnd>0 && new_m1_open>=InpEvaluationEnd)
     {
      if(!g_evaluation_closed)
        {
         ulong end_ticket=0; // Owned ticket at the liquidation gate
         ENUM_POSITION_TYPE end_type=POSITION_TYPE_BUY; // Owned position direction
         int end_count=OwnedPositionCount(end_ticket,end_type); // Owned count
         if(end_count>1 || (end_count==1 && !CloseOwnedPosition(end_ticket)))
           {
            FailRuntime("Common-end liquidation failed");
            return;
           }
         g_pending_signal=BOUNDARY_SIGNAL_NONE;
         g_evaluation_closed=true;
        }
      return;
     }

//--- Refresh all completed data before attempting a pending signal
   if(!ProcessCompletedM1Bars(new_m1_open))
     {
      FailRuntime("Completed M1 or shifted-state processing failed");
      return;
     }
   g_current_m1_open=new_m1_open;

//--- Cancel a pending signal if the latest complete SMA state disagrees
   if((g_pending_signal==BOUNDARY_SIGNAL_LONG && g_previous_fast<=g_previous_slow) ||
      (g_pending_signal==BOUNDARY_SIGNAL_SHORT && g_previous_fast>=g_previous_slow))
     {
      g_pending_signal=BOUNDARY_SIGNAL_NONE;
      g_d.cancelled_signals++;
     }
   if(g_pending_signal==BOUNDARY_SIGNAL_NONE)
      return;

//--- Attempt at most once per new M1; only a closed market is retryable
   g_d.trade_attempts++;
   long signal_age=(long)(new_m1_open-g_d.signal_time); // Causal delay in seconds
   if(signal_age>g_d.max_signal_age_seconds)
      g_d.max_signal_age_seconds=signal_age;
   g_retryable_trade_failure=false;
   if(ExecuteSignal(g_pending_signal))
      g_pending_signal=BOUNDARY_SIGNAL_NONE;
   else
      if(!g_retryable_trade_failure)
         FailRuntime("Non-retryable trade or position invariant failure");
  }

Rolling SMA crossover

Two circular buffers maintain sums over shifted closes. No indicator handle reads the terminal's standard target-timeframe series.

The slow SMA becomes ready after 30 published shifted bars. That first complete pair initializes the previous SMA state; crossover evaluation starts at the following shifted close. The warm-up control series starts the Strategy Tester on 2025-05-25 and disables trading before 2025-06-01. Both SMA windows are therefore ready before the trading gate.

The code uses <= and > for the long signal. It uses >= and < for the short signal, matching the verbal rules exactly.

ProcessShiftedBar() updates the rolling averages and stores a new signal as the pending signal. OnTick() checks the pending signal against the latest completed SMA state before execution. During the warm-up control series, SMA state advances while signals are suppressed.

//+------------------------------------------------------------------+
//|Update both SMAs and evaluate an exact shifted-bar crossover      |
//+------------------------------------------------------------------+
bool ProcessShiftedBar(const SShiftedBar &shifted_bar)
  {
//--- Add the immutable shifted close to both fixed rolling windows
   if(!g_fast_sma.Add(shifted_bar.close) ||
      !g_slow_sma.Add(shifted_bar.close))
     {
      ::Print("Unable to update the rolling SMA state");
      return(false);
     }

//--- Wait until both windows contain their complete periods
   if(!g_fast_sma.Ready() || !g_slow_sma.Ready())
      return(true);

//--- Capture the current averages after adding the same shifted close
   double current_fast=g_fast_sma.Value(); // Current fast SMA
   double current_slow=g_slow_sma.Value(); // Current slow SMA
   if(!g_have_previous_smas)
     {
      g_previous_fast=current_fast;
      g_previous_slow=current_slow;
      g_have_previous_smas=true;
      g_d.sma_ready_time=g_d.decision_time;
      return(true);
     }

//--- Apply the exact previous/current crossover operators
   ENUM_BOUNDARY_SIGNAL signal=BOUNDARY_SIGNAL_NONE; // Confirmed crossover
   if(g_previous_fast<=g_previous_slow && current_fast>current_slow)
      signal=BOUNDARY_SIGNAL_LONG;
   else
      if(g_previous_fast>=g_previous_slow && current_fast<current_slow)
         signal=BOUNDARY_SIGNAL_SHORT;

//--- Advance comparison state before applying the common trading gate
   g_previous_fast=current_fast;
   g_previous_slow=current_slow;
   if(InpEvaluationStart>0 && g_d.decision_time<InpEvaluationStart)
      return(true);
   if(signal==BOUNDARY_SIGNAL_NONE)
      return(true);
   g_pending_signal=signal;
   g_d.signal_time=g_d.decision_time;
//--- Trade once only after all newly completed data have been consumed
   return(true);
  }

Position handling

OwnedPositionCount() scans account positions and filters by both symbol and magic number. A count above one is an invariant failure. On a netting account, an existing position on the same symbol with a different magic number blocks trading, preventing the EA from modifying a position it does not own.

An opposite position is closed by ticket before the new direction is opened. Each operation checks the CTrade result, server return code, and resulting position state. The reversal is not atomic. If closing succeeds but opening is rejected because the market is closed, the account remains flat until a valid pending signal can be executed. A non-retryable rejection invalidates the pass.

The builder and rolling-SMA objects keep only in-memory state and require no resource-release handler. With InpEvaluationEnd=0, the Strategy Tester closes the remaining position at its end. The common-end control series instead liquidates at the first available M1 tick at or after the liquidation gate and prevents further trading. A failed liquidation in the common-end control series invalidates the pass.

Custom optimization criterion

OnTester() returns net profit for a valid pass and −1.0e100 for a runtime failure or an enabled-export failure. Measured net profit remains a separate CSV field. Analysis requires a complete CSV with runtime_failed=false, a matching valid custom optimization criterion, and no export-failure log. A missing file makes the series incomplete; it must never be silently omitted from a summary of all 15 boundary offsets.

Each CSV records original symbol and tag, replay inputs, account configuration, source version, runtime status, full-precision statistics, realized deal costs, and source diagnostics. Strategy Tester dates, modeling mode, and execution delay are explicit declared fields. SafeFileComponent() retains ASCII letters, digits, and hyphens and encodes other filename characters. Original string values are preserved in quoted CSV fields. FileOpen() creates missing subfolders when writing; opening or writing errors are logged and return the invalid custom optimization criterion.

//+------------------------------------------------------------------+
//|Export an auditable pass and reject runtime or evidence failures  |
//+------------------------------------------------------------------+
double OnTester(void)
  {
//--- Keep net profit separate from the custom optimization criterion
   double net_profit=::TesterStatistics(STAT_PROFIT); // Measured net profit
   double criterion=(g_runtime_failed ? -1.0e100 : net_profit); // Invalid sentinel
   if(!InpExportEvidence)
      return(criterion);

//--- Preserve original metadata while encoding filename components
   string target_name=::EnumToString(InpTargetTimeframe); // Target label
   string file_name=::StringFormat("BoundarySensitivity\\%s_%s_%s_offset_%02d.csv",
                                   SafeFileComponent(InpEvidenceTag),
                                   SafeFileComponent(_Symbol),target_name,
                                   InpBoundaryOffset); // Common evidence path
   ::ResetLastError();
   int file_handle=::FileOpen(file_name,FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,
                              ',',CP_UTF8); // UTF-8 CSV with original metadata
   if(file_handle==INVALID_HANDLE)
     {
      ::PrintFormat("PASS_EXPORT_FAILED | %s | error=%d",file_name,::GetLastError());
      return(-1.0e100);
     }

//--- Sum actual Strategy Tester deal charges, rather than assuming zero costs
   double commission=0.0; // Signed deal commissions
   double swap=0.0; // Signed realized deal swaps
   double fees=0.0; // Signed deal fees
   if(!::HistorySelect(InpTestStart,InpTestEnd))
      FailRuntime("Unable to select deal history for cost audit");
   for(int index=0;index<::HistoryDealsTotal();index++)
     {
      ulong ticket=::HistoryDealGetTicket(index); // Deal selected by index
      if(::HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol ||
         (ulong)::HistoryDealGetInteger(ticket,DEAL_MAGIC)!=InpMagic)
         continue;
      commission+=::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
      swap+=::HistoryDealGetDouble(ticket,DEAL_SWAP);
      fees+=::HistoryDealGetDouble(ticket,DEAL_FEE);
     }
   criterion=(g_runtime_failed ? -1.0e100 : net_profit);

//--- Write replay inputs, status, full-precision metrics and source diagnostics
   int total_trades=(int)::TesterStatistics(STAT_TRADES); // Closed trades
   int profit_trades=(int)::TesterStatistics(STAT_PROFIT_TRADES); // Winning trades
   double win_rate=(total_trades>0 ? 100.0*profit_trades/total_trades : 0.0); // Percent winners
   ::ResetLastError();
   uint header_bytes=::FileWrite(file_handle,
      "symbol","target_timeframe","boundary_offset","evidence_tag",
      "server","terminal_build","account_mode","account_currency","leverage","initial_deposit",
      "test_start","test_end_exclusive","modeling_mode_declared","evaluation_start","evaluation_end",
      "fast_period","slow_period","volume","magic","deviation_points","execution_delay_ms_declared","source_version",
      "runtime_failed","failure_reason","failure_time","net_profit","profit_factor",
      "max_equity_drawdown_pct","total_trades","profit_trades","win_rate_pct","expected_payoff","on_tester_value",
      "commission_signed","swap_signed","fees_signed","contract_size","point",
      "tick_size","tick_value","currency_profit","volume_min","volume_step","swap_mode","spread_floating",
      "first_m1","last_m1","first_shifted","last_shifted","sma_ready_time",
      "source_count","shifted_count","gap_count","absent_minutes_including_sessions",
      "partial_shifted_bars","discarded_initial_sources","unpublished_tail_sources",
      "trade_attempts","cancelled_signals","max_signal_age_seconds","pending_signal_at_end","m1_spread_min_points","m1_spread_max_points"); // Header bytes
   uint row_bytes=::FileWrite(file_handle,
      CsvField(_Symbol),CsvField(target_name),InpBoundaryOffset,CsvField(InpEvidenceTag),
      CsvField(::AccountInfoString(ACCOUNT_SERVER)),(long)::TerminalInfoInteger(TERMINAL_BUILD),
      CsvField(::EnumToString(g_account_mode)),CsvField(::AccountInfoString(ACCOUNT_CURRENCY)),
      (long)::AccountInfoInteger(ACCOUNT_LEVERAGE),
      ::TesterStatistics(STAT_INITIAL_DEPOSIT),InpTestStart,InpTestEnd,CsvField(InpModelingMode),
      InpEvaluationStart,InpEvaluationEnd,InpFastPeriod,InpSlowPeriod,InpVolume,InpMagic,InpDeviationPoints,InpExecutionDelayMs,CsvField("1.12"),
      g_runtime_failed,CsvField(g_failure_reason),g_d.failure_time,net_profit,
      ::TesterStatistics(STAT_PROFIT_FACTOR),::TesterStatistics(STAT_EQUITY_DDREL_PERCENT),
      total_trades,profit_trades,win_rate,::TesterStatistics(STAT_EXPECTED_PAYOFF),criterion,commission,swap,fees,
      ::SymbolInfoDouble(_Symbol,SYMBOL_TRADE_CONTRACT_SIZE),::SymbolInfoDouble(_Symbol,SYMBOL_POINT),
      ::SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE),::SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE),
      CsvField(::SymbolInfoString(_Symbol,SYMBOL_CURRENCY_PROFIT)),
      ::SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN),
      ::SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP),(long)::SymbolInfoInteger(_Symbol,SYMBOL_SWAP_MODE),(long)::SymbolInfoInteger(_Symbol,SYMBOL_SPREAD_FLOAT),
      g_d.first_m1,g_d.last_m1,g_d.first_shifted,g_d.last_shifted,g_d.sma_ready_time,
      g_d.source_count,g_d.shifted_count,g_d.gap_count,g_d.absent_minutes,g_d.partial_bars,
      g_d.discarded_sources,g_d.aggregate_sources,g_d.trade_attempts,g_d.cancelled_signals,
      g_d.max_signal_age_seconds,(int)g_pending_signal,g_d.spread_min,g_d.spread_max); // Row bytes

//--- A missing or incomplete evidence file cannot qualify as a valid pass
   ::FileFlush(file_handle);
   int write_error=::GetLastError(); // Error since opening the evidence file
   ::FileClose(file_handle);
   if(header_bytes==0 || row_bytes==0 || write_error!=0)
     {
      ::PrintFormat("PASS_EXPORT_FAILED | %s | error=%d",file_name,write_error);
      return(-1.0e100);
     }
   ::PrintFormat("PASS_EVIDENCE | %s | runtime_failed=%s",file_name,
                 (g_runtime_failed ? "true" : "false"));
   return(criterion);
  }

The official documentation defines OnTester() as the handler for the custom optimization criterion. TesterStatistics() is valid in OnTester() and returns the requested Strategy Tester statistic.


Adapting an existing EA

The builder is strategy-independent, but it cannot replace platform bars inside an already compiled EA. Native indicator handles, CopyBuffer(), iOpen(), and similar calls continue to read standard terminal series.

The supplied EA is a worked adaptation of a close-based SMA crossover. A conventional implementation can obtain its SMA from an indicator handle whose PERIOD_M15 series is built by the terminal.

The following conventional fragment is illustrative and is not part of the attached source. It omits handle validation, the CopyBuffer() return check, and IndicatorRelease() so that the standard target-timeframe dependency remains visible.

int sma_handle=::iMA(_Symbol,PERIOD_M15,10,0,MODE_SMA,PRICE_CLOSE);
double sma_value[1];
::CopyBuffer(sma_handle,0,1,1,sma_value);

The shifted version removes that target-timeframe indicator handle. The exact source slice below adds each published shifted close to the two CRollingSMA objects; the surrounding ProcessShiftedBar() function performs the readiness and crossover checks.

//--- Add the immutable shifted close to both fixed rolling windows
   if(!g_fast_sma.Add(shifted_bar.close) ||
      !g_slow_sma.Add(shifted_bar.close))
     {
      ::Print("Unable to update the rolling SMA state");
      return(false);
     }

The same replacement pattern applies to other target-timeframe dependencies:

Standard target-timeframe dependency Shifted replacement Required state
Standard target-timeframe new-bar detection One completed SShiftedBar returned by PopCompleted() CShiftedBarBuilder publication state
iOpen(), iHigh(), iLow(), and iClose() shifted_bar.open, shifted_bar.high, shifted_bar.low, and shifted_bar.close Current published SShiftedBar
iMA() and CopyBuffer() CRollingSMA updated with shifted_bar.close Independent rolling window for each SMA
ATR, channels, stochastic logic, and OHLC patterns Calculations based on the complete SShiftedBar sequence A strategy-specific shifted OHLC buffer

The builder changes target-timeframe data production, not the trade interface. Order sizing, symbol and magic filtering, and position handling can remain unchanged when they do not read standard target-timeframe bars.

Tick-path-dependent exits are different. Shifted M1 aggregation does not reconstruct the path inside a minute, so those rules require real-tick testing and a separate validation contract.

One boundary offset per Strategy Tester pass

The optimization does not process every boundary offset inside one EA instance. Each Strategy Tester pass receives one InpBoundaryOffset, initializes one CShiftedBarBuilder, and maintains one pass-specific set of rolling indicator values.

The complete main test series is the collection of those passes. For M15, boundary offsets 0 through 14 therefore produce 15 separate EA initializations and 15 pass rows.

Adaptation scope

The required work depends on how the EA consumes target-timeframe data:

  • Close-only target-timeframe logic: feed shifted_bar.close into custom rolling indicator state; demonstrated by the supplied CRollingSMA crossover.
  • OHLC-based target-timeframe logic: retain SShiftedBar values and calculate indicators from open, high, low, and close; the builder supplies shifted bars, while strategy-specific indicators remain part of the adapted EA.
  • Multiple target timeframes: use a separate builder and indicator state for each target timeframe, then define how their boundary offsets relate. This requires a separate multi-timeframe experimental contract and is outside the supplied single-target-timeframe case study.

PeriodSeconds() supplies the fixed target duration. iTime() supplies a standard target-timeframe bar opening used as the baseline anchor.

The table lists representative target timeframes. M1 is accepted but has only boundary offset 0, so it cannot support a boundary-sensitivity test.

Target timeframe Complete boundary offsets Passes
M5 0–4 5
M15 0–14 15
H1 0–59 60
H4 0–239 240
D1 0–1439 1,440
W1 0–10079 10,080

For large target timeframes, complete enumeration can be expensive. A predeclared boundary-offset step can define an exploratory sample, but the resulting test series is not a complete boundary-offset enumeration.

H1 replication example

The following illustrative H1 protocol applies the close-based strategy with one boundary offset per pass. It defines a separate test series from the reported M15 case study.

For the 60-minute H1 target, set InpTargetTimeframe=PERIOD_H1 and optimize InpBoundaryOffset from 0 through 59 in steps of 1. Run all 60 Strategy Tester passes from completed M1 source bars while keeping strategy, account, history, and execution settings fixed.

The close-based strategy permits a fast 1 minute OHLC boundary-sensitivity profile, but this does not establish execution equivalence. Market prices, spread, and trading availability can differ between models. The separate H1 example requires its own modeling-mode comparison; the reported M15 verification cannot establish suitability for H1.

Minimal integration steps

  • Include ShiftedBarBuilder.mqh and create pass-specific builder and indicator state for one boundary-offset pass.
  • Feed completed M1 source bars to the builder and consume each published shifted bar once.
  • Replace every target-timeframe OHLC or indicator dependency with calculations based on the shifted-bar stream.
  • Run the complete boundary-offset range with fixed strategy settings and assign a different evidence tag to each test series.


Strategy Tester protocol

The main test series and validation test series share account, test interval, strategy inputs, and execution delay. Each enumerates all 15 boundary offsets. The warm-up control series uses an earlier Strategy Tester start and a common trading gate. The common-end control series adds a common liquidation gate. All four test series use BoundarySensitivityEA version 1.12.

Setting Required value
EA BoundarySensitivityEA
Symbol XAUUSD
Period M1
Test interval 2025-06-01 inclusive through 2026-01-01 exclusive
Modeling mode: main test series 1 minute OHLC
Modeling mode: validation test series Every tick based on real ticks
Target timeframe InpTargetTimeframe=PERIOD_M15, fixed for the case study
Optimization Complete slow algorithm; exactly 15 passes
Optimized input InpBoundaryOffset: start 0, step 1, stop 14
Boundary offsets: validation test series All boundary offsets 0 through 14, including minimum net profit boundary offset 1, maximum net profit boundary offset 7, and maximum relative equity drawdown boundary offset 13
Fixed inputs Target M15, fast 10, slow 30, volume 0.10, magic 26081115, deviation 20 points
Evidence export: main test series InpExportEvidence=true; InpEvidenceTag=m2v112-main
Evidence export: validation test series InpExportEvidence=true; InpEvidenceTag=m2v112-real
Deposit and currency USD 10,000
Leverage 1:100
Account mode ACCOUNT_MARGIN_MODE_RETAIL_NETTING
Execution delay No delay
Broker/server and build MetaQuotes-Demo, build 6230

Only InpBoundaryOffset is optimized within a series. The complete modeling-mode comparison checks execution sensitivity instead of inferring equivalence from the absence of intraminute rules. Dates and modeling-mode labels in the evidence must be verified against each saved Strategy Tester configuration.

Trading costs and recorded symbol specification

Both modeling modes use the Strategy Tester's variable broker spread, with no fixed-spread override. The accepted M1 source bars report spreads from 0 to 1,017 points. This M1 field measures M1 source bar spread rather than every execution spread. The validation test series provides the separate bid/ask execution check.

Item Recorded value
Commission and deal fees USD 0.00 in every pass in the main test series
Realized swap, signed USD -196.26 to -193.06
Contract size 100
Point / tick size 0.01 / 0.01
Reported tick value / profit currency 0.10 / USD
Minimum volume / volume step 0.01 / 0.01 lots
Spread specification Floating
Swap calculation mode SYMBOL_SWAP_MODE_POINTS

Commission, swap, and fees are summed from the tested symbol's EA-owned deals. Symbol properties are snapshots read in OnTester(); they do not establish that the broker kept those properties unchanged throughout history. Realized costs are included in the measured net profit. A liquidation diagnostic at 23:30 returned TRADE_RETCODE_MARKET_CLOSED, demonstrating that a requested closing time can fall outside trading availability.

SourceGapAudit protocol

SourceGapAudit is a separate, non-trading Expert Advisor that runs only in the Strategy Tester. InpFirstSource and InpLastSource select the actual first and last accepted M1 source bar openings. InpAuditTag identifies its two CSV files.

SourceGapAudit reads the M1 history for that interval and checks both endpoints. It enumerates SymbolInfoSessionQuote() and SymbolInfoSessionTrade() separately. Quote sessions describe nominal source availability; trade sessions describe nominal order availability.

The weekly session snapshot is labeled WEEKLY_SNAPSHOT_UNVERIFIED. Overnight sessions cross into the following day; intervals use an inclusive start and exclusive end. Zero-length entries add no open minutes. API failures or incomplete enumeration leave affected minutes unclassified.

Each gap CSV row contains consecutive M1 source bar openings, absent-minute bounds, and absent-minute counts for quote sessions and trade sessions. The weekly session snapshot CSV preserves session bounds and API diagnostics. Weekly session snapshot context time is Strategy Tester time; it does not establish historical validity.

The gap CSV fields quote_inside_snapshot and quote_outside_snapshot count absent minutes inside quote sessions and outside quote sessions. The field quote_unknown_snapshot counts unclassified minutes. The trade_inside_snapshot, trade_outside_snapshot, and trade_unknown_snapshot fields apply the same counts to trade sessions. Historical validity is recorded separately as historical_calendar_status=UNVERIFIED.

A complete SourceGapAudit run returns 0 through OnTester(); unavailable M1 source bar endpoints or incomplete exports return −1.0e100. Initialization checks 12 cases covering session boundaries, overnight rollover, full-day and empty intervals, and unknown metadata. SourceGapAudit sends no orders.

Control-series protocol

Series Strategy Tester start Trading gate Liquidation rule Modeling mode
Main test series / validation test series 2025-06-01 0 (disabled) Strategy Tester end, 2026-01-01 1 minute OHLC / Every tick based on real ticks
Warm-up control series 2025-05-25 2025-06-01 00:00 Strategy Tester end, 2026-01-01 1 minute OHLC
Common-end control series 2025-05-25 2025-06-01 00:00 2025-12-31 18:00, first available M1 tick 1 minute OHLC

The common-end control series uses a liquidation gate at 18:00. A separate liquidation diagnostic at 23:30 failed for all 15 boundary offsets with TRADE_RETCODE_MARKET_CLOSED. Those invalid passes are excluded from the performance analysis. The control series measures dependence on the evaluation-window rules; it does not prove that every endpoint effect has disappeared.


Results across 15 boundary offsets

The results below are actual historical Strategy Tester outputs from BoundarySensitivityEA version 1.12 on build 6230. All 15 passes in the main test series and all 15 passes in the validation test series completed with runtime_failed=false. The warm-up control series and common-end control series also contain 15 valid passes each.

Image 2 displays the main test series optimization results in the Strategy Tester, in boundary offset order.

Optimization results

Image 2: XAUUSD main test series optimization results in the Strategy Tester, sorted by boundary offset.

The following table reports net profit, Profit Factor, maximum relative equity drawdown, and total trades. Display values are rounded; calculations use the original CSV precision. Monetary values are in USD. The valid custom optimization criterion equals net profit and is omitted from the table.

Boundary offset Net profit Total trades Maximum relative equity drawdown Profit Factor
0 4,450.44 519 27.80% 1.13
1 1,795.84 537 28.87% 1.05
2 4,531.54 547 24.40% 1.13
3 6,836.54 527 18.36% 1.20
4 7,004.54 527 24.17% 1.21
5 6,831.94 531 19.14% 1.20
6 6,411.34 531 22.91% 1.19
7 7,142.04 531 22.14% 1.21
8 6,646.24 521 22.86% 1.19
9 6,092.44 529 27.17% 1.17
10 6,635.64 541 28.39% 1.19
11 6,776.54 529 23.75% 1.19
12 4,638.44 523 27.35% 1.13
13 4,067.94 527 31.59% 1.11
14 6,180.34 525 31.30% 1.18

Image 3 combines net profit and Profit Factor from the main test series. Net profit remains positive at every integer-minute boundary offset and ranges from USD 1,795.84 to USD 7,142.04. Profit Factor exceeds one throughout this sample.

Boundary sensitivity

Image 3: Net profit and Profit Factor for the fixed XAUUSD strategy across all 15 boundary offsets.

The summary table reports the predeclared XAUUSD dispersion measures. The net profit IQR uses inclusive quartiles, following the QUARTILE.INC convention. The baseline net profit rank is one plus the number of boundary offsets with higher net profit, so rank 1 is best and rank 15 is worst.

Metric Result
Baseline net profit 4,450.44
Median net profit 6,411.34
Net profit IQR 2,219.25
Net profit range 5,346.20
Profitable boundary offsets 15/15
Baseline net profit rank 13/15
Profit Factor range 1.05–1.21
Maximum relative equity drawdown range 18.36%–31.59%
Total trades range 519–547

The modeling-mode comparison tables compare all 15 boundary offsets. They report net profit, Profit Factor, maximum relative equity drawdown, and total trades. The modeling-mode comparison describes this sample; it does not establish identical execution for other instruments or periods.

Boundary offset Net profit: main test series Net profit: validation test series Difference Relative difference Total trades: both test series
0 4,450.44 4,366.44 -84.00 -1.89% 519
1 1,795.84 1,413.64 -382.20 -21.28% 537
2 4,531.54 4,383.74 -147.80 -3.26% 547
3 6,836.54 6,897.74 61.20 0.90% 527
4 7,004.54 7,070.34 65.80 0.94% 527
5 6,831.94 6,661.14 -170.80 -2.50% 531
6 6,411.34 6,420.54 9.20 0.14% 531
7 7,142.04 7,153.64 11.60 0.16% 531
8 6,646.24 6,648.84 2.60 0.04% 521
9 6,092.44 6,068.84 -23.60 -0.39% 529
10 6,635.64 6,657.44 21.80 0.33% 541
11 6,776.54 6,786.34 9.80 0.14% 529
12 4,638.44 4,736.04 97.60 2.10% 523
13 4,067.94 4,159.34 91.40 2.25% 527
14 6,180.34 6,131.34 -49.00 -0.79% 525

The second modeling-mode comparison table reports Profit Factor and maximum relative equity drawdown. Display values are rounded; calculations use the original CSV precision.

Boundary offset Profit Factor: main test series Profit Factor: validation test series Maximum relative equity drawdown: main test series Maximum relative equity drawdown: validation test series
0 1.13 1.12 27.80% 27.87%
1 1.05 1.04 28.87% 30.96%
2 1.13 1.12 24.40% 24.53%
3 1.20 1.20 18.36% 17.62%
4 1.21 1.21 24.17% 24.11%
5 1.20 1.19 19.14% 19.10%
6 1.19 1.18 22.91% 22.82%
7 1.21 1.21 22.14% 22.08%
8 1.19 1.19 22.86% 22.84%
9 1.17 1.17 27.17% 27.12%
10 1.19 1.19 28.39% 28.30%
11 1.19 1.19 23.75% 23.69%
12 1.13 1.13 27.35% 27.29%
13 1.11 1.11 31.59% 31.50%
14 1.18 1.18 31.30% 31.24%

M1 source coverage and control series

Each pass in the main test series accepts 206,509 M1 source bars, from 2025-05-30 23:56 through 2025-12-31 23:57 in server time. Although the Strategy Tester starts on 2025-06-01, first_m1 records the preceding available M1 source bar, dated 2025-05-30 23:56, which is discarded with the initialization aggregate. The first accepted M1 source bar forms the discarded initialization aggregate. The 253 gaps contain 103,093 absent minutes. An absent minute does not establish missing broker data.

Published shifted-bar counts range from 13,843 to 13,995. Each boundary offset has between 248 and 378 published shifted bars with fewer than 15 M1 source bars. Between 0 and 14 M1 source bars remain in the unpublished tail. First published shifted-bar openings range from 2025-06-02 00:46 to 01:00. Last published shifted-bar openings range from 2025-12-31 23:29 to 23:43.

Observed gaps and weekly session snapshot

Four SourceGapAudit runs reproduce the M1 source coverage counters of all corresponding strategy passes. Quote sessions and trade sessions coincide in the weekly session snapshot, so their absent-minute counts match. The table reports absent-minute counts for quote sessions in the weekly session snapshot.

Series M1 source bars Gaps Absent minutes Inside quote sessions Outside quote sessions Unclassified minutes
Main test series 206,509 253 103,093 12,819 90,274 0
Validation test series 206,638 254 102,964 12,690 90,274 0
Warm-up control series 213,136 263 106,546 13,326 93,220 0
Common-end control series 212,777 263 106,546 13,326 93,220 0

In the main test series, the weekly session snapshot places 90,274 absent minutes outside quote sessions and 12,819 inside quote sessions. These counts do not establish confirmed historical closures or missing broker data. All historical causes remain unverified.

The main test series also contains 8,188 observed M1 source bars outside quote sessions in the weekly session snapshot. This disagreement prevents treating the weekly session snapshot as an authoritative historical session calendar. A dated historical session calendar with holiday and schedule exceptions is required to identify confirmed historical closures.

The validation test series exposes 129 more accepted M1 source bars than the main test series within the same M1 source bar endpoints. Its gap count is 254 rather than 253. Matching dates therefore does not establish identical M1 source coverage between modeling modes.

In the main test series, SMA readiness occurs on 2025-06-02 between 08:16 and 08:30. In the warm-up control series, SMA readiness occurs on 2025-05-26 between 08:16 and 08:30, before the trading gate. The table separates the warm-up control series net profit change from the common-end control series net profit change.

Boundary offset Net profit: main test series Net profit: warm-up control series Net profit change: warm-up control series Net profit: common-end control series Net profit change: common-end control series vs warm-up control series
0 4,450.44 4,450.44 0.00 4,445.44 -5.00
1 1,795.84 1,795.84 0.00 1,846.44 50.60
2 4,531.54 4,561.74 30.20 4,679.34 117.60
3 6,836.54 6,836.54 0.00 6,982.34 145.80
4 7,004.54 7,004.54 0.00 7,163.14 158.60
5 6,831.94 6,831.94 0.00 7,022.14 190.20
6 6,411.34 6,411.34 0.00 6,555.34 144.00
7 7,142.04 7,142.04 0.00 7,277.64 135.60
8 6,646.24 6,646.24 0.00 6,682.04 35.80
9 6,092.44 6,092.44 0.00 6,152.44 60.00
10 6,635.64 6,635.64 0.00 6,723.04 87.40
11 6,776.54 6,776.54 0.00 6,811.94 35.40
12 4,638.44 4,608.44 -30.00 4,662.84 54.40
13 4,067.94 4,067.94 0.00 4,089.14 21.20
14 6,180.34 6,180.34 0.00 6,174.34 -6.00

The warm-up control series changes net profit by USD +30.20 at boundary offset 2 and −30.00 at boundary offset 12. Other displayed net profits match the main test series. The common-end control series changes net profit by USD −6.00 to +190.20 relative to the warm-up control series. All 15 boundary offsets remain profitable in both control series. These differences show that initialization and liquidation rules contribute to the boundary-sensitivity profile.


Interpreting the boundary-sensitivity profile

In this XAUUSD sample, all 15 boundary offsets in the main test series are profitable and have Profit Factor above one. Net profit ranges from USD 1,795.84 to USD 7,142.04. Maximum relative equity drawdown ranges from 18.36% to 31.59%.

The baseline offset ranks 13th of 15, with net profit USD 1,960.90 below median net profit. This rank locates the baseline offset within the boundary-sensitivity profile; it is not a predefined test of representativeness.

Total trades range from 519 to 547 in the main test series. The execution policy processes new completed data before a retry, preventing an older pending signal from being attempted ahead of a newly confirmed crossover.

All 15 boundary offsets remain profitable in the validation test series, and total trades match the main test series. Absolute net profit differences range from 0.04% to 21.28%. Boundary offset 1 falls from USD 1,795.84 to USD 1,413.64 (−21.28%). The complete net profit ranking changes. A 1 minute OHLC boundary-sensitivity profile therefore does not establish execution equivalence with Every tick based on real ticks.

The boundary offset with maximum net profit is an observed sensitivity result, not a deployment setting.


Limitations

These results concern one XAUUSD history, one interval, and one MetaQuotes-Demo symbol specification. Net profit sign stability is limited to this sample. Other markets, periods, servers, costs, and out-of-sample windows require separate tests.

Shared M1 source bars prevent the 15 passes from being interpreted as independent replications or from supporting a conventional independence-based p-value.

M1 aggregation cannot validate rules that depend on the path inside a minute. Even this close-based EA places market orders whose execution depends on the modeling mode. The complete modeling-mode comparison measures that dependence for this sample; the demonstration excludes intrabar stops, targets, pending orders, and trailing logic.

The 10/30 SMA crossover is one transparent demonstration. Its sensitivity does not represent every trend, breakout, mean-reversion, or range-based EA.

Generic aggregation does not make an arbitrary EA plug-and-play. Each target-timeframe indicator or price dependency must be ported to the shifted-bar stream before its result can represent that strategy.

The fixed-minute engine supports standard timeframes through W1. Calendar-month boundaries require a separate calendar-aware builder and are intentionally rejected.


Reproducing the test

  1. Extract the archive into the terminal data folder and compile the EA and self-test in MetaEditor.
  2. Run the self-test and retain its complete output.
  3. Select XAUUSD, M1, the declared account settings, and the common test interval.
  4. Set InpTargetTimeframe=PERIOD_M15, fast 10, slow 30, volume 0.10, magic 26081115, and deviation 20 points.
  5. Enable export. Set InpTestStart=2025.06.01 00:00, InpTestEnd=2026.01.01 00:00, and InpExecutionDelayMs=0. Keep the trading gate and liquidation gate at zero for the main test series and validation test series.
  6. With 1 minute OHLC, set InpModelingMode to that exact label and InpEvidenceTag=m2v112-main. Run complete optimization of InpBoundaryOffset from 0 through 14, step 1.
  7. Repeat all 15 boundary offsets with Every tick based on real ticks, its matching metadata label, and InpEvidenceTag=m2v112-real.
  8. For the warm-up control series, start the Strategy Tester and InpTestStart on 2025.05.25. Set InpEvaluationStart=2025.06.01 00:00, keep InpEvaluationEnd=0, and use 1 minute OHLC with tag m2v112-warm.
  9. For the common-end control series, repeat the warm-up control series with InpEvaluationEnd=2025.12.31 18:00 and tag m2v112-end1800.
  10. Run SourceGapAudit on XAUUSD, M1, without optimization, under the corresponding modeling mode. Set InpFirstSource=first_m1 and InpLastSource=last_m1 from the strategy CSV. Use the same Strategy Tester dates and a distinct InpAuditTag for each M1 source coverage interval.
  11. With InpAuditTag=xauusd-main, collect xauusd-main_XAUUSD_gaps.csv and xauusd-main_XAUUSD_sessions.csv from Common\Files\BoundarySensitivity. Require GAP_AUDIT_COMPLETE and a SourceGapAudit custom optimization criterion of 0. Match source count, gap count, and absent minutes against the strategy evidence.
  12. Retain reports, configurations, logs, symbol specifications, and source hashes. Require 15 complete, valid rows per series before calculating summaries.

Merge the generated one-row pass files before calculating the declared summaries. Calculate median net profit, net profit IQRs using QUARTILE.INC, baseline net profit ranks, summary tables, and figures from those rows instead of transcribing values from the Strategy Tester interface.


Possible improvements

Data and aggregation:

  • Validate the weekly session snapshot comparisons against a dated historical session calendar, including holiday exceptions and schedule changes.

Strategy coverage:

  • Provide ready-made shifted implementations of common indicators.

Analysis:

  • Measure position-state agreement against the baseline offset.
  • Measure entry-time overlap under a declared tolerance.
  • Repeat the complete boundary-sensitivity profile in walk-forward windows.


Conclusion

The workflow replaces a single backtest at the baseline offset with a boundary-sensitivity profile across integer-minute boundary offsets. The exporter makes runtime validity, execution costs, and M1 source coverage auditable. SourceGapAudit records gaps and separates weekly session snapshot comparisons from unverified historical causes. The warm-up control series and common-end control series measure initialization and end-of-test clipping effects.

In the XAUUSD M15 main test series, all 15 boundary offsets remain profitable. Net profit ranges from USD 1,795.84 to USD 7,142.04, and the baseline offset ranks 13th. All boundary offsets remain profitable in the validation test series. Boundary offset 1 loses 21.28% of its main test series net profit. Net profit sign stability therefore coexists with modeling-mode sensitivity.

The workflow locates the baseline offset within the boundary-sensitivity profile and quantifies performance dispersion. The boundary-sensitivity profile provides a sample-specific diagnostic alongside out-of-sample, cost, modeling-mode, and cross-market testing. It neither defines a binary criterion of representativeness nor establishes general strategy robustness.


File manifest

MQL5.zip contains the following source files:

File name Description
MQL5\Include\BoundarySensitivity\ShiftedBarBuilder.mqh Reusable fixed-duration shifted-bar builder
MQL5\Experts\BoundarySensitivity\BoundarySensitivityEA.mq5 Demonstration EA and boundary-offset optimization harness
MQL5\Scripts\BoundarySensitivity\ShiftedBarBuilderSelfTest.mq5 Deterministic self-test for aggregation, boundary offsets, reset behavior, and validation rules
MQL5\Experts\BoundarySensitivity\SourceGapAudit.mq5 Gap CSV and weekly session snapshot CSV export
Attached files |
MQL5.zip (21.01 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.
Feature Neutralization in MQL5: Orthogonalizing Trading Signals Against Confound Factors Feature Neutralization in MQL5: Orthogonalizing Trading Signals Against Confound Factors
We implement feature neutralization so your signal is evaluated without volatility/trend confounds. The core is a native MQL5 rolling OLS engine using Gauss-Jordan elimination and out-of-sample residual scoring, with no DLLs. An EA and indicator integrate the residual and log diagnostics, while a Python companion validates numerics and runs bar-replay tests. Use it to isolate linear exposure and assess true signal contribution.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Neural Networks in Trading: Training Deep Spiking Models (Spike Integration) Neural Networks in Trading: Training Deep Spiking Models (Spike Integration)
The article presents a practical implementation of the key components of the SEW-ResNet framework using MQL5. The use of dynamic arrays and spiking mechanisms enables flexible model architecture design and efficient processing of financial time series. The proposed solutions demonstrate how SEW-ResNet can optimize computations and improve the extraction of meaningful features.