Разработка портфельного трекера криптовалют
Простой портфельный трекер — это агрегатор данных. Сложный — это система реального времени, которая сводит on-chain балансы из десятков сетей, позиции на централизованных биржах, NFT, DeFi позиции и считает всё это в единой базовой валюте. Разница в сложности примерно в 10x.
Источники данных
On-chain балансы
EVM-сети: нативный баланс через eth_getBalance, ERC-20 балансы сложнее — один RPC вызов возвращает баланс одного токена на одном адресе. При 50 токенах × 5 сетей = 250 вызовов. Решение — Multicall3 (задеплоен на всех EVM-сетях по адресу 0xcA11bde05977b3631167028862bE2a173976CA11):
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const ERC20_ABI = parseAbi(["function balanceOf(address) view returns (uint256)"]);
async function getTokenBalances(
walletAddress: `0x${string}`,
tokenAddresses: `0x${string}`[]
) {
const calls = tokenAddresses.map((tokenAddress) => ({
address: tokenAddress,
abi: ERC20_ABI,
functionName: "balanceOf" as const,
args: [walletAddress],
}));
return client.multicall({ contracts: calls });
}
Один RPC вызов вместо 50 — критично для rate limits и latency.
Альтернатива: Alchemy/Moralis Token API. Один запрос возвращает все ERC-20 балансы для адреса с метаданными (символ, decimals, USD стоимость). Платно, но экономит время разработки.
DeFi позиции
Это самая сложная часть трекера. Liquidity позиция в Uniswap V3, collateral в Aave, стейкнутые токены в Curve — каждый протокол хранит данные по-своему.
Подход 1: прямые вызовы к контрактам протоколов. Для Uniswap V3:
// Позиция по NFT ID
const position = await nftPositionManager.positions(tokenId);
// { token0, token1, fee, tickLower, tickUpper, liquidity, ... }
// Пересчёт в токены через математику тиков (сложно)
// Лучше через Uniswap V3 SDK
import { Pool, Position } from "@uniswap/v3-sdk";
Подход 2: The Graph subgraphs. Большинство DeFi протоколов имеют официальные subgraph — GraphQL API поверх on-chain данных:
query GetAavePositions($address: String!) {
user(id: $address) {
reserves {
reserve {
symbol
underlyingAsset
}
currentATokenBalance
currentVariableDebt
currentStableDebt
}
}
}
Подход 3: агрегаторы позиций — DeBank API, Zapper API, Zerion API. Они уже интегрированы с сотнями протоколов. Имеют смысл если не хотите поддерживать каждую DeFi интеграцию самостоятельно.
CEX балансы
Биржевые API отдают балансы мгновенно, но требуют read-only API ключ от пользователя:
import ccxt from "ccxt";
async function getBinanceBalances(apiKey: string, secret: string) {
const exchange = new ccxt.binance({ apiKey, secret, sandbox: false });
const balance = await exchange.fetchBalance();
return balance.total; // { BTC: 0.5, ETH: 2.3, USDT: 1000 }
}
CCXT поддерживает 100+ бирж с единым интерфейсом — незаменимая библиотека для трекеров.
Ценовые данные
Для пересчёта в USD нужны актуальные цены. Источники:
CoinGecko API — бесплатный план 30 запросов/мин, 10k/мес. Для небольших трекеров достаточно. Pro план снимает лимиты.
async function getPrices(coinIds: string[]): Promise<Record<string, number>> {
const ids = coinIds.join(",");
const data = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`
).then(r => r.json());
return Object.fromEntries(
Object.entries(data).map(([id, val]: [string, any]) => [id, val.usd])
);
}
On-chain цены (Chainlink): для реального времени без зависимости от CoinGecko. Chainlink Price Feeds доступны на всех крупных EVM-сетях.
Архитектура refresh pipeline
Данные нужно обновлять, но не молотить RPC каждую секунду. Стратегия:
| Тип данных | Частота обновления | Причина |
|---|---|---|
| On-chain балансы | Каждые 30–60 сек | Новый блок |
| CEX балансы | Каждые 30 сек | API лимиты |
| DeFi позиции | Каждые 2–5 мин | Медленно меняются |
| Цены токенов | Каждые 10–30 сек | Критично для P&L |
| Historical P&L | Фоновый job, 1/час | Тяжёлые вычисления |
Background jobs через Redis + BullMQ с отдельными очередями для разных типов данных. Результаты кешируем в Redis, фронтенд читает из кеша.
WebSocket для real-time
Для пользователей, открывших трекер в браузере — Server-Sent Events или WebSocket с push-обновлениями вместо polling:
// Server-Sent Events endpoint
app.get("/api/portfolio/stream", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
const interval = setInterval(async () => {
const portfolio = await getPortfolioSnapshot(req.user.id);
res.write(`data: ${JSON.stringify(portfolio)}\n\n`);
}, 10000);
req.on("close", () => clearInterval(interval));
});
Хранение исторических данных
Для P&L трекинга нужны снапшоты портфеля во времени. TimescaleDB (расширение PostgreSQL) идеально подходит:
CREATE TABLE portfolio_snapshots (
user_id UUID,
snapshot_at TIMESTAMPTZ NOT NULL,
total_usd NUMERIC(20, 2),
breakdown JSONB -- { "ETH": { "amount": 2.3, "usd": 7820 }, ... }
);
SELECT create_hypertable('portfolio_snapshots', 'snapshot_at');
Запрос P&L за период:
SELECT
time_bucket('1 day', snapshot_at) AS day,
last(total_usd, snapshot_at) AS end_of_day_value
FROM portfolio_snapshots
WHERE user_id = $1 AND snapshot_at > NOW() - INTERVAL '30 days'
GROUP BY day ORDER BY day;
Срок разработки MVP с поддержкой 5–6 EVM сетей, основными DeFi протоколами и 3–4 биржами: 1–2 недели.







