You might think aggregating A‑Share tick data into 1‑minute OHLCV candles is a simple
GROUP‑BYtask. 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:30candle - Last tick →
09:31candle
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:
- 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.
- 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
Without deduplication, aggregated volume = 200, real market volume = 100.
⚠️ Important misconception: deduplication built only from
timestamp + price + volumecomposite 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
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)
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:
- Parse its native trade timestamp
- Locate the corresponding minute bucket inside buffer
- 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:
- 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)
- When unique identifiers are not available upstream, construct composite deduplication key combining multiple business fields:
dedup_key = (
symbol,
timestamp,
price,
volume
)
📝 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()
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:
- 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.
- 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:
- 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.
-
Periodically prune deduplication collections: sets storing
tick_idor compositededup_keymust not grow infinitely. Run scheduled cleanup to evict out‑of‑window entries to reduce GC overhead and avoid memory leaks. - 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)