3spread
All guides

Build an insider trading screener in Python

Pull every Form 4 insider transaction for a ticker, measure buy/sell sentiment, and rank the largest trades. About 30 lines of Python against a free API key.

The 3spread TeamJuly 14, 2026Insider transactions dataset

Corporate insiders (officers, directors, and 10 percent owners) have to report their trades to the SEC on Form 4, usually within two business days. That makes Form 4 one of the few genuinely timely signals in public filings: you learn what the people closest to a company did with their own money, days after they did it.

The raw filings are XML, one document per report, with the transaction split across derivative and non-derivative tables. This guide skips all of that. We will pull the parsed transactions straight from the API and end up with a ranked screen of the largest insider trades in a name.

Everything below runs on the free Community tier: every dataset, 10,000 requests per day (600 per minute), no credit card.

Setup

pip install py3spread
export THREESPREAD_API_KEY="sk_live_..."

Grab a key at 3spread.com/auth/signup. The client reads THREESPREAD_API_KEY from the environment, so you never put it in the code.

Is the insider sentiment bullish or bearish?

Start with the aggregate. The API has a derived buy/sell ratio endpoint, so you do not have to sum transactions yourself:

from decimal import Decimal
from py3spread import Client
 
TICKER = "AAPL"
 
with Client() as client:
    ratio = client.insiders.buy_sell_ratio(ticker=TICKER, window_days=90)
 
    print(f"{TICKER} insider activity, {ratio['since']} to {ratio['until']}:")
    print(f"  buys:  {ratio['buys_count']:>4}  ${Decimal(ratio['buys_dollars']):,.0f}")
    print(f"  sells: {ratio['sells_count']:>4}  ${Decimal(ratio['sells_dollars']):,.0f}")

One call, and you know whether insiders in this name have been net buyers or net sellers over the last quarter. Worth knowing before you read too much into any single trade.

Rank the largest trades

Now the individual transactions. iter_transactions follows pagination for you and yields one row per reported transaction, so you can just loop:

import datetime as dt
 
DAYS = 180
 
end = dt.date.today()
start = end - dt.timedelta(days=DAYS)
 
trades = {}
for txn in client.insiders.iter_transactions(
    issuer_ticker=TICKER,
    transaction_start=str(start),
    transaction_end=str(end),
    transaction_kind="nonderiv",
):
    if not txn.get("transaction_total_value"):
        continue
    # One filing can report the same transaction more than once. Key on the
    # accession number plus the record index to keep it unique.
    accession = txn["filing_id"].split("_", 1)[1]
    trades[(accession, txn["record_index"])] = txn
 
trades = list(trades.values())
trades.sort(key=lambda t: Decimal(t["transaction_total_value"]), reverse=True)
 
print(f"\nlargest trades in the last {DAYS} days:")
for t in trades[:10]:
    action = "bought" if t["transaction_acquired_disposed_code"] == "A" else "sold"
    value = Decimal(t["transaction_total_value"])
    print(
        f"  {t['transaction_date']}  {t['filer_name']:<30} {action} "
        f"{Decimal(t['transaction_shares']):,.0f} shares (${value:,.0f})"
    )

Three things worth pointing out, because they are the parts people get wrong when they parse Form 4 themselves.

transaction_kind="nonderiv" filters to ordinary share transactions. Derivative rows (options, RSUs, warrants) are a different animal: an option grant is not someone buying stock on the open market, and mixing the two is the fastest way to build a screen that lights up on compensation events instead of conviction.

transaction_acquired_disposed_code is the direction: A for acquired, D for disposed. It is a single letter in the filing, and it is the whole difference between a buy and a sell.

The dedupe on (accession, record_index) matters because a filing can restate or repeat a transaction. Keying on the pair keeps one row per real event.

Where to take it

The obvious next move is to widen the screen. Swap the single ticker for a watchlist and keep the date window tight, which keeps the request count sane as well as the results readable. Then look for clusters: several different insiders at the same company buying inside the same week is a much stronger signal than one director topping up.

From there, client.insiders.owners(...) and client.insiders.biography(...) let you follow a single person across every company they file at, which is how you find the serial director whose buys actually tend to precede something.

The dataset

Full endpoint reference, response schemas and the other ways into this data are on the insider transactions dataset page. The interactive API reference has every parameter.

Run this yourself

Everything here works on the free Community tier: every dataset, 10,000 requests per day (600 per minute), no credit card. Provision a key and paste the code in.