Add Ticker24h support: hourly market snapshots with USD-normalized volume filtering

This commit is contained in:
2026-04-26 18:39:52 -04:00
parent 85fcbe1330
commit 0178b5d29d
45 changed files with 1995 additions and 170 deletions

View File

@@ -462,6 +462,80 @@ export class CCXTFetcher {
return timeframe;
}
/**
* Fetch 24h rolling ticker stats for all symbols on an exchange.
* Uses exchange.fetchTickers() — single API call, very rate-limit efficient.
* Returns an array of TickerStats-compatible objects, or throws if unsupported.
*
* @param {string} exchangeName - lowercase exchange name (e.g. "binance")
* @returns {Promise<Array>} Array of TickerStats objects
*/
async fetchAllTickers(exchangeName) {
const exchange = this.getExchange(exchangeName);
const exchangeUpper = exchangeName.toUpperCase();
if (!exchange.has['fetchTickers']) {
throw new Error(`Exchange ${exchangeUpper} does not support fetchTickers()`);
}
this.logger.info({ exchange: exchangeUpper }, 'Fetching all 24h tickers');
let rawTickers;
try {
rawTickers = await exchange.fetchTickers();
} catch (error) {
if (error.constructor?.name === 'RateLimitExceeded') {
const retryAfterMs = extractRetryAfterMs(exchange, error);
this.logger.warn({ exchange: exchangeUpper, retryAfterMs }, 'fetchTickers rate-limited');
throw new ExchangeRateLimitError(exchangeName, retryAfterMs, error.message);
}
this.logger.error({ error: error.message, exchange: exchangeUpper }, 'Error fetching tickers');
throw error;
}
const nowNs = (BigInt(Date.now()) * 1_000_000n).toString();
const tickers = [];
for (const [symbol, t] of Object.entries(rawTickers)) {
if (!t || t.last == null) continue;
// Build Nautilus-format ticker: "BASE/QUOTE.EXCHANGE"
const ticker = `${symbol}.${exchangeUpper}`;
// Extract base/quote from the CCXT market info
const market = exchange.markets?.[symbol];
const baseAsset = market?.base ?? symbol.split('/')[0] ?? '';
const quoteAsset = market?.quote ?? symbol.split('/')[1] ?? '';
// protobufjs camelCase: only removes '_' before LETTERS, not digits.
// quote_volume_24h → quoteVolume_24h (underscore before '24' is preserved)
// open_24h → open_24h, high_24h → high_24h, etc.
const stat = {
ticker,
exchangeId: exchangeUpper,
baseAsset,
quoteAsset,
lastPrice: t.last ?? 0,
priceChangePct: t.percentage ?? 0,
'quoteVolume_24h': t.quoteVolume ?? 0,
timestamp: nowNs,
};
if (t.bid != null) stat.bidPrice = t.bid;
if (t.ask != null) stat.askPrice = t.ask;
if (t.open != null) stat['open_24h'] = t.open;
if (t.high != null) stat['high_24h'] = t.high;
if (t.low != null) stat['low_24h'] = t.low;
if (t.baseVolume != null) stat['volume_24h'] = t.baseVolume;
if (t.info?.count != null) stat.numTrades = Number(t.info.count);
tickers.push(stat);
}
this.logger.info({ exchange: exchangeUpper, count: tickers.length }, 'Fetched all tickers');
return tickers;
}
/**
* Close all exchange connections
*/