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("IE00BK5BQT80") # Vanguard FTSE All-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])
Vanguard FTSE All-World UCITS ETF (USD) Accumulating 2026-09-30 4235
5.01 US NVIDIA Corp
4.52 US Apple Inc
3.58 US Microsoft Corp
[('US', 62.28004), ('JP', 6.05742), ('TW', 3.53726)]
You do not need to know the issuer: the client asks each one in turn. Pass issuer="vanguard"
to ask one directly. iShares funds need a home connection or a proxy: iShares refuses most
cloud and datacenter addresses (see Blocked by an issuer).
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 IE00BK5BQT80 --by country --format csv > exposure.csv
ucits-holdings IE00B4L5Y983 --proxy http://user:pass@proxy.example:8080 # iShares from a server
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; refuses most cloud and datacenter addresses (below) | 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 |
| UBS | not covered | none found | its product pages (ubs.com/<country>/<lang>/assetmanagement/funds/etf/<isin>.html) load the fund in the browser and offer no public holdings file we could find (checked 2026-11-02); the free sites we checked show its top 10 only |
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). Itsblockednames the issuers whose site refused your address (AccessDenied, below): the fund may be theirs.- 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:
HoldingsClient(proxy="http://..."),get_holdings(..., proxy=...)orucits-holdings --proxy ...in Python; without it the client readsHTTPS_PROXYandALL_PROXY(installhttpx[socks]for SOCKS). In Node, setHTTPS_PROXYtogether withNODE_USE_ENV_PROXY=1(Node 22.21 or later, 24.5 or later), or pass your ownTransport.
Blocked by an issuer
iShares (BlackRock) serves www.ishares.com through Akamai, which refuses most cloud and
datacenter address ranges with 403 Access Denied, whatever the request looks like. Measured on
2026-11-02 from an OVH server: every iShares and BlackRock page answered 403, with the library's
user agent, a browser's headers and a browser's TLS fingerprint alike, over IPv4 and IPv6; the
same requests from a residential connection answered 200 with the full holdings. So iShares
funds work from a laptop on a home connection and fail from most servers, CI runners and
notebooks hosted in the cloud.
The library says so instead of failing vaguely: the refusal is AccessDenied (a subclass of
IssuerUnavailable, so code that caught that still works), its message names the cause, and
FundNotFound.blocked lists ishares. A cached copy, when you have one, is served with
stale=True. To read iShares from a server, send the requests through a proxy on a residential
or otherwise unblocked address (proxy= above). The other five issuers answer from servers.
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.1
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.1.tar.gz | 129.7 kB | Without provenance | Details for ucits_etf_issuer_holdings-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ucits_etf_issuer_holdings-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for ucits_etf_issuer_holdings-0.1.1-py3-none-any.whl |
Total release size: 209.8 kB
Release files / ucits_etf_issuer_holdings-0.1.1.tar.gz
| Download URL | ucits_etf_issuer_holdings-0.1.1.tar.gz |
|---|---|
| Size | 129.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7e2fb0cd2ed24392ca45be76fe6f6784b6c09e73fa598ce4ef4bdf08a902259d
|
|
BLAKE2b-256 checksum How to use checksums |
e73d064d9250a6240289f66229fd8453fcb1cf0a869769b49af75deadcdcc465
|
| 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.1-py3-none-any.whl
| Download URL | ucits_etf_issuer_holdings-0.1.1-py3-none-any.whl |
|---|---|
| Size | 80.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f1a8142c19095776dd358c5d8a0c27e0ae4f77c891d82aa9df34bf5f19e67a5b
|
|
BLAKE2b-256 checksum How to use checksums |
d9956c5039e6d1a976df6972c99ff370b3fe2b32743ce3b0d800e7714fb2c2db
|
| 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.1
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.