Crypto Volume Screener Development
A volume screener is a tool for finding instruments with anomalous trading activity. Sharp volume spikes often precede major price movements. Professional traders use volume screeners to discover trading opportunities before moves become obvious.
Volume Metrics
Volume Ratio: current volume / average volume over N periods. Ratio > 3 indicates anomalous volume.
Relative Volume (RVOL): current candle volume relative to the average volume for that time of day. RVOL > 2 at 14:00 means: today at 14:00 the volume is 2x higher than usual for that time.
Volume Spike: a one-off volume surge in a single candle — often signals a large player.
OBV (On Balance Volume): cumulative indicator. Accumulation = smart money entering. Distribution = smart money exiting.
Screener Architecture
interface VolumeScreenerItem {
symbol: string;
exchange: string;
currentVolume: number;
avgVolume20: number; // average over 20 periods
volumeRatio: number; // current / avg20
rvol: number; // relative to same-hour average
volumeDelta: number; // buy volume - sell volume (if available)
volumeTrend: 'increasing' | 'decreasing' | 'spike';
priceChange: number; // % over period
volumePrice: 'confirming' | 'diverging'; // does volume confirm price move?
}
Volume Metrics Calculation
function calculateVolumeMetrics(
candles: OHLCV[],
currentCandle: OHLCV
): VolumeMetrics {
const period = 20;
const recentCandles = candles.slice(-period);
// Average volume
const avgVolume = recentCandles.reduce((sum, c) => sum + c.volume, 0) / period;
// Volume Ratio
const volumeRatio = currentCandle.volume / avgVolume;
// Volume trend: linear regression over last 5 candles
const recentVolumes = candles.slice(-5).map(c => c.volume);
const volumeTrendSlope = linearRegressionSlope(recentVolumes);
// Confirmation: price up + volume up = bullish confirmation
const priceChange = (currentCandle.close - candles.slice(-2)[0].close) / candles.slice(-2)[0].close;
const volumeChange = currentCandle.volume / candles.slice(-2)[0].volume - 1;
const confirming = (priceChange > 0 && volumeChange > 0) || (priceChange < 0 && volumeChange > 0);
return {
avgVolume,
volumeRatio,
volumeTrend: volumeTrendSlope > 0.1 ? 'increasing' : volumeTrendSlope < -0.1 ? 'decreasing' :
(volumeRatio > 3 ? 'spike' : 'normal'),
volumePrice: confirming ? 'confirming' : 'diverging',
};
}
function linearRegressionSlope(values: number[]): number {
const n = values.length;
const xMean = (n - 1) / 2;
const yMean = values.reduce((a, b) => a + b) / n;
let numerator = 0;
let denominator = 0;
for (let i = 0; i < n; i++) {
numerator += (i - xMean) * (values[i] - yMean);
denominator += (i - xMean) ** 2;
}
return denominator ? numerator / denominator : 0;
}
Data Collection for Multiple Pairs
class VolumeDataCollector {
private candleCache = new Map<string, OHLCV[]>();
private exchange: ccxt.Exchange;
async fetchAllCandles(symbols: string[], timeframe: string): Promise<void> {
// Parallel requests respecting rate limits
const chunks = chunkArray(symbols, 10); // 10 requests in parallel
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (symbol) => {
const candles = await this.exchange.fetchOHLCV(symbol, timeframe, undefined, 100);
this.candleCache.set(`${symbol}:${timeframe}`, candles.map(formatCandle));
})
);
await sleep(100); // small pause between chunks
}
}
async getScreenerData(timeframe: string, minVolumeRatio: number = 2): Promise<VolumeScreenerItem[]> {
const results: VolumeScreenerItem[] = [];
for (const [key, candles] of this.candleCache) {
if (!key.endsWith(`:${timeframe}`)) continue;
const symbol = key.split(':')[0];
if (candles.length < 21) continue;
const metrics = calculateVolumeMetrics(candles.slice(0, -1), candles[candles.length - 1]);
if (metrics.volumeRatio >= minVolumeRatio) {
results.push({
symbol,
currentVolume: candles[candles.length - 1].volume,
...metrics,
});
}
}
return results.sort((a, b) => b.volumeRatio - a.volumeRatio);
}
}
Screener UI
function VolumeScreener() {
const [timeframe, setTimeframe] = useState('1h');
const [minRatio, setMinRatio] = useState(2);
const [sortBy, setSortBy] = useState<'volumeRatio' | 'currentVolume'>('volumeRatio');
const [data, setData] = useState<VolumeScreenerItem[]>([]);
// Update every 5 minutes
useEffect(() => {
const interval = setInterval(() => refreshScreener(), 5 * 60 * 1000);
return () => clearInterval(interval);
}, [timeframe, minRatio]);
return (
<div>
<Controls
timeframe={timeframe} onTimeframeChange={setTimeframe}
minRatio={minRatio} onMinRatioChange={setMinRatio}
/>
<table>
<thead>
<tr>
<th>Symbol</th>
<th onClick={() => setSortBy('volumeRatio')}>Vol Ratio ↕</th>
<th>RVOL</th>
<th>Volume ($)</th>
<th>Price Change</th>
<th>Trend</th>
<th>Confirmation</th>
</tr>
</thead>
<tbody>
{data.map(item => (
<VolumeRow key={item.symbol} item={item} />
))}
</tbody>
</table>
</div>
);
}
const VolumeRow: React.FC<{ item: VolumeScreenerItem }> = ({ item }) => (
<tr className={item.volumeRatio > 5 ? 'highlight-spike' : ''}>
<td><a href={`/trade/${item.symbol}`}>{item.symbol}</a></td>
<td>
<VolumeRatioBar ratio={item.volumeRatio} />
<span>{item.volumeRatio.toFixed(1)}x</span>
</td>
<td>{item.rvol.toFixed(1)}x</td>
<td>{formatVolume(item.currentVolume)}</td>
<td className={item.priceChange > 0 ? 'green' : 'red'}>
{item.priceChange > 0 ? '+' : ''}{item.priceChange.toFixed(2)}%
</td>
<td><TrendIcon trend={item.volumeTrend} /></td>
<td>
<span className={item.volumePrice === 'confirming' ? 'green' : 'yellow'}>
{item.volumePrice === 'confirming' ? '✓ Confirm' : '⚡ Diverge'}
</span>
</td>
</tr>
);
Alerts
interface VolumeAlert {
symbol: string;
minVolumeRatio: number; // trigger when reached
notifyVia: 'telegram' | 'webhook' | 'email';
}
async function checkVolumeAlerts(screenerData: VolumeScreenerItem[], alerts: VolumeAlert[]) {
for (const alert of alerts) {
const item = screenerData.find(d => d.symbol === alert.symbol);
if (!item) continue;
if (item.volumeRatio >= alert.minVolumeRatio) {
await sendAlert(alert.notifyVia, {
message: `Volume spike on ${item.symbol}! Ratio: ${item.volumeRatio.toFixed(1)}x avg | Price: ${item.priceChange > 0 ? '+' : ''}${item.priceChange.toFixed(2)}%`,
});
}
}
}
Development of a volume screener with multi-timeframe support, alerts, and multi-exchange coverage: 4–6 weeks.







