cexswap: volume-weight WOW price, ignore dead pairs

The naive mean across all WOW pairs let stale prints on zero-volume
markets (WOW-ETH last traded at $0.000087) drag the cexswap component
~40% below its live markets, and the published WOW rate ~20% under fair.
Weight by volume7d_usd and drop pairs with no 7-day volume; if every
pair is dead the ticker falls back to the Nonlogs sample alone.
This commit is contained in:
jwinterm
2026-08-15 20:07:31 -04:00
parent 4f7c515808
commit b016eee8bf
+18 -12
View File
@@ -116,9 +116,11 @@ async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> {
/** /**
* Pull all spot markets from cexswap.cc. Endpoint already returns `last_usd` * Pull all spot markets from cexswap.cc. Endpoint already returns `last_usd`
* per pair so no BTC conversion is needed. We average across all spot pairs * per pair so no BTC conversion is needed. Prices are volume-weighted across
* for the same base ticker (e.g. WOW/USDT, WOW/BTC, WOW/XMR all converted to * the base ticker's pairs by 7-day USD volume, and pairs with no 7-day volume
* USD by cexswap upstream) for robustness. * are ignored entirely — a stale `last` print on a dead pair (WOW-ETH's
* ancient $0.000087, for example) must never drag the average. If every pair
* is dead the ticker is simply omitted and the Nonlogs sample stands alone.
*/ */
async function fetchCexswap(): Promise<Record<string, number>> { async function fetchCexswap(): Promise<Record<string, number>> {
const out: Record<string, number> = {}; const out: Record<string, number> = {};
@@ -127,22 +129,26 @@ async function fetchCexswap(): Promise<Record<string, number>> {
cf: { cacheTtl: 30, cacheEverything: true }, cf: { cacheTtl: 30, cacheEverything: true },
}); });
if (!res.ok) throw new Error(`cexswap http ${res.status}`); if (!res.ok) throw new Error(`cexswap http ${res.status}`);
const body = (await res.json()) as type CexswapRow = { base?: string; last_usd?: number | string; volume7d_usd?: number | string };
| { items?: Array<{ base?: string; last_usd?: number | string }> } const body = (await res.json()) as { items?: CexswapRow[] } | CexswapRow[];
| Array<{ base?: string; last_usd?: number | string }>;
const items = Array.isArray(body) ? body : (body.items ?? []); const items = Array.isArray(body) ? body : (body.items ?? []);
const buckets: Record<string, number[]> = {}; const num = (v: number | string | undefined): number =>
typeof v === "string" ? parseFloat(v) : (v ?? 0);
const buckets: Record<string, Array<{ usd: number; weight: number }>> = {};
for (const m of items) { for (const m of items) {
const base = (m.base ?? "").toUpperCase(); const base = (m.base ?? "").toUpperCase();
if (!NICHE_TICKERS.includes(base as (typeof NICHE_TICKERS)[number])) continue; if (!NICHE_TICKERS.includes(base as (typeof NICHE_TICKERS)[number])) continue;
const usd = typeof m.last_usd === "string" ? parseFloat(m.last_usd) : m.last_usd; const usd = num(m.last_usd);
if (usd && usd > 0) { const weight = num(m.volume7d_usd);
(buckets[base] ??= []).push(usd); if (usd > 0 && weight > 0) {
(buckets[base] ??= []).push({ usd, weight });
} }
} }
for (const [ticker, prices] of Object.entries(buckets)) { for (const [ticker, samples] of Object.entries(buckets)) {
out[ticker] = prices.reduce((a, b) => a + b, 0) / prices.length; const totalWeight = samples.reduce((a, s) => a + s.weight, 0);
out[ticker] = samples.reduce((a, s) => a + s.usd * s.weight, 0) / totalWeight;
} }
return out; return out;
} }