UNPKG

@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
'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); }