ucits-etf-issuer-holdings
Give it the ISIN of a European (UCITS) ETF, get back every holding the issuer itself publishes for that fund: name, weight, ISIN, country, sector, currency, as-of date. Python and TypeScript, one JSON shape, six issuers: iShares, Vanguard, Xtrackers, Amundi, SPDR, Invesco.
It is for the people building portfolio trackers and analysis tools who need a fund's look-through (country, sector or single-name exposure through funds) and have found that the free sources stop at the top ten holdings, cover US funds only, or broke this year.
pip install ucits-etf-issuer-holdings # Python 3.10+
npm install ucits-etf-issuer-holdings # Node 20+, no runtime dependency
The TypeScript package runs on Node.js 20 or later only: it reads the cache and the xlsx files
with node:fs, node:zlib and node:crypto, so it does not run in a browser or a webview.
Quickstart
from ucits_etf_issuer_holdings import get_holdings
fund = get_holdings("IE00B4L5Y983") # iShares Core MSCI World
print(fund.fund_name, fund.as_of, len(fund.holdings))
for h in fund.top(3):
print(f"{h.weight:6.2f} {h.country} {h.name}")
print(fund.weights_by("country")[:3])
iShares Core MSCI World UCITS ETF 2026-10-30 1271
5.62 US APPLE
5.23 US NVIDIA
4.09 US MICROSOFT
[('US', 72.63149), ('JP', 5.74119), ('GB', 3.50965)]
You do not need to know the issuer: the client asks each one in turn. Pass issuer="ishares"
to ask one directly.
Try it offline
The package ships trimmed copies of real issuer answers. The same code runs on them without a network, which is also how you test your own code against this library:
from ucits_etf_issuer_holdings import HoldingsClient
from ucits_etf_issuer_holdings.testing import RECORDED_ISINS, recorded_transport
client = HoldingsClient(transport=recorded_transport())
for isin in RECORDED_ISINS:
fund = client.get(isin)
print(isin, fund.issuer, fund.as_of, len(fund.holdings), fund.fund_name)
Look-through of a portfolio
from ucits_etf_issuer_holdings import HoldingsClient, look_through
from ucits_etf_issuer_holdings.testing import recorded_transport
positions = {"IE00B4L5Y983": 6000, "IE00BK5BQT80": 3000, "LU0274211480": 1000}
client = HoldingsClient(transport=recorded_transport()) # drop transport= for live data
funds = {isin: client.get(isin) for isin in positions}
for country, amount in look_through(positions, funds, by="country")[:5]:
print(country, round(amount, 2))
by is one of country, sector, currency, asset_class, or isin for a single-name
concentration (a line without an ISIN, such as Vanguard's, is counted under its name). Each
position is spread over its fund's holdings by weight.
TypeScript
import { getHoldings, weightsBy } from "ucits-etf-issuer-holdings";
const fund = await getHoldings("IE00BK5BQT80"); // Vanguard FTSE All-World
console.log(fund.fund_name, fund.as_of, fund.holdings.length);
console.log(weightsBy(fund, "sector").slice(0, 3));
The objects are the same JSON as Python's fund.to_dict(), described by
schema/fund.schema.json. Offline replay:
import { recordedTransport } from "ucits-etf-issuer-holdings/testing".
Command line
ucits-holdings IE00B5M1WJ87 --format table
ucits-holdings IE00B4L5Y983 --by country --format csv > exposure.csv
What you get
{
"isin": "IE00B4L5Y983",
"issuer": "ishares",
"fund_name": "iShares Core MSCI World UCITS ETF",
"as_of": "2026-10-30",
"currency": "USD",
"source_url": "https://www.ishares.com/...",
"fetched_at": "2026-11-02T09:41:12Z",
"stale": false,
"holdings": [
{"name": "APPLE", "weight": 5.6239, "isin": "US0378331005", "ticker": "AAPL",
"country": "US", "country_name": "United States", "sector": "Information Technology",
"currency": "USD", "asset_class": "equity", "asset_class_raw": "Equity"}
]
}
weight is a percent of the fund. country is an ISO 3166 alpha-2 code mapped from the
issuer's own country column (country of risk where the issuer says so). asset_class is one of
equity, fixed_income, cash, derivative, fund, real_estate, commodity or other
(a label the library does not know yet), mapped from the issuer's own label, which
asset_class_raw keeps as published. as_of is the date of the holdings, fetched_at the time
the issuer's file was downloaded (for a cached copy, when it was stored). A field the issuer
does not publish is null, never guessed.
| Issuer | Found by | Holdings file | What the issuer does not give | Updated |
|---|---|---|---|---|
| iShares | fund list of the German site | product data API | nothing: every field is filled | daily |
| Vanguard | fund list of the German site | GraphQL API, paged | ISIN and currency per line | month end |
| Xtrackers | ISIN | xlsx export | ticker per line; fund name, fund currency | daily |
| Amundi | ISIN | product data API | ticker per line | daily |
| SPDR | fund list of the German site | xlsx export | ticker and asset class per line; fund currency | daily |
| Invesco | ISIN | holdings API | ticker, country, sector and asset class per line; fund name, fund currency | daily |
On 2026-11-02, through a proxy, scripts/measure_coverage.py
fetched one large fund per issuer (the contract test funds): the weights sum to 99.0 to 100
percent, and every issuer that publishes a country gives one for 100 percent of the weight
(Invesco's file has no country). Run it again to check today's files.
Errors and caching
InvalidISIN: the check digit or the shape is wrong.FundNotFound: no issuer answered with holdings. Itsreasonssay, per issuer, whether the fund is not theirs (NotListed) or the issuer could not be asked (IssuerUnavailable, with what it answered).- Fund lists are cached for a day and holdings for six hours under
~/.cache/ucits-etf-issuer-holdings(cache_dir=Noneturns it off; a transport you pass is not cached unless you also pass acache_dir). An answer is kept only when it parses, so a broken file never replaces a good copy. When an issuer is down, refuses (403), times out (408), rate limits (429) or answers with something the library cannot read, and an older copy exists, you get that copy withfund.stale == Truerather than an error. HoldingsClientis a context manager (with HoldingsClient() as client:), or callclient.close(), to release its connections.- Proxies: the Python client reads
HTTPS_PROXYandALL_PROXY(installhttpx[socks]for SOCKS).
Issuers change their sites without notice. A contract test fetches one real fund per issuer every morning (contract workflow), so a moved endpoint shows up as a red badge the same day.
Why this one
| Option | UCITS issuers | Full holdings | By ISIN | Free, open | Maintained |
|---|---|---|---|---|---|
| ucits-etf-issuer-holdings | 6 | yes, the issuer's file | yes | MIT | daily contract tests |
| etf-scraper | no (US providers) | yes | by ticker | yes | last release 2025-03 |
| justETF based scrapers (justetf-scraping, Apify actors) | yes | top 10 | yes | varies | break with the site |
| etfpy, pyetfdb-scraper | no (etfdb.com, US) | top holdings | by ticker | yes | |
| OpenBB | no (US providers) | provider dependent | by ticker | yes | yes |
| Trackinsight | yes | yes | yes | no, behind a firewall that broke Ghostfolio's enhancer | |
| wealthfolio/asset-profiles | partly | aggregated | by ticker | data CC-BY-NC-SA | last push 2026-05 |
| Intrinio, FactSet, Eulerpool | yes | yes | yes | paid | yes |
The usual workaround is a hand-kept map of issuer endpoints inside each app (see
Aurelio's issuers.py).
This library is that map, shared and tested every day, in Python (Aurelio's language) and
TypeScript (Ghostfolio's). Trackers that cannot load either package, such as Wealthfolio (a Rust
core with a webview front end) or Portfolio Performance (Java), can call the ucits-holdings
command line.
Plugging it into a tracker
Most trackers store a fund's exposure as a list of {code, weight} pairs with weights as
fractions. That is one call:
import { getHoldings, weightsBy } from "ucits-etf-issuer-holdings";
async function countryProfile(isin: string) {
const fund = await getHoldings(isin);
return weightsBy(fund, "country")
.filter(([code]) => code !== null)
.map(([code, pct]) => ({ code, weight: pct / 100 }));
}
The same pattern gives sectors ("sector") or a single-name concentration across a portfolio
(look_through(..., by="isin") in Python, lookThrough(..., "isin") in TypeScript). Open requests this answers:
Ghostfolio #5924 (ETF country and holdings
data), Wealthfolio #827 (UCITS
look-through), Portfolio Performance #1665
(asset allocation from the holdings list; its Java code can call the ucits-holdings CLI with
--by country --format csv).
Notes
- Swap based ETFs (for example "Amundi MSCI World Swap") publish their substitute basket, not the index they track. The file is returned as published; check the fund name.
- Terms of use: these are the files each issuer serves to any visitor of its public product pages. The library fetches one file per fund on demand, caches it, and identifies itself in its user agent. You are responsible for respecting each issuer's website terms in your use.
- Not investment data you can rely on for trading decisions: see each issuer's disclaimer.
Contributing
Issuer moved something, or you want another issuer? See CONTRIBUTING.md. The endpoints and the shape of every answer are documented in docs/PLAN.md.
License
MIT
Metadata
Release files for ucits-etf-issuer-holdings 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ucits_etf_issuer_holdings-0.1.0.tar.gz | 123.8 kB | Without provenance | Details for ucits_etf_issuer_holdings-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ucits_etf_issuer_holdings-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for ucits_etf_issuer_holdings-0.1.0-py3-none-any.whl |
Total release size: 202.1 kB
Release files / ucits_etf_issuer_holdings-0.1.0.tar.gz
| Download URL | ucits_etf_issuer_holdings-0.1.0.tar.gz |
|---|---|
| Size | 123.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
12519b80b11711291eca79aef03919febcd0f6872a6c68f9a67e93f5f5e5c8b3
|
|
BLAKE2b-256 checksum How to use checksums |
00e7737d3625c48db4c29453cdd5a3142682dc5beaaefbad4ccf57908b68e0e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ucits_etf_issuer_holdings-0.1.0-py3-none-any.whl
| Download URL | ucits_etf_issuer_holdings-0.1.0-py3-none-any.whl |
|---|---|
| Size | 78.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dc787ce24b21bf28a53e45d3021659032af0c7800ec6020a10b23a69aac7ed29
|
|
BLAKE2b-256 checksum How to use checksums |
eb0f575f7e0da9512e35d682775aac376b572b52c634911f9b8c0e426d51ccc5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for ucits-etf-issuer-holdings 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.