Every quarter, institutions managing over $100M disclose their US holdings on SEC Form 13F. Two views matter: a fund's portfolio (what does Berkshire own?) and a stock's ownership (which funds hold NVDA?). This cookbook builds both, plus the thing trackers are really for: what changed.
1 · A fund's portfolio
Funds are identified by SEC CIK (Berkshire Hathaway is 1067983 — find others via
EDGAR full-text search or ask the MCP). One row per security; multi-manager
sub-entity rows are already summed, and equity / puts / calls / notes stay separate, labeled rows:
import requests
BASE = "https://mlq.ai/api/v1"
H = {"Authorization": "Bearer YOUR_KEY"}
r = requests.get(f"{BASE}/funds/1067983/holdings/", params={"limit": 500}, headers=H)
pf = r.json()
total = sum(h["value"] or 0 for h in pf["results"])
print(pf["investorName"], pf["periodOfReport"], f"${total/1e9:.1f}B, {pf['count']} positions")
for h in pf["results"][:10]:
label = h["symbol"] or h["issuerName"]
if h["putCall"]:
label += f" ({h['putCall']}s)" # options are separate labeled rows
print(f" {label:28} {h['shares']:>15,.0f} sh ${h['value']/1e9:>6.2f}B")
Read the labels. putCall = "Put"/"Call" rows report the
underlying share count of an option position; sharesType: "PRN" rows report
bond principal, not shares. Summing blindly across them is the classic 13F-tracker bug — the rows
are separated precisely so you can't make it silently.
2 · What changed this quarter
Snapshot each quarter, then diff by CUSIP: new positions, exits, adds, trims.
import json, pathlib
def snapshot(cik):
r = requests.get(f"{BASE}/funds/{cik}/holdings/", params={"limit": 1000}, headers=H)
pf = r.json()
# key by (cusip, putCall) so "AAPL" and "AAPL calls" never collide
return pf["periodOfReport"], {(h["cusip"], h["putCall"]): h for h in pf["results"]}
period, cur = snapshot("1067983")
store = pathlib.Path("brk_last.json")
if store.exists():
prev_period, prev = json.loads(store.read_text())
prev = {tuple(json.loads(k)): v for k, v in prev.items()}
if period != prev_period: # a new quarter landed
for k, h in cur.items():
name = h["symbol"] or h["issuerName"]
if k not in prev:
print(f"NEW {name:26} ${h['value']/1e6:,.0f}M")
elif (h["shares"] or 0) != (prev[k]["shares"] or 0):
d = (h["shares"] or 0) - (prev[k]["shares"] or 0)
print(f"{'ADD ' if d > 0 else 'TRIM':5} {name:26} {d:+,.0f} sh")
for k, h in prev.items():
if k not in cur:
print(f"EXIT {h['symbol'] or h['issuerName']}")
store.write_text(json.dumps([period, {json.dumps(list(k)): v for k, v in cur.items()}]))
Run it on a schedule (13Fs land ~45 days after each quarter-end, in waves through mid-Feb / mid-May / mid-Aug / mid-Nov). Wire the prints to email and you have the tracker product people charge $14/month for.
3 · Who owns a stock
r = requests.get(f"{BASE}/companies/NVDA/institutional-ownership/",
params={"limit": 15}, headers=H)
j = r.json()
print(f"{j['count']:,} institutional holders as of {j['periodOfReport']}")
for h in j["results"]:
print(f" {h['investorName'][:36]:36} {h['shares']:>16,.0f} sh")
One row per fund, largest first, equity only by default — add
include_options=true to also see put/call/note rows (still separate and labeled,
so an options-heavy filer can never inflate the share count). Want the bearish side? That's a
one-liner: filter the options rows for putCall == "Put".
4 · Why you can trust the numbers
- Reconciled to the filer's own cover page: parsed portfolio totals are checked against each filing's self-reported total (99%+ reconcile; see accuracy).
- Amendments handled: restatement amendments supersede the original filing; additive ones merge. You always see the current version of the quarter.
- Value units normalized: older filings reported $thousands; everything here is actual dollars.
- Provenance: every row carries the source accession — one click to the filing on EDGAR.
Using an agent instead?
Over the MCP: "Using MLQ, what did Berkshire buy and sell last
quarter? Compare its two most recent portfolios and cite the filings." The
fund_holdings and institutional_ownership tools do the rest.