DEV Community

DAPDEV
DAPDEV

Posted on

Python Script to Monitor When Billionaires Change Their Stock Portfolios

Every quarter, hedge funds and institutional investors managing over $100M are required to disclose their stock holdings to the SEC via 13F filings. This is public data — and it reveals exactly what Warren Buffett, Ray Dalio, Michael Burry, and thousands of other big players are buying and selling.

In this tutorial, we'll build a Python script that monitors these filings and detects portfolio changes — new positions, exits, and significant increases or decreases.

How 13F Filings Work

The SEC requires institutional investment managers to file Form 13F quarterly. Each filing lists every equity position the fund holds, including the number of shares and market value. By comparing consecutive filings, you can see exactly what changed.

The catch? The SEC's EDGAR system is notoriously difficult to work with — inconsistent formats, XML parsing, rate limiting, and CIK number lookups. We'll skip all that pain by using an API that handles it for us.

Setup

We'll use the SEC EDGAR Financial Data API on RapidAPI, which provides clean, structured access to 13F holdings for 10,000+ companies.

Subscribe to the API to get your key, then:

pip install requests
Enter fullscreen mode Exit fullscreen mode

Step 1: Find an Investor's CIK Number

Every SEC filer has a CIK (Central Index Key). Let's search for one:

import requests

RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
BASE_URL = "https://sec-edgar-financial-data-api.p.rapidapi.com"
HEADERS = {
    "x-rapidapi-host": "sec-edgar-financial-data-api.p.rapidapi.com",
    "x-rapidapi-key": RAPIDAPI_KEY,
}

def search_company(query):
    resp = requests.get(
        f"{BASE_URL}/companies/search",
        params={"query": query},
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

results = search_company("Berkshire Hathaway")
for company in results.get("companies", [])[:5]:
    print(f"{company['name']} — CIK: {company['cik']}")
Enter fullscreen mode Exit fullscreen mode

This gives you the CIK for any filer. Berkshire Hathaway's CIK is 0001067983.

Step 2: Fetch 13F Holdings

Now pull the actual portfolio holdings:

def get_holdings(cik):
    resp = requests.get(
        f"{BASE_URL}/holdings/13f/{cik}",
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()

holdings = get_holdings("0001067983")  # Berkshire Hathaway

# Show top 10 positions by value
positions = holdings.get("holdings", [])
positions_sorted = sorted(positions, key=lambda x: x.get("value", 0), reverse=True)

print(f"Total positions: {len(positions)}")
print(f"
Top 10 Holdings:")
for pos in positions_sorted[:10]:
    name = pos.get("name", "Unknown")
    value = pos.get("value", 0)
    shares = pos.get("shares", 0)
    print(f"  {name}: ${value:,.0f} ({shares:,.0f} shares)")
Enter fullscreen mode Exit fullscreen mode

Step 3: Detect Portfolio Changes

The real value comes from comparing two quarters. Here's a function that diffs two sets of holdings and highlights what changed:

def detect_changes(current_holdings, previous_holdings):
    current = {h["cusip"]: h for h in current_holdings}
    previous = {h["cusip"]: h for h in previous_holdings}

    changes = {
        "new_positions": [],
        "exited_positions": [],
        "increased": [],
        "decreased": [],
    }

    # New positions (in current but not previous)
    for cusip in current:
        if cusip not in previous:
            changes["new_positions"].append(current[cusip])

    # Exited positions (in previous but not current)
    for cusip in previous:
        if cusip not in current:
            changes["exited_positions"].append(previous[cusip])

    # Changed positions
    for cusip in current:
        if cusip in previous:
            curr_shares = current[cusip].get("shares", 0)
            prev_shares = previous[cusip].get("shares", 0)
            if prev_shares == 0:
                continue
            pct_change = ((curr_shares - prev_shares) / prev_shares) * 100

            if pct_change > 5:  # increased by more than 5%
                changes["increased"].append({
                    **current[cusip],
                    "prev_shares": prev_shares,
                    "pct_change": pct_change,
                })
            elif pct_change < -5:  # decreased by more than 5%
                changes["decreased"].append({
                    **current[cusip],
                    "prev_shares": prev_shares,
                    "pct_change": pct_change,
                })

    return changes
Enter fullscreen mode Exit fullscreen mode

Step 4: Generate a Report

Let's put it all together with a clean report:

def print_report(investor_name, changes):
    print(f"
{'='*60}")
    print(f"  PORTFOLIO CHANGES: {investor_name}")
    print(f"{'='*60}")

    if changes["new_positions"]:
        print(f"
  NEW POSITIONS ({len(changes['new_positions'])})")
        for pos in changes["new_positions"]:
            print(f"    + {pos['name']}: {pos.get('shares', 0):,.0f} shares "
                  f"(${pos.get('value', 0):,.0f})")

    if changes["exited_positions"]:
        print(f"
  EXITED POSITIONS ({len(changes['exited_positions'])})")
        for pos in changes["exited_positions"]:
            print(f"    - {pos['name']}: sold all "
                  f"{pos.get('shares', 0):,.0f} shares")

    if changes["increased"]:
        print(f"
  INCREASED ({len(changes['increased'])})")
        for pos in sorted(changes["increased"],
                         key=lambda x: x["pct_change"], reverse=True)[:10]:
            print(f"    ^ {pos['name']}: +{pos['pct_change']:.1f}% "
                  f"({pos['prev_shares']:,.0f} -> {pos.get('shares', 0):,.0f})")

    if changes["decreased"]:
        print(f"
  DECREASED ({len(changes['decreased'])})")
        for pos in sorted(changes["decreased"],
                         key=lambda x: x["pct_change"])[:10]:
            print(f"    v {pos['name']}: {pos['pct_change']:.1f}% "
                  f"({pos['prev_shares']:,.0f} -> {pos.get('shares', 0):,.0f})")
Enter fullscreen mode Exit fullscreen mode

Step 5: Monitor Multiple Funds

Track a watchlist of investors and check them all at once:

watchlist = {
    "Berkshire Hathaway": "0001067983",
    "Bridgewater Associates": "0001350694",
    "Citadel Advisors": "0001423053",
    "Renaissance Technologies": "0001037389",
}

def monitor_watchlist(watchlist):
    for name, cik in watchlist.items():
        print(f"
Fetching data for {name}...")
        try:
            data = get_holdings(cik)
            holdings = data.get("holdings", [])
            total_value = sum(h.get("value", 0) for h in holdings)
            print(f"  Positions: {len(holdings)}")
            print(f"  Total reported value: ${total_value:,.0f}")
        except Exception as e:
            print(f"  Error: {e}")

monitor_watchlist(watchlist)
Enter fullscreen mode Exit fullscreen mode

Ideas to Extend This

  • Scheduled monitoring — run it as a cron job and send email or Slack alerts when new filings appear
  • Consensus picks — find stocks that multiple top funds are all buying simultaneously
  • Historical tracking — store quarterly snapshots in a database and chart position sizes over time
  • Sector analysis — categorize holdings by sector to see if big money is rotating into tech, energy, healthcare, etc.

Important Note

13F filings are reported with a ~45-day delay after each quarter ends. This isn't real-time data — it's a quarterly snapshot. But it's still incredibly valuable for understanding what the smartest money in the world is doing with their portfolios.

Wrapping Up

With the SEC EDGAR Financial Data API and a bit of Python, you can programmatically track portfolio changes for any institutional investor filing 13F reports. No more manually navigating the SEC website or parsing raw XML.

Subscribe on RapidAPI and start tracking the funds you care about.

Which investor would you track first? Let me know in the comments.

Top comments (0)