Розроблення скринера по обсягам криптовалют
Скринер по обсягам — це інструмент для пошуку інструментів з аномальною торговельною активністю. Різкий ріст обсягу часто передує великому руху ціни. Професійні трейдери використовують 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);
// Підтвердження: ціна вгору + обсяг вгору = бичаче підтвердження
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);
}
}
Інтерфейс скринера
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 тижнів.







