Institutional investment managers with more than $100 million in qualifying assets have to disclose their equity positions every quarter on Form 13F. It is the closest thing there is to a public register of who owns what: the funds, the size of their stakes, and how those stakes change.
The catch is that 13F is filed per manager, not per security. If you want to know who owns a stock, you are implicitly asking to invert thousands of filings. This guide does that in one call, then drills into a single manager.
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_..."Who owns it, largest first
13F reports positions by CUSIP, so that is what we query on. iter_holdings
streams across managers' filings, so a filter on CUSIP inverts the question for
you: instead of one manager's book, you get one security's holders.
import itertools
from decimal import Decimal
from py3spread import Client
CUSIP = "037833100" # Apple
with Client() as client:
holdings = list(itertools.islice(
client.institutional_holdings.iter_holdings(
cusip=CUSIP,
min_value=1_000_000_000, # positions over $1B
sort="value",
order="desc",
),
20,
))
if not holdings:
print("no holdings found")
raise SystemExit
print(f"largest reported positions in {holdings[0]['name_of_issuer']}:")
for h in holdings:
value = Decimal(h["value_usd"])
shares = Decimal(h["ssh_prnamt"])
print(
f" {h['filing_manager_name']:<45} ${value / 10**9:,.1f}B "
f"({shares:,.0f} {h['ssh_prnamt_type']}, period {h['period_of_report']})"
)itertools.islice is doing real work here, and not only for tidiness.
iter_holdings will keep paging for as long as you let it, and every page is a
request against your daily budget. Slicing the iterator stops the pagination as
soon as you have your twenty rows, instead of pulling thousands and throwing
most of them away. Get in the habit: bound the iterator, filter server-side, and
you will not notice the limits.
Two fields to understand before you trust the output. value_usd is the
reported market value of the position, which is what you almost always want to
sort on. ssh_prnamt is the share or principal amount, and ssh_prnamt_type
tells you which of those it is, because 13F does not only cover common stock.
Sorting on shares without checking the type is how you end up comparing a bond
position to an equity one.
Drill into one manager
Once you have a manager, their filing history is one more call:
manager = holdings[0]
print(f"\nrecent 13F filings by {manager['filing_manager_name']}:")
page = client.institutional_holdings.list(
filing_manager_cik=manager["filing_manager_cik"], limit=5
)
for f in page["data"]:
print(f" {f['accepted_time']} {f['form_type']} period {f['period_of_report']}")Note the two different time fields, because confusing them is the classic 13F
mistake. period_of_report is the quarter the holdings describe.
accepted_time is when the filing actually landed at the SEC, which can be up
to 45 days later. If you are backtesting, accepted_time is the honest one: it
is when you could have known.
Where to take it
Pull two consecutive quarters for the same manager and diff the holdings, and you have their trades: new positions, exits, adds, trims. That is the basis of every "what did the big funds buy last quarter" story, and it is a couple of dictionaries and a set difference away from the code above.
Widen it and you can measure crowding: for a given security, how concentrated is institutional ownership, and is it rising? A stock where the top five holders keep adding is a different setup from one being quietly distributed.
The dataset
Full endpoint reference and response schemas are on the 13F institutional holdings dataset page, and every parameter is in the interactive API reference.