Publishing¶
openbasket is an aggregator and a quick-lookup tool. It holds no data of its own: every fund, every row and every page says which public source it came from, with a link, an as-of date and the source’s credit line. It is not investment advice and is not affiliated with any fund issuer.
The scheduled build runs two pipelines after the usual ones:
kedro run --pipeline=universe
kedro run --pipeline=publish --params=publish.release_tag=2026-11
universe looks through every fund openbasket knows in one batch;
publish turns the result into the static JSON API described in
Data API and the release files. Neither runs as part of a plain
kedro run.
The universe¶
Every fund in the issuer lists (issuer_products: iShares UK and US, SPDR US
and UCITS) and every N-PORT series with a latest filing (fund_master).
- One fund, many ISINs
Share classes of one issuer portfolio are one fund. The largest share class by net assets gives the fund its
id; the others are aliases. An N-PORT series’ share-class ISINs are those other funds hold whose issuer LEI is the series’ LEI and whose issuer type is a registered fund (security_master). A series with no known ISIN is keyed by its series ID.- One fund, two sources
A US ETF that is both an issuer fund and an N-PORT series is one fund, keyed by the issuer’s ISIN. The series is matched by ISIN first, then, for US issuer sites only, by ticker (UCITS tickers are local codes). The series ID stays on the fund, so its N-PORT holdings remain a fallback, and
series_matchsays how it was found.
Every ISIN names one fund; issuer funds claim theirs first. On 2026 Q3 N-PORT and the 3 November 2026 issuer lists the universe is 14,870 funds: 1,913 iShares, 362 SPDR and 12,595 N-PORT series, with 605 US issuer ETFs matched to their series (261 by ISIN, 344 by ticker).
Running it¶
Each fund is expanded with the same engine and providers as the
lookthrough pipeline, named by its fund ref rather than matched from an
ISIN.
A fund that raises is recorded with its error and a short traceback, and the batch moves on. Warnings logged while a fund is expanded (an issuer file that could not be read, say) are kept in its
warningscolumn, and a fund with no holdings says why in itsnote.Each fund’s time is recorded in
seconds; progress is logged everyuniverse.log_everyfunds with an estimate of the time left.Requests to one host are spaced at least
universe.http.default_interval_sapart (1 s), whichever client makes them;per_host_interval_soverrides it per host. Issuer files are cached per fund per day, so a rerun on the same day makes no requests.Once a source resolves
lookthrough.min_resolved_pct, later sources are not read (universe.stop_early). TickerInside is asked only for tickers its fund list puts in the issuer layer (universe.tickerinside_prefilter).Which N-PORT series issues an ISIN is looked up in an index built with one scan of N-PORT; a series’ own lines are read by series ID from
nport_holdings, which is sorted by series ID.OpenFIGI names and types are added once, for every constituent ISIN of the run, through the shared cache, which is saved every
universe.openfigi_save_everyfunds and at the end.Constituent rows are written to Parquet parts under
universe.work_diras they are made, so memory does not grow with the universe.
Quick runs¶
Parameter |
Effect |
|---|---|
|
Fund lists to include: |
|
Only these fund ids or series IDs. |
|
This many funds per list, chosen with |
|
Then only the first this many. |
kedro run --pipeline=universe --params=universe.sample=14 # 42 funds
Outputs¶
universe(data/03_primary/universe.parquet)The whole universe before selection:
id,isin,isins,series_id,issuer_id,name,ticker,issuer,universe_source,series_match.universe_funds(data/07_model_output/universe_funds.parquet)One row per fund run: the columns above plus
source,source_url,source_ref,credit,as_of,matched_via,note,sources_used,sources_tried,reported_total_pct,resolved_pct,unresolved_pct,unidentified_lines_pct,n_constituents,error,traceback,warningsandseconds.sourceis the source of the fund’s own holdings, or the list the fund came from when no source had any.creditjoins the credit lines of every source its rows came from, its own first.universe_constituents(data/07_model_output/universe_constituents.parquet)Every constituent row of every fund, with
fund_id, and one unresolved-weight row per fund. Columns as in Output, plussource_urlandname_source(who supplied the name: the holdings source, oropenfigi). Unlikelookthrough.csv, a row never mixes sources: an ISIN held directly according to N-PORT and through a sub-fund according to that sub-fund’s issuer file is two rows, each with its ownsource,as_ofandvia_fund.universe_run(data/08_reporting/universe_run.json)Start and end, seconds, funds attempted, succeeded, failed and without holdings, rows written, HTTP requests and time spent waiting.
Publishing¶
publish writes, under publish.site_dir (default build/site, not
committed), api/v1/meta.json, api/v1/index.json, one
api/v1/funds/{id}.json per fund and an {"alias_of": id} file for every
other share-class ISIN; and under publish.release_dir (default
build/release) the three release files. Funds whose run failed are left
out of the API but kept, with their error, in openbasket-funds.parquet.
- Sources
publish.sourcesinconf/base/parameters.ymlholds each source’s name, link, licence and credit line.meta.sourceslists every source that contributed a row or a fund, with the number of funds and the range of as-of dates; a source missing from the configuration is still listed, by its id, with a warning. Each fund file’screditnames every source its rows came from, and each constituent row carries its ownsourceandas_of.- Release links
publish.release_tag(default: the current UTC year and month) andpublish.repo(omaxim/openbasket) giverelease_urlandfull_data_url, the download link ofopenbasket-lookthrough.parquet.- Size
Fund files keep their
publish.max_constituents(1,000) heaviest rows and saytruncated: truewhen rows were cut. Ifapi/v1still comes out larger thanpublish.site_budget_mb(800), the cut is lowered, never belowpublish.min_constituents(50), and the large funds are rewritten. Every cut is logged and recorded in the quality report; no row is dropped from the release file.quality-report.jsonFunds listed, attempted, succeeded, failed, without holdings and published per list; funds by holdings source, by how they were matched and by how their series was found; the resolved-weight distribution; seconds per fund by list; the most common errors and warnings, with example funds; every fund whose source’s lines add up to more than 5 points away from 100%; API size and cuts; release file sizes; and the run record. It is also saved as
data/08_reporting/quality_report.json.
Sample run¶
Run on 3 November 2026 with universe.sample=14 (42 funds, 14 per list),
2026 Q3 N-PORT and that day’s issuer files, starting with no issuer files
cached:
iShares |
SPDR |
N-PORT |
|
|---|---|---|---|
Funds attempted |
14 |
14 |
14 |
Succeeded |
14 |
14 |
14 |
No holdings |
2 |
0 |
0 |
Seconds per fund, mean |
0.98 |
0.93 |
0.25 |
Seconds per fund, median |
0.96 |
0.94 |
0.03 |
No fund failed. The two iShares funds without holdings are index mutual funds whose page lists only the ten largest lines.
Of the 40 funds with holdings, 29 resolve 99 to 101% of their weight to ISINs, 6 resolve 90 to 99%, 1 resolves 50 to 90% (ProShares Short FTSE China 50, whose N-PORT lines add up to 74.77%) and 4 more than 101%, whose reported lines add up to more than their net assets (two core bond ETFs at 121% and 120%). The median is 99.89%.
The run took 57 s: 30 s expanding funds, of which 12 s waiting on the throttle, and 24 s mapping 4,609 new constituent ISINs through OpenFIGI.
publishtook under a second and wrote 1.9 MB of API (42 fund files, 5 cut to 1,000 rows), a 0.8 MB look-through file and a 27 kB fund file.A rerun the same day, with every issuer file cached, took 5 s.
A larger sample of 300 funds (universe.sample=100, universe.seed=1) gives
steadier timings: 1.61 s per iShares fund (median 1.00, slowest 23 s for a
large bond fund), 0.95 s per SPDR fund and 0.36 s per N-PORT series (median
0.03). 28 of the 100 iShares funds had no full holdings table. 271 funds had
holdings; their median resolved weight was 99.82% and 252 resolved 90% or
more. The API came to 12.8 MB, about 43 kB a fund. OpenFIGI mapped 31,220
new ISINs in 152 s, about 205 a second.
The full universe¶
From the 300-fund timings:
Step |
Estimate |
|---|---|
1,913 iShares funds |
about 50 min (requests to one host are spaced 1 s apart) |
362 SPDR funds |
about 6 min |
12,595 N-PORT series |
30 to 75 min (most take 0.03 s; large funds and the 223 with a TickerInside ticker take longer) |
OpenFIGI, first run |
about 27 min for the 330,000 N-PORT ISINs not yet in the cache; minutes once the cache is kept between runs |
|
a few minutes |
That is two to three hours for a first run and under two and a half once the
OpenFIGI cache (data/03_primary/openfigi_cache.parquet) is kept between
builds, well inside a six-hour job. The API should come to about 630 MB,
under the 800 MB budget, so fund files would be cut only at 1,000
constituents.
Reference¶
Look through every fund openbasket knows, in one batch.
The universe is every fund in the issuer lists (iShares, SPDR) plus every N-PORT series with a latest filing. A US ETF that is both is one fund, keyed by the issuer’s ISIN, with the series kept so N-PORT can still serve as a fallback. Share classes of one portfolio are aliases of one fund.
Funds are expanded one at a time with the same engine and providers as the
lookthrough pipeline. Unlike its output, a constituent row here is one
ISIN from one source through one fund, so every row’s source is exact. A fund that raises is recorded with its error and
the batch moves on. Constituent rows go to Parquet parts on disk as they are
made, so memory stays flat however many funds there are.
- openbasket.pipelines.universe.nodes.build_universe(issuer_products, fund_master, security_master, params)[source]¶
Every known fund, one row each, with every ISIN that names it in
isins.A series joins an issuer fund when one of its share-class ISINs is the issuer fund’s ISIN, or, for US issuer sites, when one of its tickers is the issuer fund’s ticker. An ISIN belongs to one fund only: issuer funds claim theirs first.
- Parameters:
issuer_products (DataFrame)
fund_master (DataFrame)
security_master (DuckDBPyRelation)
params (dict)
- Return type:
DataFrame
- openbasket.pipelines.universe.nodes.select_universe(universe, params)[source]¶
The funds to run:
sources, thenids, thensamplefunds per source, thenlimit.- Parameters:
universe (DataFrame)
params (dict)
- Return type:
DataFrame
- openbasket.pipelines.universe.nodes.fund_ref(fund)[source]¶
The engine’s ref for a universe row; issuer funds keep their series for the N-PORT fallback.
- Parameters:
fund (dict)
- Return type:
- openbasket.pipelines.universe.nodes.expand_fund(fund, providers, params)[source]¶
Look through one universe fund. Never raises: an error is returned in the summary.
- Parameters:
fund (dict)
params (dict)
- Return type:
tuple[dict, DataFrame]
- openbasket.pipelines.universe.nodes.run_universe(universe, nport_holdings, security_master, fund_master, ticker_isin_map, issuer_products, cusip_isin_map, sedol_isin_map, lookthrough_params, params, sources)[source]¶
Expand every selected fund. Returns the fund table, the constituent rows and a run record.
- Parameters:
universe (DataFrame)
nport_holdings (DuckDBPyRelation)
security_master (DuckDBPyRelation)
fund_master (DataFrame)
ticker_isin_map (DataFrame)
issuer_products (DataFrame)
cusip_isin_map (DuckDBPyRelation)
sedol_isin_map (DataFrame)
lookthrough_params (dict)
params (dict)
sources (dict)
- Return type:
tuple[DataFrame, DuckDBPyRelation, dict]
Turn a universe run into the static JSON API and the release files.
The shapes are those in docs/source/data-api.md. Every fund file names
its source, a link, the as-of date and a credit line; every constituent row
names its own source and as-of date; meta.json lists every source that
contributed a row, with its licence and credit, from publish.sources.
Fund files are cut to max_constituents rows, and if the API still comes
out larger than site_budget_mb the cut is lowered and the large funds are
rewritten. Each cut is logged and recorded in the quality report; the full
lists are always in the release file.
- openbasket.pipelines.publish.nodes.release_urls(repo, tag)[source]¶
The release page and the full look-through file’s download link for
tag.- Parameters:
repo (str)
tag (str)
- Return type:
tuple[str, str]
- openbasket.pipelines.publish.nodes.fund_document(fund, rows, n_rows, full_data_url)[source]¶
One
funds/{id}.json.rowsare the kept constituent rows,n_rowshow many there are in all.- Parameters:
fund (dict)
rows (list[tuple])
n_rows (int)
full_data_url (str)
- Return type:
dict
- openbasket.pipelines.publish.nodes.publish(universe, universe_funds, universe_constituents, universe_run, params)[source]¶
Write
api/v1undersite_dirand the release files underrelease_dir; return the quality report.- Parameters:
universe (DataFrame)
universe_funds (DataFrame)
universe_constituents (DuckDBPyRelation)
universe_run (dict)
params (dict)
- Return type:
dict
- openbasket.pipelines.publish.nodes.quality_report(universe, funds, run, published)[source]¶
Counts per source, resolution, timing, errors and funds whose lines do not add up to about 100%.
- Parameters:
universe (DataFrame)
funds (DataFrame)
run (dict)
published (dict)
- Return type:
dict
A process-wide politeness throttle for the sites openbasket reads.
Issuer sites are public web servers, not APIs with published limits. A
universe run asks each of them for thousands of files, so every request to a
host waits until at least min_interval seconds have passed since the last
request to that host, whichever client made it.
- openbasket.throttle.DEFAULT_INTERVAL_S = 0.5¶
Seconds between requests to one host unless configured otherwise.