UNPKG

@davidosborn/crypto-tax-calculator

Version:

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

298 lines (267 loc) 8.55 kB
'use strict' import markdownTable from 'markdown-table' import stream from 'stream' import Assets from './assets' import formatTime from './format-time' /** * A stream that formats the capital gains for the user. */ class CapitalGainsFormatStream extends stream.Transform { /** * 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', { minimumFractionDigits: 8, maximumFractionDigits: 8, useGrouping: false }) this._amountFormatNoTrailingZeros = new Intl.NumberFormat('en-CA', { maximumFractionDigits: 8, useGrouping: false }) this._valueFormat = new Intl.NumberFormat('en-CA', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) this._valueFormatNoGrouping = new Intl.NumberFormat('en-CA', { minimumFractionDigits: 2, maximumFractionDigits: 2, useGrouping: false }) } /** * Formats the capital gains for the user. * @param {CapitalGains} chunk The capital gains. * @param {string} encoding The encoding type (always 'Buffer'). * @param {function} callback A callback for when the transformation is complete. */ _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. if (chunk.forwardByAsset?.size) { this._pushLine() this._pushLine('## Carried forward from last year') this._pushLine() this._pushLine(markdownTable( [[ 'Asset', 'Balance', 'Adjusted cost base' ]] .concat(Array.from(chunk.forwardByAsset, ([asset, forward]) => [ asset, this._formatAmount(forward.balance), this._formatValue(forward.acb) ])))) } // Write the trades. this._pushLine() this._pushLine('## Trades') this._pushLine() this._pushLine(markdownTable( [[ '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 : '', formatTime(trade.time), trade.exchange ])))) // Sort the ledgers by asset. let ledgerByAsset = Array.from(chunk.ledgerByAsset) ledgerByAsset.sort(function(a, b) { return a[0].localeCompare(b[0]) }) // Write the dispositions. this._pushLine() this._pushLine('## Dispositions') this._pushLine() this._pushLine(markdownTable( [[ 'Asset', 'Units', 'Proceeds of disposition', 'Adjusted cost base', 'Outlays and expenses', 'Gain (or loss)', 'Time', 'Exchange' ]] .concat(...ledgerByAsset.map(([asset, ledger]) => Array.from(ledger.dispositions, disposition => [ asset, this._formatAmount(disposition.amount), this._formatValue(disposition.pod), this._formatValue(disposition.acb), this._formatValue(disposition.oae), this._formatValue(disposition.gain), formatTime(disposition.time), disposition.exchange ]) )))) // Find the assets with dispositions. let ledgerByAssetWithDisposition = ledgerByAsset .filter(([asset, ledger]) => ledger.dispositions.length) // Write the aggregate disposition per asset. this._pushLine() this._pushLine('## Aggregate disposition per asset') this._pushLine() this._pushLine(markdownTable( [[ 'Asset', 'Units', 'Proceeds of disposition', 'Adjusted cost base', 'Outlays and expenses', 'Gain (or loss)' ]] .concat(ledgerByAssetWithDisposition.map(([asset, ledger]) => [ asset, this._formatAmount(ledger.aggregateDisposition.amount), this._formatValue(ledger.aggregateDisposition.pod), this._formatValue(ledger.aggregateDisposition.acb), this._formatValue(ledger.aggregateDisposition.oae), this._formatValue(ledger.aggregateDisposition.gain) ])))) // Write the summary per asset. this._pushLine() this._pushLine('## Summary per asset') this._pushLine() this._pushLine(markdownTable( [[ 'Asset', 'Gain (or loss)', 'Balance', 'Adjusted cost base', ]] .concat(ledgerByAsset.map(([asset, ledger]) => [ asset, this._formatValue(ledger.aggregateDisposition.gain), this._formatAmount(ledger.balance), this._formatValue(ledger.acb) ])))) // Write the summary. this._pushLine() this._pushLine('## Summary') this._pushLine() this._pushLine(markdownTable([ ['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(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. let ledgerByAssetWithBalance = ledgerByAsset .filter(([asset, ledger]) => ledger.balance < -0.000000005 || ledger.balance >= 0.000000005) // Write the balance specification for next year. if (ledgerByAssetWithBalance.length) { this._pushLine() this._pushLine('## Carry forward to the next year') this._pushLine() this._pushLine('The following specification can be passed to the calculator next year to carry forward this year\'s balance and adjusted cost base.') this._pushLine() this._pushLine('```') this._pushLine('--init=\\') for (let [asset, ledger] of ledgerByAssetWithBalance) { let last = asset === ledgerByAssetWithBalance[ledgerByAssetWithBalance.length - 1][0] let balance = this._amountFormatNoTrailingZeros.format(ledger.balance) let acb = this._valueFormatNoGrouping.format(ledger.acb) this._pushLine(asset + ':' + balance + ':' + acb + (last ? '' : ',\\')) } this._pushLine('```') } this._pushLine() callback() } _formatAssetAmount(asset, amount) { return Assets.getPriority(asset) !== 0 ? this._formatAmount(amount) : this._formatValue(amount) } _formatAmount(amount) { let amountString = this._amountFormat.format(Math.abs(amount)) if (amount < -0.000000005) amountString = `(${amountString})` return amountString } _formatValue(value) { let valueString = this._valueFormat.format(Math.abs(value)) if (value < -0.005) valueString = `(${valueString})` return '$' + valueString } _pushLine(line) { this.push(line) this.push('\n') } } export default function(...args) { return new CapitalGainsFormatStream(...args) }