Your strategy shows a Sharpe of 1.8 in backtest and 0.4 in production. Before you blame your alpha model, check your data. In our experience, dirty market data accounts for 30–60% of the gap between backtest and live performance — and almost nobody measures it. The rest of this article is a structured dissection of why that number is not an exaggeration.
The pattern is so consistent it has a shape: a clean-looking backtest, a confident pitch deck, a real allocation, and then a month-three meeting where nobody can explain where the alpha went. The first reflex is always to re-tune the model. The second is to widen the universe. The third — eventually — is to look at the data. By then the capital has already been lost.
Most of the alpha you lost was never there.
This article is for the engineers, quants, and risk managers who have lived that month. The premise is concrete: most of the alpha you lost was never there. It was data-quality artifact dressed up as edge. The taxonomy below names the eight mechanisms. The worked example shows the magnitude. The pipeline section gives you the structure to catch the next one before it costs anything.
The raw Sharpe of 1.61 is an artifact of dirty data, not alpha. Each cleaning step reduces apparent performance. The final number is lower — but it is real.
What "dirty data" actually means.
Market data problems are not random noise. They are structured, recurring, and systematic — which means they bias results in a consistent direction. Below is a working taxonomy of the eight failure modes every backtesting engineer should internalise.
| # | Issue type | Definition | Common origin | Impact on backtest |
|---|---|---|---|---|
| 1 | Missing ticks | Gap in time series with no observation | Illiquid names, pre-market sessions | Understated volatility, missed signals |
| 2 | Duplicate ticks | Same timestamp, same or slightly different price | Raw FIX feed artifacts | Inflated volume, noise in signal construction |
| 3 | Stale prices | Price unchanged for N+ consecutive periods | Illiquid bonds, ETFs at open | Artificial mean-reversion — strategy "sees" autocorrelation that doesn't exist |
| 4 | Price spikes | Single tick 3σ+ from surrounding prices | Vendor error, fat finger | False signal trigger; strategy enters on a phantom move |
| 5 | Timezone errors | Timestamps in wrong or mixed timezone | Cross-market data joins | Look-ahead bias — future information bleeds into signals |
| 6 | Corporate action gaps | Split/dividend not in adjusted series | Vendor lag, manual processing | Phantom P&L; a post-split stock ranks as a 50% overnight winner |
| 7 | Out-of-sequence ticks | Non-monotonic timestamps | Network jitter in raw feeds | Causal ordering errors in order-of-operations logic |
| 8 | Survivorship bias | Universe excludes delisted names | Point-in-time dataset failure | Systematic return inflation — the graveyard of failed companies is missing |
One concrete example per signal family:
- Momentum. A missing bar during a breakout suppresses volatility estimates, leading the ranking model to oversize a position — the actual signal is understated, but the backtest doesn't know that.
- Mean-reversion. Stale prices in illiquid ETFs create sequences of zero returns followed by a "reversion jump." The strategy sees this as a signal; it is actually a data artifact.
- Pairs trading. Timezone misalignment in a cross-listed pair introduces a one-bar lead-lag that doesn't exist in live markets — the "arbitrage" disappears on day one of production.
How bad is it — quantifying the problem.
Universe: US equities, top-500 names by average daily volume. Period: January 2020 – December 2024. Source: generic institutional vendor (anonymised, cross-validated against a secondary source). Frequency: raw tick + 1-minute OHLCV resampled. QC thresholds: spike = 3σ vs. 20-day rolling σ; stale = 5+ consecutive identical prices; gap = any missing bar during exchange hours (09:30–16:00 ET).
Based on industry literature and systematic QC pipeline analysis, typical findings for a liquid US equity dataset:
- Missing tick rate: 0.3–2.1% of expected ticks; rate spikes above 1.5% during high-volatility events (VIX > 30).
- Price spike rate: 0.01–0.05% of ticks exceed the 3σ threshold — low frequency, high impact.
- Stale price sequences: 0.5–3.0% of 1-minute bars in the bottom liquidity quintile have 5+ consecutive identical prices.
- Corporate action mismatches between two independent vendors: 3–8% of events differ on effective date or adjustment factor.
- Survivorship bias: annualised return overstatement of 1.6–4.9 percentage points depending on universe and strategy period.
For a simple 20-day momentum strategy (long-only, equal-weight, daily rebalance), the impact of each uncorrected issue type on the Sharpe ratio is material:
| Data issue | Uncorrected Sharpe | Corrected Sharpe | Δ Sharpe |
|---|---|---|---|
| Missing ticks (forward fill retained) | 1.42 | 1.31 | −0.11 |
| Missing ticks (drop affected bars) | 1.42 | 1.38 | −0.04 |
| Price spikes (unfiltered) | 1.42 | 1.29 | −0.13 |
| Stale prices (undetected) | 1.42 | 1.35 | −0.07 |
| Corporate action errors | 1.42 | 1.18 | −0.24 |
| All combined | 1.42 | 1.09 | −0.33 |
Ranges are calibrated to academic-literature benchmarks including Korajczyk & Sadka (2004), Chordia et al. (2011), and survivorship-bias studies by Brown, Goetzmann, Ibbotson & Ross. Actual degradation is strategy-dependent — HFT strategies are more sensitive to tick-level issues; lower- frequency strategies are disproportionately affected by corporate-action errors and survivorship bias. Corporate action errors and survivorship bias dominate, and that is not a coincidence: both produce systematic upward shifts in apparent returns, not just noise.
How you fill the gaps matters.
This is where the expertise gap between "data cleaning" and "data methodology" becomes decisive. Five methods — radically different risk profiles.
01Forward fill (LOCF).
Default in pandas ffill(). Simple and fast. The problem: LOCF introduces
artificial positive autocorrelation in the filled series — a price held constant for 3
bars looks like a trend signal to any model with a rolling window. Acceptable only for gaps under 3 bars
in highly liquid instruments.
02Linear interpolation.
Smooth and unbiased for slow-moving processes. Critical implementation risk: if the endpoint used to anchor the line is a future observation, you have introduced look-ahead bias — the bar at t+3 is being used to construct t+1. The correct implementation is causal linear only: extrapolate forward from the last observed value using the local drift estimate, with no future data.
03VWAP-based resampling.
Aggregate available ticks within each bar window using volume-weighted average price. Materially superior to LOCF for bar-construction accuracy. Not a gap-filling method: if the ticks are missing, VWAP gives you nothing.
04Kalman filter interpolation.
The state-space formulation treats price as a latent state and observations as noisy measurements.
During gaps, the filter propagates state uncertainty via the prediction step — the gap is handled
naturally without inventing data. pykalman supports masked observations natively, making
implementation straightforward:
import numpy as np
from pykalman import KalmanFilter
def kalman_fill(prices: np.ndarray) -> np.ndarray:
"""
Gap-fill a price series using a Kalman smoother.
Missing values must be np.nan; convert to masked array.
Returns smoothed series with gaps filled from state estimates.
"""
from numpy import ma
masked = ma.masked_invalid(prices)
kf = KalmanFilter(
transition_matrices=[1],
observation_matrices=[1],
initial_state_mean=masked[~masked.mask][0],
initial_state_covariance=1.0,
observation_covariance=1.0, # tune to instrument σ²
transition_covariance=0.01 # process noise: smaller = smoother
)
kf = kf.em(masked, n_iter=10)
smoothed_means, _ = kf.smooth(masked)
filled = prices.copy()
filled[np.isnan(prices)] = smoothed_means.flatten()[np.isnan(prices)]
return filled
Parameter sensitivity is the core risk: underestimating observation noise overfits to data;
underestimating process noise over-smooths through real price moves. Calibrate
observation_covariance from the instrument's realized variance and
transition_covariance from the expected price change per bar.
05Model-based (cross-sectional) imputation.
Train an imputation model on correlated instruments: when AAPL has a gap, use MSFT, QQQ and SPY to impute. Best accuracy for correlated universes, but computationally expensive and introduces model risk into the data layer itself. Justified use cases: option chains (put-call parity constrains missing strikes) and fixed-income curves (Nelson-Siegel fills missing maturities).
| Method | Bias | Variance | Look-ahead risk | Compute | Best use case |
|---|---|---|---|---|---|
| Forward fill | Medium | Low | None | Minimal | Short gaps (<3 bars), liquid names |
| Linear interp | Low | Low | High if naive | Minimal | Slow-moving non-volatile series |
| VWAP resample | Low | Low | None | Low | Bar construction from tick data |
| Kalman filter | Low | Medium | None | Medium | Continuous noisy series |
| ML imputation | Low | High | None | High | Derivatives chains, fixed-income curves |
Five steps to a realistic Sharpe.
Scenario: 20-day momentum strategy, 500 US equities, daily rebalance, equal-weighted, long-only, January 2020 – December 2024.
| Version | Sharpe | Ann. return | Max DD | Hit rate | Turnover |
|---|---|---|---|---|---|
| Raw data (unprocessed) | 1.61 | 18.3% | −14.2% | 54.1% | 4.2× |
| After spike removal | 1.52 | 17.1% | −13.8% | 53.8% | 4.1× |
| After gap handling (LOCF) | 1.48 | 16.7% | −13.5% | 53.4% | 4.2× |
| After gap handling (Kalman) | 1.43 | 16.2% | −14.1% | 53.1% | 4.0× |
| After corporate action fix | 1.31 | 14.9% | −15.3% | 52.6% | 4.1× |
| After survivorship bias fix | 1.19 | 13.4% | −17.1% | 52.0% | 4.3× |
The Kalman treatment correctly increases max drawdown slightly relative to LOCF — because LOCF artificially dampens volatility in gap periods, making drawdowns look shallower than they are. Corporate-action correction has the largest single-step impact (−0.12 Sharpe). Survivorship-bias correction brings the final degradation: the strategy that looked like 1.61 delivers 1.19 on a realistic historical universe.
Each cleaning step reduces apparent performance. The final number is lower — but it is real. The raw Sharpe of 1.61 was an artifact of dirty data, not alpha. The −0.42 swing from top to bottom is not a corner case; a −0.33 to −0.42 degradation is typical for an equity momentum book run on uncleaned vendor data.
Production data validation — four mandatory layers.
A production QC pipeline is not a preprocessing script. It is a continuous validation layer with four independent checks — any single layer alone is insufficient.
- Layer 1 — Schema validation (before storage). Timestamp monotonicity; price > 0, volume ≥ 0; bid ≤ ask for quote data; symbol in known universe. Fast, and catches the most egregious issues before they touch any data store.
- Layer 2 — Statistical validation (per bar). Price deviation:
|p_t − p_{t−1}| / σ_{20d} > θ→ flag. Volume spike:V_t > 5 × ADV_{20d}→ flag. Zero-volume bar during exchange hours → flag. - Layer 3 — Sequence validation (per session). Expected bar count vs. actual; gap detection with configurable tolerance; stale detection for N+ consecutive identical prices.
- Layer 4 — Cross-source validation (daily). Compare the same instrument across two or more independent vendors. Price discrepancy > 0.1% on adjusted close → investigate. Corporate-action date discrepancy → manual review queue.
Here is a minimal Python implementation:
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class DataQualityChecker:
series: pd.Series
expected_freq: str = "1min"
spike_threshold: float = 3.0
stale_n: int = 5
_issues: dict = field(default_factory=dict)
def check_spike(self) -> pd.Index:
returns = self.series.pct_change().dropna()
roll_std = returns.rolling(20).std()
spikes = returns.index[np.abs(returns) > self.spike_threshold * roll_std]
self._issues["spikes"] = len(spikes)
return spikes
def check_gaps(self) -> pd.DataFrame:
full_idx = pd.date_range(
self.series.index[0], self.series.index[-1], freq=self.expected_freq
)
missing = full_idx.difference(self.series.index)
gaps = pd.DataFrame({"timestamp": missing, "size": 1})
self._issues["gaps"] = len(missing)
return gaps
def check_stale(self) -> pd.Index:
diffs = self.series.diff().abs()
stale_mask = diffs.rolling(self.stale_n).sum() == 0
self._issues["stale_bars"] = int(stale_mask.sum())
return self.series.index[stale_mask]
def summary_report(self) -> pd.DataFrame:
_ = self.check_spike()
_ = self.check_gaps()
_ = self.check_stale()
total = len(self.series)
rows = [
{"issue": k, "count": v,
"pct_of_series": round(v / total * 100, 3),
"severity": "HIGH" if v / total > 0.01 else "MEDIUM" if v / total > 0.001 else "LOW"}
for k, v in self._issues.items()
]
return pd.DataFrame(rows)
The vendor problem nobody talks about.
No data vendor has zero data-quality issues. The distinction that matters is not Tier 1 vs. free — it is known failure modes vs. unknown failure modes.
Tier 1 vendors (Bloomberg, Refinitiv) have dedicated data operations and systematic corporate-action processing. They still have issues — particularly in non-US markets, small caps and OTC instruments. The gaps are documented and often correctable. Free or low-cost vendors have systematic gaps that are acceptable for research but categorically not for production capital allocation.
The most dangerous segment: paid vendors with institutional branding but retail-grade data operations. The feed looks credible until a corporate action is wrong — and that error persists in your backtest for five years of history.
At Nyquist, data-quality validation is not a preprocessing script that runs before backtesting — it is a continuous pipeline layer built into the data infrastructure itself. Every tick is validated against schema, statistical and cross-source checks before it reaches the API layer. We log every anomaly, every gap-fill method applied, and every data point that was imputed vs. observed — because when a strategy fails in production, the first question is always "was it the model or the data?" We make sure the answer is never ambiguous.
When a strategy fails in production, the first question is always — was it the model or the data? The point of validation infrastructure is that the answer is never ambiguous.
Key takeaways.
- Dirty data systematically inflates backtest Sharpe by 0.2–0.5 in typical equity momentum strategies — this is measurable, not theoretical.
- The gap between backtest and live performance is more often a data-quality problem than an alpha-decay problem.
- Forward fill is the default but not the best: LOCF introduces artificial autocorrelation that inflates trend signals; the Kalman filter handles gaps without this artifact.
- Corporate-action errors have the largest single-step impact on long-term accuracy — a 0.24 Sharpe degradation in the example above.
- Survivorship bias alone can inflate Sharpe by 15–30% depending on strategy period and universe scope.
- Production validation requires four independent layers — schema, statistical, sequence and cross-source — and any single layer alone is insufficient.
Two caveats keep the numbers honest. Sharpe degradation figures are illustrative and calibrated to academic-literature ranges — actual impact is highly strategy-dependent (HFT vs. daily vs. weekly rebalancing). And every fix carries its own risk: Kalman filters need careful noise calibration, cross-vendor validation is expensive and infeasible for exotic instruments, survivorship-bias correction requires a point-in-time universe not every vendor provides, and ML-based imputation imports model risk directly into the data layer.
pykalman masked-observation smoothing; Nelson-Siegel curve fitting; put-call-parity-constrained option-chain imputation.