Реалізація портфолио-трекера у мобільному крипто-приложенні
Портфоліо-трекер — екран, де користувач видить суммарну вартість всіх активів, їх розподіл та динаміку за період. Виглядає простим: суммуй баланс × ціна. На практиці: мультивалютність, історичні ціни для P&L, агрегація даних з різних бирж та кошельків, оновлення в реальному часі без деградації батареї.
Джерела даних
Три типи джерел даних для портфоліо:
Ручний ввід — користувач сам додає активи. Просто, працює без ключів API, але застаріває відразу після додавання.
API біржи — через read-only API ключи. Binance, OKX, Bybit, Coinbase мають /account/balances або аналог. Важливо: приймати тільки read-only ключі, явно блокувати можливість торговлі. На UI — попередження, що ключ повинен бути без прав на withdrawal.
On-chain дані — публічні адреси кошельків, балансі читаються через RPC. Для EVM: eth_getBalance для нативної валюти, balanceOf(address) через eth_call для ERC-20 токенів. Для кількох токенів одночасно — Multicall3:
// Multicall3 — один запит для отримання балансів 50+ токенів
final multicall = DeployedContract(
ContractAbi.fromJson(multicall3Abi, 'Multicall3'),
EthereumAddress.fromHex('0xcA11bde05977b3631167028862bE2a173976CA11'),
);
final calls = tokenAddresses.map((token) => [
token, // target
false, // allowFailure
balanceOfCalldata(walletAddr), // callData: balanceOf(address)
]).toList();
final result = await ethClient.call(
contract: multicall,
function: multicall.function('aggregate3'),
params: [calls],
);
Ціни в реальному часі
CoinGecko /coins/markets?ids=bitcoin,ethereum,... — безплатний лімит 30 req/min для Demo API, достатньо для polling кожні 30 секунд. Для real-time без polling — WebSocket: Binance wss://stream.binance.com/ws/!miniTicker@arr дає оновлення цін за всіма парами.
Ключове питання: оновлювати ціни всіх активів одночасно або підписуватися на конкретні тикери. Polling за розписанням — простіше та передбачувано за нагрузкою на батарею. WebSocket — швидше, але потрібно управляти перез'єднанням.
На Flutter: Timer.periodic для polling, web_socket_channel для WebSocket. Для фонових оновлень — WorkManager (Android) / BGAppRefreshTask (iOS), але iOS строго обмежує частоту фонових оновлень.
Розрахунок P&L
Суммарний P&L = (поточна вартість портфеля) - (вартість при покупці). Для ручного вводу — користувач вводить середню ціну покупки. Для біржевого API — рахуємо з історії сделок за FIFO:
class PnLCalculator {
// Черга покупок (FIFO) для розрахунку cost basis
final _buyQueue = Queue<({double price, double quantity})>();
double _totalCost = 0;
double _totalQuantity = 0;
void addBuy(double price, double quantity) {
_buyQueue.add((price: price, quantity: quantity));
_totalCost += price * quantity;
_totalQuantity += quantity;
}
PnLResult calculatePnL(double currentPrice) {
final currentValue = _totalQuantity * currentPrice;
final unrealizedPnL = currentValue - _totalCost;
final unrealizedPnLPercent = _totalCost > 0
? (unrealizedPnL / _totalCost) * 100
: 0.0;
return PnLResult(
unrealizedPnL: unrealizedPnL,
unrealizedPnLPercent: unrealizedPnLPercent,
avgEntryPrice: _totalCost / _totalQuantity,
);
}
}
Візуалізація розподілу активів
Pie chart / Donut chart з процентами за кожним активом. Проблема: 50 активів роблять pie нечитаним. Рішення: показуємо топ-5 за долею, решту — «Інші». При тапу на сегмент — drill-down у список активів цієї категорії.
На Flutter fl_chart PieChart з PieChartSectionData:
List<PieChartSectionData> buildSections(List<Asset> assets, double totalValue) {
final sorted = [...assets]..sort((a, b) => b.currentValue.compareTo(a.currentValue));
final top5 = sorted.take(5).toList();
final othersValue = sorted.skip(5).fold(0.0, (sum, a) => sum + a.currentValue);
final sections = top5.map((asset) => PieChartSectionData(
value: asset.currentValue / totalValue * 100,
title: '${(asset.currentValue / totalValue * 100).toStringAsFixed(1)}%',
color: _colorForAsset(asset.symbol),
radius: 60,
)).toList();
if (othersValue > 0) {
sections.add(PieChartSectionData(
value: othersValue / totalValue * 100,
title: 'Інші',
color: Colors.grey,
radius: 60,
));
}
return sections;
}
Історичний графік вартості портфеля
Графік вартості за 24h / 7d / 30d / 1y. Для цього потрібні історичні ціни всіх активів у кожен момент часу — дорогий запит. CoinGecko /coins/{id}/market_chart?vs_currency=usd&days=30 — для кожного активу окремо.
Оптимізація: не запитувати історичні дані при кожному відкритті. Кешувати з TTL 1 година, оновлювати при зміні набору активів або за явним pull-to-refresh.
Мультивалютність
Користувачі очікують бачити вартість у своїй фіатній валюті (USD, EUR, RUB). CoinGecko підтримує vs_currencies параметр. Курс фіат/фіат для конвертації (USD → RUB) — через окремий currency API або Forex feed.
Зберігаємо переважну валюту користувача в SharedPreferences / UserDefaults, при зміні — пересчитуємо без повторного запиту до CoinGecko (просто множимо на курс).
Що входить у роботу
- Ручний ввід активів + додавання бирж через API ключі
- On-chain мониторинг через Multicall (для EVM-кошельків)
- Реальне час ціни (polling або WebSocket)
- Розрахунок P&L (unrealized, realized — залежно від вимог)
- Donut chart розподілу активів
- Історичний LineChart з таймфреймами
- Мультивалютність (USD/EUR/інші)
Строки
MVP з ручним вводом, цінами та pie chart: 2–3 тижні. Повнофункціональний трекер з біржевим API, on-chain мониторингом, історичним графіком та P&L: 6–10 тижнів. Вартість розраховується індивідуально після аналізу вимог.







