<?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: wang vball</title>
    <description>The latest articles on DEV Community by wang vball (@wang_vball_2e41b5cfbc194e).</description>
    <link>https://dev.to/wang_vball_2e41b5cfbc194e</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%2F4202915%2F58867154-2ba3-402f-9667-abac2f6111df.png</url>
      <title>DEV Community: wang vball</title>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wang_vball_2e41b5cfbc194e"/>
    <language>en</language>
    <item>
      <title>Day Trading vs Settlement: Same-Day US Stock Trades Do Not Mean T+0 Settlement</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Sat, 14 Nov 2026 23:24:36 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/day-trading-vs-settlement-same-day-us-stock-trades-do-not-mean-t0-settlement-13h1</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/day-trading-vs-settlement-same-day-us-stock-trades-do-not-mean-t0-settlement-13h1</guid>
      <description>&lt;p&gt;Can you buy and sell a US stock on the same day, and does that mean it settles immediately? Same-day trading can be possible under applicable account rules, but most ordinary US stock transactions use T+1 settlement. Keeping trade date, expected settlement date and confirmed settlement status separate prevents a trading interface from presenting an executed sale as already settled cash.&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%2Fimd96q5c9zy4umxusq55.png" 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%2Fimd96q5c9zy4umxusq55.png" alt="Day trading and settlement are separate" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A same-day purchase and sale describe when two trades happen. Settlement describes when the resulting obligations are completed. Calling both processes “T+0” hides a distinction that matters to investors and to the software displaying their accounts.&lt;/p&gt;

&lt;p&gt;For ordinary US stock transactions covered by the standard cycle, the settlement convention is T+1, not T+0. That does not mean every investor must hold a stock overnight. It means same-day trading permission and settlement timing are different questions. Neither a quote feed nor a successful order submission answers both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the T actually measure?
&lt;/h2&gt;

&lt;p&gt;In settlement notation, T is the trade date. T+1 means one business day after that date; T+0 would mean settlement on the trade date. Execution time is more precise: it records when an order, or part of an order, was filled. A trade date is a business label, not simply the date shown on the viewer's laptop.&lt;/p&gt;

&lt;p&gt;The SEC's T+1 transition FAQ identifies May 28, 2024 as the compliance date for the shortened standard cycle. Its guidance also describes exceptions and limited circumstances involving expressly agreed settlement terms. The useful statement is therefore “most ordinary US stock transactions settle on T+1,” not “every security settles tomorrow.”&lt;/p&gt;

&lt;p&gt;Imagine a hypothetical purchase and sale on the same Monday, with Tuesday an ordinary applicable business day. The trades occur on Monday, while their expected standard settlement date can be Tuesday. Closing the position does not turn Monday into the settlement date. This illustration assumes an ordinary transaction and no intervening holiday; it is not a calendar or account-eligibility calculation.&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%2Fknc0hh4g1vv1hh3tsjd5.png" 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%2Fknc0hh4g1vv1hh3tsjd5.png" alt="Trading on the same trade date and settlement on the next business day are separate processes" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustration of ordinary T+1 transactions. Instrument-specific terms and applicable business calendars still matter.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Does “same-day trading” mean unlimited trading?
&lt;/h2&gt;

&lt;p&gt;No. It describes timing, not permission. Whether an account can enter a transaction depends on its funding, type, restrictions and the broker's current controls.&lt;/p&gt;

&lt;p&gt;FINRA's current cash-account guidance says securities must be fully paid for before they are sold. For intraday purchases and sales, it emphasizes using settled funds at purchase. It also distinguishes a good-faith violation involving unsettled sale proceeds from free-riding. A screen that shows sale proceeds is not, by itself, proof that those proceeds are settled and available for every subsequent use.&lt;/p&gt;

&lt;p&gt;Margin accounts follow a different framework. FINRA adopted new intraday margin requirements effective June 4, 2026, with a permitted broker transition period through October 20, 2027. During that transition, generic summaries of historical pattern-day-trader rules are not a reliable substitute for checking the rules a particular broker applies. This article does not calculate buying power or determine whether an account may trade.&lt;/p&gt;

&lt;p&gt;Frequent trading carries financial risk, and margin can increase losses beyond the original deposit. The distinction explained here is educational, not a recommendation to trade more often or a way to avoid account restrictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Market data is not your settlement record
&lt;/h2&gt;

&lt;p&gt;A market-data message tells you about the market. A broker execution report tells you about your fills. A settlement record tells you the broker-reported state of the resulting obligations. These sources answer different questions even when their timestamps look similar.&lt;/p&gt;

&lt;p&gt;TickDB's documented US-stock market data is useful for displaying prices and observing market activity. Its ticker documentation includes a market-data timestamp and conditionally available quote fields. Those fields should not be interpreted as proof that your account bought a security, that an order was filled, or that funds have settled.&lt;/p&gt;

&lt;p&gt;For a portfolio interface, keep the price panel separate from the order and cash ledger. A fresh TickDB quote can update valuation without changing an execution or settlement flag. Likewise, a broker's execution notification should not automatically change a pending-settlement balance into a settled balance.&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%2Fn8h46k7c2p45tyhaga0a.png" 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%2Fn8h46k7c2p45tyhaga0a.png" alt="Market data, execution reports and settlement records supply different evidence" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A quote timestamp is not a settlement confirmation. This is an information-model illustration, not a screenshot of a broker system.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A small state model that avoids the shortcut
&lt;/h2&gt;

&lt;p&gt;The following Python example keeps three facts separate: trade date, an externally supplied expected settlement date, and a confirmation flag. All records are fabricated. The dates are inputs, not a calculation of US holidays or settlement eligibility.&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;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;trade_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;trade_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
    &lt;span class="n"&gt;expected_settlement_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;settlement_confirmed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settlement_confirmed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Broker reported settled"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_settlement_date&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Settlement date unknown"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Expected {self.expected_settlement_date}; not confirmed"&lt;/span&gt;

&lt;span class="c1"&gt;# Fabricated records; dates are supplied, never calculated here.&lt;/span&gt;
&lt;span class="n"&gt;buy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TradeRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"BUY-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&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="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TradeRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELL-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&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="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trade_date&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trade_date&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settlement_confirmed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settlement_confirmed&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s2"&gt;"not confirmed"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TradeRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"UNKNOWN-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&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="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Settlement date unknown"&lt;/span&gt;
&lt;span class="n"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TradeRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DONE-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&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;11&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="p"&gt;,&lt;/span&gt;
                        &lt;span class="nf"&gt;date&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;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Broker reported settled"&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_status&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_status&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local example prints the same expected date for both trades, with “not confirmed” attached to each. It also checks an unknown-date case and an explicitly confirmed case. A same-day round trip does not set either confirmation flag.&lt;/p&gt;

&lt;p&gt;This is deliberately not a brokerage integration. In production, only a validated, appropriately matched broker event should change the confirmation state. Store the source, event identifier and update time so that the interface can explain why it changed. Preserve corrections rather than treating the first notification as permanent truth.&lt;/p&gt;

&lt;p&gt;Do not replace a missing expected date with “today plus one day.” Weekends are not the only calendar issue, and instrument terms can matter. Do not use the current wall clock passing an expected date as proof of completion either: expected and confirmed remain separate states. If a source is delayed, show that uncertainty instead of manufacturing a settled status.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the interface actually say?
&lt;/h2&gt;

&lt;p&gt;Prefer narrow labels such as “order filled,” “expected settlement date,” “settlement pending,” and “broker reported settled.” Avoid one green “complete” badge that conflates execution, position closure, cash availability and settlement.&lt;/p&gt;

&lt;p&gt;When explaining a rejection or cash restriction, use the broker's current response and account records, not an inference from the market-data timestamp. When explaining T+1, name the applicable scope and distinguish business days from calendar days. If the needed confirmation is unavailable, “unknown” is more accurate than a confident date-derived answer.&lt;/p&gt;

&lt;p&gt;The practical answer to the original question is simple: same-day US stock trading does not establish same-day settlement. Treat trading permission, execution and settlement as separate facts, and require the right evidence for each.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Official materials checked November 15, 2026: SEC T+1 FAQ, FINRA Frequent Intraday Trading and New Intraday Margin Requirements, and TickDB ticker documentation. The code and diagrams are educational examples, not live account tests or investment advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Choosing a Futures Tick Database: Test Event Fidelity Before Query Speed</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Fri, 13 Nov 2026 23:16:48 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/choosing-a-futures-tick-database-test-event-fidelity-before-query-speed-5aap</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/choosing-a-futures-tick-database-test-event-fidelity-before-query-speed-5aap</guid>
      <description>&lt;p&gt;Which database is best for futures tick data? Choose one that preserves the events your application needs before comparing query speed. Two trades can share a timestamp, continuous symbols can change their underlying contracts, and database deduplication rules differ. A small synthetic replay test can expose these mistakes before a fast query turns them into plausible but incorrect results.&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%2Fwew7oi7vzkaqmll9c08h.png" 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%2Fwew7oi7vzkaqmll9c08h.png" alt="Choosing a futures tick database, with event fidelity before query speed" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For trade analytics, start with a trustworthy trade-event archive and a database whose duplicate and correction handling you can verify. For order-book reconstruction, trade records alone are insufficient: you also need the relevant book events and recovery rules. A fast database cannot reconstruct events that its upstream feed never supplied.&lt;/p&gt;

&lt;p&gt;This is an evaluation method, not a database speed ranking or a claim that one market-data service covers every futures venue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the feed from the database
&lt;/h2&gt;

&lt;p&gt;A market-data API delivers a documented representation of the market. A raw event log preserves what your collector received. A query database organizes that material for analysis. These responsibilities can share infrastructure, but they should have separate acceptance criteria.&lt;/p&gt;

&lt;p&gt;TickDB is a useful example of the first layer. Its current documentation describes REST and WebSocket trade data for China and Hong Kong futures. That establishes a documented data-source option for those markets; it does not establish a user-queryable SQL database, exchange-native packet capture, or complete historical replay.&lt;/p&gt;

&lt;p&gt;Preserve the original message, source, receive time and a durable collector receipt identifier before normalization. Keep source event time separately, with its documented unit and meaning. Receipt order can reproduce what your collector observed; sorting by event time answers a different question. Neither ordering should silently replace the other.&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%2Fm9qqc73jndxfgu8fils5.png" 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%2Fm9qqc73jndxfgu8fils5.png" alt="Three separate layers: market data API, raw event log, and query database" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Suggested architecture: delivery, preservation and analytical queries are separate responsibilities, not a vendor architecture diagram.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what identifies a futures event
&lt;/h2&gt;

&lt;p&gt;A symbol plus a millisecond timestamp is not a universal event key. Two genuine trades can arrive with the same timestamp and price. Collapsing them can undercount volume while leaving a chart looking reasonable.&lt;/p&gt;

&lt;p&gt;Use a documented event identifier within its documented scope when one exists. That scope may require a source, venue, session and instrument. Do not assume an ID is globally unique or survives a session reset. If the source provides no reliable identifier, preserve separate receipts and mark identity unresolved instead of silently deleting apparent duplicates.&lt;/p&gt;

&lt;p&gt;The distinction matters when adapting TickDB messages. Its WebSocket documentation specifies single-trade objects for China and Hong Kong futures, and explicitly lists a trade ID for Hong Kong futures. The China futures example does not establish the same identifier guarantee. One universal adapter that assumes every message has an ID would overstate the documented contract.&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%2Fqvh3plmtv7f7293dr8hs.png" 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%2Fqvh3plmtv7f7293dr8hs.png" alt="Two synthetic trades share time 1000 milliseconds but have different IDs and both are retained" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two events can share a timestamp. This synthetic example illustrates identity, not measured exchange traffic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Contract identity needs the same care. Store the concrete contract and the effective mapping of any continuous alias separately. TickDB's data specification distinguishes regular China futures contracts from continuous symbols intended for market tracking, not delivery. Do not treat a continuous series as one permanent tradable contract. Record the roll rule and any price adjustment before comparing results across a rollover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the projection without destroying the evidence
&lt;/h2&gt;

&lt;p&gt;Keep the raw deliveries immutable and build a separate analytical projection. The following standard-library Python example uses a deliberately synthetic schema. It assumes source-defined IDs scoped by source, session and contract, plus explicitly ordered revision numbers. Those are test assumptions, not fields promised by every provider.&lt;/p&gt;

&lt;p&gt;The seven deliveries include two trades at the same time, a repeated delivery, a correction, a late older revision and two receipts without IDs. The projection retains both identified trades, applies the correction, and leaves the unidentified receipts unresolved.&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;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Synthetic schema: IDs and revision order are source-defined."""&lt;/span&gt;
    &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unresolved&lt;/span&gt; &lt;span class="o"&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="p"&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;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;row&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="s2"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;unresolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Never guess identity from time/price.&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&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;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"contract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latest&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;key&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;old&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"revision"&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&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&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;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"receipt_id"&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;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Conflicting payload at the same revision"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unresolved&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example&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="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"synthetic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"S1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"FUT-A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;event_ms&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;price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"100.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revision&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="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&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="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;deliveries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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="s2"&gt;"100.25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unresolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deliveries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unresolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"100.25"&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"7 deliveries: 2 resolved events, 2 unresolved receipts"&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;example&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;The local synthetic run prints &lt;code&gt;7 deliveries: 2 resolved events, 2 unresolved receipts&lt;/code&gt;. It does not prove market completeness, throughput or database durability. The unresolved receipts may include duplicates; retaining them avoids making an unsupported identity decision. Do not aggregate them as confirmed distinct trades without further reconciliation.&lt;/p&gt;

&lt;p&gt;A changed payload at the same revision raises an error rather than silently picking a winner. Production ingestion should quarantine that conflict while retaining its raw message. This example does not implement cancellations, persistent checkpoints or a source-specific sequence protocol. Add those cases according to the feed contract, including sequence resets and recovery after a collector restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare duplicate semantics before performance
&lt;/h2&gt;

&lt;p&gt;QuestDB and ClickHouse illustrate why “supports deduplication” is not a sufficient selection criterion.&lt;/p&gt;

&lt;p&gt;QuestDB's current deduplication documentation describes explicit key columns, including the designated timestamp, for WAL tables. Its UPSERT mode keeps the latest write for matching keys. That is write order, not necessarily the highest source revision: a late old correction needs deliberate handling. Enabling deduplication also does not clean up pre-existing duplicates retroactively.&lt;/p&gt;

&lt;p&gt;ClickHouse ReplacingMergeTree identifies duplicates through the sorting key and removes them during background merges whose timing is not guaranteed. A query cannot assume those merges have already completed. Evaluate query-time replacement semantics, including &lt;code&gt;FINAL&lt;/code&gt; where appropriate, and measure the actual query shape you will use.&lt;/p&gt;

&lt;p&gt;Neither behavior is inherently the right answer for every workload. An immutable audit archive and a latest-corrected analytical view have different requirements. Feed the same test deliveries into each candidate and compare the results immediately after ingestion, after maintenance, and after replay. Check contract separation, retained revisions and unresolved records, not just the final row count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark only the candidates that preserve meaning
&lt;/h2&gt;

&lt;p&gt;Once the semantic tests pass, measure your real workload: sustained ingestion, late arrivals, narrow contract-and-time queries, broader aggregates, and recovery. Hold the input dataset, hardware, retention policy and query results constant. Report cold and warm query behavior, concurrency and storage usage rather than publishing a single unexplained latency number.&lt;/p&gt;

&lt;p&gt;Validate backfill separately. TickDB's documented REST trades endpoint accepts a symbol and a limit of up to 1,000 records; that alone does not demonstrate arbitrary time-range recovery or lossless polling. Ask what can be recovered after a longer outage, and test the exact boundary. Storage and redistribution rights also need separate confirmation.&lt;/p&gt;

&lt;p&gt;Choose the simplest system that passes these checks at your required scale. If a candidate cannot explain which events survived replay, a faster query is answering an uncertain question.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Documentation checked November 14, 2026: TickDB Data Specification and Trade documentation; QuestDB Deduplication; ClickHouse ReplacingMergeTree. Code results are synthetic local validation, not a provider benchmark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>python</category>
    </item>
    <item>
      <title>Can You Use a Real-Time Forex API for Free? A Trial Has Limits</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Thu, 12 Nov 2026 23:19:23 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/can-you-use-a-real-time-forex-api-for-free-a-trial-has-limits-4lf8</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/can-you-use-a-real-time-forex-api-for-free-a-trial-has-limits-4lf8</guid>
      <description>&lt;p&gt;A free forex API can be useful for evaluating live quotes, but “free” does not necessarily mean ongoing access for a production application. Check the trial expiry, permitted currency pairs, request and subscription budgets, historical access, and usage rights separately. TickDB provides a concrete example: its documented Free plan is a seven-day trial, not an unlimited free production tier.&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%2F6x1gnn05ukdtlvuahsvd.png" 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%2F6x1gnn05ukdtlvuahsvd.png" alt="A technical cover contrasting a free forex API evaluation with production requirements." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful question is not just whether an API returns EURUSD. It is whether the access you can test today covers the application you intend to operate next month. A prototype that retrieves one quote can succeed even when a multi-user dashboard would exceed a connection limit, need an unavailable pair, or require historical queries outside the plan.&lt;/p&gt;

&lt;p&gt;This article uses TickDB's public documentation, checked on November 13, 2026, to show how to make that decision. It is not a provider ranking or a live performance benchmark. The same evaluation questions can be applied to another supplier, but the numerical limits below belong to TickDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  A free trial is a time-limited evaluation
&lt;/h2&gt;

&lt;p&gt;TickDB's Pricing documentation lists a seven-day Free trial with selected symbols, five concurrent subscriptions, one WebSocket connection, and an API rate limit of 30 requests per minute. It includes real-time quotes and real-time candlesticks. Those are separate constraints: access to the quote endpoint does not establish that every currency pair is included, and one open connection does not imply unlimited subscriptions.&lt;/p&gt;

&lt;p&gt;Start by writing down the exact pairs and data your prototype needs. A currency converter using occasional snapshots has a different workload from a streaming dashboard. Test those specific symbols with the actual evaluation account rather than assuming that a supported-market list is the same as an account's entitlement list.&lt;/p&gt;

&lt;p&gt;For a permanent free tier, the evidence must explicitly establish continuing access after the trial period. The documented seven-day trial does not do that. An application intended to run indefinitely therefore needs a plan for what happens when evaluation access ends, even if its traffic is small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate request volume from streaming capacity
&lt;/h2&gt;

&lt;p&gt;REST requests, open WebSocket connections, and concurrent subscriptions measure different activity. Reducing polling can lower REST traffic without changing how many symbols remain subscribed. Combining updates on one connection can reduce the connection count without reducing the subscription count.&lt;/p&gt;

&lt;p&gt;Consider an illustrative workload, not a measured service result. Polling one batched snapshot every two seconds produces 30 requests per minute. That reaches the documented Free request limit before allowing for any other calls. Polling every three seconds produces 20 requests per minute, leaving ten requests of arithmetic headroom, but it does not prove the server will accept a burst or that other applications sharing the key are idle. A rate limit is not a recommended target operating level.&lt;/p&gt;

&lt;p&gt;The Ticker Snapshot documentation allows up to 50 symbols per request. That endpoint maximum is not a grant of access to 50 trial symbols. Batch only symbols the account can actually access, and budget all calls sharing the relevant allowance. For a streaming implementation, verify connection and subscription limits independently instead of treating “WebSocket supported” as the end of the check.&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%2Fbqi4z7t1rmem81zxmvp4.png" 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%2Fbqi4z7t1rmem81zxmvp4.png" alt="A sequence of access checks moves from a trial through symbols, budgets, historical access and usage rights to a production decision." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. An evaluation checklist, not a measured benchmark or a guarantee of account access.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Candlestick access does not promise arbitrary history
&lt;/h2&gt;

&lt;p&gt;A common surprise is that a plan can expose candlesticks while restricting time-range queries. TickDB's Candlestick Data documentation says that the Free plan can query candlesticks but cannot specify a range using &lt;code&gt;start_time&lt;/code&gt; or &lt;code&gt;end_time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters when a prototype becomes a backtest. A chart showing currently available bars is not evidence that the same account can retrieve the last year for every pair. The pricing page lists historical data separately, while the endpoint documentation describes the exact parameter restriction. Read both before deciding whether a free evaluation can answer your research question.&lt;/p&gt;

&lt;p&gt;Make the historical requirement explicit: pairs, interval, requested date range, and any completeness checks. If a query mode is unavailable, preserve that result. Removing the dates until the endpoint returns something may help diagnose the issue, but it changes the question and cannot validate the original backtest requirement. Also account for the last returned candle still forming; its changing values are not automatically a data error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make access failures visible in the integration
&lt;/h2&gt;

&lt;p&gt;A failed request should tell you whether to change the workload, check permissions, or investigate transport. Repeating a forbidden request does not create permission. TickDB documents &lt;code&gt;3001&lt;/code&gt; as a rate-limit error, &lt;code&gt;3006&lt;/code&gt; as product access unavailable on the current plan, &lt;code&gt;3008&lt;/code&gt; as unavailable candlestick time-range queries, and &lt;code&gt;3009&lt;/code&gt; as an endpoint permission problem.&lt;/p&gt;

&lt;p&gt;The following Python example makes one snapshot request and records a useful failure category. It requires the third-party &lt;code&gt;requests&lt;/code&gt; package and an API key supplied through the &lt;code&gt;TICKDB_API_KEY&lt;/code&gt; environment variable. Run it on a trusted backend, not in a public browser bundle. It is an implementation example based on the documented contract; no live account or market-data performance was tested for this article.&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_forex_snapshot&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="s2"&gt;"EURUSD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&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="s2"&gt;"https://api.tickdb.ai/v1/market/ticker"&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="s2"&gt;"symbols"&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="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"forex"&lt;/span&gt;&lt;span class="p"&gt;}&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="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"X-API-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TICKDB_API_KEY"&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="n"&gt;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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&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;f&lt;/span&gt;&lt;span class="s2"&gt;"HTTP {response.status_code}: non-JSON response"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&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="s2"&gt;"Unexpected response shape"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UNKNOWN"&lt;/span&gt;&lt;span class="p"&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"0"&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;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"3001"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="s2"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Rate limited; Retry-After={retry_after!r}"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"3006"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"3008"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"3009"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Check product, query-mode, or endpoint access"&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inspect the documented error before retrying"&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;f&lt;/span&gt;&lt;span class="s2"&gt;"HTTP {response.status_code}, code={code}: {action}"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&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="s2"&gt;"data"&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="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&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="s2"&gt;"No usable snapshot returned"&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;rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code checks both the HTTP outcome and the business code, normalizing the latter because error codes can be strings or integers. It deliberately performs no automatic retry. A production caller should honor the documented rate-limit wait, use bounded retries for transient failures, and stop on permission failures until the request or account changes. Connection exceptions and timeouts also need handling at that caller boundary.&lt;/p&gt;

&lt;p&gt;A successful response is still only one piece of evidence. Inspect the requested symbol, its timestamp, and the fields your application uses. The ticker documentation makes fields such as bid and ask conditional on availability; a generic response schema is not proof that every forex instrument returns them. Do not invent missing values simply to make a comparison screen complete.&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%2Fhq15ewxrdlgx6axqzvzc.png" 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%2Fhq15ewxrdlgx6axqzvzc.png" alt="A conceptual error handling diagram routes rate limits, permission errors and transient transport failures differently." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Proposed failure handling. Different failure causes require different next actions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose paid access from requirements rather than the largest number
&lt;/h2&gt;

&lt;p&gt;The public monthly pricing lists Starter at $99 per month, Professional at $299, and Enterprise at $899. Their documented API limits are 60, 600, and 1,800 requests per minute respectively, with one, three, and ten WebSocket connections. These figures describe monthly plans as checked on November 13, 2026; other billing periods and later pricing changes should be verified separately.&lt;/p&gt;

&lt;p&gt;Those numbers are useful only after you know the binding constraint. A higher request allowance may solve polling pressure but not an unsupported field or an unsuitable data license. Likewise, a larger historical allowance does not by itself establish the coverage or quality needed by a particular strategy. Ask the provider to resolve any market-specific ambiguity before treating a general pricing table as a production promise.&lt;/p&gt;

&lt;p&gt;Public display, redistribution, storage, and commercial use also require their own rights check. The technical access facts above do not establish those permissions. Obtain the terms applicable to your intended use rather than inferring them from a working key or a paid price.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision a free evaluation should produce
&lt;/h2&gt;

&lt;p&gt;Finish the trial with a short acceptance record: which pairs worked, which data and date ranges were available, the workload you tested, failures observed, the expected paid configuration, and unresolved usage rights. Keep documented limits separate from measurements you actually made.&lt;/p&gt;

&lt;p&gt;A free forex API is enough for an evaluation when it lets you answer those questions within its time and access limits. It is enough for ongoing production only when the continuing plan, workload, market coverage, and rights all support that use. If one remains unverified, the honest result is an open requirement, not a claim that the integration is ready.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>Choose a Stock API by Verifying Instrument Identity Before Comparing Prices</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Thu, 12 Nov 2026 00:02:28 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-verifying-instrument-identity-before-comparing-prices-1idl</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-verifying-instrument-identity-before-comparing-prices-1idl</guid>
      <description>&lt;p&gt;A stock API can return a valid price for the wrong instrument if an integration treats a ticker as a universal identifier. Compare providers on their ability to resolve the requested instrument, retain exchange and currency context, and reject ambiguous mappings before scoring speed or cost. A small identity contract and deliberately conflicting test fixtures make these differences visible without pretending that a successful request proves historical coverage or entitlement.&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%2Fun6i5kqwmam6uxkc6nci.png" 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%2Fun6i5kqwmam6uxkc6nci.png" alt="Cover: instrument identity acceptance" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve the instrument before trusting its price
&lt;/h2&gt;

&lt;p&gt;For a multi-market watchlist, the first acceptance question is whether each response belongs to the security the user selected. A ticker string alone is not a complete identity contract. A market suffix can help distinguish a provider's symbol namespace, but it should not be silently treated as a globally stable instrument identifier.&lt;/p&gt;

&lt;p&gt;Store the provider's exact symbol alongside the expected market or exchange and currency. Keep a separate application identifier so that switching providers does not require rewriting every watchlist, alert and historical row. This is an integration design recommendation, not a claim that a particular provider guarantees permanent identifiers.&lt;/p&gt;

&lt;p&gt;The difference matters when a mapping is incomplete. A request that returns HTTP success can still be unusable if the integration cannot establish which instrument it received. A blank currency must remain unknown; filling it with a convenient default turns missing evidence into a false assertion.&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%2Ff0udoehcromwc8g111hi.png" 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%2Ff0udoehcromwc8g111hi.png" alt="Suggested identity contract" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make providers answer the same identity questions
&lt;/h2&gt;

&lt;p&gt;Start with a small, documented basket representing the instruments your application actually supports. Include ordinary symbols, similarly named instruments across markets, malformed requests, and known historical changes if those changes matter to your product. Do not manufacture a claim that a real ticker changed merely to populate the test.&lt;/p&gt;

&lt;p&gt;For every candidate, record its exact request symbol, returned symbol, exchange field, currency field, and behavior when the symbol is ambiguous or unsupported. Distinguish an explicit error from an empty result and from a successful response with missing metadata. All three require different handling in your adapter.&lt;/p&gt;

&lt;p&gt;TickDB's stock-information documentation gives a concrete example of this contract. It lists symbols such as AAPL.US and 700.HK, describes exchange and currency fields, and documents an optional product type for disambiguation after an AMBIGUOUS_SYMBOL error. It also states that conditional fields may be omitted when data is unavailable. These are documentation statements checked on November 12, 2026, not results from a live account test.&lt;/p&gt;

&lt;p&gt;Those details provide a starting point for an adapter. They do not establish that the endpoint supplies historical identifier mappings, delisted universes, or point-in-time metadata. If a backtest depends on those features, obtain separate evidence rather than inferring them from a current stock-information response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a contract that fails visibly
&lt;/h2&gt;

&lt;p&gt;The following Python example validates already-normalized metadata. Its field names belong to the example adapter, not to a claimed universal provider schema. Map each provider's documented fields explicitly before calling the validator. In particular, normalize exchange codes using a reviewed mapping rather than equating a market suffix with an exchange.&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;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider_symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;identity_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"provider_symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider_symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&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;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wanted&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&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;field&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;actual&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{field}: missing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{field}: expected {wanted}, got {actual}"&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;errors&lt;/span&gt;

&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Expected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"EXAMPLE.US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"EXAMPLE_EXCHANGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"provider_symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EXAMPLE.US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EXAMPLE_EXCHANGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;identity_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;good&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="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;identity_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"HKD"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;identity_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;identity_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"provider_symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EXAMPLE.HK"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"4 synthetic identity checks passed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixture names are deliberately synthetic. The code checks the validator, not a supplier's accuracy. For a live acceptance run, preserve the raw response and the normalization version so a rejected row can be traced back to its source. Quarantine mismatches instead of silently attaching them to an existing chart.&lt;/p&gt;

&lt;p&gt;Exact comparison is useful only after normalization is defined. If one provider names an exchange differently, that is not automatically incorrect data. Review the mapping against documentation, version it, and then compare normalized values. Do not remove punctuation or suffixes broadly just to make a test pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate current identity from historical continuity
&lt;/h2&gt;

&lt;p&gt;A current symbol list answers what the service recognizes now. A historical research workflow may need to know what was recognized on a past date, which symbol applied then, and whether the data universe contains instruments that later disappeared. These are different requirements.&lt;/p&gt;

&lt;p&gt;For a current-price widget, explicit symbol resolution and missing-field handling may be sufficient. For a historical portfolio simulation, make dated mappings and universe coverage contractual acceptance items. Ask for a documented example spanning a real change and retain the evidence. If that evidence is unavailable, mark the requirement unresolved; do not award a passing score simply because today's quote works.&lt;/p&gt;

&lt;p&gt;The same boundary applies when evaluating TickDB: its documented stock-information fields support a current metadata inspection, while historical identity behavior must be verified independently for the intended use. This avoids turning a narrow endpoint description into a broad product promise.&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%2Fgdw8e9iggwzj7yketpwv.png" 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%2Fgdw8e9iggwzj7yketpwv.png" alt="Evidence states for provider acceptance" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare operational cost only after the identity gate
&lt;/h2&gt;

&lt;p&gt;Once the basket resolves correctly, compare the cost of maintaining that mapping. Can the application refresh metadata within documented limits? Are unsupported instruments distinguishable from temporarily missing data? Can the team explain and reproduce a mapping change? Record these outcomes separately from response latency.&lt;/p&gt;

&lt;p&gt;A useful comparison sheet contains three states for every requirement: evidenced pass, observed failure, and unresolved. An unresolved mandatory identity requirement should block acceptance for the affected workflow. Optional display metadata can remain a documented limitation if the interface handles it honestly.&lt;/p&gt;

&lt;p&gt;This method does not rank providers universally. It tells you which candidate can satisfy your application's identity contract with evidence you can maintain. A fast quote becomes useful only after the application knows what it is a quote for.&lt;/p&gt;

&lt;p&gt;Source reviewed: TickDB Docs, Stock Information, November 12, 2026. Code examples use synthetic data and are not supplier benchmarks.&lt;/p&gt;

</description>
      <category>api</category>
    </item>
    <item>
      <title>Choose a Stock API by Testing Disconnect Recovery, Not Just Streaming Speed</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Tue, 10 Nov 2026 23:26:27 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-testing-disconnect-recovery-not-just-streaming-speed-5cbh</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-testing-disconnect-recovery-not-just-streaming-speed-5cbh</guid>
      <description>&lt;p&gt;A stock API can reconnect quickly while leaving an application with incomplete subscriptions or an unexplained gap in event history. Choose it by testing when data becomes usable again, not merely when the socket opens. A fresh snapshot can restore current state; only a documented replay or backfill mechanism can establish the required event continuity. The acceptance test below separates those outcomes and includes a small, locally tested recovery gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the comparison at the point where the connection breaks
&lt;/h2&gt;

&lt;p&gt;A fast first message is useful, but it answers a narrow question. After a network interruption, the same client may connect successfully, receive a heartbeat, and still be missing one of the subscriptions its application needs. Alternatively, all subscriptions may return, yet trades that happened during the interruption remain absent.&lt;/p&gt;

&lt;p&gt;These are different failures. A price display may recover once it has a valid current quote for each required instrument. A trade-derived calculation may need every event in the missing interval, with duplicates handled correctly. Do not give both applications the same definition of “recovered.”&lt;/p&gt;

&lt;p&gt;For an API selection trial, write down two acceptance contracts before testing. The &lt;strong&gt;state contract&lt;/strong&gt; specifies which instruments and fields must be restored, how freshness is judged, and whether partial availability is allowed. The &lt;strong&gt;history contract&lt;/strong&gt; specifies whether missing events must be recovered, which identifier orders them, and how the application handles an interval it cannot reconstruct. An API can pass the first contract and fail the second without contradicting its documentation.&lt;/p&gt;

&lt;p&gt;This is an evaluation method, not a ranking or a live vendor benchmark. The code below was tested with synthetic events; no provider recovery timings were measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give recovery more than one finish line
&lt;/h2&gt;

&lt;p&gt;Record the failure injection, the moment the client detects it, the new connection opening, subscription confirmation, reconciliation completion, and the point at which the application releases usable data. Use a monotonic clock for these local durations. Keep provider event timestamps separately; subtracting unrelated clocks can turn clock skew into an apparent latency result.&lt;/p&gt;

&lt;p&gt;Measure at least three intervals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection delay:&lt;/strong&gt; failure injection to the client marking the session unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport recovery:&lt;/strong&gt; failure injection to the replacement connection opening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable-data recovery:&lt;/strong&gt; failure injection to the application meeting its state and, where required, history contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you also report recovery after the network is restored, label that start point separately. A forced ten-second outage should not be compared with a one-second outage as though they were the same test. Runs that never recover within the observation window remain failures or censored observations, not missing rows discarded from a latency average.&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%2Forag7cn3dewlbi1wi0f3.png" 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%2Forag7cn3dewlbi1wi0f3.png" alt="Five recovery gates from disconnected to usable" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. An illustrative client recovery model. Opening a connection is only one gate; this is not a vendor protocol state machine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A successful subscription response is evidence about a subscription request, not proof that its data has arrived. Keep the desired subscription set outside the socket object, then compare it with the evidence available on the new connection. Track this per channel and instrument when the application depends on several streams. If one subscription is rejected, leave that portion unavailable rather than promoting the whole session to healthy.&lt;/p&gt;

&lt;p&gt;A heartbeat belongs to the transport check. TickDB's Ping documentation, for example, describes a ping/pong exchange that checks connection availability. Its timestamp is not a quote timestamp, and receiving a pong does not establish that a requested stock subscription is active or that a missed trade has been recovered.&lt;/p&gt;

&lt;p&gt;The same distinction prevents a misleading timeout rule: a quiet instrument is not automatically a broken connection. Freshness policy must respect the market session, instrument activity, and the meaning of the channel. A newly received snapshot can legitimately contain an old last-trade timestamp. If the application needs a recent trade, that instrument may remain unusable even though transport and snapshot delivery are working correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask exactly what happens to the missing interval
&lt;/h2&gt;

&lt;p&gt;Suppose the client receives event E1, disconnects while E2 and E3 occur, and reconnects before E4. A current snapshot may provide the state after E4. It does not reveal the path through E2 and E3. That distinction matters for volume accumulation, event-driven signals, and audit records, even when the displayed price looks correct.&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%2F0lc15097aacc8y3ihug3.png" 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%2F0lc15097aacc8y3ihug3.png" alt="Snapshot repairs current state while replay repairs missing history" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Illustrative recovery semantics. Replay can repair event history only when the source supports the required interval and ordering; a snapshot alone cannot prove continuity.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For snapshot recovery, ask how the client recognizes the complete required state. Is there a completion marker, a stable expected symbol set, or an explicitly documented reconciliation procedure? A batch-size limit is not a completion signal. Nor is “no messages arrived for a moment.”&lt;/p&gt;

&lt;p&gt;TickDB's US Full-Market Ticker documentation provides a concrete example of why these questions matter: subscribing to US_Stock sends a batched initial snapshot, then changed tickers, with at most 500 ticker messages per array. Its Best Practices page says to resend subscriptions after reconnect and that a full-market subscription receives another initial snapshot. The reviewed pages do not establish a replay cursor or an explicit snapshot-end marker. That is a documentation boundary to resolve for the intended application, not proof that those features cannot exist.&lt;/p&gt;

&lt;p&gt;For an event-history contract, ask for the actual replay boundary: what cursor or event identifier can be resumed, how long events are retained, whether limits apply per instrument or connection, and what happens when the requested point is too old. A timestamp alone may not uniquely identify events. Do not invent deduplication using price and time unless the feed guarantees that combination is unique.&lt;/p&gt;

&lt;p&gt;Also resolve the join between backfill and the resumed live stream. Buffering new events while fetching history is useful only when a documented ordering or watermark lets the client merge them correctly. Without that contract, a gap or a duplicate at the seam can survive a successful backfill request. Mark continuity as unverified instead of claiming that two successful requests prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the gate independently of the network client
&lt;/h2&gt;

&lt;p&gt;The following standard-library Python example demonstrates an application-level gate for a finite set of required instruments. It is deliberately not a WebSocket client or an implementation of replay. The adapter must supply subscription evidence and an explicit reconciliation decision. In production, those decisions need to come from the provider's documented semantics and the application's acceptance policy.&lt;/p&gt;

&lt;p&gt;The generation number rejects callbacks left over from an older connection. Every reconnect starts with empty evidence, and a heartbeat cannot make the gate usable. The example uses local receipt age only; provider event-age validation belongs in the adapter before accepting a record.&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;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecoveryGate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;generation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;subscribed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;reconciled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;dict&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;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconciled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generation&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&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="kc"&gt;None&lt;/span&gt;&lt;span class="p"&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;generation&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&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="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"disconnect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconciled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"subscription_ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"subscription_error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconciled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"record"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&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;symbol&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&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="n"&gt;now&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"reconciled"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconciled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="c1"&gt;# Heartbeats deliberately do not advance recovery.&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconciled&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
                    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_age&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecoveryGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"AAA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"BBB"&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="n"&gt;max_age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"disconnect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"subscription_ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Stale callback.&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribed&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"subscription_ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AAA"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"reconciled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# BBB is still missing.&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"record"&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="s2"&gt;"BBB"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usable&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="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# AAA has aged out.&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"subscription_error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Synthetic recovery-gate checks passed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a Python 3.9-or-newer script. AAA and BBB are synthetic instrument labels, and five seconds is an illustrative receipt-age threshold, not a vendor SLA. The expected output is “Synthetic recovery-gate checks passed.”&lt;/p&gt;

&lt;p&gt;The small gate assumes one aggregate subscription decision and serialized callbacks. A real adapter may need per-channel decisions, locking, bounded buffers, and schema checks. For an event-dependent application, do not emit “reconciled” until the missing interval and the live/backfill seam satisfy the history contract. For a display, reconciliation may be a narrower current-state decision. Keep those policies explicit rather than embedding them in the reconnect handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a failure-injection trial that can actually reject a candidate
&lt;/h2&gt;

&lt;p&gt;Use the same required instruments, market session, channel mix, account permissions, client region, and outage durations for each candidate. Record the client version and subscription request so a change in the harness does not masquerade as a provider improvement. Test only connections you control, within service limits; do not generate disruptive load against a provider.&lt;/p&gt;

&lt;p&gt;Start with four cases, because each challenges a different claim:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clean disconnect.&lt;/strong&gt; Close the socket, reconnect, and confirm that the desired subscriptions are restored. The pass condition is usable data under the declared contract, not a successful open callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent interruption.&lt;/strong&gt; In a test harness or permitted local network proxy, stop delivery without a clean close. Verify that the client eventually detects the loss and stops presenting the old session as usable. Keep a fixture that delivers heartbeats but withholds records to test the separate data gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial recovery.&lt;/strong&gt; Reject one requested subscription or withhold one required instrument in the fixture. The application must report the missing coverage and remain wholly or partially unavailable according to its predeclared policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery race.&lt;/strong&gt; Deliver a delayed callback from the previous connection, repeat an event, and interrupt again during reconciliation. Confirm that old-generation callbacks cannot update the new session and that any event deduplication uses a documented identity rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use local fixtures to prove that the client handles these conditions; use permitted live trials to learn how a candidate behaves. A synthetic rejection is evidence about the harness and client, not evidence that a provider produces that rejection. Record those two evidence types separately.&lt;/p&gt;

&lt;p&gt;Include a configuration failure as a separate check. For example, TickDB documents mandatory permessage-deflate negotiation for full-market subscriptions, with error 2008 when it is missing, and permission error 4005 when the key lacks access. Retrying the same invalid configuration does not demonstrate resilience. The trial should verify that the client exposes an actionable error instead of looping indefinitely. Follow the selected provider's documented retry policy and stop conditions rather than applying one universal schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the selection from acceptance results, then compare speed
&lt;/h2&gt;

&lt;p&gt;For each candidate, keep one record per injected failure: scenario, session and instruments, detection delay, transport recovery, usable-data recovery, missing subscriptions, unresolved history interval, and final outcome. Label every result as documentation-based, synthetic-test evidence, live observation, or unknown. Report the observation window and failures alongside any latency summary. With a small sample, show the individual runs instead of presenting a percentile as a stable service property.&lt;/p&gt;

&lt;p&gt;A current-state application can accept snapshot recovery if it meets its coverage and freshness requirements. An event-dependent application needs a credible way to reconstruct the missing interval, or an explicitly accepted degraded mode that stops the affected calculation. Neither decision should be hidden inside a green connection indicator.&lt;/p&gt;

&lt;p&gt;Reject a candidate that cannot satisfy the application's recovery contract, even if its first message arrives sooner. Among candidates that pass, compare usable-data recovery time, operating effort, and the rest of the commercial requirements. That ordering makes streaming speed useful without allowing it to conceal a broken recovery path.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Documentation basis: TickDB Best Practices, Ping, and US Full-Market Ticker, reviewed for this article. Product statements are documentation-based; the executable example uses synthetic data only.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>websocket</category>
      <category>python</category>
    </item>
    <item>
      <title>Choose a US Stocks API That Keeps Extended Hours Data Distinct</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Mon, 09 Nov 2026 23:23:33 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-us-stocks-api-that-keeps-extended-hours-data-distinct-58h6</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-us-stocks-api-that-keeps-extended-hours-data-distinct-58h6</guid>
      <description>&lt;p&gt;A US stocks API can return a valid price while leaving an extended-hours screen misleading. For a product that shows pre-market, post-market or overnight activity, choose a feed whose coverage and session semantics you can verify, then keep each observation tied to its session and timestamp. TickDB documents separate extended-session snapshot objects; Alpaca documents separate stock feeds. Those are different integration choices, and neither removes the need to test missing data and session transitions.&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%2F7td62gw5ne9l6zomynqs.png" 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%2F7td62gw5ne9l6zomynqs.png" alt="Four separate panels represent pre-market, regular, post-market and overnight data." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suppose an overnight price panel still displays the previous regular-session trade. The number may be accurate for that earlier observation, but the panel now makes a different promise. A successful request, a familiar ticker and a recent HTTP response do not establish that the price belongs to the session the user selected.&lt;/p&gt;

&lt;p&gt;For this kind of product, the deciding question is whether the API lets you preserve that distinction all the way from the source to the screen. A regular-hours research tool can reasonably have a narrower requirement. An extended-hours monitor needs evidence for the additional sessions, symbols and uses it promises to support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feed choice and session choice answer different questions
&lt;/h2&gt;

&lt;p&gt;A feed identifies the data source or coverage you requested. A session identifies the trading period represented by an observation. Keep both in your evaluation notes and in your application's normalized records. A label such as “US stocks” is too broad to settle either question.&lt;/p&gt;

&lt;p&gt;Alpaca's Real-time Stock Data documentation lists SIP, IEX, delayed SIP, BOATS and a derived overnight feed separately. It also says a subscription without access to a requested feed will fail during authentication. For an overnight use case, ask which of these sources actually serves the required observation and whether the production account can access it. Do not assume that a regular-session connection proves overnight entitlement.&lt;/p&gt;

&lt;p&gt;TickDB takes a different shape in its documented REST snapshot: pre-market, post-market and overnight observations can appear as separate objects. Each has its own price and timestamp, and the documentation makes their presence conditional on the product and data availability. That is useful when a screen must show sessions separately, but a documented optional object is not evidence that every symbol will always have it.&lt;/p&gt;

&lt;p&gt;These examples illustrate interface choices, not a vendor ranking. A sample payload does not prove consolidated coverage, quote completeness, historical availability or redistribution permission. Resolve those requirements for the exact feed, account and application before a production commitment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A price should retain the session it came from
&lt;/h2&gt;

&lt;p&gt;During a trial, inspect the original response before reducing it to a single price field. If a requested session is unavailable, preserve that state. Falling back to another session may make a card look populated while silently changing its meaning.&lt;/p&gt;

&lt;p&gt;For a TickDB overnight snapshot adapter, the proposed rule is straightforward: use the documented overnight object when it exists, along with that object's timestamp. If it is absent, report unavailable rather than borrowing the top-level price. The same discipline applies to pre-market and post-market objects. Do not label the top-level field as a regular-session close without separate evidence for that meaning.&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%2Fyiq6d1bp492nmk8bxy6j.png" 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%2Fyiq6d1bp492nmk8bxy6j.png" alt="A proposed TickDB snapshot adapter uses an overnight object's price and timestamp when present and returns unavailable when it is missing." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Proposed session-selection behavior. Optional fields remain optional; this is an adapter design, not a live response.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Preserve the original symbol, the requested source, the session label, the source timestamp and your own receipt time. The receipt time says when your application received the message. It does not, by itself, establish when the underlying market event occurred. Likewise, milliseconds or nanoseconds describe timestamp precision, not delivery speed.&lt;/p&gt;

&lt;p&gt;Avoid using a generic “last updated” label for both clocks. An observation can be valid but old because no qualifying activity occurred, because the market is closed, or because the application has lost updates. Investigate those cases separately rather than treating every old timestamp as proof of a broken provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a calendar policy instead of a fixed UTC offset
&lt;/h2&gt;

&lt;p&gt;Session classification needs a date, a market calendar and the source's documented rules. NYSE lists a normal core session of 9:30 a.m. to 4:00 p.m. Eastern Time, and its calendar includes holidays and early closes. Different venues also list different extended-hours schedules. That is enough to rule out one universal hard-coded UTC window for every US equity feed.&lt;/p&gt;

&lt;p&gt;Use a timezone-aware calendar for the relevant venue or feed, including daylight-saving changes and exceptional days. For overnight data, also verify which trading date the provider assigns to events that cross midnight. A timestamp's local calendar date and the provider's trading date need not serve the same purpose.&lt;/p&gt;

&lt;p&gt;The following small validator deliberately accepts a session window from the caller. It does not generate an exchange calendar or guess a session from a timestamp. Your provider-specific adapter must establish the normalized fields from the response and the configured feed; it must not invent a session label merely to make a record pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the session requirement into an executable test
&lt;/h2&gt;

&lt;p&gt;This Python example checks that an observation matches the requested symbol, feed and session, has a usable price, and falls inside a supplied half-open time window. The field names are an application schema, not a vendor API schema. The synthetic fixture verifies the local logic only; it does not query a market or measure a supplier.&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;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InvalidOperation&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&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="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&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="n"&gt;end_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Windows are [start, end), supplied by your calendar policy.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&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="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="kd"&gt;type&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="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
            &lt;span class="ow"&gt;and&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"invalid test window"&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;record&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"unavailable"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"invalid_record"&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"symbol"&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="s2"&gt;"feed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&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;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"scope_mismatch"&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;record&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="s2"&gt;"event_ms"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"invalid_timestamp"&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;start_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;ts&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"outside_session"&lt;/span&gt;
    &lt;span class="k"&gt;try&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="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"price"&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;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InvalidOperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"invalid_price"&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;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="s2"&gt;"invalid_price"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"accepted"&lt;/span&gt;


&lt;span class="c1"&gt;# Synthetic unit-test values, not a real trading session or price.&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&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="s2"&gt;"TEST.US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"fixture-feed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"overnight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_ms&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_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;dict&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="s2"&gt;"TEST.US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"fixture-feed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"overnight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1500&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="s2"&gt;"10.25"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;accept_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"accepted"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;accept_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"unavailable"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;accept_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"outside_session"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;accept_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"regular"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"scope_mismatch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The accepted result means only that the record passed these checks. It says nothing about whether all trades were delivered or whether a quoted price is executable. The example also leaves freshness thresholds out intentionally: an appropriate threshold depends on the symbol, market state and product promise.&lt;/p&gt;

&lt;p&gt;At ingestion, reject or quarantine records with an unknown source or session instead of filling those fields from the screen's current selection. Preserve the original payload alongside a rejected record so that a mapping error can be distinguished from missing source data. If the upstream format has finer timestamp precision than this millisecond example, retain the original precision where the application needs it.&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%2Fr5vg6z8k8nxyppo55xme.png" 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%2Fr5vg6z8k8nxyppo55xme.png" alt="Three proposed tests cover a session change, a missing session object and an event outside the requested time window." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Boundary tests target misleading output. Passing a sample does not establish complete market coverage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A useful trial crosses a session boundary. Capture responses on both sides and check that the interface does not carry an earlier observation into the new session without a clear label. Then remove the requested session object from a saved fixture and confirm that the application shows unavailable. Finally, inject an event from outside the requested window and verify that the mismatch is reported rather than silently accepted.&lt;/p&gt;

&lt;p&gt;Run these tests for the symbols and sessions the product actually needs. If a quiet symbol produces no observation, record that outcome and investigate its cause; do not turn the absence into an invented zero price or an unsupported claim that the feed lacks the symbol. A finite trial can expose failures, but it cannot prove complete future coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose according to the product you will ship
&lt;/h2&gt;

&lt;p&gt;For a regular-hours tool, a clearly bounded regular-session feed may be sufficient. For an extended-hours display, prefer an interface and entitlement package that let you identify the requested session, preserve its timestamp and represent missing observations honestly. For a cross-feed comparison, align source coverage and event types before treating price differences as errors.&lt;/p&gt;

&lt;p&gt;A technical trial and a usage review answer different questions. Confirm the exact production access and permitted display or redistribution use with the provider; this article does not determine contractual rights. Keep unresolved requirements visible in the selection decision instead of interpreting an API success response as approval for every use.&lt;/p&gt;

&lt;p&gt;The final acceptance condition is concrete: when the requested session changes or becomes unavailable, the product must still tell the truth about the price it displays. That is a stronger basis for choosing an extended-hours US stocks API than a demonstration that returns one plausible number.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Documentation checked November 10, 2026: TickDB REST Market Snapshot and WebSocket Ticker; Alpaca Real-time Stock Data; NYSE Holidays and Trading Hours. The code uses synthetic test fixtures, not live market results.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article is prepared for TickDB's publication channels and discusses TickDB as one candidate. It is not an independent performance benchmark or investment advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>Choose a Stock API by Testing Requirements Before Comparing Price</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Mon, 09 Nov 2026 02:08:31 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-testing-requirements-before-comparing-price-4mh5</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/choose-a-stock-api-by-testing-requirements-before-comparing-price-4mh5</guid>
      <description>&lt;p&gt;A stock API can return valid JSON and still be unsuitable for your application. Choose candidates that meet your required data coverage, permitted use and operating capacity, then compare their total cost under the same workload. Alpaca’s documented IEX and SIP feed distinction shows why matching ticker symbols is not enough. A small acceptance test, with unresolved requirements kept visible, produces a more defensible shortlist than a feature count.&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%2Ffuaizsehi9w1y5jskule.png" 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%2Ffuaizsehi9w1y5jskule.png" alt="Conceptual cover showing stock API selection through successive filters" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For an internal end-of-day research tool, prioritize the historical universe, corporate-action treatment and reproducibility. For a customer-facing price display, prioritize permission for that use, the required quote coverage and session behavior, then test delivery and recovery under load. These applications can reasonably select different providers even when their developers start with the same search: “stock API.”&lt;/p&gt;

&lt;p&gt;The useful comparison unit is a particular feed and plan used for a particular job. A provider’s broad product page is only the beginning of that comparison. This article develops an acceptance method, not a measured ranking of providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the application into a data contract
&lt;/h2&gt;

&lt;p&gt;Start with one sentence precise enough to fail a candidate. For example: “Display last-trade prices for a defined list of US stocks during regular trading hours, with an explicit stale state, for external customers.” This is an illustrative requirement, not a statement about any provider’s license.&lt;/p&gt;

&lt;p&gt;The words in that sentence have consequences. A last trade is different from a best bid or ask. Regular hours exclude other sessions unless you add them. External customers create a different permission question from a private prototype. An explicit stale state means the application must preserve data time and detect failures even when its own server continues answering requests.&lt;/p&gt;

&lt;p&gt;Write down the following evidence before requesting a quote. Keep the requirements specific to the application; unnecessary requirements can eliminate a suitable, simpler service.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Evidence to obtain&lt;/th&gt;
&lt;th&gt;Acceptance decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instruments and price meaning&lt;/td&gt;
&lt;td&gt;Symbol list, venues or feed scope, trade versus quote, session definitions&lt;/td&gt;
&lt;td&gt;Required instruments and semantics match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intended use&lt;/td&gt;
&lt;td&gt;Written terms or confirmation covering audience, display, storage and redistribution where relevant&lt;/td&gt;
&lt;td&gt;The actual use is covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;Date range, delisted instruments where needed, adjustment and correction policy&lt;/td&gt;
&lt;td&gt;Research can be reproduced within the required scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating capacity&lt;/td&gt;
&lt;td&gt;Plan limits and a trial using expected subscriptions, bursts and recovery traffic&lt;/td&gt;
&lt;td&gt;Workload fits with agreed headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure behavior&lt;/td&gt;
&lt;td&gt;Timeout, rejection, stale-data and reconnect observations&lt;/td&gt;
&lt;td&gt;Failures are visible and recovery is acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat a missing answer as unresolved. It is not proof that a provider lacks a capability, but it is also not a passing result. Keep permission questions with the applicable agreement and obtain confirmation from the provider; an API response alone cannot establish permitted use.&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%2Fwuy65d3ejdp2bivbf3fe.png" 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%2Fwuy65d3ejdp2bivbf3fe.png" alt="Two stage selection diagram separating hard requirements from tradeoffs" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Clear mandatory requirements before comparing cost and integration effort. This is a suggested evaluation process, not a provider ranking.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the feed before comparing the price
&lt;/h2&gt;

&lt;p&gt;Alpaca’s Market Data FAQ distinguishes an IEX feed from a SIP feed and exposes a feed choice for historical queries. That distinction matters even when both requests use the same stock symbol: a feed limited to one venue and a consolidated feed do not describe the same set of market events. A price or volume difference between them is not, by itself, evidence of an error. These statements describe the documentation reviewed on November 9, 2026, not a new market-data test.&lt;/p&gt;

&lt;p&gt;Specify the required feed explicitly in a trial and record the account entitlement used. Otherwise, a comparison can quietly change when a subscription changes or a default selects a different feed. Compare identical sessions, event types, bar boundaries and adjustment settings wherever those controls exist. If they cannot be aligned, document the difference instead of reporting an accuracy score.&lt;/p&gt;

&lt;p&gt;TickDB is a relevant candidate for applications evaluating US, Hong Kong and mainland China stock data: its Ticker Snapshot documentation lists examples from those markets. The same page describes a maximum of 50 symbols per snapshot request and conditional availability for fields beyond symbol, last price and timestamp. Those are useful inputs to a trial. They do not establish the feed’s suitability for every display requirement, nor do they confirm redistribution rights or measured reliability.&lt;/p&gt;

&lt;p&gt;For a trade-price display, check the required last-price fields first. If your application also needs bid and ask, make their availability for your instruments a separate acceptance condition. A sample response containing a field is weaker evidence than a defined field contract plus a trial across the sessions you actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make unknown requirements visible in code
&lt;/h2&gt;

&lt;p&gt;A weighted score can hide a fatal problem. A candidate with attractive cost and excellent documentation still cannot pass a mandatory requirement whose status is unknown. The following Python example implements that distinction before any ranking takes place.&lt;/p&gt;

&lt;p&gt;The input is an assessment record created by your team after reviewing evidence. The names and values below are fictional; the function does not inspect contracts, query providers or measure data quality. It requires an explicit &lt;code&gt;True&lt;/code&gt;, &lt;code&gt;False&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt; for each mandatory criterion. Missing entries remain unresolved.&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;REQUIRED&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;coverage&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;permitted_use&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;capacity&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;recovery&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;assess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&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;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checks&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;name&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;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: use True, False or None&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;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;reject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eligible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="n"&gt;candidates&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;Candidate A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Candidate B&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="o"&gt;**&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permitted_use&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Candidate C&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="o"&gt;**&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;assess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A becomes eligible for comparison; B stays on hold until the permission question is resolved; C fails the current capacity requirement. A failed requirement takes precedence over an unknown one, while the returned record retains both lists. “Reject” means unsuitable under the assessed requirements and plan, not unsuitable for every customer.&lt;/p&gt;

&lt;p&gt;Attach an evidence reference, reviewer and review date to each real assessment in your own record system. The compact code deliberately omits that storage layer. Rerun the decision when requirements or plans change; a saved Boolean should not outlive the evidence that justified it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a trial that can reveal a bad fit
&lt;/h2&gt;

&lt;p&gt;Use the same instrument set, time window, receiving environment and requested data meaning for each candidate. Include actively traded instruments, less active instruments and any relevant session transitions. A quiet stock’s old last-trade timestamp may simply mean that no new trade occurred. Measure transport health separately from event age so that you do not label all inactivity as an outage.&lt;/p&gt;

&lt;p&gt;Record request start and completion times, HTTP status, requested and returned symbols, source timestamps and parse failures. For streams, also record subscription acknowledgements, disconnects and the first valid update after recovery. Preserve enough raw samples to investigate a discrepancy without retaining credentials. Use a monotonic clock for elapsed durations and a synchronized wall clock when comparing data timestamps with receive time.&lt;/p&gt;

&lt;p&gt;Calculate request success as successful requests divided by attempted requests, and report missing requested symbols separately. A successful HTTP response can still omit an instrument. Define a freshness threshold from the application’s needs before the trial, report the eligible observation count, and separate regular hours from closed sessions. Receive time minus source time includes more than network delay and depends on timestamp semantics; do not label it end-to-end feed latency without that qualification.&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%2Fwb3x3jndb9kovf95fq3v.png" 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%2Fwb3x3jndb9kovf95fq3v.png" alt="Trial loop with a shared workload, provider adapters, evidence and decision" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Apply the same workload and preserve evidence before accepting a candidate. The diagram represents a proposed test, not measured results.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Test a controlled interruption as well as normal delivery. After reconnection, determine whether the service returns only a current snapshot, resumes updates or offers a documented way to recover the missing interval. A live display may tolerate an explicit stale state followed by a new snapshot. A system that must retain every event needs a stronger recovery contract. A short successful trial cannot prove a long-term availability commitment; assess the agreement and longer observation window separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare total cost among eligible candidates
&lt;/h2&gt;

&lt;p&gt;Once the mandatory requirements pass, compare costs over the same period, currency and workload. Include the subscription, applicable data permissions or fees, extra usage, storage, network egress, implementation and ongoing operation. Record each item as quoted, included, estimated or unresolved. No provider prices or permission grants are assumed here.&lt;/p&gt;

&lt;p&gt;Keep one-time migration effort separate from recurring spend, then evaluate both over a stated planning horizon. For example, an inexpensive subscription can require extra work if the application must compensate for a missing historical workflow. Conversely, a richer feed can be unnecessary expense when the application only requires a permitted delayed display. Neither conclusion follows from the headline price alone.&lt;/p&gt;

&lt;p&gt;TickDB should pass the same assessment as every other candidate: confirm the intended usage and plan capacity, test the required instruments and fields, and record the unresolved items before comparing cost. Documentation can justify a shortlist; it cannot substitute for a workload trial or a permission decision.&lt;/p&gt;

&lt;p&gt;The final selection record should say which requirement each candidate passed, what remains unknown, the cost assumptions and the conditions that would trigger reevaluation. If only one candidate passes, you have a conditional choice. If none pass, revise the plan, narrow the application requirement where acceptable, or keep searching. Lowering a score threshold does not resolve a failed requirement.&lt;/p&gt;

&lt;p&gt;Source basis: Alpaca Market Data FAQ and TickDB Ticker Snapshot documentation, reviewed November 9, 2026. Product statements above are documentation-based. The assessment example uses fictional inputs; no live provider benchmark or contract review was performed for this article.&lt;/p&gt;

&lt;p&gt;Disclosure: Prepared as technical content for the TickDB promotion workflow. This is not an independent provider benchmark.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>testing</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Stock API Watchlists Can Share Requests Without Hiding Stale Prices</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:23:28 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-watchlists-can-share-requests-without-hiding-stale-prices-54m5</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-watchlists-can-share-requests-without-hiding-stale-prices-54m5</guid>
      <description>&lt;h1&gt;
  
  
  Stock API Watchlists Can Share Requests Without Hiding Stale Prices
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; A stock watchlist can exhaust an API budget even when every user follows the same symbols. Where data permissions allow it, batching those symbols and sharing retrieval across viewers reduces duplicate upstream requests while preserving each record's timestamp. In a hypothetical case with 20 identical 120-symbol watchlists, batches of 50, and a five-second polling interval, independent polling needs 720 requests per minute; one shared collector needs 36, before retries and other traffic.&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%2Fmzlz91c1tnwdtd27zela.png" 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%2Fmzlz91c1tnwdtd27zela.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A stock API lets software request or receive stock-related data. In a watchlist, the application uses that interface to populate rows with prices and other available fields. The same data can support a portfolio overview or a research dashboard, but the API does not decide how often the screen should refresh or how the application should label an old observation.&lt;/p&gt;

&lt;p&gt;Those decisions become visible as an app grows. If every open browser starts its own polling loop, upstream requests grow with the audience even when the requested symbols are identical. A shared collector can remove that duplication. It must also preserve data age: serving an old record quickly does not make the record current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count upstream requests before choosing a refresh interval
&lt;/h2&gt;

&lt;p&gt;For polling, count the batches needed for one refresh cycle, then multiply by the cycles per minute. If 120 distinct symbols fit into batches of at most 50, each cycle needs three requests. A five-second interval gives 12 cycles per minute, or 36 requests per minute for one collector.&lt;/p&gt;

&lt;p&gt;Twenty independent collectors doing the same work require 720 requests per minute. One collector serving those same viewers still needs 36 upstream requests per minute under these assumptions. The application must still deliver updates to its clients; sharing retrieval does not remove that downstream work.&lt;/p&gt;

&lt;p&gt;The batch size has a concrete implementation example: &lt;a href="https://tickdb.ai/" rel="noopener noreferrer"&gt;TickDB&lt;/a&gt; provides a Ticker Snapshot endpoint that accepts up to 50 symbols per request. Its response includes the symbol, last traded price, and a timestamp; other market-data fields are conditional on the product, session, and availability. This makes it a useful example for a watchlist adapter, provided the selected plan and data permissions fit the application.&lt;/p&gt;

&lt;p&gt;The calculation below is an offline planning tool. It uses synthetic identifiers, makes no API calls, and assumes all lists belong to one permitted sharing group with the same feed, fields, and refresh cadence.&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;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isfinite&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;watchlists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval_s&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;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch_size must be a positive integer&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="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;interval_s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interval_s must be positive and finite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&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;symbols&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;watchlists&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;union&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cycles_per_minute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;interval_s&lt;/span&gt;
    &lt;span class="n"&gt;independent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&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;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;batch_size&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&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;union&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;independent_rpm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;independent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cycles_per_minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_rpm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cycles_per_minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;symbols&lt;/span&gt; &lt;span class="o"&gt;=&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;DEMO_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&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="nf"&gt;request_budget&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&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="c1"&gt;# {'independent_rpm': 720.0, 'shared_rpm': 36.0}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function deduplicates symbols within each list and across the sharing group. For different watchlists, it uses their actual union rather than assuming every viewer follows the same stocks. Separate groups must be calculated separately.&lt;/p&gt;

&lt;p&gt;These are steady-state average HTTP request counts, not a bill, a measured benchmark, or proof that a plan has enough capacity. They omit retries, startup requests, other endpoints, and provider-specific charging units. Short-window burst limits also matter: three simultaneous requests and three evenly spaced requests have the same average but different burst behavior.&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%2F4usc8bntolbu55p6e5jx.png" 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%2F4usc8bntolbu55p6e5jx.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Calculated example, not a performance test. Twenty viewers follow the same 120 symbols, with 50 symbols per request and a five-second polling interval. Counts exclude retries and other traffic; shared delivery assumes compatible permissions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Share collection only across compatible requests
&lt;/h2&gt;

&lt;p&gt;The collector should combine subscriptions or polling demand for the same data, then distribute the results to authorized viewers. Keep the provider credential on the server. Each browser connects to the application using its own authenticated session, and the application checks what that viewer may receive.&lt;/p&gt;

&lt;p&gt;A cache key needs enough context to prevent unlike records from colliding. Include the instrument identity and relevant feed, session, and data-mode settings. Separate entitlement groups when their access differs. A delayed feed and a real-time feed should not become interchangeable because they use the same ticker string.&lt;/p&gt;

&lt;p&gt;Request sharing is an application design choice. It does not itself establish permission to display or redistribute a provider's data. Confirm that the intended audience and delivery model are allowed before combining viewers into one sharing group.&lt;/p&gt;

&lt;p&gt;For a polling implementation, keep one refresh operation in flight per shared batch. If a slow response is still pending, a new screen refresh should reuse the available cache or wait for that operation rather than start an identical upstream request. When the last viewer leaves, remove unused demand after a deliberate grace period. In an application with several server instances, coordinate collection so each instance does not recreate the same upstream workload.&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%2F8v7xl73kdhhhnm6mo4ad.png" 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%2F8v7xl73kdhhhnm6mo4ad.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Suggested application architecture using TickDB snapshots as the source. The collector, cache, and viewer authorization belong to the application. Sharing requires compatible data permissions; the diagram does not assert redistribution rights or measured performance.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A cache hit says nothing about market freshness
&lt;/h2&gt;

&lt;p&gt;Keep the provider timestamp, the time your server received the record, and the time the browser rendered it as separate values. TickDB documents its snapshot timestamp in Unix milliseconds. Preserve that value and confirm its event meaning for the selected data before using it to measure market-data latency.&lt;/p&gt;

&lt;p&gt;A local cache lifetime tells the application when to attempt another fetch. It does not guarantee that the next response will contain a newer market observation. Polling every second can return the same last-trade price repeatedly, especially when a stock is inactive or its trading session has ended.&lt;/p&gt;

&lt;p&gt;Display wording should reflect what the application knows. “Last checked” can describe a successful fetch. “Data as of” can show the provider timestamp. “Refresh failed” can explain why an older value remains visible. Do not overwrite the original timestamp with the current browser time and label the result live.&lt;/p&gt;

&lt;p&gt;Choose freshness thresholds for the field and session being displayed. An old last trade during a quiet period is different from a failed connection. A watchlist can retain the last available value while showing that updates are unavailable; a feature that needs fresher input can decline to use it. Missing optional fields should remain unavailable rather than becoming zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming changes the workload but keeps the state problem
&lt;/h2&gt;

&lt;p&gt;A WebSocket stream can replace repeated snapshot requests when the application needs ongoing updates. TickDB documents a ticker channel with symbol-level subscribe and unsubscribe commands. A shared server connection can maintain the union of current demand, subject to the provider's connection and subscription limits and the application's data permissions.&lt;/p&gt;

&lt;p&gt;Receiving events and repainting the screen are separate operations. The application can maintain its latest record for each symbol while rendering the visible watchlist at a manageable cadence. That is appropriate for a latest-value display. It is not a substitute for retaining every event when the task is trade analysis, auditing, or another event-complete workflow.&lt;/p&gt;

&lt;p&gt;A connected socket alone does not establish that every requested symbol is subscribed or current. Track subscription outcomes and connection health separately from each record's timestamp. After a disconnect, show the loss of updates and follow the provider's recovery behavior before declaring the view current again. A new snapshot restores a latest-value view; it does not reconstruct events missed during the interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle limits without multiplying the load
&lt;/h2&gt;

&lt;p&gt;TickDB's error documentation distinguishes rate limiting, exhausted quota, connection limits, and subscription limits. For its rate-limit error, the documented action is to wait for the Retry-After duration. A permissions error or exhausted quota requires a different response; repeatedly retrying it will not create access.&lt;/p&gt;

&lt;p&gt;Make the shared collector responsible for retries. If every waiting browser retries on its own, one failed refresh can turn into a burst of duplicate upstream requests. Use bounded retries and jitter where appropriate, reserve capacity for other work, and keep a visible degraded state while updates are unavailable. A quota budget should leave room for recovery traffic rather than allocate every request to routine refreshes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source does not remove the upstream limits
&lt;/h2&gt;

&lt;p&gt;Open-source stock-data clients are available. Databento, for example, publishes an open-source Python client for its market-data service. Such software can make integration easier to inspect and extend, but the client's license does not define the service's data access, request limits, or display permissions.&lt;/p&gt;

&lt;p&gt;For a watchlist, inspect whether a client batches symbols, retries automatically, or creates one connection per instance. A wrapper can simplify a request while still duplicating the same work across viewers. The application remains responsible for its total demand and for how it presents cached values.&lt;/p&gt;

&lt;p&gt;Before shipping, test two viewers requesting the same data at once, a refresh that takes longer than its interval, an omitted symbol, and an upstream limit response. Confirm that duplicate work stays controlled and that old values keep their original timestamps. The result should be a watchlist whose request volume and freshness labels remain understandable as more people open it.&lt;/p&gt;

&lt;p&gt;Documentation basis: TickDB Ticker Snapshot, Real-Time Quotes and Error Codes, and the Databento Python client repository, checked November 5, 2026. The request-volume example is calculated from stated assumptions, not measured on a live service.&lt;/p&gt;

</description>
      <category>stockapi</category>
      <category>tickdb</category>
      <category>marketdata</category>
    </item>
    <item>
      <title>Stock API Charts Need Complete Downloads and Explicit Data Gaps</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Wed, 04 Nov 2026 23:21:16 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-charts-need-complete-downloads-and-explicit-data-gaps-1ea8</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-charts-need-complete-downloads-and-explicit-data-gaps-1ea8</guid>
      <description>&lt;h1&gt;
  
  
  Stock API Charts Need Complete Downloads and Explicit Data Gaps
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; A stock chart can look convincing even when its history is incomplete. A stock API supplies records under specific retrieval and aggregation rules; your application must finish the download and preserve missing observations. This article explains pagination, absent candles, and a small Python example that keeps an unknown price from becoming a false market move.&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%2F8umd0b5tzpp56agu48da.png" 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%2F8umd0b5tzpp56agu48da.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A stock API is a programmatic interface for retrieving stock-related data. For a historical chart, that often means requesting bars: records containing open, high, low, close, and volume for an interval. Those records can power chart viewers, research dashboards, and historical comparisons. They do not, by themselves, establish that every part of your requested window has been downloaded.&lt;/p&gt;

&lt;p&gt;Consider a chart that has prices before and after a missing minute. Replacing the missing price with zero invents a collapse. Repeating the previous price creates a flat segment that was never observed. A sound implementation keeps the absence explicit while it determines whether the cause is normal market behavior, a retrieval problem, or a coverage limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A successful response may contain only part of the history
&lt;/h2&gt;

&lt;p&gt;The first completeness check belongs in the downloader. An HTTP success means a request succeeded; a paginated endpoint may still have more records to return.&lt;/p&gt;

&lt;p&gt;Alpaca's Historical bars documentation gives a concrete example. Its multi-symbol results are ordered by symbol and then timestamp, so the first page can contain only one symbol. The page limit applies across the response, and a short page can still have a continuation token. An application must follow the documented token rather than decide that fewer rows means the download is finished.&lt;/p&gt;

&lt;p&gt;That behavior is specific to the endpoint. Other APIs use different retrieval controls. TickDB's Candlestick Data endpoint, for example, exposes a symbol, interval, time bounds, and a record limit. Its documentation caps the limit at 1,000 and says the Free plan cannot specify start or end times. These constraints matter when designing a history browser: access to recent candles does not automatically provide arbitrary historical navigation.&lt;/p&gt;

&lt;p&gt;Keep retrieval logic in the provider adapter. Confirm how boundaries and ordering work before splitting a request into time windows. If that behavior is undocumented, treat it as an unresolved integration requirement; guessing how to advance a timestamp can skip records or repeatedly retrieve the same window.&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%2Fbr9khzuf56ydai6tu48g.png" 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%2Fbr9khzuf56ydai6tu48g.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Suggested application flow. Retrieval follows the chosen endpoint's rules; validation and gap handling happen before rendering. This is a design illustration, not a measured system result.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An absent candle does not identify its own cause
&lt;/h2&gt;

&lt;p&gt;Once retrieval has finished, a missing interval still needs interpretation. Databento's Aggregate bars documentation states that an interval without a trade produces no record. Under that convention, an absent bar can be an expected result. It does not establish that the service lost data, and the rule should not be assumed for every provider.&lt;/p&gt;

&lt;p&gt;Start with the query context. Was the instrument trading during that period? Does the selected dataset cover the session you want? Does the provider omit intervals without qualifying observations? Did the downloader finish all required requests? These questions distinguish expected absence from an unexplained gap.&lt;/p&gt;

&lt;p&gt;An exchange calendar also belongs in this interpretation. Do not generate a continuous minute grid across nights, weekends, and holidays and label every empty slot a failure. Use the calendar and session policy appropriate to the instrument. Keep timestamp conversion separate from session selection, including daylight saving changes.&lt;/p&gt;

&lt;p&gt;Bar boundaries need their own check. Databento documents that its daily OHLCV schema follows UTC dates. A daily bucket therefore should not automatically be treated as the exchange-session day your chart intends to show.&lt;/p&gt;

&lt;p&gt;Until the cause is established, a useful status is simply “no bar returned.” Labels such as “market closed” or “no trades” make stronger claims and need supporting context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep missing prices separate from derived display values
&lt;/h2&gt;

&lt;p&gt;Suppose a synthetic series has a close of 100.00 at 14:30 UTC, no returned bar at 14:31, and a close of 101.00 at 14:32. The missing slot contains no observed close. It is neither a zero nor proof that the price stayed at 100.00.&lt;/p&gt;

&lt;p&gt;For a close-price line, pass an explicit missing value to the chart adapter and configure the renderer to show a break. Some charting systems connect surrounding observations unless told otherwise, so inspect the rendered result as well as the input array. For candlesticks, do not manufacture an OHLC record from a missing slot.&lt;/p&gt;

&lt;p&gt;Forward filling can serve a different purpose, such as displaying a last-known value in a dashboard. Preserve its original observation time and mark it as carried forward. Store that derived value separately from the observed series so it cannot silently become a reported trade or candle.&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%2Fk2brbfdcobgk9rbzq5j9.png" 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%2Fk2brbfdcobgk9rbzq5j9.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Synthetic close-price example in UTC. The middle slot has no returned observation. Its cause is unknown from these records alone; it must not become an invented zero-price candle.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A small adapter that preserves the missing slot
&lt;/h2&gt;

&lt;p&gt;The following Python example runs offline with synthetic data. It accepts an already selected, increasing UTC time grid and normalized close records for one instrument. It preserves missing slots, accepts identical duplicate records, and rejects conflicting duplicates instead of silently choosing a value.&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;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;align_closes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&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;slots&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Slots must be unique and increasing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;observed&lt;/span&gt; &lt;span class="o"&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bar outside the selected grid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;close&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;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a positive finite stock close&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;time&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;observed&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="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;close&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conflicting close for the same slot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;observed&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;
    &lt;span class="k"&gt;return&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="n"&gt;observed&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;time&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;time&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;slots&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;14:30&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;14:31&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;14:32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Synthetic UTC grid
&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&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;time&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;14:30&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;close&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;100.00&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;time&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;14:32&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;close&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;101.00&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;align_closes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&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;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;MISSING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is &lt;code&gt;14:30 100.00&lt;/code&gt;, &lt;code&gt;14:31 MISSING&lt;/code&gt;, and &lt;code&gt;14:32 101.00&lt;/code&gt; on separate lines. The function has preserved what the input says without claiming why the middle slot is absent.&lt;/p&gt;

&lt;p&gt;This is a close-series example, not a downloader or exchange calendar. Production adapters should use full normalized timestamps rather than time-of-day strings, partition records by instrument and series settings, and validate the complete OHLCV record. A conflicting revision may be legitimate, but resolving it requires a documented revision policy. Raising an error here makes that decision visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source helps with implementation but cannot restore absent history
&lt;/h2&gt;

&lt;p&gt;Open-source clients and data-processing tools can help build a stock API application. The distinction is between the software you can inspect or run and the underlying market-data service. An open-source client does not establish the upstream feed's coverage, usage rights, or historical depth.&lt;/p&gt;

&lt;p&gt;For this charting task, inspect what the client does with pagination and missing values. A convenient wrapper may return a clean table while hiding a truncated query or a fill operation. Trace those transformations before using the result for research.&lt;/p&gt;

&lt;p&gt;The same separation applies when using TickDB: the chart layer should retain the requested series settings and retrieval status alongside the returned candles. TickDB also documents that the final candle may still be forming. Mark that record as provisional when applicable; a changing current bar and an absent historical bar require different display behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the window before calling the chart complete
&lt;/h2&gt;

&lt;p&gt;An acceptance check should cover more than whether the chart renders. Confirm that retrieval reached the endpoint's documented stopping condition, that the result is partitioned by the intended instrument and settings, and that unexplained gaps remain visible. Exercise an unfinished download, a missing slot, an identical duplicate, and a conflicting duplicate. Each should produce a deliberate result.&lt;/p&gt;

&lt;p&gt;Do not turn the ratio of returned bars to clock minutes into a completeness score without establishing the expected-bar policy. A sparse instrument can legitimately have fewer bars under a trade-based aggregation rule. Conversely, a full-looking grid can conceal invented values.&lt;/p&gt;

&lt;p&gt;The useful promise of a historical chart is that its displayed observations and limitations can be explained. A stock API supplies the records. The application must preserve the difference between a value that was observed, a value it derived, and a value it does not know.&lt;/p&gt;

&lt;p&gt;Source basis: Alpaca Historical bars, Databento Aggregate bars, and TickDB Candlestick Data documentation, checked November 4, 2026. Code and numerical examples are synthetic.&lt;/p&gt;

</description>
      <category>stock</category>
      <category>api</category>
      <category>tickdb</category>
      <category>marketdata</category>
    </item>
    <item>
      <title>Stock API Price Alerts Need State as Well as a Price Threshold</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Tue, 03 Nov 2026 23:53:02 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-price-alerts-need-state-as-well-as-a-price-threshold-2bc1</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/stock-api-price-alerts-need-state-as-well-as-a-price-threshold-2bc1</guid>
      <description>&lt;h1&gt;
  
  
  Stock API Price Alerts Need State as Well as a Price Threshold
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; A stock API can supply prices for an alert, but a successful subscription does not define when a notification should fire. A useful alert needs an observation type, a trigger rule, remembered state, and a policy for missing data. In a synthetic five-update example, checking whether the price exceeds $100 produces three matches, while detecting an observed upward crossing produces two events. The distinction prevents repeated updates above the threshold from becoming repeated crossing alerts.&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%2F0cks2rbak6frufs32gl3.png" 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%2F0cks2rbak6frufs32gl3.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the API contributes to a price alert
&lt;/h2&gt;

&lt;p&gt;A stock API gives software access to stock-related observations, such as recent prices, trades, quotes, or historical bars. A price alert is one application of that data. The API supplies observations; the application decides which observations qualify, whether the rule has changed state, and what message to deliver.&lt;/p&gt;

&lt;p&gt;That division matters even for a small watchlist. The expression &lt;code&gt;price &amp;gt; 100&lt;/code&gt; answers whether the current value is above a threshold. It does not say whether this is the first time the application has seen that condition, whether a notification has already been sent, or what happened during a disconnection.&lt;/p&gt;

&lt;p&gt;For a straightforward implementation, start with one instrument and one definition: notify when two consecutive eligible observations move from at or below $100 to above $100. “Eligible” means that the observations meet your chosen price type, feed, session, freshness, and ordering requirements. This is an observed crossing in the data your application accepts; it is not a claim that every market event was captured.&lt;/p&gt;

&lt;p&gt;TickDB's ticker documentation, for example, provides stock messages with a symbol, a latest-price field, and a timestamp. Those are useful inputs to an alerting application. Reading that schema does not establish that a ticker update represents every individual trade, nor does it supply the application's notification policy.&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%2Funqx8qz7xkuhprpw3bbs.png" 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%2Funqx8qz7xkuhprpw3bbs.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Suggested application flow. Eligibility, trigger state, and notification delivery are separate responsibilities; the diagram is not a provider architecture or a delivery guarantee.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide whether the user wants a condition or a crossing
&lt;/h2&gt;

&lt;p&gt;Suppose five accepted observations arrive in this order: $99, $101, $102, $100, and $101. These are synthetic values, not a market replay.&lt;/p&gt;

&lt;p&gt;Checking &lt;code&gt;price &amp;gt; 100&lt;/code&gt; matches the second, third, and fifth observations. That can be appropriate for a status indicator that stays active while the condition is true. If each match sends a message, however, a user asking for an upward-crossing alert receives an extra notification at $102 even though the previous observation was already above $100.&lt;/p&gt;

&lt;p&gt;A crossing rule compares the current observation with the previous eligible one. It fires at the second and fifth updates. The fourth update, exactly $100, puts the rule back on the at-or-below side. Under this definition, reaching $100 is not an upward crossing; moving above it is.&lt;/p&gt;

&lt;p&gt;There are other valid policies. A one-time target alert can deactivate permanently after its first event. A condition alert can notify immediately when enabled above the threshold. A crossing alert can use the first observation only to establish a baseline. Choose one behavior and describe it in the interface. Quietly mixing them makes it difficult to explain apparently missing or repeated messages.&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%2F3vocdi6aq4o2yarbve4x.png" 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%2F3vocdi6aq4o2yarbve4x.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Synthetic observations for a threshold of $100. Values above the threshold occur three times; upward crossings between accepted observations occur twice.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose data that can answer the rule
&lt;/h2&gt;

&lt;p&gt;An alert about the latest reported trade, the best ask, and a bar close represents three different tasks. A daily-close alert can use completed daily bars under an explicit session policy. An intraday trade alert needs observations appropriate to that definition. A latest-price field with unclear semantics is not enough to claim either behavior.&lt;/p&gt;

&lt;p&gt;Polling also limits what the application can observe. Imagine two polls both return $99, while the price briefly rises to $101 and returns between them. Comparing the two snapshots cannot reveal that excursion. A bar high may indicate that the interval contained an above-threshold value, but it does not reconstruct every crossing or its exact sequence.&lt;/p&gt;

&lt;p&gt;Streaming can reduce that sampling gap, but its usefulness still depends on the feed and channel. A ticker stream may summarize changes; a trade stream has its own coverage and filtering rules. Neither a WebSocket connection nor the word “real-time” proves complete observation of every relevant event.&lt;/p&gt;

&lt;p&gt;Subscription success is another separate check. Alpaca's streaming documentation distinguishes connection, authentication, and subscription, and returns the active subscription list after changes. An application should verify the requested feed and symbols rather than treating an open socket as proof that the intended observations are arriving.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal observed-crossing detector
&lt;/h2&gt;

&lt;p&gt;The following Python example isolates the rule from the network and notification service. It requires only the standard library. Prices are synthetic decimal strings, and the caller must provide already validated, ordered observations for one rule. The threshold is fixed at $100 for this 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidOperation&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpwardCrossing&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&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;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&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;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&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="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;InvalidOperation&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a decimal 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;from&lt;/span&gt; &lt;span class="n"&gt;exc&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;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a finite positive 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;crossed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crossed&lt;/span&gt;

&lt;span class="c1"&gt;# Synthetic observations; no API call or message delivery.
&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UpwardCrossing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;prices&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;99&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;101&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;102&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;100&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;101&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [2, 5]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeated observations on the same side of the threshold do not produce another crossing. An initial value above $100 produces no event because the rule has no earlier baseline. Invalid numeric input raises an error without replacing the previous value. The surrounding application must decide whether that error also means continuity is uncertain and requires a reset.&lt;/p&gt;

&lt;p&gt;Use separate state for each instrument and rule configuration. Do not feed several symbols through one instance or change the price definition while retaining the old baseline. This small detector does not validate timestamps, identify duplicate events, process corrections, persist state, or send notifications.&lt;/p&gt;

&lt;p&gt;It also does not suppress repeated genuine oscillations around $100. A cooldown limits notification frequency, while hysteresis requires a lower rearming level before another trigger. Both change the user-visible policy. Add them only when their behavior matches the alert the user requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reconnect cannot fill in an unseen interval
&lt;/h2&gt;

&lt;p&gt;Suppose the last accepted value before a connection loss was $99 and the first value after recovery is $101. The endpoint observations are on different sides, but the application did not observe the intervening sequence. Its UI should not imply that it watched a specific crossing happen continuously.&lt;/p&gt;

&lt;p&gt;One conservative policy is to mark the rule unavailable during the interruption, reconnect and restore the subscription, then use the first eligible observation as a new baseline. In the example, the caller invokes &lt;code&gt;reset()&lt;/code&gt; when continuity is lost. The next call to &lt;code&gt;observe()&lt;/code&gt; establishes state without firing. This deliberately sacrifices possible alerts during the gap in exchange for a clear live-observation policy.&lt;/p&gt;

&lt;p&gt;TickDB's WebSocket guidance explicitly calls for resubscribing after reconnect and notes that full-market subscriptions receive another initial snapshot. That describes how to resume receiving data. A snapshot establishes current state; it does not reconstruct the sequence of missed observations. Where suitable history is available, a separate recovery process can evaluate it and label resulting notifications as retrospective.&lt;/p&gt;

&lt;p&gt;The same policy question arises at startup, a session change, or a restart that loses in-memory state. Persisting the previous price alone is insufficient if you cannot establish continuity with the next observation. Record the interruption and the rule's status so that silence is not mistaken for evidence that the threshold was never crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting an event and delivering it are separate outcomes
&lt;/h2&gt;

&lt;p&gt;An alert event can be detected correctly while the notification service is unavailable. Conversely, retrying a delivery after an uncertain response can produce duplicate messages even if the crossing detector ran only once.&lt;/p&gt;

&lt;p&gt;For a durable implementation, persist the updated rule state and its alert event together, then let a delivery worker process that event with a stable identifier. Track pending, delivered, and failed attempts separately. Where the destination supports idempotency, reuse the event identifier on retries. If it does not, an ambiguous delivery result can still leave a duplicate risk; the price rule cannot solve that on its own.&lt;/p&gt;

&lt;p&gt;This is an application design recommendation, not a guarantee supplied by a stock API or by the sample code. A market-data subscription also does not place a trade. Order submission and execution belong to a separate workflow with its own account permissions and outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the behavior the user will actually see
&lt;/h2&gt;

&lt;p&gt;Before connecting live data, run a short synthetic sequence through the rule and inspect the events. Include an initial value above the threshold, several values staying above it, a return to exactly the threshold, a new upward crossing, and a reset during a gap. Then exercise the surrounding application with stale inputs, out-of-order observations, a failed subscription, and notification retries.&lt;/p&gt;

&lt;p&gt;An open source client can help receive and parse data, but it does not choose these policies for the application. The useful promise is precise: which observations qualify, what transition creates an event, when the rule is unavailable, and how delivery status is reported. That gives a stock API alert a meaning the user can understand and a behavior the developer can verify.&lt;/p&gt;

&lt;p&gt;Sources consulted on November 3, 2026: TickDB Docs, Real-Time Quotes and WebSocket Best Practices; Alpaca, WebSocket Stream. The code and price sequences are offline demonstrations. No live feed completeness, latency, notification delivery, or trading performance is claimed.&lt;/p&gt;

</description>
      <category>stock</category>
      <category>api</category>
      <category>marketdata</category>
      <category>tickd</category>
    </item>
    <item>
      <title>Why Stock API Prices Differ and Which Price Your App Should Use</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Mon, 02 Nov 2026 23:46:02 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/why-stock-api-prices-differ-and-which-price-your-app-should-use-3a4g</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/why-stock-api-prices-differ-and-which-price-your-app-should-use-3a4g</guid>
      <description>&lt;h1&gt;
  
  
  Why Stock API Prices Differ and Which Price Your App Should Use
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Why does a stock API show a different price from a chart or brokerage screen? The values may describe different trades, quotes, sessions, feeds, or adjustment rules. Choose a price definition for each feature before comparing numbers: a watchlist can display the latest reported trade, a spread display needs bid and ask, and a historical chart needs a consistent bar and adjustment policy. A worked quote example shows why several valid prices can coexist for one stock.&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%2F7oaf3utbwbfgys4z2und.png" 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%2F7oaf3utbwbfgys4z2und.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One stock can have several meaningful prices
&lt;/h2&gt;

&lt;p&gt;A stock API lets an application retrieve or subscribe to stock-related data. It does not make “the stock price” a single, universal field. The value you receive might represent a transaction, an available quote, an aggregate, or a historical series transformed for comparison over time.&lt;/p&gt;

&lt;p&gt;Imagine a synthetic stock whose latest reported trade is $100.00. A subsequent quote shows a $99.98 bid and a $100.06 ask. Its previous regular-session close was $99.50. A watchlist, a spread display, and a daily-change calculation can legitimately use different numbers from this set.&lt;/p&gt;

&lt;p&gt;The first question is what each number describes. A difference alone does not establish that a provider is wrong. Equally, different definitions should not become a blanket excuse for bad data: once the instrument, observation, and conventions are aligned, unexplained discrepancies deserve investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade prices and quoted prices answer different questions
&lt;/h2&gt;

&lt;p&gt;The last trade reports a completed transaction included by the feed. A bid is a quoted buying price; an ask is a quoted selling price. The best bid and ask refer to the coverage of the selected feed, which may be narrower than the overall market.&lt;/p&gt;

&lt;p&gt;For the synthetic quote above, the spread is $0.08 and the midpoint is $100.02. The midpoint is an arithmetic reference derived from the two quote sides. There need not have been a trade at that price. The earlier $100.00 trade can remain the latest trade while the quote changes around it.&lt;/p&gt;

&lt;p&gt;This distinction helps define an alert. “Notify me when a reported trade exceeds $100” and “notify me when the ask exceeds $100” describe different conditions. Choose the observation type explicitly, then decide how the application handles repeated updates and reconnects. A price alert also remains separate from an order: a displayed quote does not guarantee an execution at that price or for the desired quantity.&lt;/p&gt;

&lt;p&gt;Alpaca's Latest Quotes reference makes the distinction concrete by describing best bid and ask prices and exposing a feed parameter. Its documented feed choices include IEX, SIP, and delayed SIP. The endpoint name alone therefore does not establish the venue coverage or freshness of a particular response.&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%2Fyaqn384ju9u86fj6nti1.png" 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%2Fyaqn384ju9u86fj6nti1.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Synthetic USD values for one stock. The midpoint and spread are calculated from the bid and ask; the last trade is a separate observation. No live market data is shown.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A close needs an interval and a session
&lt;/h2&gt;

&lt;p&gt;In an OHLCV bar, the close is the closing value of a specified interval under the provider's aggregation rules. OHLCV stands for open, high, low, close, and volume. A one-minute close and a daily close summarize different intervals; neither is automatically the latest price available now.&lt;/p&gt;

&lt;p&gt;A daily bar also needs a session definition. Ask whether it includes extended hours and whether the application needs an exchange's official closing value or the close of a vendor-generated bar. Those terms should not be treated as interchangeable without checking the documentation. Databento's schema guide notes that OHLCV results can vary with interval boundaries and the handling of trade breaks, halts, and periods with no trades.&lt;/p&gt;

&lt;p&gt;For a daily-change feature, preserve the reference as well as the current value. In the synthetic example, comparing the $100.00 last trade with the $99.50 previous regular-session close produces approximately +0.50%. Substituting the $100.02 midpoint produces approximately +0.52%. Both calculations are arithmetically correct, but they describe different measures. Label the chosen measure and keep it consistent across the application.&lt;/p&gt;

&lt;p&gt;TickDB's ticker documentation is an example of why field descriptions matter here. It defines &lt;code&gt;prev_close&lt;/code&gt; as the previous close or reference price for the statistics window, when available. It also says that stock fields ending in &lt;code&gt;24h&lt;/code&gt; usually refer to the trading day or session rather than a universal rolling 24-hour window. An application should establish that window before labeling a value “today's change.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Adjusted history serves a different purpose
&lt;/h2&gt;

&lt;p&gt;Historical charts often need to account for corporate actions. Suppose a stock undergoes a two-for-one split. Ignoring other price movement, a pre-split $100 share becomes two $50 shares. A raw price series can show a large step even though the split itself did not halve the holder's total position value.&lt;/p&gt;

&lt;p&gt;A split-adjusted series can express earlier prices on a comparable share basis. Dividend adjustments address a different effect and should not be assumed to follow the same rules. Alpaca's Historical Bars reference, for example, exposes separate raw, split, and dividend settings.&lt;/p&gt;

&lt;p&gt;Choose the transformation to fit the feature. A chart intended to show comparable prices across a split benefits from a stated adjustment policy. A question about what price actually printed at a historical moment calls for the corresponding raw observation. Do not join adjusted historical bars to an unadjusted live series without checking that their price bases align at the join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every application feature a price definition
&lt;/h2&gt;

&lt;p&gt;A small amount of explicit metadata prevents a large amount of ambiguity. Keep the instrument identity and currency, the observation type, the source feed, the relevant time, the session or interval, and the adjustment policy alongside the value. These can come from documented request settings as well as response fields; they should not be guessed from the number itself.&lt;/p&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;Price definition to choose&lt;/th&gt;
&lt;th&gt;Label or context to preserve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Watchlist&lt;/td&gt;
&lt;td&gt;Latest reported trade or another explicitly defined latest price&lt;/td&gt;
&lt;td&gt;Observation type, source, and time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spread display&lt;/td&gt;
&lt;td&gt;Bid and ask from one coherent quote&lt;/td&gt;
&lt;td&gt;Feed coverage, quote time, and sizes when available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily change&lt;/td&gt;
&lt;td&gt;Chosen current value versus a defined previous close&lt;/td&gt;
&lt;td&gt;Both price definitions and the reference session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical chart&lt;/td&gt;
&lt;td&gt;Bars with a fixed interval and adjustment policy&lt;/td&gt;
&lt;td&gt;Session coverage and adjustment basis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With TickDB or another provider, preserve useful source metadata during normalization. TickDB documents an extended-session field for US stocks with values such as &lt;code&gt;pre_market&lt;/code&gt;, &lt;code&gt;post_market&lt;/code&gt;, and &lt;code&gt;overnight&lt;/code&gt;. That context can explain why a displayed update belongs to a different session from the chart beside it. If metadata is absent, record the uncertainty or resolve it from documented context instead of inventing a value.&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%2Fwqg98o9f0ikliv9pyc70.png" 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%2Fwqg98o9f0ikliv9pyc70.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Suggested application design. Define the observation and its context before reducing a stock API response to a displayed number.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A small quote calculation with an explicit boundary
&lt;/h2&gt;

&lt;p&gt;This Python example calculates the midpoint and spread of a synthetic quote. It uses only the standard library and an application-defined schema. It checks numeric inputs and refuses a crossed quote, where the bid exceeds the ask, because this simple display example does not attempt to interpret that condition.&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;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvalidOperation&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quote_metrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bid_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ask_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bid_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;ask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;InvalidOperation&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected decimal prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ask&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected finite positive prices&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;bid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ask&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Crossed quote requires review&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;midpoint&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;bid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Synthetic USD quote; not a response from a live service.
&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quote_metrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;99.98&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;100.06&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="s"&gt;Midpoint: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;midpoint&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; USD&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="s"&gt;Spread: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spread&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; USD&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;The output is &lt;code&gt;Midpoint: 100.02 USD&lt;/code&gt; and &lt;code&gt;Spread: 0.08 USD&lt;/code&gt;. The code deliberately does not call the midpoint a last trade or an executable price. Before applying it to live data, ensure both sides refer to the same instrument, currency, feed, and coherent quote state. On a stream that sends partial updates, maintaining that state requires the provider's update rules.&lt;/p&gt;

&lt;p&gt;This calculation cannot certify quote freshness, available liquidity, or market-wide coverage. A zero spread is possible for a locked quote and is not rejected here; accepting that arithmetic result still says nothing about whether a trade can be executed. Input validation protects the calculation from obvious misuse, while market interpretation requires more context.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a difference becomes worth investigating
&lt;/h2&gt;

&lt;p&gt;To compare an API value with another screen, capture both observations and their metadata before refreshing either one. Match the instrument and currency, then the price type, feed scope, time, session, and adjustment basis. For historical bars, match the exact interval and aggregation conventions as well.&lt;/p&gt;

&lt;p&gt;If those conditions align and a discrepancy remains, retain the original payloads and request parameters. Check delayed access, cache behavior, missing updates, corrections, and normalization errors. Two requests made at different moments are not a controlled comparison of the same event.&lt;/p&gt;

&lt;p&gt;Changing to an open source client does not resolve these definitions automatically. A client can make requests easier to write, but it still consumes a particular upstream feed and must interpret that feed's records. The useful result for the user is a price with an understandable meaning: a latest trade, a bid and ask, a defined close, or an adjusted historical value, accompanied by enough context to use it correctly.&lt;/p&gt;

&lt;p&gt;Sources consulted on November 2, 2026: Alpaca, Latest Quotes and Historical Bars; Databento, What's a Schema; TickDB Docs, Real-Time Quotes. All numerical examples and diagrams are synthetic or conceptual, not live measurements or supplier performance comparisons.&lt;/p&gt;

</description>
      <category>stock</category>
      <category>tickdb</category>
      <category>marketdata</category>
      <category>api</category>
    </item>
    <item>
      <title>A Stock API Provides Market Data for Apps Within Feed and Usage Limits</title>
      <dc:creator>wang vball</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:02:52 +0000</pubDate>
      <link>https://dev.to/wang_vball_2e41b5cfbc194e/a-stock-api-provides-market-data-for-apps-within-feed-and-usage-limits-469e</link>
      <guid>https://dev.to/wang_vball_2e41b5cfbc194e/a-stock-api-provides-market-data-for-apps-within-feed-and-usage-limits-469e</guid>
      <description>&lt;h1&gt;
  
  
  A Stock API Provides Market Data for Apps Within Feed and Usage Limits
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; A stock API gives an application programmatic access to prices and related financial data, but what can you build with it? Charts, watchlists, research tools, and alerts are common uses; their reliability depends on the feed, timestamps, data definitions, and permitted use. A latest trade, a bid–ask quote, and a daily price bar describe different things, while an open source client library grants no automatic rights to redistribute the underlying data.&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%2Futyab87wsk9h5ko1d90r.png" 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%2Futyab87wsk9h5ko1d90r.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a stock API actually provides
&lt;/h2&gt;

&lt;p&gt;A stock API is an interface through which software requests or receives stock-related data. An application can use it to retrieve recent prices, download historical observations, or subscribe to updates. Some services also expose company fundamentals, corporate actions, or news through separate endpoints.&lt;/p&gt;

&lt;p&gt;The interface makes data accessible to code. The feed behind it determines which events are represented, how quickly they arrive, and how much history is available. Two services can both return a price for the same ticker while describing different observations.&lt;/p&gt;

&lt;p&gt;An endpoint is a particular operation offered by the service, such as retrieving historical bars. Many APIs deliver a structured response, often JSON, that a program can parse into fields. An SDK is a client library that wraps those operations in convenient functions. Installing an SDK does not change the coverage or permissions of the underlying service.&lt;/p&gt;

&lt;p&gt;It also helps to separate market data from trading. Reading a price does not submit an order. A trading application may use a market data API alongside a broker API for account information, order placement, and execution reports. Even when one provider offers both, these are separate capabilities.&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%2Fld9ylq7yqxofvt3kikxg.png" 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%2Fld9ylq7yqxofvt3kikxg.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. A conceptual market data path. Feed coverage and normalization affect what reaches the application; a trade and a quote describe different events.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The data type determines the feature you can build
&lt;/h2&gt;

&lt;p&gt;A trade records a completed transaction. A quote describes a bid and an ask within the coverage of the feed. A price bar summarizes observations over an interval, typically as open, high, low, close, and volume, or OHLCV. These records serve different purposes.&lt;/p&gt;

&lt;p&gt;A daily bar is useful for a long-term chart, but it cannot reveal the sequence of price changes inside that day. A last-trade price can support a watchlist, but it is not a promise that a new order will execute at that price. That distinction matters as soon as a display becomes an input to an alert or trading decision.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Useful data&lt;/th&gt;
&lt;th&gt;Important boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Watchlist&lt;/td&gt;
&lt;td&gt;Latest prices and timestamps&lt;/td&gt;
&lt;td&gt;Show when the observation was recorded and whether it is delayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical chart&lt;/td&gt;
&lt;td&gt;OHLCV bars&lt;/td&gt;
&lt;td&gt;Keep interval, session, and adjustment settings consistent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price alert&lt;/td&gt;
&lt;td&gt;A defined trade or quote stream&lt;/td&gt;
&lt;td&gt;Handle disconnects and decide which observation triggers the alert.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio view&lt;/td&gt;
&lt;td&gt;Prices plus holdings&lt;/td&gt;
&lt;td&gt;Holdings and cash flows come from a separate source unless explicitly provided.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research or backtesting&lt;/td&gt;
&lt;td&gt;Historical observations and reference data&lt;/td&gt;
&lt;td&gt;Check missing periods, corporate actions, and which securities existed at the time.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, TickDB documents a ticker stream with stock symbols, latest prices, and timestamps, including examples for US stocks, Hong Kong stocks, and A-shares. This illustrates the input a watchlist can consume. The application still has to interpret the fields, maintain its own display state, and explain the age of the observation to the reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  REST and WebSocket describe delivery methods
&lt;/h2&gt;

&lt;p&gt;With a REST-style HTTP API, the client makes a request and receives a response. This fits an on-demand lookup or a historical download. Repeating the request creates polling: the application checks again at an interval it chooses.&lt;/p&gt;

&lt;p&gt;A WebSocket connection lets the server send updates over a persistent connection. This can suit a watchlist or alert service that needs frequent changes without repeatedly asking for the same symbols. The client must still manage subscriptions, detect a broken connection, and recover according to the provider's protocol. Reconnecting does not by itself prove that missed events have been recovered.&lt;/p&gt;

&lt;p&gt;Neither method establishes the quality of the feed. A WebSocket can carry delayed data, and an HTTP response can contain a recent observation. Similarly, transport latency and market coverage are separate questions. Alpaca's Market Data documentation, for example, distinguishes real-time IEX coverage on its Basic equities plan from broader exchange coverage on another plan. “Real-time” alone does not identify the set of venues represented.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small example that keeps the timestamp visible
&lt;/h2&gt;

&lt;p&gt;A watchlist should retain the observation time alongside the price. Otherwise, a successful request can make an old observation look current simply because the screen has just refreshed.&lt;/p&gt;

&lt;p&gt;The following Python example runs offline using synthetic data and an application-defined schema. It requires only the standard library. It calculates observation age and applies an illustrative 60-second display threshold; it does not measure network latency or decide whether the market should be trading.&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_age_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;observed&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;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observed_at&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&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;+00:00&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcoffset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcoffset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timezone-aware timestamps 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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_finite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a finite positive stock 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;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&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;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check clock or timestamp semantics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_age_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;older than display threshold&lt;/span&gt;&lt;span class="sh"&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;within display threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&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;row&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="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;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | age=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Synthetic observation and clock; not a live API response.
&lt;/span&gt;&lt;span class="n"&gt;sample&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;EXAMPLE.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;last_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;100.25&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;observed_at&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-10-28T14:30:00Z&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;now&lt;/span&gt; &lt;span class="o"&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;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is &lt;code&gt;EXAMPLE.US 100.25 | age=75s | older than display threshold&lt;/code&gt;. The displayed number is still available, but the application no longer presents its recency ambiguously.&lt;/p&gt;

&lt;p&gt;For a real integration, map the provider's fields into this small internal schema only after checking their meanings. TickDB's ticker documentation specifies Unix milliseconds for its timestamp field and describes latest prices as strings. Converting milliseconds as if they were seconds would invalidate the age calculation. Its documentation also notes that a &lt;code&gt;24h&lt;/code&gt; field suffix does not imply a rolling 24-hour window in every market; stock statistics usually refer to a trading day or session.&lt;/p&gt;

&lt;p&gt;The threshold remains an application policy. An old last trade can be normal after a session ends or when an instrument has not traded recently. Use a market calendar and the feed's timestamp definition before turning an age label into a service-health warning. This example also leaves ingestion validation, symbol mapping, persistence, and recovery to the surrounding application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is there an open source stock API available
&lt;/h2&gt;

&lt;p&gt;Yes, open source software exists for accessing stock data. The useful distinction is whether you mean an open source client, a server you can host, or a dataset with permissions that fit your project.&lt;/p&gt;

&lt;p&gt;For example, yfinance is an open source Python library for accessing Yahoo Finance data. Its repository states that it is not affiliated with Yahoo, describes its research and educational purpose, and directs users to Yahoo's terms for rights to downloaded data. The repository also notes that the Yahoo Finance API is intended for personal use. The software license and the terms governing the data are separate.&lt;/p&gt;

&lt;p&gt;Hosting your own API around downloaded data gives you control over your application interface. It does not create new data rights, improve the original feed's coverage, or guarantee that its upstream service will remain available. A free hosted API is a different option again: it may offer access subject to quotas and conditions without publishing its server implementation.&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%2Fe4iqm5xjlcrxmr4lvceh.png" 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%2Fe4iqm5xjlcrxmr4lvceh.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Open source software, access to a data service, and permission to use the data are separate considerations.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations that matter after the first successful request
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coverage and freshness need separate checks.&lt;/strong&gt; Confirm which markets, venues, instruments, and sessions the feed represents. A response labeled “latest” may contain the most recent available observation rather than an observation from the present moment. Extended-hours support also needs an explicit check; a regular-session chart should not silently mix in a different session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical data has definitions and revisions.&lt;/strong&gt; A split changes the relationship between share count and price. Dividend adjustments serve another purpose. Alpaca's historical bars reference exposes separate adjustment options, which illustrates why a chart should record its settings instead of relying on an unnoticed default. For a backtest, also check delisted instruments and information availability dates; today's list of surviving companies is not automatically an appropriate historical universe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capacity depends on the workload.&lt;/strong&gt; Suppose a dashboard polls 120 symbols individually every 10 seconds. That is 720 requests per minute before retries. If an endpoint supports all 120 symbols in one batch, the same schedule requires six requests per minute, although the provider may meter symbols or data volume separately. Streaming reduces repeated polling but introduces connection and subscription limits of its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access and public use are different requirements.&lt;/strong&gt; An API key proves that a service accepts a credential for a request. It does not, on its own, establish permission for public display, redistribution, or every commercial use. Check the relevant data agreement for the intended application before building a public product around a personal-use prototype.&lt;/p&gt;

&lt;p&gt;Start with a feature narrow enough to verify: one chart, a small watchlist, or one alert rule. Record its required data type, acceptable observation age, session coverage, and intended use. Then check both a normal response and an empty, delayed, or interrupted one. A stock API is useful when those conditions match the application—not merely when a request returns a price.&lt;/p&gt;

&lt;p&gt;Sources consulted on November 1, 2026: Bavest, Stock API – What You Need to Know; Databento, Stock Market API and schema documentation; Alpaca, About Market Data API and Historical Bars; TickDB Docs, Real-Time Quotes; yfinance repository README. Examples and diagrams are explanatory, not live market measurements.&lt;/p&gt;

</description>
      <category>stock</category>
      <category>api</category>
      <category>market</category>
      <category>data</category>
    </item>
  </channel>
</rss>
