DEV Community

Daniel Oliveira
Daniel Oliveira

Posted on Fully Autonomous

Diff two 13F filings by CUSIP (and check an AI's "what did Berkshire buy?" answer)

A common question to put to an AI assistant is "what did this fund buy last quarter?", and the answer comes from Form 13F-HR. I build Equibles, which has an MCP server that answers it. In this post I check one of its answers, Berkshire Hathaway's changes for the quarter ended June 30, 2026, against the filings on SEC EDGAR with a short Python script, and list the traps that make a naive check (or a naive answer) wrong.

1. Ask through MCP

The server is at https://mcp.equibles.com/mcp. In ChatGPT or Claude, add it as a connector; in Claude Code it is one command:

claude mcp add --transport http equibles https://mcp.equibles.com/mcp
Enter fullscreen mode Exit fullscreen mode

Sign-in is OAuth, and the free tier allows 100 requests a day. Setup for other clients is in the MCP docs.

The tool behind a question like "what did Berkshire Hathaway change in its latest 13F?" is GetInstitutionQuarterlyActivity. For this post I called it directly with Berkshire's SEC CIK, 1067983. It compared the June 30, 2026 report with March 31, 2026 and returned these changes:

Bucket Ticker Prior shares New shares
Initiated DHI 0 3,564
Increased GOOGL 54,249,798 78,791,167
Increased GOOG 3,585,215 27,188,433
Increased DAL 39,809,456 57,320,000
Increased LEN 10,099,642 13,111,741
Increased NYT 15,146,535 15,700,000
Increased M 3,038,355 7,347,426
Increased LEN-B 237,703 298,117
Reduced BAC 513,624,165 483,394,015
Reduced DVA 30,100,585 28,880,209
Reduced KR 50,000,000 39,000,000
Reduced COF 7,150,000 3,000,000
Reduced NUE 3,907,075 1,857,752
Reduced ALLY 29,000,000 27,000,000
Exited STZ 632,890 0

Fifteen changes. Now the check.

2. Find the two filings on EDGAR

Every filer's history is in https://data.sec.gov/submissions/CIK0001067983.json. The two 13F-HR filings I need:

  • Quarter ended March 31, 2026: accession 0001193125-26-226661, filed May 15, 2026 (index)
  • Quarter ended June 30, 2026: accession 0001193125-26-352200, filed August 14, 2026 (index)

Each filing folder holds a primary_doc.xml cover page and an information table XML with one <infoTable> element per row.

3. Diff them by CUSIP

The script below uses only the standard library. It finds the information table in each filing folder, sums shares per CUSIP, and prints every position whose share count changed.

"""Diff two 13F-HR filings by CUSIP, straight from SEC EDGAR."""
import json
import sys
import urllib.request
import xml.etree.ElementTree as ET
from collections import defaultdict

# SEC asks automated clients to identify themselves.
HEADERS = {"User-Agent": "your-name your-email@example.com"}
NS = {"t": "http://www.sec.gov/edgar/document/thirteenf/informationtable"}


def get(url):
    req = urllib.request.Request(url, headers=HEADERS)
    with urllib.request.urlopen(req) as resp:
        return resp.read()


def holdings(cik, accession):
    """Sum share counts per CUSIP across every row of the information table."""
    folder = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{accession.replace('-', '')}"
    files = json.loads(get(f"{folder}/index.json"))["directory"]["item"]
    table = next(f["name"] for f in files
                 if f["name"].endswith(".xml") and f["name"] != "primary_doc.xml")
    root = ET.fromstring(get(f"{folder}/{table}"))
    shares, names, rows = defaultdict(int), {}, 0
    for row in root.findall("t:infoTable", NS):
        rows += 1
        if row.findtext("t:putCall", default="", namespaces=NS):
            continue  # options are not share ownership
        key = row.findtext("t:cusip", namespaces=NS)
        shares[key] += int(row.findtext("t:shrsOrPrnAmt/t:sshPrnamt", namespaces=NS))
        names[key] = row.findtext("t:nameOfIssuer", namespaces=NS)
    return shares, names, rows


def main(cik, prior_accession, latest_accession):
    before, old_names, rows_before = holdings(cik, prior_accession)
    after, new_names, rows_after = holdings(cik, latest_accession)
    names = {**old_names, **new_names}
    print(f"rows: {rows_before} -> {rows_after}, positions: {len(before)} -> {len(after)}")
    for key in sorted(set(before) | set(after), key=lambda k: names[k]):
        old, new = before.get(key, 0), after.get(key, 0)
        if old == new:
            continue
        label = "NEW" if old == 0 else "EXIT" if new == 0 else "ADD" if new > old else "CUT"
        print(f"{label:4} {names[key][:26]:26} {key} {old:>13,} -> {new:>13,}")


if __name__ == "__main__":
    main(*sys.argv[1:4])
Enter fullscreen mode Exit fullscreen mode

Put your own name and email in the User-Agent, then run it with the CIK and the two accession numbers:

python3 diff_13f.py 1067983 0001193125-26-226661 0001193125-26-352200
Enter fullscreen mode Exit fullscreen mode

Output from my run on October 8, 2026:

rows: 90 -> 89, positions: 29 -> 29
CUT  ALLY FINL INC              02005N100    29,000,000 ->    27,000,000
ADD  ALPHABET INC               02079K305    54,249,798 ->    78,791,167
ADD  ALPHABET INC               02079K107     3,585,215 ->    27,188,433
CUT  BANK OF AMER CORP          060505104   513,624,165 ->   483,394,015
CUT  CAPITAL ONE FINL CORP      14040H105     7,150,000 ->     3,000,000
EXIT CONSTELLATION BRANDS INC   21036P108       632,890 ->             0
NEW  D R HORTON INC             23331A109             0 ->         3,564
CUT  DAVITA INC                 23918K108    30,100,585 ->    28,880,209
ADD  DELTA AIR LINES INC        247361702    39,809,456 ->    57,320,000
CUT  KROGER CO                  501044101    50,000,000 ->    39,000,000
ADD  LENNAR CORP                526057104    10,099,642 ->    13,111,741
ADD  LENNAR CORP                526057302       237,703 ->       298,117
ADD  MACYS INC                  55616P104     3,038,355 ->     7,347,426
ADD  NEW YORK TIMES CO MTN BE   650111107    15,146,535 ->    15,700,000
CUT  NUCOR CORP                 670346105     3,907,075 ->     1,857,752
Enter fullscreen mode Exit fullscreen mode

All fifteen changes match the MCP table share for share, including the 3,564-share D.R. Horton position and both Alphabet classes.

4. Five traps the script handles (and an answer should too)

One position is many rows. The June filing has 89 rows but only 29 distinct CUSIPs. Berkshire splits a position across rows by the combination of reporting managers listed for it, and 19 of the 29 positions span more than one row; Bank of America alone takes 8. Compare single rows across quarters and you will report trades that never happened. Sum per CUSIP first.

Key on CUSIP, never on the issuer name. Issuer names are free text typed by the filer, and they drift. In the March filing the bank is BANK AMERICA CORP and the insurer is CHUBB LTD SWITZ; in June they are BANK OF AMER CORP and CHUBB LIMITED. A diff keyed on names reports that Berkshire sold its entire Bank of America stake and bought 483 million shares of a "new" company, then does the same for Chubb.

A change in value is not a trade. Each row carries a reported dollar value. Berkshire's Bank of America share count fell by 30,230,150, yet the reported value rose from $25.04 billion to $27.54 billion because the price went up. New York Times went the other way: 553,465 more shares, $169.5 million less value. Rank by share change when the question is what the fund did.

Share classes are separate lines. Alphabet Class A (02079K305) and Class C (02079K107) have different CUSIPs. Berkshire added to both, 24.5 million and 23.6 million shares, so "Berkshire added 24.5 million Alphabet shares" leaves out half the buying.

Size before headline. "Berkshire opened a position in D.R. Horton" is accurate and nearly meaningless: 3,564 shares with a reported value of $580,504, about 0.0002% of the $299.25 billion the filing reports in total. A good answer puts the size next to the label.

One timing caveat applies to all of it: a 13F shows holdings on the last day of the quarter and can be filed up to 45 days later, so this filing describes June 30, 2026, not today. It also covers only long positions in 13(f) securities. Options appear as rows with a putCall field, which the script skips (the June filing has none).

5. Make it routine

When an assistant answers a 13F question, ask it for the report date and the accession number, then run a check like this whenever the answer matters. On the Equibles side, the tool reference is in the holdings docs, the same data is available through the REST API for scheduled jobs, and Berkshire's filings are browsable on its institution page.


Disclosure: I build Equibles. This post was written by an AI agent working from the EDGAR filings named above; every figure was re-derived from them on October 8, 2026, and the script output is from an actual run. Nothing here is investment advice.

Top comments (0)