Разработка скринера по объёмам
Скринер по объёмам — это инструмент для поиска инструментов с аномальной торговой активностью. Резкий рост объёма часто предшествует крупному движению цены. Профессиональные трейдеры используют volume screener для поиска торговых возможностей до того, как движение стало очевидным.
Метрики объёма
Volume Ratio: текущий объём / средний объём за N периодов. Ratio > 3 — аномальный объём.
Relative Volume (RVOL): объём текущей свечи относительно среднего объёма для этого времени суток. RVOL > 2 в 14:00 значит: сегодня в 14:00 объём в 2× больше чем обычно в это время.
Volume Spike: разовый всплеск объёма в одной свече — часто сигнал крупного участника.
OBV (On Balance Volume): накопительный индикатор. Накопление = умные деньги входят. Распределение = умные деньги выходят.
Архитектура скринера
interface VolumeScreenerItem {
symbol: string;
exchange: string;
currentVolume: number;
avgVolume20: number; // средний за 20 периодов
volumeRatio: number; // current / avg20
rvol: number; // relative to same-hour average
volumeDelta: number; // объём buy - sell (если есть данные)
volumeTrend: 'increasing' | 'decreasing' | 'spike';
priceChange: number; // % за период
volumePrice: 'confirming' | 'diverging'; // объём подтверждает ценовое движение?
}
Расчёт объёмных метрик
function calculateVolumeMetrics(
candles: OHLCV[],
currentCandle: OHLCV
): VolumeMetrics {
const period = 20;
const recentCandles = candles.slice(-period);
// Средний объём
const avgVolume = recentCandles.reduce((sum, c) => sum + c.volume, 0) / period;
// Volume Ratio
const volumeRatio = currentCandle.volume / avgVolume;
// Тренд объёма: линейная регрессия за последние 5 свечей
const recentVolumes = candles.slice(-5).map(c => c.volume);
const volumeTrendSlope = linearRegressionSlope(recentVolumes);
// Подтверждение: цена вверх + объём вверх = 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;
}
Сбор данных для множества пар
class VolumeDataCollector {
private candleCache = new Map<string, OHLCV[]>();
private exchange: ccxt.Exchange;
async fetchAllCandles(symbols: string[], timeframe: string): Promise<void> {
// Параллельный запрос, соблюдая rate limits
const chunks = chunkArray(symbols, 10); // по 10 запросов параллельно
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); // небольшая пауза между чанками
}
}
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);
}
}
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[]>([]);
// Обновляем каждые 5 минут
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>
);
Алерты
interface VolumeAlert {
symbol: string;
minVolumeRatio: number; // триггер при достижении
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)}%`,
});
}
}
}
Разработка volume screener с поддержкой нескольких таймфреймов, алертами и мультибиржевым охватом: 4–6 недель.







