Major improvements to Emacs configuration: 1. Fixed CUA mode and C-Shift-Arrow selection issues - Properly configured CUA mode for copy/paste (C-c, C-v, C-x) - Fixed C-Shift-Arrow word selection that was being intercepted - Added mode-specific CUA disabling for special modes 2. Replaced Corfu with Company mode - Removed problematic Corfu configuration causing errors - Installed and configured Company for stable auto-completion - Set up proper completion triggers and navigation 3. Integrated standalone fix files into existing configuration - Merged keybinding-fix.el into relevant config files - Added diagnostic functions for debugging keybinding issues - Cleaner organization with fixes in their respective modules 4. Enhanced diagnostics - Added diagnose-cua-selection for CUA/selection issues - Added diagnose-key-conflicts for debugging key bindings - Emergency editing restoration functions preserved All changes tested and verified working. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
452 lines
19 KiB
EmacsLisp
452 lines
19 KiB
EmacsLisp
;;; portfolio-tracker-v2.el --- Portfolio tracking with transaction history -*- lexical-binding: t -*-
|
|
|
|
;;; Commentary:
|
|
;; Enhanced portfolio tracker with full transaction history and cost basis tracking
|
|
;; Supports buy/sell transactions, dividends, and multiple lots (FIFO/LIFO)
|
|
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'tabulated-list)
|
|
(require 'url)
|
|
(require 'json)
|
|
|
|
(defgroup portfolio-tracker nil
|
|
"Portfolio tracking with live price updates."
|
|
:group 'applications)
|
|
|
|
(defcustom portfolio-tracker-update-interval 300
|
|
"Interval in seconds between automatic price updates."
|
|
:type 'integer
|
|
:group 'portfolio-tracker)
|
|
|
|
(defcustom portfolio-tracker-cost-basis-method 'fifo
|
|
"Method for calculating cost basis: fifo or lifo."
|
|
:type '(choice (const :tag "FIFO" fifo)
|
|
(const :tag "LIFO" lifo))
|
|
:group 'portfolio-tracker)
|
|
|
|
;; Data structures
|
|
(cl-defstruct portfolio-transaction
|
|
date
|
|
type ; buy, sell, dividend, split
|
|
symbol
|
|
shares
|
|
price
|
|
fees
|
|
notes)
|
|
|
|
(cl-defstruct portfolio-lot
|
|
symbol
|
|
shares ; remaining shares in this lot
|
|
purchase-date
|
|
purchase-price
|
|
fees)
|
|
|
|
(cl-defstruct portfolio-holding
|
|
symbol
|
|
name
|
|
total-shares
|
|
lots ; list of portfolio-lot structs
|
|
average-cost
|
|
current-price
|
|
previous-close
|
|
value
|
|
unrealized-gain
|
|
unrealized-gain-percent
|
|
realized-gain ; from sell transactions
|
|
dividends ; total dividends received
|
|
type) ; stock, etf, crypto
|
|
|
|
(defvar portfolio-tracker-transactions nil
|
|
"List of all transactions.")
|
|
|
|
(defvar portfolio-tracker-holdings nil
|
|
"Current holdings calculated from transactions.")
|
|
|
|
(defvar portfolio-tracker-prices-cache nil
|
|
"Cache of current prices.")
|
|
|
|
;; Transaction processing
|
|
(defun portfolio-tracker-process-transactions ()
|
|
"Process all transactions to calculate current holdings."
|
|
(let ((holdings-table (make-hash-table :test 'equal))
|
|
(realized-gains (make-hash-table :test 'equal))
|
|
(dividends-table (make-hash-table :test 'equal)))
|
|
|
|
;; Sort transactions by date
|
|
(setq portfolio-tracker-transactions
|
|
(sort portfolio-tracker-transactions
|
|
(lambda (a b)
|
|
(string< (portfolio-transaction-date a)
|
|
(portfolio-transaction-date b)))))
|
|
|
|
;; Process each transaction
|
|
(dolist (txn portfolio-tracker-transactions)
|
|
(let ((symbol (portfolio-transaction-symbol txn))
|
|
(type (portfolio-transaction-type txn)))
|
|
|
|
(cond
|
|
;; Buy transaction - add lot
|
|
((eq type 'buy)
|
|
(let* ((holding (gethash symbol holdings-table))
|
|
(new-lot (make-portfolio-lot
|
|
:symbol symbol
|
|
:shares (portfolio-transaction-shares txn)
|
|
:purchase-date (portfolio-transaction-date txn)
|
|
:purchase-price (portfolio-transaction-price txn)
|
|
:fees (or (portfolio-transaction-fees txn) 0))))
|
|
(if holding
|
|
(push new-lot (portfolio-holding-lots holding))
|
|
(puthash symbol
|
|
(make-portfolio-holding
|
|
:symbol symbol
|
|
:lots (list new-lot)
|
|
:realized-gain 0
|
|
:dividends 0)
|
|
holdings-table))))
|
|
|
|
;; Sell transaction - remove shares using FIFO/LIFO
|
|
((eq type 'sell)
|
|
(let ((holding (gethash symbol holdings-table))
|
|
(shares-to-sell (portfolio-transaction-shares txn))
|
|
(sell-price (portfolio-transaction-price txn))
|
|
(realized 0))
|
|
(when holding
|
|
(let ((lots (if (eq portfolio-tracker-cost-basis-method 'lifo)
|
|
(reverse (portfolio-holding-lots holding))
|
|
(portfolio-holding-lots holding))))
|
|
(dolist (lot lots)
|
|
(when (> shares-to-sell 0)
|
|
(let ((shares-from-lot (min shares-to-sell
|
|
(portfolio-lot-shares lot))))
|
|
(setq realized
|
|
(+ realized
|
|
(* shares-from-lot
|
|
(- sell-price (portfolio-lot-purchase-price lot)))))
|
|
(setf (portfolio-lot-shares lot)
|
|
(- (portfolio-lot-shares lot) shares-from-lot))
|
|
(setq shares-to-sell (- shares-to-sell shares-from-lot)))))
|
|
;; Remove empty lots
|
|
(setf (portfolio-holding-lots holding)
|
|
(cl-remove-if (lambda (lot) (<= (portfolio-lot-shares lot) 0))
|
|
(portfolio-holding-lots holding)))
|
|
;; Add to realized gains
|
|
(setf (portfolio-holding-realized-gain holding)
|
|
(+ (portfolio-holding-realized-gain holding) realized))))))
|
|
|
|
;; Dividend transaction
|
|
((eq type 'dividend)
|
|
(let ((holding (gethash symbol holdings-table)))
|
|
(when holding
|
|
(setf (portfolio-holding-dividends holding)
|
|
(+ (portfolio-holding-dividends holding)
|
|
(* (portfolio-transaction-shares txn)
|
|
(portfolio-transaction-price txn)))))))))
|
|
|
|
;; Calculate totals for each holding
|
|
(maphash (lambda (symbol holding)
|
|
(let ((total-shares 0)
|
|
(total-cost 0))
|
|
(dolist (lot (portfolio-holding-lots holding))
|
|
(setq total-shares (+ total-shares (portfolio-lot-shares lot)))
|
|
(setq total-cost (+ total-cost
|
|
(* (portfolio-lot-shares lot)
|
|
(portfolio-lot-purchase-price lot)))))
|
|
(setf (portfolio-holding-total-shares holding) total-shares)
|
|
(setf (portfolio-holding-average-cost holding)
|
|
(if (> total-shares 0)
|
|
(/ total-cost total-shares)
|
|
0))))
|
|
holdings-table)
|
|
|
|
;; Convert hash table to list
|
|
(setq portfolio-tracker-holdings nil)
|
|
(maphash (lambda (symbol holding)
|
|
(when (> (portfolio-holding-total-shares holding) 0)
|
|
(push holding portfolio-tracker-holdings)))
|
|
holdings-table)))
|
|
|
|
;; Yahoo Finance integration
|
|
(defun portfolio-tracker-fetch-price (symbol callback)
|
|
"Fetch current price for SYMBOL and call CALLBACK with the result."
|
|
(let ((url (format "https://query1.finance.yahoo.com/v8/finance/chart/%s" symbol)))
|
|
(url-retrieve url
|
|
(lambda (status)
|
|
(if (plist-get status :error)
|
|
(message "Error fetching price for %s" symbol)
|
|
(goto-char (point-min))
|
|
(re-search-forward "^$")
|
|
(let* ((json-object-type 'plist)
|
|
(json-array-type 'list)
|
|
(json (json-read))
|
|
(result (plist-get json :chart))
|
|
(data (car (plist-get result :result)))
|
|
(meta (plist-get data :meta))
|
|
(price (plist-get meta :regularMarketPrice))
|
|
(prev-close (plist-get meta :previousClose))
|
|
(name (plist-get meta :longName)))
|
|
(funcall callback symbol price prev-close name))))
|
|
nil t)))
|
|
|
|
(defun portfolio-tracker-update-prices ()
|
|
"Update prices for all holdings."
|
|
(interactive)
|
|
(message "Updating prices...")
|
|
(dolist (holding portfolio-tracker-holdings)
|
|
(portfolio-tracker-fetch-price
|
|
(portfolio-holding-symbol holding)
|
|
(lambda (symbol price prev-close name)
|
|
(let ((h (cl-find symbol portfolio-tracker-holdings
|
|
:key #'portfolio-holding-symbol
|
|
:test #'string=)))
|
|
(when h
|
|
(setf (portfolio-holding-current-price h) price)
|
|
(setf (portfolio-holding-previous-close h) prev-close)
|
|
(when name
|
|
(setf (portfolio-holding-name h) name))
|
|
;; Calculate unrealized gains
|
|
(let ((total-cost 0))
|
|
(dolist (lot (portfolio-holding-lots h))
|
|
(setq total-cost (+ total-cost
|
|
(* (portfolio-lot-shares lot)
|
|
(portfolio-lot-purchase-price lot)))))
|
|
(setf (portfolio-holding-value h)
|
|
(* (portfolio-holding-total-shares h) price))
|
|
(setf (portfolio-holding-unrealized-gain h)
|
|
(- (portfolio-holding-value h) total-cost))
|
|
(setf (portfolio-holding-unrealized-gain-percent h)
|
|
(if (> total-cost 0)
|
|
(* 100 (/ (portfolio-holding-unrealized-gain h) total-cost))
|
|
0))))
|
|
(portfolio-tracker-refresh-display))))))
|
|
|
|
;; Display functions
|
|
(eval-and-compile
|
|
(defvar portfolio-tracker-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
;; Use string keys directly for single characters
|
|
(define-key map "g" 'portfolio-tracker-refresh)
|
|
(define-key map "a" 'portfolio-tracker-add-transaction)
|
|
(define-key map "t" 'portfolio-tracker-show-transactions)
|
|
(define-key map "h" 'portfolio-tracker-refresh-display)
|
|
(define-key map "s" 'portfolio-tracker-save)
|
|
(define-key map "l" 'portfolio-tracker-load)
|
|
(define-key map "r" 'portfolio-tracker-refresh)
|
|
(define-key map "q" 'quit-window)
|
|
map)
|
|
"Keymap for portfolio-tracker-mode.")
|
|
|
|
(define-derived-mode portfolio-tracker-mode tabulated-list-mode "Portfolio"
|
|
"Major mode for tracking investment portfolio.
|
|
\\{portfolio-tracker-mode-map}"
|
|
:keymap portfolio-tracker-mode-map
|
|
;; Disable CUA mode to allow single-key commands
|
|
(setq-local cua-mode nil)
|
|
(setq-local cua-enable-cua-keys nil)
|
|
(setq tabulated-list-format
|
|
[("Symbol" 8 t)
|
|
("Name" 20 t)
|
|
("Shares" 10 t)
|
|
("Avg Cost" 10 nil)
|
|
("Current" 10 nil)
|
|
("Value" 12 nil)
|
|
("Unrealized" 12 nil)
|
|
("Realized" 10 nil)
|
|
("Dividends" 10 nil)
|
|
("Total %" 8 nil)])
|
|
(setq tabulated-list-padding 2)
|
|
(setq tabulated-list-sort-key (cons "Symbol" nil))
|
|
(tabulated-list-init-header)))
|
|
|
|
(define-derived-mode portfolio-transactions-mode tabulated-list-mode "Transactions"
|
|
"Major mode for viewing portfolio transactions."
|
|
(setq tabulated-list-format
|
|
[("Date" 12 t)
|
|
("Type" 8 t)
|
|
("Symbol" 8 t)
|
|
("Shares" 10 nil)
|
|
("Price" 10 nil)
|
|
("Total" 12 nil)
|
|
("Fees" 8 nil)
|
|
("Notes" 30 nil)])
|
|
(setq tabulated-list-padding 2)
|
|
(setq tabulated-list-sort-key (cons "Date" t))
|
|
(tabulated-list-init-header))
|
|
|
|
(defun portfolio-tracker-refresh-display ()
|
|
"Refresh the holdings display."
|
|
(interactive)
|
|
(let ((buf (get-buffer "*Portfolio Holdings*")))
|
|
(when buf
|
|
(with-current-buffer buf
|
|
(let ((inhibit-read-only t))
|
|
(setq tabulated-list-entries
|
|
(mapcar (lambda (holding)
|
|
(list holding
|
|
(vector
|
|
(portfolio-holding-symbol holding)
|
|
(or (portfolio-holding-name holding) "")
|
|
(format "%.2f" (portfolio-holding-total-shares holding))
|
|
(format "$%.2f" (portfolio-holding-average-cost holding))
|
|
(if (portfolio-holding-current-price holding)
|
|
(format "$%.2f" (portfolio-holding-current-price holding))
|
|
"...")
|
|
(format "$%.2f" (or (portfolio-holding-value holding) 0))
|
|
(propertize (format "$%.2f" (or (portfolio-holding-unrealized-gain holding) 0))
|
|
'face (if (>= (or (portfolio-holding-unrealized-gain holding) 0) 0)
|
|
'success 'error))
|
|
(format "$%.2f" (portfolio-holding-realized-gain holding))
|
|
(format "$%.2f" (portfolio-holding-dividends holding))
|
|
(propertize (format "%.1f%%" (or (portfolio-holding-unrealized-gain-percent holding) 0))
|
|
'face (if (>= (or (portfolio-holding-unrealized-gain-percent holding) 0) 0)
|
|
'success 'error)))))
|
|
portfolio-tracker-holdings))
|
|
(tabulated-list-print t)
|
|
;; Add summary at the bottom
|
|
(portfolio-tracker-display-summary)))))))
|
|
|
|
(defun portfolio-tracker-display-summary ()
|
|
"Display portfolio summary with totals at bottom of buffer."
|
|
(let ((inhibit-read-only t)
|
|
(total-cost 0)
|
|
(total-value 0)
|
|
(total-unrealized 0)
|
|
(total-realized 0)
|
|
(total-dividends 0))
|
|
;; Calculate totals
|
|
(dolist (holding portfolio-tracker-holdings)
|
|
(let ((cost (* (portfolio-holding-total-shares holding)
|
|
(portfolio-holding-average-cost holding))))
|
|
(setq total-cost (+ total-cost cost))
|
|
(setq total-value (+ total-value (or (portfolio-holding-value holding) cost)))
|
|
(setq total-unrealized (+ total-unrealized (or (portfolio-holding-unrealized-gain holding) 0)))
|
|
(setq total-realized (+ total-realized (portfolio-holding-realized-gain holding)))
|
|
(setq total-dividends (+ total-dividends (portfolio-holding-dividends holding)))))
|
|
|
|
;; Display summary
|
|
(goto-char (point-max))
|
|
(insert "\n" (make-string 100 ?-) "\n")
|
|
(insert (propertize "PORTFOLIO TOTALS\n" 'face 'bold))
|
|
(insert (format " Total Buy-In (Cost Basis): %s%.2f\n"
|
|
(propertize "$" 'face 'default)
|
|
total-cost))
|
|
(insert (format " Current Market Value: %s%.2f\n"
|
|
(propertize "$" 'face 'default)
|
|
total-value))
|
|
(insert (format " Unrealized Gain/Loss: %s\n"
|
|
(propertize (format "$%.2f (%.1f%%)"
|
|
total-unrealized
|
|
(if (> total-cost 0)
|
|
(* 100 (/ total-unrealized total-cost))
|
|
0))
|
|
'face (if (>= total-unrealized 0) 'success 'error))))
|
|
(insert (format " Realized Gain/Loss: %s\n"
|
|
(propertize (format "$%.2f" total-realized)
|
|
'face (if (>= total-realized 0) 'success 'error))))
|
|
(insert (format " Total Dividends Received: %s%.2f\n"
|
|
(propertize "$" 'face 'default)
|
|
total-dividends))
|
|
(insert (make-string 100 ?-) "\n")
|
|
(let ((total-gain (+ total-unrealized total-realized total-dividends))
|
|
(total-return-pct (if (> total-cost 0)
|
|
(* 100 (/ (+ total-unrealized total-dividends) total-cost))
|
|
0)))
|
|
(insert (format " TOTAL RETURN: %s\n"
|
|
(propertize (format "$%.2f (%.1f%%)"
|
|
total-gain
|
|
total-return-pct)
|
|
'face (if (>= total-gain 0) 'success 'error))))
|
|
(insert (make-string 100 ?=) "\n"))
|
|
(insert "\nKeys: [g] Refresh | [a] Add Transaction | [t] Show Transactions | [s] Save | [l] Load | [q] Quit\n")))
|
|
|
|
(defun portfolio-tracker-refresh ()
|
|
"Refresh prices and redisplay."
|
|
(interactive)
|
|
(portfolio-tracker-process-transactions)
|
|
(portfolio-tracker-update-prices)
|
|
(portfolio-tracker-refresh-display))
|
|
|
|
(defun portfolio-tracker-add-transaction ()
|
|
"Add a new transaction interactively."
|
|
(interactive)
|
|
(let* ((date (read-string "Date (YYYY-MM-DD): " (format-time-string "%Y-%m-%d")))
|
|
(type (intern (completing-read "Type: " '("buy" "sell" "dividend") nil t)))
|
|
(symbol (upcase (read-string "Symbol: ")))
|
|
(shares (read-number "Shares: "))
|
|
(price (read-number "Price per share: "))
|
|
(fees (read-number "Fees (0 if none): " 0))
|
|
(notes (read-string "Notes (optional): ")))
|
|
(push (make-portfolio-transaction
|
|
:date date
|
|
:type type
|
|
:symbol symbol
|
|
:shares shares
|
|
:price price
|
|
:fees fees
|
|
:notes notes)
|
|
portfolio-tracker-transactions)
|
|
(portfolio-tracker-process-transactions)
|
|
(portfolio-tracker-update-prices)))
|
|
|
|
(defun portfolio-tracker-show-transactions ()
|
|
"Show all transactions in a separate buffer."
|
|
(interactive)
|
|
(let ((buf (get-buffer-create "*Portfolio Transactions*")))
|
|
(with-current-buffer buf
|
|
(portfolio-transactions-mode)
|
|
(setq tabulated-list-entries
|
|
(mapcar (lambda (txn)
|
|
(list txn
|
|
(vector
|
|
(portfolio-transaction-date txn)
|
|
(symbol-name (portfolio-transaction-type txn))
|
|
(portfolio-transaction-symbol txn)
|
|
(format "%.2f" (portfolio-transaction-shares txn))
|
|
(format "$%.2f" (portfolio-transaction-price txn))
|
|
(format "$%.2f" (* (portfolio-transaction-shares txn)
|
|
(portfolio-transaction-price txn)))
|
|
(format "$%.2f" (or (portfolio-transaction-fees txn) 0))
|
|
(or (portfolio-transaction-notes txn) ""))))
|
|
(reverse portfolio-tracker-transactions)))
|
|
(tabulated-list-print))
|
|
(switch-to-buffer buf)))
|
|
|
|
(defun portfolio-tracker-save (file)
|
|
"Save portfolio data to FILE."
|
|
(interactive "FSave portfolio to: ")
|
|
(with-temp-file file
|
|
(insert ";; Portfolio Tracker Data\n")
|
|
(insert ";; Transaction history and settings\n\n")
|
|
(insert "(setq portfolio-tracker-transactions\n")
|
|
(pp portfolio-tracker-transactions (current-buffer))
|
|
(insert ")\n"))
|
|
(message "Portfolio saved to %s" file))
|
|
|
|
(defun portfolio-tracker-load (file)
|
|
"Load portfolio data from FILE."
|
|
(interactive "fLoad portfolio from: ")
|
|
(load-file file)
|
|
(portfolio-tracker-process-transactions)
|
|
(portfolio-tracker-refresh-display)
|
|
(portfolio-tracker-update-prices)
|
|
(message "Portfolio loaded from %s" file))
|
|
|
|
;;;###autoload
|
|
(defun portfolio-tracker ()
|
|
"Start the portfolio tracker."
|
|
(interactive)
|
|
(require 'tabulated-list)
|
|
(require 'cl-lib)
|
|
(let ((buf (get-buffer-create "*Portfolio Holdings*")))
|
|
(switch-to-buffer buf)
|
|
(unless (eq major-mode 'portfolio-tracker-mode)
|
|
(portfolio-tracker-mode))
|
|
(when portfolio-tracker-transactions
|
|
(portfolio-tracker-process-transactions)
|
|
(portfolio-tracker-refresh-display))
|
|
(message "Portfolio Tracker: 'a' add transaction | 't' show transactions | 'g' refresh prices | 'l' load portfolio")))
|
|
|
|
(provide 'portfolio-tracker-v2)
|
|
;;; portfolio-tracker-v2.el ends here
|