MLQ.ai
About Sign in Subscribe
API GUIDES · COOKBOOK

Build a 13F tracker

What any fund holds, what changed this quarter, and who owns any stock — reconciled to the filings.

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.

The Power Delivery GapReport
Research

The Power Delivery Gap

The interconnection queue holds more than 1,100 GW of power. The grid delivers renewables quickly and firm gas slowly, if at all. A model of what the US grid can actually deliver, and when, built on MLQ's queue and generator data.

No spam. Unsubscribe anytime.