Skip to content

Commit bb286b5

Browse files
committed
Parse official NAV/Index returns and distribution tax breakdown
The product page's own "Total Returns" table already carries NAV Returns and Underlying Index Returns rows alongside the Market Price Returns row this script parsed; both are now captured into meta.json without touching the existing primary field. Its "Recent Distributions" table (ex/record/payable date plus Ordinary Income/ST Cap Gains/LT Cap Gains/Return of Capital/Total) is now the primary distributions source, merged with Yahoo dividend events only for ex-dates the official table doesn't cover; each row's origin stays visible via a source label. Updated app.tsx subtitles that had become factually wrong once Yahoo was no longer the sole source.
1 parent 5f69fc0 commit bb286b5

4 files changed

Lines changed: 387 additions & 40 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WisdomTree
22

3-
WisdomTree ETF holdings to Watchlist. A single-file client-side tool reading the generated `./api/wisdomtree` static feed (WisdomTree product pages, SEC EDGAR N-PORT-P holdings, Yahoo Finance history) into a searchable ETF/asset-class catalog with per-fund tabs, watchlist, N-PORT upload, CSV/TXT export — the same look, feel, columns and business logic as the sibling applications.
3+
WisdomTree ETF holdings to Watchlist. A single-file client-side tool reading the generated `./api/wisdomtree` static feed (WisdomTree product pages — including official Market Price/NAV/Underlying Index returns and the official tax-character distributions table, SEC EDGAR N-PORT-P holdings, Yahoo Finance history with a narrow distributions/returns fallback) into a searchable ETF/asset-class catalog with per-fund tabs, watchlist, N-PORT upload, CSV/TXT export — the same look, feel, columns and business logic as the sibling applications.
44

55
## Using Bun
66

@@ -31,7 +31,8 @@ The **Update WisdomTree ETF data** GitHub Actions workflow exposes the same sett
3131
| --- | --- |
3232
| Catalog (all US WisdomTree ETFs) | `https://www.wisdomtree.com/investments` (WisdomTree product table) |
3333
| Holdings per fund | SEC EDGAR N-PORT-P `primary_doc.xml` (WisdomTree Trust CIK 0001350487) |
34-
| Daily history, distributions | Yahoo Finance chart API for daily history |
34+
| Per-fund returns, distributions | WisdomTree product page's "Total Returns" table (Market Price/NAV/Underlying Index rows) and "Recent Distributions" table (ex/record/payable date + Ordinary Income/ST/LT Cap Gains/Return of Capital) — the primary source for both |
35+
| Daily history; returns/distributions fallback | Yahoo Finance chart API for daily history unconditionally, and as fallback for Market Price Returns tenors and distribution ex-dates the official page doesn't cover |
3536
| Fallback | WisdomTree product pages for NAV, expense ratio, yields |
3637

3738
Each fund carries a derived `metrics` object that powers the catalog columns shared with the sibling sites:

‎app.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,7 +1565,7 @@ function renderOverviewTable(fund: FundRow): void {
15651565
}
15661566

15671567
el.tickerCount.textContent = fund.ticker;
1568-
renderSubtitle(`${fund.ticker} overview · ${rows.length} metrics. Returns are derived from adjusted market-price closes, not official NAV returns.`);
1568+
renderSubtitle(`${fund.ticker} overview · ${rows.length} metrics. Headline returns use official Market Price Returns where WisdomTree publishes them, falling back to adjusted market-price closes; official NAV/Underlying Index returns are recorded separately in the data feed.`);
15691569
}
15701570

15711571
function renderDistributionsTable(fund: FundRow): void {
@@ -1601,7 +1601,7 @@ function renderDistributionsTable(fund: FundRow): void {
16011601
}
16021602

16031603
el.tickerCount.textContent = fund.ticker;
1604-
renderSubtitle(`${fund.ticker} distributions · dividend history from the Yahoo chart feed (ex-date, amount); frequency is inferred from the cadence.`);
1604+
renderSubtitle(`${fund.ticker} distributions · the official WisdomTree tax-character breakdown for recent ex-dates, with Yahoo dividend events (ex-date, amount only) filling in older ex-dates it doesn't cover; frequency is inferred from the cadence.`);
16051605
}
16061606

16071607
// =========================================================================

‎scripts/update-data.test.ts‎

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
annualizedToTotal,
66
cleanHoldingTicker,
77
inferDistributionFrequency,
8+
mergeDistributionRecords,
89
nportUrlFor,
910
normalizeHoldingName,
1011
parseAumRange,
@@ -13,6 +14,7 @@ import {
1314
parseEdgarAtomFilings,
1415
parseFundTickerMap,
1516
parseNport,
17+
parseOfficialDistributions,
1618
parseProductPageSummary,
1719
parseRange,
1820
priceReturns,
@@ -68,13 +70,110 @@ describe('WisdomTree catalog parser', () => {
6870
});
6971

7072
describe('official WisdomTree product page parser', () => {
73+
// Verified live against wisdomtree.com/us/products/fixed-income/usfr: the
74+
// "Total Returns" table carries three rows per section, in this order —
75+
// Underlying Index Returns, NAV Returns, Market Price Returns.
76+
const page = `# USFR WisdomTree Floating Rate Treasury Fund\n\n### 3.68%\n\n30-day SEC yield\n\nAs of 9/15/2026\n\n| Product Overview | As of 9/16/2026 |\n| --- | --- |\n| Expense Ratio | 0.15% |\n| CUSIP | 97717Y527 |\n| Total Assets (000) | $19,560,180.26 |\n| SEC 30-day Yield | 3.68% |\n\n### Net Asset Value\n\n| Net Asset Value | As of 9/16/2026 |\n| --- | --- |\n| NAV | $50.476 |\n| Premium/Discount to NAV | 0.01% |\n\n### Closing Market Price\n\n| Closing Market Value | As of 9/15/2026 |\n| --- | --- |\n| Closing Market Price | $50.470 |\n\n### Total Returns\n\nMonth End Performance (8/31/2026)\n\n| Cumulative | 1 Month | 3 Month | YTD | Since Inception* | |\n| --- | --- | --- | --- | --- | --- |\n| Underlying Index Returns | 0.33% | 1.04% | 2.70% | 30.94% | |\n| NAV Returns | 0.32% | 1.00% | 2.57% | 28.21% | |\n| Market Price Returns | 0.32% | 1.02% | 2.59% | 28.05% | |\n| Average Annual | 1 Year | 3 Year | 5 Year | 10 Year | Since Inception* |\n| Underlying Index Returns | 4.18% | 4.84% | 4.04% | 2.67% | 2.17% |\n| NAV Returns | 3.98% | 4.66% | 3.86% | 2.49% | 2.00% |\n| Market Price Returns | 4.00% | 4.65% | 3.86% | 2.52% | 1.99% |\n\nQuarter End Performance (6/30/2026)\n\n| Cumulative | Since Inception* | | | | |\n| --- | --- | --- | --- | --- | --- |\n| Underlying Index Returns | 30.04% | | | | |\n| NAV Returns | 27.37% | | | | |\n| Market Price Returns | 27.19% | | | | |\n| Average Annual | 1 Year | 3 Year | 5 Year | 10 Year | Since Inception* |\n| Underlying Index Returns | 4.30% | 4.90% | 4.10% | 2.70% | 2.20% |\n| NAV Returns | 4.10% | 4.70% | 3.90% | 2.50% | 2.02% |\n| Market Price Returns | 4.12% | 4.69% | 3.90% | 2.53% | 2.01% |`;
77+
7178
test('reads NAV, market price, SEC yield, CUSIP and month-end market-price returns', () => {
72-
const page = `# USFR WisdomTree Floating Rate Treasury Fund\n\n### 3.68%\n\n30-day SEC yield\n\nAs of 9/15/2026\n\n| Product Overview | As of 9/16/2026 |\n| --- | --- |\n| Expense Ratio | 0.15% |\n| CUSIP | 97717Y527 |\n| Total Assets (000) | $19,560,180.26 |\n| SEC 30-day Yield | 3.68% |\n\n### Net Asset Value\n\n| Net Asset Value | As of 9/16/2026 |\n| --- | --- |\n| NAV | $50.476 |\n| Premium/Discount to NAV | 0.01% |\n\n### Closing Market Price\n\n| Closing Market Value | As of 9/15/2026 |\n| --- | --- |\n| Closing Market Price | $50.470 |\n\n### Total Returns\n\nMonth End Performance (8/31/2026)\n\n| Cumulative | 1 Month | 3 Month | YTD | Since Inception* | |\n| --- | --- | --- | --- | --- | --- |\n| Market Price Returns | 0.32% | 1.02% | 2.59% | 28.05% | |\n| Average Annual | 1 Year | 3 Year | 5 Year | 10 Year | Since Inception* |\n| Market Price Returns | 4.00% | 4.65% | 3.86% | 2.52% | 1.99% |`;
7379
const parsed = parseProductPageSummary(page);
7480
expect(parsed).toMatchObject({ name: 'WisdomTree Floating Rate Treasury Fund', cusip: '97717Y527', nav: 50.476, marketPrice: 50.47, premiumDiscount: 0.01, secYield: 3.68, totalAssets: 19560180260 });
7581
expect(parsed.navAsOfDate).toBe('2026-09-16');
7682
expect(parsed.officialReturns.monthEnd).toMatchObject({ asOfDate: '2026-08-31', ytd: 2.59, cagr3y: 4.65, siAnn: 1.99 });
7783
});
84+
85+
test('captures NAV Returns and Underlying Index Returns rows alongside Market Price Returns, without overwriting it', () => {
86+
const parsed = parseProductPageSummary(page);
87+
const monthEnd = parsed.officialReturns.monthEnd!;
88+
// Market Price Returns (the primary row) is untouched.
89+
expect(monthEnd).toMatchObject({ mo1: 0.32, qtd: 1.02, ytd: 2.59, yr1: 4.00, cagr3y: 4.65, cagr5y: 3.86, cagr10y: 2.52, siAnn: 1.99 });
90+
// NAV Returns and Underlying Index Returns are distinct labeled rows.
91+
expect(monthEnd.navReturns).toMatchObject({ mo1: 0.32, qtd: 1.00, ytd: 2.57, yr1: 3.98, cagr3y: 4.66, cagr5y: 3.86, cagr10y: 2.49, siAnn: 2.00 });
92+
expect(monthEnd.indexReturns).toMatchObject({ mo1: 0.33, qtd: 1.04, ytd: 2.70, yr1: 4.18, cagr3y: 4.84, cagr5y: 4.04, cagr10y: 2.67, siAnn: 2.17 });
93+
});
94+
95+
test('handles the Quarter End cumulative row, which the site renders with blank trailing cells', () => {
96+
// Verified live against wisdomtree.com/us/products/fixed-income/usfr:
97+
// the Quarter End "Cumulative" row only ever publishes Since Inception,
98+
// leaving the 1-month/3-month/YTD cells blank. Pre-existing indexing
99+
// (unchanged by this parser extension) reads that single value into the
100+
// same cumulative-row slot Month End uses for its 1-month figure; the
101+
// point of this test is that the blank cells parse cleanly to null on
102+
// the new nav/index rows too, instead of throwing or becoming NaN.
103+
const parsed = parseProductPageSummary(page);
104+
const quarterEnd = parsed.officialReturns.quarterEnd!;
105+
expect(quarterEnd.mo1).toBe(27.19);
106+
expect(quarterEnd.qtd).toBeNull();
107+
expect(quarterEnd.navReturns).toMatchObject({ mo1: 27.37, qtd: null, ytd: null, yr1: 4.10, cagr3y: 4.70, cagr5y: 3.90, cagr10y: 2.50, siAnn: 2.02 });
108+
expect(quarterEnd.indexReturns).toMatchObject({ mo1: 30.04, qtd: null, ytd: null, yr1: 4.30, cagr3y: 4.90, cagr5y: 4.10, cagr10y: 2.70, siAnn: 2.20 });
109+
});
110+
111+
test('leaves navReturns/indexReturns null when the table only publishes Market Price Returns', () => {
112+
const marketOnly = `# GDE WisdomTree Efficient Gold Plus Equity Strategy Fund\n\n### Total Returns\n\nMonth End Performance (8/31/2026)\n\n| Cumulative | 1 Month | 3 Month | YTD | Since Inception* | |\n| --- | --- | --- | --- | --- | --- |\n| Market Price Returns | 1.00% | 2.00% | 3.00% | 4.00% | |`;
113+
const parsed = parseProductPageSummary(marketOnly);
114+
expect(parsed.officialReturns.monthEnd).toMatchObject({ mo1: 1.00, navReturns: null, indexReturns: null });
115+
});
116+
});
117+
118+
describe('official WisdomTree distributions parser', () => {
119+
test('reads ex-date/record/payable dates and the full tax-character breakdown', () => {
120+
// Verified live against wisdomtree.com/us/products/equity/dgrw.
121+
const page = '### Recent Distributions\n\n| Ex-Dividend Date | Record Date | Payable Date | Ordinary Income | Short Term Capital Gains | Long Term Capital Gains | Return of Capital | Total Distribution |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 9/25/2026 | 9/25/2026 | 9/29/2026 | $0.17000 | $0.00000 | $0.00000 | $0.00000 | $0.17000 |\n| 8/26/2026 | 8/26/2026 | 8/28/2026 | $0.05500 | $0.00000 | $0.00000 | $0.00000 | $0.05500 |\n\n### Thought Leadership\n\n| Name | Ticker |\n| --- | --- |\n| unrelated | table |';
122+
const rows = parseOfficialDistributions(page);
123+
expect(rows).toHaveLength(2);
124+
expect(rows[0]).toEqual({ exDate: '2026-09-25', recordDate: '2026-09-25', payableDate: '2026-09-29', ordinaryIncome: 0.17, shortTermCapitalGains: 0, longTermCapitalGains: 0, returnOfCapital: 0, total: 0.17 });
125+
expect(rows[1].exDate).toBe('2026-08-26');
126+
});
127+
128+
test('tolerates missing/blank cells the site sometimes renders', () => {
129+
const page = '### Recent Distributions\n\n| Ex-Dividend Date | Record Date | Payable Date | Ordinary Income | Short Term Capital Gains | Long Term Capital Gains | Return of Capital | Total Distribution |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 3/25/2020 | 3/25/2020 | 3/27/2020 | $0.05000 | | | | $0.05000 |\n| 12/24/2019 | 12/24/2019 | 12/26/2019 | — | — | — | — | — |';
130+
const rows = parseOfficialDistributions(page);
131+
expect(rows).toHaveLength(2);
132+
expect(rows[0]).toMatchObject({ ordinaryIncome: 0.05, shortTermCapitalGains: null, longTermCapitalGains: null, returnOfCapital: null, total: 0.05 });
133+
expect(rows[1]).toMatchObject({ ordinaryIncome: null, shortTermCapitalGains: null, longTermCapitalGains: null, returnOfCapital: null, total: null });
134+
});
135+
136+
test('returns an empty list when the page has no Recent Distributions section', () => {
137+
expect(parseOfficialDistributions('# GDE\n\n### Total Returns\n\nno distributions section here')).toEqual([]);
138+
});
139+
});
140+
141+
describe('official-first distributions merge (house policy: official wins, Yahoo fills only what it does not cover)', () => {
142+
test('prefers the official row over an overlapping Yahoo dividend event', () => {
143+
const official = parseOfficialDistributions('### Recent Distributions\n\n| Ex-Dividend Date | Record Date | Payable Date | Ordinary Income | Short Term Capital Gains | Long Term Capital Gains | Return of Capital | Total Distribution |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 9/25/2026 | 9/25/2026 | 9/29/2026 | $0.17000 | $0.00000 | $0.00000 | $0.00000 | $0.17000 |');
144+
const yahoo = [{ epoch: Date.UTC(2026, 8, 25) / 1000, amount: 0.169 }]; // Yahoo's own rounding for the same ex-date.
145+
const { rows, yahooOnlyCount } = mergeDistributionRecords(official, yahoo);
146+
expect(rows).toHaveLength(1);
147+
expect(rows[0]).toMatchObject({ total: 0.17, ordinaryIncome: 0.17 });
148+
expect(yahooOnlyCount).toBe(0);
149+
});
150+
151+
test('falls back to Yahoo dividend events for older ex-dates the small official table does not cover', () => {
152+
const official = parseOfficialDistributions('### Recent Distributions\n\n| Ex-Dividend Date | Record Date | Payable Date | Ordinary Income | Short Term Capital Gains | Long Term Capital Gains | Return of Capital | Total Distribution |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 9/25/2026 | 9/25/2026 | 9/29/2026 | $0.17000 | $0.00000 | $0.00000 | $0.00000 | $0.17000 |');
153+
const yahoo = [
154+
{ epoch: Date.UTC(2013, 5, 20) / 1000, amount: 0.05 }, // an old, pre-tax-breakdown-era ex-date the official table no longer shows.
155+
{ epoch: Date.UTC(2026, 8, 25) / 1000, amount: 0.169 },
156+
];
157+
const { rows, yahooOnlyCount } = mergeDistributionRecords(official, yahoo);
158+
expect(rows).toHaveLength(2);
159+
expect(yahooOnlyCount).toBe(1);
160+
expect(rows[0]).toMatchObject({ exDate: '2013-06-20', total: 0.05, ordinaryIncome: null });
161+
expect(rows[1]).toMatchObject({ exDate: '2026-09-25', total: 0.17 });
162+
});
163+
164+
test('is exercised end-to-end as the fallback path for a fund whose official page has no distributions table at all', () => {
165+
// Simulates a fund where productPageMarkdown fetched fine but carries no
166+
// Recent Distributions section (or the product-page fetch failed
167+
// entirely) — the same shape update-data.ts builds when
168+
// parseOfficialDistributions([]) short-circuits to [].
169+
const official = parseOfficialDistributions('# Some Fund\n\nno distributions section here');
170+
expect(official).toEqual([]);
171+
const yahoo = [{ epoch: Date.UTC(2026, 5, 25) / 1000, amount: 0.16 }, { epoch: Date.UTC(2026, 6, 28) / 1000, amount: 0.065 }];
172+
const { rows, yahooOnlyCount } = mergeDistributionRecords(official, yahoo);
173+
expect(yahooOnlyCount).toBe(2);
174+
expect(rows.map((row) => row.total)).toEqual([0.16, 0.065]);
175+
expect(rows.every((row) => row.ordinaryIncome === null)).toBe(true);
176+
});
78177
});
79178

80179
describe('SEC mapping and N-PORT', () => {

0 commit comments

Comments
 (0)