Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save carbonphyber/9f4ea9befb06b15c1e92d7918eecb316 to your computer and use it in GitHub Desktop.
Save carbonphyber/9f4ea9befb06b15c1e92d7918eecb316 to your computer and use it in GitHub Desktop.
Convert Coinbase transactions export to a cost basis CSV almost compatible with TurboTax import. No Warranties.
/**
* Read a Coinbase transaction export and print out a Cost Basis CSV (useful for tax calculation
* of cryptocurrency profits). The output is near the format that TurboTax expects, but not perfect
* (likely headers/columns are slightly deformed). This script was written with an export file
* which had only a single Coinbase account / wallet, only two currencies, and all transactions were
* USD <-> BTC (no conversions/transactions of one crypto for another).
*
* This script is meant to be run as CLI from Node (probably compatible with 12+ because of use of fs.promises).
* Needs an npm repo around this source file, then run `npm install` / `yarn`.
* Example commands:
* - $ mkdir coinbase-export-cost-basis
* - $ cd coinbase-export-cost-basis && npm init -y
* - $ npm install papaparse
* - $ node index.js
*/
const fs = require('fs').promises;
const papaparse = require('papaparse');
const [appName, scriptName, filePath] = process.argv;
const allBuyRecords = {};
const allSellRecords = {};
const allCostBasis = {};
const bchHardForkDate = new Date('2017-08-01T00:00:00Z');
const btcOriginDate = new Date('2009-08-01T00:00:00Z');
const milliSecondsInYear = 31536000000;
// note that this rounds to the nearest cent, so expect the data to be lossy at a sub-cent level
const formatDollars = (num) => Math.round((num) * 100) / 100;
const csvContents = fs.readFile(filePath)
// convert file contents from buffer to string
.then(contents => contents.toString('utf8'))
// truncate anything before the first CSV header line (requirest exact text match)
.then(contents => {
const csvBeginsAt = contents.indexOf('Timestamp,Transaction Type,Asset,Quantity Transacted,USD Spot Price at Transaction,USD Subtotal,USD Total (inclusive of fees),USD Fees,Notes');
if (csvBeginsAt < 0) {
throw new Error('Could not find headers in this file');
}
return csvBeginsAt === 0
? contents
: contents.substr(csvBeginsAt)
})
// trim any whitespace at the beginning or end of the CSV string
.then(contents => contents.replace(/^\s+|\s+$/, ''))
// read CSV string into papaparse object
.then(contents => papaparse.parse(contents, {
delimiter: ',',
newline: "\n",
quoteChar: '"',
header: true,
}))
// Gather all buy data in one iteration
.then(data => {
// only supports BCH and BTC
['BCH', 'BTC'].forEach(asset => {
allBuyRecords[asset] = data.data
.filter(row => row['Asset'] === asset && ['Buy', 'Receive'].includes(row['Transaction Type']))
.map(row => ({
'Timestamp': new Date(row['Timestamp']),
'Transaction Type': 'Buy',
'Quantity Transacted': Number(row['Quantity Transacted'] || 0.0),
'Amount Excluding Fees': Number(row['USD Total (inclusive of fees)'] || 0.0),
'Amount Including Fees': Number(row['USD Subtotal'] || 0.0),
}));
allSellRecords[asset] = data.data
.filter(row => row['Asset'] === asset && ['Sell'].includes(row['Transaction Type']))
.map(row => ({
'Timestamp': new Date(row['Timestamp']),
'Transaction Type': 'Sell',
'Quantity Transacted': Number(row['Quantity Transacted'] || 0.0),
'Amount Excluding Fees': Number(row['USD Total (inclusive of fees)'] || 0.0),
'Amount Including Fees': Number(row['USD Subtotal'] || 0.0),
}));
});
return {
buys: allBuyRecords,
sells: allSellRecords,
};
})
// Iterate through all sale data
.then(data => {
['BCH', 'BTC'].forEach(asset => {
if (!Array.isArray(allCostBasis[asset])) {
allCostBasis[asset] = [];
}
let runningAssetsSold = 0.0;
allSellRecords[asset].forEach((saleRow, i) => {
// running aggregates
let runningAssetsBought = 0.0;
// default cost basis data object
const buyObj = {
'Buy Date': new Date('1970-01-01T00:00:00Z'),
'Quantity': 0.0,
'Price': 0.0,
};
const quantitySold = Number(saleRow['Quantity Transacted'] || 0.0);
const saleDate = saleRow['Timestamp'];
const proceeds = formatDollars(Number(saleRow['Amount Excluding Fees'] || 0.0));
let buyDone = false;
allBuyRecords[asset].forEach(buyRow => {
const buyQuantity = Number(buyRow['Quantity Transacted'] || 0.0);
const buyPrice = Number(buyRow['Amount Including Fees'] || 0.0);
let intersectQuantity = 0.0;
let fork = 'default';
if (runningAssetsBought + buyQuantity < runningAssetsSold) {
fork = 'not yet';
// If this buy record was apart of a previous sell cost basis
// don't include in this cost basis.
} else if (runningAssetsBought > runningAssetsSold + quantitySold) {
fork = 'completed';
// If we have already calculated the full cost basis for this sell transaction
// don't include in this cost basis.
buyDone = true;
// } else if (runningAssetsBought + buyQuantity <= runningAssetsSold + quantitySold) {
} else {
fork = 'include';
// Include the remaining part in this cost basis.
const intersectionUpperBound = Math.min(
runningAssetsSold + quantitySold,
runningAssetsBought + buyQuantity,
);
const intersectionLowerBound = Math.max(
0.0,
runningAssetsSold,
runningAssetsBought,
);
const intersectionMax = intersectionUpperBound - intersectionLowerBound;
runningAssetsSold + quantitySold - runningAssetsBought;
intersectQuantity = Math.min(buyQuantity, intersectionMax);
const pricePerUnit = buyQuantity > 0.0
? buyPrice / buyQuantity
: 0.0;
// Only add the quantity within this buy+sell intersection to buyObj
buyObj['Quantity'] += intersectQuantity;
// add the propertion of remaining quantity at the current buyPrice
buyObj['Price'] += (pricePerUnit * intersectQuantity);
buyObj['Buy Date'] = new Date(buyRow['Timestamp']);
}
runningAssetsBought += buyQuantity;
});
const buyDate = buyObj['Buy Date'];
const quantity = quantitySold;
// Avoid Divide by Zero condition
const costBasis = buyObj['Quantity'] > 0.0
? formatDollars(buyObj['Price'])
: 0.0;
const yearsHeld = (saleDate - buyDate) / milliSecondsInYear;
const assetCostBasisObj = {
'Service name': 'Coinbase',
'Asset': asset,
'Buy Date': buyDate,
'Sale Date': saleDate,
'Quantity': quantity,
'Proceeds': proceeds,
'Cost Basis': costBasis,
'Gain/Loss': formatDollars(proceeds - costBasis),
'Term': yearsHeld >= 1.0
? 'Long'
: 'Short',
};
allCostBasis[asset].push(assetCostBasisObj);
runningAssetsSold += quantity;
});
});
return data;
})
// print out
.then(data => {
const bchText = papaparse.unparse(allCostBasis['BCH'], {
delimiter: ',',
newline: "\n",
quoteChar: '"',
});
const btcText = papaparse.unparse(allCostBasis['BTC'], {
delimiter: ',',
newline: "\n",
quoteChar: '"',
});
console.log(`**** BCH ****
${bchText}
**** BTC ****
${btcText}`);
return data;
})
// handle errors
.catch(err => console.error(err));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment