Реализация портфолио-трекера в мобильном криптоприложении
Портфолио-трекер — экран, где пользователь видит суммарную стоимость всех активов, их распределение и динамику за период. Кажется простым: суммируй баланс × цена. На практике: мультивалютность, исторические цены для 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 chart нечитаем. Решение: показываем топ-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 недель. Стоимость рассчитывается индивидуально.







