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