DEV Community

kelos
kelos

Posted on

Production‑ready patterns: Aggregate reliable 1‑Minute K‑Lines from A‑Share API Tick Data

You might think aggregating A‑Share tick data into 1‑minute OHLCV candles is a simple GROUP‑BY task. Production reality tells a different story. Let’s walk through out‑of‑order packets, duplicate ticks, buffer design and candle finalization with practical Python examples.

Tags: #quantdev #backend #marketdata #fintech #websocket


Hey folks 👋

If you’ve ever built your own market‑data pipeline for China’s A‑Share market, you’ve probably faced this seemingly easy requirement:

Fetch raw tick‑by‑tick data via an A‑Share API, then compute 1‑minute OHLCV K‑lines (candles) on your backend.

On paper this is trivial.
Group trades by minute → compute open/high/low/close → sum volume. Done ✅

But once you deploy this logic to production, weird things start happening.

Your locally‑generated candles diverge from broker quotes and trusted market‑data platforms. There’s no crash, no stack trace, no obvious error in logs. Yet downstream backtesting, technical indicators and strategy signals quietly become unreliable.

These are silent data bugs, and they’re notoriously hard to diagnose.

After debugging multiple production incidents, I’ve learned that the math of OHLCV aggregation is rarely the problem. The real pain points are edge‑cases most hello‑world tutorials skip:

  • Out‑of‑order trades that cross minute boundaries
  • Duplicate tick messages after WebSocket reconnection
  • The surprisingly tricky question: when should a minute candle be marked as final?

In this post I’ll share real‑world failure scenarios, root‑cause analysis, and battle‑tested engineering patterns you can copy‑paste into your quant pipelines.

🚨 Production failures: two silent data corruption cases

Our first‑version implementation made a classic mistake I keep seeing among junior quant backend engineers.

We used the WebSocket packet arrival timestamp on our server to decide which minute bucket each tick belonged to.

Take these realistic A‑Share intraday trade timestamps:
09:30:59.800, 09:30:59.950, 09:31:00.020

Based on exchange matching time:

  • First two ticks → 09:30 candle
  • Last tick → 09:31 candle

But networks do not guarantee ordered delivery. Your backend can easily receive packets in this jumbled sequence:
09:30:59.950 → 09:31:00.020 → 09:30:59.800

With our naive logic, that delayed tick 09:30:59.800 got misassigned into the 09:31 bucket.
Both adjacent candles ended up with wrong price ranges and corrupted volume values. Everything built on top — backtests, indicators, strategy triggers — lost validity.

The second common failure happens on WebSocket reconnect.
When your connection drops and re‑establishes, many A‑Share API providers re‑transmit a small window of recent tick history.

Without deduplication logic, identical trades get counted multiple times.

Example: a real trade with volume 100 arrives twice as duplicate packets. Your aggregation will output volume 200, double the true market value.

💡 Key insight: silent failures are invisible to your application monitoring. You will only spot them when you compare against authoritative benchmark market data. Debugging can consume hours of engineering time.

🔍 Root‑cause deep dive

Cross‑minute out‑of‑order ticks: trade time ≠ packet receive time

Two different timestamps must never be confused when building market‑data systems:

  1. Trade timestamp inside the tick payload: the exact moment when the trade matched on the exchange. This is your single source‑of‑truth for business grouping.
  2. Server‑side packet arrival timestamp: system time when WebSocket data arrives on your host. This is affected by network jitter and upstream scheduling. Never use this for time‑bucket grouping.

In production it is very normal for an earlier trade’s packet to arrive later than trades belonging to the next minute.

Another subtle gotcha: receiving the first tick of a new minute does not mean all ticks for the previous minute have arrived.
Even after your aggregation switches to the next minute context, late‑arriving ticks from the prior minute can still show up. Blindly dropping these messages creates incomplete candles.

Duplicate ticks can be more destructive than out‑of‑order delivery

Duplicate tick messages are usually triggered by:

  • WebSocket disconnect & reconnect
  • Market‑data subscription restarts
  • Message queue duplicate‑consumption events

Out‑of‑order delivery only misplaces data into the wrong candle bucket. Duplicate ticks directly inflate volume metrics and break data integrity.

Sample duplicate tick payloads:

09:30:12.123  15.20  100
09:30:12.123  15.20  100
Enter fullscreen mode Exit fullscreen mode

Without deduplication, aggregated volume = 200, real market volume = 100.

⚠️ Important misconception: deduplication built only from timestamp + price + volume composite keys cannot be 100% accurate. Real A‑Share markets can produce independent trades that coincidentally share identical timestamp, price and volume. Composite‑key deduplication carries risk of false‑positive filtering for valid trades.

✅ Production‑grade solutions for tick aggregation

1. Bucket candles exclusively using the tick’s native trade timestamp

We enforced a hard internal coding rule: candle minute bucket assignment must only use the trade timestamp embedded inside each tick. Packet arrival time must not participate in grouping logic at all.

Convert raw Unix timestamp to minute‑level bucket key:

minute = tick_timestamp // 60
Enter fullscreen mode Exit fullscreen mode

You can also format timestamps into human‑readable string keys such as 2026‑09‑07 09:30 to map to in‑memory candle objects.

2. Bounded sliding in‑memory buffer for late out‑of‑order ticks

A lot of demo code finalizes the previous candle immediately once minute boundary is detected, then instantiates a new candle:

if tick_minute != current_minute:
    finalize(current_kline)
    current_kline = create_kline(tick)
    current_minute = tick_minute
else:
    update_kline(current_kline, tick)
Enter fullscreen mode Exit fullscreen mode

This works perfectly for controlled demo environments with strictly ordered packets. It introduces dangerous hidden bugs on real‑world networks.

After you have switched aggregation context to a new minute, late ticks for the prior minute may still arrive. Simply discarding these ticks creates permanent data loss.

✅ Our production approach:
Maintain a bounded sliding in‑memory buffer, holding candle instances for only the most recent 3‑5 minutes.

For every incoming tick:

  1. Parse its native trade timestamp
  2. Locate the corresponding minute bucket inside buffer
  3. Update that candle instance

Only ticks whose timestamps fall outside buffer time window get discarded. This design gracefully absorbs brief out‑of‑order delivery caused by regular network jitter.

3. Two‑tier deduplication: prioritize upstream unique identifiers

We apply hierarchical deduplication strategy against duplicate tick records:

  1. If your A‑Share API provides trade‑level unique identifiers (trade‑id, global sequence number), use ID‑based idempotent filtering first. This is the most robust solution:
if tick_id in processed_ticks:
    return
processed_ticks.add(tick_id)
Enter fullscreen mode Exit fullscreen mode
  1. When unique identifiers are not available upstream, construct composite deduplication key combining multiple business fields:
dedup_key = (
    symbol,
    timestamp,
    price,
    volume
)
Enter fullscreen mode Exit fullscreen mode

📝 Documentation note: composite keys reduce duplicate probability but cannot guarantee perfect accuracy. Make sure downstream consumers are aware of this constraint.

4. Decouple pipeline into modular layers — avoid giant god‑functions

For better testability, easier incident debugging and safer iteration, do not squeeze reception, cleansing and aggregation logic inside one huge function. Split workflow into three clear layers:

Layer Core Responsibility
Reception Layer Maintain WebSocket sessions and ingest raw tick payloads from A‑Share API
Cleansing Layer Timestamp validation, idempotent deduplication, filter malformed / abnormal market‑data records
Aggregation Layer Consume cleansed ticks and compute minute‑level OHLCV candle metrics

Minimal WebSocket client demonstration:

import websocket
import json

def on_message(ws, message):
    data = json.loads(message)

    for tick in data.get("data", []):
        process_tick(tick)

ws = websocket.WebSocketApp(
    "wss://api.alltick.co/stock/websocket",
    on_message=on_message
)

ws.run_forever()
Enter fullscreen mode Exit fullscreen mode

Note: this is minimal conceptual sample. Real‑world projects need subscription parameters and field parsing configured strictly according to your market‑data API official documentation.

5. Separate real‑time display and persistent‑storage pipelines; rethink candle finalization

One very common engineering pitfall: finalizing candles purely based on your server’s system clock.

When your system clock hits 09:31:00, calendar time moves to new minute. That does not guarantee all ticks belonging to 09:30 have arrived at your backend.

Our production design:
Add a short grace waiting window. Optionally combine with upstream‑provided sequence numbers to decide when a minute bucket can safely close. Split processing into two independent pipelines:

  1. Real‑time display pipeline: prioritize low‑latency rendering for dashboards. Allow active candle values to be revised within grace window. This is acceptable for live UI.
  2. Persistent‑storage pipeline: trade minor latency for correctness. Wait until grace window expires and confirm no more late ticks are incoming. Complete deduplication and correction before writing finalized candle records into database as authoritative historical data.

⚡ Memory & compute optimizations for multi‑symbol subscription

Once core aggregation logic works, subscribing to dozens or hundreds of stock symbols can lead to uncontrolled memory growth and heavy GC pressure. Here are three production‑proven optimizations:

  1. Bound your candle sliding‑buffer window: never keep candle objects for an entire trading day in memory. Align with A‑Share trading hours, retain only latest 3‑5 minutes. Release expired candle instances; rely on database for historical persistence.
  2. Periodically prune deduplication collections: sets storing tick_id or composite dedup_key must not grow infinitely. Run scheduled cleanup to evict out‑of‑window entries to reduce GC overhead and avoid memory leaks.
  3. Symbol‑aware differentiated processing: optimize update logic for high‑liquidity heavily‑traded stocks, minimize unnecessary object copies. Reuse generic aggregation logic for thinly‑traded symbols and avoid over‑engineering.

📝 Closing thoughts

Generating reliable 1‑minute candles from raw tick data is far more complex than a simple GROUP‑BY.
Your final market‑data quality is determined by small but critical architectural choices:

  • Time‑bucket assignment rules
  • Tolerance for out‑of‑order network packets
  • Idempotent handling for duplicate payloads
  • Carefully‑designed candle finalization semantics

Market data is the foundation for backtesting, technical indicators and live‑trading signals in any quantitative system. Many weird production‑data anomalies are not caused by complex algorithms. They appear because engineers ignored subtle real‑world edge‑cases during early‑stage design. Embedding these rules into architecture upfront significantly improves overall quant‑pipeline stability.

All code snippets in this article are for educational demonstration only. Production deployment needs exception handling, automatic WebSocket reconnection, monitoring, alerting and structured logging. When you are evaluating raw tick‑data providers, you can source tick feeds from AllTick API and apply the aggregation patterns covered in this article for custom post‑processing.

💬 Discussion

Have you run into any counter‑intuitive edge‑case bugs while aggregating ticks from A‑Share market‑data APIs? What’s the most painful market‑data bug you have debugged? Drop a comment below — I’m curious about your war stories!

Top comments (0)