Разработка системы расчета себестоимости (FIFO, LIFO, средневзвешенная)
Cost basis метод напрямую влияет на налоговое обязательство. FIFO при росте рынка даёт более высокий налог (продаём самые дешёвые, накопленные ранее). HIFO минимизирует текущий налог. Средневзвешенная — наиболее простая, требуется в Германии и Нидерландах.
Выбор метода по юрисдикциям
| Юрисдикция | Разрешённые методы | Рекомендация |
|---|---|---|
| США | FIFO, HIFO, Spec ID | Spec ID (HIFO) для минимизации |
| Великобритания | Section 104 pool | Обязательный, не выбирается |
| Германия | FIFO (для crypto) | Единственный вариант |
| Австралия | FIFO, HIFO | HIFO при росте |
| Канада | ACB (average cost) | Обязательный |
Реализация всех методов
interface TaxLot {
id: string;
asset: string;
amount: number;
costPerUnit: number;
totalCost: number;
acquiredAt: Date;
remaining: number;
}
class CostBasisEngine {
// FIFO: первые купленные = первые проданные
async calculateFIFO(
userId: string,
asset: string,
disposalAmount: number,
disposalDate: Date,
proceedsUSD: number
): Promise<CostBasisResult> {
const lots = await this.db.getLots(userId, asset, { orderBy: "acquired_at ASC" });
return this.consumeLots(lots, disposalAmount, proceedsUSD, disposalDate);
}
// LIFO: последние купленные = первые проданные
async calculateLIFO(
userId: string,
asset: string,
disposalAmount: number,
disposalDate: Date,
proceedsUSD: number
): Promise<CostBasisResult> {
const lots = await this.db.getLots(userId, asset, { orderBy: "acquired_at DESC" });
return this.consumeLots(lots, disposalAmount, proceedsUSD, disposalDate);
}
// HIFO: самые дорогие = первые проданные (минимизация налога)
async calculateHIFO(
userId: string,
asset: string,
disposalAmount: number,
disposalDate: Date,
proceedsUSD: number
): Promise<CostBasisResult> {
const lots = await this.db.getLots(userId, asset, { orderBy: "cost_per_unit DESC" });
return this.consumeLots(lots, disposalAmount, proceedsUSD, disposalDate);
}
// Average Cost (Средневзвешенная): Германия, Канада
async calculateAverageCost(
userId: string,
asset: string,
disposalAmount: number,
proceedsUSD: number
): Promise<CostBasisResult> {
const { totalRemaining, totalCost } = await this.db.getAggregatedPosition(userId, asset);
if (totalRemaining === 0) throw new Error("No remaining lots");
const avgCostPerUnit = totalCost / totalRemaining;
const costBasis = avgCostPerUnit * disposalAmount;
// В average cost методе уменьшаем pool пропорционально
await this.db.reduceAverageCostPool(userId, asset, disposalAmount);
return {
costBasis,
gain: proceedsUSD - costBasis,
lots: [{ amount: disposalAmount, costPerUnit: avgCostPerUnit }],
method: "AVG_COST",
};
}
// UK Section 104 Pool: с учётом same-day rule и 30-day bed-and-breakfast rule
async calculateUKSection104(
userId: string,
asset: string,
disposalAmount: number,
disposalDate: Date,
proceedsUSD: number
): Promise<CostBasisResult> {
// 1. Same-day rule: сначала match с покупками в тот же день
const sameDayAcquisitions = await this.db.getSameDayAcquisitions(userId, asset, disposalDate);
// 2. 30-day rule (bed & breakfast): match с покупками в следующие 30 дней
const next30DaysAcquisitions = await this.db.getNext30DaysAcquisitions(
userId, asset, disposalDate
);
// 3. Section 104 pool: остаток из пула средней стоимости
const poolBasis = await this.db.getSection104Pool(userId, asset);
return this.applyUKMatching(
disposalAmount, proceedsUSD,
sameDayAcquisitions, next30DaysAcquisitions, poolBasis
);
}
private consumeLots(
lots: TaxLot[],
disposalAmount: number,
proceedsUSD: number,
disposalDate: Date
): CostBasisResult {
let remaining = disposalAmount;
let totalCostBasis = 0;
const usedLots: UsedLot[] = [];
for (const lot of lots) {
if (remaining <= 0) break;
const used = Math.min(lot.remaining, remaining);
const proportionalCost = (used / lot.amount) * lot.totalCost;
const proportionalProceeds = (used / disposalAmount) * proceedsUSD;
totalCostBasis += proportionalCost;
remaining -= used;
usedLots.push({
lotId: lot.id,
amount: used,
costBasis: proportionalCost,
proceeds: proportionalProceeds,
acquiredAt: lot.acquiredAt,
holdingDays: Math.floor((disposalDate.getTime() - lot.acquiredAt.getTime()) / 86400000),
});
}
return {
totalCostBasis,
totalProceeds: proceedsUSD,
gain: proceedsUSD - totalCostBasis,
usedLots,
};
}
}
Сравнение методов для одной сделки
async function compareMethods(
userId: string,
asset: string,
amount: number,
proceeds: number
): Promise<MethodComparison> {
const [fifo, lifo, hifo, avg] = await Promise.all([
engine.calculateFIFO(userId, asset, amount, new Date(), proceeds),
engine.calculateLIFO(userId, asset, amount, new Date(), proceeds),
engine.calculateHIFO(userId, asset, amount, new Date(), proceeds),
engine.calculateAverageCost(userId, asset, amount, proceeds),
]);
return {
FIFO: { costBasis: fifo.totalCostBasis, gain: fifo.gain },
LIFO: { costBasis: lifo.totalCostBasis, gain: lifo.gain },
HIFO: { costBasis: hifo.totalCostBasis, gain: hifo.gain },
AVG_COST: { costBasis: avg.costBasis, gain: avg.gain },
lowestTaxOption: ["FIFO","LIFO","HIFO","AVG_COST"].reduce(
(min, m) => ({ FIFO: fifo, LIFO: lifo, HIFO: hifo, AVG_COST: avg }[m].gain <
{ FIFO: fifo, LIFO: lifo, HIFO: hifo, AVG_COST: avg }[min].gain ? m : min)
),
};
}
Система расчёта cost basis поддерживающая FIFO, LIFO, HIFO, Average Cost и UK Section 104 — 2-3 недели разработки.







