;;; inventory.lisp --- Security lot engine: ACB pool (CRA) + FIFO book (IRS). ;;; ;;; SPDX-License-Identifier: GPL-3.0-or-later ;;; ;;; Copyright (C) 2026 Anthony Green ;;; ;;; The two authorities apportion basis on different *scopes* as well as ;;; different methods, so we keep two independent structures: ;;; ;;; * ACB-POOL (CRA, CAD) -- a single running average cost. The CRA pools ;;; identical property across ALL of a taxpayer's non-registered ;;; accounts, so many accounts can share one pool (keyed per owner + ;;; security in gains.lisp). Registered plans are excluded (sheltered) ;;; and get their own per-account pool. ;;; ;;; * FIFO-BOOK (IRS, USD) -- FIFO lots, always per account, each lot ;;; struck at its acquisition-date exchange rate. ;;; ;;; Money is cambl (exact rational arithmetic via money.lisp); unit counts ;;; are plain RATIONAL scalars. No FX conversion happens here -- costs and ;;; proceeds arrive already valued in each currency (see gains.lisp). (in-package #:chirp) ;;; --- CRA: adjusted-cost-base pool (may span accounts) ---------------------- (defstruct acb-pool (units 0 :type rational) (cad nil)) ; total ACB, a CAD money (or NIL) (defun acb-buy (pool units cost-cad) (incf (acb-pool-units pool) units) (setf (acb-pool-cad pool) (money+ (acb-pool-cad pool) cost-cad))) (defun acb-sell (pool units) "Cost basis (CAD money) for disposing UNITS at the pool average; reduce the pool. Caps at units held. Returns (values COST-CAD UNITS-SOLD)." (let* ((have (acb-pool-units pool)) (sell (min units have)) (per (money/ (acb-pool-cad pool) (if (plusp have) have 1))) (cost (money* per sell))) (setf (acb-pool-cad pool) (money- (acb-pool-cad pool) cost) (acb-pool-units pool) (- have sell)) (values cost sell))) ;;; --- IRS: FIFO lot book (per account) -------------------------------------- (defstruct (lot (:constructor %make-lot (date units usd))) (date "" :type string) ; acquisition date, ISO (units 0 :type rational) ; units remaining in this lot (usd nil)) ; USD money cost remaining for those units (defstruct fifo-book (units 0 :type rational) (lots '())) ; oldest first (defun fifo-buy (book date units cost-usd) (incf (fifo-book-units book) units) (setf (fifo-book-lots book) (nconc (fifo-book-lots book) (list (%make-lot date units cost-usd))))) (defun fifo-sell (book units) "Cost basis (USD money) for disposing UNITS oldest-first; reduce the book. Caps at units held. Returns (values COST-USD EARLIEST-DATE UNITS-SOLD CHUNKS), where CHUNKS is a list of (ACQ-DATE UNITS USD-COST) per consumed lot slice so callers can split the gain by holding period (short- vs long-term)." (let ((remaining units) (cost nil) (earliest nil) (sold 0) (chunks '())) (loop while (and (plusp remaining) (fifo-book-lots book)) for lot = (first (fifo-book-lots book)) for take = (min (lot-units lot) remaining) for per = (money/ (lot-usd lot) (if (plusp (lot-units lot)) (lot-units lot) 1)) for chunk = (money* per take) do (unless earliest (setf earliest (lot-date lot))) (push (list (lot-date lot) take chunk) chunks) (setf cost (money+ cost chunk)) (incf sold take) (decf remaining take) (decf (lot-units lot) take) (setf (lot-usd lot) (money- (lot-usd lot) chunk)) (when (zerop (lot-units lot)) (pop (fifo-book-lots book)))) (decf (fifo-book-units book) sold) (values cost earliest sold (nreverse chunks)))) ;;; --- realized disposal record ---------------------------------------------- (defstruct realized security account (sale-date "") (units 0) (proceeds-cad nil) (proceeds-usd nil) (cost-cad nil) (cost-usd nil) ; cost-cad via ACB pool, cost-usd via FIFO (gain-cad nil) (gain-usd nil) (acq-from nil) ; earliest FIFO lot date consumed (lots nil) ; consumed FIFO chunks: (acq-date units usd-cost) (pooled nil) ; ACB pooled across >1 account? (oversold nil)) ; sold more units than the account held