Skip to content
daggerokPublic

About

NEOS ETF holdings-to-watchlist: static ./api/neos feed generated by a Bun updater from neosfunds.com lineup table, official fund pages, daily holdings CSV

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

72 Commits

Folders and files

Repository files navigation

NEOS

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.

Using Bun

bunx degit daggerok/Neos#main ./12345 && cd $_ && bun install && bun run serve
open http://localhost:1234

The 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.

Column types and filters

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)

Updating the static NEOS data

Run the updater with Bun:

bun install --frozen-lockfile
bun test
bun scripts/update-data.ts

Run 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.

Data sources

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.

Metrics and caveats

The daily holdings CSV

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.

Return arithmetic

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 —.

Returns basis and as-of date

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.

Expense ratio mapping

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.

Run behavior

  • MAX_FETCHES batches wrap around the end of the lineup, count only funds that pass the filters, and the cursor is scoped to the filter set. A TICKERS or 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 new meta.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 with dataFile: null and an all-null metrics 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.

dividendYieldBasis

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

Known value limitations

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

Update controls

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.

Examples

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.ts

TypeScript and verification

The 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 --check

bun test also covers the config file, CONTROL_NAMES, --help, this README's controls table and the workflow file.

Brands table

Brand Where to get the data
AAM aamlive.com | AAM
abrdn (Aberdeen) aberdeeninvestments.com | aberdeen
Amplify amplifyetfs.com | Amplify
ARK Invest ark-funds.com | ARK
Capital Group capitalgroup.com | Capital-Group
Fidelity fidelity.com | Fidelity
First Trust ftportfolios.com | First-Trust
Franklin Templeton franklintempleton.com | Franklin
Global X globalxetfs.com/explore | Global-X
Goldman Sachs am.gs.com | Goldman-Sachs
Invesco invesco.com | Invesco
iShares ishares.com | iShares
JPMorgan am.jpmorgan.com | JPMorgan
NEOS neosfunds.com | Neos
Northern Trust etfs.ntam.northerntrust.com | Northern-Trust
Pacer ETFs paceretfs.com | Pacer
Parametric eatonvance.com | Parametric
ProShares proshares.com | ProShares
Schwab schwabassetmanagement.com | Schwab
SP Funds sp-funds.com | SP-Funds
SPDR ssga.com | SPDR
Sprott ETFs sprottetfs.com | Sprott
Tema ETFs temaetfs.com | Tema
Themes ETFs themesetfs.com/etfs | Themes
VanEck vaneck.com | VanEck
Vanguard investor.vanguard.com | Vanguard
VictoryShares vcm.com VictoryShares ETFs | VictoryShares
WisdomTree wisdomtree.com | WisdomTree
Xtrackers etf.dws.com | Xtrackers

Sibling applications

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

License

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.

About

NEOS ETF holdings-to-watchlist: static ./api/neos feed generated by a Bun updater from neosfunds.com lineup table, official fund pages, daily holdings CSV

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages