When to Use
Use this skill when you hold a capture of market data in which each event carries both a venue-stamped send time and a locally stamped receive time, and the two clocks run at measurably different rates. Uncorrected skew shows up as a delay series that trends across the session: the same event appears to take longer and longer to arrive, when in fact the local clock is losing or gaining time. Typical consumers:
- Merging captures from several hosts into one ordered event stream for order-book reconstruction or backtesting.
- Latency analysis where a drifting recorder would otherwise produce a fake trend in tick-to-trade or feed-arrival measurements.
- Re-stamping an archive taken while a host was running on plain NTP, or before PTP was deployed.
ClockSkewCorrector fits delay ≈ alpha + beta·t to the minimum delay in each time
window, then subtracts the fitted line from the local timestamps and forces the output
to increase strictly.
When NOT to Use
- To satisfy a clock-synchronisation obligation. Under MiFID II RTS 25 the
timestamps a firm records for reportable events must come from a clock synchronised
and traceable to UTC — 100 µs maximum divergence and 1 µs granularity for
high-frequency algorithmic trading. That is achieved by disciplining the clock
(
clock-synchronization-ptp-for-trading-hosts), not by regressing timestamps after the fact. Statistically corrected timestamps are a research artifact; never file them or present them as an RTS 25 record. Seereferences/standards.md. - To measure one-way latency after removing the constant term. From one-way data the
minimum transit delay and the constant clock offset are not separable — they enter
the measurement only as a sum. The default correction removes both, so
corrected − exchange_tsis excess delay above the session minimum, not latency. Useremove_constant_offset=Falsefor latency work, which removes drift only. - When the host is already PTP-disciplined to sub-microsecond accuracy. Fitting
skew to a hardware-disciplined clock mostly fits your own measurement noise. Monitor
it instead (
clock-drift-monitoring-alerting-thresholds). - On ticks merged from multiple venues or multiple capture hosts. Skew is a property of one clock pair. One line fitted across a mixed series describes no real pair of clocks. Split by (venue, host) and fit each separately; the implementation rejects input that is not ordered by exchange time, which catches the common form of this mistake.
- Across a clock step. NTP slews small offsets but steps the clock once the offset exceeds 128 ms by default, and a leap second is a 1 s step. A single straight line fitted across a step is wrong on both sides of it — split the series at the step.
- When the venue timestamp is coarser than the drift you are chasing. A venue that stamps to the millisecond cannot evidence tens of microseconds of drift over a short window; you need a much longer span or a better source timestamp.
Prerequisites
- Paired
exchange_tsandlocal_tsper event, both from the same venue/host pair, sorted by exchange time, in a single unit (s,ms,us,ns). - Timestamps as UTC epoch counts, never local wall-clock — a DST transition in a
wall-clock series is a one-hour "skew" that this model will happily fit. See
daylight-saving-time-transition-handling. - For tick-resolution work, int64 nanoseconds with
time_unit="ns". Float64 POSIX seconds have a ULP of ~238 ns near 1.7e9 and physically cannot hold two events 100 ns apart, whatever the correction does. - Enough events per window to make a minimum meaningful — the default
min_points_per_window=10drops sparser windows rather than trusting them. - NumPy. No other runtime dependency.
Workflow
- Split the series by clock pair. One fit per (venue, capture host). Sort by
exchange timestamp;
fitraises rather than silently returning zero drift if you do not. - Compute one-way delays
local_ts − exchange_ts, differencing in the input's integer unit before scaling to seconds so the difference is not swallowed by float rounding at epoch magnitudes. - Take the minimum per window, not the mean or median. Queueing delay is one-sided and strictly positive, so any central statistic is biased upward by congestion and moves with it. Anchor each retained point at the exchange time of the sample that achieved the minimum, not at the window's start edge.
- Drop windows with too few samples. The minimum of k draws falls as k grows, so a window holding one tick contributes a raw jitter draw as though it were a lower bound. Under a U-shaped intraday volume profile that converts tick density into fake skew.
- Fit the line and then interrogate it before believing it. Check
diagnostics:reliableFalse means the fit fell back to zero drift, not that the clock is good. A lowr_squared, a largeresidual_std_sec, or a step warning in the log means the lower envelope is not a straight line. (A step mid-series inflates the residual spread too, sofitdetects steps from jumps between consecutive window minima rather than from residual outliers.) A drift beyondmax_drift_ppm(default 1000 ppm) raises — the Unix kernel discipline NTP relies on slews at most 500 ppm, so a larger sustained rate is a data problem: mixed units, mixed hosts, or a step inside the window. - Decide what to subtract. Default (
remove_constant_offset=True) puts the output on the venue's timescale and origin. For latency analysis passFalseto remove drift only and keep delays positive. - Apply, then enforce monotonicity.
T_final,i = max(T_corr,i, T_final,i−1 + ε). ε is clamped up to one output tick in integer mode; in float mode the guarantee is still enforced, falling back to one-ULP separation with a warning when the epsilon is below what the magnitude can represent. - Keep the model causal in production.
fitrecords its first exchange timestamp as the reference epoch andtransformmeasures against that epoch, so calibrating on a warm-up window and applying to later ticks is correct.fit_transformis in-sample by construction — offline research only.
Full procedure: see
references/workflows.md. Standards and evidence: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Regressing on all delay points. Network queueing is positive-only. Ordinary least squares over every sample is biased upward by the mean queueing delay and reads a congestion episode as clock drift. Filter to window minima first.
- Re-basing the correction on each batch. If
transformmeasures elapsed time from its own first tick rather than the epoch used atfittime, the intercept is applied at the wrong origin. At 100 ppm, a 300 s gap between calibration and application is a silent 30 ms error — larger than most of the effects being studied. - Calling the intercept a clock offset. It is
minimum_transit + clock_offset, and the two are not separable from one-way data. Reporting it as an offset overstates the clock error by the whole propagation delay. - Trusting a "strict monotonicity" guarantee in float64 seconds. Adding a 1 ns epsilon to a POSIX-epoch float64 is a no-op — the ULP there is ~238 ns — so a naive implementation emits equal timestamps while claiming they increase. Use int64 nanoseconds.
- Fitting across a clock step. A 128 ms NTP step or a leap second is a discontinuity, not drift; one line across it is wrong everywhere. Detect and segment.
- Treating a sustained latency regime change as clock skew. If every sample in a
run of windows is delayed — a route change, a switch upgrade, a saturated link — the
lower envelope itself moves, and one-way data cannot distinguish that from the clock.
Minimum filtering only rejects congestion that leaves quiet samples in each window.
Corroborate large fitted drifts against
clock-drift-monitoring-alerting-thresholdstelemetry before believing them. - Extrapolating a stale fit. Crystal drift changes with temperature. Applying a morning calibration to an afternoon session accumulates error; the implementation warns when asked to extrapolate beyond its calibration span.
- In-sample correction leaking into a backtest.
fit_transformover a whole day uses future ticks to stamp earlier ones. In any research pipeline that feeds a strategy, calibrate on a prior window only. Seelookahead-bias-elimination. - Silently accepting unsorted input. Shuffled exchange timestamps used to collapse the windowing and return zero drift with no error at all, which looks exactly like a healthy clock.
Verification
python -m unittest discover -s skills/clock-skew-correction-for-tick-timestamps/scripts(30 tests). Coverage includes: recovery of a known injected drift rate (positive, negative and zero) to within 5 ppm; rejection of intermittent congestion that would push naive OLS past 100 ppm; the reference-epoch regression (chunkedtransformmust equal a single call); strict monotonicity with an epsilon below the float64 ULP; int64-nanosecond mode preserving 100 ns separation; and rejection of unsorted, NaN, empty and mismatched input.- Generate a synthetic feed with a known drift (e.g. 100 µs/s) plus strictly positive
jitter, then confirm
drift_ppmmatches the injected rate within a few ppm andnp.all(np.diff(out) > 0)holds. - Confirm scale: a 2M-tick session fits and transforms in well under a second. If it takes tens of seconds, the windowing has degraded to a per-window scan of the whole array.
- Cross-check any material fitted drift against independent clock telemetry
(
chronyc tracking,pmc/ptp4loffsets). A regression on market data alone cannot tell a drifting clock from a drifting network path.