Do your 13F Workflow in Claude
The Q2 deadline passed August 14. Next morning, my quarterly 13F diff script said Bill Ackman had put on thirteen new positions. He hadn't. Fixing the script — and asking Claude instead.
The quarterly diff script every edgartools user has written produced a spectacularly wrong answer this week. Here's what happened — and the two ways to get the right one.
Friday, August 14 was the Q2 2026 deadline: forty-five days after quarter end, every institutional manager over $100M had to have filed their 13F. Saturday morning I did what I suspect a lot of you did — ran the quarterly ritual, the script that compares each manager's fresh 13F filing against last quarter's. For Pershing Square, my script reported that Bill Ackman had put on thirteen new positions and grown the book from $569 million to $19.5 billion in a single quarter.
But he hadn't. The script was doing exactly what I wrote it to do, and its answer was still wrong. That silent failure, plausible-looking, caused by paperwork rather than portfolio moves is worth walking through, because it says something about what "compare two 13F filings" actually requires. It also makes a good first entry in a series I've been meaning to start: taking a workflow edgartools users run in Python and showing what it looks like as a conversation in Claude.
The ritual
The quarterly diff is maybe the most-written edgartools script that nobody publishes. Pull a manager's latest two 13F-HR filings, join on CUSIP, and report what's new, what's gone, and what changed:
import pandas as pd
from edgar import Company, set_identity
set_identity("you@example.com")
company = Company(1336528) # Pershing Square Capital Management, L.P.
current_filing, previous_filing = company.get_filings(form="13F-HR").latest(2)
current, previous = current_filing.obj(), previous_filing.obj()
cur = current.infotable.groupby("Cusip").agg(
Shares=("SharesPrnAmount", "sum"), Issuer=("Issuer", "first"))
prev = previous.infotable.groupby("Cusip").agg(
Shares=("SharesPrnAmount", "sum"), Issuer=("Issuer", "first"))
merged = cur.join(prev, lsuffix="_cur", rsuffix="_prev", how="outer")
for cusip, row in merged.iterrows():
if pd.isna(row.Shares_prev):
print(f"NEW {row.Issuer_cur}: {row.Shares_cur:,.0f} shares")
elif pd.isna(row.Shares_cur):
print(f"EXITED {row.Issuer_prev}")
elif row.Shares_cur != row.Shares_prev:
pct = (row.Shares_cur - row.Shares_prev) / row.Shares_prev * 100
print(f"CHANGED {row.Issuer_cur}: {pct:+.1f}% shares")
Through Q1 this worked beautifully. Run in May, it correctly reported the quarter's story: a new 5.65M-share Microsoft position, Hilton exited entirely, Amazon up 19.2%, the Alphabet stake cut by ninety-five percent — all from the Q1 filing 0001172661-26-002336. Twenty lines of edgartools and pandas, and the 13F parsing underneath got 8x faster last year, so the whole thing runs in seconds. The library sees just under a million PyPI downloads a month now, and I'd bet a decent fraction of you have some version of this exact loop sitting in a repo.
The quarter it lied
Run the same script on Saturday and the first surprise is that Pershing Square's newest 13F isn't a 13F-HR at all. What the L.P. filed on Friday is a 13F-NT — 0001172661-26-003777 — a notice with no holdings in it. Its cover page says, in full:
Holdings of this reporting manager are now included in the report of its public parent company.
That "public parent company" is Pershing Square Inc., CIK 2026053, and it filed the actual Q2 book the same day: 0001172661-26-003790, fourteen positions, $19.5 billion.
So you point the script at the new CIK and get the answer I opened with: thirteen NEW positions, because Pershing Square Inc.'s own Q1 filing held exactly one name — Howard Hughes, $569 million — while the main book sat under the L.P. The join is correct. The CUSIPs are correct. The baseline is wrong, because "Pershing Square" stopped being one filer and your script doesn't know that. It reports Uber, Brookfield, Microsoft and Amazon as new buys when they simply changed mailing address, and it reports Howard Hughes up 209% when the position didn't move.
This is the worst kind of wrong answer — the kind that parses cleanly, sums plausibly, and would sail straight into a Monday-morning note. If your ritual script tracks any manager that restructured, merged, or moved its book to a parent entity, it has this failure mode waiting.
Fixing it in code
The fix isn't a better join — it's an entity map. The 13F-NT tells you, in machine-readable XML, exactly which filer absorbed the book, so the correct Q2 baseline is the union of both entities' Q1 filings: build each entity's book with the same groupby("Cusip"), pd.concat the two Q1 books, then diff as before. Against the right baseline, the quarter finally tells the truth — this is the corrected script's complete output:
Current: 2026-06-30 (0001172661-26-003790), 14 positions, $19.47B
Baseline: 2026-03-31 L.P. + Inc. combined, 11 positions, $14.28B
ADDED UBER TECHNOLOGIES INC: +14.6% shares, now $2,477M
TRIM BROOKFIELD CORP: -3.7% shares, now $2,448M
ADDED MICROSOFT CORP: +9.8% shares, now $2,315M
TRIM AMAZON COM INC: -25.2% shares, now $2,041M
ADDED RESTAURANT BRANDS INTL INC: +14.0% shares, now $1,872M
ADDED META PLATFORMS INC: +20.1% shares, now $1,800M
NEW VISA INC: 3,270,470 shares, $1,122M
NEW MASTERCARD INCORPORATED: 2,124,646 shares, $1,091M
NEW S&P GLOBAL INC: 2,593,155 shares, $1,056M
NEW NETFLIX INC.: 13,081,465 shares, $934M
NEW PERSHING SQUARE USA LTD: 4,000,000 shares, $150M
TRIM HERTZ GLOBAL HLDGS INC: -1.6% shares, now $34M
EXITED ALPHABET INC (was $89M)
EXITED ALPHABET INC (was $9M)
Thirteen fake new positions collapse into four new market bets — about $4.2 billion into Visa, Mastercard, S&P Global and Netflix — plus a $150M stake in the firm's own listed vehicle, Pershing Square USA. Around them: a 25% Amazon trim and the exit of Alphabet's two residual lots. Howard Hughes and Seaport don't appear at all, because against the combined baseline their share counts are identical — the 209% Howard Hughes jump was pure transfer. That's the actual Q2 story, and it's a much better one than the fake version.
The corrected script, in full
import pandas as pd
from edgar import Company, set_identity
set_identity("you@example.com")
def book(cik: int, index: int = 0):
"""One entity's 13F book: (positions by CUSIP, period, accession)."""
filing = Company(cik).get_filings(form="13F-HR").latest(2)[index]
positions = filing.obj().infotable.groupby("Cusip").agg(
Value=("Value", "sum"), Shares=("SharesPrnAmount", "sum"),
Issuer=("Issuer", "first"))
return positions, str(filing.header.period_of_report), filing.accession_no
# Q2 2026: the whole book is under Pershing Square Inc.
current, cur_period, cur_accession = book(2026053)
# Q1 2026 baseline: the L.P.'s main book plus Inc.'s own (Howard Hughes only)
lp_q1, lp_period, _ = book(1336528) # latest L.P. 13F-HR is still Q1
inc_q1, _, _ = book(2026053, index=1)
previous = pd.concat([lp_q1, inc_q1]).groupby(level=0).agg(
Value=("Value", "sum"), Shares=("Shares", "sum"), Issuer=("Issuer", "first"))
merged = current.join(previous, lsuffix="_cur", rsuffix="_prev", how="outer")
print(f"Current: {cur_period} ({cur_accession}), {len(current)} positions, "
f"${current.Value.sum() / 1e9:,.2f}B")
print(f"Baseline: {lp_period} L.P. + Inc. combined, {len(previous)} positions, "
f"${previous.Value.sum() / 1e9:,.2f}B\n")
for cusip, row in merged.sort_values("Value_cur", ascending=False).iterrows():
name = row.Issuer_cur if isinstance(row.Issuer_cur, str) else row.Issuer_prev
cur_shares, prev_shares = row.Shares_cur, row.Shares_prev
if pd.isna(cur_shares):
print(f" EXITED {name} (was ${row.Value_prev / 1e6:,.0f}M)")
elif pd.isna(prev_shares):
print(f" NEW {name}: {cur_shares:,.0f} shares, "
f"${row.Value_cur / 1e6:,.0f}M")
elif cur_shares != prev_shares:
change_pct = (cur_shares - prev_shares) / prev_shares * 100
label = "ADDED " if cur_shares > prev_shares else "TRIM "
print(f" {label} {name}: {change_pct:+.1f}% shares, "
f"now ${row.Value_cur / 1e6:,.0f}M")
The reader's version of this fix is the point, though: to get here you had to notice a 13F-NT, read its cover page, find a second CIK, and merge two filings. edgartools gives you every piece — but the knowing which pieces is the workflow.
The same question, as a sentence
Here's the other way I asked it this week. My Claude connects to the hosted MCP server at edgar.tools, so the entire workflow above compresses to typing:
What changed in Pershing Square's portfolio last quarter?
Claude calls one tool — manager_holdings — and gets back the manager resolved from a plain-English name (no CIK lookup), every position with its portfolio weight, the quarter-over-quarter status of each, a turnover summary, and an eight-quarter trend of the whole book. This is the actual response from Saturday, trimmed to the interesting parts:
{
"manager": {"cik": 1336528, "name": "Pershing Square Capital Management, L.P.",
"headline": "Bill Ackman"},
"summary": {"quarter": "2026-03-31", "holdings_count": 11,
"total_value_thousands": 13714296},
"holdings": [
{"issuer_name": "BROOKFIELD CORP", "pct_portfolio": 17.62,
"change": {"status": "decreased", "share_change_pct": -2.78}},
{"issuer_name": "AMAZON COM INC", "pct_portfolio": 17.39,
"change": {"status": "increased", "share_change_pct": 19.19}},
{"issuer_name": "UBER TECHNOLOGIES INC", "pct_portfolio": 15.71,
"change": {"status": "decreased", "share_change_pct": -0.82}}
],
"change_summary": {"new": 1, "closed": 1, "increased": 1, "decreased": 6,
"turnover_pct": 18.18},
"data_quality": {
"as_of": "2026-03-31", "freshness_lag_days": 137,
"caveats": [
"Positions are as-of quarter-end; managers file up to 45 days later, and the consolidated dataset can trail a manager's newest 13F by a quarter.",
"The source series contains non-quarter-end snapshots (amendment or merge artifacts); they are excluded from the trend and change baselines."
]
}
}

Two things about this response earn their space. First, the change_summary — one new position, one closed, six trimmed, 18% turnover — matches my Q1 script run exactly, which it should, because underneath it's the same parsing discipline this library established. Second, and I'd argue more valuable: the answer states its own freshness. As I write, the consolidated dataset serves the Q1 book and says so — as_of March 31, 137 days behind, with a caveat telling you the newest filing may not be in yet. Those caveats aren't boilerplate; excluding amendment artifacts from trend baselines, reconciling filers, deciding what "previous quarter" even means when a book changes CIKs — that reconciliation is the product. The Pershing entity move went onto our own dataset's issue tracker within the hour, because absorbing exactly this kind of mess is the job the consolidated view signed up for.
And because it's a conversation, the follow-ups cost one sentence each: "just the Amazon position" filters the book to the single matching row; "how has the total book trended?" is already in the response.
manager_holdings tool — plus company financials, filing search, and insider activity — is part of the hosted MCP at edgar.tools. The free tier is enough to run everything in this section.Which lane is yours
I built both of these, so I'll give you the split honestly rather than pitch one.
Stay in Python when you're working the wire. The library read Friday's filing on Friday — fresh off EDGAR, no consolidation delay — and when the data does something weird, you're holding the steering wheel: you can read the 13F-NT yourself, follow it to the sibling CIK, and build whatever baseline you decide is true. Bulk pipelines, custom analytics, anything that feeds a model — that's library territory, same as it's always been.
Ask Claude when you want the reconciled view and the conversation. Portfolio weights, QoQ changes, turnover and trend arrive pre-computed with their freshness stated, the manager resolves from a plain-English name, and the follow-up questions are free. It's also the version of this workflow that exists when you're nowhere near an editor. On the free tier the response shows the top three positions along with the full change summary and trend; the complete book, past quarters, and closed positions come with the paid tiers.
The part I care most about: it's the same lineage under both. The AI ecosystem that grew up around edgartools exists because parsing SEC filings is the hard part, and the hosted MCP is my own continuation of that work — the library's parsing discipline, packaged as a data layer your assistant can query. Your ritual script doesn't retire; it gets a second front end.
This is the first post in an occasional series — your edgartools workflow, now in Claude — that takes one real Python workflow per post and runs it both ways, wrong turns included. If there's a workflow you'd want treated this way, open an issue on the repo and tell me about it.