nonlogs: ignore markets with no 24h volume, like the cexswap leg already does

The cexswap leg drops pairs with no 7-day volume and says why: a stale
last print on a dead pair must never drag the average. The nonlogs leg
did not, and averaged WOW-USDT — a 0.00723 print with null volume — against
a WOW-BTC route with real trades behind it.

Today that pulls the nonlogs leg 15% low, and since the leg is half the
WOW blend, the published price 8.4% low: 0.009830 against 0.010653. The
drag grows as WOW moves away from whenever that print happened.

Both routes are now volume-weighted by 24h quote volume, and a venue with
no volume anywhere is omitted so the other venue's sample stands alone —
the same shape as the cexswap function.

This feed is what Hash Bags, Smirk, wowlet and the desk's own fair value
display, so the number moves for all of them on deploy.
This commit is contained in:
jwinterm
2026-09-03 12:51:21 -04:00
parent 3f38aa4fdc
commit 074b232267
+42 -16
View File
@@ -3,7 +3,9 @@
* *
* One Cloudflare Worker that: * One Cloudflare Worker that:
* - Fetches USD spot prices from Kraken for majors (BTC/XMR/LTC/DOGE/ETH/BCH/XNO). * - Fetches USD spot prices from Kraken for majors (BTC/XMR/LTC/DOGE/ETH/BCH/XNO).
* - Fetches WOW from NonKYC (Wownero is too small for major CEX listings). * - Fetches WOW from Nonlogs + cexswap.cc (too small for major CEX listings).
* Both legs ignore markets with no 24h volume: a stale last print is not a
* price, and this feed is what Hash Bags, Smirk and wowlet display.
* - Caches results in KV every 60s via cron, then serves from KV on request. * - Caches results in KV every 60s via cron, then serves from KV on request.
* *
* Endpoints: * Endpoints:
@@ -110,9 +112,20 @@ async function fetchKraken(): Promise<Record<string, number>> {
} }
/** /**
* Pull all markets from Nonlogs in one call. Returns per-ticker USD price * Pull all markets from Nonlogs in one call. Returns a per-ticker USD price,
* by averaging the (TICKER-BTC × BTC/USD) and (TICKER-USDT) routes when both * volume-weighting the (TICKER-BTC × BTC/USD) and (TICKER-USDT) routes by
* exist. Algorithm taken from ~/src/smirk-backend/src/infra/prices.rs. * their 24h quote volume.
*
* Routes with no 24h volume are ignored, for the same reason the cexswap
* function ignores them: `last_price` on a market nobody has traded is
* whatever the last person paid, whenever that was, and averaging it against
* a live route drags the published price. WOW-USDT here is the live example —
* a 0.00723 print with null volume against a WOW-BTC route trading at the
* equivalent of 0.00816, which pulled the Nonlogs leg about 6% low and, since
* the leg is half the WOW blend, every product's displayed price with it.
*
* If no route on this venue has volume, the ticker is omitted entirely and
* the cexswap sample stands alone.
*/ */
async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> { async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> {
const out: Record<string, number> = {}; const out: Record<string, number> = {};
@@ -123,23 +136,36 @@ async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> {
cf: { cacheTtl: 30, cacheEverything: true }, cf: { cacheTtl: 30, cacheEverything: true },
}); });
if (!res.ok) throw new Error(`nonlogs http ${res.status}`); if (!res.ok) throw new Error(`nonlogs http ${res.status}`);
const body = (await res.json()) as { markets?: Record<string, { last_price?: string }> }; type NonlogsRow = { last_price?: string | null; quote_volume?: string | null };
const body = (await res.json()) as { markets?: Record<string, NonlogsRow> };
const markets = body.markets ?? {}; const markets = body.markets ?? {};
const num = (v: string | null | undefined): number => {
const n = parseFloat(v ?? "");
return Number.isFinite(n) ? n : 0;
};
for (const ticker of NICHE_TICKERS) { for (const ticker of NICHE_TICKERS) {
const sources: number[] = []; // [usd price, weight in USD] per route.
const btcPair = markets[`${ticker}-BTC`]?.last_price; const samples: Array<[number, number]> = [];
if (btcPair) {
const p = parseFloat(btcPair); const btcRow = markets[`${ticker}-BTC`];
if (p > 0) sources.push(p * btcUsd); const btcPrice = num(btcRow?.last_price);
const btcVol = num(btcRow?.quote_volume); // volume in BTC
if (btcPrice > 0 && btcVol > 0) {
samples.push([btcPrice * btcUsd, btcVol * btcUsd]);
} }
const usdtPair = markets[`${ticker}-USDT`]?.last_price;
if (usdtPair) { const usdtRow = markets[`${ticker}-USDT`];
const p = parseFloat(usdtPair); const usdtPrice = num(usdtRow?.last_price);
if (p > 0) sources.push(p); const usdtVol = num(usdtRow?.quote_volume); // volume in USDT ≈ USD
if (usdtPrice > 0 && usdtVol > 0) {
samples.push([usdtPrice, usdtVol]);
} }
if (sources.length) {
out[ticker] = sources.reduce((a, b) => a + b, 0) / sources.length; if (samples.length) {
const weight = samples.reduce((a, [, w]) => a + w, 0);
out[ticker] = samples.reduce((a, [p, w]) => a + p * w, 0) / weight;
} }
} }
return out; return out;