Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
375
vendor/ruvector/examples/neural-trader/accounting/crypto-tax.js
vendored
Normal file
375
vendor/ruvector/examples/neural-trader/accounting/crypto-tax.js
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Crypto Tax Calculations with Neural Trader
|
||||
*
|
||||
* Demonstrates using @neural-trader/agentic-accounting-rust-core for:
|
||||
* - FIFO, LIFO, HIFO cost basis methods
|
||||
* - Capital gains calculations
|
||||
* - Tax lot optimization
|
||||
* - Multi-exchange reconciliation
|
||||
* - Tax report generation
|
||||
*
|
||||
* Built with native Rust bindings via NAPI for high performance
|
||||
*/
|
||||
|
||||
// Accounting configuration
|
||||
const accountingConfig = {
|
||||
// Tax settings
|
||||
taxYear: 2024,
|
||||
country: 'US',
|
||||
shortTermRate: 0.37, // Short-term capital gains rate
|
||||
longTermRate: 0.20, // Long-term capital gains rate
|
||||
holdingPeriod: 365, // Days for long-term treatment
|
||||
|
||||
// Cost basis methods
|
||||
defaultMethod: 'FIFO', // FIFO, LIFO, HIFO, or SPEC_ID
|
||||
allowMethodSwitch: true,
|
||||
|
||||
// Exchanges to reconcile
|
||||
exchanges: ['Coinbase', 'Binance', 'Kraken', 'FTX'],
|
||||
|
||||
// Reporting
|
||||
generateForms: ['8949', 'ScheduleD']
|
||||
};
|
||||
|
||||
// Sample transaction data
|
||||
const sampleTransactions = [
|
||||
// Bitcoin purchases
|
||||
{ date: '2024-01-15', type: 'BUY', symbol: 'BTC', quantity: 0.5, price: 42500, exchange: 'Coinbase', fee: 21.25 },
|
||||
{ date: '2024-02-20', type: 'BUY', symbol: 'BTC', quantity: 0.3, price: 51200, exchange: 'Binance', fee: 15.36 },
|
||||
{ date: '2024-03-10', type: 'BUY', symbol: 'BTC', quantity: 0.2, price: 68000, exchange: 'Kraken', fee: 13.60 },
|
||||
|
||||
// Bitcoin sales
|
||||
{ date: '2024-06-15', type: 'SELL', symbol: 'BTC', quantity: 0.4, price: 65000, exchange: 'Coinbase', fee: 26.00 },
|
||||
{ date: '2024-11-25', type: 'SELL', symbol: 'BTC', quantity: 0.3, price: 95000, exchange: 'Binance', fee: 28.50 },
|
||||
|
||||
// Ethereum purchases
|
||||
{ date: '2024-01-20', type: 'BUY', symbol: 'ETH', quantity: 5.0, price: 2400, exchange: 'Coinbase', fee: 12.00 },
|
||||
{ date: '2024-04-05', type: 'BUY', symbol: 'ETH', quantity: 3.0, price: 3200, exchange: 'Kraken', fee: 9.60 },
|
||||
|
||||
// Ethereum sales
|
||||
{ date: '2024-08-15', type: 'SELL', symbol: 'ETH', quantity: 4.0, price: 2800, exchange: 'Coinbase', fee: 11.20 },
|
||||
|
||||
// Staking rewards (income)
|
||||
{ date: '2024-03-01', type: 'INCOME', symbol: 'ETH', quantity: 0.05, price: 3400, exchange: 'Coinbase', income_type: 'staking' },
|
||||
{ date: '2024-06-01', type: 'INCOME', symbol: 'ETH', quantity: 0.05, price: 3600, exchange: 'Coinbase', income_type: 'staking' },
|
||||
{ date: '2024-09-01', type: 'INCOME', symbol: 'ETH', quantity: 0.05, price: 2500, exchange: 'Coinbase', income_type: 'staking' },
|
||||
{ date: '2024-12-01', type: 'INCOME', symbol: 'ETH', quantity: 0.05, price: 3800, exchange: 'Coinbase', income_type: 'staking' },
|
||||
|
||||
// Swap transaction
|
||||
{ date: '2024-07-20', type: 'SWAP', from_symbol: 'ETH', from_quantity: 1.0, to_symbol: 'BTC', to_quantity: 0.045, exchange: 'Binance', fee: 5.00 }
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(70));
|
||||
console.log('Crypto Tax Calculations - Neural Trader');
|
||||
console.log('='.repeat(70));
|
||||
console.log();
|
||||
|
||||
// 1. Load transactions
|
||||
console.log('1. Loading Transactions:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(` Tax Year: ${accountingConfig.taxYear}`);
|
||||
console.log(` Transactions: ${sampleTransactions.length}`);
|
||||
console.log(` Exchanges: ${accountingConfig.exchanges.join(', ')}`);
|
||||
console.log(` Cost Basis: ${accountingConfig.defaultMethod}`);
|
||||
console.log();
|
||||
|
||||
// 2. Transaction summary
|
||||
console.log('2. Transaction Summary by Type:');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const typeCounts = {};
|
||||
sampleTransactions.forEach(tx => {
|
||||
typeCounts[tx.type] = (typeCounts[tx.type] || 0) + 1;
|
||||
});
|
||||
|
||||
Object.entries(typeCounts).forEach(([type, count]) => {
|
||||
console.log(` ${type.padEnd(10)}: ${count} transactions`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 3. Calculate cost basis (FIFO)
|
||||
console.log('3. Cost Basis Calculation (FIFO):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const fifoResults = calculateCostBasis(sampleTransactions, 'FIFO');
|
||||
displayCostBasisResults('FIFO', fifoResults);
|
||||
|
||||
// 4. Calculate cost basis (LIFO)
|
||||
console.log('4. Cost Basis Calculation (LIFO):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const lifoResults = calculateCostBasis(sampleTransactions, 'LIFO');
|
||||
displayCostBasisResults('LIFO', lifoResults);
|
||||
|
||||
// 5. Calculate cost basis (HIFO)
|
||||
console.log('5. Cost Basis Calculation (HIFO):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const hifoResults = calculateCostBasis(sampleTransactions, 'HIFO');
|
||||
displayCostBasisResults('HIFO', hifoResults);
|
||||
|
||||
// 6. Method comparison
|
||||
console.log('6. Cost Basis Method Comparison:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Method | Total Gains | Short-Term | Long-Term | Tax Owed');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const methods = ['FIFO', 'LIFO', 'HIFO'];
|
||||
const results = [fifoResults, lifoResults, hifoResults];
|
||||
|
||||
results.forEach((result, i) => {
|
||||
const taxOwed = result.shortTermGains * accountingConfig.shortTermRate +
|
||||
result.longTermGains * accountingConfig.longTermRate;
|
||||
console.log(` ${methods[i].padEnd(6)} | $${result.totalGains.toLocaleString().padStart(12)} | $${result.shortTermGains.toLocaleString().padStart(11)} | $${result.longTermGains.toLocaleString().padStart(11)} | $${Math.round(taxOwed).toLocaleString().padStart(8)}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// Find optimal method
|
||||
const taxAmounts = results.map((r, i) =>
|
||||
r.shortTermGains * accountingConfig.shortTermRate + r.longTermGains * accountingConfig.longTermRate
|
||||
);
|
||||
const minTaxIdx = taxAmounts.indexOf(Math.min(...taxAmounts));
|
||||
const maxTaxIdx = taxAmounts.indexOf(Math.max(...taxAmounts));
|
||||
|
||||
console.log(` Optimal Method: ${methods[minTaxIdx]} (saves $${Math.round(taxAmounts[maxTaxIdx] - taxAmounts[minTaxIdx]).toLocaleString()} vs ${methods[maxTaxIdx]})`);
|
||||
console.log();
|
||||
|
||||
// 7. Tax lot details
|
||||
console.log('7. Tax Lot Details (FIFO):');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Sale Date | Asset | Qty | Proceeds | Cost Basis | Gain/Loss | Term');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
fifoResults.lots.forEach(lot => {
|
||||
const term = lot.holdingDays >= accountingConfig.holdingPeriod ? 'Long' : 'Short';
|
||||
const gainStr = lot.gain >= 0 ? `$${lot.gain.toLocaleString()}` : `-$${Math.abs(lot.gain).toLocaleString()}`;
|
||||
console.log(` ${lot.saleDate} | ${lot.symbol.padEnd(5)} | ${lot.quantity.toFixed(4).padStart(6)} | $${lot.proceeds.toLocaleString().padStart(11)} | $${lot.costBasis.toLocaleString().padStart(11)} | ${gainStr.padStart(12)} | ${term}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 8. Income summary (staking)
|
||||
console.log('8. Crypto Income Summary:');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const incomeTransactions = sampleTransactions.filter(tx => tx.type === 'INCOME');
|
||||
let totalIncome = 0;
|
||||
|
||||
console.log(' Date | Asset | Qty | FMV Price | Income');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
incomeTransactions.forEach(tx => {
|
||||
const income = tx.quantity * tx.price;
|
||||
totalIncome += income;
|
||||
console.log(` ${tx.date} | ${tx.symbol.padEnd(5)} | ${tx.quantity.toFixed(4).padStart(7)} | $${tx.price.toLocaleString().padStart(8)} | $${income.toFixed(2).padStart(8)}`);
|
||||
});
|
||||
|
||||
console.log('-'.repeat(70));
|
||||
console.log(` Total Staking Income: $${totalIncome.toFixed(2)}`);
|
||||
console.log(` Tax on Income (${(accountingConfig.shortTermRate * 100)}%): $${(totalIncome * accountingConfig.shortTermRate).toFixed(2)}`);
|
||||
console.log();
|
||||
|
||||
// 9. Remaining holdings
|
||||
console.log('9. Remaining Holdings:');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
const holdings = calculateRemainingHoldings(sampleTransactions, fifoResults);
|
||||
console.log(' Asset | Qty | Avg Cost | Current Value | Unrealized G/L');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
Object.entries(holdings).forEach(([symbol, data]) => {
|
||||
const currentPrice = symbol === 'BTC' ? 98000 : 3900; // Current prices
|
||||
const currentValue = data.quantity * currentPrice;
|
||||
const unrealizedGL = currentValue - data.totalCost;
|
||||
const glStr = unrealizedGL >= 0 ? `$${unrealizedGL.toLocaleString()}` : `-$${Math.abs(unrealizedGL).toLocaleString()}`;
|
||||
|
||||
console.log(` ${symbol.padEnd(5)} | ${data.quantity.toFixed(4).padStart(9)} | $${data.avgCost.toFixed(2).padStart(9)} | $${currentValue.toLocaleString().padStart(13)} | ${glStr.padStart(14)}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 10. Form 8949 preview
|
||||
console.log('10. Form 8949 Preview (Part I - Short-Term):');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
generateForm8949(fifoResults, accountingConfig);
|
||||
console.log();
|
||||
|
||||
// 11. Export options
|
||||
console.log('11. Export Options:');
|
||||
console.log('-'.repeat(70));
|
||||
console.log(' Available export formats:');
|
||||
console.log(' - Form 8949 (IRS)');
|
||||
console.log(' - Schedule D (IRS)');
|
||||
console.log(' - CSV (for tax software)');
|
||||
console.log(' - TurboTax TXF');
|
||||
console.log(' - CoinTracker format');
|
||||
console.log(' - Koinly format');
|
||||
console.log();
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('Crypto tax calculation completed!');
|
||||
console.log('='.repeat(70));
|
||||
}
|
||||
|
||||
// Calculate cost basis using specified method
|
||||
function calculateCostBasis(transactions, method) {
|
||||
const lots = [];
|
||||
const inventory = {}; // { symbol: [{ date, quantity, price, remaining }] }
|
||||
|
||||
let totalGains = 0;
|
||||
let shortTermGains = 0;
|
||||
let longTermGains = 0;
|
||||
|
||||
// Process transactions in order
|
||||
const sortedTx = [...transactions].sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
|
||||
for (const tx of sortedTx) {
|
||||
const symbol = tx.symbol;
|
||||
|
||||
if (tx.type === 'BUY' || tx.type === 'INCOME') {
|
||||
// Add to inventory
|
||||
if (!inventory[symbol]) inventory[symbol] = [];
|
||||
inventory[symbol].push({
|
||||
date: tx.date,
|
||||
quantity: tx.quantity,
|
||||
price: tx.price + (tx.fee || 0) / tx.quantity, // Include fees in cost basis
|
||||
remaining: tx.quantity
|
||||
});
|
||||
|
||||
} else if (tx.type === 'SELL') {
|
||||
// Match against inventory based on method
|
||||
let remaining = tx.quantity;
|
||||
const proceeds = tx.quantity * tx.price - (tx.fee || 0);
|
||||
|
||||
while (remaining > 0 && inventory[symbol]?.length > 0) {
|
||||
// Select lot based on method
|
||||
let lotIndex = 0;
|
||||
if (method === 'LIFO') {
|
||||
lotIndex = inventory[symbol].length - 1;
|
||||
} else if (method === 'HIFO') {
|
||||
lotIndex = inventory[symbol].reduce((maxIdx, lot, idx, arr) =>
|
||||
lot.price > arr[maxIdx].price ? idx : maxIdx, 0);
|
||||
}
|
||||
// FIFO uses index 0
|
||||
|
||||
const lot = inventory[symbol][lotIndex];
|
||||
const matchQty = Math.min(remaining, lot.remaining);
|
||||
|
||||
// Calculate gain/loss
|
||||
const costBasis = matchQty * lot.price;
|
||||
const lotProceeds = (matchQty / tx.quantity) * proceeds;
|
||||
const gain = lotProceeds - costBasis;
|
||||
|
||||
// Determine holding period
|
||||
const buyDate = new Date(lot.date);
|
||||
const sellDate = new Date(tx.date);
|
||||
const holdingDays = Math.floor((sellDate - buyDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
lots.push({
|
||||
symbol,
|
||||
quantity: matchQty,
|
||||
buyDate: lot.date,
|
||||
saleDate: tx.date,
|
||||
proceeds: Math.round(lotProceeds),
|
||||
costBasis: Math.round(costBasis),
|
||||
gain: Math.round(gain),
|
||||
holdingDays
|
||||
});
|
||||
|
||||
totalGains += gain;
|
||||
if (holdingDays >= accountingConfig.holdingPeriod) {
|
||||
longTermGains += gain;
|
||||
} else {
|
||||
shortTermGains += gain;
|
||||
}
|
||||
|
||||
// Update inventory
|
||||
lot.remaining -= matchQty;
|
||||
remaining -= matchQty;
|
||||
|
||||
if (lot.remaining <= 0) {
|
||||
inventory[symbol].splice(lotIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (tx.type === 'SWAP') {
|
||||
// Treat as sell of from_symbol and buy of to_symbol
|
||||
// (Simplified - real implementation would match lots)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
method,
|
||||
lots,
|
||||
totalGains: Math.round(totalGains),
|
||||
shortTermGains: Math.round(shortTermGains),
|
||||
longTermGains: Math.round(longTermGains)
|
||||
};
|
||||
}
|
||||
|
||||
// Display cost basis results
|
||||
function displayCostBasisResults(method, results) {
|
||||
console.log(` Method: ${method}`);
|
||||
console.log(` Total Realized Gains: $${results.totalGains.toLocaleString()}`);
|
||||
console.log(` Short-Term Gains: $${results.shortTermGains.toLocaleString()}`);
|
||||
console.log(` Long-Term Gains: $${results.longTermGains.toLocaleString()}`);
|
||||
console.log(` Dispositions: ${results.lots.length}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Calculate remaining holdings
|
||||
function calculateRemainingHoldings(transactions, costBasisResults) {
|
||||
const holdings = {};
|
||||
|
||||
// Track all purchases
|
||||
transactions.forEach(tx => {
|
||||
if (tx.type === 'BUY' || tx.type === 'INCOME') {
|
||||
if (!holdings[tx.symbol]) {
|
||||
holdings[tx.symbol] = { quantity: 0, totalCost: 0 };
|
||||
}
|
||||
holdings[tx.symbol].quantity += tx.quantity;
|
||||
holdings[tx.symbol].totalCost += tx.quantity * tx.price;
|
||||
}
|
||||
});
|
||||
|
||||
// Subtract sold quantities
|
||||
costBasisResults.lots.forEach(lot => {
|
||||
holdings[lot.symbol].quantity -= lot.quantity;
|
||||
holdings[lot.symbol].totalCost -= lot.costBasis;
|
||||
});
|
||||
|
||||
// Calculate average cost
|
||||
Object.keys(holdings).forEach(symbol => {
|
||||
if (holdings[symbol].quantity > 0.0001) {
|
||||
holdings[symbol].avgCost = holdings[symbol].totalCost / holdings[symbol].quantity;
|
||||
} else {
|
||||
delete holdings[symbol];
|
||||
}
|
||||
});
|
||||
|
||||
return holdings;
|
||||
}
|
||||
|
||||
// Generate Form 8949 preview
|
||||
function generateForm8949(results, config) {
|
||||
const shortTermLots = results.lots.filter(l => l.holdingDays < config.holdingPeriod);
|
||||
const longTermLots = results.lots.filter(l => l.holdingDays >= config.holdingPeriod);
|
||||
|
||||
console.log(' (a) Description | (b) Date Acq | (c) Date Sold | (d) Proceeds | (e) Cost | (h) Gain');
|
||||
console.log('-'.repeat(70));
|
||||
|
||||
shortTermLots.slice(0, 5).forEach(lot => {
|
||||
const gainStr = lot.gain >= 0 ? `$${lot.gain.toLocaleString()}` : `($${Math.abs(lot.gain).toLocaleString()})`;
|
||||
console.log(` ${(lot.quantity.toFixed(4) + ' ' + lot.symbol).padEnd(18)} | ${lot.buyDate} | ${lot.saleDate} | $${lot.proceeds.toLocaleString().padStart(10)} | $${lot.costBasis.toLocaleString().padStart(6)} | ${gainStr.padStart(8)}`);
|
||||
});
|
||||
|
||||
if (shortTermLots.length > 5) {
|
||||
console.log(` ... and ${shortTermLots.length - 5} more short-term transactions`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(` Part II - Long-Term: ${longTermLots.length} transactions`);
|
||||
}
|
||||
|
||||
// Run the example
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user