<?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: SIGNUM HQ</title>
    <description>The latest articles on DEV Community by SIGNUM HQ (@signumhq).</description>
    <link>https://dev.to/signumhq</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%2F4191805%2Ff0c815a0-0283-4003-84cc-a623fab8588b.png</url>
      <title>DEV Community: SIGNUM HQ</title>
      <link>https://dev.to/signumhq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/signumhq"/>
    <language>en</language>
    <item>
      <title>What a 5.5% Nasdaq drop looked like next to options-implied earnings moves (and how we scored them)</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:38:31 +0000</pubDate>
      <link>https://dev.to/signumhq/what-a-55-nasdaq-day-looked-like-next-to-options-implied-earnings-moves-and-how-we-scored-them-2i6i</link>
      <guid>https://dev.to/signumhq/what-a-55-nasdaq-day-looked-like-next-to-options-implied-earnings-moves-and-how-we-scored-them-2i6i</guid>
      <description>&lt;p&gt;On Nov 5, 2026 the S&amp;amp;P 500 closed down 4.36%, the Nasdaq down 5.52%, the Philadelphia Semiconductor Index (SOX) down 12.25%, and VIX closed at 28.18. It was a chip-led drop. That same week we were scoring earnings reactions against the move that options had priced in beforehand, so we had a small, awkward test case: how does a pre-announcement "expected move" look next to what happened on a session like that?&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://www.signumhq.com/en?from=devto_1106" rel="noopener noreferrer"&gt;SIGNUM HQ&lt;/a&gt;, a US stock app that shows an expected move before each earnings report and the actual move after it. This post is the scorecard for the week of Nov 2, how it is computed, and what it can and cannot say. It is not a forecast and not investment advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers (interim, n = 21)
&lt;/h2&gt;

&lt;p&gt;Of the 21 earnings reactions where we had a complete stored pair (expected move recorded before the report, actual move computed after), &lt;strong&gt;15 landed inside the expected range and 6 outside&lt;/strong&gt;. The median expected move was 7.7%; the median absolute actual move was 3.73%.&lt;/p&gt;

&lt;p&gt;The six outside, by how far the actual absolute move exceeded the expected one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ticker&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;th&gt;Excess&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XYZ&lt;/td&gt;
&lt;td&gt;±8.8%&lt;/td&gt;
&lt;td&gt;+15.30%&lt;/td&gt;
&lt;td&gt;+6.50 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARM&lt;/td&gt;
&lt;td&gt;±9.9%&lt;/td&gt;
&lt;td&gt;−13.08%&lt;/td&gt;
&lt;td&gt;+3.18 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DD&lt;/td&gt;
&lt;td&gt;±6.5%&lt;/td&gt;
&lt;td&gt;+8.61%&lt;/td&gt;
&lt;td&gt;+2.11 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QCOM&lt;/td&gt;
&lt;td&gt;±8.4%&lt;/td&gt;
&lt;td&gt;−9.49%&lt;/td&gt;
&lt;td&gt;+1.09 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BKNG&lt;/td&gt;
&lt;td&gt;±8.8%&lt;/td&gt;
&lt;td&gt;+9.83%&lt;/td&gt;
&lt;td&gt;+1.03 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETN&lt;/td&gt;
&lt;td&gt;±6.7%&lt;/td&gt;
&lt;td&gt;+7.00%&lt;/td&gt;
&lt;td&gt;+0.30 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight of the 21 reactions happened on Nov 5 itself. Six of those eight were inside the range; the two outside were ARM and QCOM, both of which reported after the Nov 4 close. Their reaction day was also the day SOX fell 12.25%. Our scorecard is a raw close-to-close number, not market-adjusted, so it cannot separate the company's report from the market's session. On Nov 5 the briefing in our app points to same-session reports of declines at Dell, HPE and Super Micro as the strongest candidate for the chip move, labelled as a possible cause rather than a finding.&lt;/p&gt;

&lt;p&gt;Five names that reported after the Nov 5 close (TEAM, AXON, NET, AFRM, IONQ) are not in the set yet. Missing records are excluded, never counted as a zero move, and we will confirm them on Nov 7 morning. So this is an interim snapshot, not the full week.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the expected move is computed
&lt;/h2&gt;

&lt;p&gt;For each report we take the at-the-money straddle on the first expiry after the announcement, add the call and put mid prices, and divide by the underlying price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="c1"&gt;// ATM straddle mid / spot, as a percentage&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expectedMovePct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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="nf"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Equality counts as inside the range&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isWithinRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualPct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;expectedPct&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Positive = the reaction was bigger than priced, in percentage points&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;excessPp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actualPct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;expectedPct&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;Two details matter more than the formula:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The horizons differ.&lt;/strong&gt; The expected move runs to option expiry (some of these expire days after the report, a few more than two weeks after). The actual move is one regular session, close to close (previous close to announcement-day close for before-open reports, announcement-day close to next close for after-close reports). A range that spans several days is easier to stay inside than a one-day move, so "15 of 21 inside" is not a measure of how accurate the options market is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a size, not a direction.&lt;/strong&gt; The straddle says how big the move was priced, nothing about up or down.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also reject any expected-move record that was captured after the release, and anything incomplete is omitted rather than filled in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the app
&lt;/h2&gt;

&lt;p&gt;The dashboard now carries a selloff briefing on days like Nov 5: the index moves, VIX, a "Why" line that states a possible cause only as far as the sources support it, and the earnings that finished beyond their options-implied range.&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%2Fpsru89bsx8dl0lki9wnb.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%2Fpsru89bsx8dl0lki9wnb.png" alt="SIGNUM HQ dashboard at 375px: Market Selloff Briefing for 2026-11-05 with S&amp;amp;P 500, Nasdaq, SOX and VIX moves, a Why line, and earnings beyond the options-implied range" width="750" height="1330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The earnings calendar shows the week's running count and the reminder that the two horizons differ:&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%2Fc4n5zvo50w4b3inrmn9t.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%2Fc4n5zvo50w4b3inrmn9t.png" alt="SIGNUM HQ earnings calendar at 375px: &amp;quot;This week: 15 of 21 within range&amp;quot; and options-implied moves with expiry for upcoming reports" width="750" height="1255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also wrote a short explainer on what to check when US stocks sell off (VIX versus VIX3M, why the chip index can move more than the Nasdaq, and how to read an expected move in a selloff week): &lt;a href="https://www.signumhq.com/en/learn/market-selloff?from=devto_1106" rel="noopener noreferrer"&gt;When US stocks sell off&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The set is interim and excludes missing records.&lt;/li&gt;
&lt;li&gt;Raw reactions include the market's own move that day.&lt;/li&gt;
&lt;li&gt;21 observations is a small sample; read it as a log, not a statistic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scorecard API behind this is public if you want to check the rows: &lt;code&gt;https://www.signumhq.com/api/earnings/scorecard?week=2026-11-02&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: SIGNUM HQ is our own product, and this post was generated with AI for the SIGNUM HQ team. Information only, not investment advice or a recommendation to buy or sell any security.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have an iPhone and want to try it: code EARNINGS gives 1 month of PRO (first 500 redemptions): &lt;a href="https://www.signumhq.com/app?code=EARNINGS&amp;amp;from=devto_1106" rel="noopener noreferrer"&gt;https://www.signumhq.com/app?code=EARNINGS&amp;amp;from=devto_1106&lt;/a&gt;&lt;/p&gt;

</description>
      <category>earnings</category>
      <category>stocks</category>
      <category>fintech</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Model an observation with its expiry and time</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:45:10 +0000</pubDate>
      <link>https://dev.to/signumhq/model-an-observation-with-its-expiry-and-time-290l</link>
      <guid>https://dev.to/signumhq/model-an-observation-with-its-expiry-and-time-290l</guid>
      <description>&lt;p&gt;An options observation is more than a numeric value. For an earnings comparison, the symbol, expiry and observation time determine what the value refers to. A value for a different expiry is not a safe substitute for a missing one.&lt;/p&gt;

&lt;p&gt;One proposed model separates an observed value and its context from an unavailable state with a reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;observed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing-source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request-failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other-expiry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consumer can then handle a missing source field differently from a failed request. A zero remains a possible observed value rather than a default for every failure.&lt;/p&gt;

&lt;p&gt;A concrete case from tonight: AMD reports after the close on November 3. Using the at-the-money straddle for the first expiry after the event (the November 4 weekly, strike 467.5, straddle mid 42.55 against a 466.84 reference price, from the closing mids of the November 2 session), the options market implies a range of roughly plus or minus 9.1 percent. That figure says nothing about direction, and a realized move can land outside it. Its value only makes sense together with its expiry, its basis and its observation time, which is the whole point of the model above.&lt;/p&gt;

&lt;p&gt;A useful review follows each state through every consumer, including caches and summaries. Test observed zero, missing input, request failure and another expiry as distinct cases. This is illustrative design, not a description of an implemented SIGNUM HQ schema or a claim that a test has passed.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on SIGNUM HQ, a US earnings and options research app, so this is our own product. This post was generated with AI for the SIGNUM HQ development team. If you want to see how the app shows these fields, there is one free month of PRO per device here: &lt;a href="https://www.signumhq.com/app?camp=weekend-1101&amp;amp;from=devto_1103&amp;amp;l=en" rel="noopener noreferrer"&gt;https://www.signumhq.com/app?camp=weekend-1101&amp;amp;from=devto_1103&amp;amp;l=en&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>data</category>
    </item>
    <item>
      <title>Missing values are not zero in a small TypeScript model</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Mon, 02 Nov 2026 14:47:41 +0000</pubDate>
      <link>https://dev.to/signumhq/missing-values-are-not-zero-in-a-small-typescript-model-5c39</link>
      <guid>https://dev.to/signumhq/missing-values-are-not-zero-in-a-small-typescript-model-5c39</guid>
      <description>&lt;p&gt;This AI-generated design example is illustrative and does not describe deployed SIGNUM HQ code. A feed that is unavailable and an observed zero are different states. Converting both to zero can silently change an average or a condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;observed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;observedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;observed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Consumers must decide how to display or exclude missing observations. Keep the distinction through parsing, storage and presentation rather than trying to recover it after formatting.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Validate a ratio before formatting it as a percentage</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:49:05 +0000</pubDate>
      <link>https://dev.to/signumhq/validate-a-ratio-before-formatting-it-as-a-percentage-13k0</link>
      <guid>https://dev.to/signumhq/validate-a-ratio-before-formatting-it-as-a-percentage-13k0</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: AI was used to draft this standalone example. It is an input-validation exercise, not a description of code deployed in SIGNUM HQ.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A formatting function can make an invalid input look authoritative. For a ratio that represents a subset of a whole, validating the denominator and coverage should happen before producing a percentage. Missing, non-finite and out-of-range inputs deserve separate outcomes.&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;subset_percent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;whole&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;part&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;whole&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;return&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;missing&lt;/span&gt;&lt;span class="sh"&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;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&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="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InvalidOperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;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;invalid&lt;/span&gt;&lt;span class="sh"&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;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;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;w&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="k"&gt;return&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;nonfinite&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;w&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="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;p&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="ow"&gt;or&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="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;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;invalid_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Synthetic fixtures, not observed market values.
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;subset_percent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&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;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&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;40.0&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;ok&lt;/span&gt;&lt;span class="sh"&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;subset_percent&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;5&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&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;assert&lt;/span&gt; &lt;span class="nf"&gt;subset_percent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&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;0&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;subset_percent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&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;5&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;subset_percent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Infinity&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;5&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nonfinite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subset assumption matters. This function is not a generic ratio validator: growth rates and leverage ratios can legitimately exceed one hundred percent. It also cannot detect a numerator from one session paired with a denominator from another. That requires metadata validation before this arithmetic step.&lt;/p&gt;

&lt;p&gt;For a real record, preserve the original inputs, source, unit and observation window alongside the classification. Do not silently clamp a bad percentage into the display range. A separately reported validation failure makes the source problem reviewable.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.python.org/3/library/decimal.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/decimal.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the same idea shows up in a product screen
&lt;/h2&gt;

&lt;p&gt;I work on SIGNUM HQ, a US-stock options and earnings app. Its public "Earnings verification receipts" page applies the same habit to a different problem: each row keeps the option session, option expiry, the stored &lt;code&gt;capturedAt&lt;/code&gt; timestamp and a "pre-announcement timing unverified" status next to the numbers, instead of presenting every comparison as if it were a clean forecast. The screenshot below is the week of 2026-10-19.&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%2Foox89c98uurlqsaffa9m.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%2Foox89c98uurlqsaffa9m.png" alt="SIGNUM HQ earnings verification receipts, week of 2026-10-19" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Public page: &lt;a href="https://www.signumhq.com/en/receipts/earnings?from=devto_1026" rel="noopener noreferrer"&gt;https://www.signumhq.com/en/receipts/earnings?from=devto_1026&lt;/a&gt;&lt;br&gt;
App: &lt;a href="https://www.signumhq.com/app?from=devto_1026" rel="noopener noreferrer"&gt;https://www.signumhq.com/app?from=devto_1026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an engineering note, not investment advice or a recommendation about any security.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>finance</category>
      <category>data</category>
    </item>
    <item>
      <title>What options price in for MSFT, GOOGL, AMZN and INTC this earnings week, and how last week's five compared</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:01:37 +0000</pubDate>
      <link>https://dev.to/signumhq/what-options-price-in-for-msft-googl-amzn-and-intc-this-earnings-week-and-how-last-weeks-five-2ced</link>
      <guid>https://dev.to/signumhq/what-options-price-in-for-msft-googl-amzn-and-intc-this-earnings-week-and-how-last-weeks-five-2ced</guid>
      <description>&lt;p&gt;Four big earnings reports land in the last days of October: Microsoft and Alphabet on Oct 28, Amazon and Intel on Oct 29, all after the close according to the companies' own announcements. Here is what the options market was pricing at Friday's close (Oct 23), how that number is computed, and how the same kind of number did last week. This is a description of option prices, not a forecast and not advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;Implied move = ATM straddle ÷ stock price. Take the strike nearest the stock price, add the call and put mid prices at that strike, and divide by the stock price. It is a market-implied range for the whole life of the option, and it carries no direction.&lt;/p&gt;

&lt;p&gt;Closing mids on Oct 23, expiry Oct 30:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ticker&lt;/th&gt;
&lt;th&gt;Strike&lt;/th&gt;
&lt;th&gt;Straddle&lt;/th&gt;
&lt;th&gt;Stock&lt;/th&gt;
&lt;th&gt;Implied move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MSFT&lt;/td&gt;
&lt;td&gt;525&lt;/td&gt;
&lt;td&gt;$27.68&lt;/td&gt;
&lt;td&gt;$524.51&lt;/td&gt;
&lt;td&gt;±5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOOGL&lt;/td&gt;
&lt;td&gt;320&lt;/td&gt;
&lt;td&gt;$19.38&lt;/td&gt;
&lt;td&gt;$320.21&lt;/td&gt;
&lt;td&gt;±6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMZN&lt;/td&gt;
&lt;td&gt;247.5&lt;/td&gt;
&lt;td&gt;$19.45&lt;/td&gt;
&lt;td&gt;$248.00&lt;/td&gt;
&lt;td&gt;±7.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INTC&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;$8.63&lt;/td&gt;
&lt;td&gt;$86.96&lt;/td&gt;
&lt;td&gt;±9.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Oct 30 is the first expiry that contains every reaction session (Oct 29 for MSFT and GOOGL, Oct 30 for AMZN and INTC). It also contains the days before the reports, so &lt;strong&gt;these are ranges to expiry, not the size of a one-day gap&lt;/strong&gt;. The weekend and Monday-to-Wednesday option prices will move before the reports, so treat Friday's figures as a snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the same kind of number did last week
&lt;/h2&gt;

&lt;p&gt;I store the implied move from the app's earnings page and compare it with the first regular-session reaction after the report. Last week's five names:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ticker&lt;/th&gt;
&lt;th&gt;Implied move&lt;/th&gt;
&lt;th&gt;Actual reaction&lt;/th&gt;
&lt;th&gt;Inside the range?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NFLX&lt;/td&gt;
&lt;td&gt;±9.3%&lt;/td&gt;
&lt;td&gt;-5.3%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEE&lt;/td&gt;
&lt;td&gt;±3.3%&lt;/td&gt;
&lt;td&gt;-0.7%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IBM&lt;/td&gt;
&lt;td&gt;±7.4%&lt;/td&gt;
&lt;td&gt;+0.3%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LRCX&lt;/td&gt;
&lt;td&gt;±7.1%&lt;/td&gt;
&lt;td&gt;-11.8%&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TSLA&lt;/td&gt;
&lt;td&gt;±5.6%&lt;/td&gt;
&lt;td&gt;-3.1%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of five were inside, and LRCX moved about 1.7 times its implied range. I would not read that as a hit rate, for three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The implied move runs to the Oct 23 expiry while the reaction is a single session, so the periods differ.&lt;/li&gt;
&lt;li&gt;The saved timestamps are not verified to be before each report (the page labels all five "Pre-announcement timing unverified"), so these are reference comparisons, not logged forecasts.&lt;/li&gt;
&lt;li&gt;Five names is far too few to say anything about how reliable the options market is.&lt;/li&gt;
&lt;/ul&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%2Foox89c98uurlqsaffa9m.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%2Foox89c98uurlqsaffa9m.png" alt="Earnings receipts page for the week of 2026-10-19" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check before comparing any two numbers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Which expiry does the implied move run to?&lt;/li&gt;
&lt;li&gt;Which session were the option prices taken from?&lt;/li&gt;
&lt;li&gt;Was it saved before the release, and can that be shown?&lt;/li&gt;
&lt;li&gt;Is the "actual" a close-to-close move over one regular session?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one of those is unknown, the comparison gets labelled instead of counted.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build SIGNUM HQ, a US-stock options app, and used AI to help draft this text. The figures come from the app's public API and earnings page. The app: &lt;a href="https://www.signumhq.com/app?from=devto_1024" rel="noopener noreferrer"&gt;https://www.signumhq.com/app?from=devto_1024&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>finance</category>
      <category>stocks</category>
      <category>data</category>
    </item>
    <item>
      <title>A timestamp gate for an earnings scorecard: reject observations made after the release</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:47:14 +0000</pubDate>
      <link>https://dev.to/signumhq/a-timestamp-gate-for-an-earnings-scorecard-reject-observations-made-after-the-release-4dc4</link>
      <guid>https://dev.to/signumhq/a-timestamp-gate-for-an-earnings-scorecard-reject-observations-made-after-the-release-4dc4</guid>
      <description>&lt;p&gt;An earnings comparison can look reasonable while using information that was unavailable before the announcement. A useful first check is temporal: was the expected move actually captured before the event, and has the chosen outcome window finished?&lt;/p&gt;

&lt;p&gt;Disclosure: this article was generated with AI. I work on SIGNUM HQ. The Python below is a standalone example using synthetic timestamps, not a description of our production implementation or a claim about forecasting performance.&lt;/p&gt;

&lt;p&gt;The example accepts a comparison only when the observation precedes the release and the outcome window is complete.&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;comparable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;release_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome_ready_at&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;stamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;release_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome_ready_at&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&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;t&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stamps&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 required&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="n"&gt;snapshot_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;release_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;outcome_ready_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strict inequality at the first boundary is intentional. An observation stamped exactly at the release is not sufficient evidence that it was available beforehand. A delayed retrieval of an older observation is different: retain the original observation timestamp instead of replacing it with retrieval time.&lt;/p&gt;

&lt;p&gt;Using timezone-aware synthetic UTC timestamps, the local exercise checks five cases: a valid completed window, a post-release observation, an incomplete outcome, an outcome ending before the release, and a timestamp without a timezone. Reproduction: running the example file under Python 3.9.6 on my machine printed &lt;code&gt;scorecard_cutoff: 5 cases passed&lt;/code&gt;. No market API is called by the example.&lt;/p&gt;

&lt;p&gt;This gate is deliberately small. It cannot verify that a feed's timestamp is truthful, select the correct options expiry, establish whether prices are adjusted consistently, or decide which return window makes sense. Those definitions belong in the comparison record too.&lt;/p&gt;

&lt;p&gt;Missing results should remain missing. A stock that has not completed the selected outcome window is not an observation of zero movement. Similarly, “inside the expected magnitude” is not a profitable options trade or a correct directional prediction.&lt;/p&gt;

&lt;p&gt;For a real scorecard, I would keep the pre-event reference, release timestamp, expiry, outcome definition and data revision together. That makes the comparison inspectable before anyone summarizes it as a hit rate.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.python.org/3/library/datetime.html#aware-and-naive-objects" rel="noopener noreferrer"&gt;Python aware and naive datetime objects&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Product context, from the team that wrote this example: &lt;a href="https://www.signumhq.com/en/receipts/earnings?from=devto_receipts" rel="noopener noreferrer"&gt;Earnings verification receipts&lt;/a&gt;, a public page that keeps the pre-announcement record, expiry and outcome window next to each comparison.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>datascience</category>
      <category>testing</category>
    </item>
    <item>
      <title>How we decide which earnings options records count as pre-announcement</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:49:04 +0000</pubDate>
      <link>https://dev.to/signumhq/how-we-decide-which-earnings-options-records-count-as-pre-announcement-2ndj</link>
      <guid>https://dev.to/signumhq/how-we-decide-which-earnings-options-records-count-as-pre-announcement-2ndj</guid>
      <description>&lt;p&gt;&lt;em&gt;AI disclosure: this article was drafted with generative AI for the SIGNUM HQ team, then checked against the public page it describes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An earnings story is easy to tell after the fact. "The options market priced a 4% move, the stock fell 5.6%." But when did we record that 4%? Before the release, or after we already knew what happened? If we cannot answer that, the comparison tells a reader nothing. (Those numbers are real: they are the C row further down, and its range was reconstructed after the release, which is exactly why the question matters.)&lt;/p&gt;

&lt;p&gt;We built a public page to make the answer inspectable: &lt;a href="https://www.signumhq.com/en/receipts/earnings?from=devto_1021" rel="noopener noreferrer"&gt;Earnings verification receipts&lt;/a&gt;. This post explains the record-keeping rules behind it, using the week of Oct 12 as the worked example. It is a description of method and limits, not a claim about forecasting skill.&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%2Fd6eidb3iach0exok6epw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6eidb3iach0exok6epw.jpg" alt="Mobile view of the receipts page: counts for pre-announcement pairs, later reconstructions, unverified timing and missing comparisons" width="375" height="812"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two numbers, two different horizons
&lt;/h2&gt;

&lt;p&gt;For each scheduled report the page shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Options-implied range:&lt;/strong&gt; the at-the-money call plus put price divided by the stock price. It describes option pricing through the option's expiry. It is not a SIGNUM forecast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actual reaction:&lt;/strong&gt; one regular-session close-to-close change. For a before-open release, that is the previous trading close to the release-day close. For an after-close release, the release-day close to the next trading close. Extended-hours prices are excluded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The horizons differ on purpose, and the page says so: the implied range runs to expiry, the reaction covers one trading day. "Within range" simply means the absolute reaction was no larger than the range.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as "pre-announcement"
&lt;/h2&gt;

&lt;p&gt;The row that matters is the storage timestamp (&lt;code&gt;capturedAt&lt;/code&gt;). Only a capture stored &lt;em&gt;before&lt;/em&gt; the known release time qualifies. If the company gives no exact time, the rule is deliberately conservative: before 00:00 ET on the release day for a before-open report, or before the regular close for an after-close report.&lt;/p&gt;

&lt;p&gt;Everything else is labeled rather than dropped:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-announcement record&lt;/td&gt;
&lt;td&gt;Stored before the release by the rule above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Later reconstruction&lt;/td&gt;
&lt;td&gt;Rebuilt from end-of-day chains, or stored after the release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-announcement timing unverified&lt;/td&gt;
&lt;td&gt;Ambiguous same-day capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No record&lt;/td&gt;
&lt;td&gt;Nothing comparable, and the row stays visible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The conservative rule can leave a genuinely early record in the "unverified" bucket. We accept that, because the opposite error (promoting a record that was really written after the fact) is the one that makes the whole page worthless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The week of Oct 12 as data
&lt;/h2&gt;

&lt;p&gt;Twelve announcements were scheduled. The page counted 4 pre-announcement pairs, 6 later reconstructions, 1 unverified timing and 1 missing comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stock&lt;/th&gt;
&lt;th&gt;Options-implied range&lt;/th&gt;
&lt;th&gt;Actual reaction&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ASML&lt;/td&gt;
&lt;td&gt;±5.5%&lt;/td&gt;
&lt;td&gt;-3.94%&lt;/td&gt;
&lt;td&gt;Pre-announcement, within&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MS&lt;/td&gt;
&lt;td&gt;±4.0%&lt;/td&gt;
&lt;td&gt;-2.75%&lt;/td&gt;
&lt;td&gt;Pre-announcement, within&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TSM&lt;/td&gt;
&lt;td&gt;±3.7%&lt;/td&gt;
&lt;td&gt;-1.80%&lt;/td&gt;
&lt;td&gt;Pre-announcement, within&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BAC&lt;/td&gt;
&lt;td&gt;±4.2%&lt;/td&gt;
&lt;td&gt;-4.76%&lt;/td&gt;
&lt;td&gt;Pre-announcement, outside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;±4.0%&lt;/td&gt;
&lt;td&gt;-5.60%&lt;/td&gt;
&lt;td&gt;Later reconstruction, outside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JNJ&lt;/td&gt;
&lt;td&gt;±3.6%&lt;/td&gt;
&lt;td&gt;-5.73%&lt;/td&gt;
&lt;td&gt;Later reconstruction, outside&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last two rows are the reason the categories exist. C and JNJ both moved more than their ranges, but their ranges were rebuilt from end-of-day chains, so the page shows them for reference and keeps them out of the pre-announcement counts. Mixing them in would change the tally, and it would change what the tally means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails we kept
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two sources for closes.&lt;/strong&gt; Both closes come from one data vendor and are checked against a second source with a 0.1% tolerance. Rows without that confirmation are marked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily bars are final only after the session.&lt;/strong&gt; A reaction is not computed until the bar has had time to settle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing stays missing.&lt;/strong&gt; PLD had no announced release timing, so there is no reaction window and the row shows "No record" rather than a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not an immutable ledger.&lt;/strong&gt; Records are kept for 120 days, and manual reconstruction, price corrections and overwrites are possible. A complete revision history is not available, and the page says that too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A sketch of the classification
&lt;/h2&gt;

&lt;p&gt;This is an illustration of the rule, not our production code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bmo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;qualifies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;at&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Timing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;regularCloseET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;capturedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// exact time known&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bmo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;capturedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;startOfDayET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// before 00:00 ET on release day&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;capturedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regularCloseET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// before the regular close&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that fails this check and came from an end-of-day chain becomes a later reconstruction. The unverified bucket exists for captures the rule cannot place on either side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the counts are not
&lt;/h2&gt;

&lt;p&gt;The counts do not measure investment performance or prediction accuracy. Four pairs in one week is a very small sample, and an implied range that runs to expiry is not meant to bound a single day. The useful part is the audit trail: for every row you can see the option session, the expiry, the capture timestamp, the source and both closes, and decide for yourself how much weight it deserves.&lt;/p&gt;

&lt;p&gt;I work on SIGNUM HQ, a US stock and options research app. The page described here: &lt;a href="https://www.signumhq.com/en/receipts/earnings?from=devto_1021" rel="noopener noreferrer"&gt;https://www.signumhq.com/en/receipts/earnings?from=devto_1021&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>webdev</category>
      <category>data</category>
      <category>programming</category>
    </item>
    <item>
      <title>How we compute session VWAP from 1-minute bars when the data feed does not give you one</title>
      <dc:creator>SIGNUM HQ</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:27:48 +0000</pubDate>
      <link>https://dev.to/signumhq/how-we-compute-session-vwap-from-1-minute-bars-when-the-data-feed-does-not-give-you-one-bc</link>
      <guid>https://dev.to/signumhq/how-we-compute-session-vwap-from-1-minute-bars-when-the-data-feed-does-not-give-you-one-bc</guid>
      <description>&lt;p&gt;Our stock app showed a "VWAP" number for every ticker. For a while that number was a lie. This is a short write-up of how we found out, and how we now compute session VWAP from 1-minute bars when the data feed does not give you one.&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%2F64gjt91fspirtvkci98x.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%2F64gjt91fspirtvkci98x.png" alt="SIGNUM HQ command screen showing the NVDA session VWAP row (Korean UI)" width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The VWAP row on the stock command screen, now computed from 1-minute bars.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What was wrong
&lt;/h2&gt;

&lt;p&gt;The market-data snapshot we use has a &lt;code&gt;vwap&lt;/code&gt; field on the daily aggregate, but it is empty for a lot of tickers, during the session and on weekends. At some point someone (me) filled the gap with the usual shortcut from a daily bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vwap ≈ (high + low + close) / 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the &lt;em&gt;typical price&lt;/em&gt; of one bar. It is not a volume-weighted average of anything. It happens to land near the real VWAP on quiet days, which is why nobody noticed. On a day with a big gap and a late-session fade it can be off by a lot, and it was labelled "VWAP" in the UI.&lt;/p&gt;

&lt;p&gt;We removed the fake value. The field then became &lt;code&gt;null&lt;/code&gt; everywhere and the screen showed an em dash. Honest, but useless. So the real fix was to compute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The computation
&lt;/h2&gt;

&lt;p&gt;VWAP for a session is the sum of price times volume divided by the sum of volume, over the regular session only. We already cache the 1-minute chart bars per ticker for the chart screen, so we reuse them and make zero extra calls to the data vendor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bars: 1-minute bars, each tagged with its ET date and session (PRE | REG | POST)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sessionVwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etDate&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;          &lt;span class="c1"&gt;// newest trading day only&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etDate&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pv&lt;/span&gt;  &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;high&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;low&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// typical price x volume&lt;/span&gt;
    &lt;span class="nx"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&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="nx"&gt;vol&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="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Pre-market and after-hours bars are excluded on purpose. They are thin, they print at odd prices, and most people mean "regular session" when they say VWAP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guards that took longer than the formula
&lt;/h2&gt;

&lt;p&gt;The formula is five lines. These were the parts that actually broke:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. During the session, show only today's value.&lt;/strong&gt; An early version merged the previous trading day's value when today's bars were not there yet (right after the open, or when the cache was stale). A VWAP that silently mixes two days is worse than a dash. During market hours we now accept only bars with today's ET date, and otherwise return null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Treating &lt;code&gt;null&lt;/code&gt; from the source as authoritative wiped good data.&lt;/strong&gt; On weekends the vendor returns &lt;code&gt;vwap = null&lt;/code&gt; for every ticker. A change that said "if the source says null, show null" erased Friday's perfectly valid session VWAP. We reverted it. The rule is now: decide by the &lt;em&gt;session date&lt;/em&gt;, not by whether a field is null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Holidays are not weekends.&lt;/strong&gt; On a weekday market holiday there are no new bars, but the "latest trading day" logic must still point at the previous session. We wired the exit check to a US market holiday calendar so a Monday holiday keeps showing Friday's VWAP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Stale cache refresh without a stampede.&lt;/strong&gt; If the minute-bar cache is old, we refresh it in the background rather than making the user wait. The first version used "read lock, then set lock", which has a race: two requests could both refresh. It is now a single atomic &lt;code&gt;SET key NX EX 60&lt;/code&gt; per ticker, and if the lock is not acquired we simply do not call the vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that mattered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Empty bars (volume 0, null prices) are skipped, not treated as zero.&lt;/li&gt;
&lt;li&gt;A PRE-only or zero-volume series returns null, not 0.&lt;/li&gt;
&lt;li&gt;Two bars with known prices and volumes give an exactly checkable VWAP.&lt;/li&gt;
&lt;li&gt;A "closed" lookup time and a "last trade" time are separate inputs. One of our tests failed because it used the 20:30 ET query time as the trade time. The test was wrong, the product code was fine.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;A cheap approximation with the real name on it is a bug, even if it looks right most days.&lt;/li&gt;
&lt;li&gt;When a field is missing, decide what "missing" means (before open, holiday, vendor gap) before you decide what to display.&lt;/li&gt;
&lt;li&gt;Reusing data you already cache is the cheapest way to avoid new vendor calls and new rate limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is from the data layer of &lt;a href="https://www.signumhq.com/app?from=devto_post1" rel="noopener noreferrer"&gt;SIGNUM HQ&lt;/a&gt;, a US-stock app I build solo (options flow, dark pool prints, earnings calendar).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I used an AI assistant to help draft and edit this post. The incident, the code and the conclusions are from my own project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
