One of the app's features lets you select SP Funds ETFs in the Watchlist and aggregate their holdings to see how often each ticker appears across the selected funds. Repeated holdings make overlapping exposure visible: the more selected funds include a ticker, the greater its potential influence on the portfolio; gains in that holding may help, while declines may hurt, and actual impact also depends on each fund's position size. Another feature makes it faster and easier to find funds with stronger growth over different periods, higher dividend yields or distributions, greater Total Return (price performance plus dividends), and other key performance metrics. A single-file client-side tool that reads the generated ./api/spfunds static feed (official sp-funds.com homepage catalog and per-fund pages - legal name, identifiers, pricing, month-end/quarter-end returns, distribution history and document links - plus each fund's published daily holdings CSV, with SEC EDGAR N-PORT-P as a holdings fallback and Yahoo Finance daily prices/history/dividend fallbacks) into a searchable ETF catalog with per-fund tabs, watchlist aggregation, ticker copy and CSV/TXT export - the same look, feel, columns and business logic as the sibling applications.
bunx degit daggerok/SP-Funds#main ./12345 && cd $_
bun install
bun run servebun run serve starts the Parcel dev server (it copies api/ to dist/api first) and prints the local address. bun run build writes the production site into dist, and bun run build-github-pages does the same with the /SP-Funds/ public URL.
The application is live at https://daggerok.github.io/SP-Funds/ (GitHub Pages is deployed from main by .github/workflows/github-pages.yml, which builds dist and publishes it).
Every column of the ETF catalog and of the Watchlist, Holdings, History and Distributions tabs has a type: text (ABC), number (123), percentage (%), money ($), date (D), date and time (DT) or time of day (T). The type is detected from the texts the column shows (80% of the filled cells must agree, otherwise text) and is written in the badge next to the column title: click it to cycle the type, Shift+click to return to auto-detection. Dates are read as 2024-06-15, 6/15/2024, 15.06.2024, Jun 15, 2024 or 15-Jun-2024, date and time as 2024-06-15T09:30:00Z or 2024-06-15 09:30, time as 09:30, 16:00:00 or 9:30 PM
A row of filter inputs sits under the column headers (the Filters button hides it, Clear filters empties it). Filters of different columns are combined with AND, the search box applies on top, and Copy Tickers and the exports use the filtered rows. Filters and type overrides are remembered in the browser. Sticky # (next to Filters, off by default, remembered in the browser) numbers the rows by their rank in the table sorted by the current column before the column filters, so a filtered fund keeps its rank and the numbers keep gaps; the sort, the search and the category and blacklist choices rank again. The catalog starts sorted by Net Assets, largest first, unavailable values sort last in both directions, and every export starts with the # column. The red Clear button opens a dialog that lists what can be reset (the selection, searches, sort order, open tab, shown columns, column filters, remembered table views and the blacklist), all ticked the first time and afterwards as they were left at the last OK; Enter confirms, Esc or a click outside cancels, the theme is always kept, so the page looks like a first visit (also after a reload)
Inside one filter: a space means AND, a comma means OR, a leading ! means NOT, ? matches an empty or unavailable value and !? a value that is there; a value that is unavailable matches only ? and negated conditions. An unquoted space ends the value, so quote values that contain one (>="2024-06-15 09:30")
| Type | Examples |
|---|---|
| Text | bank contains, "two words", !bank, =exact, ^starts, ends$, /regex/, tech, health |
| Number, percentage, money | >10, >=10 <50, =22 (matches what rounds to 22), !=22, 10..50, ..50, 10.., >1B and K M B T suffixes, an optional $ or % |
| Date, date and time | >2024-06-01, 2024 (the whole year), 2024-06 (the whole month), 2024-01..2024-06, today, yesterday, -7d.. (the last 7 days), +2w, -3m, -1y |
| Time | >09:30, 09:30..16:00, =12:00 (the whole minute) |
The Columns menu next to Filters lists every column of the ETF table from the first to the last, all of them shown by default, with a search box and the All, Clear, Toggle and Reset buttons. Use and Ticker are listed but locked. Hiding a column only removes it from the table: the filters, the sorting, the exports and Copy Tickers still use it. The choice is remembered in the browser (localStorage, never the data) and the menu is shown on the ETF catalog only
The asset classes are one Asset classes multi-select next to the All ETFs pill instead of one tab per class: every class is selected by default (= all ETFs), Only or unchecking narrows the table, and the All ETFs pill is lit only while nothing narrows it (all or none of the classes checked); clicking the pill clears the selection. The choice is remembered in the browser (localStorage, never the data)
Run the updater with Bun:
bun test
./scripts/update-data.tsRun ./scripts/update-data.ts -h (or --help) to print every configuration variable with its default and usage examples.
Defaults live in scripts/update-data.config.json (every control as a string). Precedence: file defaults < advanced JSON < nonblank individual inputs < protected Actions variable/environment. An explicitly set environment variable always wins, even when empty, and invalid values fail with a clear message instead of falling back silently. The Update SP Funds ETF data GitHub Actions workflow runs on Sundays at 00:00 UTC and on demand, and resolves controls with the same resolveControls function as the CLI: individual workflow_dispatch inputs are blank by default and inherit the file, and the advanced input accepts a JSON object with any control (for example {"EDGAR_FALLBACK":"false","VERBOSE":"true"}; GitHub allows at most 25 inputs). The updater only writes below api/spfunds. All supplied filters use AND logic, and pacing stays polite at 1 second per lane with 2 lanes because sp-funds.com is not WAF-protected.
| Block | Source |
|---|---|
| Catalog (all five SP Funds ETFs) | Official sp-funds.com "Our ETFs" homepage menu - SPUS, SPRE, SPSK, SPTE, SPWO; a built-in five-fund lineup is retained if the homepage markup changes. |
| Holdings per fund | Official daily CSV https://www.sp-funds.com/wp-content/uploads/data/TidalFG_Holdings_{TICKER}.csv (Date, Account, StockTicker, CUSIP, SecurityName, Shares, Price, MarketValue, Weightings, NetAssets, SharesOutstanding, CreationUnits); e.g. SPUS. |
| Fund page facts | https://www.sp-funds.com/<ticker-lowercase>/: Fund Details (inception, primary exchange, CUSIP, expense ratio, 30-day SEC yield), Fund Details & Pricing (NAV, net assets, shares outstanding, premium/discount, closing price), Performance Statistics (Month-End and Quarter-End tables), Distribution Details and the documents block. The homepage menu link wins over a homepage card that points at a section page. |
| Daily history, distributions | Yahoo Finance chart API (daily close/adjusted close/volume and dividends, adjusted closes rounded to two decimals); the fund page's Distribution Details table supplies the published distribution calendar and frequency when present. |
| Fallback | SEC EDGAR N-PORT-P holdings only (Tidal Trust I, CIK 0001742912, for SPUS/SPRE/SPSK; SP Funds Trust, CIK 0001989916, for SPTE/SPWO - exact series/registrant matching) + previously published data as the last resort. |
SEC endpoints have answered HTTP 403 to GitHub-hosted runners, so the N-PORT-P fallback is exercised offline against a small inline sample and is used only when the issuer's daily CSV is unavailable. Yahoo's adjusted close is recomputed server-side and can move by one cent between otherwise identical requests, so a repeat run may rewrite a history page without any other change.
- Holdings come from the issuer's own daily CSV, which publishes no asset-class column; the Watchlist category is derived from the row itself (cash lines Cash, options Option, fund units Fund, everything else Equity). Cash cushions keep the provider's negative weight and value
- SPRE holds foreign listings; their exchange-suffixed tickers (for example
GMG AU) are kept as published - Returns are the official month-end NAV figures as published (3Y/5Y/10Y annualized); cumulative figures are derived from them. A tenor longer than the fund's age at the table date (for example 3 years for SPTE and SPWO, which began in November and December 2023) is published by the site as
0.00or-and is left empty here, never 0: every tenor is checked as inception + N years <= table date - Daily history, closing prices beyond the page's own close and dividends are Yahoo Finance market data; the distribution calendar, frequency and indicated yield are derived estimates, not issuer-reported yields. Official NAV, net assets and the 30-day SEC yield come from the fund page with their as-of dates
HISTORY_RANGEnarrows the Yahoo request window (explicit period1/period2; period2 is now + 1 day so the latest bar is included). Published rows older than the window are kept and only the window is refreshed- Fund-level consistency: every fund is either fully updated or fully kept. If the fund page, the holdings (CSV, then an N-PORT-P filing that is not older than the published holdings) or the Yahoo history fails for a fund that already has published files, that fund stays exactly as published and the run reports it; when every processed fund is kept this way the run exits non-zero. A fund page that loads without one of its tables keeps the published values of that section as a unit (returns together with
performanceAsOfandreturnsBasis); a table that is present with a blank cell publishes the blank - Writes are atomic (temp file + rename) and ordered: pages, then
meta.json, then the index; stale pages are removed after the newmeta.json. A rerun with identical upstream data writes nothing (generatedAtmoves only with content). The run stops taking new funds after 25 minutes and still writes the index; unprocessed funds without files havedataFile: nulland an all-nullmetricsobject. New lineup tickers printNEW FUNDS: ...and go to the step summary - Published shapes: history rows are oldest first with
Sep 30 2026dates, holdingsWeight(percent) andMarket Value(USD) are plain numbers,generatedAthas no milliseconds, index rows carryclosePriceValue,premiumDiscountValueandterGrossValue.terValueis the net expense ratio (the only one SP Funds publishes),terGrossValuestaysnullunless a gross figure appears on the page. A semi-annual distribution frequency means 2 payments per year.secYieldmay legitimately be a published 0.00 - No ticker exclusions: the five ETFs are the lineup; the site's six target-date mutual funds are out of scope
Each fund carries a derived metrics object that powers the catalog columns shared with the sibling sites:
-
ytd/tr1y- official NAV YTD and 1-year returns -> YTD Return, TR 1Y -
cagr3y/cagr5y/cagr10y- official annualized 3Y/5Y/10Y figures -> CAGR 3Y/5Y/10Y -
tr3y/tr5y/tr10y- cumulative 3Y/5Y/10Y figures(1 + CAGR)^n - 1-> TR 3Y/5Y/10Y -
siAnn- since-inception annualized NAV return when the site publishes it -
dividendYield- indicated distribution rate (latest distribution × frequency ÷ NAV) when the page publishes no explicit rate;—otherwise -
dividendYieldBasis- code of the definition behinddividendYield,nullexactly whendividendYieldisnull; here onlyindicated(SP Funds publishes no distribution yield). The other codes of the shared standard are not produced:Code Meaning Used here official-trailing-12mprovider-published trailing 12-month distribution yield no official-distribution-rateprovider-published distribution rate no official-otherprovider-published yield with another or unclear definition no computed-trailing-12mupdater sums the last 12 months of distributions over price/NAV no indicatedlatest distribution x inferred payments per year / NAV yes -
secYield- 30-day SEC yield when published;—otherwise -
returnsBasis- mandatory non-empty label of how the returns are computed: here always the official SP Funds month-end NAV total returns from the fund page performance table, with the cumulative 3/5/10-year figures derived from the published annualized ones -
performanceAsOf- mandatory ISOYYYY-MM-DDdate of that performance table (the month-endDatecolumn), not the NAV date;nullonly when the table date is unknown. Both fields are the last two keys ofmetrics
Defaults below are from scripts/update-data.config.json; blank Actions inputs do not override them. The individual workflow inputs cover every control except SEC_UA, EDGAR_FALLBACK, VERBOSE and USE_SYSTEM_CA, which are set through advanced.
| Environment variable | Default | Meaning |
|---|---|---|
MAX_FETCHES |
0 |
Batch evaluation size: positive resumes the scoped cursor in api/spfunds/update-state.json; 0 is a full selected pass. A failed fund prevents cursor advancement |
REQUEST_SLEEP |
1 |
Minimum seconds between outgoing request starts in each independent lane, including retries |
CONCURRENCY |
2 |
Parallel fund workers (integer >= 1), each with its own paced request lane. CONCURRENCY=15 ./scripts/update-data.ts fetches up to 15 funds at once; the lineup has five funds, so at most five run together |
TICKERS |
all | Space/comma/semicolon allowlist, e.g. SPUS SPSK SPTE. Unknown requested tickers fail before writes |
AUM |
: |
Net Assets range: USD amounts or K/M/B/T suffixes; nano/micro/small/mid/large presets; inclusive min:max |
TER |
: |
Net expense ratio (terValue) range in % (strict min:max); funds without a published ratio do not pass an active range |
DIVIDEND_YIELD |
: |
Distribution-yield range in %, min:max; missing values do not pass an active range |
SEC_YIELD |
: |
Official 30-day SEC-yield range in %, min:max; missing values do not pass |
HOLDINGS_PAGE_SIZE |
250 |
Current holdings rows per JSON page |
HISTORY_PAGE_SIZE |
1000 |
Daily history rows per JSON page |
MAX_RETRIES |
2 |
Retries after the initial request, integer >= 1 (transient HTTP/network failures only) |
HISTORY_RANGE |
max |
Yahoo Finance history window: max or <N>y (for example 5y); applied to the request as explicit period1/period2 and replaces the published daily history with that window |
PERFORMANCE_YTD |
: |
YTD performance percent min:max (3Y/5Y/10Y annualized) |
PERFORMANCE_1Y |
: |
1Y performance percent min:max (3Y/5Y/10Y annualized) |
PERFORMANCE_3Y |
: |
3Y performance percent min:max (3Y/5Y/10Y annualized) |
PERFORMANCE_5Y |
: |
5Y performance percent min:max (3Y/5Y/10Y annualized) |
PERFORMANCE_10Y |
: |
10Y performance percent min:max (3Y/5Y/10Y annualized) |
TOTAL_RETURN_YTD |
: |
YTD cumulative total return percent min:max |
TOTAL_RETURN_1Y |
: |
1Y cumulative total return percent min:max |
TOTAL_RETURN_3Y |
: |
3Y cumulative total return percent min:max |
TOTAL_RETURN_5Y |
: |
5Y cumulative total return percent min:max |
TOTAL_RETURN_10Y |
: |
10Y cumulative total return percent min:max |
SEC_UA |
daggerok ETF feed daggerok@gmail.com |
SEC EDGAR User-Agent contact; redacted in logs. The repository Actions variable SEC_UA overrides it in the workflow |
SKIP_SPFUNDS |
false |
Skip the official sp-funds.com fund pages (keeps published facts); alias SKIP_SP_FUNDS |
SKIP_YAHOO |
false |
Skip Yahoo Finance (keeps published history/dividends) |
EDGAR_FALLBACK |
true |
Use SEC EDGAR N-PORT-P when the issuer's daily holdings file is unavailable |
VERBOSE |
false |
Provider, fallback and retry detail; the compact fund reporter always retains real zero/false values and omits missing fields |
USE_SYSTEM_CA |
auto |
TLS trust store: auto restarts the updater once with Bun's --use-system-ca when a request fails with an untrusted-certificate error; true always uses the system CA store; false never restarts. Not an individual workflow input: use advanced, the config file or the CLI environment. |
TICKERS combines with AUM, TER, yield and return filters using AND logic; it does not override them. Funds not selected for a successful update keep their prior published metadata and data files.
MAX_FETCHES=10 ./scripts/update-data.ts
TICKERS="SPUS SPSK SPTE" ./scripts/update-data.ts
AUM="1B:" TER=":0.5" ./scripts/update-data.ts
PERFORMANCE_1Y="15:" ./scripts/update-data.ts
HISTORY_RANGE=5y CONCURRENCY=15 ./scripts/update-data.tsThe browser app lives in src/: index.html carries the markup and bootstrap, main.tsx is the TypeScript app and index.css holds Tailwind v4 and the component styles. Parcel bundles them into dist (bun run build) - no tsconfig.json is needed. Bun runs the updater TypeScript out of the box.
Verification before every publish:
bun install --frozen-lockfile
bun test
bun build --target=bun scripts/update-data.ts --outfile=/dev/null
git diff --check| Application | Data provider | Repository |
|---|---|---|
| AAM | Official AAM catalog/detail HTML + full holdings XLS + SEC N-PORT holdings fallback + Yahoo market history/dividends | AAM |
| abrdn (Aberdeen) | Official Aberdeen gateway + SEC N-PORT holdings fallback + Yahoo history/dividends | aberdeen |
| Amplify | Amplify ETFs Firestore data feed + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance history/dividends | Amplify |
| ARK Invest | ark-funds.com fund pages + overview/NAV-history/performance JSON + official daily holdings CSV + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance distributions/history fallback | ARK |
| Capital Group | Official Capital Group fund data + SEC N-PORT holdings fallback + Yahoo history fallback | Capital-Group |
| Fidelity | SEC EDGAR N-PORT-P + Yahoo Finance | Fidelity |
| First Trust | ftportfolios.com official ETF list + fund summary, holdings, distribution and price-history export pages + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance history fallback | First-Trust |
| Franklin Templeton | franklintempleton.com ETF listings + product pages + SEC EDGAR N-PORT-P | Franklin |
| Global X | globalxetfs.com Next.js catalog and fund pages + dated full-holdings CSV | Global-X |
| Goldman Sachs | am.gs.com fund finder + detail pages + SEC EDGAR N-PORT-P | Goldman-Sachs |
| Invesco | invesco.com fund pages and sitemap + official Invesco fund API (monthly returns, NAV, AUM, yields, daily holdings, expense ratio) + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance history/dividends | Invesco |
| iShares | iShares (BlackRock) product workbooks | iShares |
| JPMorgan | am.jpmorgan.com fund explorer + product-data JSON | JPMorgan |
| NEOS | neosfunds.com lineup table + official fund pages + daily holdings CSV | Neos |
| Northern Trust | etfs.ntam.northerntrust.com funds list + per-fund CSV/JSON downloads | Northern-Trust |
| Pacer ETFs | paceretfs.com product catalog and fund pages (Cloudflare WAF; r.jina.ai proxy fallback) + SEC EDGAR N-PORT-P (Pacer Funds Trust) + Yahoo Finance history/dividends | Pacer |
| Parametric | eatonvance.com ETF catalog and Parametric product pages + SEC EDGAR N-PORT-P holdings + Yahoo Finance history/dividends | Parametric |
| ProShares | proshares.com ETF finder + fund pages + official data host | ProShares |
| Schwab | schwabassetmanagement.com product pages + CSV exports | Schwab |
| SP Funds | sp-funds.com homepage catalog, fund pages and daily holdings CSV + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance history/dividends | SP-Funds |
| SPDR | SSGA / State Street public feeds | SPDR |
| Sprott ETFs | sprottetfs.com fund pages + SEC EDGAR N-PORT-P (Sprott Funds Trust) + Yahoo Finance history/dividends | Sprott |
| Tema ETFs | Tema official fund pages + dated daily holdings CSV; SEC EDGAR N-PORT-P holdings fallback only + Yahoo Finance price/history/dividend fallback | Tema |
| Themes ETFs | themesetfs.com catalog + daily holdings CSV + Yahoo Finance history/dividends + SEC N-PORT-P holdings fallback | Themes |
| VanEck | vaneck.com ETF finder + product pages | VanEck |
| Vanguard | Vanguard product pages + SEC EDGAR N-PORT-P | Vanguard |
| VictoryShares | VCM VictoryShares catalog and product JSON + SEC EDGAR N-PORT-P holdings fallback + Yahoo Finance adjusted-market-price history | VictoryShares |
| WisdomTree | WisdomTree product table + SEC EDGAR N-PORT-P + Yahoo Finance | WisdomTree |
| Xtrackers | Official DWS catalog/US sitemap + PDP/XLSX + SEC N-PORT-P holdings fallback + Yahoo Finance daily prices/history/dividends | Xtrackers |
MIT — same as all sibling ETF repositories.
SP Funds, the fund names and tickers referenced here, and all index names are the property of their respective owners. This is an independent, unofficial tool; it is not affiliated with, endorsed by, or sponsored by SP Funds or the funds' trusts. All data is reproduced from sp-funds.com's own public fund pages and downloads, public SEC EDGAR filings and Yahoo Finance for research purposes. All other trademarks are the property of their respective owners.