UNPKG

@davidosborn/crypto-tax-calculator

Version:

A tool to calculate the capital gains of cryptocurrency assets for Canadian taxes

214 lines (162 loc) 7.14 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = _default; var _binarySearchBounds = _interopRequireDefault(require("binary-search-bounds")); var _fs = _interopRequireDefault(require("fs")); var _nodeFetch = _interopRequireDefault(require("node-fetch")); var _stream = _interopRequireDefault(require("stream")); var _util = _interopRequireDefault(require("util")); var _formatTime = _interopRequireDefault(require("./format-time")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * A stream that calculates the value of each trade. */ class TradeValueStream extends _stream.default.Transform { /** * Initializes a new instance. * @param {object} [options] The options. * @param {object} [options.history] The historical data. * @param {boolean} [options.verbose] A value indicating whether to write extra information to the console. * @param {boolean} [options.web] A value indicating whether to request asset values from the internet. */ constructor(options) { super({ objectMode: true }); this._options = options; } /** * Calculates the value of a trade. * @param {Trade} chunk The trade. * @param {string} encoding The encoding type (always 'Buffer'). * @param {function} callback A callback for when the transformation is complete. */ async _transform(chunk, encoding, callback) { var _this$_options; // Log the trade. if ((_this$_options = this._options) === null || _this$_options === void 0 ? void 0 : _this$_options.verbose) { console.log('Trade ' + chunk.baseAsset + '/' + chunk.quoteAsset + ' on ' + (0, _formatTime.default)(chunk.time) + (chunk.exchange ? ' on ' + chunk.exchange : '') + '.'); } // Calculate the value of the asset. chunk.value = await this._getValue(chunk.baseAsset, chunk.baseAmount, chunk.time); // Calculate the value of the fee. chunk.feeValue = !chunk.feeAmount ? 0 : chunk.feeAsset === chunk.baseAsset ? chunk.baseAmount ? chunk.value * chunk.feeAmount / chunk.baseAmount : 0 : chunk.feeAsset === chunk.quoteAsset ? chunk.quoteAmount ? chunk.value * chunk.feeAmount / chunk.quoteAmount : 0 : await this._getValue(chunk.feeAsset, chunk.feeAmount, chunk.time); this.push(chunk); callback(); } /** * Gets the value of an asset. * @param {string} asset The asset. * @param {number} amount The amount. * @param {number} time The time, as a UNIX timestamp. * @returns {number} The value, in Canadian dollars. */ async _getValue(asset, amount, time) { if (asset === 'CAD') return amount; // Look up the value of the asset in the history. let value = this._lookupValue(asset, amount, time); if (!isNaN(value)) return value; // Request the value of the asset from the internet. value = await this._requestValue(asset, amount, time); if (!isNaN(value)) return value; console.log('WARNING: Unable to determine value of ' + amount + ' ' + asset + ' at ' + (0, _formatTime.default)(time) + '.'); throw new Error('Failed to convert ' + asset + ' to CAD.'); } /** * Looks up the value of an asset in the history. * @param {string} asset The asset. * @param {number} amount The amount. * @param {number} time The time, as a UNIX timestamp. * @returns {number} The value, in Canadian dollars. */ _lookupValue(asset, amount, time) { if (!this._options.history) return NaN; // Look up the value of the asset directly. let value = this._lookupValue0(asset, 'CAD', time); if (!isNaN(value)) return amount * value; // Look up the value of the asset indirectly. let usdValue = this._lookupValue0(asset, 'USD', time); let cadValue = this._lookupValue0('USD', 'CAD', time); if (!isNaN(usdValue) && !isNaN(cadValue)) { return amount * usdValue * cadValue; } return NaN; } /** * Looks up the value of an asset in the history. * @param {string} baseAsset The base asset. * @param {string} quoteAsset The quote asset. * @param {number} time The time, as a UNIX timestamp. * @returns {number} The value. */ _lookupValue0(baseAsset, quoteAsset, time) { let value = this._lookupValue1(baseAsset, quoteAsset, time); if (isNaN(value)) value = 1 / this._lookupValue1(quoteAsset, baseAsset, time); return value; } /** * Looks up the value of an asset in the history. * @param {string} baseAsset The base asset. * @param {string} quoteAsset The quote asset. * @param {number} time The time, as a UNIX timestamp. * @returns {number} The value. */ _lookupValue1(baseAsset, quoteAsset, time) { let history = this._options.history[baseAsset.toUpperCase() + '-' + quoteAsset.toUpperCase()]; if (!history) return NaN; let i = _binarySearchBounds.default.lt(history, time, TradeValueStream._compareHistoryTime); if (i === -1) return NaN; if (i === history.length - 1) { return history[i - 1].close; } // Approximate the value using linear interpolation. let t = (time - history[i].time) / (history[i + 1].time - history[i].time); return TradeValueStream._lerp(history[i].open, history[i + 1].open, t); } /** * Requests the value of an asset from the internet. * @param {string} asset The asset. * @param {number} amount The amount. * @param {number} time The time, as a UNIX timestamp. * @returns {number} The value, in Canadian dollars. */ async _requestValue(asset, amount, time) { if (!this._options.web) return NaN; switch (asset) { case 'USD': { let response = await (0, _nodeFetch.default)(`https://blockchain.info/tobtc?currency=USD&nosavecurrency=true&time=${time}&value=${amount}`); amount = parseFloat((await response.text()).replace(',', '')); if (isNaN(amount)) { console.log('WARNING: Request to blockchain.info failed: ' + response.text()); return amount; } // Fall through to BTC. } case 'BTC': { let response = await (0, _nodeFetch.default)(`https://blockchain.info/frombtc?currency=CAD&nosavecurrency=true&time=${time}&value=${Math.round(amount * 100000000)}`); amount = parseFloat((await response.text()).replace(',', '')); if (isNaN(amount)) console.log('WARNING: Request to blockchain.info failed: ' + response.text()); return amount; } } return NaN; } /** * Compares a historical record with a time. * @param {object} a The record. * @param {object} b The time. * @returns {number} The result. */ static _compareHistoryTime(a, b) { return a.time - b; } /** * Linearly interpolates between two values. * @param {number} a The first value. * @param {number} b The second value. * @param {number} t The interpolation factor. * @returns {number} The interpolated value. */ static _lerp(a, b, t) { return a * t + b * (1 - t); } } function _default(...args) { return new TradeValueStream(...args); }