'use strict'
/**
* Treemap data provider — batch RSI + momentum for all traded symbols.
* Combines ticker24hr (price, change%, volume) with RSI-14 (5m).
* Caches results 30s so the endpoint is cheap even with 200+ symbols.
*/
const { createLogger } = require('./logger')
const log = createLogger('treemap')
let _getProxyCached = null
let _binanceFapi = null
// Cache
let _cache = null // { ts, data[] }
const CACHE_TTL = 30_000 // 30s
// RSI settings
const RSI_PERIOD = 14
const RSI_TF = '5m'
const RSI_KLINE_LIMIT = RSI_PERIOD + 2
// Concurrency control — don't hammer Binance
const MAX_CONCURRENT = 8
function init({ getProxyCached, BINANCE_FAPI }) {
_getProxyCached = getProxyCached
_binanceFapi = BINANCE_FAPI
log.info('Treemap provider initialized')
}
/**
* Compute RSI for a single symbol (same algo as alerts.js)
*/
async function computeRSI(symbol) {
try {
const ctrl = new AbortController()
const tid = setTimeout(() => ctrl.abort(), 10_000)
let klines
try {
const res = await fetch(`${_binanceFapi}/fapi/v1/klines?symbol=${symbol}&interval=${RSI_TF}&limit=${RSI_KLINE_LIMIT}`, { signal: ctrl.signal })
if (!res.ok) return null
klines = await res.json()
} finally { clearTimeout(tid) }
if (!Array.isArray(klines) || klines.length < RSI_PERIOD + 1) return null
const closes = klines.map(k => parseFloat(k[4]))
let gains = 0, losses = 0
for (let i = 1; i <= RSI_PERIOD; i++) {
const diff = closes[i] - closes[i - 1]
if (diff > 0) gains += diff
else losses -= diff
}
let avgGain = gains / RSI_PERIOD
let avgLoss = losses / RSI_PERIOD
for (let i = RSI_PERIOD + 1; i < closes.length; i++) {
const diff = closes[i] - closes[i - 1]
avgGain = (avgGain * (RSI_PERIOD - 1) + (diff > 0 ? diff : 0)) / RSI_PERIOD
avgLoss = (avgLoss * (RSI_PERIOD - 1) + (diff < 0 ? -diff : 0)) / RSI_PERIOD
}
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss
return 100 - (100 / (1 + rs))
} catch {
return null
}
}
/**
* Batch-fetch RSI for symbols with concurrency limit
*/
async function batchRSI(symbols) {
const results = new Map()
const queue = [...symbols]
async function worker() {
while (queue.length > 0) {
const sym = queue.shift()
const rsi = await computeRSI(sym)
results.set(sym, rsi)
}
}
const workers = Array.from({ length: MAX_CONCURRENT }, () => worker())
await Promise.all(workers)
return results
}
/**
* Get treemap data — ticker + RSI merged. Cached 30s.
* Returns: [{ symbol, price, changePct, volume, rsi, sector }]
*/
async function getData() {
// Return cache if fresh
if (_cache && Date.now() - _cache.ts < CACHE_TTL) {
return _cache.data
}
try {
// 1. Get ticker24hr for all symbols (cache → Bottleneck, maxConcurrent=50 supports weight=40)
let tickers = _getProxyCached('ticker24hr', 60_000)
if (!Array.isArray(tickers) || tickers.length === 0) {
try {
const ctrl = new AbortController()
const tid = setTimeout(() => ctrl.abort(), 15_000)
try {
const res = await fetch(`${_binanceFapi}/fapi/v1/ticker/24hr`, { signal: ctrl.signal })
if (res.ok) tickers = await res.json()
} finally { clearTimeout(tid) }
} catch (e) {
log.warn({ err: e.message }, 'Failed to fetch ticker24hr for treemap')
}
}
if (!Array.isArray(tickers) || tickers.length === 0) {
log.warn('No ticker data for treemap')
return _cache ? _cache.data : []
}
// Filter USDT perpetuals, sort by volume, take top 100
const usdtTickers = tickers
.filter(t => t.symbol.endsWith('USDT') && parseFloat(t.quoteVolume) > 0)
.sort((a, b) => parseFloat(b.quoteVolume) - parseFloat(a.quoteVolume))
.slice(0, 100)
// 2. Batch compute RSI for top 100
const symbols = usdtTickers.map(t => t.symbol)
const rsiMap = await batchRSI(symbols)
// 3. Merge into treemap items
const data = usdtTickers.map(t => {
const rsi = rsiMap.get(t.symbol)
const changePct = parseFloat(t.priceChangePercent) || 0
const volume = parseFloat(t.quoteVolume) || 0
const price = parseFloat(t.lastPrice) || 0
return {
symbol: t.symbol.replace('USDT', ''),
pair: t.symbol,
price,
changePct: Math.round(changePct * 100) / 100,
volume: Math.round(volume),
rsi: rsi !== null ? Math.round(rsi * 10) / 10 : null,
// Classify sector by volume rank
tier: volume > 1e9 ? 'mega' : volume > 2e8 ? 'large' : volume > 5e7 ? 'mid' : 'small'
}
})
_cache = { ts: Date.now(), data }
log.info({ count: data.length, withRSI: data.filter(d => d.rsi !== null).length }, 'Treemap data refreshed')
return data
} catch (err) {
log.error({ err: err.message }, 'Treemap getData failed')
return _cache ? _cache.data : []
}
}
function getStats() {
return {
cached: _cache ? true : false,
cacheAge: _cache ? Math.round((Date.now() - _cache.ts) / 1000) : null,
symbols: _cache ? _cache.data.length : 0,
withRSI: _cache ? _cache.data.filter(d => d.rsi !== null).length : 0
}
}
module.exports = { init, getData, getStats }
📜 Git History
fc64796fix: treemap bypass Bottleneck — direct fetch for RSI klines + ticker24hr8 weeks ago
59cbf69fix: eliminate crash loop + 29x faster page load8 weeks ago
22987cbfeat: RSI/Momentum Treemap tab (Step 9 — market visualization)8 weeks ago
Show last diff
Loading...