Does Your EA Depend on Candle Boundaries? A Shifted-Bar Stress Test in MQL5
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.

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.

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.

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
- Extract the archive into the terminal data folder and compile the EA and self-test in MetaEditor.
- Run the self-test and retain its complete output.
- Select XAUUSD, M1, the declared account settings, and the common test interval.
- Set InpTargetTimeframe=PERIOD_M15, fast 10, slow 30, volume 0.10, magic 26081115, and deviation 20 points.
- 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.
- 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.
- Repeat all 15 boundary offsets with Every tick based on real ticks, its matching metadata label, and InpEvidenceTag=m2v112-real.
- 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.
- For the common-end control series, repeat the warm-up control series with InpEvaluationEnd=2025.12.31 18:00 and tag m2v112-end1800.
- 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.
- 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.
- 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 |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Features of Custom Indicators Creation
Feature Neutralization in MQL5: Orthogonalizing Trading Signals Against Confound Factors
Features of Experts Advisors
Neural Networks in Trading: Training Deep Spiking Models (Spike Integration)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use