-
Notifications
You must be signed in to change notification settings - Fork 0
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.
daybook supports three cost basis strategies, all implementing the CostBasisStrategy interface:
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 FIFODisposes 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 HIFOUser 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.jsonWhen using --method specific-id without --lot-selections, an Ink-based interactive picker launches. For each disposal:
- The disposal details are shown (asset, amount, date)
- Available lots are displayed with: lot ID, acquisition date, amount, unit cost, holding period
- Select lots using checkbox-style controls
- A running total shows progress toward the required amount
- When the total meets the disposal amount, the picker advances to the next disposal
- Press
sto 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.
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.
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.
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
nullif 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 differs from fungible tokens — there is no CoinGecko equivalent for individual NFTs. Pricing relies on:
- 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.
-
Manual price overrides —
daybook overrides set <contractAddress>:<tokenId> <date> <price>sets a price for a specific NFT on a specific date. - 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.
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)
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): alwayswashSaleFlag = 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.
daybook compare 2024Runs 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 |
daybook supports four export formats, all producing identical numbers from the same TaxResult data.
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)
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
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)
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.
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.
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.
Getting Started
Usage
Architecture