Over a week-long holiday I built a small dashboard for a narrow problem: investors in mainland China who want US index exposure.
The context: many onshore Nasdaq-100 funds now cap purchases at ¥5–100 a day, or have paused them entirely. Demand moved to exchange-traded ETFs, and on Sep 30 those were trading 10–11% above their indicative NAV (IOPV). Put ¥10,000 in and you pay roughly ¥900–1,000 more than the underlying assets are worth.
The page itself is simple: a list of ETFs sorted by that premium. The hard part is that every number comes from free public endpoints that lag, rate-limit and change shape without notice. A wrong number on a page like this costs readers real money, so a surprising amount of the code exists to decide when not to show a number.
Three pieces of it.
1. Two numbers from the same response must agree
The quote endpoint returns one ~-separated string per ticker. Field 3 is the last price, field 78 is the IOPV, and field 77 is the source's own premium. I compute the premium myself and only accept the IOPV when my number matches theirs:
const iopv = Number(fields[78]);
const reported =
fields[77] !== "" && fields[77] !== undefined ? Number(fields[77]) : NaN;
const calculated = positive(iopv) ? (price / iopv - 1) * 100 : null;
// Cross-check both fields before accepting the reference value.
const validReference =
calculated !== null &&
Number.isFinite(reported) &&
Math.abs(calculated - reported) <= 0.05;
return {
// ...price, bid/ask, turnover, quote time...
iopv: validReference ? iopv : null,
premiumPercent: validReference ? calculated : null,
};
Price and IOPV come out of the same response, so at least they describe the same moment. If the two premiums disagree by more than 0.05 percentage points, something upstream is off (a stale field, a shifted column), and the card says "IOPV unavailable" instead of showing a confident-looking number.
The same rule carries into the ranking. Without a usable IOPV, a card falls back to the last published NAV, labelled as such, and is sorted in a separate group. A day-old NAV next to a live price makes the premium look bigger or smaller than it really is, so the two never share one list. There's a test with exactly that name:
test("ETF premium preserves unknowns and does not mix IOPV with lagged NAV in rankings", () => {
// ...
});
2. A failed fetch must not look like fresh data
Every API route is the same small wrapper around a loader (lightly trimmed):
export function createFeedHandler(loader, ttlSeconds, mergePayload = (prev, next) => next) {
let lastGood = null;
let pending = null;
let expires = 0;
async function retrieve() {
if (lastGood && Date.now() < expires) return lastGood;
if (pending) return pending;
pending = (async () => {
try {
const value = await loader();
lastGood = mergePayload(lastGood, {
...value,
checkedAt: new Date().toISOString(),
stale: false,
});
expires = Date.now() + (value.errors?.length ? 60 : ttlSeconds) * 1000;
return lastGood;
} catch (error) {
if (!lastGood) throw error;
return {
...lastGood,
stale: true,
lastAttemptAt: new Date().toISOString(),
error: error.message,
};
} finally {
pending = null;
}
})();
return pending;
}
// ...fetch(request) calls retrieve() and sets the headers below
}
Three things in there are deliberate:
-
On failure it returns the last good payload, marked
stale: true, with the time of the failed attempt and the error. The data keeps its original timestamp. The UI shows that date and a stale badge; it never re-stamps old numbers with "now". -
pendingmakes the upstream call single-flight. Concurrent requests share one fetch, which matters when the sources rate-limit. - A partial failure expires in 60 seconds instead of the full TTL (300 s for ETF quotes, 900 s for fund purchase status, a day for holdings), so a hiccup heals quickly instead of being cached for an hour.
Then the CDN headers:
// CDN expiry is explicit. No stale-while-revalidate that could hide an outage.
const headers = {
"Cache-Control": "public, max-age=0, must-revalidate",
"Vercel-CDN-Cache-Control": `public, s-maxage=${duration}`,
};
stale-while-revalidate is usually a fine default. Here it would let the CDN keep serving an old payload without the stale flag while revalidation fails in the background: precisely the "looks fresh, isn't" case the handler above exists to prevent.
3. Nothing is fetched while nobody is looking
There is no cron job. The server only loads data when a request arrives, and the client only polls while the tab is visible:
const check = () => {
if (
!document.hidden &&
Date.now() - lastAttempt.current >= retryInterval.current
)
refresh();
};
const timer = setInterval(check, Math.min(config.interval, 60000));
document.addEventListener("visibilitychange", check);
window.addEventListener("online", check);
window.addEventListener("focus", check);
A background tab costs zero requests; coming back to it triggers a check immediately. Together with the shared server cache, ten readers cost the upstream about as much as one. Since the whole thing runs on public sources with no paid keys, being a polite client is part of keeping it alive.
What the page says out loud
Every card shows when its quote was taken and where it came from, with a link back to the source. Research notes are dated to the filing they cite, and refreshing prices never rewrites them.
None of this is clever. It's the lesson from my notes export post again: a pipeline that fails quietly is worse than one that fails loudly. On a page about money, "I don't know" is a valid output.
The site is live at https://nasdaq-observer.vercel.app (Chinese UI). Not investment advice, just the receipts.
Top comments (0)