How do I calculate the holdings overlap between two ETFs?
Overlap is the sum over shared holdings of the smaller of the two weights. Compute it from data.topHoldings, or call GET /overlap?isins=A,B for a name-matched result. Formula, Python and the free tool.Published September 12, 2026 · Updated October 6, 2026 · by FundFacts APIShort answer
Overlap = Σ over holdings present in both funds of min(weight in A, weight in B), in percent of portfolio. GET https://fundfactsapi.com/api/v1/overlap?isins=A,B (Starter and above) does the name matching and returns overlap, sharedCount and the shared list per pair; on any plan you can compute the same from each fund's data.topHoldings, which holds its largest positions only (ten for most funds, up to 50 for some issuers), so the figure is a lower bound unless both lists cover most of each portfolio. The free ETF overlap tool on this site runs the endpoint in the browser.
See it on a real fund page: the IE00B4L5Y983 page shows the figures this answer describes, with their as-of date.
The formula
For two funds A and B with holdings weights wA[name] and wB[name] in percent:
textoverlap(A, B) = Σ_{name ∈ A ∩ B} min(wA[name], wB[name])
An overlap of 40 means 40% of each portfolio's weight sits in names the other fund also holds (at the smaller of the two weights). Two share classes of the same index fund overlap near 100; a world ETF and an emerging-markets ETF near 0.
From the payload
pythonimport os, requestsH = {"Authorization": f"Bearer {os.environ['FUNDFACTS_API_KEY']}"}def holdings(isin):d = requests.get(f"https://fundfactsapi.com/api/v1/funds/{isin}", headers=H, timeout=300).json()["data"]return {h["name"].upper(): h["weight"] for h in d["topHoldings"]}a, b = holdings("IE00B4L5Y983"), holdings("IE00B3RBWM25")shared = {n: min(a[n], b[n]) for n in a.keys() & b.keys()}print(round(sum(shared.values()), 2), "% overlap over published holdings;", len(shared), "shared names")
This is exact over the positions stored in topHoldings (the ten largest for most funds, up to 50 for some issuers, even where the issuer publishes a full holdings file) and is what the endpoint does, with better name matching. Over top lists it is a lower bound on the true overlap.
The endpoint
bashcurl -s "https://fundfactsapi.com/api/v1/overlap?isins=IE00B4L5Y983,IE00B3RBWM25" -H "Authorization: Bearer $FUNDFACTS_API_KEY"
json{ "pairs": [ { "a": "IE00B4L5Y983", "b": "IE00B3RBWM25", "overlap": 18.4, "sharedCount": 9, "disclosed": { "a": 24.9, "b": 21.7 }, "shared": [ { "label": "NVIDIA", "a": 5.48, "b": 4.61 } ] } ] }
disclosed states what share of each portfolio the stored holdings cover, so you can tell a near-complete comparison (both close to 100) from a top-ten lower bound like this one. Several ISINs return every pair. One request per ISIN; Starter and above. The MCP server's fund_overlap tool wraps it for assistants.
Reading the number
Overlap is about names, not about risk: two funds with 30% overlap can still be 95% correlated because they hold the same sectors and countries. Pair it with the sector and geography tables, or with POST /portfolio for the aggregate view. How to calculate ETF holdings overlap goes deeper into matching and edge cases.
Verify it yourself
The demo endpoint returns the live payload for a fund that is already in the store, without a key. Everything on this page can be checked against it.bashcurl -s https://fundfactsapi.com/api/v1/demo/funds/IE00B4L5Y983 | jq '{name, asOf: .data.dataAsOf, ter: .data.headlineMetrics.ter, risk: .data.riskRating, top: .data.topHoldings[:3]}'