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_afteris 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) andF(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/sicto get the tickers.