Temporal Causality Audit in MQL5: Validating Recorded Dependencies Against the Decision Time
Introduction
An Expert Advisor opens a BUY on EURUSD at 12:35 on the M5 chart and reads an H1 trend value. Was the value available at 12:35, or does it reflect the H1 candle's final close at 13:00? An equity curve cannot answer that question.
A forming candle is not automatically evidence of look-ahead: an Expert Advisor may use its current value under an intrabar policy. The key is to establish which value was used, when it became available, and whether that timing is consistent with the decision policy.
This article builds an MQL5 library to make those assumptions explicit. You will learn how to record the inputs behind a decision, select a closed-bar or intrabar policy, and reproduce named validation failures with deterministic scenarios. The audit checks declared timing only. It cannot detect an input that was not recorded, or prove that an indicator did not repaint.
When a Backtest Knows Too Much
Look-ahead leaks rarely come from deliberate intent. They come from assumptions that hold in one context and break in another:
- Shift 0 in closed-bar logic. Index 0 is the currently forming bar, and its value keeps moving until the bar closes.
- Higher-timeframe reads. The current H1 value keeps changing until 13:00, so a 12:35 decision that ends up with its final version has consumed 25 minutes of future information.
- Other symbols. GBPUSD and EURUSD tick independently, so a GBPUSD value can carry a timestamp later than the EURUSD decision that uses it.
- Index arithmetic. A negative shift or an off-by-one offset can point at a bar that has not started yet.
The H1 case is the reference example for the rest of the article. At 12:35, the previous H1 bar is complete, while the current one is only partly formed, as shown below.

Figure 1. An M5 decision at 12:35 against the closed H1 bar (11:00–12:00) and the forming H1 bar (12:00–13:00)
An equity curve cannot separate these cases, because it never shows which version of a bar the logic read. The attached demo Expert Advisor makes the difference visible. It runs five scenarios anchored to 2026.01.01 12:35: the two correctly built ones pass, while the future bar, the unfinished H1 candle, and the mismatched timestamp fail.

Figure 2. Experiment summary printed by TemporalAuditDemo.mq5 in the Experts log
Implementation in MQL5
The implementation starts with the timing model that every check relies on and runs the original M5/H1 case as a minimal example. It then covers the validation rules, the library structure and integration workflow, and the reproducible demo and tests.
Timing Model and Validation BoundariesMost rules compare times, so the timing model comes first. A decision involves several times, and only some of them are recorded:
- Decision time is the moment the logic commits to an action, such as 12:35.
- Bar open and close times delimit the bar a value belongs to. A bar that closes after the decision was still forming when the decision was made.
- Observation timestamp is the time at which the value became available, as asserted by the caller. For a closed bar, the convention is the bar's close time. For a forming bar, it is the moment the value was sampled.
- Read time and indicator calculation time describe when the Expert Advisor retrieved the value and when the indicator computed it. Neither is recorded.
Because the observation timestamp describes availability, a value may be read long after it became available and still pass. A value from the 09:55–10:00 bar read at 10:05 for a 10:10 decision is declared with an observation timestamp of 10:00. Recording 10:05 instead would put a read time into the availability field, and Rule 5 would reject the declaration.
Bar boundaries and the closed flag come from the caller and are taken as given. Reading them back with iTime() would tie each verdict to the loaded history and rule out tests without market data. Some history-free checks would remain deterministic, for example comparing the bar length with PeriodSeconds() of its timeframe. The prototype does not implement these checks yet. Its verdicts are therefore only as reliable as the caller's metadata.
Each verdict pairs a status (TEMPORAL_PASS, TEMPORAL_WARNING, or TEMPORAL_FAIL) with a violation type. Every violation type other than VIOLATION_NONE maps to exactly one rule; VIOLATION_NONE is used for PASS and WARNING outcomes.
// --- Specific violation types detected by the validator enum ENUM_VIOLATION_TYPE { VIOLATION_NONE, // No violation detected VIOLATION_FUTURE_TIMESTAMP, // Observation timestamp after the decision time VIOLATION_FUTURE_BAR, // Referenced bar opens after the decision time VIOLATION_UNFINISHED_BAR, // Unfinished bar used in closed-bar mode VIOLATION_MTF_TIMING, // Higher-timeframe candle not closed at decision time VIOLATION_TIMESTAMP_MISMATCH, // Observation timestamp outside its own bar range VIOLATION_INVALID_SHIFT, // Bar shift is negative (only negative shifts are caught) VIOLATION_CROSS_SYMBOL_TIMING // Cross-symbol dependency with a future observation timestamp };
The STemporalAuditResult structure stores both fields together with the decision context and all dependency metadata, so each result can explain itself.
A Minimal Closed-Bar M5/H1 Example
Before the rules are examined one by one, the reference case from Figure 1 can be run end to end. The demo Expert Advisor sets the closed-bar policy in OnInit(). These values match the constructor defaults, but setting them explicitly keeps the policy visible at the call site.
//+------------------------------------------------------------------+ //| OnInit - configure policy and run both demonstration suites | //+------------------------------------------------------------------+ int OnInit() { //--- configure the main audit policy (closed-bar decisions) g_config.decision_mode = DECISION_ON_BAR_CLOSE; // closed-bar decision mode g_config.require_closed_bars = true; // enforce closed bars g_config.timestamp_tolerance_seconds = 1; // one second precision tolerance g_config.reject_unfinished_mtf = true; // reject unfinished MTF candles g_config.validate_cross_symbol_time = true; // validate cross-symbol timing //--- run the five primary scenarios RunScenarios(); // execute the main experiment //--- run the supplementary intrabar demonstration RunIntrabarDemonstration(); // execute the intrabar example return(INIT_SUCCEEDED); // signal successful initialization }
RunScenarios() then records the two versions of the H1 input as two separate decisions. The blocks below are copied from that function, where base is the fixed decision time 2026.01.01 12:35, and the audit and report objects are created at the top of the function. The first block, scenario 5 in Table 3, declares the closed 11:00–12:00 H1 bar, which became available at 12:00.
//--- scenario 5: correct MTF dependency Print("\n=== Scenario 5: Correct MTF dependency ==="); // section banner audit.BeginDecision(base, DEMO_SYMBOL, PERIOD_M5, "BUY"); // start decision context audit.AddDependency("H1 Trend", DEMO_SYMBOL, PERIOD_H1, base - 2100, 1, base - 5700, base - 2100, true); // previous closed H1 candle audit.Audit(); // execute validation report.PrintReport(audit); // print detailed diagnostics scenario_names[4] = "Correct MTF"; // record scenario label scenario_results[4] = audit.GetOverallStatus(); // record overall status
The second block, scenario 3 in Table 3, declares the forming 12:00–13:00 H1 bar at shift 0.
//--- scenario 3: unfinished MTF candle Print("\n=== Scenario 3: Unfinished MTF candle ==="); // section banner audit.BeginDecision(base, DEMO_SYMBOL, PERIOD_M5, "BUY"); // start decision context audit.AddDependency("H1 Trend", DEMO_SYMBOL, PERIOD_H1, base - 300, 0, base - 2100, base + 1500, false); // forming H1 candle audit.Audit(); // execute validation report.PrintReport(audit); // print detailed diagnostics scenario_names[2] = "Unfinished H1 candle"; // record scenario label scenario_results[2] = audit.GetOverallStatus(); // record overall status
The first decision passes, because its H1 bar closed 35 minutes before the decision. The second fails with VIOLATION_MTF_TIMING, reported by Rule 4, which the next section defines. Figure 4 later shows its detailed diagnostics.
In live code, each dependency is declared with the same symbol, timeframe, and shift as the trading input. For example, at 12:35, iTime("EURUSD", PERIOD_H1, 1) returns 11:00, and adding PeriodSeconds(PERIOD_H1) yields a close of 12:00, which is used as the observation timestamp even though the value is read at 12:35. At shift 0, the same arithmetic yields 12:00–13:00, a forming bar that Rule 4 rejects. The library never makes these calls itself; trading logic, market data, and the decision all stay with the Expert Advisor.
Validation Rules and Policy
CTemporalValidator applies the rules below in a fixed order and stops at the first one that fires. In the table, t_dec is the decision time, t_obs the observation timestamp, and τ the tolerance in seconds.
| Order | Rule | Condition that fires | Status | Violation type |
|---|---|---|---|---|
| 1 | Future bar | Bar open time > t_dec | FAIL | VIOLATION_FUTURE_BAR |
| 2 | Invalid shift | Bar shift < 0 | FAIL | VIOLATION_INVALID_SHIFT |
| 3 | Cross-symbol future timestamp | Symbol differs from the decision symbol and t_obs > t_dec, compared without tolerance (only when validate_cross_symbol_time is true) | FAIL | VIOLATION_CROSS_SYMBOL_TIMING |
| 4 | Higher-timeframe timing | Dependency timeframe is longer than the decision timeframe and bar close time > t_dec (only when reject_unfinished_mtf is true) | FAIL | VIOLATION_MTF_TIMING |
| 5 | Timestamp mismatch | t_obs outside the inclusive range from bar open time to bar close time | FAIL | VIOLATION_TIMESTAMP_MISMATCH |
| 6 | Unfinished bar | Closed bars required and the bar is still forming | FAIL | VIOLATION_UNFINISHED_BAR |
| 7 | Future timestamp | t_obs > t_dec + τ | FAIL | VIOLATION_FUTURE_TIMESTAMP |
| 8 | Within tolerance | t_dec < t_obs ≤ t_dec + τ | WARNING | VIOLATION_NONE |
Table 1. Validation rules in the order they are evaluated
The complete Validate() method is in the attached CTemporalValidator.mqh. Each rule there is a short if block that sets the status, the violation type, and a diagnostic message, and then returns. The method relies on three private helpers.
//--- IsHigherTimeframe - compare two timeframes by actual duration bool IsHigherTimeframe(const ENUM_TIMEFRAMES a, const ENUM_TIMEFRAMES b) const { int seconds_a = PeriodSeconds(a); // period duration of the first timeframe int seconds_b = PeriodSeconds(b); // period duration of the second timeframe return (seconds_a > seconds_b); // true when the first is strictly longer } //--- IsInsideBarRange - check whether a timestamp lies inside a bar range bool IsInsideBarRange(const datetime ts, const datetime bar_open, const datetime bar_close) const { return (ts >= bar_open && ts <= bar_close); // inclusive range comparison } //--- DECISION_ON_BAR_CLOSE always requires closed bars. The //--- require_closed_bars flag enforces closed bars independently, //--- which allows a stricter configuration than the decision mode //--- alone would imply. bool RequiresClosedBars() const { return (m_config.decision_mode == DECISION_ON_BAR_CLOSE) || m_config.require_closed_bars; // combined policy check }
Rules 1–4 reject inputs that cannot be trusted at decision time. A bar opening after the decision fails Rule 1 even when its shift is also negative. Rule 2 rejects negative shifts, but it cannot check large positive ones without price history. Rule 3 catches a cross-symbol timestamp later than the decision. It compares without tolerance by design, treating a later quote from another symbol as a synchronization failure. Stale quotes and other synchronization problems are not detected. Rule 4 rejects a higher-timeframe bar that closes after the decision, which is exactly the H1 case from Figure 1. IsHigherTimeframe() compares durations, not the numeric order of ENUM_TIMEFRAMES values.
Rules 5–8 cover consistency, policy, and precision. Under the availability convention, a value cannot become available outside its own bar, so Rule 5 rejects an observation timestamp outside the bar range. The range is inclusive because a closed bar's value becomes available exactly at its close; the cost is that a boundary timestamp also equals the next bar's open. Rule 6 enforces the closed-bar policy, and Rules 7 and 8 apply the tolerance.
Because the validator returns on the first match, rule order is a reporting policy, not a severity ranking. Only the first failing rule is reported; it may not be the root cause. Two cases from the attached code illustrate this:
- A bar opening at 12:40 with shift −1 is reported as FUTURE_BAR rather than INVALID_SHIFT.
- A forming H1 bar in closed-bar mode is reported as MTF_TIMING rather than UNFINISHED_BAR.
Rules 7 and 8 compare the observation timestamp with the decision time as follows:
- If t_obs ≤ t_dec, the dependency passes.
- If t_dec < t_obs ≤ t_dec + τ, the result is WARNING.
- If t_obs > t_dec + τ, the result is FAIL with VIOLATION_FUTURE_TIMESTAMP.
The tolerance absorbs rounding between time sources, such as a tick time and a bar time. It applies to Rules 7 and 8 only; Rules 1, 3, 4, and 5 compare times exactly. A WARNING is not a softer pass. It means the recorded metadata is too coarse to decide either way. With the default τ of 1 second, the warning zone covers a single datetime step.
RequiresClosedBars() returns true in closed-bar mode or whenever require_closed_bars is set, so the flag can only tighten the policy. A forming bar can therefore pass only under DECISION_INTRABAR with the flag cleared. Even then, a forming bar from a higher timeframe is rejected by Rule 4 as long as reject_unfinished_mtf is true. The five policy fields and their defaults are summarized below.
| Field | Type | Default value | Effect |
|---|---|---|---|
| decision_mode | ENUM_DECISION_MODE | DECISION_ON_BAR_CLOSE | Selects the closed-bar or intrabar decision policy |
| require_closed_bars | bool | true | Requires closed bars regardless of the decision mode |
| timestamp_tolerance_seconds | int (seconds) | 1 | Width of the WARNING zone after the decision time |
| reject_unfinished_mtf | bool | true | Enables Rule 4 for higher-timeframe bars that close after the decision |
| validate_cross_symbol_time | bool | true | Enables Rule 3, which rejects future timestamps on other symbols |
Table 2. Fields of STemporalConfig
Library Structure and Integration Workflow
The library consists of seven headers. TemporalTypes.mqh and TemporalConfig.mqh hold the shared enumerations, the result structure, and the policy. CTemporalDependency.mqh and CDependencyTracker.mqh record the declared inputs, and CTemporalValidator.mqh applies the rules. CTemporalAudit.mqh orchestrates the audit, while CCausalityReport.mqh prints the results.

Figure 3. Flow of recorded dependencies through the audit pipeline
The trading code works with CTemporalAudit through four methods: BeginDecision(), AddDependency(), Audit(), and GetResult(). The orchestrator owns a dependency tracker, a validator, and the result array. Its BeginDecision() resets the tracker and also clears earlier results.
//--- BeginDecision - reset tracker and clear previous results void BeginDecision(const datetime decision_time, const string decision_symbol, const ENUM_TIMEFRAMES decision_timeframe, const string decision_action) { m_tracker.BeginDecision(decision_time, decision_symbol, decision_timeframe, decision_action); // forward decision context ArrayResize(m_results, 0); // clear previous results }
AddDependency() is forwarded to CDependencyTracker, which appends one CTemporalDependency record and assigns it the next identifier.
// --- AddDependency - record one dependency against the decision void AddDependency(const string feature_name, const string symbol, const ENUM_TIMEFRAMES timeframe, const datetime timestamp, const int bar_shift, const datetime bar_open_time, const datetime bar_close_time, const bool is_bar_closed, const int parent_dependency_id = -1) { int size = ArraySize(m_dependencies); // current array size ArrayResize(m_dependencies, size + 1); // grow array by one slot m_dependencies[size] = CTemporalDependency(feature_name, symbol, timeframe, timestamp, bar_shift, bar_open_time, bar_close_time, is_bar_closed, parent_dependency_id); // construct dependency in place m_dependencies[size].SetDependencyID(m_next_dependency_id); // assign identifier m_next_dependency_id++; // advance identifier counter }
Eight of the nine parameters fall into three groups:
- feature_name, symbol, and timeframe identify the value.
- timestamp is the observation timestamp, that is, the availability time.
- bar_shift, bar_open_time, bar_close_time, and is_bar_closed describe the referenced bar, with the closed flag evaluated at the decision time.
The ninth, parent_dependency_id, is optional and is stored but never validated.
Audit() then passes every record to the validator.
//--- Audit - validate every recorded dependency against the context int Audit() { int count = m_tracker.GetDependencyCount(); // number of recorded dependencies ArrayResize(m_results, count); // allocate result slots datetime decision_time = m_tracker.GetDecisionTime(); // decision timestamp string decision_symbol = m_tracker.GetDecisionSymbol(); // decision symbol ENUM_TIMEFRAMES decision_timeframe = m_tracker.GetDecisionTimeframe(); // decision timeframe //--- delegate each dependency to the validator with full context for(int i = 0; i < count; i++) // iterate over dependencies { CTemporalDependency dep; // dependency buffer if(m_tracker.GetDependency(i, dep)) // retrieve the dependency m_validator.Validate(dep, decision_time, decision_symbol, decision_timeframe, m_results[i]); // validate else { m_results[i].status = TEMPORAL_WARNING; // fallback status on retrieval failure m_results[i].violation = VIOLATION_NONE; // no violation recorded m_results[i].message = "Dependency retrieval failed."; // diagnostic message } } return count; // return audited count }
If a record cannot be retrieved, its slot becomes a WARNING, so an unreadable dependency never passes silently. The method returns the number of dependencies audited, and GetOverallStatus() folds the results into one verdict.
//--- GetOverallStatus - aggregate status across all dependencies ENUM_TEMPORAL_STATUS GetOverallStatus() const { bool has_warning = false; // flag for warning presence for(int i = 0; i < ArraySize(m_results); i++) // iterate over all results { if(m_results[i].status == TEMPORAL_FAIL) // fail short-circuits return TEMPORAL_FAIL; // return fail immediately if(m_results[i].status == TEMPORAL_WARNING) // warning detected has_warning = true; // remember warning presence } if(has_warning) // any warnings found return TEMPORAL_WARNING; // return warning status return TEMPORAL_PASS; // otherwise all pass }
Any FAIL wins, then any WARNING, and otherwise the result is PASS. An empty result array also yields PASS, so a caller should confirm that Audit() returned a positive count before trusting the verdict. CCausalityReport only formats the results for the Experts log and contains no rules.
To reproduce the results, install and run the attached files in the following order:
- Copy the seven include files to MQL5\Include\TemporalAudit, TemporalAuditDemo.mq5 to MQL5\Experts\TemporalAudit, and TemporalAuditTest.mq5 to MQL5\Scripts\TemporalAudit. Both programs include the headers as <TemporalAudit/...>, so the folder name must match.
- Compile TemporalAuditDemo.mq5 and TemporalAuditTest.mq5 in MetaEditor.
- Attach TemporalAuditDemo to any chart. The demo runs in OnInit() and writes to the Experts tab. Its experiment table should show PASS for "Correct causal decision" and "Correct MTF", and FAIL for "Future bar", "Unfinished H1 candle", and "Timestamp mismatch". The supplementary intrabar block should report PASS.
- Check the detailed diagnostics. Each dependency block lists the bar range with its state, the dependency and decision times, the status, and the reason set by the rule that fired. For the unfinished H1 candle, the reason reads "Higher-timeframe candle was not closed at the decision time."
- Run TemporalAuditTest on any chart. Each test prints a PASS or FAIL line, and the summary should read "Total: 12 Passed: 12 Failed: 0".
Deterministic Demo and Test Coverage
The demo Expert Advisor sets the default closed-bar policy in OnInit() and then calls RunScenarios() and RunIntrabarDemonstration(). RunScenarios() reuses a single CTemporalAudit object for five decisions, which works because BeginDecision() clears both dependencies and results; its full listing is in the attached TemporalAuditDemo.mq5. Every demo and test time is an offset from 2026.01.01 12:35, and no quotes are read. The output is therefore identical on any chart and with any broker, in the Strategy Tester or live.
| No. | Scenario | Dependency (timeframe) | Observation time | Referenced bar | Bar state | Shift (bars) | Result |
|---|---|---|---|---|---|---|---|
| 1 | Correct causal decision | RSI(14), ATR(14) (M5); H1 Trend (H1) | 12:30; 12:00 | 12:25–12:30; 11:00–12:00 | Closed | 1 | PASS |
| 2 | Future bar | Future Bar (M5) | 12:40 | 12:40–12:45 | Forming | −1 | FAIL, FUTURE_BAR |
| 3 | Unfinished H1 candle | H1 Trend (H1) | 12:30 | 12:00–13:00 | Forming | 0 | FAIL, MTF_TIMING |
| 4 | Timestamp mismatch | Mismatched Feature (M5), decision at 10:00 | 10:05 | 09:55–10:00 | Closed | 1 | FAIL, TIMESTAMP_MISMATCH |
| 5 | Correct multi-timeframe input | H1 Trend (H1) | 12:00 | 11:00–12:00 | Closed | 1 | PASS |
| S | Intrabar mode | Current M5 RSI (M5) | 12:35 | 12:34–12:39 | Forming | 0 | PASS |
Table 3. Deterministic scenarios in TemporalAuditDemo.mq5 (EURUSD, M5 decision)
Scenarios 1 and 5 pass because every input comes from a bar that closed before the decision. Scenario 2 references a bar opening at 12:40, so Rule 1 fires before Rule 2 sees the negative shift. Scenario 3 is the H1 case from Figure 1, rejected by Rule 4. In scenario 4, the decision is at 10:00, and the value is declared as available at 10:05 for a bar that ended at 10:00. A value from that bar is available at 10:00, so Rule 5 rejects the declaration before Rule 7 is reached.

Figure 4. Detailed diagnostics for scenario 3 (unfinished H1 candle)
The supplementary case shows a complete instrumented decision in intrabar mode.
//+------------------------------------------------------------------+ //| RunIntrabarDemonstration - supplementary intrabar scenario | //+------------------------------------------------------------------+ void RunIntrabarDemonstration() { Print("\n=== Supplementary: Intrabar mode ==="); // section banner //--- intrabar configuration permits forming bars STemporalConfig intrabar_cfg; // intrabar configuration object intrabar_cfg.decision_mode = DECISION_INTRABAR; // intrabar decision mode intrabar_cfg.require_closed_bars = false; // forming bars permitted intrabar_cfg.timestamp_tolerance_seconds = 1; // one second precision tolerance intrabar_cfg.reject_unfinished_mtf = true; // reject unfinished MTF candles intrabar_cfg.validate_cross_symbol_time = true; // validate cross-symbol timing datetime base = D'2026.01.01 12:35'; // fixed anchor timestamp CTemporalAudit audit(intrabar_cfg); // audit orchestrator CCausalityReport report; // report generator //--- intrabar decision with the forming current M5 bar audit.BeginDecision(base, DEMO_SYMBOL, PERIOD_M5, "BUY"); // start decision context audit.AddDependency("Current M5 RSI", DEMO_SYMBOL, PERIOD_M5, base, 0, base - 60, base + 240, false); // forming current M5 bar audit.Audit(); // execute validation report.PrintReport(audit); // print detailed diagnostics }
With intrabar mode selected and require_closed_bars cleared, Rule 6 is skipped. The observation time also equals the decision time, so the result is PASS. Shift 0 is therefore not wrong by definition; its validity depends on the policy, the bar's timeframe, and the availability time. The declared bar, from 12:34 to 12:39, is off the M5 grid, and the validator does not check grid alignment. The aligned 12:35–12:40 bar would produce the same result.
The test script TemporalAuditTest.mq5 turns the rules into twelve assertions. They cover five of the seven violation types, including the cross-symbol rule that the demo never reaches, as well as the warning zone and both decision modes. Tests 01 to 10 use closed-bar mode, and tests 11 and 12 use intrabar mode.
| Test | Purpose | Key metadata | Expected status | Expected violation |
|---|---|---|---|---|
| 01 | Past dependency | Observed 12:30, bar 12:25–12:30, closed | PASS | NONE |
| 02 | Exact valid timestamp | Observed 12:35, bar 12:30–12:35, closed | PASS | NONE |
| 03 | Future timestamp | Observed 12:37, bar 12:35–12:40, marked closed | FAIL | FUTURE_TIMESTAMP |
| 04 | Future bar | Bar 12:40–12:45, shift −1 | FAIL | FUTURE_BAR |
| 05 | Unfinished current bar | Bar 12:34–12:39, forming, closed-bar mode | FAIL | UNFINISHED_BAR |
| 06 | Valid previous H1 candle | H1 bar 11:00–12:00, closed | PASS | NONE |
| 07 | Unfinished H1 candle | H1 bar 12:00–13:00, forming | FAIL | MTF_TIMING |
| 08 | Cross-symbol future timestamp | GBPUSD observed 12:36 | FAIL | CROSS_SYMBOL_TIMING |
| 09 | Tolerance boundary | Observed 12:35:01, τ = 1 s | WARNING | NONE |
| 10 | Multiple dependencies | Two closed M5 values observed 12:30 | PASS | NONE |
| 11 | Intrabar current bar | Intrabar mode, forming bar, observed 12:35 | PASS | NONE |
| 12 | Intrabar future timestamp | Intrabar mode, forming bar, observed 12:35:10 | FAIL | FUTURE_TIMESTAMP |
Table 4. Tests in TemporalAuditTest.mq5 (decision at 12:35 on EURUSD M5)
In test 09, the observation timestamp sits one second after the decision, inside the tolerance, so the test expects WARNING rather than FAIL. Test 03 needs a word of caution: its 12:35–12:40 bar is marked closed at 12:35, which is physically impossible. That flag is what lets it pass Rule 6 and reach Rule 7; with the correct flag, Rule 6 would report UNFINISHED_BAR. No rule currently checks the closed flag against the bar times.
Each outcome is checked by ExpectStatus().
//+------------------------------------------------------------------+ //| ExpectStatus - check overall status and first-dependency class | //+------------------------------------------------------------------+ bool ExpectStatus(const CTemporalAudit &audit, const ENUM_TEMPORAL_STATUS expected_status, const ENUM_VIOLATION_TYPE expected_violation) { //--- confirm the aggregated status is correct if(audit.GetOverallStatus() != expected_status) // compare aggregated status return false; // mismatch detected //--- confirm the first dependency carries the expected violation STemporalAuditResult res; // result buffer if(!audit.GetResult(0, res)) // retrieve first dependency return false; // retrieval failed return (res.violation == expected_violation); // compare violation type }
The helper compares both the overall status and the violation type of the first dependency. The second comparison matters. The GBPUSD input in test 08 breaks Rules 3 and 7, so a status-only check would still see FAIL if Rule 3 were removed or reordered. In test 10, the second dependency is covered through the overall status. VIOLATION_TIMESTAMP_MISMATCH and VIOLATION_INVALID_SHIFT have no isolated test, so the script would not catch a regression in Rule 2 or Rule 5. When the library behaves as documented, all twelve tests pass.

Figure 5. Output of TemporalAuditTest.mq5 with the twelve test results and the summary line
Limitations
The library is a research prototype and has not been wired into a live trading system. Its limitations are as follows:
- Instrumentation dependency. Only explicitly recorded inputs are validated. An input that is consumed but never recorded is invisible to the audit.
- Caller-supplied metadata. Bar boundaries and the closed flag are taken as given. Bar existence, interval direction, duration, grid alignment, closed-flag consistency, and the true source of the value are not checked, as the off-grid intrabar bar and test 03 show.
- No read time. Only the declared availability time is recorded, so the audit cannot confirm how the Expert Advisor actually behaved.
- Repainting indicators. Recalculated or look-ahead indicator values pass whenever the declared bar looks correct.
- Inclusive bar boundaries. A timestamp at a bar's close also equals the next bar's open. A half-open interval with an exception for the close would remove this ambiguity.
- Tolerance scope. The tolerance applies only to Rules 7 and 8, so a cross-symbol timestamp just one second after the decision is reported as FAIL rather than WARNING.
- Narrow cross-symbol rule. Only future cross-symbol timestamps are caught; stale or unsynchronized quotes are not.
- One violation per dependency. Only the first failing rule is reported, and it may not be the root cause.
- Negative shifts only. Large positive shifts are not checked against the available history.
- No dependency ancestry. The parent identifier is stored but never followed, so problems inherited by derived features are not traced.
- Empty audits report PASS. A decision without results reports PASS, so the count returned by Audit() should be checked.
- WARNING as uncertainty. A warning means the metadata is too coarse to decide, not that the input is acceptable.
Above all, passing the audit shows that the declared timing is consistent with the policy, not that the Expert Advisor avoided future data. This article is educational, and any strategy should be tested on a demo account before live use.
Conclusion
The library makes the declared timing of a decision's inputs checkable through eight ordered rules. Its examples demonstrate how the selected policy and the first matching rule affect the reported result. In particular, for a 12:35 M5 decision, a completed 11:00–12:00 H1 input passes under the default policy, while the still-forming 12:00–13:00 H1 input fails.
The workflow is to start a decision with BeginDecision(), record its inputs with AddDependency(), run Audit(), and inspect both the per-dependency results and GetOverallStatus(). Check that Audit() returned a positive count before accepting an overall PASS. Treat WARNING as unresolved timing uncertainty, not approval.
The demo uses fixed timestamps to make its scenarios reproducible, and the test script contains twelve assertions. However, invalid shift and timestamp mismatch do not have isolated tests, so those rule paths are not independently covered by the test suite. Finally, a passing audit means that the recorded metadata is consistent with the selected policy—not that the Expert Advisor is free of look-ahead.
Programs Used in the Article:
| # | File name | File type | Description |
|---|---|---|---|
| 1 | TemporalTypes.mqh | Include file | Status, violation, and decision-mode enumerations and the per-dependency result structure |
| 2 | TemporalConfig.mqh | Include file | STemporalConfig policy structure with its default values |
| 3 | CTemporalDependency.mqh | Include file | Record of one consumed dependency and its temporal metadata |
| 4 | CDependencyTracker.mqh | Include file | Holds the decision context and the dependencies recorded for it |
| 5 | CTemporalValidator.mqh | Include file | The eight ordered temporal validation rules |
| 6 | CTemporalAudit.mqh | Include file | Orchestrator that runs the validator and aggregates the results |
| 7 | CCausalityReport.mqh | Include file | Formats audit results and the experiment table for the Experts log |
| 8 | TemporalAuditDemo.mq5 | Expert Advisor | Five deterministic scenarios plus the supplementary intrabar case |
| 9 | TemporalAuditTest.mq5 | Script | Twelve deterministic tests that assert both status and violation type |
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.