Home / Guides / Ownership & holdings
Ownership & holdings

Insider trading analysis for one company

Pull every open market insider purchase and sale for a ticker from Form 4 data, summarize by year, and rank insiders by value sold.

Form 4 filings record every trade a company's officers, directors and 10% owners make in its stock. For a single company, the full history answers useful research questions: how much do insiders sell each year, who sells the most, and how much of the selling runs through pre-arranged Rule 10b5-1 plans?

This tutorial builds that profile for one ticker with the SecFeed Insider Trading API.

Fetch the transactions

GET /v1/insider-trading returns one row per transaction. Filter with ticker, code (comma-separated SEC transaction codes) and startDate, and page with from and size (up to 200 per page). Code P is an open market purchase and S an open market sale.

import os
from collections import defaultdict
import requests

BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
TICKER = "JPM"


def transactions(ticker, codes, start):
    rows, offset = [], 0
    while True:
        r = requests.get(f"{BASE}/v1/insider-trading", headers=HEADERS, timeout=120,
                         params={"ticker": ticker, "code": codes, "startDate": start, "from": offset, "size": 200})
        r.raise_for_status()
        page = r.json()["transactions"]
        rows += page
        if len(page) < 200:
            return rows
        offset += 200


rows = transactions(TICKER, "P,S", "2022-01-01")
print(f"{TICKER}: {len(rows)} open market transactions since 2022")
print()

by_year = defaultdict(lambda: {"P": [0, 0.0], "S": [0, 0.0]})
for t in rows:
    if (t["price"] or 0) > 5000:  # skip obvious price typos in the source filings
        continue
    y = t["tx_date"][:4]
    by_year[y][t["code"]][0] += 1
    by_year[y][t["code"]][1] += t["value"] or 0

print(f"{'Year':<6}{'Buys':>6}{'Buy value':>16}{'Sales':>8}{'Sale value':>18}{'10b5-1 sales':>15}")
plan = defaultdict(int)
for t in rows:
    if t["code"] == "S" and t["aff10b5one"]:
        plan[t["tx_date"][:4]] += 1
for y in sorted(by_year):
    b, s = by_year[y]["P"], by_year[y]["S"]
    print(f"{y:<6}{b[0]:>6}{b[1]:>16,.0f}{s[0]:>8}{s[1]:>18,.0f}{plan[y]:>15}")

Output:

JPM: 151 open market transactions since 2022

Year    Buys       Buy value   Sales        Sale value   10b5-1 sales
2022       1         100,286       9         9,743,144              0
2023       1          50,449      21        40,973,050             15
2024       0               0      37       222,696,474             37
2025       0               0      40       334,452,252             40
2026       0               0      42       126,049,070             42

The aff10b5one field is the Rule 10b5-1 checkbox that Forms 4 and 5 have carried since April 2023, so sales before that date show 0 in the last column by construction, not because no plans existed. From 2024 on, every open market sale in this sample was made under a trading plan.

The price filter drops rows with an implausible price per share. Form 4 data is typed by filers, and the occasional misplaced decimal would otherwise dominate a dollar total.

Rank insiders by value sold

Each transaction carries owners, the reporting persons with their relationship to the company: isDirector, isOfficer, officerTitle and isTenPercentOwner.

import os
from collections import defaultdict
import requests

BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}

r = requests.get(f"{BASE}/v1/insider-trading", headers=HEADERS, timeout=120,
                 params={"ticker": "JPM", "code": "S", "startDate": "2025-01-01", "size": 200})
rows = r.json()["transactions"]

people = defaultdict(lambda: {"title": "", "sales": 0, "shares": 0.0, "value": 0.0})
for t in rows:
    o = t["owners"][0]
    p = people[o["name"]]
    p["title"] = o["officerTitle"] or ("Director" if o["isDirector"] else "")
    p["sales"] += 1
    p["shares"] += t["shares"] or 0
    p["value"] += t["value"] or 0

print(f"{'Insider':<24}{'Role':<30}{'Sales':>6}{'Shares':>12}{'Value':>16}")
for name, p in sorted(people.items(), key=lambda x: -x[1]["value"])[:8]:
    print(f"{name[:23]:<24}{p['title'][:29]:<30}{p['sales']:>6}{p['shares']:>12,.0f}{p['value']:>16,.0f}")

Output:

Insider                 Role                           Sales      Shares           Value
DIMON JAMES             Chairman & CEO                    15   1,200,000     326,630,684
Rohrbaugh Troy L        Co-CEO CIB                         4     125,000      34,938,500
Erdoes Mary E.          CEO Asset & Wealth Management      7      55,065      15,816,152
Barnum Jeremy           Chief Financial Officer            5      54,432      14,480,468
Piepszak Jennifer       Chief Operating Officer            7      43,428      12,357,398
Lake Marianne           CEO CCB                            7      41,408      11,838,715
BACON ASHLEY            Chief Risk Officer                 6      36,517      10,444,827
Petno Douglas B         Co-CEO CIB                         7      35,415      10,157,133

Ideas to extend it

  • Holdings context: owned_after is the position after each trade. Divide shares sold by shares held before the sale to see what fraction of a stake was sold.
  • Footnotes: many sales are reported as a weighted average price, with the price range in footnotes. Keep them when you need execution detail.
  • Other codes: add M (option exercise) and F (shares withheld for taxes) to see how much selling follows equity awards.
  • Compare companies: loop the same summary over a sector list, using /v1/mapping/sic to get the tickers.
Insider transactions are a disclosure record, not a recommendation. Planned sales under 10b5-1 are scheduled in advance by design.

Next steps

More ownership & holdings guides

Run this guide with your own key

500 free API calls a month. Every example on this page works on the free plan.

Get free API key →