Розробка системи податкового обліку крипто-валют
Податковий облік крипто-валют складніше ніж здається: кожен своп, стейкинг-нагорода, airdrop, продаж NFT — потенційно opodatkovia подія. Для користувача з активною DeFi історією це сотні транзакцій на рік. Система повинна класифікувати їх, розрахувати cost basis та сформувати звіт у форматі потрібної юрисдикції.
Класифікація податкових подій
Ключовий крок перед будь-яким розрахунком — правильна класифікація кожної транзакції:
enum TaxEventType {
DISPOSAL = "disposal", // продаж, своп — capital gains/losses
INCOME = "income", // mining reward, staking, airdrops — звичайний дохід
PURCHASE = "purchase", // покупка крипто за фіат — не opodatkovia сама по собі
TRANSFER = "transfer", // передача між своїми гаманцями — не opodatkovia
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; // справедлива ринкова вартість у момент подіїї
costBasis?: number; // для disposal: cost basis реалізуємого активу
gainsOrLoss?: number; // proceeds - cost_basis
isLongTerm?: boolean; // > 1 року володіння (у США: нижча ставка)
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): Німеччина, Нідерланди та ряд інших країн ЄС.
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: [], // немає окремих лотів у середній вартості
};
}
}
Історичні ціни
Cost basis вимагає справедливої ринкової вартості в момент кожної транзакції. Джерела:
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;
}
}
Проблема: для токенів без ліквідності (невідомі altcoins) історичні ціни можуть відсутствувати. У таких випадках або використовуємо дані CEX, або документуємо як "ціна не визначена".
Специфіка DeFi
DeFi транзакції — найскладніша частина:
Забезпечення ліквідності (Uniswap V2 LP): депозит двох токенів → отримання LP токенів. Це НЕ opodatkovia саме по собі у більшості юрисдикцій. Але при виведенні ліквідності — кожен отриманий токен порівнюється з cost basis LP токенів.
Uniswap V3 (зосереджена ліквідність): ще складніше, оскільки позиція має діапазон та impermanent loss. Кожна зміна комісій — потенційна подія доходу.
Yield farming / стейкинг нагороди: більшість юрисдикцій трактує як звичайний дохід при отриманні за справедливою ринковою вартістю.
Airdrop: спірно. США: opodatkovia дохід при отриманні. Ряд країн ЄС: opodatkovia тільки при продажу.
Формування податкового звіту
Різні формати для різних юрисдикцій:
// США: Schedule D формат, сумісний
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",
}));
}
// Великобританія: HMRC Capital Gains Summary
function generateHMRCSummary(events: TaxEvent[], taxYear: string): HMRCSummary {
// Великобританія використовує метод "池" (Section 104 pool) + правило 30 днів
const ukEvents = applyUKPoolingRules(events);
return formatHMRCReport(ukEvents, taxYear);
}
Стек
| Компонент | Технологія |
|---|---|
| Імпорт транзакцій | Exchange APIs (Binance, Coinbase) + wallet indexing |
| Історія цін | CoinGecko + CryptoCompare |
| Cost basis двигун | Node.js + PostgreSQL |
| Генерація звітів | PDF (PDFKit) + CSV + Excel |
| Frontend | React + TypeScript |
Система податкового обліку крипто з FIFO/LIFO/Average, класифікацією DeFi та мультиюрисдикційними звітами — 6-10 тижнів розробки.







