initial commit
This commit is contained in:
228
hifo/index.js
Normal file
228
hifo/index.js
Normal 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()
|
||||
Reference in New Issue
Block a user