P&L Calculator Implementation in Mobile Crypto App
P&L in crypto is not just (sell_price - buy_price) × quantity. Multiple buys at different prices, fees in different tokens, realized and unrealized P&L, tax accounting methods (FIFO vs LIFO vs average cost) — each factor changes the final number. Calculation error surfaces when filing tax return.
P&L Calculation Models
Three accounting methods giving different results for same trade set:
FIFO (First In, First Out) — sell earliest purchased units first. Standard for most jurisdictions.
LIFO (Last In, First Out) — sell latest purchased. Advantageous on rising market (sell expensively purchased recently).
Average Cost — divide total position cost by quantity. Simpler to understand, allowed in some countries.
Real-world example — difference is clear:
- Buy 1: 1 BTC at $20,000
- Buy 2: 1 BTC at $30,000
- Sell: 1 BTC at $35,000
| Method | Cost Basis | P&L |
|---|---|---|
| FIFO | $20,000 | +$15,000 |
| LIFO | $30,000 | +$5,000 |
| Average Cost | $25,000 | +$10,000 |
Implement all three, with switching in settings:
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; // convert fee to base asset
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;
}
}
// Unrealized P&L on remaining queue
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,
);
}
}
Fee Accounting
Fees cut P&L, non-trivial to account for. On exchanges fees can be:
- In quote currency (sold BTC/USDT — fee in USDT, deducted from received amount)
- In base currency (fee in BTC — reduces received amount)
- In third token (BNB on Binance with fee discount enabled)
For correct P&L — convert all fees to quote currency at trade-time rate:
double normalizeFeeToCurrency(Trade trade, double feeTokenPriceAtTime) {
if (trade.feeAsset == trade.quoteCurrency) {
return trade.fee; // already correct currency
}
// Fee in different token (BNB etc.)
return trade.fee * feeTokenPriceAtTime;
}
Price feeTokenPriceAtTime — from CoinGecko historical API or Binance Klines for exact trade date.
Unrealized P&L in Real-Time
unrealizedPnL = (currentPrice - avgEntryPrice) * holdingQuantity
To update real-time subscribe to WebSocket ticker. avgEntryPrice after each buy:
// On new buy recalculate average price (average cost method)
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> for _currentPrice — on price change only P&L recalculates, not entire screen.
UI: Displaying P&L Clearly
Three information blocks on one screen:
-
Unrealized P&L — current position, updates in real-time. Large font, green/red color, absolute value + percent.
-
Realized P&L — total from closed trades for selected period. Less critical for monitoring, important for taxes.
-
Breakdown — table per pair with entry price, quantity, current price, P&L.
Method switcher (FIFO/LIFO/Average Cost) — in settings, with warning that method change recalculates entire history.
Tax Export
CSV format with columns: Date, Pair, Type (buy/sell), Price, Quantity, Fee, Fee Currency, Realized P&L, Method. Basic format for most crypto tax services (Koinly, CoinTracker) on import.
Scope of Work
- Three calculation method implementation (FIFO, LIFO, Average Cost) with switching
- Fee accounting in different currencies
- Unrealized P&L with real-time updates via WebSocket
- Realized P&L from trade history
- Breakdown per trading pair
- CSV export for tax reporting
Timeframe
Basic calculator (one method, manual input): 3–5 days. Full with three methods, exchange API, real-time and export: 2–3 weeks. Cost calculated individually.







