When somebody crosses 5 percent ownership of a public company, they have to say so. The form they use tells you almost everything about their intent.
Schedule 13D is the activist filing. You file it when you might actually do something: push for board seats, agitate for a sale, run a proxy fight. It comes with a deadline measured in days and a requirement to describe your purpose.
Schedule 13G is the passive one. Index funds and long-only managers who happen to own more than 5 percent file it, and it says, in effect, we are just holding this.
Same threshold, very different signal. A 13D landing on a mid-cap is news. A 13G from Vanguard is Tuesday. This guide pulls both and tells them apart.
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_..."Recent ownership filings for a company
import itertools
from py3spread import Client
TICKER = "AAPL"
with Client() as client:
filings = list(itertools.islice(client.beneficial_ownership.iter(ticker=TICKER), 10))
if not filings:
print(f"no 13D/13G filings for {TICKER}")
raise SystemExit
print(f"recent beneficial ownership filings for {TICKER}:")
for f in filings:
kind = f.get("schedule_type") or f["form_type"]
amended = " (amendment)" if f["is_amendment"] else ""
event = f" event {f['event_date']}" if f.get("event_date") else ""
print(f" {f['accepted_time'][:10]} {kind:<6}{event}{amended}")schedule_type is the field that matters: it is what separates a 13D from a
13G. Falling back to form_type covers the filings where the schedule is not
stated separately.
Two other fields worth understanding. is_amendment tells you this is an
update to an existing position rather than a new one, and amendments are where
the interesting movement lives: an activist raising their stake, or exiting.
event_date is the date the ownership event actually happened, which is not the
same as when it was filed. That gap is the reporting lag, and it can be days.
Who is actually behind the stake
The filing detail carries the reporting persons, which is where the names are:
detail = client.beneficial_ownership.get(filings[0]["filing_id"])
print(f"\nlatest filing: {detail['filing_id']}")
for p in detail.get("reporting_persons") or []:
pct = p.get("percent_of_class")
pct = f"{float(pct):.1f}% of class" if pct else "pct not stated"
print(f" {p['names']} ({pct}, source of funds {p.get('source_of_funds') or '?'})")
print(f"source: {detail['source_url']}")percent_of_class is the size of the stake. source_of_funds is a small field
that occasionally says a lot: it is a coded hint about where the money came
from, and a stake funded by borrowings reads differently from one funded from a
fund's working capital.
Note the defensive .get() calls and the or []. Beneficial ownership filings
are less rigidly structured than Form 4s, and not every filer populates every
field. Code that assumes all of them are present will break on real data.
source_url points at the original filing on EDGAR, which is worth keeping in
anything you build. When a number looks strange, you want to be one click from
the document that produced it.
Where to take it
Drop the ticker filter and iterate the whole family, keeping only rows where
schedule_type is 13D and is_amendment is false, and you have an activist
alert: every new activist position being opened, across the market, as it is
disclosed.
Pair it with the insider transactions dataset and you can ask a sharper question: when an activist shows up, do the insiders start buying alongside them, or quietly selling into the rally?
The dataset
Full endpoint reference and response schemas are on the beneficial ownership dataset page.