Реалізація 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 для податкової звітності
Строки
Базовий калькулятор (один метод, ручний ввід): 3–5 днів. Повнофункціональний з трьома методами, біржевим API, реальним часом та експортом: 2–3 тижні. Вартість розраховується індивідуально.







