Add Ticker24h support: hourly market snapshots with USD-normalized volume filtering
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user