Skip to content

Tax Engine

Griffen Fargo edited this page May 4, 2026 · 3 revisions

Tax Engine

The tax engine lives in packages/tax/. It computes cost basis, gain/loss, and produces tax-ready CSV output from classified ledger entries.

Cost Basis Methods

daybook supports three cost basis strategies, all implementing the CostBasisStrategy interface:

FIFO (First In, First Out)

The IRS default. Disposes of the oldest lots first. Often produces long-term gains (taxed at lower rates) but on lots with the lowest basis (larger gains).

daybook export 2024 --method FIFO

HIFO (Highest In, First Out)

Disposes of the highest-cost lots first. Minimizes taxable gain in the current year. Allowed by the IRS as "Specific ID with consistent application."

daybook export 2024 --method HIFO

Specific ID

User hand-picks which lots to dispose. Maximum flexibility for tax-loss harvesting.

# Interactive lot picker
daybook export 2024 --method specific-id

# Replay from saved selections
daybook export 2024 --method specific-id --lot-selections ./selections.json

Interactive Lot Picker

When using --method specific-id without --lot-selections, an Ink-based interactive picker launches. For each disposal:

  1. The disposal details are shown (asset, amount, date)
  2. Available lots are displayed with: lot ID, acquisition date, amount, unit cost, holding period
  3. Select lots using checkbox-style controls
  4. A running total shows progress toward the required amount
  5. When the total meets the disposal amount, the picker advances to the next disposal
  6. Press s to skip a disposal (falls back to FIFO for that one)

After completion, the selections are serialized to JSON and the path is printed for replay.

JSON Replay

The --lot-selections <path> flag loads a JSON file mapping lot IDs to disposal amounts:

{
  "lot-abc123": "1.5",
  "lot-def456": "0.75"
}

If a lot ID in the file no longer exists in the LotBook (e.g., after reclassification), the tool exits with an error listing the missing IDs.

LotBook

The LotBook (packages/tax/src/lot-book.ts) manages per-asset queues of lots. Operations:

  • acquire — add a new lot (from income, trade buy leg, or transfer in)
  • dispose — remove lots according to the selected strategy

Lot splitting is handled automatically — if a disposal partially consumes a lot, the remainder stays in the book.

Universal pooling is used: all lots for an asset are in one pool regardless of which account they came from.

NftLotBook

The NftLotBook (packages/tax/src/nft-lot-book.ts) manages per-NFT lots. Each NFT is identified by <contractAddress>:<tokenId> (lowercased) and tracked as a single indivisible lot with quantity 1.

Operations:

  • acquire — add a lot for an NFT (from purchase, mint, airdrop, or trade). Overwrites if the same NFT is acquired again (with warning).
  • dispose — remove and return the lot by NFT identifier. Returns null if no matching lot exists.
  • has — check if a lot exists for a given NFT identifier.

Unlike the fungible LotBook, there is no lot splitting or pooling. Each NFT lot is consumed entirely or not at all.

NFT Pricing

NFT pricing differs from fungible tokens — there is no CoinGecko equivalent for individual NFTs. Pricing relies on:

  1. Counterpart legs — if the same transaction contains a fungible payment (ETH out for a purchase, ETH in for a sale), the USD value of that leg becomes the NFT's cost basis or proceeds.
  2. Manual price overridesdaybook overrides set <contractAddress>:<tokenId> <date> <price> sets a price for a specific NFT on a specific date.
  3. Zero fallback — airdrops and transfers with no counterpart and no override default to zero cost basis.

Unpriced NFT events are tracked separately in the unpricedEvents list and surfaced in the export summary with guidance to set overrides.

NFT Identifier Formatting

Three formatting functions in packages/tax/src/nft-helpers.ts:

  • nftId(contractAddress, tokenId) — canonical form: 0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d:4523 (lowercased, full)
  • formatNftId(contractAddress, tokenId) — CLI display: 0xbc4c...a1:4523 (first 6 + last 2 chars)
  • formatNftDescription(contractAddress, tokenId) — IRS forms: 1 0xbc4c...f13d:4523 (first 6 + last 4 chars)

Wash Sale Flagging

File: packages/tax/src/wash-sale.ts

After the main cost-basis computation, a wash sale pass runs over all DisposalResult records:

  • For each disposal with a loss (gainLoss < 0): check if the same asset was acquired within ±30 calendar days (UTC) of the disposal date
  • If a matching acquisition exists: washSaleFlag = true
  • If no match: washSaleFlag = false
  • Disposals with gains (gainLoss >= 0): always washSaleFlag = false (no lookup needed)

Calendar days are computed as Math.floor(date.getTime() / 86_400_000) for consistent UTC-day comparison.

The wash sale flag is informational only. daybook does not compute or apply disallowance amounts. The flag appears in the CSV export as a Wash Sale? column (Y or N) and in the export summary as a count of candidates.

Use --no-wash-sale-flag to suppress both the column and the summary.

Method Comparison

daybook compare 2024

Runs computeTax with both FIFO and HIFO strategies against the same ledger entries and displays a side-by-side Ink table:

FIFO HIFO
Short-term gain $4,820 $1,210
Long-term gain $12,340 $14,890
Total taxable $17,160 $16,100
Income $1,205 $1,205

Export Formats

daybook supports four export formats, all producing identical numbers from the same TaxResult data.

Form 8949 PDF

File: packages/tax/src/form-8949.ts

Generates IRS Form 8949 (Sales and Other Dispositions of Capital Assets) by filling the official IRS fillable PDF template via pdf-lib AcroForm fields.

  • Part I — short-term capital gains and losses
  • Part II — long-term capital gains and losses
  • One row per disposal: description (<amount> <asset>), date acquired, date sold, proceeds, cost basis, gain/loss
  • Automatic continuation sheets when disposals exceed 14 rows per part
  • Per-page column totals for proceeds, cost basis, and gain/loss
  • Checkbox category A/B/C (default C — transactions not reported on 1099-B)

Key functions:

  • buildForm8949Data() — splits disposals by term, paginates, computes totals
  • renderForm8949Pdf() — fills template, clones pages for continuations → Uint8Array
  • parseForm8949Pdf() — reads fields back for round-trip testing
  • formatForm8949() — convenience wrapper (build + render)

Schedule D PDF

File: packages/tax/src/schedule-d.ts

Generates IRS Schedule D (Capital Gains and Losses) summary by filling the official IRS fillable PDF template.

  • Line 1a — short-term totals (proceeds, cost basis, gain/loss) from Form 8949 Part I
  • Line 7 — net short-term capital gain or loss
  • Line 8a — long-term totals from Form 8949 Part II
  • Line 15 — net long-term capital gain or loss
  • Lines not computable from daybook data (carryover losses, 28% rate gains, etc.) are left blank

Key functions:

  • buildScheduleDData() — aggregates totals from disposals
  • renderScheduleDPdf() — fills template → Uint8Array
  • formatScheduleD() — convenience wrapper

TXF Export

File: packages/tax/src/txf-export.ts

Generates TXF (Tax Exchange Format) v042 text files for import into TurboTax and other tax software.

  • Header: version line (V042), software identifier (Adaybook), date
  • One record per disposal with tax line reference, description, dates, cost basis, proceeds
  • CRLF line endings, ASCII encoding per TXF spec
  • Tax line mapping: checkbox C → 712/714, A → 321/323, B → 711/713

Key functions:

  • formatTxf()TaxResult → TXF string
  • parseTxf() — TXF string → TxfRecord[] with validation (for round-trip testing)

CSV Export

The CSV exporter (packages/tax/src/csv-export.ts) produces one row per disposal with columns:

  • Date Acquired
  • Date Sold
  • Asset
  • Amount
  • Proceeds (USD)
  • Cost Basis (USD)
  • Gain/Loss (USD)
  • Term (Short/Long)
  • Wash Sale? (Y/N) — omitted with --no-wash-sale-flag

A summary footer includes totals for short-term gain, long-term gain, total income, and wash sale candidate count.

Pricing Integration

The tax engine uses the Pricing module to resolve USD values for each event. Prices are resolved at computation time, not at sync time, so re-running the export after adding manual overrides produces updated results.

Decimal Precision

All amount math uses decimal.js. Amounts are stored as strings in the database and converted to Decimal at math boundaries. JavaScript floating-point is never used for financial calculations.

Clone this wiki locally