-
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.
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