@davidosborn/crypto-tax-calculator
Version:
A tool to calculate the capital gains of cryptocurrency assets for Canadian taxes
204 lines (176 loc) • 6.3 kB
JavaScript
import bounds from 'binary-search-bounds'
import fs from 'fs'
import fetch from 'node-fetch'
import stream from 'stream'
import util from 'util'
import formatTime from './format-time'
/**
* A stream that calculates the value of each trade.
*/
class TradeValueStream extends stream.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) {
// Log the trade.
if (this._options?.verbose) {
console.log('Trade ' + chunk.baseAsset + '/' + chunk.quoteAsset + ' on ' + formatTime(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 ' + formatTime(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 = bounds.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 fetch(`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 fetch(`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)
}
}
export default function(...args) {
return new TradeValueStream(...args)
}