MT5 stale quotes: check price-feed freshness before using a price

6 min readMetaKit

A successful quote request proves that your application received a response. It does not prove that the broker supplied a new price recently. A dashboard can fetch the same last-known bid and ask repeatedly while displaying a reassuring green connection badge.

MT5 stale quote detection starts by separating three measurements: request duration, the age of the underlying quote and the time since your application last observed a new quote timestamp. They describe different failure modes. This guide builds an application-level check around MetaKit's quote endpoint; it does not promise that a price passing the check will still be executable when an order arrives.

Ask for the timestamp, not just bid and ask

MetaKit exposes GET /v1/accounts/{id}/quote?symbol=EURUSD. The current handler returns symbol, bid, ask, last and time. It obtains that value from the terminal's last tick, rather than stamping the response with the API request time. The current response does not expose time_msc or a broker-to-client latency measurement.

That distinction matches MetaQuotes' description of symbol_info_tick: it reads the most recent tick for a symbol. The native structure includes both seconds and milliseconds, but an integration cannot assume every field survives a wrapper's public API.

Use the broker's exact symbol name. EURUSD, EURUSD.pro and a similarly named synthetic instrument are not interchangeable identifiers. Resolve it with the account's symbol list and the symbol mapping guide. Read the endpoint's full contract in the MetaKit API reference.

Measure the clocks separately

MeasurementCalculationWhat it tells you
Request durationMonotonic finish minus monotonic startTime spent waiting for this response
Apparent quote ageReceive time minus validated quote timeAge relative to your wall clock
Observed stagnationMonotonic now minus last timestamp advanceHow long this client has seen no progress

Use a monotonic clock for elapsed durations so a wall-clock adjustment does not create negative request times. Use a synchronized wall clock for comparison with a genuine timestamp. Keep both values: a 40-millisecond response can contain an hour-old quote, and a one-second response can contain a recently updated quote.

There is also a timestamp interpretation prerequisite. MetaKit's current terminal adapter formats the numeric tick time as an ISO string interpreted in UTC; it does not discover and correct a broker-specific clock offset. Validate that interpretation on your actual broker/feed before setting a tight age threshold. Do not subtract an assumed two- or three-hour offset simply because a chart uses server time.

MetaQuotes' tick range documentation documents UTC handling for that native API. If observed data disagrees with a trusted reference, investigate the feed and adapter rather than treating the discrepancy as measured network latency. Preserve the raw timestamp in your diagnostic record.

A small freshness classifier

This pure JavaScript function checks a quote after retrieval. Its default limits are illustrative application settings, not MetaKit guarantees or universal forex thresholds. It assumes that the timestamp's UTC interpretation has already been validated.

function classifyQuote(q, receivedMs, {
  maxAgeMs = 5000,
  futureToleranceMs = 1000,
} = {}) {
  if (!q || typeof q.time !== "string" ||
      !/(Z|[+-]\d{2}:\d{2})$/.test(q.time)) {
    return { usable: false, reason: "missing_timezone" };
  }
  if (![q.bid, q.ask, receivedMs, maxAgeMs, futureToleranceMs]
      .every(Number.isFinite) || maxAgeMs < 0 || futureToleranceMs < 0) {
    return { usable: false, reason: "invalid_numbers" };
  }
  if (q.bid <= 0 || q.ask <= 0 || q.ask < q.bid) {
    return { usable: false, reason: "invalid_market" };
  }
  const stamp = Date.parse(q.time);
  if (!Number.isFinite(stamp)) {
    return { usable: false, reason: "invalid_timestamp" };
  }
  const ageMs = receivedMs - stamp;
  if (ageMs < -futureToleranceMs) {
    return { usable: false, reason: "clock_disagreement", ageMs };
  }
  return {
    usable: ageMs <= maxAgeMs,
    reason: ageMs <= maxAgeMs ? "within_age_budget" : "stale",
    ageMs,
  };
}

With a receive time of 2026-10-18T09:00:06Z and a quote timestamp of 09:00:00Z on that date, apparent age is 6,000 milliseconds. The default five-second budget rejects it. A quote dated two seconds in the future is rejected for clock disagreement, rather than accepted as exceptionally fresh. These are synthetic examples; the date is not a claim that a particular market was trading then.

The classifier permits a small negative age within the clock tolerance and reports the signed value. It rejects null prices, crossed bid/ask values and missing timezone information. Tests should also cover the exact age boundary, invalid dates and zero bids. The current endpoint has second precision, so this is not a tool for certifying subsecond freshness.

Treat silence according to the instrument and session

A timestamp can remain unchanged during a closed market, a trading halt, a thin session or a feed failure. The timestamp alone cannot distinguish those causes. Compare it with the instrument's actual trading schedule and broker notices. A generic “forex is open” flag is insufficient for CFDs with different sessions or holiday hours.

Price equality is not a substitute for timestamp progress. The market may produce updates with unchanged bid and ask. Conversely, a client that only polls once a minute cannot prove what happened between observations. Record timestamp regressions separately from unchanged values, and do not present a polling history as an exchange event log.

For monitoring, keep an “unknown” state until you have enough observations. After a restart, one old quote does not tell you how long the local observer has been stalled. It may still fail the absolute-age policy, but observed stagnation needs its own fresh baseline.

Respond without hiding the problem

When a quote is stale, show the last verified timestamp and mark the value unavailable for decisions requiring freshness. Do not relabel the retrieval time as the market time. A historical chart can display the last close with an explicit date; a trade sizing preview should avoid silently describing that price as current.

For an application preparing orders, re-read close to the decision and enforce your own freshness budget before handing the request to the execution path. A passed check is only an input validation step. Price movement, liquidity and broker execution still matter; the slippage guide explains why an expected price and an eventual fill differ.

The classifier was exercised with synthetic quotes, not a live broker feed. To validate deployment, log local request timings alongside raw quote timestamps, confirm clock interpretation on that account, and compare normal open-session behavior with a known closed session. Set thresholds from that evidence and the application's tolerance for stale data. Consult the API documentation for the endpoint, and keep collection diagnostics separate from investment decisions.

Sources and verification

Verified 18 October 2026. Product fields were checked against the quote route and terminal adapter. No live execution or latency benchmark was performed.