Реализация PnL-калькулятора (прибыль/убыток) в мобильном криптоприложении
P&L в крипте — не просто (цена продажи - цена покупки) × количество. Несколько покупок по разным ценам, комиссии в разных токенах, реализованный и нереализованный P&L, налоговые методы учёта (FIFO vs LIFO vs средняя стоимость) — каждый из этих факторов меняет итоговую цифру. Ошибка в расчёте обнаруживается при подаче налоговой декларации.
Модели расчёта P&L
Три метода учёта, которые дают разный результат для одинакового набора сделок:
FIFO (First In, First Out) — продаём сначала самые ранние купленные единицы. Стандарт для большинства юрисдикций.
LIFO (Last In, First Out) — продаём последние купленные. Выгоднее при растущем рынке (продаём дорого купленные недавно).
Average Cost (средняя стоимость) — делим общую стоимость позиции на количество. Проще для понимания, разрешён в ряде стран.
Пример с реальными числами — разница наглядна:
- Покупка 1: 1 BTC по $20,000
- Покупка 2: 1 BTC по $30,000
- Продажа: 1 BTC по $35,000
| Метод | Себестоимость | P&L |
|---|---|---|
| FIFO | $20,000 | +$15,000 |
| LIFO | $30,000 | +$5,000 |
| Average Cost | $25,000 | +$10,000 |
Реализуем все три, с переключением в настройках:
abstract class PnLMethod {
PnLResult calculate(List<Trade> buys, List<Trade> sells);
}
class FifoMethod implements PnLMethod {
@override
PnLResult calculate(List<Trade> buys, List<Trade> sells) {
final buyQueue = Queue<({double price, double qty, DateTime date})>();
for (final buy in buys) {
buyQueue.add((price: buy.price, qty: buy.quantity, date: buy.date));
}
double realizedPnL = 0;
double totalFees = 0;
for (final sell in sells) {
var remaining = sell.quantity;
totalFees += sell.feeInBase; // приводим комиссию к базовому активу
while (remaining > 0 && buyQueue.isNotEmpty) {
final buy = buyQueue.first;
final matched = min(remaining, buy.qty);
realizedPnL += matched * (sell.price - buy.price);
if (matched >= buy.qty) {
buyQueue.removeFirst();
} else {
buyQueue.first = (price: buy.price, qty: buy.qty - matched, date: buy.date);
}
remaining -= matched;
}
}
// Нереализованный P&L по остатку в очереди
final unrealizedCostBasis = buyQueue.fold(0.0, (sum, b) => sum + b.price * b.qty);
final unrealizedQuantity = buyQueue.fold(0.0, (sum, b) => sum + b.qty);
return PnLResult(
realizedPnL: realizedPnL - totalFees,
unrealizedCostBasis: unrealizedCostBasis,
unrealizedQuantity: unrealizedQuantity,
totalFees: totalFees,
);
}
}
Учёт комиссий
Комиссии режут P&L, и их учёт нетривиален. На биржах комиссия может быть:
- В quote currency (продал BTC/USDT — комиссия в USDT, вычитается из полученной суммы)
- В base currency (комиссия в BTC — уменьшает количество полученного)
- В третьем токене (BNB на Binance при включённом fee discount)
Для корректного P&L — конвертируем все комиссии в quote currency по курсу на момент сделки:
double normalizeFeeToCurrency(Trade trade, double feeTokenPriceAtTime) {
if (trade.feeAsset == trade.quoteCurrency) {
return trade.fee; // уже в нужной валюте
}
// Комиссия в другом токене (BNB и т.д.)
return trade.fee * feeTokenPriceAtTime;
}
Цена feeTokenPriceAtTime — из исторического API CoinGecko или Binance Klines для точной даты сделки.
Нереализованный P&L в реальном времени
unrealizedPnL = (currentPrice - avgEntryPrice) * holdingQuantity
Для обновления в реальном времени подписываемся на WebSocket тикер. avgEntryPrice после каждой покупки:
// При новой покупке пересчитываем среднюю цену (методом average cost)
void addPosition(double buyPrice, double quantity) {
final newTotalCost = (_totalQuantity * _avgEntryPrice) + (buyPrice * quantity);
_totalQuantity += quantity;
_avgEntryPrice = newTotalCost / _totalQuantity;
}
double get unrealizedPnL => (_currentPrice - _avgEntryPrice) * _totalQuantity;
double get unrealizedPnLPercent => (unrealizedPnL / (_avgEntryPrice * _totalQuantity)) * 100;
ValueNotifier<double> для _currentPrice — при обновлении цены пересчитывается только P&L, не весь экран.
UI: как показывать P&L понятно
Три блока информации на одном экране:
-
Нереализованный P&L — текущая позиция, обновляется в реальном времени. Крупный шрифт, зелёный/красный цвет, абсолютное значение + процент.
-
Реализованный P&L — итог по закрытым сделкам за выбранный период. Менее критичен для мониторинга, но важен для налогов.
-
Breakdown — таблица по каждой паре с entry price, количеством, текущей ценой, P&L.
Переключатель метода (FIFO/LIFO/Average Cost) — в настройках, с предупреждением что смена метода пересчитает всю историю.
Экспорт для налогов
Формат CSV с колонками: Date, Pair, Type (buy/sell), Price, Quantity, Fee, Fee Currency, Realized P&L, Method. Это базовый формат для большинства криптоналоговых сервисов (Koinly, CoinTracker) при импорте.
Что входит в работу
- Реализация трёх методов расчёта (FIFO, LIFO, Average Cost) с переключением
- Учёт комиссий в разных валютах
- Нереализованный P&L с real-time обновлением через WebSocket
- Реализованный P&L по истории сделок
- Разбивка по торговым парам
- Экспорт CSV для налоговой отчётности
Сроки
Базовый калькулятор (один метод, manual input): 3–5 дней. Полноценный с тремя методами, биржевым API, реальным временем и экспортом: 2–3 недели. Стоимость рассчитывается индивидуально.







