Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@davidosborn/crypto-tax-calculator

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@davidosborn/crypto-tax-calculator - npm Package Compare versions

Comparing version 0.0.15 to 0.0.16

54

lib/capital-gains-calculate-stream.js

@@ -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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc