A backtest can complete without errors while using duplicated candles, incomplete history or a bar whose high is below its close. The result may look plausible because the calculation engine cannot tell whether the input was collected correctly. Data validation needs its own stage before indicators or strategy rules run.
This guide audits MT5 candle data quality at the record and sequence level. It complements the history retrieval guide, which explains how to obtain candles. The goal here is to decide whether a collected dataset is fit for a stated use, while preserving evidence of anything unresolved.
Start with the data contract
MetaQuotes defines each native bar through MqlRates: opening time, OHLC prices, volumes and spread. A bar timestamp describes the beginning of its period, not the moment your HTTP client received it.
MetaKit's /v1/accounts/{id}/candles returns those price fields with time, tickVolume, realVolume and spread. The envelope includes count and truncated. The current response cap is 5,000 candles. truncated: false means the API did not apply that cap; it is not a guarantee that the broker supplied every observation your research requires.
Native history is also constrained by what the terminal has available. MetaQuotes' copy_rates_range documentation discusses available chart history and the “Max. bars in chart” setting. A small successful response can therefore need investigation even when no transport error occurred.
Keep request parameters, account/server identity, exact symbol, retrieval time and the raw response with your audit result. If the broker later changes a historical bar, that provenance helps distinguish a source revision from a collector bug.
Check records before checking the timeline
For an ordinary positive-price forex dataset, require finite numeric OHLC values. Verify that high is at least the maximum of open, low and close, and low is at most the minimum of open, high and close. Reject strings that merely look numeric unless ingestion explicitly converts and validates them.
These constraints are deliberately specific to the stated data domain. Do not reuse a positive-price-only validator for every asset class without reviewing its assumptions. Similarly, high equal to low is not automatically an invalid bar, and zero reported real volume is not automatically missing history. The volume-field guide explains why.
Validate timestamps as instants with explicit timezone information. Preserve any normalization applied to the source time. An ISO-looking suffix is not a reason to subtract a broker offset guessed from a different account. Compare a known boundary on the actual feed when time interpretation is uncertain.
An audit that reports rather than repairs
This JavaScript example checks fixed-duration bars such as M1 or H1. It leaves the input unchanged and reports row indexes for investigation. It does not know the instrument's holiday calendar, so a large time gap is a review item, not proof of an error.
function auditCandles(rows, stepMs) {
if (!Array.isArray(rows) || !Number.isSafeInteger(stepMs) || stepMs <= 0) {
throw new Error("Expected rows and a positive fixed bar interval");
}
const issues = [];
const seen = new Map();
let previous = null;
if (!rows.length) issues.push({ code: "empty" });
rows.forEach((bar, index) => {
if (!bar || typeof bar !== "object") {
issues.push({ index, code: "invalid_row" });
return;
}
const prices = [bar.open, bar.high, bar.low, bar.close];
if (!prices.every(x => Number.isFinite(x) && x > 0) ||
bar.high < Math.max(bar.open, bar.low, bar.close) ||
bar.low > Math.min(bar.open, bar.high, bar.close)) {
issues.push({ index, code: "invalid_ohlc" });
}
const time = typeof bar.time === "string" &&
/(Z|[+-]\d{2}:\d{2})$/.test(bar.time) ? Date.parse(bar.time) : NaN;
if (!Number.isFinite(time)) {
issues.push({ index, code: "invalid_time" });
return;
}
const signature = JSON.stringify(prices);
if (seen.has(time)) {
issues.push({ index, code: seen.get(time) === signature
? "duplicate_ohlc" : "conflicting_ohlc" });
} else seen.set(time, signature);
if (previous !== null) {
const delta = time - previous;
if (delta < 0) issues.push({ index, code: "out_of_order" });
else if (delta > stepMs) issues.push({ index, code: "review_gap", delta });
if (delta > 0 && delta % stepMs !== 0) {
issues.push({ index, code: "irregular_interval", delta });
}
}
previous = time;
});
return issues;
}The duplicate comparison concerns OHLC only. Two rows with identical prices but different volume or spread still need a separate full-record comparison if those fields enter your model. The function also checks elapsed spacing, not whether every timestamp lies on your intended session grid; calendar and alignment checks remain separate.
A synthetic H1 sequence at 09:00, 10:00 and 12:00 produces a gap review at the last row. Repeating the 10:00 row produces a duplicate; changing its close produces a conflict, and may independently violate OHLC bounds. Those cases, reversed ordering, malformed timestamps and invalid prices were exercised with fixtures.
Investigate a gap before filling it
A weekend, holiday, session break or absence of quotes can explain an interval without a bar. A truncated response, incomplete fetch window or available-history limit can explain another. The elapsed gap alone cannot tell you which happened.
Compare the requested range with the first and last returned bars. Re-fetch a small settled interval, compare the terminal's available history and consult the instrument's actual schedule. Record a gap classification together with the evidence. “Unknown” is a useful state when the source cannot establish coverage.
Do not insert flat candles by default. An invented open/high/low/close changes volatility, return alignment and event timing. A strategy that trades only on observed bars may need a different policy from a reporting grid that intentionally displays empty intervals. Make that policy part of the dataset version.
For weekly or monthly bars, a fixed millisecond step is the wrong calendar model. Months differ in length and trading boundaries may depend on the instrument. Use a calendar-aware rule instead of passing a nominal thirty-day duration to the example.
Treat revisions and live bars differently
The current MetaKit latest-bars path can include a candle that is still forming. Its high, low, close and volume may legitimately change between reads. Separate that from a settled historical bar changing later. Freeze your research dataset at a documented cutoff and exclude unfinished bars where the method requires final values.
Keep original and revised records if you need reproducibility. Silently overwriting a conflicting timestamp makes it difficult to explain why yesterday's backtest differs from today's. An audit can identify the change without deciding which record is authoritative; that decision belongs to the collection policy and source evidence.
Price bounds passing does not prove a quote is economically correct. A bad spike can still form internally consistent OHLC. Use an outlier flag based on a declared rule, then investigate; do not delete every large move because it is inconvenient for a backtest.
Connect validation to downstream checks
Store issue counts, coverage notes and dataset identity beside the output. Stop a research job on structurally impossible prices; require review for unexplained gaps or revisions. Different uses can have different acceptance criteria, but they should consume the same recorded evidence.
The correlation guide shows how missing endpoints can corrupt paired returns. The Strategy Tester guide explains why realistic execution assumptions matter after input validation. Neither a clean audit nor a successful simulation establishes a profitable strategy.
Verify current endpoint fields in the MetaKit documentation. The example is a local audit of supplied records, not a live broker history certification or a data-repair service.
Sources and verification
Verified 20 October 2026 against MetaKit's candle handler and serializer. Tests used synthetic records only.
- MetaQuotes MqlRates, bar fields and opening time.
- MetaQuotes copy_rates_range, retrieval and available-history limits.
- MetaKit API reference, candle endpoint and response envelope.