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