Разработка системы налогового учета криптовалют
Налоговый учёт криптовалют сложнее чем кажется: каждый swap, staking reward, airdrop, NFT продажа — потенциально налогооблагаемое событие. Для пользователя с активной DeFi историей это сотни транзакций в год. Система должна классифицировать их, рассчитать cost basis и сформировать отчёт в формате нужной юрисдикции.
Классификация налоговых событий
Ключевой шаг перед любым расчётом — правильная классификация каждой транзакции:
enum TaxEventType {
DISPOSAL = "disposal", // продажа, своп — capital gains/losses
INCOME = "income", // mining reward, staking, airdrops — ordinary income
PURCHASE = "purchase", // покупка крипты за фиат — не taxable само по себе
TRANSFER = "transfer", // перевод между своими кошельками — не taxable
GAS_FEE = "gas_fee", // может добавляться к cost basis или списываться
GIFT_SENT = "gift_sent",
GIFT_RECEIVED = "gift_received",
FORK = "fork", // hard fork / airdrop
}
interface TaxEvent {
id: string;
userId: string;
timestamp: Date;
type: TaxEventType;
asset: string;
amount: number;
usdValueAtTime: number; // fair market value в момент события
costBasis?: number; // для disposal: cost basis реализуемого актива
gainsOrLoss?: number; // proceeds - cost_basis
isLongTerm?: boolean; // > 1 года владения (в US: пониженная ставка)
txHash: string;
exchange?: string;
notes?: string;
}
Cost Basis методы
Разные юрисдикции требуют разные методы:
FIFO (First In, First Out): первые купленные — первые проданные. США, Великобритания, большинство юрисдикций по умолчанию.
LIFO (Last In, First Out): последние купленные — первые проданные. Разрешён в некоторых случаях в США.
HIFO (Highest In, First Out): продаём сначала самое дорогое — минимизирует налог. Выгодно при росте рынка.
Средневзвешенная (Average Cost): Германия, Нидерланды и ряд других EU стран.
class CostBasisCalculator {
// FIFO реализация
async calculateFIFO(
asset: string,
userId: string,
disposalAmount: number,
disposalDate: Date
): Promise<CostBasisResult> {
// Получаем все покупки в хронологическом порядке
const lots = await this.db.getAssetLots(userId, asset, {
orderBy: "acquired_at ASC",
remainingAmount: "> 0",
});
let remainingToDispose = disposalAmount;
let totalCostBasis = 0;
const usedLots: LotUsage[] = [];
for (const lot of lots) {
if (remainingToDispose <= 0) break;
const amountFromThisLot = Math.min(lot.remainingAmount, remainingToDispose);
const costBasisFromLot = (amountFromThisLot / lot.originalAmount) * lot.totalCostBasis;
totalCostBasis += costBasisFromLot;
remainingToDispose -= amountFromThisLot;
usedLots.push({
lotId: lot.id,
amountUsed: amountFromThisLot,
costBasisUsed: costBasisFromLot,
acquiredAt: lot.acquiredAt,
holdingPeriodDays: Math.floor(
(disposalDate.getTime() - lot.acquiredAt.getTime()) / 86400000
),
});
// Обновляем остаток лота
await this.db.reduceLotAmount(lot.id, amountFromThisLot);
}
return { totalCostBasis, usedLots, isLongTerm: this.isLongTerm(usedLots) };
}
// Средневзвешенная (для DE, NL)
async calculateAverageCost(
asset: string,
userId: string,
disposalAmount: number
): Promise<CostBasisResult> {
const { totalAmount, totalCost } = await this.db.getAggregatedPosition(userId, asset);
const averageCostPerUnit = totalCost / totalAmount;
return {
totalCostBasis: averageCostPerUnit * disposalAmount,
usedLots: [], // нет отдельных лотов в average cost
};
}
}
Исторические цены
Cost basis требует fair market value в момент каждой транзакции. Источники:
class PriceHistoryService {
async getHistoricalPrice(asset: string, timestamp: Date): Promise<number> {
// 1. Проверяем собственный кеш
const cached = await this.cache.get(asset, timestamp);
if (cached) return cached;
// 2. CoinGecko API (free tier: 1 год истории)
const price = await this.coingecko.getHistoricalPrice(asset, timestamp);
// 3. Fallback: CryptoCompare, Messari
if (!price) {
return this.cryptoCompare.getHistoricalClose(asset, timestamp);
}
await this.cache.set(asset, timestamp, price);
return price;
}
}
Проблема: для токенов без ликвидности (obscure altcoins) исторические цены могут отсутствовать. В таких случаях нужно либо использовать CEX данные, либо документировать как "price not determinable".
DeFi специфика
DeFi транзакции — самая сложная часть:
Liquidity provision (Uniswap V2 LP): депозит двух токенов → получение LP токенов. Это NOT taxable само по себе в большинстве юрисдикций. Но при выводе ликвидности — каждый полученный токен сравнивается с cost basis LP токенов.
Uniswap V3 (concentrated liquidity): ещё сложнее, так как позиция имеет range и impermanent loss. Каждое изменение fees — потенциальный income event.
Yield farming / staking rewards: большинство юрисдикций трактует как ordinary income в момент получения по fair market value.
Airdrop: спорно. США: taxable income при получении. Ряд EU стран: taxable только при продаже.
Формирование налогового отчёта
Разные форматы для разных юрисдикций:
// US: Schedule D compatible format
function generateScheduleD(events: TaxEvent[]): ScheduleDRow[] {
return events
.filter(e => e.type === TaxEventType.DISPOSAL)
.map(e => ({
description: `${e.amount} ${e.asset}`,
dateAcquired: formatDate(e.costBasisLot.acquiredAt),
dateSold: formatDate(e.timestamp),
proceeds: e.usdValueAtTime,
costBasis: e.costBasis!,
gainOrLoss: e.gainsOrLoss!,
term: e.isLongTerm ? "LONG" : "SHORT",
}));
}
// UK: HMRC Capital Gains Summary
function generateHMRCSummary(events: TaxEvent[], taxYear: string): HMRCSummary {
// UK использует "pool" method (Section 104 pool) + 30-day same-day rule
const ukEvents = applyUKPoolingRules(events);
return formatHMRCReport(ukEvents, taxYear);
}
Стек
| Компонент | Технология |
|---|---|
| Transaction import | Exchange APIs (Binance, Coinbase) + wallet indexing |
| Price history | CoinGecko + CryptoCompare |
| Cost basis engine | Node.js + PostgreSQL |
| Report generation | PDF (PDFKit) + CSV + Excel |
| Frontend | React + TypeScript |
Система налогового учёта с FIFO/LIFO/Average, DeFi классификацией и мультиюрисдикционными отчётами — 6-10 недель разработки.







