Appearance

Personalize your experience

Mode
Accent Color
Layout
Direction
All articles

Building a Market Data Pipeline That Survives the Day

Websocket ingestion, tick-to-candle aggregation, reconnect handling and the gap-detection layer that stops a strategy trading on data it never received.

QuaTick Engineering13 min read2,433 words
PostLinkedIn
On this page

A strategy is only as good as the prices it reads. Most retail systems get this backwards: enormous care goes into the signal logic, and the data feeding it is a websocket callback that appends to a list. That works until the socket drops during a volatile minute, at which point the strategy carries on computing indicators over stale numbers and has no idea anything is wrong.

The shape of a market data pipeline

System architecture

Four responsibilities, deliberately kept apart

Separating connection, buffering, aggregation and strategy is what allows aggregation to be tested without a live socket and a session to be replayed offline.

The broker websocket delivers ticks on a network thread and its callback only enqueues onto a bounded queue before returning. A consumer reads the queue and feeds both a tick aggregator that builds bars and flags gaps, and a raw tick store kept for replay and forensics. The connection layer also publishes a feed health state. The strategy loop consumes bars and reads that health state to gate new entries.
The health state being published rather than merely logged is what lets the strategy refuse to trade. A log line cannot be read by the code that needs it.

The useful separation is between the part that talks to the broker, the part that turns a tick stream into bars, and the part that runs strategy logic. Keeping those distinct is what allows you to test aggregation without a live socket and to replay a session offline.

Each boundary in that chain is also a place to record health. The connection layer knows about reconnects, the aggregation layer knows about gaps, and the strategy layer knows whether the bar it just received is trustworthy. Collapsing these into one module is what makes "why did it trade there?" unanswerable.

From tick to candle

Process flow

Aggregation driven by the boundary-crossing tick

A bar is emitted when the first tick of the next interval arrives, not when a wall-clock timer fires. That choice removes a race that is hard to reproduce.

A tick arrives with price, quantity and timestamp. Its interval bucket is computed as the timestamp minus the timestamp modulo the interval. If the bucket matches the open bar, high, low, close and volume are updated. If the bucket is later, the open bar is emitted exactly once. When more than one interval has elapsed the emitted bar is flagged as having a gap. The bar reaches the strategy carrying its tick count and gap flag.
The gap flag is what separates a market with no trades from a feed that was not being read. Without it those two states are indistinguishable downstream.

Aggregation looks trivial and contains most of the subtle bugs in a retail data stack. The specification worth writing down before coding: a bar covers a half-open interval, it is emitted only once, and it is emitted when the first tick of the next interval arrives rather than when a wall-clock timer fires.

data/aggregator.pypython
from dataclasses import dataclass, field  @dataclassclass Bar:    start: int          # epoch seconds, interval start    open: float    high: float    low: float    close: float    volume: int = 0    tick_count: int = 0    had_gap: bool = False  class CandleAggregator:    """Emits a bar when the first tick of the next interval arrives."""     def __init__(self, interval_seconds: int):        self.interval = interval_seconds        self._current: Bar | None = None     def _bucket(self, ts: int) -> int:        return ts - (ts % self.interval)     def on_tick(self, ts: int, price: float, qty: int = 0) -> Bar | None:        bucket = self._bucket(ts)        current = self._current         if current is None:            self._current = Bar(bucket, price, price, price, price, qty, 1)            return None         if bucket == current.start:            current.high = max(current.high, price)            current.low = min(current.low, price)            current.close = price            current.volume += qty            current.tick_count += 1            return None         if bucket < current.start:            # Late tick for an already-closed interval. Do not silently fold            # it in; that rewrites history a strategy may have acted on.            raise OutOfOrderTick(ts, current.start)         completed = current        # More than one interval elapsed: the feed had no ticks in between.        completed.had_gap = bucket > current.start + self.interval        self._current = Bar(bucket, price, price, price, price, qty, 1)        return completed  class OutOfOrderTick(Exception):    pass
Interval-boundary aggregation with provenance. The tick_count and had_gap fields are what let a strategy decide whether to trust the bar.

Two decisions in that code are deliberate and worth defending. Raising on an out-of-order tick rather than absorbing it means a clock or sequencing problem surfaces immediately instead of quietly corrupting bars. And flagging had_gap when more than one interval elapses distinguishes "no trades happened" from "we were not listening" — which is exactly the distinction a strategy needs and almost never has.

What happens when the socket drops

Timeline

The reconnect sequence, and when trading resumes

Entries stop at the second step and do not resume until reconciliation succeeds. Exits stay available throughout.

A watchdog detects silence rather than waiting for a socket close event. The feed is immediately marked degraded and new entries are blocked. Reconnection uses exponential backoff with jitter. Subscriptions are re-established explicitly because they rarely survive a reconnect. Missing data is backfilled and reconciled against existing bars rather than appended. Only after reconciliation succeeds is the feed marked healthy and entries permitted again.
Elapsed times are illustrative. What matters is the ordering: healthy status is the last step, never an optimistic assumption made at reconnect.

Assume the connection will drop several times a session — on broker maintenance, on a network blip, on your own process being slow to read. The question is not whether you handle it but what state the system is in while it is happening.

The critical rule: a disconnect must propagate to the strategy as a state change, not merely be logged. A strategy that does not know the feed is down will keep evaluating conditions against its last known price, and the outcome depends entirely on luck.

The sequence worth implementing

  1. Detect the drop quickly — a heartbeat or an expected-tick-interval watchdog, not just the socket close event, which can be slow or absent.
  2. Mark the feed degraded and publish that state. New entries should be blocked immediately.
  3. Reconnect with exponential backoff and jitter. A tight reconnect loop from many clients is how a broker ends up rate-limiting you.
  4. Re-subscribe explicitly. Do not assume subscriptions survived the reconnect; many implementations drop them.
  5. Backfill the gap from the REST historical endpoint, and reconcile it against your existing bars rather than appending blindly.
  6. Only after reconciliation succeeds, mark the feed healthy and allow entries again.

The reconciliation step is the one most often skipped. Broker historical bars and your locally aggregated bars will not always agree, because they may use different tick filtering or different interval conventions. Discovering that mismatch during a quiet reconnect is much better than discovering it from a strategy that acted on a spliced series.

Detecting staleness before the strategy acts

A price with no timestamp attached is not usable for a trading decision. The cheapest high-value control in the whole pipeline is a freshness assertion immediately before any order is generated.

data/freshness.pypython
import time # Maximum acceptable silence, per instrument class.MAX_SILENCE_SECONDS = {    "index_future": 5,    "index_option_atm": 10,    "index_option_wing": 60,    "stock_future": 15,}  def is_fresh(last_tick_ts: float, instrument_class: str, now: float | None = None) -> bool:    now = time.time() if now is None else now    limit = MAX_SILENCE_SECONDS.get(instrument_class, 30)    return (now - last_tick_ts) <= limit  def assert_tradeable(book, instrument, instrument_class: str) -> None:    if not book.feed_healthy:        raise FeedDegraded(instrument)    if not is_fresh(book.last_tick_ts(instrument), instrument_class):        raise StaleQuote(instrument, book.last_tick_ts(instrument))  class FeedDegraded(Exception):    pass  class StaleQuote(Exception):    pass
Thresholds have to be per instrument. An illiquid option strike is legitimately quiet in a way an index future is not.

Per-instrument thresholds matter because a single global number cannot be right. Five seconds of silence on an index future means something is broken; sixty seconds on a far out-of-the-money strike is an ordinary Tuesday. A single threshold either produces constant false alarms on the wings or fails to notice a genuinely dead feed on the liquid legs.


Where the pipeline usually breaks

Decision funnel

What has to survive for a tick to reach a decision

Each stage discards some data legitimately. The failure mode is discarding it silently, so the strategy cannot tell a thin stage from a broken one.

Every tick the exchange feed publishes must first be received while the socket is up, since disconnects and re-subscribe gaps lose some. It must then be read before a bounded queue overflows, because slow consumers drop the oldest entries. It must be in order and within session, as out-of-order and stale timestamps are rejected. It is then folded into a bar carrying a tick count and gap flag, and finally must pass a per-instrument staleness check before a strategy may act on it.
Each narrowing is acceptable if it is counted. A pipeline that cannot report how much it dropped at each stage cannot tell you whether today was normal.

Across retail data stacks the same handful of failures recur, and they are almost never in the interesting part of the code.

FailureHow it presentsControl that catches it
Work done in the socket callbackUnexplained disconnects under loadQueue between ingestion and processing
Subscriptions lost on reconnectOne instrument silently stops updatingPer-instrument freshness watchdog
Timezone or epoch-unit mismatchBars offset by hours, or by 1000×Assert bar timestamps fall inside session hours
Missing gap flagIndicators computed across a hole in the datahad_gap propagated to the strategy
Unreconciled backfillA duplicated or double-counted barCompare overlap before splicing
No freshness check pre-orderOrders priced off a frozen quoteFreshness assertion in the order path
Recurring failure modes and the control that catches each

A pipeline you can trust on a bad day

  • The socket callback only enqueues

    No indicator maths, no database writes, no order placement on the network thread.

  • Feed health is a published state, not a log line

    The strategy can read it and must check it before entering.

  • Entries blocked while degraded; exits and cancels still permitted

    Verify both halves separately — this is where a single guard gets it wrong.

  • Bars carry tick count and a gap flag

    A strategy can then refuse to act on a bar built from two ticks.

  • Backfill is reconciled against local bars before use

    Compare the overlapping region and alert on mismatch rather than trusting either side.

  • Session replay from stored ticks reproduces the same bars

    If replay does not reproduce live output, one of them is wrong and you cannot yet tell which.

None of this is exotic engineering. It is a queue, a state flag, a timestamp check and a reconciliation step. What it buys is the ability to say, after an unexpected trade, whether the data was sound at that moment — and that answer is the difference between fixing a strategy and guessing at it.

Frequently asked questions

Why should a websocket callback only put ticks on a queue?

Broker clients deliver ticks on a network thread, so doing real work there — computing indicators, writing to a database, placing orders — blocks the socket. When the client stops reading fast enough the broker eventually disconnects it. Enqueue and return, then process on a consumer thread. This alone resolves a large share of otherwise mysterious disconnections.

Should candles be closed by a timer or by the next tick?

By the first tick of the next interval. A wall-clock timer races with tick processing, so a tick can land in the wrong bar when the timer fires first, producing bars that differ subtly from the broker's own only under load. Use a timer solely as a fallback to close a bar in an instrument that has genuinely stopped trading, and mark such bars distinctly.

How should a strategy behave when the market data feed drops?

The disconnect should propagate as a published state change, not just a log entry. New entries are blocked immediately, while exits and cancellations continue to work on last known state, since a degraded feed is exactly when flattening may be necessary. Entries resume only after reconnect, explicit re-subscription and successful reconciliation of backfilled data.

How can a system tell a quiet market from a dead feed?

By storing timestamps and gap information alongside prices, then applying per-instrument silence thresholds. Five seconds without a tick on an index future indicates a problem, whereas sixty seconds on a far out-of-the-money strike is normal. Bars should also carry a tick count and a gap flag so a strategy can refuse to act on a bar built from almost no data.

Why does backfilled data need reconciliation?

Broker historical bars and locally aggregated bars can disagree because of different tick filtering or interval conventions. Appending backfill blindly can duplicate or double-count a bar. Comparing the overlapping region and alerting on mismatch surfaces the discrepancy during a quiet reconnect rather than through a strategy acting on a spliced series.

Is it worth storing raw ticks as well as candles?

Yes. Raw ticks with arrival timestamps allow any interval to be rebuilt, a bug to be reproduced exactly, and the reasonableness of a fill to be assessed after the fact. Candles alone discard the information most needed when something unexpected has happened, and tick storage is inexpensive relative to one unexplainable trading day.

Primary sources

QuaTick Engineering

Platform & execution

The engineering team builds QuaTick’s order routing, market-data pipeline and broker integrations. These pieces cover the parts of automation that only show up in production.

A Python Algo Trading Stack That Survives Production

How to structure a Python algo trading system for Indian markets: process boundaries, idempotent order handling, tick storage, reconciliation and the monitoring that makes failures visible.

QuaTick Engineering11 min read

Choosing a Broker API for Algo Trading in India

What to evaluate in an Indian broker API before you build against it: rate limits, websocket subscription caps, order-type coverage, historical data access and failure behaviour.

QuaTick Engineering9 min read

Slippage and Impact Cost on NSE

How slippage behaves on Indian instruments across the trading day, why averages are the wrong model, and a practical way to estimate impact cost before sizing an order.

QuaTick Engineering10 min read

Discussion

Sign in to join the discussion. Your posts and replies show up on your profile.