Python Intermediate

Institutional Holdings Tracker

Track which hedge funds and institutions hold a stock using 13F data from SEC filings.

Note: The code examples in this tutorial have not yet been verified against the live API. If you encounter issues, please let us know.

What You'll Build

A command-line tool that shows who the biggest institutional holders of a stock are. Institutional investors — mutual funds, hedge funds, pension funds, and other managers with over $100 million in assets — are required to disclose their holdings quarterly through SEC 13F filings. This tool pulls that data and presents it in a readable table, so you can see exactly which major players own a stock and how much they hold.

You will also add a --compare flag that compares two quarters side by side, showing which institutions increased or decreased their positions.

Prerequisites

  • Python 3.8 or later
  • The requests library (pip install requests)
  • A stockdata.dev API key — get one free

The free tier includes 1,000 API calls per month. Each run of this script uses 1-3 API calls depending on whether you use the compare feature.

The Code

Create a file called holdings.py. This script fetches institutional holdings for a given ticker and displays the top holders in a formatted table.

Python
import sys
import argparse
import requests

API_KEY = "your_api_key_here"
BASE_URL = "https://api.stockdata.dev/v1"
HEADERS = {"X-API-Key": API_KEY}


def format_number(n):
    """Format a large number with commas."""
    return f"{n:,}"


def format_value(val_thousands):
    """Format a value in thousands (13F convention) to display dollars."""
    # 13F reports values in thousands of USD
    val = val_thousands * 1000
    if val >= 1e12:
        return f"${val / 1e12:.1f}T"
    if val >= 1e9:
        return f"${val / 1e9:.1f}B"
    if val >= 1e6:
        return f"${val / 1e6:.1f}M"
    return f"${val:,.0f}"


def fetch_holdings(ticker, period=None):
    """Fetch institutional holdings for a ticker."""
    params = {}
    if period:
        params["period"] = period

    resp = requests.get(
        f"{BASE_URL}/institutions/{ticker}",
        headers=HEADERS,
        params=params,
    )
    if resp.status_code != 200:
        print(f"Error: {resp.json().get('error', resp.text)}")
        return None
    return resp.json()


def fetch_company(ticker):
    """Fetch company name for display."""
    resp = requests.get(
        f"{BASE_URL}/company/{ticker}",
        headers=HEADERS,
    )
    if resp.status_code != 200:
        return {"name": ticker}
    return resp.json()


def display_holdings(ticker, company_name, data, top_n=10):
    """Display the top institutional holders in a table."""
    holdings = data.get("holdings", [])
    if not holdings:
        print("No institutional holdings found.")
        return

    # Sort by value descending
    holdings.sort(key=lambda h: h.get("value", 0), reverse=True)
    top = holdings[:min(top_n, len(holdings))]

    # Determine the reporting period
    period = "Unknown"
    if top and top[0].get("report_date"):
        rd = top[0]["report_date"]  # e.g. "2025-12-31"
        year = rd[:4]
        month = int(rd[5:7])
        quarter = (month - 1) // 3 + 1
        period = f"{year}-Q{quarter}"

    width = 63
    print(f"\nInstitutional Holdings: {company_name} ({ticker})")
    print(f"Period: {period}")
    print("=" * width)
    print(f"{'#':<4}{'Institution':<31}{'Shares':>14}{'Value':>14}")
    print("-" * width)

    total_shares = 0
    total_value = 0

    for i, h in enumerate(top, 1):
        name = h["institution_name"][:29]
        shares = h.get("shares", 0)
        value = h.get("value", 0)
        total_shares += shares
        total_value += value

        print(f"{i:<4}{name:<31}{format_number(shares):>14}{format_value(value):>14}")

    print("-" * width)
    print(f"{'Top ' + str(len(top)) + ' Total:':<35}{format_number(total_shares):>14}{format_value(total_value):>14}")


def compare_quarters(ticker, company_name, current, previous):
    """Compare holdings between two quarters and show changes."""
    curr_holdings = {
        h["institution_name"]: h for h in current.get("holdings", [])
    }
    prev_holdings = {
        h["institution_name"]: h for h in previous.get("holdings", [])
    }

    # Build list of changes for institutions in the current quarter
    changes = []
    for name, curr in curr_holdings.items():
        curr_shares = curr.get("shares", 0)
        prev = prev_holdings.get(name)
        prev_shares = prev.get("shares", 0) if prev else 0
        change = curr_shares - prev_shares
        pct = (change / prev_shares * 100) if prev_shares else 0
        is_new = prev is None
        changes.append((name, curr_shares, change, pct, is_new))

    # Sort by absolute change descending
    changes.sort(key=lambda x: abs(x[2]), reverse=True)

    width = 78
    print(f"\nQuarter-over-Quarter Changes: {company_name} ({ticker})")
    print("=" * width)
    print(f"{'Institution':<31}{'Current':>14}{'Change':>16}{'% Change':>12}")
    print("-" * width)

    for name, shares, change, pct, is_new in changes[:10]:
        short_name = name[:29]
        if is_new:
            tag = "NEW"
        elif change > 0:
            tag = f"+{pct:.1f}%"
        elif change < 0:
            tag = f"{pct:.1f}%"
        else:
            tag = "unchanged"

        sign = "+" if change >= 0 else ""
        print(f"{short_name:<31}{format_number(shares):>14}{sign + format_number(change):>16}{tag:>12}")

    print("-" * width)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Track institutional holders of a stock"
    )
    parser.add_argument("ticker", help="Stock ticker symbol")
    parser.add_argument(
        "--top", type=int, default=10,
        help="Number of top holders to show (default: 10)",
    )
    parser.add_argument(
        "--compare", action="store_true",
        help="Compare current quarter vs previous quarter",
    )
    args = parser.parse_args()
    ticker = args.ticker.upper()

    # Fetch company name
    company = fetch_company(ticker)
    company_name = company.get("name", ticker)

    # Fetch current holdings
    print(f"Fetching institutional holdings for {ticker}...")
    current = fetch_holdings(ticker)
    if current is None:
        sys.exit(1)

    # Display the top holders
    display_holdings(ticker, company_name, current, top_n=args.top)

    # Optionally compare with previous quarter
    if args.compare:
        print("\nFetching previous quarter for comparison...")
        previous = fetch_holdings(ticker, period="previous")
        if previous:
            compare_quarters(ticker, company_name, current, previous)

How It Works

What Are 13F Filings?

Every institutional investment manager with at least $100 million in qualifying assets must file a Form 13F with the SEC each quarter. These filings list every equity position the institution holds, including the number of shares and market value. The filings are due 45 days after the end of each quarter, so there is always a delay between the reporting period and when the data becomes available.

Because of the 45-day filing deadline, 13F data is always at least 6 weeks old by the time it is published. Institutions may have already changed their positions since the report date.

Institutional Holdings: GET /v1/institutions/{ticker}

This endpoint returns 13F holdings data for a given ticker. Each entry includes the institution name, number of shares held, the value in thousands of dollars (the standard 13F reporting unit), the reporting period end date, and the actual filing date.

HTTP
GET https://api.stockdata.dev/v1/institutions/AAPL
X-API-Key: your_api_key_here

The response looks like this:

JSON
{
  "ticker": "AAPL",
  "holdings": [
    {
      "institution_name": "Vanguard Group Inc",
      "shares": 1287654321,
      "value": 292500000,
      "report_date": "2025-12-31",
      "filing_date": "2026-02-14"
    }
  ]
}

The value field is reported in thousands of USD, which is the standard 13F convention. The script multiplies by 1,000 when formatting for display. So a value of 292500000 represents $292.5 billion.

The period Parameter

By default, the endpoint returns the most recent quarter's data. Pass period=previous to get the prior quarter's filings, which is what the --compare flag uses to calculate position changes.

Running It

Install the dependency and run the script:

Shell
pip install requests

View top holders

Shell
$ python holdings.py AAPL
Fetching institutional holdings for AAPL...

Institutional Holdings: Apple Inc. (AAPL)
Period: 2025-Q4
═══════════════════════════════════════════════════════════════
#   Institution                    Shares          Value
───────────────────────────────────────────────────────────────
1   Vanguard Group Inc            1,287,654,321   $292.5B
2   BlackRock Inc                 1,024,789,012   $232.8B
3   Berkshire Hathaway Inc          400,000,000    $90.9B
4   State Street Corp               623,456,789   $141.7B
5   FMR LLC                         350,123,456    $79.6B
6   Geode Capital Management        267,890,123    $60.9B
7   Morgan Stanley                  198,765,432    $45.2B
8   Northern Trust Corp             187,654,321    $42.6B
9   JP Morgan Chase & Co            176,543,210    $40.1B
10  Bank of America Corp            165,432,109    $37.6B
───────────────────────────────────────────────────────────────
Top 10 Total:                     4,682,308,773   $1,063.9B

Show only top 5

Shell
$ python holdings.py AAPL --top 5

Quarter-over-Quarter Changes

The --compare flag fetches both the current and previous quarter's data and displays who increased or decreased their positions.

Shell
$ python holdings.py AAPL --compare
Fetching institutional holdings for AAPL...

Institutional Holdings: Apple Inc. (AAPL)
Period: 2025-Q4
═══════════════════════════════════════════════════════════════
#   Institution                    Shares          Value
───────────────────────────────────────────────────────────────
1   Vanguard Group Inc            1,287,654,321   $292.5B
2   BlackRock Inc                 1,024,789,012   $232.8B
3   Berkshire Hathaway Inc          400,000,000    $90.9B
4   State Street Corp               623,456,789   $141.7B
5   FMR LLC                         350,123,456    $79.6B
6   Geode Capital Management        267,890,123    $60.9B
7   Morgan Stanley                  198,765,432    $45.2B
8   Northern Trust Corp             187,654,321    $42.6B
9   JP Morgan Chase & Co            176,543,210    $40.1B
10  Bank of America Corp            165,432,109    $37.6B
───────────────────────────────────────────────────────────────
Top 10 Total:                     4,682,308,773   $1,063.9B

Fetching previous quarter for comparison...

Quarter-over-Quarter Changes: Apple Inc. (AAPL)
══════════════════════════════════════════════════════════════════════════════
Institution                    Current           Change      % Change
──────────────────────────────────────────────────────────────────────────────
Vanguard Group Inc            1,287,654,321    +15,234,567       +1.2%
BlackRock Inc                 1,024,789,012     -8,901,234       -0.9%
State Street Corp               623,456,789    +12,345,678       +2.0%
Berkshire Hathaway Inc          400,000,000    -15,000,000       -3.6%
FMR LLC                         350,123,456     +5,678,901       +1.6%
Geode Capital Management        267,890,123     +3,456,789       +1.3%
Morgan Stanley                  198,765,432     -2,345,678       -1.2%
Northern Trust Corp             187,654,321     +1,234,567       +0.7%
JP Morgan Chase & Co            176,543,210       +987,654       +0.6%
Bank of America Corp            165,432,109       -876,543       -0.5%
──────────────────────────────────────────────────────────────────────────────

This view makes it easy to spot meaningful changes. Large percentage decreases from major holders like Berkshire Hathaway often make headlines, while steady increases from index funds like Vanguard and State Street are typical as the stock's market cap grows.

New positions show "NEW" in the % Change column. These are institutions that appear in the current quarter but were not present in the previous quarter's filing.

Enhancements

Here are several ways to extend the holdings tracker:

  • Track a specific institution — Add a --institution "Berkshire Hathaway" flag to filter results to a single institution's holdings across multiple tickers. This lets you see what a specific fund is buying and selling.
  • Alert on new positions — Compare consecutive quarters and send a notification (email, Slack, etc.) when a notable institution opens a new position in a stock on your watchlist.
  • CSV export — Add a --csv output.csv flag to write the holdings table to a CSV file for analysis in a spreadsheet or data tool.
  • Ownership percentage — If shares outstanding data is available from the company profile, calculate each institution's ownership as a percentage of total shares. This gives a clearer picture of concentration.
  • Historical tracking — Fetch multiple quarters of data and chart an institution's position size over time to identify accumulation or distribution patterns.
  • Multi-ticker scan — Accept multiple tickers and show a matrix of which institutions appear across multiple stocks in your watchlist, revealing shared holders and potential correlations.

Ready to build?

Get your free API key and start coding in minutes.

Get Free API Key