Skip to content

Instantly share code, notes, and snippets.

@CaptEmulation
Last active February 6, 2016 02:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CaptEmulation/02b20542f97d56c94ead to your computer and use it in GitHub Desktop.
Save CaptEmulation/02b20542f97d56c94ead to your computer and use it in GitHub Desktop.
2014 Bitcoin taxes cost basis and income calculations. Uses Libra Tax files as input
var argv = require('minimist')(process.argv.slice(2));
var fs = require('fs');
var csvParse = require('csv-parse');
var csvStringify = require('csv-stringify');
var moment = require('moment');
if (argv._.length === 0) {
console.log('Usage: node tax --year 2014 Libra.csv output.csv');
process.exit();
}
// first full arg
var input = argv._[0];
// second full arg
var output = argv._[1] || 'output.txt';
var year = argv.year || argv.y;
var error;
error = !input && new Error('No input file specified');
if (error) {
throw error;
}
// Shared global data model
var data = {
transactions: [],
buys: [],
sells: [],
generated: [],
taxables: [],
delta: 0
}
/**
* Simple FIFO algorithm matching received coins with spent coins
*/
var firstBuy = (function () {
// The current index of owned coins
var index = 0;
return function (sell) {
var needToSell = -sell.amount;
var costBasis = 0;
while (needToSell > 0) {
var buy = data.buys[index];
if (buy) {
var buyAmountAvailable = buy.sold ? buy.amount - buy.sold : buy.amount;
var selling = needToSell > buyAmountAvailable ? buyAmountAvailable : needToSell;
var buyCostBasis = Math.abs(selling / buyAmountAvailable * buy.totalValue);
buy.sold += selling;
costBasis += buyCostBasis;
needToSell -= selling;
index++;
} else {
console.log('warning, not enough purchases to cover sales!');
break;
}
}
// Taxable event object
sell.costBasis = costBasis;
var t = {
date: moment(sell.date).format('MM-DD-YYYY'),
amount: sell.amount.toFixed(8),
proceeds: Math.abs(sell.totalValue).toFixed(2),
costBasis: sell.costBasis.toFixed(2)
};
t.delta = (Math.abs(sell.totalValue) - sell.costBasis);
data.taxables.push(t);
};
}());
/**
* Helps caluclate total USD profit/loss from sales
*/
var profitLoss = function (taxable) {
data.delta += taxable.delta;
};
/**
* Write taxable events to output CSV
*/
var writeTaxables = function () {
var taxables = [['Description', 'Date', 'Amount', 'Purchase price', 'Proceeds', 'Cost Basis']].concat(data.taxables.map(function (tax) {
return ['Bitcoin sales', tax.date, tax.amount, 'VARIOUS', tax.proceeds, tax.costBasis];
}));
csvStringify(taxables, function (err, data) {
fs.writeFile(output, data, function (err) {
if (err) throw err;
console.log('done');
});
});
}
/**
* Caluclated all mined blocks
*/
var calculateGeneratedIncome = function () {
var income = {
btc: 0,
usd: 0
};
data.generated.forEach(function (g) {
income.btc += g.amount;
income.usd += g.totalValue;
});
return income;
}
/**
* Read input CSV and display results
*/
fs.readFile(input, 'utf8', function (err, file) {
if (err) throw err;
importCsv(file, function (data) {
data.sells.forEach(firstBuy);
data.taxables.forEach(profitLoss);
writeTaxables();
console.log('Mining income: ', calculateGeneratedIncome());
console.log('Transaction income (USD): $' + data.delta.toFixed(2));
//console.log(data.taxables);
});
});
/**
* Turns CSV headers into Javascript friendly object field names. No spaces and camel casing
*/
function objectify(string) {
return string.split(' ').reduce(function (prev, curr, index) {
return prev += curr.charAt(0)[index ? 'toUpperCase' : 'toLowerCase']() + curr.slice(1);
}, '');
}
/**
* CSV parser to data model
*/
var importCsv = function (file, callback) {
var fields;
csvParse(file, function (err, output) {
if (err) throw err;
output.forEach(function (row, index) {
if (index === 0) {
// Creates object field names from headers and saves for later
fields = row.map(objectify);
} else if (row.length > 1) {
// Unpacks the row into object t using objectify names, above
var t = row.reduce(function (prev, curr, index) {
prev[fields[index]] = curr;
return prev;
}, {});
// Some post processing
t.date = new Date(t.date);
t.amount = parseFloat(t.amount);
t.spotValue = parseFloat(t.spotValue);
t.totalValue = parseFloat(t.totalValue);
// Filtering by year or all if unspecified
if (year ? t.date.getFullYear() === year : true) {
data.transactions.push(t);
if (t.taxType === 'sale') {
data.sells.push(t);
}
if (t.taxType === 'purchase') {
t.sold = 0;
data.buys.push(t);
}
// These are what p2pool generated (mined) coins tracked by BlockChain look like in Libra Tax CSV
if (t.destination === 'Blockchain Wallet') {
data.generated.push(t);
t.generated = true;
}
}
}
});
// All done callback
callback(data);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment