<?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: itsRaxzey</title>
    <description>The latest articles on DEV Community by itsRaxzey (@itsraxzey).</description>
    <link>https://dev.to/itsraxzey</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%2F4058335%2F9478eb6e-5b64-4cd4-8b4e-1c653ad79870.jpg</url>
      <title>DEV Community: itsRaxzey</title>
      <link>https://dev.to/itsraxzey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itsraxzey"/>
    <language>en</language>
    <item>
      <title>8-K corporate events hide in SGML headers: how to classify them</title>
      <dc:creator>itsRaxzey</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:36:04 +0000</pubDate>
      <link>https://dev.to/itsraxzey/8-k-corporate-events-hide-in-sgml-headers-how-to-classify-them-14p2</link>
      <guid>https://dev.to/itsraxzey/8-k-corporate-events-hide-in-sgml-headers-how-to-classify-them-14p2</guid>
      <description>&lt;p&gt;If you want to know &lt;em&gt;the moment&lt;/em&gt; a public company reports earnings, fires a CEO, or signs a merger, the source of truth is a Form 8-K on SEC EDGAR. There's a catch that surprises everyone who tries to consume 8-Ks programmatically: &lt;strong&gt;the thing you actually want — which "items" the 8-K reports — is not in the document. It's in the filing's SGML header.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://filingpulse.io" rel="noopener noreferrer"&gt;FilingPulse&lt;/a&gt;, an API that normalizes EDGAR filings, so I've had to make this robust across every 8-K the SEC has accepted. Here's what the problem looks like and how the classifier works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an 8-K "item" is
&lt;/h2&gt;

&lt;p&gt;8-K disclosures are keyed to a fixed list of item numbers. A few you'll recognize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2.02&lt;/strong&gt; — Results of Operations (earnings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5.02&lt;/strong&gt; — Departure/Election of Directors or Officers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.01&lt;/strong&gt; — Entry into a Material Definitive Agreement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7.01&lt;/strong&gt; — Regulation FD Disclosure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8.01&lt;/strong&gt; — Other Events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One filing can carry several items at once. A single 8-K might be &lt;code&gt;[2.02, 7.01, 9.01]&lt;/code&gt; — an earnings release, a Reg-FD statement, and the exhibits index. So the useful signal is &lt;em&gt;which item codes are present&lt;/em&gt;, structured, not prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: the body doesn't reliably contain the codes
&lt;/h2&gt;

&lt;p&gt;The primary 8-K document is HTML meant for humans. Sometimes it lists "Item 2.02" as a heading; sometimes it just says "Results of Operations and Financial Condition"; sometimes the whole thing is a press release attached as an exhibit and the body barely mentions the item at all. Parsing the body for item codes is a losing game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: the index-headers file
&lt;/h2&gt;

&lt;p&gt;Every filing folder on EDGAR publishes a small header file — the SGML submission header wrapped in HTML — at a predictable URL:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.../Archives/edgar/data/&amp;lt;CIK&amp;gt;/&amp;lt;ACCESSION_NODASH&amp;gt;/&amp;lt;ACCESSION&amp;gt;-index-headers.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside, each reported item shows up as a line like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ITEM INFORMATION: Results of Operations and Financial Condition
ITEM INFORMATION: Financial Statements and Exhibits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So you never parse the document at all. You pull one small header file, read the &lt;code&gt;ITEM INFORMATION:&lt;/code&gt; lines, and map each caption back to its code:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import re, html

&lt;p&gt;def item_codes(header_text):&lt;br&gt;
    caps = re.findall(r"ITEM INFORMATION:\s*([^\r\n&amp;lt;]+)", header_text)&lt;br&gt;
    return [CAPTION_TO_CODE.get(norm(html.unescape(c))) for c in caps]&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Two quirks that will bite you&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Building the caption-to-code map from the SEC's official item list &lt;em&gt;almost&lt;/em&gt; works. Two things break it against real filings:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. EDGAR's captions don't always match the form instructions.&lt;/strong&gt; The official text for item 3.03 is "Material Modification to Rights of Security Holders" (singular). A large share of real headers say "Material &lt;strong&gt;Modifications&lt;/strong&gt;" (plural). Match captions exactly and you silently drop 3.03 events. You need an alias table built from what EDGAR actually emits, not from the rulebook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Captions are HTML-escaped.&lt;/strong&gt; Apostrophes arrive as &lt;code&gt;&amp;amp;#39;&lt;/code&gt;, ampersands as &lt;code&gt;&amp;amp;amp;&lt;/code&gt;. "Changes in Registrant's Certifying Accountant" won't match unless you unescape first.&lt;/p&gt;

&lt;p&gt;I found both the only way you ever really find these: by classifying a live feed and watching which items came back as unmapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why headers, not full-text search
&lt;/h2&gt;

&lt;p&gt;People reach for EDGAR full-text search or scraping the document because it's the obvious path. The header approach wins on three axes: it's &lt;strong&gt;one small request&lt;/strong&gt; per filing instead of downloading a multi-MB exhibit bundle; it's &lt;strong&gt;structured&lt;/strong&gt; (the SEC already classified it at filing time); and it's &lt;strong&gt;complete&lt;/strong&gt; (every item the filer declared is there, even the ones the body forgets to mention).&lt;/p&gt;

&lt;h2&gt;
  
  
  If you just want the JSON
&lt;/h2&gt;

&lt;p&gt;This is one endpoint in FilingPulse — &lt;code&gt;/v1/events?item=2.02&lt;/code&gt; returns every earnings 8-K, newest first, as normalized JSON, and webhooks push new ones within minutes of filing. Free tier, no card. But honestly the header trick above is the whole secret and you can run it against EDGAR yourself in an afternoon — the fair-access rules just want a declared User-Agent and a sane request rate.&lt;/p&gt;

&lt;p&gt;It's data infrastructure, not investment advice — it reports what was filed, nothing else. Happy to answer EDGAR questions in the comments.&lt;/p&gt;

</description>
      <category>sec</category>
      <category>api</category>
      <category>python</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Parsing SEC EDGAR's Form 4 feed: the edge cases nobody warns you about</title>
      <dc:creator>itsRaxzey</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:46:14 +0000</pubDate>
      <link>https://dev.to/itsraxzey/parsing-sec-edgars-form-4-feed-the-edge-cases-nobody-warns-you-about-1gm0</link>
      <guid>https://dev.to/itsraxzey/parsing-sec-edgars-form-4-feed-the-edge-cases-nobody-warns-you-about-1gm0</guid>
      <description>&lt;p&gt;Every US public-company insider trade is public data. When an executive buys or sells their own stock, they have two business days to file a Form 4 with the SEC, and it lands on EDGAR — free, no login, no API key. I spent the last month building a pipeline that ingests every one of them in near-real-time, and the gap between "the data is public" and "the data is usable" turned out to be the entire project.&lt;/p&gt;

&lt;p&gt;Here's the field guide I wish I'd had.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The feed's form-type filter is a lie (it's a prefix match)
&lt;/h2&gt;

&lt;p&gt;EDGAR's current-filings feed takes a &lt;code&gt;type&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&amp;amp;type=4&amp;amp;output=atom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'd expect &lt;code&gt;type=4&lt;/code&gt; to return Form 4s. It returns everything that &lt;em&gt;starts with&lt;/em&gt; 4 — including 424B2 prospectuses and 425 merger communications, which have nothing to do with insider trading. You must re-filter on the exact form type parsed from each entry's title. This isn't documented anywhere I could find; you discover it when a bond prospectus shows up in your insider-trades table.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every filing appears twice
&lt;/h2&gt;

&lt;p&gt;The same filing is listed once under the reporting owner's CIK and once under the issuer's CIK — same accession number, different index URLs. If you dedupe on URL, you store everything twice. The accession number (&lt;code&gt;0001628280-26-049369&lt;/code&gt;) is the real identity of a filing; treat it as your primary key and make inserts idempotent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;IGNORE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;filings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Not every XML in the filing folder is the filing
&lt;/h2&gt;

&lt;p&gt;A Form 4 filing folder contains several files, and the tempting move — "grab the .xml" — breaks quietly, because filing-fee exhibit XMLs live in the same folder. Validate the root element (&lt;code&gt;&amp;lt;ownershipDocument&amp;gt;&lt;/code&gt;) before parsing, not the file extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Some Form 4s contain no transactions at all
&lt;/h2&gt;

&lt;p&gt;Holdings-only filings exist: no transaction table, no holdings table, just remarks and a signature. They're legal, they're rare, and they will null-pointer your parser at 2am. Multi-owner filings also exist — one filing reporting for three retirement-plan trustees simultaneously — so "the reporting owner" is actually a list.&lt;/p&gt;

&lt;p&gt;Amendments (form type &lt;code&gt;4/A&lt;/code&gt;) reference the original filing and carry an amendment date. If you don't handle them, your dataset double-counts corrected transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The interesting data hides in footnotes
&lt;/h2&gt;

&lt;p&gt;Share counts and prices carry footnote references (&lt;code&gt;&amp;lt;footnoteId id="F1"/&amp;gt;&lt;/code&gt;), and the footnote text often changes the meaning of the number — "price is a weighted average of executions between $18.11 and $18.29" — so a parser that drops footnotes is destroying information. Keep them attached to the fields they annotate.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Decide your philosophy: values as filed, or values as "fixed"
&lt;/h2&gt;

&lt;p&gt;The biggest design decision in the whole pipeline: numbers in filings are typed by humans at law firms. They contain typos, weird formats, and occasionally nonsense. You can either coerce everything to floats (and silently corrupt the record when coercion guesses wrong) or deliver strings exactly as filed and let consumers coerce at their edge.&lt;/p&gt;

&lt;p&gt;I chose strings-as-filed, and it's the choice I'd defend hardest: this is &lt;em&gt;federal filing data&lt;/em&gt;. If an insider fat-fingers a share count, you should see what the SEC saw — plus two invariants that make the schema livable: every documented field is always present, and &lt;code&gt;null&lt;/code&gt; always means "not stated in the filing."&lt;/p&gt;

&lt;h2&gt;
  
  
  7. HTML entities are everywhere
&lt;/h2&gt;

&lt;p&gt;Issuer names arrive HTML-escaped in some feed views and not others: &lt;code&gt;Organon &amp;amp;amp; Co.&lt;/code&gt; in one place, &lt;code&gt;Organon &amp;amp; Co.&lt;/code&gt; in another. Normalize with &lt;code&gt;html.unescape()&lt;/code&gt; on ingest or enjoy duplicate-company bugs forever. (The related 8-K feed also HTML-escapes apostrophes in item captions, which breaks caption-matching — same fix.)&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The 403 that isn't a throttle
&lt;/h2&gt;

&lt;p&gt;The nastiest production incident so far: EDGAR serves &lt;strong&gt;403 Forbidden — not 404 — for daily-index files that were never published&lt;/strong&gt; (weekends, holidays). A 403 is also exactly what EDGAR's rate limiter serves when you're being throttled. My backfill hit a Sunday index file, interpreted the 403 as throttling, and politely retried the same URL on a backoff loop for 17 hours.&lt;/p&gt;

&lt;p&gt;The fix is a canary request: on a 403 for an index file, immediately fetch a URL you know exists. If the canary returns 200, you're not throttled — the index is genuinely absent, and that day can be checkpointed as empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Be polite — it's cheap and it's the rules
&lt;/h2&gt;

&lt;p&gt;EDGAR's fair-access guidance asks for a declared User-Agent with contact info and a cap of 10 req/s. I run a process-wide rate limiter pinned to 2 req/s with exponential backoff on 403/429 — one enforcement point that every request goes through, so adding a new worker can't accidentally exceed the budget. The entire pipeline — feed polling, two years of backfill, ~90,000 Form 4s and ~30,000 8-Ks so far — runs comfortably inside that on a €5 VPS with SQLite in WAL mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The punchline
&lt;/h2&gt;

&lt;p&gt;None of these problems is hard in isolation. The product is surviving &lt;em&gt;all of them at once&lt;/em&gt;, continuously, unattended — which is why "the data is free" and "the data is usable" are separated by a parser with a regression corpus of real filings: the amendments, the holdings-only ones, the multi-owner ones, the footnote-heavy ones.&lt;/p&gt;

&lt;p&gt;I packaged all of this as &lt;a href="https://filingpulse.io" rel="noopener noreferrer"&gt;FilingPulse&lt;/a&gt; — normalized Form 4 + 8-K JSON over REST and HMAC-signed webhooks, free tier with the full schema (2,500 req/mo, email signup, no card). If you'd rather build your own pipeline, honestly: take the edge-case list above, it's yours. That's the part I couldn't Google a month ago.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Standard disclaimer that matters in this domain: this is data infrastructure. Nothing here is investment advice — the API reports what was filed, nothing more.&lt;/em&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
