<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: EmilyL</title>
    <description>The latest articles on DEV Community by EmilyL (@kaihang_ho_2ad23569cdb965).</description>
    <link>https://dev.to/kaihang_ho_2ad23569cdb965</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3911216%2F2c99c763-1332-402d-bc8f-f3e5f7b38306.png</url>
      <title>DEV Community: EmilyL</title>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaihang_ho_2ad23569cdb965"/>
    <language>en</language>
    <item>
      <title>Tick Data Integration in Practice: Building a Stable Feed Pipeline</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:49:24 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/tick-data-integration-in-practice-building-a-stable-feed-pipeline-3026</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/tick-data-integration-in-practice-building-a-stable-feed-pipeline-3026</guid>
      <description>&lt;p&gt;When tick data first enters your system, it’s easy to underestimate what’s happening. I’ve seen this firsthand as a finance lecturer mentoring a student quant team. They had a working monitor based on minute bars, and switching to tick data seemed like a small upgrade. Instead, their pipeline buckled under the load. In this post, I’ll break down why tick data behaves differently and how to design a system that can handle its rhythm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Startup Scenario: From Candles to a Firehose
&lt;/h2&gt;

&lt;p&gt;The student team’s tool originally polled minute-level K-lines via REST. It was simple and stable. Then they added a WebSocket feed for real-time tick data. Immediately, the console flooded with timestamps, prices, and volumes. They thought adding a cache would fix the slowdown, but latency only got worse. The root cause? They treated a continuous push stream as if it were a batch of discrete responses. Tick data is an event stream, not a result set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tick Data Breaks Naive Implementations
&lt;/h2&gt;

&lt;p&gt;When tick data becomes part of your core pipeline—real-time monitoring, aggregation, state triggers, or replay—its continuous nature exposes several pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unstable push frequency&lt;/strong&gt;: bursts of ticks can overwhelm a synchronous consumer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering and gaps&lt;/strong&gt;: network jitter can reorder or drop messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low per-message value&lt;/strong&gt;: you must aggregate ticks to extract useful signals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking downstream&lt;/strong&gt;: processing each tick synchronously builds latency fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t just theoretical concerns; they’re the difference between a system that runs fine for a day and one that collapses during market open.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Layered Approach to Consuming Tick Data
&lt;/h2&gt;

&lt;p&gt;The fix is to decouple the flow. In the student project, we introduced three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access layer&lt;/strong&gt;: maintains the WebSocket connection, handles reconnects and heartbeats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffer layer&lt;/strong&gt;: uses an in-memory queue or message broker to smooth traffic spikes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumption layer&lt;/strong&gt;: asynchronously aggregates, computes, and updates state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the access layer code we ended up with, stripped down but realistic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# In a real system, this would typically go into a queue or cache
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | price=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | vol=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subscribe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbols&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US.AAPL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://stream.alltick.co/v1/market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, and you’ll see a continuous stream in the console—a raw visualization of time-series flow. That’s when most developers realize tick data isn’t meant to be read one message at a time; it’s a stream to be processed in aggregate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing Maintenance with Unified Data Formats
&lt;/h2&gt;

&lt;p&gt;Once you move beyond a single market, data format inconsistencies become a real cost. Different venues have different field names, timestamp conventions, and volume units. Writing adapters for each one bloats your access layer and introduces bugs. In several projects, I’ve found it more efficient to use a data provider that normalizes tick data across markets—such as &lt;a href="http:\alltick.co" rel="noopener noreferrer"&gt;ALLTICK API&lt;/a&gt;. That way, the access and logging layers stay clean, and you spend time on strategy logic instead of format wrestling. For small teams, this kind of upfront standardization is a huge long-term win.&lt;/p&gt;

&lt;p&gt;Tick data is simple in concept but ruthless in practice. It rewards systems built for flow and punishes those designed for requests. If you’re about to integrate tick data, start by respecting its rhythm—your architecture will thank you.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3abfy3eislfr1r0q66p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3abfy3eislfr1r0q66p.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
    </item>
    <item>
      <title>Handling Time Boundaries for Hong Kong Stock API Real-Time Data</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:17:37 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/handling-time-boundaries-for-hong-kong-stock-api-real-time-data-3p44</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/handling-time-boundaries-for-hong-kong-stock-api-real-time-data-3p44</guid>
      <description>&lt;p&gt;When our team started working with a Hong Kong stock API for a trading platform, we thought the hard part would be WebSocket reconnection or handling high message rates. It wasn't. The real pain was &lt;strong&gt;time boundaries&lt;/strong&gt; — especially around market open, close, and midnight.&lt;/p&gt;

&lt;p&gt;In this post, I'll share the approach we developed for professional trading systems and fund development teams. It's focused on extensibility and clean separation of concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Naive Time Checks Break at the Edges
&lt;/h2&gt;

&lt;p&gt;Consider this common pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;market_open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right? But at 17:00, &lt;code&gt;market_open&lt;/code&gt; is still &lt;code&gt;True&lt;/code&gt;. You've now labeled after-hours data as live trading data. If your risk engine or strategy consumes this, you have a silent bug.&lt;/p&gt;

&lt;p&gt;The issue is that Hong Kong's trading day has multiple phases, not just “open” and “closed.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Model: Define Sessions Explicitly
&lt;/h2&gt;

&lt;p&gt;We start by splitting the day into discrete sessions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Hong Kong Time&lt;/th&gt;
&lt;th&gt;Programmatic Treatment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-open auction&lt;/td&gt;
&lt;td&gt;09:00–09:30&lt;/td&gt;
&lt;td&gt;Pre-market data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Morning session&lt;/td&gt;
&lt;td&gt;09:30–12:00&lt;/td&gt;
&lt;td&gt;Continuous trading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lunch break&lt;/td&gt;
&lt;td&gt;12:00–13:00&lt;/td&gt;
&lt;td&gt;Non-trading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Afternoon session&lt;/td&gt;
&lt;td&gt;13:00–16:00&lt;/td&gt;
&lt;td&gt;Continuous trading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-close&lt;/td&gt;
&lt;td&gt;After 16:00&lt;/td&gt;
&lt;td&gt;Non-trading&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then we use interval logic instead of a single threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_market_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;morning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;afternoon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;afternoon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes ambiguity at boundaries like 16:00 and 09:30.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timezone Normalization: Always Convert to Asia/Hong_Kong
&lt;/h2&gt;

&lt;p&gt;A typical Hong Kong stock API returns Unix timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00700.HK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;520.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788226200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Unix timestamp is an absolute instant, but it doesn't carry timezone info. If your server is in UTC or Singapore, you must convert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1788226200&lt;/span&gt;
&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia/Hong_Kong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after conversion do we run session logic. This prevents server timezone changes from breaking your market state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Open Data: A Distinct State
&lt;/h2&gt;

&lt;p&gt;Between 09:00 and 09:30, quotes exist, but they are not continuous trading data. We don't mark it as &lt;code&gt;market_open=True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We use a state enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_market_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pre_open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;morning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;afternoon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the UI to show “Pre-open” while strategies consume only &lt;code&gt;morning&lt;/code&gt; and &lt;code&gt;afternoon&lt;/code&gt;. Clean decoupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Date Issues and Trading Calendar
&lt;/h2&gt;

&lt;p&gt;At 1 AM, this check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;will correctly return &lt;code&gt;closed&lt;/code&gt; for session state. But if you also need to know the current &lt;strong&gt;trading day&lt;/strong&gt;, comparing only &lt;code&gt;time&lt;/code&gt; is insufficient. You need the date.&lt;/p&gt;

&lt;p&gt;Our rule: &lt;strong&gt;session state uses &lt;code&gt;datetime.time&lt;/code&gt;; trading day uses &lt;code&gt;datetime.date&lt;/code&gt;.&lt;/strong&gt; Never mix them in one variable.&lt;/p&gt;

&lt;p&gt;Also, Hong Kong has public holidays. Don't assume weekdays are trading days. Maintain a separate trading calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  K-Line Aggregation: Strict Boundaries
&lt;/h2&gt;

&lt;p&gt;When you build 1-minute candles from tick data, boundaries must be precise. &lt;code&gt;09:29:59&lt;/code&gt; and &lt;code&gt;09:30:00&lt;/code&gt; are different minutes.&lt;/p&gt;

&lt;p&gt;We use left-closed, right-open intervals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;09:30:00 &amp;lt;= tick_time &amp;lt; 09:31:00&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This ensures each tick falls into exactly one candle.&lt;/p&gt;

&lt;p&gt;In practice, when integrating with &lt;a href="http:\alltick.co" rel="noopener noreferrer"&gt;ALLTICK API&lt;/a&gt;, we convert all timestamps to &lt;code&gt;Asia/Hong_Kong&lt;/code&gt; before session checks and K-line aggregation, following the API's documented field structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Processing Pipeline
&lt;/h2&gt;

&lt;p&gt;Here's the flow we use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API real-time data
     ↓
Parse timestamp
     ↓
Convert to Asia/Hong_Kong
     ↓
Determine trading date
     ↓
Determine session state
     ↓
Filter/classify quotes
     ↓
K-line aggregation or strategy calculation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By isolating time logic and avoiding magic numbers like &lt;code&gt;09:30&lt;/code&gt; or &lt;code&gt;16:00&lt;/code&gt;, we made the system easier to extend to other markets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Separate &lt;strong&gt;data time&lt;/strong&gt; from &lt;strong&gt;trading time&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Always convert timestamps to &lt;code&gt;Asia/Hong_Kong&lt;/code&gt; before session logic.&lt;/li&gt;
&lt;li&gt;Use interval checks, not point comparisons.&lt;/li&gt;
&lt;li&gt;Treat pre-open and post-close as distinct states.&lt;/li&gt;
&lt;li&gt;Keep session state and trading day in separate variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Time boundaries are foundational. Get them right, and everything downstream becomes simpler.&lt;/p&gt;

&lt;p&gt;What time-related bugs have you encountered in market data systems? Let me know in the comments!&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feefp8ep562506tkbb970.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feefp8ep562506tkbb970.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Detect Missing Data Intervals in My Hong Kong Stock API Feed (And Why You Should Too)</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Wed, 26 Aug 2026 03:01:58 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/how-i-detect-missing-data-intervals-in-my-hong-kong-stock-api-feed-and-why-you-should-too-5f1g</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/how-i-detect-missing-data-intervals-in-my-hong-kong-stock-api-feed-and-why-you-should-too-5f1g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgosxnlteto4njd83occ.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbgosxnlteto4njd83occ.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
I write a lot of content about Hong Kong equities, and I used to assume that if my WebSocket connection was alive, my data was complete. Then I published a chart that didn’t match the official exchange feed, and a reader called me out on it. That was a wake-up call.&lt;/p&gt;

&lt;p&gt;The problem wasn’t a broken connection. It was silent gaps — small stretches of missing ticks that happened without any error or warning. For a content creator who relies on real-time Hong Kong stock data, these gaps can quietly corrupt your analysis. In this post, I’ll show you the simple methods I now use to detect and fix them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Root Cause: Why Real-Time Feeds Have Gaps
&lt;/h2&gt;

&lt;p&gt;Real-time market data typically arrives over a long-lived connection, with each tick carrying a timestamp. Under ideal conditions, you can reconstruct minute bars, volume profiles, and intraday patterns perfectly. But networks are never ideal. Latency spikes, brief packet loss, or even your own machine’s processing lag can cause a sequence of ticks to disappear without breaking the connection.&lt;/p&gt;

&lt;p&gt;Here’s a real example from one of my tracking sessions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Data Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10:00:01&lt;/td&gt;
&lt;td&gt;Received normally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:00:02&lt;/td&gt;
&lt;td&gt;Received normally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:00:03-10:00:15&lt;/td&gt;
&lt;td&gt;No data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:00:16&lt;/td&gt;
&lt;td&gt;Resumed receiving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twelve seconds of silence. In a liquid market, that could mean dozens of missed trades. If I build a chart from this feed without checking, my volume and price movement analysis will be wrong — and my readers won’t know why.&lt;/p&gt;
&lt;h2&gt;
  
  
  Detecting Gaps with Timestamp Analysis
&lt;/h2&gt;

&lt;p&gt;The first method I use is timestamp gap analysis. For every tick, I store the original timestamp and compare it to the previous tick. If the gap exceeds a reasonable threshold, I flag it as a potential missing interval.&lt;/p&gt;

&lt;p&gt;Here’s the core logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;last_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;last_time&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_time&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Detected data gap:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;last_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold isn’t universal. A blue-chip stock might tick every second, while a small-cap might only tick every ten seconds. I adjust the threshold based on each stock’s typical tick frequency to avoid false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Sequence Numbers for Stronger Detection
&lt;/h2&gt;

&lt;p&gt;Timestamps are helpful, but they can miss issues when ticks are close together. That’s why I also look at message sequence numbers when the feed provides them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20001&lt;/li&gt;
&lt;li&gt;20002&lt;/li&gt;
&lt;li&gt;20003&lt;/li&gt;
&lt;li&gt;20007&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The missing numbers are immediately obvious. If the API doesn’t include sequence numbers, I add a heartbeat check: periodically inspect the latest tick time, and if it hasn’t updated for too long, log the state and resubscribe. This combination catches nearly all meaningful gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together in a Real Workflow
&lt;/h2&gt;

&lt;p&gt;I separate data ingestion from validation. One process receives the ticks, and another process checks for anomalies. I’ve been using &lt;a href="http:\alltick.co" rel="noopener noreferrer"&gt;AllTick&lt;/a&gt;’s WebSocket feed for Hong Kong stocks, which provides raw ticks I can validate before using them in my content. Here’s a more complete implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="n"&gt;last_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;last_timestamp&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_timestamp&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Possible missing interval:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;last_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/stock/websocket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After finding a gap, I save the time range and later fetch historical data to fill it. This keeps my charts and analysis based on a complete record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery: Don’t Skip This Step
&lt;/h2&gt;

&lt;p&gt;Detecting gaps is only the first part. If you don’t recover the missing data, your analysis is still incomplete. When I confirm a gap, I follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record the exact start and end time.&lt;/li&gt;
&lt;li&gt;Request historical tick data for that range.&lt;/li&gt;
&lt;li&gt;Verify the recovered data is continuous with my existing records and avoid duplicates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Duplicate records can distort volume and K-line calculations just as much as missing data. So recovery needs the same level of care.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Has Improved My Content
&lt;/h2&gt;

&lt;p&gt;Since adding these checks to my Hong Kong stock data pipeline, my charts now match the exchange feed much more closely. I no longer worry about phantom spikes or false volume dips. My readers have noticed the improvement too — they’ve told me my analysis feels more reliable and grounded.&lt;/p&gt;

&lt;p&gt;If you’re a creator or developer working with a Hong Kong stock API, don’t trust “connected” as a sign of data health. Add timestamp gap analysis and sequence checks to your workflow. It’s a small investment that pays off in more accurate, more trustworthy content.&lt;/p&gt;

</description>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Detect Anomalies in Stock Data API Feeds (With Python Code)</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Tue, 25 Aug 2026 05:49:34 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/how-to-detect-anomalies-in-stock-data-api-feeds-with-python-code-3fdn</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/how-to-detect-anomalies-in-stock-data-api-feeds-with-python-code-3fdn</guid>
      <description>&lt;p&gt;When I first started building real-time market data pipelines, I focused almost entirely on connection stability and update speed. But after running a complete data flow, I realized that data accuracy matters just as much. Sometimes the feed looks fine, the process keeps running, and yet when you inspect historical records, you find small deviations in prices, timestamps, or volumes.&lt;/p&gt;

&lt;p&gt;These anomalies aren’t caused by the market. They come from the processing layer. Real-time quotes keep flowing into your system, and without validation, a few bad records can corrupt your K-lines, indicators, and strategy results. That’s why I now put data validation directly in the quote processing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need Validation for Real-Time Quotes
&lt;/h2&gt;

&lt;p&gt;Stock market data is a continuously changing stream. Every second produces new prices and trade information. During reception and processing, several types of anomalies can appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamp order issues: new data arrives with a time earlier than the previous record.&lt;/li&gt;
&lt;li&gt;Price changes that clearly exceed the normal range.&lt;/li&gt;
&lt;li&gt;Volume fields missing or formatted incorrectly.&lt;/li&gt;
&lt;li&gt;Critical fields like symbol or trading status being empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these records go straight into your database, later K-line generation can drift. For example, a wrong price inside a one-minute candle can distort the high, low, and close, which then affects every technical indicator built on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the Timestamp Order First
&lt;/h2&gt;

&lt;p&gt;When processing real-time quotes, I check the timestamp field first. Normally, push messages for the same symbol should be increasing over time. If the program receives a record with an earlier time, it should be temporarily filtered out to avoid breaking the downstream ordering. A simple Python check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;last_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, you also need to account for exchange time zones, server time, and data source time. U.S. market data is especially tricky during daylight saving time transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect Anomalous Price Moves
&lt;/h2&gt;

&lt;p&gt;Price validation is another important step. A stock price won’t change without limit in an extremely short time, so you can set a threshold based on historical volatility. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;old_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;old_price&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold needs to be adjusted per stock type. Large-cap stocks and high-volatility stocks shouldn’t use the same criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate Fields After Receiving a Push
&lt;/h2&gt;

&lt;p&gt;Beyond price and time, I check whether the quote data structure is complete. For a real-time push, I don’t send the record to the calculation module immediately. Instead, I confirm the following first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;symbol&lt;/code&gt; exists.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; is not empty.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;volume&lt;/code&gt; is correctly formatted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt; is valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, when using AllTick API’s WebSocket to receive stock quotes, I run field checks before handing the data to the next stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://apis.alltick.co/websocket-api/stock-websocket-interface-api/transaction-quote-subscription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps invalid records from reaching your core calculation logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Anomalies Instead of Silently Dropping Them
&lt;/h2&gt;

&lt;p&gt;Many developers prefer to filter out bad data and move on. I think it’s better to keep a record. For each blocked item, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Symbol.&lt;/li&gt;
&lt;li&gt;Data timestamp.&lt;/li&gt;
&lt;li&gt;Original price.&lt;/li&gt;
&lt;li&gt;Reason for rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later, reviewing these logs helps you determine whether the problem comes from the data source or from your own processing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience After Building Several Market Data Systems
&lt;/h2&gt;

&lt;p&gt;The longer I work on stock quote systems, the more I care about data quality. A stock data API provides the source of truth, but validation is what makes that source usable. For real-time quotes, quantitative analysis, or automated trading, adding basic data checks reduces a lot of hidden risk.&lt;/p&gt;

&lt;p&gt;A stable market data system isn’t only about speed. It’s about making sure every record that flows through it is reliable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam0yjcdpkygkzeec0rgd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fam0yjcdpkygkzeec0rgd.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>How to Handle Real-Time and Historical Data from a Stock Data Interface in One Normalized Pipeline</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 20 Aug 2026 03:42:55 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/how-to-handle-real-time-and-historical-data-from-a-stock-data-interface-in-one-normalized-pipeline-pdg</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/how-to-handle-real-time-and-historical-data-from-a-stock-data-interface-in-one-normalized-pipeline-pdg</guid>
      <description>&lt;p&gt;We have been reviewing brokerage tools and market data feeds for a while, and one pattern keeps coming up. Teams build a live quote dashboard and a separate historical backtest pipeline, only to discover later that the two datasets do not agree. In this post, we will share the normalization approach we use with enterprise financial data analysts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A typical stock data interface returns many data types: ticks, minute bars, daily bars, and so on. Real-time feeds usually contain &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;volume&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt;. Historical bars contain &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;, and &lt;code&gt;volume&lt;/code&gt;. If each module keeps the raw API shape, field mismatches and timezone drift start appearing as soon as you combine live and historical data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution: A Light Transformation Layer
&lt;/h2&gt;

&lt;p&gt;We insert a small conversion step before data enters storage. Every message becomes a consistent object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;market_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAPL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;225.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-08-14T13:30:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this shape, live ticks and historical bars can be stored and queried under the same rules. You do not need a custom reader for each data type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standardize Time from the Start
&lt;/h2&gt;

&lt;p&gt;Time zone differences are easy to miss. Exchange local time versus UTC can shift every indicator. Our rule is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalize all timestamps to UTC at ingestion.&lt;/li&gt;
&lt;li&gt;Convert to exchange local time only for display or reporting.&lt;/li&gt;
&lt;li&gt;Keep the same rule for streaming and batch data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one decision saves a lot of debugging later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Real-Time and Historical Feeds
&lt;/h2&gt;

&lt;p&gt;A typical stock analysis page works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load a historical window of K-lines.&lt;/li&gt;
&lt;li&gt;Subscribe to real-time ticks.&lt;/li&gt;
&lt;li&gt;Update the chart continuously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick is ensuring the end of the historical window aligns with the start of the live stream. We pass live data through the same normalization layer first, then into cache or storage. This keeps front-end and strategy code away from raw vendor formats.&lt;/p&gt;

&lt;p&gt;We tried this flow with a WebSocket feed for stock ticks, and the normalization looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;market_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;market_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/stock/websocket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The focus here is not just receiving data, but enforcing a consistent standard for everything entering the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Details Worth Planning Early
&lt;/h2&gt;

&lt;p&gt;Based on our experience, plan for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not bind application fields directly to vendor response keys. Use a mapping layer.&lt;/li&gt;
&lt;li&gt;Standardize price precision and volume units.&lt;/li&gt;
&lt;li&gt;Handle reconnect backfill for live streams.&lt;/li&gt;
&lt;li&gt;Define a merge policy between real-time cache and historical storage.&lt;/li&gt;
&lt;li&gt;Keep the transformation layer simple and single-purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Real-time and historical data are not separate concerns. They are different stages of the same data lifecycle. Setting up a shared schema and time rule early makes charting, strategy analysis, and backtesting more reliable. The data interface is just the starting point. The way you govern that data determines the quality of the system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjl2yd5155i2w31i2xgfq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjl2yd5155i2w31i2xgfq.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
    </item>
    <item>
      <title>Optimizing Historical K-Line Queries from a Gold Real-Time API with a Simple Cache</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:00:03 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/optimizing-historical-k-line-queries-from-a-gold-real-time-api-with-a-simple-cache-3ino</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/optimizing-historical-k-line-queries-from-a-gold-real-time-api-with-a-simple-cache-3ino</guid>
      <description>&lt;p&gt;I recently measured the performance of my gold market analysis tool and found a problem: a single backtest run was triggering 950 historical K-line requests and spending about 3.2 seconds just on data retrieval. After I introduced a local caching layer, the same task completed in 0.6 seconds.&lt;/p&gt;

&lt;p&gt;That's an 81% improvement, and it came from a relatively simple change in how data flows through the system. Here's a detailed breakdown of the problem, the solution, and the implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Redundant Requests for Static Data
&lt;/h2&gt;

&lt;p&gt;In quantitative trading, historical K-lines are accessed constantly. Whether you're calculating moving averages, Bollinger Bands, or running a full strategy backtest, you need to read candles from days or months in the past.&lt;/p&gt;

&lt;p&gt;In my first version, every part of the code called the gold real-time API directly when it needed historical data. With a small dataset, that worked fine. But once I ran multiple strategies in parallel or swept through many parameter combinations, the latency became obvious.&lt;/p&gt;

&lt;p&gt;The key realization was this: the API wasn't the bottleneck. The repeated network round-trips and JSON parsing were eating up time — and all for data that never changed.&lt;/p&gt;

&lt;p&gt;Historical K-lines have a special property: once a candle is closed, it's immutable. Yesterday's 1-minute gold K-line won't change tomorrow. So fetching it repeatedly from a remote server is wasted work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A Two-Step Data Access Pattern
&lt;/h2&gt;

&lt;p&gt;I changed the data access logic to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check whether the local cache already covers the requested range.&lt;/li&gt;
&lt;li&gt;If data is missing or incomplete, fetch only the missing portion from the API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This simple pattern eliminated most redundant traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementing the Cache Layer
&lt;/h2&gt;

&lt;p&gt;I designed two cache types based on how often the data changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Data: In-Memory Cache
&lt;/h3&gt;

&lt;p&gt;Real-time quotes change constantly, so I keep them in memory and only retain the most recent tick or price. This provides fast reads without persistence overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Historical Data: Persistent Storage
&lt;/h3&gt;

&lt;p&gt;Historical K-lines are stable, so I persist them to a local file or database. On the next startup, the program loads the cache instead of re-downloading everything.&lt;/p&gt;

&lt;p&gt;When I store historical candles, I include these fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;symbol&lt;/td&gt;
&lt;td&gt;Identifies the trading instrument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timeframe&lt;/td&gt;
&lt;td&gt;Identifies the K-line period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;start and end time&lt;/td&gt;
&lt;td&gt;Matches the requested range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OHLC data&lt;/td&gt;
&lt;td&gt;Used for indicators and backtesting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With this metadata, I can quickly determine whether the cache satisfies a query. If only a few hours are missing, I fetch just that slice and merge it with what's already stored.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bridging Real-Time Data and Cache
&lt;/h2&gt;

&lt;p&gt;Real-time and historical data shouldn't be treated as isolated systems. If they are, you'll end up with a gap between the latest candle and the historical series.&lt;/p&gt;

&lt;p&gt;My approach is to route real-time ticks into the cache first, then build K-lines from those ticks based on the timeframe. When a period closes, I save the completed K-line to persistent storage.&lt;/p&gt;

&lt;p&gt;Here's an example using the AllTick API, where I receive tick data over WebSocket and store the latest price in a memory cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="n"&gt;market_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;market_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the latest price is in the cache, subsequent K-line calculations and chart displays can read from memory directly, avoiding additional API calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pitfalls and Maintenance
&lt;/h2&gt;

&lt;p&gt;A cache is not a set-and-forget component.&lt;/p&gt;

&lt;p&gt;The gold market operates nearly 24 hours, and real-time data can go stale quickly. So I use different expiration policies for real-time and historical data. Real-time prices expire fast; historical candles can be stored long-term.&lt;/p&gt;

&lt;p&gt;Another common mistake is updating the cache by re-downloading an entire range when only a small segment is missing. Instead, fetch just the missing part and merge it. For a long-running system, this difference accumulates significantly.&lt;/p&gt;

&lt;p&gt;If you need multiple strategies to read the same data concurrently, consider upgrading to Redis so different processes can share one cached copy. That's the natural next step when the system scales.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Data Flow Matters as Much as API Speed
&lt;/h2&gt;

&lt;p&gt;This optimization changed how I think about performance in market data systems. The API speed is only one factor. How data moves through your application is equally important.&lt;/p&gt;

&lt;p&gt;Real-time data provides fresh prices; the cache eliminates redundant reads. Together, they keep backtesting and indicator calculations stable.&lt;/p&gt;

&lt;p&gt;If your application queries historical K-lines frequently, treat caching as a core part of the data pipeline — not an optional enhancement. Design it early, and you'll have a much easier time scaling to more symbols and larger datasets later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4uhlenaeju9wvq7287k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4uhlenaeju9wvq7287k.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stock API Minute‑to‑Daily Bar Aggregation: How We Solved Timezone Offset Issues in Our Trading Data Pipeline</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 13 Aug 2026 02:57:28 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/stock-api-minute-to-daily-bar-aggregation-how-we-solved-timezone-offset-issues-in-our-trading-data-om2</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/stock-api-minute-to-daily-bar-aggregation-how-we-solved-timezone-offset-issues-in-our-trading-data-om2</guid>
      <description>&lt;p&gt;Hey devs! Today I want to share a data engineering war story that will resonate with anyone who has ever built a financial data pipeline. Our team supports a group of cross‑border quantitative traders who analyze US equities. To keep infrastructure costs lean, we fetch minute‑level OHLC data from a stock API and aggregate it into daily bars ourselves. Everything was fine until one Monday morning when a strategist asked, “Why does the daily open for AAPL on July 1 not match the exchange?” That question kicked off a deep dive into exchange timezones, daylight saving rules, and trading calendars.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scenario: Building a Cost‑Efficient Daily Bar Factory
&lt;/h4&gt;

&lt;p&gt;We work with independent investors and small trading desks who operate across multiple markets. Paying for premium daily bar feeds for every region would eat up most of their research budget. So we architected an internal data service that consumes an affordable stock API for minute data and produces daily bars on‑the‑fly. The design is simple: ingest minute bars, group by date, compute OHLC. But that simplicity hides a critical assumption—that the date attached to each minute bar already belongs to the correct trading session.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Core Pain: UTC Grouping Splits Trading Sessions
&lt;/h4&gt;

&lt;p&gt;The stock API we use returns timestamps in UTC. For US stocks, trading happens in Eastern Time. In daylight saving, the market opens at 09:30 ET (13:30 UTC) and closes at 16:00 ET (20:00 UTC). If we group by the UTC date, the bars between 19:00 and 20:00 UTC belong to the &lt;em&gt;next&lt;/em&gt; UTC day. The result: a single trading day gets split across two daily bars. Here’s the simple illustration that made everything click for us:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time Type&lt;/th&gt;
&lt;th&gt;Corresponding Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eastern Trading Time&lt;/td&gt;
&lt;td&gt;2026-07-01 09:30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTC Time&lt;/td&gt;
&lt;td&gt;2026-07-01 13:30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once you see this, it’s obvious. But in a large pipeline that processes hundreds of symbols, the error manifests as sporadic opening‑price jumps that are maddeningly hard to trace.&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution: A Time‑Normalization Layer Before Any Aggregation
&lt;/h4&gt;

&lt;p&gt;We refactored the pipeline to treat time conversion as a mandatory preprocessing step. No piece of code that computes OHLC ever sees a raw UTC timestamp. The flow now looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse the original timestamp from the API response.&lt;/li&gt;
&lt;li&gt;Load the target exchange timezone from a configuration map (for US stocks, &lt;code&gt;America/New_York&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Use Python’s &lt;code&gt;zoneinfo&lt;/code&gt; to convert to local time, fully accounting for DST.&lt;/li&gt;
&lt;li&gt;Assign a trading date based on the converted timestamp and session rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core conversion logic is self‑contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="n"&gt;utc_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-01 13:30:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;utc_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utc_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;market_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utc_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;market_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet runs on every batch load, guaranteeing that all minute bars are aligned to the exchange clock before aggregation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extending the Solution: Trading Sessions and Calendars
&lt;/h4&gt;

&lt;p&gt;Timezone conversion alone isn’t enough. Many APIs include pre‑market and after‑hours trades. If those get mixed into the daily bar, your technical indicators will quietly degrade. We therefore added a session filter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only minute bars between 09:30 and 16:00 Eastern are used for the standard daily bar.&lt;/li&gt;
&lt;li&gt;Extended‑hours data is routed to a separate analytics store.&lt;/li&gt;
&lt;li&gt;A trading calendar service dynamically adjusts for half‑days and holidays, so the system never assumes a fixed bar count.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Real‑Time Streaming Consistency
&lt;/h4&gt;

&lt;p&gt;For live trading dashboards, we ingest real‑time ticks through a WebSocket. To keep real‑time bars identical to historical ones, the tick processor reuses the same time‑normalization module. We use a low‑latency, budget‑friendly feed from AllTick for our US equity streams, and the integration was seamless because we had already solved the time problem generically.&lt;/p&gt;

&lt;p&gt;Here is our real‑time tick handler. Note how time conversion is the very first operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;trade_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tradeTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;trade_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;market_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;market_time&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/stock/websocket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Lessons Learned and Practical Tips
&lt;/h4&gt;

&lt;p&gt;Throughout this project, we’ve built a small checklist that might save you some pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize early.&lt;/strong&gt; If you consume data from multiple stock APIs, agree on a single timezone standard before any merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve raw timestamps.&lt;/strong&gt; Store them as a debug column. When an analyst questions a daily bar, the raw value is your audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hard‑code bar counts.&lt;/strong&gt; Markets have early closes. Use a calendar to determine expected bar counts dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encapsulate time logic.&lt;/strong&gt; A shared time‑conversion module used by batch and real‑time paths eliminates whole classes of bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;What started as a minor discrepancy in a daily bar turned into a comprehensive refinement of our entire data architecture. The fix wasn’t in the aggregation math; it was in the invisible timestamp semantics that preceded it. For any developer building financial tools on top of a stock API, my honest advice is to obsess over timezone correctness early. Once your temporal foundation is solid, the candlesticks you generate will finally reflect the true market narrative, and your traders can focus on strategy instead of data forensics.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxlfmeshdnk23tapcot6.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzxlfmeshdnk23tapcot6.jpeg" alt=" " width="644" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Tutorial: Fix US Stock API Candlestick Gaps with Session-Aware Aggregation</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:02:35 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/tutorial-fix-us-stock-api-candlestick-gaps-with-session-aware-aggregation-17n5</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/tutorial-fix-us-stock-api-candlestick-gaps-with-session-aware-aggregation-17n5</guid>
      <description>&lt;p&gt;Have you ever built a candlestick chart for US equities, only to find that the pre-market and after-hours segments look like a broken zipper? We’ve been there while powering financial bloggers’ data dashboards. The good news is that the issue rarely lies in your frontend library—it’s almost always a data modeling problem. In this tutorial, we’ll share how we re-architected our US stock market API pipeline to produce continuous candlestick charts across all trading sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding Why Extended Hours Create Visual Breaks&lt;/strong&gt;&lt;br&gt;
US stocks trade outside the 09:30–16:00 ET regular window, with official pre-market and after-hours sessions. These sessions contain real trades, but at drastically lower frequencies. A typical implementation that chops the day into fixed 5-minute windows will encounter many intervals with zero trades during extended hours. If your code simply skips those intervals, the time series compresses and the chart displays a price jump. The fix is to stop treating all ticks equally and start respecting the market’s session structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Force Time Consistency in Your Data Pipeline&lt;/strong&gt;&lt;br&gt;
We learned early that not all US stock APIs speak the same time language. Some return UTC, others Eastern Time. To avoid candlestick shifts, we normalize every incoming tick to UTC at the ingestion layer. We retain the original exchange timestamp in a separate field for debugging. When rendering a chart for end users, we convert back to Eastern Time. This one practice eliminates entire categories of offset bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Label Every Tick with Its Trading Session&lt;/strong&gt;&lt;br&gt;
We enrich each tick with a session identifier based on its Eastern Time timestamp:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trading Session&lt;/th&gt;
&lt;th&gt;Time Range (ET)&lt;/th&gt;
&lt;th&gt;Handling Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-Market&lt;/td&gt;
&lt;td&gt;Before 09:30&lt;/td&gt;
&lt;td&gt;Recorded separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regular Trading&lt;/td&gt;
&lt;td&gt;09:30–16:00&lt;/td&gt;
&lt;td&gt;Normal aggregation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After-Hours&lt;/td&gt;
&lt;td&gt;After 16:00&lt;/td&gt;
&lt;td&gt;Processed independently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With this tagging, your aggregation logic can branch. Need a pure regular-session view? Filter by the “Regular Trading” tag. Building a full-day continuous chart? Aggregate each session separately and then merge by timestamp. The chart’s time axis stays linear because no interval is discarded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Aggregate Raw Ticks Instead of Using Pre-Built Candles&lt;/strong&gt;&lt;br&gt;
In production, we avoid consuming pre-aggregated candlestick data from any API. Instead, we stream raw tick data over WebSocket. By tapping a service like AllTick API, we receive real-time US stock trades with precise timestamps, and we perform the candlestick construction in our own code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Parse incoming market data
&lt;/span&gt;    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# Stock ticker
&lt;/span&gt;    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# Latest trade price
&lt;/span&gt;    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Trade timestamp
&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Establish a WebSocket connection
&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/stock/websocket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Keep listening for new trades
&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the aggregation loop, we check the timestamp against the session boundaries, place the trade into the appropriate candlestick bucket, and emit completed candles once the bucket’s time is up. This gives us complete command over how pre-market, regular, and after-hours candles merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Define the Merge Rules Upfront&lt;/strong&gt;&lt;br&gt;
Before showing charts to your audience, nail down the business rules. Will your daily bar incorporate after-hours prices? Do 5-minute charts during the pre-market appear as standalone segments or blend into the regular session? We document these choices with the content creators we support, so every chart they publish aligns with their analytical narrative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: Charts Your Users Can Trust&lt;/strong&gt;&lt;br&gt;
Since implementing this pipeline, the financial writers we work with have stopped receiving “why does this chart look broken?” replies. Their content is more authoritative because the underlying data respects actual market structure. If you’re building a US stock chart application, give session-aware aggregation a try—you’ll clean up those extended-hours artifacts for good.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9d57lhq0kzxs0pl58ee.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9d57lhq0kzxs0pl58ee.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Multi-Asset WebSocket Market Data APIs for Stocks, Forex, Crypto &amp; Commodities: A 2026 Technical Comparison with AllTick</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:44:51 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/multi-asset-websocket-market-data-apis-for-stocks-forex-crypto-commodities-a-2026-technical-3nig</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/multi-asset-websocket-market-data-apis-for-stocks-forex-crypto-commodities-a-2026-technical-3nig</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Engineers building real-time dashboards, algorithmic trading systems, or market-screening tools frequently hit the same pain points: fragmented APIs that force you to stitch together WebSocket feeds from disparate providers, inconsistent data schemas across asset classes, opaque rate‑limit models that cripple backtesting, and the operational burden of managing multiple authentication tokens and connection life‑cycles. In 2026, the landscape still demands a careful evaluation of latency, coverage, and developer ergonomics before committing to a market data backbone.&lt;/p&gt;

&lt;p&gt;This article provides a technical comparison of three public APIs — AllTick, Finnhub, and Binance — with a focus on their real‑time WebSocket capabilities and REST‑based historical data retrieval. AllTick serves as the primary implementation reference because it offers native multi‑asset coverage under a single API contract, making it a representative example for workflows that span equities, forex, crypto, and commodities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selection Criteria
&lt;/h2&gt;

&lt;p&gt;Three core benchmarks guide the evaluation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real‑Time Data Delivery &amp;amp; Latency&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
How quickly does a tick travel from the exchange to the subscriber? Includes WebSocket protocol efficiency, geo‑proximity of gateway clusters, and observed end‑to‑end latency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asset Class Coverage &amp;amp; Data Granularity&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The breadth of instrument types (stocks, forex pairs, crypto, commodities) and the finest available resolution (true tick‑by‑tick vs. aggregated 1‑minute bars).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Integration Effort &amp;amp; Developer Experience&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Onboarding friction: authentication model, SDK availability, WebSocket subscription logic, rate‑limit transparency, and historical data retrieval mechanics.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparative Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mini‑Reviews
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AllTick&lt;/strong&gt; – A unified market data API delivering low‑latency WebSocket streams and REST endpoints for equities, forex, crypto, and commodities, designed to reduce the number of vendor integrations in multi‑asset applications.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finnhub&lt;/strong&gt; – A developer‑friendly API with strong US equity fundamentals and a generous free tier, though forex and crypto feeds are comparatively light and heavily rate‑limited.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binance API&lt;/strong&gt; – The de‑facto crypto‑native data pipe, offering exhaustive tick‑level streams, deep historical order book snapshots, and virtually unrestricted public data access, but scoped exclusively to digital assets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Comparison Matrix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;AllTick&lt;/th&gt;
&lt;th&gt;Finnhub&lt;/th&gt;
&lt;th&gt;Binance API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free‑Tier Rate Limits&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30 REST requests/min, 1 WebSocket connection, up to 10 symbols&lt;/td&gt;
&lt;td&gt;60 REST calls/min, 1 WebSocket connection, 50 symbols (US equities only on WS free)&lt;/td&gt;
&lt;td&gt;Public market data: no strict request caps; up to 5 WebSocket connections, 200 streams each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real‑Time Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Typically &amp;lt;100 ms (Asian gateway)&lt;/td&gt;
&lt;td&gt;100‑200 ms (US equities via Finnhub WS)&lt;/td&gt;
&lt;td&gt;&amp;lt;100 ms (Binance cloud; edge clusters globally)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Granularity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tick, 1m, 5m, 15m, 30m, 1h, 4h, daily&lt;/td&gt;
&lt;td&gt;1m, 5m, 15m, 30m, 1h, daily (tick only for US stocks on paid plans)&lt;/td&gt;
&lt;td&gt;Tick (trade/aggTrade), 1m, 3m, 5m, …, daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supported Protocols&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST + WebSocket&lt;/td&gt;
&lt;td&gt;REST + WebSocket&lt;/td&gt;
&lt;td&gt;REST + WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Historical Data Depth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Up to 10 years (stocks), 5 years (forex), full exchange history (crypto); free tier includes recent 12 months&lt;/td&gt;
&lt;td&gt;1 year for free tier; extended history on paid plans&lt;/td&gt;
&lt;td&gt;Full exchange history (e.g., Binance Spot since 2017)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ideal Use Cases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi‑asset dashboards, cross‑market arbitrage scanners, brokerage back‑offices&lt;/td&gt;
&lt;td&gt;US stock sentiment analysis, lightweight portfolio tracking&lt;/td&gt;
&lt;td&gt;Crypto trading bots, deep order‑book analytics, DeFi oracles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AllTick data in the matrix reflects the standard public plan; enterprise tiers relax rate limits and extend connectivity options.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Guide
&lt;/h2&gt;

&lt;p&gt;The following examples use the AllTick API to demonstrate typical market data workflows. All code is production‑ready Python 3.10+ and relies only on standard libraries plus &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;websocket-client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; – Every request must include the API key in the header &lt;code&gt;X-API-Key&lt;/code&gt;. Free keys are obtainable from the AllTick developer portal.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. REST API – Fetch Candlestick (K‑Line) Data
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/klines&lt;/code&gt; endpoint returns OHLCV bars for a given instrument and interval. Parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code&lt;/code&gt; – Instrument identifier (e.g., &lt;code&gt;"AAPL.US"&lt;/code&gt;, &lt;code&gt;"EUR/USD"&lt;/code&gt;, &lt;code&gt;"BTC/USDT"&lt;/code&gt;, &lt;code&gt;"XAU/USD"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kline_type&lt;/code&gt; – Resolution: &lt;code&gt;"1m"&lt;/code&gt;, &lt;code&gt;"5m"&lt;/code&gt;, &lt;code&gt;"15m"&lt;/code&gt;, &lt;code&gt;"30m"&lt;/code&gt;, &lt;code&gt;"1h"&lt;/code&gt;, &lt;code&gt;"4h"&lt;/code&gt;, &lt;code&gt;"1d"&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;count&lt;/code&gt; – Number of bars to return (max 1000 per call).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_ALLTICK_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.alltick.io/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_klines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kline_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/klines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kline_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kline_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; O:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;o&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; H:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; L:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; C:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; V:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example: last 10 daily bars for Apple Inc.
&lt;/span&gt;&lt;span class="nf"&gt;fetch_klines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAPL.US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Workflow note&lt;/strong&gt; – The response envelope always contains &lt;code&gt;"code":0&lt;/code&gt; on success, an array of OHLCV objects under &lt;code&gt;"data"&lt;/code&gt;, and an optional &lt;code&gt;"total"&lt;/code&gt; field when a time range is queried (see historical retrieval). The timestamp &lt;code&gt;t&lt;/code&gt; is epoch milliseconds in UTC.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. WebSocket – Real‑Time Tick Data
&lt;/h3&gt;

&lt;p&gt;AllTick’s WebSocket gateway supports concurrent subscription to multiple instruments across asset classes. A single connection can carry equity quotes, forex prices, crypto trades, and commodity ticks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection &amp;amp; authentication&lt;/strong&gt; – Pass the API key as a query parameter. The gateway returns a heartbeat every 30 seconds; clients should implement a reconnection back‑off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;

&lt;span class="n"&gt;WS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://ws.alltick.io/stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Filter out heartbeats
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; @ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  price=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  vol=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WebSocket error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection closed – reconnecting in 5s...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;start_stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Subscribe to multiple instruments
&lt;/span&gt;    &lt;span class="n"&gt;subscribe_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subscribe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbols&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAPL.US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EUR/USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTC/USDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XAU/USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscribe_msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subscribed to real-time ticks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;WS_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?token=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on_close&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_close&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Run forever with automatic ping/pong (websocket-client handles ping)
&lt;/span&gt;    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ping_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ping_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;start_stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tick object structure&lt;/strong&gt; – Each tick message includes &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;volume&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt; (epoch ms), and an optional &lt;code&gt;bid/ask&lt;/code&gt; spread for forex/commodities. The gateway guarantees ordered delivery within a symbol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture decision&lt;/strong&gt; – Opening a single WebSocket with multi‑symbol subscription reduces the number of file descriptors and simplifies application‑level reconnection logic compared to one connection per symbol (the pattern required by many legacy APIs). AllTick enforces a maximum of 10 symbols on the free tier; paid plans lift this limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Historical Data Retrieval
&lt;/h3&gt;

&lt;p&gt;For backtesting or down‑sampling, you often need large chunks of archived data. The &lt;code&gt;/history/kline&lt;/code&gt; REST endpoint accepts a time window and returns paginated results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameters&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code&lt;/code&gt;, &lt;code&gt;kline_type&lt;/code&gt; – same as before.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_time&lt;/code&gt; / &lt;code&gt;end_time&lt;/code&gt; – epoch milliseconds in UTC.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;limit&lt;/code&gt; – batch size (max 1000). If the total number of bars in the window exceeds &lt;code&gt;limit&lt;/code&gt;, the response includes a &lt;code&gt;total&lt;/code&gt; field and you must paginate using the last returned timestamp as the new &lt;code&gt;start_time&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_historical_klines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kline_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/history/kline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;all_bars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;start_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;end_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kline_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kline_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;all_bars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Set next start to timestamp of last received bar + 1 ms to avoid duplicates
&lt;/span&gt;        &lt;span class="n"&gt;start_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="c1"&gt;# Respect rate limits: free tier allows 30 req/min
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all_bars&lt;/span&gt;

&lt;span class="c1"&gt;# Example: 1-minute bars for EUR/USD from August 1 to August 5, 2026
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;end&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_historical_klines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EUR/USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; 1m bars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error handling&lt;/strong&gt; – The function pauses 2.1 seconds between pages to stay within the 30‑request‑per‑minute window. For production, parse the &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; header when available and implement an adaptive wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow advantage&lt;/strong&gt; – The same endpoint serves stocks, forex, crypto, and commodities, and the response schema remains identical. This uniformity lets you reuse pagination logic across all asset types with zero code changes.&lt;/p&gt;

&lt;p&gt;API Docs：&lt;a href="https://apis.alltick.co/" rel="noopener noreferrer"&gt;https://apis.alltick.co/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub：&lt;a href="https://github.com/alltick/alltick-realtime-forex-crypto-stock-tick-finance-websocket-api" rel="noopener noreferrer"&gt;https://github.com/alltick/alltick-realtime-forex-crypto-stock-tick-finance-websocket-api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>ai</category>
    </item>
    <item>
      <title>Reconstructing a Cryptocurrency Order Book from Real-Time API Incremental Updates — A Practical Guide</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 06 Aug 2026 03:02:33 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/reconstructing-a-cryptocurrency-order-book-from-real-time-api-incremental-updates-a-practical-2oci</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/reconstructing-a-cryptocurrency-order-book-from-real-time-api-incremental-updates-a-practical-2oci</guid>
      <description>&lt;p&gt;When we started building out our crypto trading infrastructure, we quickly ran into a challenge that sounds simple but is actually a deep engineering problem: turning a firehose of incremental order book updates from a cryptocurrency API into a continuously accurate local representation of the market. This post is the guide we wish we’d had back then, written from the perspective of a team of independent high-frequency traders. We’ll cover the requirements, the data pain points, the specific product-level solutions we built, and how it all fits into real industry applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Problem Are We Solving?
&lt;/h2&gt;

&lt;p&gt;We need a local order book that accurately reflects the exchange’s live state. Every strategy — from quoting to statistical arbitrage to liquidity analysis — depends on this ground truth. But the API doesn’t send us a full picture; it sends small incremental patches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Pain Points: Understanding What You’re Actually Receiving
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Incremental Updates Are Instructions, Not States
&lt;/h3&gt;

&lt;p&gt;A full order book contains every active bid and ask level with their respective quantities. However, pushing the entire book on each change would be prohibitively bandwidth-heavy. Instead, cryptocurrency APIs use incremental feeds that deliver only the modified levels. You’ll see something like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Quantity Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buy&lt;/td&gt;
&lt;td&gt;65000&lt;/td&gt;
&lt;td&gt;+0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sell&lt;/td&gt;
&lt;td&gt;65010&lt;/td&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think of this as a log entry: “Apply this delta.” You cannot interpret it in isolation; you must maintain your own state and apply each delta in sequence. If you treat each delta as an independent event, the order of application can get scrambled, and your local book will drift from reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sequence Numbers Are Your Only Safeguard
&lt;/h3&gt;

&lt;p&gt;Network transport means messages can arrive out of order. A later message might reach your server before an earlier one. If you apply them by arrival time, the state becomes logically corrupted. Our solution is strict sequence-number discipline. The API provides a &lt;code&gt;sequence&lt;/code&gt; or &lt;code&gt;updateId&lt;/code&gt; with every message. Our rule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a full snapshot and record its sequence number.&lt;/li&gt;
&lt;li&gt;Buffer all subsequent deltas.&lt;/li&gt;
&lt;li&gt;Only apply deltas whose sequence number is exactly one greater than our current state.&lt;/li&gt;
&lt;li&gt;If a sequence gap appears, discard the local state and re-fetch a fresh snapshot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures we never build on a broken foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Functionality: Designing the Local Book and Connection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a Dictionary, Not a List
&lt;/h3&gt;

&lt;p&gt;Early on, we used arrays for price levels. As the number of levels grew, update performance tanked. Now we use dictionaries (maps) keyed by price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;order_book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="mi"&gt;65000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;64999&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="mi"&gt;65001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;65002&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.1&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On each delta: if new quantity &amp;gt; 0, set the price key; if quantity == 0, delete the key. This structure makes getting best bid/ask and calculating depth extremely fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket for Low-Latency Feeds
&lt;/h3&gt;

&lt;p&gt;HTTP polling introduces unacceptable latency for order book updates. We always use WebSocket connections. While integrating one feed, we based our initial handler on the AllTick API WebSocket market data pattern. The core processing loop is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;order_book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_order_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;order_book&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;order_book&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;order_book&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;order_book&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTCUSDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;update_order_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://apis.alltick.co/websocket-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the foundation. Wrap it with sequence validation, heartbeat checks, and automatic snapshot recovery on disconnect for a production-grade system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry Applications: Where Accurate Depth Matters
&lt;/h2&gt;

&lt;p&gt;We use this book for market making, arbitrage signals, and real-time liquidity monitoring. A small consistent error in depth data can gradually erode profitability. Two additional notes from our production experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After a WebSocket disconnect, the local book is outdated. Reconnect logic must fetch a fresh snapshot first, then resume deltas.&lt;/li&gt;
&lt;li&gt;Price precision: avoid floating-point keys. Convert all prices to integers based on the tick size to prevent matching errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accurate order book reconstruction is not just about receiving data; it’s about synchronizing a distributed state. Get this right, and your trading strategies have a solid foundation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag5hjrju5f82vb9zorio.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag5hjrju5f82vb9zorio.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Reliable XAUUSD Tick Pipeline with a Precious Metals API: Our Battle with Daylight Saving Time</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Wed, 05 Aug 2026 03:31:16 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/building-a-reliable-xauusd-tick-pipeline-with-a-precious-metals-api-our-battle-with-daylight-3bed</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/building-a-reliable-xauusd-tick-pipeline-with-a-precious-metals-api-our-battle-with-daylight-3bed</guid>
      <description>&lt;h3&gt;
  
  
  What happens when your gold trading strategy suddenly breaks in summer
&lt;/h3&gt;

&lt;p&gt;It was one of those late-night debugging sessions that every cross-border fintech startup knows too well. Our team had just rolled out an internal platform that combined a precious metals API with a custom backtesting engine for XAUUSD tick data. The system performed beautifully during the winter months. Our momentum-based intraday strategies were hitting win rates and Sharpe ratios that made our investors raise their eyebrows — in a good way.&lt;/p&gt;

&lt;p&gt;Then, without any code changes, the models began to deteriorate. Signals that should have triggered right at the New York open were instead firing one hour late. Live simulations diverged from backtests in ways we couldn’t explain. As the founders, we were staring at a crisis of data integrity — and we needed to fix it before our track record suffered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unpacking the problem: when time becomes a variable
&lt;/h3&gt;

&lt;p&gt;We traced every failure back to one assumption we’d made early on: that converting timestamps from the precious metals API to UTC was as simple as adding a constant offset. The API returns data in US Eastern Time. We’d hard-coded Eastern Time as “UTC minus 5 hours.” That’s true in winter. It’s false during daylight saving time.&lt;/p&gt;

&lt;p&gt;The error is subtle but devastating at tick resolution. Here’s a concrete example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time Status&lt;/th&gt;
&lt;th&gt;US Eastern&lt;/th&gt;
&lt;th&gt;UTC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Time&lt;/td&gt;
&lt;td&gt;09:30&lt;/td&gt;
&lt;td&gt;14:30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daylight Saving&lt;/td&gt;
&lt;td&gt;09:30&lt;/td&gt;
&lt;td&gt;13:30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tick generated during the summer at 13:31 UTC was being wrongly classified under the 14:31 UTC minute candle, which completely reshuffled the order book snapshots our strategy relied on. Our precious metals API was feeding us perfect prices — our own code was misplacing them in time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engineering the fix: a UTC-centric data pipeline
&lt;/h3&gt;

&lt;p&gt;We rebuilt our tick ingestion pipeline around a single principle: &lt;strong&gt;UTC is the only timezone that exists inside the system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Regardless of the source format, every timestamp is transformed into UTC at the earliest possible moment — before it enters the database, before it reaches the K-line aggregator, and certainly before any strategy code sees it. Python’s &lt;code&gt;zoneinfo&lt;/code&gt; module became our trusted ally because it dynamically resolves DST transitions based on the operating system’s timezone database.&lt;/p&gt;

&lt;p&gt;The conversion function we now use everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="n"&gt;time_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-06-05 09:30:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;new_york&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;utc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;time_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;local_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_york&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;utc_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC time:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utc_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real-time data, we applied the identical logic in our WebSocket message handler. When we connected to a reliable market data provider such as AllTick for live XAUUSD ticks, the first thing &lt;code&gt;on_message&lt;/code&gt; does is convert the timezone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;zoneinfo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;trade_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tradeTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;eastern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;utc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;trade_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;utc_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eastern&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utc_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point forward, K-line aggregation, strategy logic, and storage all operate on UTC. The rendering layer handles conversion to any display timezone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The business impact: reclaimed time and regained trust
&lt;/h3&gt;

&lt;p&gt;Before this fix, each DST transition season required two engineers to spend several days manually reviewing timestamps, correcting historical datasets, and re-running backtests. That’s a significant annual drain on a lean startup’s resources. Now, those days are fully reclaimed for feature development and alpha research.&lt;/p&gt;

&lt;p&gt;But the larger benefit is confidence. When your precious metals API delivers tick data and your strategies consume it, you must be absolutely certain that every data point sits in the correct temporal context. We learned the hard way that a one-hour offset can be more damaging than a pricing error. Fixing it gave us a foundation we can scale on — no matter how many strategies or markets we add.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudrrlr32r06zr051lmli.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudrrlr32r06zr051lmli.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>Handling Out‑of‑Order Level2 Messages from a US Stock API – A Practical Guide</title>
      <dc:creator>EmilyL</dc:creator>
      <pubDate>Thu, 30 Jul 2026 02:52:24 +0000</pubDate>
      <link>https://dev.to/kaihang_ho_2ad23569cdb965/handling-out-of-order-level2-messages-from-a-us-stock-api-a-practical-guide-3b72</link>
      <guid>https://dev.to/kaihang_ho_2ad23569cdb965/handling-out-of-order-level2-messages-from-a-us-stock-api-a-practical-guide-3b72</guid>
      <description>&lt;p&gt;As developers supporting quantitative trading desks, we’ve spent countless hours debugging order book inconsistencies. The root cause almost always traces back to one issue: &lt;strong&gt;message sequencing&lt;/strong&gt;. In this post, we’ll share our battle‑tested approach to processing Level2 data from a US Stock API, focusing on how we maintain a reliable order book despite network‑induced reordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client Requirements: Zero Tolerance for State Errors
&lt;/h3&gt;

&lt;p&gt;Our primary users are professional traders and fund quant developers. They consume our Level2 feed to build real‑time liquidity models and execute automated strategies. Their non‑negotiable demand: the local order book must be a bit‑perfect replica of the exchange’s limit order book at any given moment. A single mis‑applied delta – for example, a modification applied before its corresponding add – can skew the entire price ladder and trigger incorrect trades. We learned this when a client reported a persistent spread miscalculation; after investigation, we found that out‑of‑order cancellation messages were the culprit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pain Point: Network Jitter and Packet Reordering
&lt;/h3&gt;

&lt;p&gt;Unlike consolidated tape data, Level2 streams consist of discrete order‑state transitions – insert, update, delete. These operations are inherently dependent. When you fetch Level2 via a typical US Stock API over WebSocket, you’re at the mercy of internet routing. It’s entirely possible for a later event to arrive before an earlier one. For instance, the exchange emits:&lt;br&gt;
&lt;code&gt;Insert (seq=100) → Update (seq=101) → Delete (seq=102)&lt;/code&gt;&lt;br&gt;
But our receiver may see:&lt;br&gt;
&lt;code&gt;Update (101) → Insert (100) → Delete (102)&lt;/code&gt;&lt;br&gt;
Applying them in arrival order would cause an “order not found” error on the update, and later an invalid insert. This becomes especially frequent during high‑volatility periods when message rates exceed 10,000 per second.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sequence Numbers as the Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;We quickly dismissed timestamp‑based sorting – it’s unreliable due to clock skew and low resolution. Instead, we rely on the monotonic &lt;code&gt;sequence&lt;/code&gt; field included in each Level2 message. Here’s an example payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"AAPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;185.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"volume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10001&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We maintain a &lt;code&gt;last_processed_seq&lt;/code&gt; variable. If the incoming &lt;code&gt;sequence&lt;/code&gt; equals &lt;code&gt;last_processed_seq + 1&lt;/code&gt;, we apply the delta. If it’s greater, we declare a gap and trigger a full snapshot recovery. If it’s smaller, we discard it as duplicate. This logic is simple yet effective – it catches missing messages immediately, preventing silent data corruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture: Snapshot + Incremental with Recovery
&lt;/h3&gt;

&lt;p&gt;Our production system follows a “snapshot + incremental” model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialization&lt;/strong&gt;: Fetch a complete order book snapshot (all price levels and aggregated sizes) via a REST endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming&lt;/strong&gt;: Open a WebSocket connection to receive incremental updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt;: For each delta, check sequence continuity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery&lt;/strong&gt;: On any gap, pause incremental processing, fetch a fresh snapshot, replace the local book, and reset the sequence counter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also implement a small delay buffer: when we receive a message with a sequence number slightly ahead (e.g., we expect 10003 but get 10004), we hold it for up to 50ms to see if 10003 arrives. If it does, we reorder and apply. If not, we fetch a snapshot. This reduces unnecessary full refreshes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Walkthrough – Python WebSocket Listener
&lt;/h3&gt;

&lt;p&gt;Below is the core implementation we use as a starting point. The example uses a common WebSocket endpoint (we’ve integrated with various providers; the pattern is identical). Notice the sequence validation in the callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;last_sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;last_sequence&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sequence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;last_sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;last_sequence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data gap detected – re‑sync required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;last_sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebSocketApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://api.alltick.co/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;on_message&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, we add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic reconnection with exponential backoff.&lt;/li&gt;
&lt;li&gt;A thread‑safe cache for out‑of‑order messages.&lt;/li&gt;
&lt;li&gt;Health checks that compare our local book against occasional snapshot hashes.&lt;/li&gt;
&lt;li&gt;Metrics to monitor gap frequency and recovery latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;p&gt;The hardest part of Level2 processing isn’t writing the update logic – it’s ensuring the update order is correct. We’ve come to treat sequence validation as our primary defense against data corruption. If you’re building any system that consumes depth‑of‑market data, invest in this foundation first. Accurate prices follow accurate sequences – never the other way around.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47jnzp2hn1s2lj6kupet.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47jnzp2hn1s2lj6kupet.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
