DEV Community

Hudson Enterprises
Hudson Enterprises

Posted on

Reading SEC insider trades — net buying from Form 4, with pandas

When a corporate insider — an officer, director, or 10% owner — buys or sells their own company's stock, the SEC requires a Form 4. Those filings are public and free on EDGAR. The catch is the format: one XML document per filing, terse single-letter transaction codes, and a nested relationship blob you have to decode. If you want "net insider buying by ticker" as a number, you first spend a while writing a parser.

This post skips the parser. It uses a free 100-row sample of parsed Form 4 data — one clean row per transaction — to compute net insider buying in a few lines. No signup to grab the sample.

What a parsed Form 4 row looks like

Instead of XML, each transaction is a flat, typed row:

Column Meaning
issuer_ticker / issuer_name The company
insider_name / insider_relationship Who traded and their role (JSON: officer / director / 10%-owner)
transaction_date When
transaction_code SEC Form 4 code (P, S, A, M, F, …)
shares / price_per_share / total_value_usd Size and price
shares_owned_after Holdings after the trade
accession_number / filing_url Link back to the exact filing on sec.gov

The transaction_code is the load-bearing field. The two that matter most are P (an open-market purchase — an insider buying with their own money) and S (a sale). A is a compensation grant, M is an option exercise, F is shares withheld to pay taxes.

Net insider buying in a few lines

Download the sample from secdata.hudsonenterprisesllc.cominsider-trades-form4_2026-07-07_sample.csv. Then bucket each code into acquire vs dispose and sum by ticker:

import pandas as pd

df = pd.read_csv("insider-trades-form4_2026-07-07_sample.csv")

acquire = {"P", "A", "M"}   # purchase, grant/award, option exercise
dispose = {"S", "F"}        # sale, tax withholding

df["signed_shares"] = df.apply(
    lambda r: r["shares"] if r["transaction_code"] in acquire
    else -r["shares"] if r["transaction_code"] in dispose
    else 0.0,
    axis=1,
)

net = (
    df.groupby("issuer_ticker")["signed_shares"].sum()
    .rename("net_insider_shares").reset_index()
)

print(
    net.sort_values("net_insider_shares", ascending=False)
    .head(10).to_string(index=False)
)
Enter fullscreen mode Exit fullscreen mode

That's the whole thing — group, sign, sum. The 100-row sample is a thin slice, so treat the specific tickers as a shape check; the full dataset is 80,700 transactions.

A couple of judgment calls worth making explicit, because they're where naive insider analysis goes wrong:

  • Grants and option exercises aren't conviction. Bucketing A and M as "acquire" inflates buying — an executive being handed equity is not the same signal as one buying on the open market. If you only care about the classic signal, filter to transaction_code == "P" and ignore the rest.
  • Direction beats magnitude. One insider buying with their own money often matters more than a larger routine sale by a compensated executive. Read insider_relationship alongside the code.

Because every row carries accession_number and filing_url, you can open any transaction against the original Form 4 on sec.gov and confirm it — nothing is a black box.

The full dataset

The full pack is 80,700 insider transactions as Parquet (plus a gzip CSV), with a data dictionary and a runnable notebook — a one-time $29 download at secdata.hudsonenterprisesllc.com. Two companion packs are there too: S&P 500 fundamentals (CSV, $19) and the latest quarter of 13F institutional holdings (Parquet, $29). All are point-in-time snapshots dated 2026-07-07 — the date is in every filename, so it's reproducible and citable. If you'd rather pull this from a live API than a one-time file, that's the Filingrail API.

Source: SEC EDGAR (U.S. government, public domain). These are lawful, public Section 16 disclosures — descriptive historical data, not investment advice.

Top comments (0)