- Morningstar retired its free Instant X-Ray tool in April 2025. European passive investors lost the only tool that worked with ISINs and showed multi-fund look-through.
- I built a free replacement: https://nwc-advisory.com/xray
- Enter fund ISINs + amounts, get six analysis views: allocation before/after look-through, geography, sectors, stock overlap, fee analysis, and top holdings.
- No login for on-screen results. The whole pipeline runs on public data sources - 16 of them.
- The interesting engineering: a fetcher chain that goes from iShares CSV downloads to PDF parsing, bond classification via free regulatory APIs (GLEIF + ESMA FIRDS), and a proxy-routing system for synthetic ETFs whose holdings are legally unavailable.
The gap Morningstar left
In April 2025, Morningstar retired Instant X-Ray - the free tool that let you paste in any combination of fund ISINs and see the actual stocks underneath. For European passive investors, there was nothing comparable: every alternative either required US tickers (irrelevant for IE/LU/CH-domiciled funds), stopped at five holdings before hitting a paywall, or broke down at the portfolio level and only analyzed individual funds one at a time.
The problem X-Ray solved is real. A typical European ETF portfolio has visible diversification but hidden concentration. An iShares Core MSCI World (IE00B4L5Y983) and an iShares Core S&P 500 (IE00B5BMR087) are "two different funds from two different indices." They are also about 78% identical at the individual stock level, because MSCI World is already 70% US. Without a look-through tool, you cannot see that Apple alone appears in both, with a combined weight of ~7%, before you add any other holding. Multiply that across a four-fund portfolio and the overlap story compounds.
The subscription alternatives charge $249-299/year to see this. The data they use is public.
What the tool does
https://nwc-advisory.com/xray takes ISIN + amount pairs, fetches the holdings of each fund from public sources, aggregates everything, and returns six views:
View | What it shows |
| Allocation| Asset class breakdown before and after look-through

| Geography| Economic exposure by country (not fund domicile) |
| Sectors| GICS sectors aggregated across your portfolio |
| Overlap| Stocks that appear in two or more funds, with fund attribution |
| Fees| Weighted portfolio TER vs a passive benchmark, annual savings estimate |
| Holdings | Top 30 underlying securities by weight |
No login required for any of this. Email is only asked for the PDF download (a 9-page branded report). The email gate was originally the first analysis; it is now the third, so you see two full results before anything is asked of you.
The data pipeline
The hardest constraint was "no paid APIs." Every data point has to come from a source that is either openly published or freely accessible.
Layer 1: The fetcher chain
For ETFs and mutual funds, factsheet data is the source. The chain runs in priority order and stops at the first fetcher that returns holdings with >3 results:
iShares CSV-BlackRock publishes complete daily holdings for all iShares ETFs as downloadable CSVs. The URL pattern is https://www.ishares.com/us/products/{slug}/1467271812596.ajax?fileType=csv&dataType=fund. With the fund slug, you get thousands of holdings with ISIN, weight, sector, and country. The slug is in the fund's page URL and derivable from the product name. ~3,000 iShares ETFs covered, 100% holdings.
SPDR Excel - State Street publishes daily holdings as Excel workbooks for 124 EMEA ETFs and 18 US ETFs. Direct download, no scraping required. The URL pattern is https://www.ssga.com/library-content/products/fund-data/etfs/{region}/holdings-daily-{region}-en-{slug}.xlsx. One openpyxl call returns 500-3,500 rows.
JustETF- HTML scraping. Returns top 10-250 holdings with GICS sectors and TER. The real value here is the sector data, which iShares CSV omits.
Gerifonds PDF - Swiss Investment Funds Administrator. Their factsheets are published as PDFs. pdfplumber extracts the holdings table.
FT.com- Fallback for funds none of the above catch. Returns 10 holdings. Low coverage but broad reach - Comgest, Vontobel, DWS, JPMorgan active funds all appear here.
8 legacy fetchers- extraETF (German ETF site), MarketScreener, SwissFundData, Fundinfo, Brandes, FUW.ch, FinanzFluss, BCGE direct. Each covers a niche; none is reliable enough to be in the primary chain.
Results are cached to disk for 24 hours. A cron job warms the top 35 most-analyzed ETFs daily at 3am. Popular funds return in under a second. Unknown funds take 15-30 seconds on first request.
Layer 2: Bond identification
Fund look-through is straightforward when the underlying securities are equities -- they have names, countries, and sectors from the factsheet. Bonds are harder. An ISIN like US46647PDM59 tells you nothing at a glance. The tool needs to identify the issuer (JPMorgan Chase), classify it (corporate, not sovereign), and place it on a maturity ladder.
Three years ago I would have paid for Bloomberg or Refinitiv to do this. It turns out you do not need to.
Layer 3: Synthetic ETF proxy routing
Some funds are swap-based -- they hold derivatives that track an index, not the actual stocks. Amundi CW8 (LU1681043599) is the canonical example: a widely-held European MSCI World ETF that is synthetically replicated. There are no public holdings because the legal holdings are swap contracts, not equities.
The solution is a curated proxy map of 14 known-problem ETFs:
FUND_PROXY_MAP = {
"LU1681043599": ("IE00B4L5Y983", "swap", "Tracks MSCI World via swap; iShares physical equivalent used for look-through"),
# 13 more entries...
}
When a fund ISIN appears in this map, the engine fetches holdings for the proxy target instead (a physical ETF tracking the same index with full holdings published). The result is tagged with proxy_of, proxy_target, proxy_type, and proxy_reason so the caller can display a disclosure. Seven previously-broken ETFs (Xtrackers MSCI World, Amundi S&P 500, BNP Easy S&P 500, Invesco S&P 500) went from 0-0.3% coverage to 78-100% coverage via this path.
Two bugs that came from mixed-portfolio testing
When you test funds in isolation, some bugs hide. The test portfolio that exposed them had US equities, Swiss equities, US corporate bonds, sovereign bonds, Reg-S eurobonds, an S&P 500 ETF, a gold ETC, and a REIT ETF -- all at once.
What it cannot do (honest)
- Morningstar's style box - the 3x3 value/growth/blend grid is proprietary. No free tool can replicate it.
- Structured products - no public factsheets. Coverage is zero.
- Real-time yields and durations - these require live bond prices. The tool uses a par-assumption for YTM and modified duration and labels every page that shows them.
- FINRA TRACE full coverage - OpenFIGI's free tier has spotty corporate bond coverage outside Microsoft. The GLEIF/ESMA/SEC EDGAR maps fill the gap for named issuers, but an unlisted issuer returns only ISIN-prefix classification.
- Nested look-through beyond level 2 - a fund holding a fund holding stocks is resolved, but a fund holding a fund holding a fund is not.
- Sector data for all funds- sectors come from iShares CSV and SPDR Excel. Smaller funds that only publish via JustETF or FT.com may have no sector breakdown.
Numbers
- 16 data sources - 8 primary fetchers + 8 legacy fallbacks
- 68 verified CUSIP prefixes for corporate bond identification
- 94 exact XS ISINs for Reg-S eurobonds (Alphabet, Amazon, AT&T, Pfizer, Verizon, and more)
- 566 tests - 338 fixed-income coverage, 50 core views, 91 extended, 87 targeted
- ~65% typical coverage on a European multi-fund portfolio; 100% for iShares/SPDR-heavy portfolios
- less than 1 second for cached funds; 15-30 seconds on first request for unknown funds
Tech stack
Python 3.13, FastAPI, httpx, reportlab, pdfplumber, openpyxl, beautifulsoup4. No ML. No vector database. No fine-tuning. Every data operation is deterministic: fetch, parse, aggregate, classify.
The server is a single Ubuntu VM running the API on port 8071 behind nginx, managed by systemd. Total infrastructure cost: the same VM that runs 15 other services.
Try it
Paste in a few ISINs -- IE00B5BMR087 (iShares Core S&P 500), IE00B4L5Y983 (iShares Core MSCI World), and anything Swiss or European you hold alongside them. The overlap section usually produces the first surprise.
If you use ISINs I have not covered, or if a bond falls through to "unknown", the ISIN and I will add the issuer map entry. That is how the coverage list grew from 34 to 68 CUSIP prefixes in a week.
The tool is also available as a REST API if you want to integrate fund look-through into your own tools: POST /api/analyze accepts an array of {identifier, amount} pairs and returns the six views as structured JSON. Docs at /api/health.
Top comments (0)