EZRate
Sign in

Outlier Detection in Price Feeds: Median Bands, MAD, Z-scores and Jump Guards Compared

Outlier detection in price feeds compared: mean vs median, percentage bands, MAD and robust Z-scores, weighted medians, jump guards and minimum sources.

Ersan Fazli
· Web Developer
Published 28 October 2026, 20:00 · 3 min read
Outlier Detection in Price Feeds
Key takeaways
  • Price feeds built from many quotes must reject fat-finger errors, stale reposts and manipulation without lagging real moves.
  • Median-based methods (percentage band, MAD) are robust; mean-and-standard-deviation methods are easily fooled by the outliers they try to catch.
  • A jump guard and a minimum-source rule handle the cases cross-sectional filters cannot.

When a feed aggregates dozens of independent quotes, a few will always be wrong: a missing zero, a Rial price in a Toman list, an hour-old repost, a deliberate fake. Outlier detection in price feeds decides which quotes to trust. This article compares the main techniques and explains the combination EZRate uses.

★
Short answer
Use the median as the centre, not the mean. Drop quotes outside a band around it — a fixed percentage (EZRate uses about ±1%) or a multiple of the MAD. Combine the survivors with a weighted median. Then add two time-series rules: a jump guard that requires big moves to persist, and a minimum number of sources (five at EZRate) below which the previous value is kept and flagged.

Part of the Iranian Rial exchange rate API guide. Pipeline overview: OTC market data aggregation.

Why the mean fails

python
from statistics import mean, median
quotes = [105_100, 105_200, 105_150, 105_250, 10_520]   # one quote in the wrong unit
print(mean(quotes))    # 86,244 — useless
print(median(quotes))  # 105,150 — unaffected

The mean has a breakdown point of 0%: one bad value can move it arbitrarily. The median has a breakdown point of 50%: it stays correct as long as most quotes are honest.

Method 1: percentage band around the median

python
def band(quotes, pct=0.01):
    m = median(quotes)
    return [q for q in quotes if abs(q / m - 1) <= pct]

Simple, interpretable and well suited to liquid assets whose honest quotes cluster tightly. The band must suit the asset: too tight rejects real dispersion, too wide lets errors through.

Method 2: MAD and robust Z-scores

python
import numpy as np

def mad_filter(quotes, k=3.5):
    x = np.asarray(quotes, float)
    m = np.median(x)
    mad = np.median(np.abs(x - m)) or 1e-9
    z = 0.6745 * (x - m) / mad          # robust z-score
    return x[np.abs(z) <= k].tolist()

The band adapts to the current dispersion: wider when the market is choppy, tighter when it is calm. A common threshold is |z| ≤ 3.5. MAD collapses to zero when most quotes are identical, so set a floor.

Method 3: classic Z-score (and why to avoid it here)

The classic z = (x − mean) / std uses the very statistics that outliers distort. With few quotes, one large error inflates the standard deviation enough to hide itself. Use it only on already-cleaned data.

MethodRobust?Adapts to volatility?Notes
Mean ± k·stdNoYesMasks outliers in small samples
Median ± fixed %YesNoSimple; tune per asset
Median ± k·MADYesYesNeeds a floor when MAD ≈ 0
Trimmed meanPartlyNoDrop top/bottom x%; wastes good data
Dot plot of quotes for one minute with median, ±1% band and MAD band, outliers marked
Which quotes survive each filter

Weighted median

python
def weighted_median(values, weights):
    pairs = sorted(zip(values, weights))
    half, acc = sum(weights) / 2, 0
    for v, w in pairs:
        acc += w
        if acc >= half:
            return v

Weights can reflect source reliability (historical agreement with consensus). Cap individual weights so no single source decides the result.

Time-series guards

  • Jump guard: if the new value differs from the last published one by more than a threshold, publish only after it persists for a few cycles or across enough sources. This stops flash spikes but does delay genuine jumps slightly — a deliberate trade-off.
  • Minimum sources: below N valid sources, do not compute a new value; keep the old one and flag it (low_sources).
  • Time window: only quotes from the last few minutes count; deduplicate edited or reposted messages.

EZRate combines a ±1% median band, a weighted median, a jump guard and a five-source minimum; details on the methodology page. Persian readers: median and outliers in currency pricing.

US Dollar270,680Toman+0.40%
Time-series chart showing a fake spike filtered out by the jump guard
A jump guard stopping a false spike

Whatever the method, log rejected quotes with their reason; reviewing them regularly is the fastest way to tune bands and spot broken sources.

Related live rates: US dollar rate, 18K gold price, Emami coin price.

What is the best outlier filter for price feeds?
Median-based filters — a percentage band or MAD — followed by a weighted median and time-series guards.
Why not use mean and standard deviation?
Outliers distort both, so a large error can hide itself, especially with few quotes.
What is a robust Z-score?
0.6745 × (x − median) / MAD; values above about 3.5 are typically treated as outliers.
Does a jump guard delay real moves?
Slightly, by design; it trades a short delay for protection against false spikes.
Ersan Fazli
I work in software development and programming, with a strong interest in building practical and efficient solutions. My focus is on developing new features, improving system performance, and solving technical challenges using modern technologies. I strive to create reliable, scalable, and high-quality software through continuous learning and the application of modern development practices.
All posts by author →

Comments (0)

Comments are shown after review. Sign-in is needed to send.

Related

Market data
OTC Market Data Aggregation: How EZRate Cleans Unstructured Quotes in Real Time
Market data
Iran Official vs Free Market Rate: Why Standard Forex APIs Get IRR Wrong
API & Developers
USDT to IRT API: Tracking Tether to Toman Rates in Real Time via REST