@davidosborn/crypto-tax-calculator
Advanced tools
Comparing version 0.0.15 to 0.0.16
@@ -36,7 +36,8 @@ 'use strict'; | ||
* @typedef {object} CapitalGains | ||
* @property {Map.<string, Forward>} [forwardByAsset] The assets that were carried forward from last year. | ||
* @property {array.<Trade>} trades The trades. | ||
* @property {Map.<string, Ledger>} ledgerByAsset The ledger of each asset. | ||
* @property {Disposition} aggregateDisposition The aggregate disposition. | ||
* @property {number} taxableGain The taxable gain (or loss). | ||
* @property {Map.<string, Forward>} [forwardByAsset] The assets that were carried forward from last year. | ||
* @property {array.<Trade>} trades The trades. | ||
* @property {Map.<string, Ledger>} ledgerByAsset The ledger of each asset. | ||
* @property {Map.<string, NegativeBalance>} negativeBalanceByAsset The assets that had a negative balance. | ||
* @property {Disposition} aggregateDisposition The aggregate disposition. | ||
* @property {number} taxableGain The taxable gain (or loss). | ||
*/ | ||
@@ -52,2 +53,16 @@ | ||
/** | ||
* The information about the negative balance of an asset. | ||
* @typedef {object} NegativeBalance | ||
* @property {NegativeBalanceEvent} first The first negative balance. | ||
* @property {NegativeBalanceEvent} minimum The minimum negative balance. | ||
*/ | ||
/** | ||
* The information about an occurrence of a negative balance. | ||
* @typedef {object} NegativeBalanceEvent | ||
* @property {number} balance The balance at the event. | ||
* @property {number} time The time of the event, as a UNIX timestamp. | ||
*/ | ||
/** | ||
* A stream that calculates the capital gains. | ||
@@ -59,3 +74,3 @@ */ | ||
* @param {object} [options] The options. | ||
* @param {Set.<string>} [options.assets] The assets to retain. | ||
* @param {Set.<string>} [options.assets] The assets to consider. | ||
* @param {Map.<string, Forward>} [options.forwardByAsset] The assets to carry forward from last year. | ||
@@ -90,6 +105,6 @@ */ | ||
* The assets that had a negative balance. | ||
* @type {Set} | ||
* @type {Map.<string, number>} | ||
*/ | ||
this._assetsWithNegativeBalance = new Set(); // Initialize the ledger of the assets to carry forward from last year. | ||
this._negativeBalanceByAsset = new Map(); // Initialize the ledger of the assets to carry forward from last year. | ||
@@ -154,6 +169,22 @@ if (this._forwardByAsset) for (let [asset, forward] of this._forwardByAsset.entries()) this._ledgerByAsset.set(asset, { | ||
if (ledger.balance < -0.000000005 && !this._assetsWithNegativeBalance.has(chunk.asset)) { | ||
this._assetsWithNegativeBalance.add(chunk.asset); | ||
if (ledger.balance < -0.000000005) { | ||
let negativeBalance = this._negativeBalanceByAsset.get(chunk.asset); | ||
console.log('WARNING: Encountered a negative balance for ' + chunk.asset + ' on ' + (0, _formatTime.default)(chunk.time) + '.'); | ||
if (negativeBalance === undefined) { | ||
negativeBalance = { | ||
first: { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
}, | ||
minimum: { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
} | ||
}; | ||
this._negativeBalanceByAsset.set(chunk.asset, negativeBalance); | ||
} else if (ledger.balance < negativeBalance.minimum.balance) negativeBalance.minimum = { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
}; | ||
} // Clear the ACB when the balance is negative. | ||
@@ -206,2 +237,3 @@ | ||
ledgerByAsset: this._ledgerByAsset, | ||
negativeBalanceByAsset: this._negativeBalanceByAsset, | ||
aggregateDisposition, | ||
@@ -208,0 +240,0 @@ taxableGain: aggregateDisposition.gain / 2 // Capital gains are taxable at 50%. |
@@ -22,6 +22,12 @@ 'use strict'; | ||
class CapitalGainsFormatStream extends _stream.default.Transform { | ||
constructor() { | ||
/** | ||
* Initializes a new instance. | ||
* @param {object} [options] The options. | ||
* @param {Set.<string>} [options.assets] The assets to show. | ||
*/ | ||
constructor(options) { | ||
super({ | ||
writableObjectMode: true | ||
}); | ||
this._options = options; | ||
this._amountFormat = new Intl.NumberFormat('en-CA', { | ||
@@ -55,5 +61,19 @@ minimumFractionDigits: 8, | ||
_transform(chunk, encoding, callback) { | ||
var _chunk$forwardByAsset; | ||
var _this$_options, _chunk$forwardByAsset; | ||
// Write the balance that was carried forward from last year. | ||
// Filter the assets. | ||
if ((_this$_options = this._options) === null || _this$_options === void 0 ? void 0 : _this$_options.assets) { | ||
// Filter the assets that were carried forward from last year. | ||
for (let asset of chunk.forwardByAsset.keys()) if (!this._options.assets.has(asset)) chunk.forwardByAsset.delete(asset); // Filter the trades. | ||
chunk.trades = chunk.trades.filter(trade => this._options.assets.has(trade.asset)); // Filter the ledgers. | ||
for (let asset of chunk.ledgerByAsset.keys()) if (!this._options.assets.has(asset)) chunk.ledgerByAsset.delete(asset); // Filter the negative balances. | ||
for (let asset of chunk.negativeBalanceByAsset.keys()) if (!this._options.assets.has(asset)) chunk.negativeBalanceByAsset.delete(asset); | ||
} // Write the balance that was carried forward from last year. | ||
if ((_chunk$forwardByAsset = chunk.forwardByAsset) === null || _chunk$forwardByAsset === void 0 ? void 0 : _chunk$forwardByAsset.size) { | ||
@@ -76,3 +96,3 @@ this._pushLine(); | ||
this._pushLine((0, _markdownTable.default)([['Asset', 'Units acquired (or disposed)', 'Value', 'Balance', 'Adjusted cost base', 'Fee', 'Fee asset', 'Time', 'Exchange']].concat(Array.from(chunk.trades, trade => [trade.asset, this._formatAmount(trade.amount), this._formatValue(trade.value), this._formatAmount(trade.balance), this._formatValue(trade.acb), trade.feeAsset ? this._formatAssetAmount(trade.feeAsset, trade.feeAmount) : '', trade.feeAsset ? trade.feeAsset : '', (0, _formatTime.default)(trade.time), trade.exchange])))); // Sort the assets. | ||
this._pushLine((0, _markdownTable.default)([['Asset', 'Units acquired (or disposed)', 'Value', 'Balance', 'Adjusted cost base', 'Fee', 'Fee asset', 'Time', 'Exchange']].concat(Array.from(chunk.trades, trade => [trade.asset, this._formatAmount(trade.amount), this._formatValue(trade.value), this._formatAmount(trade.balance), this._formatValue(trade.acb), trade.feeAsset ? this._formatAssetAmount(trade.feeAsset, trade.feeAmount) : '', trade.feeAsset ? trade.feeAsset : '', (0, _formatTime.default)(trade.time), trade.exchange])))); // Sort the ledgers by asset. | ||
@@ -120,5 +140,21 @@ | ||
this._pushLine((0, _markdownTable.default)([['Field', 'Value'], ['Total proceeds of disposition', this._formatValue(chunk.aggregateDisposition.pod)], ['Total adjusted cost base', this._formatValue(chunk.aggregateDisposition.acb)], ['Total outlays and expenses', this._formatValue(chunk.aggregateDisposition.oae)], ['Total gain (or loss)', this._formatValue(chunk.aggregateDisposition.gain)], ['Taxable gain (or loss)', `**${this._formatValue(chunk.taxableGain)}**`]])); // Find the assets that are carrying a balance. | ||
this._pushLine((0, _markdownTable.default)([['Field', 'Value'], ['Total proceeds of disposition', this._formatValue(chunk.aggregateDisposition.pod)], ['Total adjusted cost base', this._formatValue(chunk.aggregateDisposition.acb)], ['Total outlays and expenses', this._formatValue(chunk.aggregateDisposition.oae)], ['Total gain (or loss)', this._formatValue(chunk.aggregateDisposition.gain)], ['Taxable gain (or loss)', `**${this._formatValue(chunk.taxableGain)}**`]])); // Sort the negative balances by asset. | ||
let negativeBalanceByAsset = Array.from(chunk.negativeBalanceByAsset); | ||
negativeBalanceByAsset.sort(function (a, b) { | ||
return a[0].localeCompare(b[0]); | ||
}); // Write the negative balances. | ||
if (negativeBalanceByAsset.length) { | ||
this._pushLine(); | ||
this._pushLine('## Negative balances'); | ||
this._pushLine(); | ||
this._pushLine((0, _markdownTable.default)([['Asset', 'First negative balance', 'Time of first negative balance', 'Minimum balance', 'Time of minimum balance']].concat(negativeBalanceByAsset.map(([asset, negativeBalance]) => [asset, this._formatAmount(negativeBalance.first.balance), (0, _formatTime.default)(negativeBalance.first.time), this._formatAmount(negativeBalance.minimum.balance), (0, _formatTime.default)(negativeBalance.minimum.time)])))); | ||
} // Find the assets that are carrying a balance. | ||
let ledgerByAssetWithBalance = ledgerByAsset.filter(([asset, ledger]) => ledger.balance < -0.000000005 || ledger.balance >= 0.000000005); // Write the balance specification for next year. | ||
@@ -125,0 +161,0 @@ |
@@ -17,5 +17,6 @@ 'use strict'; | ||
hour: '2-digit', | ||
hour12: false, | ||
hour12: true, | ||
minute: '2-digit', | ||
month: 'short', | ||
second: '2-digit', | ||
year: 'numeric' | ||
@@ -22,0 +23,0 @@ }); |
@@ -88,2 +88,7 @@ 'use strict'; | ||
}, { | ||
short: 's', | ||
long: 'show', | ||
argument: 'spec', | ||
description: 'Only show the specified assets.' | ||
}, { | ||
short: 't', | ||
@@ -126,4 +131,7 @@ long: 'take', | ||
let assets = undefined; | ||
if (opts.options.assets) assets = new Set(opts.options.assets.value.split(',').map(_assets.default.normalizeCode)); // Parse the initial balance and ACB of each asset to carry it forward from last year. | ||
if (opts.options.assets) assets = new Set(opts.options.assets.value.split(',').map(_assets.default.normalizeCode)); // Parse the assets to show when filtering the results. | ||
let show = undefined; | ||
if (opts.options.show) show = new Set(opts.options.show.value.split(',').map(_assets.default.normalizeCode)); // Parse the initial balance and ACB of each asset to carry it forward from last year. | ||
let forwardByAsset = undefined; | ||
@@ -136,3 +144,3 @@ | ||
balance: parseFloat(balance), | ||
acb: parseFloat(acb) | ||
acb: acb != null ? parseFloat(acb) : 0 | ||
}]; | ||
@@ -148,3 +156,3 @@ })); | ||
let stream = (0, _mergeSortStream.default)(_compareTradeTime, sources.map(function (path) { | ||
return _fs.default.createReadStream(path).pipe((0, _toUtf.default)()).pipe((0, _lineStream.default)()).pipe((0, _csvNormalizeStream.default)()).pipe((0, _csvParse.default)({ | ||
return _fs.default.createReadStream(path).pipe((0, _toUtf.default)()).pipe((0, _lineStream.default)('\n')).pipe((0, _csvNormalizeStream.default)()).pipe((0, _csvParse.default)({ | ||
columns: true, | ||
@@ -168,7 +176,9 @@ skip_empty_lines: true | ||
forwardByAsset | ||
})).pipe((0, _capitalGainsFormatStream.default)()), _fs.default.createReadStream(__dirname + '/../res/output_footer.md')]); // Convert the output from Markdown to HTML. | ||
})).pipe((0, _capitalGainsFormatStream.default)({ | ||
assets: show | ||
})), _fs.default.createReadStream(__dirname + '/../res/output_footer.md')]); // Convert the output from Markdown to HTML. | ||
if (opts.options.html) stream = stream.pipe((0, _markedStream.default)()); // Pipe the stream to the output file. | ||
if (!opts.options.silent) stream.pipe(destination ? _fs.default.createWriteStream(destination) : _process.default.stdout); | ||
if (!opts.options.quiet) stream.pipe(destination ? _fs.default.createWriteStream(destination) : _process.default.stdout); | ||
} | ||
@@ -175,0 +185,0 @@ /** |
@@ -25,10 +25,10 @@ 'use strict'; | ||
* @property {string} baseAsset The base currency. | ||
* @property {number} baseAmount The amount of the base currency. | ||
* @property {string} quoteAsset The quote currency. | ||
* @property {number} baseAmount The amount of the base currency. | ||
* @property {number} quoteAmount The amount of the quote currency. | ||
* @property {number} [value] The value of the assets, in Canadian dollars. | ||
* @property {boolean} sell True if the trade represents a sale. | ||
* @property {number} time The time at which the trade occurred, as a UNIX timestamp. | ||
* @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. | ||
@@ -89,16 +89,16 @@ */ | ||
async _transformBinance(chunk) { | ||
let amount = TradeParseStream._parseNumber(chunk.Amount); | ||
let amount = TradeParseStream._parseNumber(chunk['Amount']); | ||
let price = TradeParseStream._parseNumber(chunk.Price); | ||
let price = TradeParseStream._parseNumber(chunk['Price']); | ||
this.push({ | ||
exchange: 'Binance', | ||
baseAsset: _assets.default.normalizeCode(chunk.Market.substring(chunk.Market.length - 3)), | ||
quoteAsset: _assets.default.normalizeCode(chunk.Market.substring(0, chunk.Market.length - 3)), | ||
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, | ||
sell: chunk.Type.includes('SELL'), | ||
feeAsset: _assets.default.normalizeCode(chunk['Fee Coin']), | ||
feeAmount: TradeParseStream._parseNumber(chunk['Fee']), | ||
time: TradeParseStream._parseTime(chunk['Date(UTC)']), | ||
feeAsset: _assets.default.normalizeCode(chunk['Fee Coin']), | ||
feeAmount: TradeParseStream._parseNumber(chunk.Fee) | ||
sell: chunk['Type'].includes('SELL') | ||
}); | ||
@@ -113,3 +113,3 @@ } | ||
async _transformBittrex1(chunk) { | ||
let [baseAsset, quoteAsset] = chunk.Exchange.split('-'); | ||
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-'); | ||
baseAsset = _assets.default.normalizeCode(baseAsset); | ||
@@ -120,9 +120,9 @@ quoteAsset = _assets.default.normalizeCode(quoteAsset); | ||
baseAsset: baseAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Price']), | ||
quoteAsset: quoteAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk.Price), | ||
quoteAmount: TradeParseStream._parseNumber(chunk.Quantity), | ||
sell: chunk.Type.includes('SELL'), | ||
time: TradeParseStream._parseTime(chunk.Closed), | ||
quoteAmount: TradeParseStream._parseNumber(chunk['Quantity']), | ||
feeAsset: baseAsset, | ||
feeAmount: TradeParseStream._parseNumber(chunk.CommissionPaid) | ||
feeAmount: TradeParseStream._parseNumber(chunk['CommissionPaid']), | ||
time: TradeParseStream._parseTime(chunk['Closed']), | ||
sell: chunk['Type'].includes('SELL') | ||
}); | ||
@@ -137,9 +137,9 @@ } | ||
async _transformBittrex2(chunk) { | ||
let [baseAsset, quoteAsset] = chunk.Exchange.split('-'); | ||
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-'); | ||
baseAsset = _assets.default.normalizeCode(baseAsset); | ||
quoteAsset = _assets.default.normalizeCode(quoteAsset); | ||
let quantity = TradeParseStream._parseNumber(chunk.Quantity); | ||
let quantity = TradeParseStream._parseNumber(chunk['Quantity']); | ||
let quantityRemaining = TradeParseStream._parseNumber(chunk.QuantityRemaining); | ||
let quantityRemaining = TradeParseStream._parseNumber(chunk['QuantityRemaining']); | ||
@@ -149,9 +149,9 @@ this.push({ | ||
baseAsset: baseAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Price']), | ||
quoteAsset: quoteAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk.Price), | ||
quoteAmount: quantity - quantityRemaining, | ||
sell: chunk.OrderType.includes('SELL'), | ||
time: TradeParseStream._parseTime(chunk.TimeStamp), | ||
feeAsset: baseAsset, | ||
feeAmount: TradeParseStream._parseNumber(chunk.Commission) | ||
feeAmount: TradeParseStream._parseNumber(chunk['Commission']), | ||
time: TradeParseStream._parseTime(chunk['TimeStamp']), | ||
sell: chunk['OrderType'].includes('SELL') | ||
}); | ||
@@ -168,3 +168,3 @@ } | ||
if (chunk.type !== 'trade') { | ||
if (chunk['type'] !== 'trade') { | ||
if (chunks.length > 0) { | ||
@@ -180,6 +180,6 @@ console.log('WARNING: Found unpaired trade 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. | ||
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. | ||
@@ -200,9 +200,9 @@ }; | ||
baseAsset: baseChunk.asset, | ||
baseAmount: Math.abs(baseChunk.amount), | ||
quoteAsset: quoteChunk.asset, | ||
baseAmount: Math.abs(baseChunk.amount), | ||
quoteAmount: Math.abs(quoteChunk.amount), | ||
sell: baseChunk.amount > 0, | ||
feeAsset: baseChunk.asset, | ||
feeAmount: baseChunk.fee, | ||
time: baseChunk.time, | ||
feeAsset: baseChunk.asset, | ||
feeAmount: baseChunk.fee | ||
sell: baseChunk.amount > 0 || quoteChunk.amount < 0 | ||
}); | ||
@@ -222,29 +222,62 @@ chunks.length = 0; | ||
if (buySell !== 'Buy' && buySell !== 'Sell') return; | ||
let [quoteAsset, baseAsset] = chunk.Coin.split('/'); | ||
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 [amount, amountAsset] = chunk.Amount.match(splitAmountAssetRegExp).slice(1); | ||
let [feeAmount, feeAsset] = chunk.Fee.match(splitAmountAssetRegExp).slice(1); | ||
amountAsset = _assets.default.normalizeCode(amountAsset); | ||
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 (amountAsset !== quoteAsset) { | ||
console.log('WARNING: Expected amount of ' + quoteAsset + ' but found ' + amountAsset + ' instead.'); | ||
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, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Filled Price']), | ||
quoteAmount: TradeParseStream._parseNumber(chunk.Amount), | ||
sell: buySell === 'Sell', | ||
time: TradeParseStream._parseTime(chunk.Time), | ||
feeAsset: baseAsset, | ||
feeAmount: feeAmount | ||
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. | ||
@@ -277,3 +310,4 @@ * @param {string} s The string. | ||
'txid|refid|time|type|aclass|asset|amount|fee|balance': TradeParseStream.prototype._transformKraken, | ||
'Coin|Time|Buy/Sell|Filled Price|Amount|Fee|Volume': TradeParseStream.prototype._transformKuCoin | ||
'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 | ||
/** | ||
@@ -280,0 +314,0 @@ * Initializes a new instance. |
@@ -58,3 +58,3 @@ 'use strict'; | ||
chunk.feeValue = chunk.feeAsset === chunk.baseAsset ? chunk.value * chunk.feeAmount / chunk.baseAmount : chunk.feeAsset === chunk.quoteAsset ? chunk.value * chunk.feeAmount / chunk.quoteAmount : await this._getValue(chunk.feeAsset, chunk.feeAmount, chunk.time); | ||
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); | ||
@@ -61,0 +61,0 @@ callback(); |
{ | ||
"name": "@davidosborn/crypto-tax-calculator", | ||
"version": "0.0.15", | ||
"version": "0.0.16", | ||
"description": "A tool to calculate the capital gains of cryptocurrency assets for Canadian taxes", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
The calculation can be limited to a subset of the available assets by the '--assets' option. | ||
The argument is a comma-separated list of assets. | ||
A subset of the available assets can be considered for the calculation by the '--assets' option, | ||
or used to filter the results by the '--show' option. | ||
The argument in both cases is a comma-separated list of assets. | ||
The balances can be carried forward from last year by the '--init' option. | ||
The argument is a comma-separated list of assets, each with its balance and adjusted cost base, | ||
in the form of '<asset>:<balance>:<acb>,...', such as '--init=BTC:1:5000,ETH:5:1000,LTC:10:80'. | ||
in the form of '<asset>:<balance>[:<acb>],...', such as '--init=BTC:1:5000,ETH:5:1000,LTC:10:80'. |
@@ -26,7 +26,8 @@ 'use strict' | ||
* @typedef {object} CapitalGains | ||
* @property {Map.<string, Forward>} [forwardByAsset] The assets that were carried forward from last year. | ||
* @property {array.<Trade>} trades The trades. | ||
* @property {Map.<string, Ledger>} ledgerByAsset The ledger of each asset. | ||
* @property {Disposition} aggregateDisposition The aggregate disposition. | ||
* @property {number} taxableGain The taxable gain (or loss). | ||
* @property {Map.<string, Forward>} [forwardByAsset] The assets that were carried forward from last year. | ||
* @property {array.<Trade>} trades The trades. | ||
* @property {Map.<string, Ledger>} ledgerByAsset The ledger of each asset. | ||
* @property {Map.<string, NegativeBalance>} negativeBalanceByAsset The assets that had a negative balance. | ||
* @property {Disposition} aggregateDisposition The aggregate disposition. | ||
* @property {number} taxableGain The taxable gain (or loss). | ||
*/ | ||
@@ -39,2 +40,14 @@ /** | ||
*/ | ||
/** | ||
* The information about the negative balance of an asset. | ||
* @typedef {object} NegativeBalance | ||
* @property {NegativeBalanceEvent} first The first negative balance. | ||
* @property {NegativeBalanceEvent} minimum The minimum negative balance. | ||
*/ | ||
/** | ||
* The information about an occurrence of a negative balance. | ||
* @typedef {object} NegativeBalanceEvent | ||
* @property {number} balance The balance at the event. | ||
* @property {number} time The time of the event, as a UNIX timestamp. | ||
*/ | ||
@@ -48,3 +61,3 @@ /** | ||
* @param {object} [options] The options. | ||
* @param {Set.<string>} [options.assets] The assets to retain. | ||
* @param {Set.<string>} [options.assets] The assets to consider. | ||
* @param {Map.<string, Forward>} [options.forwardByAsset] The assets to carry forward from last year. | ||
@@ -79,5 +92,5 @@ */ | ||
* The assets that had a negative balance. | ||
* @type {Set} | ||
* @type {Map.<string, number>} | ||
*/ | ||
this._assetsWithNegativeBalance = new Set | ||
this._negativeBalanceByAsset = new Map | ||
@@ -151,5 +164,22 @@ // Initialize the ledger of the assets to carry forward from last year. | ||
// Check whether the balance is negative, which would indicate an accounting error. | ||
if (ledger.balance < -0.000000005 && !this._assetsWithNegativeBalance.has(chunk.asset)) { | ||
this._assetsWithNegativeBalance.add(chunk.asset) | ||
console.log('WARNING: Encountered a negative balance for ' + chunk.asset + ' on ' + formatTime(chunk.time) + '.') | ||
if (ledger.balance < -0.000000005) { | ||
let negativeBalance = this._negativeBalanceByAsset.get(chunk.asset) | ||
if (negativeBalance === undefined) { | ||
negativeBalance = { | ||
first: { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
}, | ||
minimum: { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
} | ||
} | ||
this._negativeBalanceByAsset.set(chunk.asset, negativeBalance) | ||
} | ||
else if (ledger.balance < negativeBalance.minimum.balance) | ||
negativeBalance.minimum = { | ||
balance: ledger.balance, | ||
time: chunk.time | ||
} | ||
} | ||
@@ -210,2 +240,3 @@ | ||
ledgerByAsset: this._ledgerByAsset, | ||
negativeBalanceByAsset: this._negativeBalanceByAsset, | ||
aggregateDisposition, | ||
@@ -212,0 +243,0 @@ taxableGain: aggregateDisposition.gain / 2 // Capital gains are taxable at 50%. |
@@ -12,3 +12,8 @@ 'use strict' | ||
class CapitalGainsFormatStream extends stream.Transform { | ||
constructor() { | ||
/** | ||
* Initializes a new instance. | ||
* @param {object} [options] The options. | ||
* @param {Set.<string>} [options.assets] The assets to show. | ||
*/ | ||
constructor(options) { | ||
super({ | ||
@@ -18,2 +23,4 @@ writableObjectMode: true | ||
this._options = options | ||
this._amountFormat = new Intl.NumberFormat('en-CA', { | ||
@@ -49,2 +56,23 @@ minimumFractionDigits: 8, | ||
_transform(chunk, encoding, callback) { | ||
// Filter the assets. | ||
if (this._options?.assets) { | ||
// Filter the assets that were carried forward from last year. | ||
for (let asset of chunk.forwardByAsset.keys()) | ||
if (!this._options.assets.has(asset)) | ||
chunk.forwardByAsset.delete(asset) | ||
// Filter the trades. | ||
chunk.trades = chunk.trades.filter((trade) => this._options.assets.has(trade.asset)) | ||
// Filter the ledgers. | ||
for (let asset of chunk.ledgerByAsset.keys()) | ||
if (!this._options.assets.has(asset)) | ||
chunk.ledgerByAsset.delete(asset) | ||
// Filter the negative balances. | ||
for (let asset of chunk.negativeBalanceByAsset.keys()) | ||
if (!this._options.assets.has(asset)) | ||
chunk.negativeBalanceByAsset.delete(asset) | ||
} | ||
// Write the balance that was carried forward from last year. | ||
@@ -96,3 +124,3 @@ if (chunk.forwardByAsset?.size) { | ||
// Sort the assets. | ||
// Sort the ledgers by asset. | ||
let ledgerByAsset = Array.from(chunk.ledgerByAsset) | ||
@@ -188,2 +216,30 @@ ledgerByAsset.sort(function(a, b) { | ||
// Sort the negative balances by asset. | ||
let negativeBalanceByAsset = Array.from(chunk.negativeBalanceByAsset) | ||
negativeBalanceByAsset.sort(function(a, b) { | ||
return a[0].localeCompare(b[0]) | ||
}) | ||
// Write the negative balances. | ||
if (negativeBalanceByAsset.length) { | ||
this._pushLine() | ||
this._pushLine('## Negative balances') | ||
this._pushLine() | ||
this._pushLine(markdownTable( | ||
[[ | ||
'Asset', | ||
'First negative balance', | ||
'Time of first negative balance', | ||
'Minimum balance', | ||
'Time of minimum balance' | ||
]] | ||
.concat(negativeBalanceByAsset.map(([asset, negativeBalance]) => [ | ||
asset, | ||
this._formatAmount(negativeBalance.first.balance), | ||
formatTime(negativeBalance.first.time), | ||
this._formatAmount(negativeBalance.minimum.balance), | ||
formatTime(negativeBalance.minimum.time) | ||
])))) | ||
} | ||
// Find the assets that are carrying a balance. | ||
@@ -190,0 +246,0 @@ let ledgerByAssetWithBalance = ledgerByAsset |
@@ -13,5 +13,6 @@ 'use strict' | ||
hour: '2-digit', | ||
hour12: false, | ||
hour12: true, | ||
minute: '2-digit', | ||
month: 'short', | ||
second: '2-digit', | ||
year: 'numeric' | ||
@@ -18,0 +19,0 @@ }) |
@@ -64,2 +64,8 @@ 'use strict' | ||
{ | ||
short: 's', | ||
long: 'show', | ||
argument: 'spec', | ||
description: 'Only show the specified assets.' | ||
}, | ||
{ | ||
short: 't', | ||
@@ -112,2 +118,7 @@ long: 'take', | ||
// Parse the assets to show when filtering the results. | ||
let show = undefined | ||
if (opts.options.show) | ||
show = new Set(opts.options.show.value.split(',').map(Assets.normalizeCode)) | ||
// Parse the initial balance and ACB of each asset to carry it forward from last year. | ||
@@ -121,3 +132,3 @@ let forwardByAsset = undefined | ||
balance: parseFloat(balance), | ||
acb: parseFloat(acb) | ||
acb: acb != null ? parseFloat(acb) : 0 | ||
}] | ||
@@ -137,3 +148,3 @@ })) | ||
.pipe(utf8()) | ||
.pipe(lineStream()) | ||
.pipe(lineStream('\n')) | ||
.pipe(csvNormalizeStream()) | ||
@@ -172,3 +183,5 @@ .pipe(csvParse({ | ||
})) | ||
.pipe(capitalGainsFormatStream()), | ||
.pipe(capitalGainsFormatStream({ | ||
assets: show | ||
})), | ||
fs.createReadStream(__dirname + '/../res/output_footer.md') | ||
@@ -182,3 +195,3 @@ ]) | ||
// Pipe the stream to the output file. | ||
if (!opts.options.silent) | ||
if (!opts.options.quiet) | ||
stream.pipe(destination ? fs.createWriteStream(destination) : process.stdout) | ||
@@ -185,0 +198,0 @@ } |
@@ -13,10 +13,10 @@ 'use strict' | ||
* @property {string} baseAsset The base currency. | ||
* @property {number} baseAmount The amount of the base currency. | ||
* @property {string} quoteAsset The quote currency. | ||
* @property {number} baseAmount The amount of the base currency. | ||
* @property {number} quoteAmount The amount of the quote currency. | ||
* @property {number} [value] The value of the assets, in Canadian dollars. | ||
* @property {boolean} sell True if the trade represents a sale. | ||
* @property {number} time The time at which the trade occurred, as a UNIX timestamp. | ||
* @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. | ||
@@ -38,3 +38,4 @@ */ | ||
'txid|refid|time|type|aclass|asset|amount|fee|balance': TradeParseStream.prototype._transformKraken, | ||
'Coin|Time|Buy/Sell|Filled Price|Amount|Fee|Volume': TradeParseStream.prototype._transformKuCoin | ||
'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 | ||
} | ||
@@ -90,15 +91,15 @@ | ||
async _transformBinance(chunk) { | ||
let amount = TradeParseStream._parseNumber(chunk.Amount) | ||
let price = TradeParseStream._parseNumber(chunk.Price) | ||
let amount = TradeParseStream._parseNumber(chunk['Amount']) | ||
let price = TradeParseStream._parseNumber(chunk['Price']) | ||
this.push({ | ||
exchange: 'Binance', | ||
baseAsset: Assets.normalizeCode(chunk.Market.substring(chunk.Market.length - 3)), | ||
quoteAsset: Assets.normalizeCode(chunk.Market.substring(0, chunk.Market.length - 3)), | ||
baseAsset: Assets.normalizeCode(chunk['Market'].substring(chunk['Market'].length - 3)), | ||
baseAmount: amount * price, | ||
quoteAsset: Assets.normalizeCode(chunk['Market'].substring(0, chunk['Market'].length - 3)), | ||
quoteAmount: amount, | ||
sell: chunk.Type.includes('SELL'), | ||
feeAsset: Assets.normalizeCode(chunk['Fee Coin']), | ||
feeAmount: TradeParseStream._parseNumber(chunk['Fee']), | ||
time: TradeParseStream._parseTime(chunk['Date(UTC)']), | ||
feeAsset: Assets.normalizeCode(chunk['Fee Coin']), | ||
feeAmount: TradeParseStream._parseNumber(chunk.Fee) | ||
sell: chunk['Type'].includes('SELL') | ||
}) | ||
@@ -112,3 +113,3 @@ } | ||
async _transformBittrex1(chunk) { | ||
let [baseAsset, quoteAsset] = chunk.Exchange.split('-') | ||
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-') | ||
baseAsset = Assets.normalizeCode(baseAsset) | ||
@@ -120,9 +121,9 @@ quoteAsset = Assets.normalizeCode(quoteAsset) | ||
baseAsset: baseAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Price']), | ||
quoteAsset: quoteAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk.Price), | ||
quoteAmount: TradeParseStream._parseNumber(chunk.Quantity), | ||
sell: chunk.Type.includes('SELL'), | ||
time: TradeParseStream._parseTime(chunk.Closed), | ||
quoteAmount: TradeParseStream._parseNumber(chunk['Quantity']), | ||
feeAsset: baseAsset, | ||
feeAmount: TradeParseStream._parseNumber(chunk.CommissionPaid) | ||
feeAmount: TradeParseStream._parseNumber(chunk['CommissionPaid']), | ||
time: TradeParseStream._parseTime(chunk['Closed']), | ||
sell: chunk['Type'].includes('SELL') | ||
}) | ||
@@ -136,8 +137,8 @@ } | ||
async _transformBittrex2(chunk) { | ||
let [baseAsset, quoteAsset] = chunk.Exchange.split('-') | ||
let [baseAsset, quoteAsset] = chunk['Exchange'].split('-') | ||
baseAsset = Assets.normalizeCode(baseAsset) | ||
quoteAsset = Assets.normalizeCode(quoteAsset) | ||
let quantity = TradeParseStream._parseNumber(chunk.Quantity) | ||
let quantityRemaining = TradeParseStream._parseNumber(chunk.QuantityRemaining) | ||
let quantity = TradeParseStream._parseNumber(chunk['Quantity']) | ||
let quantityRemaining = TradeParseStream._parseNumber(chunk['QuantityRemaining']) | ||
@@ -147,9 +148,9 @@ this.push({ | ||
baseAsset: baseAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Price']), | ||
quoteAsset: quoteAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk.Price), | ||
quoteAmount: quantity - quantityRemaining, | ||
sell: chunk.OrderType.includes('SELL'), | ||
time: TradeParseStream._parseTime(chunk.TimeStamp), | ||
feeAsset: baseAsset, | ||
feeAmount: TradeParseStream._parseNumber(chunk.Commission) | ||
feeAmount: TradeParseStream._parseNumber(chunk['Commission']), | ||
time: TradeParseStream._parseTime(chunk['TimeStamp']), | ||
sell: chunk['OrderType'].includes('SELL') | ||
}) | ||
@@ -166,3 +167,3 @@ } | ||
// We only care about trades. | ||
if (chunk.type !== 'trade') { | ||
if (chunk['type'] !== 'trade') { | ||
if (chunks.length > 0) { | ||
@@ -177,6 +178,6 @@ console.log('WARNING: Found unpaired trade chunk.') | ||
chunk = { | ||
asset: Assets.normalizeCode(chunk.asset), | ||
amount: TradeParseStream._parseNumber(chunk.amount), | ||
time: TradeParseStream._parseTime(chunk.time), | ||
fee: TradeParseStream._parseNumber(chunk.fee) | ||
asset: Assets.normalizeCode(chunk['asset']), | ||
amount: TradeParseStream._parseNumber(chunk['amount']), | ||
time: TradeParseStream._parseTime(chunk['time']), | ||
fee: TradeParseStream._parseNumber(chunk['fee']) | ||
} | ||
@@ -200,9 +201,9 @@ | ||
baseAsset: baseChunk.asset, | ||
baseAmount: Math.abs(baseChunk.amount), | ||
quoteAsset: quoteChunk.asset, | ||
baseAmount: Math.abs(baseChunk.amount), | ||
quoteAmount: Math.abs(quoteChunk.amount), | ||
sell: baseChunk.amount > 0, | ||
feeAsset: baseChunk.asset, | ||
feeAmount: baseChunk.fee, | ||
time: baseChunk.time, | ||
feeAsset: baseChunk.asset, | ||
feeAmount: baseChunk.fee | ||
sell: baseChunk.amount > 0 || quoteChunk.amount < 0 | ||
}) | ||
@@ -224,3 +225,3 @@ | ||
let [quoteAsset,baseAsset] = chunk.Coin.split('/') | ||
let [quoteAsset, baseAsset] = chunk['Coin'].split('/') | ||
baseAsset = Assets.normalizeCode(baseAsset) | ||
@@ -231,12 +232,18 @@ quoteAsset = Assets.normalizeCode(quoteAsset) | ||
let [amount, amountAsset] = chunk.Amount.match(splitAmountAssetRegExp).slice(1) | ||
let [feeAmount, feeAsset] = chunk.Fee.match(splitAmountAssetRegExp).slice(1) | ||
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) | ||
amountAsset = Assets.normalizeCode(amountAsset) | ||
baseAmountAsset = Assets.normalizeCode(baseAmountAsset) | ||
quoteAmountAsset = Assets.normalizeCode(quoteAmountAsset) | ||
feeAsset = Assets.normalizeCode(feeAsset) | ||
if (amountAsset !== quoteAsset) { | ||
console.log('WARNING: Expected amount of ' + quoteAsset + ' but found ' + amountAsset + ' instead.') | ||
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 | ||
} | ||
@@ -246,9 +253,9 @@ this.push({ | ||
baseAsset: baseAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Volume']), | ||
quoteAsset: quoteAsset, | ||
baseAmount: TradeParseStream._parseNumber(chunk['Filled Price']), | ||
quoteAmount: TradeParseStream._parseNumber(chunk.Amount), | ||
sell: buySell === 'Sell', | ||
time: TradeParseStream._parseTime(chunk.Time), | ||
feeAsset: baseAsset, | ||
feeAmount: feeAmount | ||
quoteAmount: TradeParseStream._parseNumber(chunk['Amount']), | ||
feeAsset: feeAsset, | ||
feeAmount: feeAmount, | ||
time: TradeParseStream._parseTime(chunk['Time']), | ||
sell: buySell === 'Sell' | ||
}) | ||
@@ -258,2 +265,27 @@ } | ||
/** | ||
* 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.normalizeCode(chunk['Base asset']), | ||
baseAmount: Math.abs(baseAmount), | ||
quoteAsset: Assets.normalizeCode(chunk['Quote asset']), | ||
quoteAmount: Math.abs(quoteAmount), | ||
feeAsset: Assets.normalizeCode(chunk['Fee asset']), | ||
feeAmount: TradeParseStream._parseNumber(chunk['Fee amount']), | ||
time: TradeParseStream._parseTime(chunk['Time']), | ||
sell: baseAmount > 0 || quoteAmount < 0 | ||
}) | ||
} | ||
/** | ||
* Parses a number. | ||
@@ -260,0 +292,0 @@ * @param {string} s The string. |
@@ -46,4 +46,5 @@ 'use strict' | ||
chunk.feeValue = ( | ||
chunk.feeAsset === chunk.baseAsset ? chunk.value * chunk.feeAmount / chunk.baseAmount : | ||
chunk.feeAsset === chunk.quoteAsset ? chunk.value * chunk.feeAmount / chunk.quoteAmount : | ||
!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)) | ||
@@ -50,0 +51,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
107437
2749