Institutional Holdings Tracker
Track which hedge funds and institutions hold a stock using 13F data from SEC filings.
In this tutorial
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
requestslibrary (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.
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.
GET https://api.stockdata.dev/v1/institutions/AAPL X-API-Key: your_api_key_here
The response looks like this:
{
"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:
pip install requests
View top holders
$ 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
$ 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.
$ 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.csvflag 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.