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:

  1. 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/2026 plus a limited-operating-history disclaimer.
  2. 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 Cumulative column in MonthlyPerformance.csv / QuarterlyPerformance.csv only 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 dailyEtfDs timeseries 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, default 2026-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 rateDate before counting (the timeseries table can in principle hold dupes for a date).

5. Units & formatting

  • Percentage points everywhere, e.g. 2.34 means +2.34% — not 0.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-inception
    • GET /etf/:ticker/premium-discount/quarterly
  • New file src/http/computed-routes.ts holds 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.