Design decisions for two new computed performance endpoints the API must expose for the fund page at gsretps.io: a daily-refreshed since-inception cumulative return %, and a quarterly premium/discount table. Both are computed in the backend from the daily NAV feed we already store — no US Bank feed change, no Prisma schema change, no new ingest job.
For Agents
Full design spec lives in the repo:
/Users/levander/coding/scharge/bankofus/docs/plans/2026-05-11-computed-performance-endpoints-design.md. This note captures the decisions and the why so they don’t get re-litigated. Status as of 2026-05-11: spec approved, implementation plan being written, no code yet.
Context
The fund page (GSR Crypto Core3 / ticker BESO / NASDAQ / inception 2026-04-22) needs two values the current API doesn’t expose:
- Since-inception cumulative return % — refreshed daily, through the previous business day, for the “Core 3 Performance” section. The frontend collapses the performance table to one line for now:
Since Inception Return: X.XX% / Inception Date: 04/22/2026plus a limited-operating-history disclaimer. - Quarterly premium/discount table — columns
Calendar Quarter | % Days at Premium | % Days at Discount, one row per calendar quarter from inception through the current quarter. Today that’s just Q2 2026; the table grows automatically as quarters pass.
Today GET /etf/:ticker/performance returns only the raw daily NAV / market-price series and the daily premium/discount values — neither aggregate is exposed.
Decisions
1. Compute both values in the backend from dailyEtfDs — do NOT ask US Bank to pre-calculate
Reasoning:
- Since-inception requirement is “updates daily / through the previous business day.” US Bank’s
Since Inception Cumulativecolumn inMonthlyPerformance.csv/QuarterlyPerformance.csvonly refreshes at month / quarter end, and is unpopulated for a fund this young. It structurally cannot satisfy a daily-refresh requirement. - Quarterly premium/discount disclosure isn’t in our feed at all. And the current in-progress quarter can only ever be computed-from-daily anyway — there is no source row for a quarter that hasn’t ended.
- We already store everything needed. Every daily NAV, market price, and premium/discount value since inception is in the
dailyEtfDstimeseries table (append-only, fed by*DailyNAV.csv).
Consequence: no US Bank feed change, no Prisma schema change, no new ingest job. Pure read-side computation.
2. Inception date = 2026-04-22, stored as a configured constant — not derived from data
- Env var
FUND_INCEPTION_DATE, default2026-04-22. - Not derived from the earliest row in the data (a late-arriving backfill or a missing early file would silently shift it).
"04/26/2026" was a typo in the requirements
The requirements text initially said inception was 04/26/2026. That was a slip — the live gsretps.io page shows 04/22/2026 and the user confirmed 04/22. Use 2026-04-22.
3. Since-inception return: expose both NAV-based and market-price-based
- Calc:
(latestRowValue / earliestRowValue − 1) × 100. - No distribution adjustment — the fund has paid none yet. (Methodology revisit flagged below for when distributions start.)
- Both a NAV figure and a market-price figure are returned; the frontend currently only renders one line but the API gives both.
4. Quarterly premium/discount: definitions
For each calendar quarter from inception through the current quarter:
% days at premium=count(daily premium/discount value > 0) / count(distinct usable dates in quarter) × 100% days at discount= same with< 0- Exact-zero days are in neither bucket — by design. This means the two percentages can sum to less than 100. That is intentional, not a bug.
- Null / unparseable daily values are excluded from both numerator and denominator.
- Dedup to one row per
rateDatebefore counting (the timeseries table can in principle hold dupes for a date).
5. Units & formatting
- Percentage points everywhere, e.g.
2.34means+2.34%— not0.0234. - 2 decimal places.
6. Architecture: new hand-written Fastify routes + pure calc functions — deliberately NOT the TrackedTabularDataSource route framework
- Two new routes:
GET /etf/:ticker/performance/since-inceptionGET /etf/:ticker/premium-discount/quarterly
- New file
src/http/computed-routes.tsholds the route handlers. - New directory
src/calc/holds pure calculation functions (testable in isolation). - Why not extend
TrackedTabularDataSource’s route generation? That abstraction is “one CSV → one table → row-shaped routes (latest / timeseries / list)“. These two endpoints are aggregations over a table — a single computed scalar, and a grouped-by-quarter rollup — which don’t fit the row-shaped model. Forcing them in would distort the abstraction. - Also adds vitest — there is no test runner in the repo today; the pure calc functions are the first thing to get unit tests.
Follow-ups (explicitly out of scope for this work)
- Ingest US Bank’s official quarterly premium/discount disclosure file once it starts arriving. Completed quarters would then use the official figures; the in-progress quarter stays computed-from-daily.
- Revisit since-inception methodology to a total-return basis when the fund starts paying distributions (~year 1).
- Per-fund inception dates if more funds are onboarded (the single env var won’t scale).
- Optionally serve the limited-operating-history disclaimer text from the API instead of hardcoding it in the frontend.
Related
- GSR Transformer
- Agent Context (GSR Transformer)
- 2026-05-04 Performance CSV ingest fix — the daily / monthly / quarterly performance CSV plumbing these endpoints sit on top of