mirror of
https://github.com/Such-Software/hash-wallet-prices.git
synced 2026-10-17 05:51:28 +02:00
fix(prices): price a live book without volume, and refuse a pool nobody trades
On 2026-09-17 this feed marked WOW at $0.00041 while its real book sat at $0.00805, twenty times low, for hours. Two guards did it, each behaving exactly as written against an input its author never saw. nonlogs served WOW-BTC at 10 bid / 11 ask with every 24h counter reading zero. fetchNonlogs requires quote_volume > 0 on every route, so all three WOW routes were discarded and nonlogs vanished from the sources entirely. But a book midpoint and a last trade are not equally trustworthy and should not face the same gate: the midpoint has already proved both sides exist, that the ask is above the bid, and that the spread is inside MAX_BOOK_SPREAD_PCT. Demanding an odometer reading on top of that discards a live market for having a quiet day. A last print has proved none of those things and still needs volume. That left cexswap alone, where the 12-hour staleness gate dropped WOW-XMR at $1,064 of volume for being 18.2h old and kept WOW-DOGE, carrying ONE CENT, for being 3.7h old. One sample, and WOW was priced by a pool nobody trades. Recent and meaningful are different questions, so niche samples now need both: the floor matches the desk's consensus_min_venue_vol_usd, which exists because a $24/day pool once carried 86% of a consensus. MAX_BOOK_SPREAD_PCT drops 60 to 25. Sixty excluded the absurd and admitted the reflexive: nonlogs WOW-USDT read 0.0079/0.0121, a 42% spread with our own ladder on both sides and no counterparty between, and its midpoint dragged the blend to $0.00901. At 25 that route is refused and the mark is $0.00802470 against a book truth of $0.00802470. Verified against the live payloads that produced the fault rather than fixtures; this repo has no test harness, and the data that broke it is the better witness.
This commit is contained in:
+73
-14
@@ -4,8 +4,10 @@
|
||||
* One Cloudflare Worker that:
|
||||
* - Fetches USD spot prices from Kraken for majors (BTC/XMR/LTC/DOGE/ETH/BCH/XNO).
|
||||
* - 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.
|
||||
* A last print still needs volume behind it, because a trade nobody has made
|
||||
* recently is not a price. A live two-sided book inside MAX_BOOK_SPREAD_PCT
|
||||
* does not: it already says where you could trade now. This feed is what
|
||||
* Hash Bags, Smirk and wowlet display.
|
||||
* - Caches results in KV every 60s via cron, then serves from KV on request.
|
||||
*
|
||||
* Endpoints:
|
||||
@@ -102,12 +104,18 @@ async function fetchEcbFiat(): Promise<Record<string, number>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A book wider than this has no meaningful midpoint, so we use the last trade
|
||||
* instead. 60% is deliberately loose: these are thin markets and a 30% spread
|
||||
* is a normal Tuesday, but the 1000%+ books that appear on dead pairs must
|
||||
* never set a price.
|
||||
* Widest bid/ask spread whose midpoint still counts as a price.
|
||||
*
|
||||
* Lowered from 60 on 2026-09-17. Sixty was chosen to exclude the absurd, and it
|
||||
* did: a 1000%-wide book has a meaningless midpoint. But it also admitted a book
|
||||
* we are both sides of. That day nonlogs WOW-USDT read 0.0079 bid / 0.0121 ask,
|
||||
* a 42% spread with our own ladder on both sides and nobody in between, and its
|
||||
* $0.01000 midpoint dragged the blended mark to $0.00901 against a truth of
|
||||
* $0.00805. A spread that wide on these venues means no counterparty stands
|
||||
* between our quotes, which is the desk pricing itself. WOW-BTC at 10/11 sat is
|
||||
* 10% wide and prices correctly; 25 keeps that and refuses the reflection.
|
||||
*/
|
||||
const MAX_BOOK_SPREAD_PCT = 60;
|
||||
const MAX_BOOK_SPREAD_PCT = 25;
|
||||
|
||||
/**
|
||||
* A cexswap `last` print older than this is not evidence of the current price.
|
||||
@@ -116,6 +124,21 @@ const MAX_BOOK_SPREAD_PCT = 60;
|
||||
*/
|
||||
const MAX_LAST_TRADE_AGE_HOURS = 12;
|
||||
|
||||
/**
|
||||
* Weight given to a route priced from a live two-sided book that reports no
|
||||
* volume. It has to be non-zero or the sample divides into nothing and counts
|
||||
* for nothing, and small so a route with real turnover still dominates when one
|
||||
* exists. This is a tie-breaker for a quiet market, not a vote.
|
||||
*/
|
||||
const BOOK_ONLY_WEIGHT_USD = 25;
|
||||
|
||||
/**
|
||||
* 7d USD volume a niche pool must clear before its last trade is evidence of a
|
||||
* price. Matches consensus_min_venue_vol_usd on the desk, which exists because
|
||||
* a $24/day pool once carried 86% of a consensus.
|
||||
*/
|
||||
const MIN_NICHE_VOL_USD = 25;
|
||||
|
||||
async function fetchKraken(): Promise<Record<string, number>> {
|
||||
const pairs = Object.values(KRAKEN_PAIRS).join(",");
|
||||
const url = `https://api.kraken.com/0/public/Ticker?pair=${pairs}`;
|
||||
@@ -201,22 +224,50 @@ async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> {
|
||||
return last;
|
||||
};
|
||||
|
||||
/**
|
||||
* A book midpoint and a last trade are not equally trustworthy, so they must
|
||||
* not face the same gate. The midpoint has already proved both sides exist,
|
||||
* that the ask is above the bid, and that the spread is inside
|
||||
* MAX_BOOK_SPREAD_PCT: it says where you could trade right now, and demanding
|
||||
* an odometer reading on top of that discards a live market for having a
|
||||
* quiet day. A last trade has proved none of those things and still needs
|
||||
* volume behind it.
|
||||
*
|
||||
* On 2026-09-17 nonlogs served WOW-BTC at 10 bid / 11 ask with every 24h
|
||||
* counter reading zero. All three WOW routes failed the volume test, nonlogs
|
||||
* dropped out of the sources entirely, and a one-cent cexswap pool was left
|
||||
* alone to price WOW at $0.00041 against a real book at $0.00805.
|
||||
*/
|
||||
const priced = (row: NonlogsRow | undefined) => {
|
||||
const bid = num(row?.highest_bid);
|
||||
const ask = num(row?.lowest_ask);
|
||||
const mid = bid > 0 && ask > bid ? (bid + ask) / 2 : 0;
|
||||
const fromBook = mid > 0 && ((ask - bid) / mid) * 100 <= MAX_BOOK_SPREAD_PCT;
|
||||
return { price: liveOrLast(row), fromBook };
|
||||
};
|
||||
|
||||
for (const ticker of NICHE_TICKERS) {
|
||||
// [usd price, weight in USD] per route.
|
||||
const samples: Array<[number, number]> = [];
|
||||
|
||||
const btcRow = markets[`${ticker}-BTC`];
|
||||
const btcPrice = liveOrLast(btcRow);
|
||||
const { price: btcPrice, fromBook: btcFromBook } = priced(btcRow);
|
||||
const btcVol = num(btcRow?.quote_volume); // volume in BTC
|
||||
if (btcPrice > 0 && btcVol > 0) {
|
||||
samples.push([btcPrice * btcUsd, btcVol * btcUsd]);
|
||||
if (btcPrice > 0 && (btcVol > 0 || btcFromBook)) {
|
||||
samples.push([
|
||||
btcPrice * btcUsd,
|
||||
Math.max(btcVol * btcUsd, btcFromBook ? BOOK_ONLY_WEIGHT_USD : 0),
|
||||
]);
|
||||
}
|
||||
|
||||
const usdtRow = markets[`${ticker}-USDT`];
|
||||
const usdtPrice = liveOrLast(usdtRow);
|
||||
const { price: usdtPrice, fromBook: usdtFromBook } = priced(usdtRow);
|
||||
const usdtVol = num(usdtRow?.quote_volume); // volume in USDT ≈ USD
|
||||
if (usdtPrice > 0 && usdtVol > 0) {
|
||||
samples.push([usdtPrice, usdtVol]);
|
||||
if (usdtPrice > 0 && (usdtVol > 0 || usdtFromBook)) {
|
||||
samples.push([
|
||||
usdtPrice,
|
||||
Math.max(usdtVol, usdtFromBook ? BOOK_ONLY_WEIGHT_USD : 0),
|
||||
]);
|
||||
}
|
||||
|
||||
if (samples.length) {
|
||||
@@ -265,9 +316,17 @@ async function fetchCexswap(): Promise<Record<string, number>> {
|
||||
// fill, and on 2026-09-06 WOW-XMR's last print was the 24h LOW while the
|
||||
// pool itself had moved 13% above it. An old print is not evidence of the
|
||||
// current price, and this source outweighs every other, so it must expire.
|
||||
//
|
||||
// A volume floor as well, because "recent" and "meaningful" are different
|
||||
// questions. On 2026-09-17 WOW-XMR carried $1,064 of 7d volume and was
|
||||
// 18.2h old, so staleness dropped it; WOW-DOGE carried ONE CENT and was
|
||||
// 3.7h old, so it survived alone and priced WOW at $0.00041 while its real
|
||||
// book sat at $0.00805. A pool nobody trades is not a second opinion. Same
|
||||
// reasoning, and the same number, as the desk's consensus_min_venue_vol_usd.
|
||||
const tradedAt = num(m.last_trade_at_unix as string | undefined);
|
||||
const ageHours = tradedAt > 0 ? (Date.now() / 1000 - tradedAt) / 3600 : Infinity;
|
||||
if (usd > 0 && weight > 0 && ageHours <= MAX_LAST_TRADE_AGE_HOURS) {
|
||||
if (usd > 0 && weight >= MIN_NICHE_VOL_USD
|
||||
&& ageHours <= MAX_LAST_TRADE_AGE_HOURS) {
|
||||
(buckets[base] ??= []).push({ usd, weight });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user