initial commit

This commit is contained in:
Matt Hill
2023-07-01 17:33:43 -06:00
commit 0c7e6ac609
8 changed files with 538 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
.DS_Store
*.csv
*.pdf
!2022-f8949.pdf

BIN
2022-f8949.pdf Normal file

Binary file not shown.

0
README.md Normal file
View File

113
fifo/index.js Normal file
View File

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

228
hifo/index.js Normal file
View File

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

77
lifo-conversion/index.js Normal file
View File

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

97
package-lock.json generated Normal file
View File

@@ -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=="
}
}
}

18
package.json Normal file
View File

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