One of the app's features lets you select NEOS 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 reading the generated ./api/neos static feed (NEOS ETF catalog, per-fund pages, official daily full-holdings CSV, NAV / market price / premium-discount, performance and distribution history, SEC EDGAR N-PORT-P and Yahoo Finance as fallbacks) into a searchable ETF/asset-class 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/Neos#main ./12345 && cd $_ && bun install && bun run serve
open http://localhost:1234The published application is available at https://daggerok.github.io/Neos/. The site is built by Parcel (bun run build writes it to dist, bun run build-github-pages does the same for the /Neos/ public URL) and deployed by the GitHub Pages workflow .github/workflows/github-pages.yml. bun run serve is the Parcel dev server (it copies api/ into dist/api first). The app reads only relative ./api/neos/** files generated by scripts/update-data.ts; the committed feed is complete, so the site works with no update run at all.
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 install --frozen-lockfile
bun test
bun scripts/update-data.tsRun bun scripts/update-data.ts --help to print every control with its default and usage examples.
Every control is listed in scripts/update-data.config.json (a flat object, all values strings). The updater and the workflow use the same resolver (resolveControls in scripts/update-data.ts), with this precedence: file defaults < advanced JSON < nonblank workflow inputs < protected Actions variable or environment variable. A blank input inherits the file value; advanced can set a key to an empty string on purpose. Unknown keys, non-scalar values and multiline values are rejected. All supplied filters use AND logic.
The Update NEOS ETF data GitHub Actions workflow (.github/workflows/update-data.yml) runs every Sunday at 00:00 UTC and on workflow_dispatch. It exposes the most common controls as individual inputs plus an advanced JSON object for the rest, and commits api/neos/** and nothing else - a data run never rewrites app code or CI config. The SEC_UA repository Actions variable, when nonblank, wins over every other layer.
| Block | Source | Notes |
|---|---|---|
| Fund universe, ticker, fund name, declared distribution frequency, distribution rate, 30-Day SEC Yield, management fee, net assets, inception date, asset class | neosfunds.com/#explore-etfs |
The server-rendered "Explore Our ETFs" table, which is also what fills the site's own tabs: Equity High Income, Boosted High Income, High Income Alternatives, Hedged Equity Income, Enhanced Fixed Income |
| CUSIP, ISIN, NAV, market price, premium / discount, 30-Day Median Bid-Ask Spread, primary exchange, shares outstanding, underlying exposure, management fee, total annual fund operating expenses | https://neosfunds.com/<ticker>/ |
The page's own Fund Details, Closing NAV Price, Closing Market Price and Premium / Discount panels. Underlying Exposure is absent on five funds (see below). |
| Holdings (daily) | https://neosfunds.com/wp-admin/admin-ajax.php?action=download_holdings_csv&ticker=<TICKER> |
The exact URL the fund page's Download Full Holdings button calls (etf-pages.js → etf_ajax.ajax_url), so the file is the official daily holdings sheet: Date, Account, StockTicker, Cusip, SecurityName, Shares, Price, MarketValue, Weightings, NetAssets, SharesOutstanding, CreationUnits, MoneyMarketFlag |
| NAV-basis performance (1 Mo/3 Mo/6 Mo/YTD/1 Yr/3 Yr/5 Yr/10 Yr/Inception, cumulative and annualized), monthly and quarter-end, plus the closing-market-price row and the benchmark row | The fund page Performance tables (#monthly-performance, #quarterly-performance) |
This feed publishes the NAV Performance row for every return metric, exactly as every other column-heading in the app assumes |
| Distribution history (declaration, ex-dividend, record, payable dates + amount per share) and the Distribution Information block (Distribution Rate, 12-Month Trailing Distribution Rate, Distribution Amount / Share in $ and %) | The fund page Distributions tabs |
Copied verbatim from the year-tabbed calendar, newest first; a month that is declared but not yet paid keeps NEOS's empty amount in the JSON and the app prints — for that cell rather than a blank |
| Growth-of-$10,000-at-NAV series since inception | The fund page Growth of $10,000 at NAV Since Inception chart (const dates / const navValues / const indexValues2) |
Stored as series metadata (start, end, point count); the app plots its own charts |
| Fund documents (prospectus, summary prospectus, SAI, annual and semi-annual report, fiscal Q1/Q3 portfolio holdings, tax insert, Form 8937) | The fund page Documents table and Form 8937 tab |
One row per published document; a document NEOS has not published yet stays null and the app prints — |
| Daily price history (Date / Close / Adj Close / Volume) | Yahoo Finance public chart API | Used for the price-history sheets; the fund's own official NAV and market price are always preferred where NEOS publishes them |
| Holdings fallback | SEC EDGAR Form N-PORT-P, NEOS ETF Trust CIK 0001848758 (file 811-23645) | Used only when a holdings CSV is unavailable (EDGAR_FALLBACK); enabled in the config file, set EDGAR_FALLBACK to false to skip it (SEC EDGAR has answered GitHub runner IPs with 403) |
| Dividend-history fallback | Yahoo Finance dividend events | Only for a fund with no published Distribution History rows; the feed marks those rows distributions.fallback: true |
neosfunds.com throttles bursts (the TLS connection is reset), so requests are globally paced by REQUEST_SLEEP (config default 2s) with bounded retries; every request has a 45 s budget (headers and body) per attempt.
The download is a real CSV served by WordPress, and it is the same file a visitor gets from the fund page. scripts/update-data.ts includes a small, dependency-free RFC-4180 reader (parseCsv) that tolerates the BOM, CRLF and quoted fields.
NEOS publishes no asset-class column, so the Watchlist's Asset Category is derived from each row's own fields — Cash (MoneyMarketFlag=Y or the Cash&Other line), Option (an OCC contract code such as SPXW 261001P07075000), Treasury (a T-bill or note), Fund (an ETF/trust the fund holds) and otherwise Equity — and that derivation is documented, tested and never applied to any other value. Every other cell is the provider's own text, including a written option's negative market value and share count.
Cusip is the identifier the CSV publishes, so it fills the shared Identifier column; option and T-bill rows key on it because they have no exchange ticker. The per-row NetAssets, SharesOutstanding and CreationUnits columns (identical on every row) are lifted to fund level.
The page publishes annualized 3 Yr / 5 Yr / 10 Yr figures and a cumulative Inception figure. The catalog carries both readings, computed from the published one with (1 + r)^n - 1 or (1 + TR)^(1/n) - 1, and returns.derivedFrom records that the basis is NEOS's own NAV Performance table. Nothing is extrapolated past a tenor the fund has not lived through: a fund younger than its 5-year mark publishes null, and the app prints —.
Every funds[].metrics row in api/neos/index.json ends with two fields: returnsBasis, a non-empty label saying the returns are NEOS's official NAV total returns from the month-end Performance table (3/5/10 Yr cumulative derived from the published annualized figures), and performanceAsOf, the ISO YYYY-MM-DD date of that table (not the NAV date), null only when the page prints none. Unavailable numbers are null, never 0.
terValue is the net expense ratio and terGrossValue the gross one. NEOS publishes a single all-in figure, Total Annual Fund Operating Expenses (management fee plus acquired fund fees, no waiver), so both carry that number and terGross repeats its text. The management fee alone is published separately as managementFee / managementFeeValue. A page that omits the total gives null, never the management fee. The TER filter reads the management fee of the lineup table.
MAX_FETCHESbatches wrap around the end of the lineup, count only funds that pass the filters, and the cursor is scoped to the filter set. ATICKERSor filtered run never deletes the cursor; only a full unfiltered pass resets it.- A fund is either fully updated (pages, then
meta.json, then its index row) or fully kept from before. Files are written through a temporary file and renamed; stale pages are removed only after the newmeta.json. - A rerun with identical upstream data changes nothing, including
generatedAt. - The run stops taking new funds after 25 minutes and still writes the index.
- New lineup funds are printed as
NEW FUNDS: ...(and appended to the step summary) and get an index row withdataFile: nulland an all-nullmetrics object until their first successful update. - The run exits non-zero when every attempted fund failed or a requested ticker is not in the lineup.
Every funds[].metrics row carries dividendYieldBasis, a code for the definition behind dividendYield; it is null exactly when dividendYield is null (placeholder rows included). A retained yield keeps its code, because the code is derived from the yield the row holds.
| Code | Meaning for NEOS |
|---|---|
official-distribution-rate |
The only code NEOS emits: the official Distribution Rate from the fund page's Distribution Information block (latest distribution annualized over ex-date NAV), or the same figure from the lineup table when the block is missing |
official-trailing-12m, official-other, computed-trailing-12m, indicated |
Never emitted: the trailing 12-month rate is kept separately as yield12M and never used as dividendYield, and nothing is computed or estimated |
| Metric | Status | Reason |
|---|---|---|
| YTD / 1 Yr / 3 Yr / 5 Yr / 10 Yr returns | published from NEOS's own performance tables; — only for a tenor the fund is too young to report |
XBCI, XQQI and XSPI (Feb 2026 inception) have no YTD, 1 Yr or SI-annualized figure yet; 14 funds have no 3 Yr; 17 have no 5 Yr; SPYI is the 10 Yr case. This is exactly what the pages themselves print as -. |
| Underlying Exposure | — for HYBI, IAUI, NIHI, QQQH and SPYH |
Those five fund pages carry an Investment Objective paragraph instead of the Underlying Exposure row; the feed publishes null rather than guessing an index from the fund's name |
| FY-Q3 Portfolio Holdings | — for 9 funds |
Not published on the fund page yet |
| Annual Report / Supplemental Tax Information | — for XBCI, XQQI and XSPI |
The three February-2026 funds have not filed a first annual report yet |
| Form 8937 | — for IAUI, NIHI, XBCI, XQQI and XSPI |
Those five 8937 tabs are empty — NEOS has not filed one for them yet |
| 12-Month Trailing Distribution Rate | — for IAUI, MLPI, NEHI, NIHI, NLSI, SPYH, XBCI, XQQI and XSPI |
Their Distribution Information block publishes the Distribution Rate and the 30-Day SEC Yield only; the trailing row is absent even where the fund is older than 12 months (IAUI), so it is never inferred from the payout calendar |
| SEDOL / FIGI | always — |
NEOS publishes neither (the holdings CSV carries a CUSIP, and the fund page CUSIP/ISIN), so the app shows the documented data limitation |
| Coupon / Maturity | always — |
NEOS publishes no fixed-income holdings sheet with coupon or maturity columns |
| SEC Yield (30-day) | official fund page figure, — where the page prints -- |
Every NEOS fund currently publishes one; negative values (e.g. QQQI -0.05%) are kept verbatim |
| Inception Date | the lineup table's figure; the fund page's own panel date is kept as inceptionDateFundPage and explained by inceptionNote |
HYBI is the one fund where the two differ: the ETF converted from the WSTCM Credit Select Risk-Managed Fund on 09/27/2024 and assumed its performance, so the lineup table keeps 09/30/2014 — the date its published 5Y and 10Y returns are measured from |
| Premium / Discount | the fund page's own Premium Discount (%) row |
The (Market Price - NAV) / NAV quotient is stored alongside as premiumDiscountDerivedValue, and is used only if a page ever omits the row |
Defaults below are the values in scripts/update-data.config.json. Environment variables use the same names; the HISTORICAL_PAGE_SIZE alias of HISTORY_PAGE_SIZE still works.
| Control | Default | Meaning |
|---|---|---|
MAX_FETCHES |
0 |
Funds to process; 0 = full pass. A positive value resumes after the cursor in api/neos/update-state.json and wraps around |
REQUEST_SLEEP |
2 |
Seconds between request starts; neosfunds.com throttles bursts |
CONCURRENCY |
2 |
Parallel fund workers |
MAX_RETRIES |
3 |
Retries (integer >= 1) after the initial request for network errors and HTTP 408/425/429/5xx |
TICKERS |
"" |
Space or comma separated ticker allowlist; ANDed with the other filters, never overriding them |
CATEGORY |
"" |
Substring match on the NEOS asset-class group |
AUM |
: |
min:max dollars; also accepts K/M/B/T suffixes and nano, micro, small, mid, large |
TER |
: |
min:max expense ratio percent |
DIVIDEND_YIELD |
: |
min:max catalog distribution rate percent |
SEC_YIELD |
: |
min:max catalog 30-day SEC yield percent |
PERFORMANCE_YTD / _1Y / _3Y / _5Y / _10Y |
"" |
min:max filters on the official NAV return; a fund without that figure is excluded |
TOTAL_RETURN_YTD / _1Y / _3Y / _5Y / _10Y |
"" |
min:max filters on the derived cumulative total return; a fund without that figure is excluded |
HOLDINGS_PAGE_SIZE |
250 |
Rows per generated holdings JSON page |
HISTORY_PAGE_SIZE |
1000 |
Rows per generated history JSON page |
HISTORY_RANGE |
max |
Yahoo history window: max or Ny (for example 5y); sent as explicit period1/period2 because Yahoo ignores range when period1=0 |
STORE_RAW_DOWNLOADS |
false |
Keep raw samples under api/neos/raw |
SEC_UA |
daggerok ETF feed daggerok@gmail.com |
Declared User-Agent for SEC EDGAR requests; redacted in config logs, the SEC_UA repository Actions variable overrides it |
EDGAR_FALLBACK |
true |
Use the fund's own N-PORT-P series (exact series name match, never older than the published holdings) when a holdings CSV is unavailable |
SKIP_YAHOO |
false |
Do not call Yahoo; the previously published history pages and manifest stay untouched |
SKIP_NEOS |
false |
Do not read neosfunds.com; the published fund data stays as it is and only the Yahoo history is refreshed |
VERBOSE |
false |
Print per-fund retry and fallback notices |
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 |
A failed or skipped Yahoo request never empties a fund's history: the previous pages and manifest are kept. Range syntax is strict min:max with exactly one colon; "" and : mean no restriction. Funds not selected for a successful update keep their prior published metadata and data files, so a bounded or partly failed run can never empty the site.
MAX_FETCHES=10 bun scripts/update-data.ts
TICKERS="SPYI QQQI CSHI" bun scripts/update-data.ts
AUM="1B:" TER=":0.70" bun scripts/update-data.ts
CATEGORY="Fixed Income" bun scripts/update-data.tsThe browser app lives in src/: index.html (markup), main.tsx (TypeScript), index.css (Tailwind CSS v4 plus the app styles) and favicon.ico, built by Parcel into dist (bun run build, same setup as daggerok/Stocks, daggerok/csv and daggerok/options-desk) - no tsconfig.json 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 --checkbun test also covers the config file, CONTROL_NAMES, --help, this README's controls table and the workflow file.
| 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.
NEOS® and the fund names/tickers referenced here are trademarks of NEOS Investment Management LLC. This is an independent, unofficial tool; it is not affiliated with, endorsed by, or sponsored by NEOS. All data is reproduced from NEOS's own public pages and downloads, with public SEC EDGAR filings and Yahoo Finance as fallbacks, for research purposes. All other trademarks, including index names, are the property of their respective owners.