commit 0c7e6ac609b37895ab0a6e9825d40fdd4ea9621d Author: Matt Hill Date: Sat Jul 1 17:33:43 2023 -0600 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b542593 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +*.csv +*.pdf +!2022-f8949.pdf \ No newline at end of file diff --git a/2022-f8949.pdf b/2022-f8949.pdf new file mode 100644 index 0000000..34995ab Binary files /dev/null and b/2022-f8949.pdf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/fifo/index.js b/fifo/index.js new file mode 100644 index 0000000..8b944b6 --- /dev/null +++ b/fifo/index.js @@ -0,0 +1,113 @@ +const csvToJson = require('convert-csv-to-json') +const converter = require('json-2-csv') +const fs = require('fs') +const BN = require('bignumber.js') + +const txs = csvToJson.fieldDelimiter(',').getJsonFromCsv(`../Ledger.csv`) +let utxos = [] +let dispositions = [] + +const handleBuy = (tx, i) => { + const { Date: date, Asset, Received, Price } = tx + utxos.push({ + Date: date, + Id: i, + Asset, + Amount: Received, + Price, + Remaining: new BN(Received), + }) +} + +const createDisposition = (tx, utxo, amount) => { + const st = new Date(tx.Date).valueOf() - new Date(utxo.Date).valueOf() < 31556926000 + const costBasis = amount.times(utxo.Price) + const proceeds = amount.times(tx.Price) + const gainLoss = proceeds.minus(costBasis) + const disposition = { + Date: tx.Date, + Asset: tx.Asset, + Utxo: utxo.Id, + Amount: amount.toFixed(8), + PurchasePrice: utxo.Price, + DateAcquired: utxo.Date, + SalePrice: tx.Price, + CostBasis: costBasis.toFixed(2), + Proceeds: proceeds.toFixed(2), + GainLoss: gainLoss.toFixed(2), + ShortTerm: st ? 'true' : '', + Tax: gainLoss.gt(0) ? gainLoss.times(st ? .24 : .15).toFixed(2) : 0 + } + dispositions.push(disposition) +} + +const consumeUtxos = (tx) => { + for (let i = 0; i < utxos.length; i++) { + let utxo = utxos[i] + + if (!utxo) { + console.error(`WARNING: No utxo available! Assuming $0 cost basis:\n-Date:${tx.Date}\n-Asset: ${tx.Asset}\n-Sent: ${tx.Sent}\n-Remaining: ${tx.Remaining.toFixed(8)}\n`) + utxo = { + Date: '2017-01-01', + Id: -1, + Asset: tx.Asset, + Amount: tx.Remaining.toFixed(8), + Price: '0', + Remaining: tx.Remaining, + } + } + + if (utxo.Asset !== tx.Asset || utxo.Remaining.isZero()) continue + + if (utxo.Remaining.gte(tx.Remaining)) { + createDisposition(tx, utxo, tx.Remaining) + utxo.Remaining = utxo.Remaining.minus(tx.Remaining) + return + } else { + createDisposition(tx, utxo, utxo.Remaining) + tx.Remaining = tx.Remaining.minus(utxo.Remaining) + utxo.Remaining = new BN(0) + return consumeUtxos(tx) + } + } +} + +txs.forEach((tx, i) => { + if (tx.Received) return handleBuy(tx, i) + tx.Remaining = new BN(tx.Sent) + return consumeUtxos(tx) +}) + +let balances = utxos.reduce((prev, curr) => { + const asset = curr.Asset + if (prev[asset]) { + prev[asset] = prev[asset].plus(curr.Remaining) + } else { + prev[asset] = curr.Remaining + } + return prev +}, {}) + +utxos = utxos.map(u => ({ + ...u, + Remaining: u.Remaining.toFixed(8), +})) + +balances = Object.keys(balances) + .filter(asset => !balances[asset].isZero()) + .map(asset => ({ + Asset: asset, + balance: balances[asset].toFixed(8) + })) + +converter.json2csv(utxos).then(csv => + fs.appendFileSync(`./utxos.csv`, csv) +) + +converter.json2csv(dispositions).then(csv => + fs.appendFileSync(`./dispositions.csv`, csv) +) + +converter.json2csv(balances).then(csv => + fs.appendFileSync(`./balances.csv`, csv) +) diff --git a/hifo/index.js b/hifo/index.js new file mode 100644 index 0000000..40964a7 --- /dev/null +++ b/hifo/index.js @@ -0,0 +1,228 @@ +const csvToJson = require('convert-csv-to-json') +const converter = require('json-2-csv') +const fs = require('fs') +const BN = require('bignumber.js') +const { PDFDocument } = require('pdf-lib') + +const txs = csvToJson.fieldDelimiter(',').getJsonFromCsv(`../Ledger.csv`) +let utxos = [] +let dispositions = [] + +const handleBuy = (tx, i) => { + const { Date: date, Asset, Received, Price } = tx + utxos.push({ + Date: date, + Id: i, + Asset, + Amount: Received, + Price, + Remaining: new BN(Received), + }) +} + +const createDisposition = (tx, utxo, amount, st) => { + const costBasis = amount.times(utxo.Price) + const proceeds = amount.times(tx.Price) + const gainLoss = proceeds.minus(costBasis) + return { + Date: tx.Date, + Asset: tx.Asset, + Utxo: utxo.Id, + Amount: amount.toFixed(8), + PurchasePrice: utxo.Price, + DateAcquired: utxo.Date, + SalePrice: tx.Price, + CostBasis: costBasis.toFixed(2), + Proceeds: proceeds.toFixed(2), + GainLoss: gainLoss.toFixed(2), + ShortTerm: st ? 'true' : '', + Tax: gainLoss.gt(0) ? gainLoss.times(st ? .24 : .15).toFixed(2) : 0 + } +} + +const consumeUtxos = (tx) => { + const copy = [] + for (i = 0; i < utxos.length; i++) { + copy[i] = utxos[i]; + } + + const sorted = copy + .filter(utxo => utxo.Asset === tx.Asset && !utxo.Remaining.isZero()) + .sort((a, b) => new BN(b.Price).minus(a.Price).toNumber()) + + const txDate = new Date(tx.Date).valueOf() + + const stUtxo = sorted.find(utxo => txDate - new Date(utxo.Date).valueOf() < 31556926000) + const ltUtxo = sorted.find(utxo => txDate - new Date(utxo.Date).valueOf() >= 31556926000) + + // find the best utxo + let utxo + // decide st vs lt + let st + + if (!stUtxo && !ltUtxo) { + console.error(`WARNING: No utxo available! Assuming $0 cost basis:\n-Date:${tx.Date}\n-Asset: ${tx.Asset}\n-Sent: ${tx.Sent}\n-Remaining: ${tx.Remaining.toFixed(8)}\n`) + utxo = { + Date: '2017-01-01T12:00:00', + Id: -1, + Asset: tx.Asset, + Amount: tx.Remaining.toFixed(8), + Price: '0', + Remaining: tx.Remaining, + } + } else if (!stUtxo) { + utxo = utxos.find(u => u.Id === ltUtxo.Id) + st = false + } else if (!ltUtxo) { + utxo = utxos.find(u => u.Id === stUtxo.Id) + st = true + } else { + const { Tax: stTax } = getProvisional(tx, stUtxo, true) + const { Tax: ltTax } = getProvisional(tx, ltUtxo, false) + + if (new BN(stTax).lt(ltTax)) { + utxo = utxos.find(u => u.Id === stUtxo.Id) + st = true + } else { + utxo = utxos.find(u => u.Id === ltUtxo.Id) + st = false + } + } + + if (utxo.Remaining.gte(tx.Remaining)) { + const disposition = createDisposition(tx, utxo, tx.Remaining, st) + dispositions.push(disposition) + utxo.Remaining = utxo.Remaining.minus(tx.Remaining) + } else { + const disposition = createDisposition(tx, utxo, utxo.Remaining, st) + dispositions.push(disposition) + tx.Remaining = tx.Remaining.minus(utxo.Remaining) + utxo.Remaining = new BN(0) + consumeUtxos(tx) + } +} + +const getProvisional = (tx, utxo, st) => { + if (utxo.Remaining.gte(tx.Remaining)) { + return createDisposition(tx, utxo, tx.Remaining, st) + } else { + return createDisposition(tx, utxo, utxo.Remaining, st) + } +} + +txs.forEach((tx, i) => { + if (tx.Received) return handleBuy(tx, i) + tx.Remaining = new BN(tx.Sent) + return consumeUtxos(tx) +}) + +let balances = utxos.reduce((prev, curr) => { + const asset = curr.Asset + if (prev[asset]) { + prev[asset] = prev[asset].plus(curr.Remaining) + } else { + prev[asset] = curr.Remaining + } + return prev +}, {}) + +utxos = utxos.map(u => ({ + ...u, + Remaining: u.Remaining.toFixed(8), +})) + +balances = Object.keys(balances) + .filter(asset => !balances[asset].isZero()) + .map(asset => ({ + Asset: asset, + balance: balances[asset].toFixed(8) + })) + +converter.json2csv(dispositions).then(csv => + fs.appendFileSync(`./dispositions.csv`, csv) +) + +converter.json2csv(balances).then(csv => + fs.appendFileSync(`./balances.csv`, csv) +) + +converter.json2csv(utxos).then(csv => + fs.appendFileSync(`./utxos.csv`, csv) +) + +// **** 8949 **** + +const getDate = (date) => { + const dateObj = new Date(date) + const month = dateObj.getMonth() + 1 + const day = dateObj.getDate() + const year = dateObj.getFullYear() + + return month + "/" + day + "/" + year; +} + +const fillFormRows = (form, page, disposition, i) => { + const dateAcquired = getDate(disposition.DateAcquired) + const dateSold = getDate(disposition.Date) + + const name = `topmostSubform[0].Page${page}[0].Table_Line1[0].Row${i + 1}[0].f${page}_` + form.getTextField(`${name}${i * 8 + 3}[0]`).setText(disposition.Asset) + form.getTextField(`${name}${i * 8 + 4}[0]`).setText(dateAcquired) + form.getTextField(`${name}${i * 8 + 5}[0]`).setText(dateSold) + form.getTextField(`${name}${i * 8 + 6}[0]`).setText(disposition.Proceeds.toString()) + form.getTextField(`${name}${i * 8 + 7}[0]`).setText(disposition.CostBasis.toString()) + form.getTextField(`${name}${i * 8 + 10}[0]`).setText(disposition.GainLoss.toString()) +} + +const fillFormTotals = (form, page, proceeds, basis) => { + form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_115[0]`).setText(proceeds.toFixed(2)) + form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_116[0]`).setText(basis.toFixed(2)) + form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_119[0]`).setText(proceeds.minus(basis).toFixed(2)) +} + +const createPDF = async () => { + const pdfDoc = await PDFDocument.create() + + const stDispositions = dispositions.filter(d => d.ShortTerm) + const ltDispositions = dispositions.filter(d => !d.ShortTerm) + + const maxTxs = Math.max(stDispositions.length, ltDispositions.length) + const numPages = Math.ceil(maxTxs / 14) + + for (let i = 0; i < numPages; i++) { + const blank8949Bytes = fs.readFileSync('../2022-8949.pdf') + const blank8949 = await PDFDocument.load(blank8949Bytes) + const form = blank8949.getForm() + const start = i * 14 + // short term + const st = stDispositions.slice(start, start + 14) + const stTotals = st.reduce((totals, d, i) => { + fillFormRows(form, 1, d, i) + totals.proceeds = totals.proceeds.plus(d.Proceeds) + totals.basis = totals.basis.plus(d.CostBasis) + return totals + }, { proceeds: new BN(0), basis: new BN(0) }) + fillFormTotals(form, 1, stTotals.proceeds, stTotals.basis) + // long term + const lt = ltDispositions.slice(start, start + 14) + const ltTotals = lt.reduce((totals, d, i) => { + fillFormRows(form, 2, d, i) + totals.proceeds = totals.proceeds.plus(d.Proceeds) + totals.basis = totals.basis.plus(d.CostBasis) + return totals + }, { proceeds: new BN(0), basis: new BN(0) }) + fillFormTotals(form, 2, ltTotals.proceeds, ltTotals.basis) + + await blank8949.save() + + const [page1, page2] = await pdfDoc.copyPages(blank8949, [0,1]) + pdfDoc.addPage(page1) + pdfDoc.addPage(page2) + } + + const pdfBytes = await pdfDoc.save() + + fs.writeFileSync(`./8949.pdf`, pdfBytes) +} + +createPDF() \ No newline at end of file diff --git a/lifo-conversion/index.js b/lifo-conversion/index.js new file mode 100644 index 0000000..cf4bac1 --- /dev/null +++ b/lifo-conversion/index.js @@ -0,0 +1,77 @@ +const csvToJson = require('convert-csv-to-json') +const converter = require('json-2-csv') +const fs = require('fs') +const BN = require('bignumber.js') + +const coin = process.argv[2] + +const pre2022 = csvToJson.fieldDelimiter(',').getJsonFromCsv(`./${coin}.csv`) + +const utxos = [] + +const handleBuy = (utxo) => { + utxo.Remaining = new BN(utxo.ReceivedQuantity) + utxos.push(utxo) +} + +const handleTrade = (trade) => { + if (trade.ReceivedCurrency === coin) return handleBuy(trade) + consumeUtxos(new BN(trade.SentQuantity)) +} + +const consumeUtxos = (amount) => { + for (let i = utxos.length - 1; i >= 0; i--) { + const utxo = utxos[i] + + if (!utxo.Remaining) continue + + if (utxo.Remaining.gt(amount)) { + utxo.Remaining = utxo.Remaining.minus(amount) + return + } else { + amount = amount.minus(utxo.Remaining) + utxo.Remaining = 0 + return consumeUtxos(amount) + } + } +} + +pre2022.forEach(tx => { + if (tx.Ignored || tx.Margin) return + + switch (tx.Type) { + case 'Buy': + case 'Receive': + handleBuy(tx) + break + case 'Sell': + case 'Send': + consumeUtxos(new BN(tx.SentQuantity)) + break + case 'Trade': + handleTrade(tx) + break + case 'Transfer': + break + } + + if (tx.FeeCurrency === coin) consumeUtxos(new BN(tx.FeeAmount)) +}) + +const remaining = utxos + .filter(utxo => !!utxo.Remaining) + .map(utxo => { + return { + Date: utxo.Date, + Asset: coin, + Credit: utxo.Remaining.toFixed(8), + Debit: '', + Price: new BN(utxo['ReceivedCostBasis(USD)']).div(utxo.ReceivedQuantity).toFixed(4), + Original: utxo.ReceivedQuantity, + } + } +) + +converter.json2csv(remaining).then(csv => + fs.appendFileSync(`./${coin}-utxos.csv`, csv) +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dd8326d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "lifotofifo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lifotofifo", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bignumber.js": "^9.1.1", + "convert-csv-to-json": "^2.0.0", + "json-2-csv": "^4.0.0", + "pdf-lib": "^1.17.1" + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, + "node_modules/convert-csv-to-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-csv-to-json/-/convert-csv-to-json-2.0.0.tgz", + "integrity": "sha512-pMDclso1Afm8tGsJfcVqay10WSPgpoGKGoHdIXWIMXJX29qayEC/MlAsd6csYr59mUjLoJxpkHi37cTUSaNUpw==" + }, + "node_modules/deeks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.0.0.tgz", + "integrity": "sha512-go4YE5vDMzDNzC9OqK7iIQd6agUUqZdBiw3TD2niRxsKGdGdB1FW8eZdrWsyCRZCNkBjV5Vutcx5uT1AikEPCg==", + "engines": { + "node": ">= 16" + } + }, + "node_modules/doc-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.0.0.tgz", + "integrity": "sha512-By/OF7rbws/bgExPZW10gHASoy3sACwXKKpKBIzYk/E19nX8q8IbILbreCeCYAVFIcO9FOxDjFYbm45wvn4I6w==", + "engines": { + "node": ">=16" + } + }, + "node_modules/json-2-csv": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-4.0.0.tgz", + "integrity": "sha512-r8WXuuLgVbeufMuCyzUXSp8VP9YK4BTuzM7rTG0X85MPKuHKa8JD6LHrp0rEKVCtBSgHA3LMu/VjrZHx+851DQ==", + "dependencies": { + "deeks": "3.0.0", + "doc-path": "4.0.0" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6d120b1 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "lifotofifo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC", + "scripts": { + "fifo": "cd ./fifo && rm -f *.csv && rm -f *.pdf && node index.js", + "hifo": "cd ./hifo && rm -f *.csv && rm -f *.pdf && node index.js" + }, + "dependencies": { + "bignumber.js": "^9.1.1", + "convert-csv-to-json": "^2.0.0", + "json-2-csv": "^4.0.0", + "pdf-lib": "^1.17.1" + } +}