@davidosborn/crypto-tax-calculator
Version:
A tool to calculate the capital gains of cryptocurrency assets for Canadian taxes
306 lines (255 loc) • 11.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = _default;
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
var _process = _interopRequireDefault(require("process"));
var _stream = _interopRequireDefault(require("stream"));
var _assets = _interopRequireDefault(require("./assets"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
* A trade.
* @typedef {object} Trade
* @property {string} exchange The exchange on which the trade was executed.
* @property {string} baseAsset The base currency.
* @property {number} baseAmount The amount of the base currency.
* @property {string} quoteAsset The quote currency.
* @property {number} quoteAmount The amount of the quote currency.
* @property {string} feeAsset The currency of the transaction fee.
* @property {number} feeAmount The amount of the transaction fee.
* @property {number} time The time at which the trade occurred, as a UNIX timestamp.
* @property {boolean} sell True if the trade represents a sale.
* @property {number} [value] The value of the assets, in Canadian dollars.
* @property {number} [feeValue] The value of the transaction fee, in Canadian dollars.
*/
/**
* A stream that transforms CSV records into trades.
*/
class TradeParseStream extends _stream.default.Transform {
/**
* The functions that can be used to parse a trade, indexed by the keys of the record.
* @type {object.<string, function.<object>>}
*/
constructor() {
super({
objectMode: true
});
/**
* A buffer that can be used to store multiple chunks that make up a single trade.
* @type {array.<object>}
*/
this._tradeChunks = [];
/**
* The keys of the unrecognized trades.
* @type {Set}
*/
this._unrecognizedTrades = new Set();
}
/**
* Transforms a CSV record into a trade.
* @param {object} chunk The CSV record.
* @param {string} encoding The encoding type (always 'Buffer').
* @param {function} callback A callback for when the transformation is complete.
*/
async _transform(chunk, encoding, callback) {
delete chunk.flatten;
delete chunk.flatMap;
let keys = Object.keys(chunk).join('|');
let parser = TradeParseStream._parsers[keys];
if (parser) await parser.call(this, chunk);else if (!this._unrecognizedTrades.has(keys)) {
this._unrecognizedTrades.add(keys);
console.log('WARNING: Unrecognized trade keys: "' + keys + '".');
}
callback();
}
/**
* Transforms a CSV record from Binance into a trade.
* @param {object} chunk The CSV record.
*/
async _transformBinance(chunk) {
let amount = TradeParseStream._parseNumber(chunk['Amount']);
let price = TradeParseStream._parseNumber(chunk['Price']);
this.push({
exchange: 'Binance',
baseAsset: _assets.default.normalizeCode(chunk['Market'].substring(chunk['Market'].length - 3)),
baseAmount: amount * price,
quoteAsset: _assets.default.normalizeCode(chunk['Market'].substring(0, chunk['Market'].length - 3)),
quoteAmount: amount,
feeAsset: _assets.default.normalizeCode(chunk['Fee Coin']),
feeAmount: TradeParseStream._parseNumber(chunk['Fee']),
time: TradeParseStream._parseTime(chunk['Date(UTC)']),
sell: chunk['Type'].includes('SELL')
});
}
/**
* Transforms a CSV record from Bittrex into a trade.
* @param {object} chunk The CSV record.
*/
async _transformBittrex1(chunk) {
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-');
baseAsset = _assets.default.normalizeCode(baseAsset);
quoteAsset = _assets.default.normalizeCode(quoteAsset);
this.push({
exchange: 'Bittrex',
baseAsset: baseAsset,
baseAmount: TradeParseStream._parseNumber(chunk['Price']),
quoteAsset: quoteAsset,
quoteAmount: TradeParseStream._parseNumber(chunk['Quantity']),
feeAsset: baseAsset,
feeAmount: TradeParseStream._parseNumber(chunk['CommissionPaid']),
time: TradeParseStream._parseTime(chunk['Closed']),
sell: chunk['Type'].includes('SELL')
});
}
/**
* Transforms a CSV record from Bittrex into a trade.
* @param {object} chunk The CSV record.
*/
async _transformBittrex2(chunk) {
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-');
baseAsset = _assets.default.normalizeCode(baseAsset);
quoteAsset = _assets.default.normalizeCode(quoteAsset);
let quantity = TradeParseStream._parseNumber(chunk['Quantity']);
let quantityRemaining = TradeParseStream._parseNumber(chunk['QuantityRemaining']);
this.push({
exchange: 'Bittrex',
baseAsset: baseAsset,
baseAmount: TradeParseStream._parseNumber(chunk['Price']),
quoteAsset: quoteAsset,
quoteAmount: quantity - quantityRemaining,
feeAsset: baseAsset,
feeAmount: TradeParseStream._parseNumber(chunk['Commission']),
time: TradeParseStream._parseTime(chunk['TimeStamp']),
sell: chunk['OrderType'].includes('SELL')
});
}
/**
* Transforms a CSV record from Kraken into a trade.
* @param {object} chunk The CSV record.
*/
async _transformKraken(chunk) {
let chunks = this._tradeChunks; // We only care about trades.
if (chunk['type'] !== 'trade') {
if (chunks.length > 0) {
console.log('WARNING: Found unpaired trade chunk.');
chunks.length = 0;
}
return;
} // Normalize the properties of the chunk.
chunk = {
asset: _assets.default.normalizeCode(chunk['asset']),
amount: TradeParseStream._parseNumber(chunk['amount']),
time: TradeParseStream._parseTime(chunk['time']),
fee: TradeParseStream._parseNumber(chunk['fee'])
}; // Process two consecutive trade chunks as a single trade.
chunks.push(chunk);
if (chunks.length === 2) {
// Ensure the chunks have the same timestamp.
if (chunks[0].time !== chunks[1].time) console.log('WARNING: Found paired trade chunks with different timestamps.'); // Determine which chunks represent the base and quote of the currency pair.
let priorities = chunks.map(c => _assets.default.getPriority(c.asset));
let isCurrencyPairReversed = priorities[0] < priorities[1];
let baseChunk = chunks[+!isCurrencyPairReversed];
let quoteChunk = chunks[+isCurrencyPairReversed];
this.push({
exchange: 'Kraken',
baseAsset: baseChunk.asset,
baseAmount: Math.abs(baseChunk.amount),
quoteAsset: quoteChunk.asset,
quoteAmount: Math.abs(quoteChunk.amount),
feeAsset: baseChunk.asset,
feeAmount: baseChunk.fee,
time: baseChunk.time,
sell: baseChunk.amount > 0 || quoteChunk.amount < 0
});
chunks.length = 0;
}
}
/**
* Transforms a CSV record from KuCoin into a trade.
* @param {object} chunk The CSV record.
*/
async _transformKuCoin(chunk) {
// If this is not a trade, then drop it.
let buySell = chunk['Buy/Sell'];
if (buySell !== 'Buy' && buySell !== 'Sell') return;
let [quoteAsset, baseAsset] = chunk['Coin'].split('/');
baseAsset = _assets.default.normalizeCode(baseAsset);
quoteAsset = _assets.default.normalizeCode(quoteAsset);
const splitAmountAssetRegExp = /^([0-9.,]+)([A-Za-z][A-Za-z0-9]*)$/;
let [baseAmount, baseAmountAsset] = chunk['Volume'].match(splitAmountAssetRegExp).slice(1);
let [quoteAmount, quoteAmountAsset] = chunk['Amount'].match(splitAmountAssetRegExp).slice(1);
let [feeAmount, feeAsset] = chunk['Fee'].match(splitAmountAssetRegExp).slice(1);
baseAmountAsset = _assets.default.normalizeCode(baseAmountAsset);
quoteAmountAsset = _assets.default.normalizeCode(quoteAmountAsset);
feeAsset = _assets.default.normalizeCode(feeAsset);
if (baseAmountAsset !== baseAsset) {
console.log('WARNING: Expected amount of ' + baseAsset + ' but found ' + baseAmountAsset + ' instead.');
return;
}
if (quoteAmountAsset !== quoteAsset) {
console.log('WARNING: Expected amount of ' + quoteAsset + ' but found ' + quoteAmountAsset + ' instead.');
return;
}
this.push({
exchange: 'KuCoin',
baseAsset: baseAsset,
baseAmount: TradeParseStream._parseNumber(chunk['Volume']),
quoteAsset: quoteAsset,
quoteAmount: TradeParseStream._parseNumber(chunk['Amount']),
feeAsset: feeAsset,
feeAmount: feeAmount,
time: TradeParseStream._parseTime(chunk['Time']),
sell: buySell === 'Sell'
});
}
/**
* Transforms a custom CSV record into a trade.
* @param {object} chunk The CSV record.
*/
async _transformCustom(chunk) {
// Ignore trades that include a special token in the comments.
if (chunk['Comments'].includes('IGNORE')) return;
let baseAmount = TradeParseStream._parseNumber(chunk['Base amount']);
let quoteAmount = TradeParseStream._parseNumber(chunk['Quote amount']);
this.push({
exchange: 'Custom',
baseAsset: _assets.default.normalizeCode(chunk['Base asset']),
baseAmount: Math.abs(baseAmount),
quoteAsset: _assets.default.normalizeCode(chunk['Quote asset']),
quoteAmount: Math.abs(quoteAmount),
feeAsset: _assets.default.normalizeCode(chunk['Fee asset']),
feeAmount: TradeParseStream._parseNumber(chunk['Fee amount']),
time: TradeParseStream._parseTime(chunk['Time']),
sell: baseAmount > 0 || quoteAmount < 0
});
}
/**
* Parses a number.
* @param {string} s The string.
* @returns {number} The number.
*/
static _parseNumber(s) {
return parseFloat(s.replace(',', ''));
}
/**
* Parses a time.
* @param {string} s The string.
* @returns {number} The time, as a UNIX timestamp.
*/
static _parseTime(s) {
return new Date(s).getTime();
}
}
_defineProperty(TradeParseStream, "_parsers", {
'Date(UTC)|Market|Type|Price|Amount|Total|Fee|Fee Coin': TradeParseStream.prototype._transformBinance,
'OrderUuid|Exchange|Type|Quantity|Limit|CommissionPaid|Price|Opened|Closed': TradeParseStream.prototype._transformBittrex1,
'Uuid|Exchange|TimeStamp|OrderType|Limit|Quantity|QuantityRemaining|Commission|Price|PricePerUnit|IsConditional|Condition|ConditionTarget|ImmediateOrCancel|Closed': TradeParseStream.prototype._transformBittrex2,
'txid|refid|time|type|aclass|asset|amount|fee|balance': TradeParseStream.prototype._transformKraken,
'Coin|Time|Buy/Sell|Filled Price|Amount|Fee|Volume': TradeParseStream.prototype._transformKuCoin,
'Base asset|Base amount|Quote asset|Quote amount|Fee asset|Fee amount|Time|Comments': TradeParseStream.prototype._transformCustom
});
function _default(...args) {
return new TradeParseStream(...args);
}