Розробка системи мониторингу курсів криптовалют
"Просто дергаємо CoinGecko API раз на хвилину" — цей підхід працює ровно до першого rate limit або до того моменту, коли продукту потрібні дані швидше ніж раз на 60 секунд. Система мониторингу курсів — це не один API запит, це pipeline: джерело даних → нормалізація → зберігання → доставка клієнтам → алерти. Кожен шар має свої вимоги до надійності.
Джерела даних: що використовувати і коли
CEX через WebSocket (найнижча затримка)
Для realtime курсів — напрямку до біржевих WebSocket стримів. Жодної агрегації, мінімальна затримка:
// Binance WebSocket — tick дані в реальному часі
const ws = new WebSocket('wss://stream.binance.com:9443/stream?streams=btcusdt@ticker/ethusdt@ticker')
ws.on('message', (data) => {
const { stream, data: tick } = JSON.parse(data)
const symbol = tick.s // BTCUSDT
const price = parseFloat(tick.c) // останній курс
const volume24h = parseFloat(tick.v)
const priceChange24h = parseFloat(tick.P) // відсоток
normalizeAndStore({ source: 'binance', symbol, price, volume24h, timestamp: Date.now() })
})
ws.on('close', () => {
// Exponential backoff reconnect — не опціональний
setTimeout(connectBinance, Math.min(reconnectDelay * 2, 30000))
})
Аналогічні стрими є у Coinbase (wss://advanced-trade-ws.coinbase.com/), OKX, Bybit. Для ліквідних пар — мінімум три джерела, медіанна ціна як захист від outliers.
DEX on-chain ціни
Для DeFi додатків ціни часто потрібні саме on-chain. Два підходи:
Uniswap V3 TWAP — Time-Weighted Average Price за останні N блоків. Стійкий до маніпуляцій (для flash loan атак потрібно утримувати позицію кілька блоків):
// На контракті — приблизно як працює читання TWAP
IUniswapV3Pool pool = IUniswapV3Pool(WETH_USDC_POOL);
(int56[] memory tickCumulatives,) = pool.observe([600, 0]); // 600 секунд назад і тепер
int56 tickDiff = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDiff / 600);
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(avgTick);
// Перевід sqrtPriceX96 в читаний людиною формат ціни...
Chainlink Price Feeds — для production смарт-контрактів це стандарт. Decentralized oracle network, агрегує дані від множини джерел:
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); // ETH/USD
(, int256 price, , uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "Price feed stale"); // перевірка свіжості
CoinGecko / CoinMarketCap (агрегована)
Для історичних даних і менш ліквідних токенів. Rate limits: CoinGecko free — 30 запитів/хв, paid API — до 500. Кешуємо агресивно.
Архітектура системи
Зберігання: TimescaleDB або InfluxDB
Курси — типовий time-series use case. PostgreSQL з розширенням TimescaleDB — хороший вибір якщо вже використовується PG:
CREATE TABLE price_ticks (
time TIMESTAMPTZ NOT NULL,
symbol VARCHAR(20) NOT NULL,
source VARCHAR(20) NOT NULL,
price NUMERIC(20, 8) NOT NULL,
volume_24h NUMERIC(30, 2),
bid NUMERIC(20, 8),
ask NUMERIC(20, 8)
);
SELECT create_hypertable('price_ticks', 'time');
-- Автоматична агрегація в OHLCV свічки
SELECT
time_bucket('1 minute', time) AS bucket,
symbol,
first(price, time) AS open,
max(price) AS high,
min(price) AS low,
last(price, time) AS close,
sum(volume_24h) AS volume
FROM price_ticks
WHERE time > NOW() - INTERVAL '1 hour'
GROUP BY bucket, symbol
ORDER BY bucket DESC;
TimescaleDB автоматично партиціонує дані за часом і надає функції time_bucket, first, last. Retention policy — автоудалення тиків старше 30 днів, зберігати тільки агрегати (свічки 1m/5m/1h/1d) для історії.
Алерти
Два типи алертів:
Price change alert — ціна змінилась на X% за Y хвилин:
class PriceAlertService {
constructor(redis) {
this.redis = redis
this.subscribers = new Map()
}
async checkAlert(symbol, currentPrice) {
const priceKey = `price:${symbol}:1m_ago`
const pastPrice = await this.redis.get(priceKey)
if (pastPrice) {
const changePercent = Math.abs((currentPrice - pastPrice) / pastPrice * 100)
if (changePercent >= ALERT_THRESHOLD_PERCENT) {
await this.triggerAlert({
symbol,
currentPrice,
pastPrice: parseFloat(pastPrice),
changePercent,
direction: currentPrice > pastPrice ? 'up' : 'down'
})
}
}
// Зберігаємо поточну ціну для наступної перевірки
await this.redis.setex(priceKey, 60, currentPrice.toString())
}
async triggerAlert(alertData) {
// Відправка в Telegram, Slack, email — залежить від конфігу
for (const [webhookUrl, config] of this.subscribers) {
if (config.symbols.includes(alertData.symbol)) {
fetch(webhookUrl, { method: 'POST', body: JSON.stringify(alertData) })
}
}
}
}
Stale data alert — якщо дані перестали оновлюватись (джерело впало):
setInterval(() => {
for (const [symbol, lastUpdate] of lastUpdates) {
if (Date.now() - lastUpdate > STALE_THRESHOLD_MS) {
sendAlert(`Data for ${symbol} stale for ${Math.round((Date.now() - lastUpdate)/1000)}s`)
}
}
}, 10000)
Доставка клієнтам
WebSocket API для фронтенду:
// Server-sent events або WebSocket — SSE простіше для read-only стрима
app.get('/stream/prices', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
const symbols = req.query.symbols?.split(',') || ['BTCUSDT', 'ETHUSDT']
const interval = setInterval(async () => {
const prices = await getPricesFromCache(symbols)
res.write(`data: ${JSON.stringify(prices)}\n\n`)
}, 1000)
req.on('close', () => clearInterval(interval))
})
Redis pub/sub між колектором даних і API шаром — щоб не навантажувати БД на кожен запит клієнта.
Що входить в роботу
- Підключення до джерел даних (біржові WebSocket, Chainlink, агрегатори)
- Нормалізація даних і TimescaleDB/InfluxDB зберігання
- REST і WebSocket API для клієнтів
- Система алертів з настроюваними умовами
- Моніторинг доступності джерел і автопереключення







