gnucash maint: [ifrs-cost-basis] use truth table to interpret stock transaction
Christopher Lam
clam at code.gnucash.org
Thu Nov 25 09:14:09 EST 2021
Updated via https://github.com/Gnucash/gnucash/commit/502ad361 (commit)
from https://github.com/Gnucash/gnucash/commit/8a80993f (commit)
commit 502ad361a6ae428ccc9ab2a9ccdbbab407d01c41
Author: Christopher Lam <christopher.lck at gmail.com>
Date: Wed Sep 29 23:29:06 2021 +0800
[ifrs-cost-basis] use truth table to interpret stock transaction
use truth table which encodes negative/zero/positive amounts for
various accounts involved in a stock transaction. the truth table
should match the type of stock transaction being recorded.
diff --git a/gnucash/report/reports/standard/ifrs-cost-basis.scm b/gnucash/report/reports/standard/ifrs-cost-basis.scm
index 78f8d2846..59ce4a446 100644
--- a/gnucash/report/reports/standard/ifrs-cost-basis.scm
+++ b/gnucash/report/reports/standard/ifrs-cost-basis.scm
@@ -22,6 +22,7 @@
(define-module (gnucash reports standard ifrs-cost-basis))
(use-modules (srfi srfi-1))
+(use-modules (srfi srfi-9))
(use-modules (ice-9 match))
(use-modules (gnucash utilities))
(use-modules (gnucash report))
@@ -57,7 +58,7 @@ that this report will accurately portray this options activity.")))
(define optname-proceeds-acct "Proceeds Account")
(define optname-dividend-acct "Dividend Account")
(define optname-capgains-acct "Cap Gains Account")
-;; (define optname-fees-acct "Fees Account")
+(define optname-fees-acct "Fees Account")
(define optname-report-currency "Report's currency")
(define optname-format-cells "Format monetary cells")
@@ -71,6 +72,10 @@ for shorts. Disable to use alternate style every other row")
(define opthelp-cap-purch-costs "Check this option to capitalise purchase \
commissions in cumulative average cost and gain/loss after commission")
+(define optname-cap-fee-action "Action field filter for fees")
+(define opthelp-cap-fee-action "This string will be used to compare with \
+the split action field to detect capitalized fees on stock activity")
+
(define (options-generator)
(let ((options (gnc:new-options)))
@@ -103,10 +108,14 @@ commissions in cumulative average cost and gain/loss after commission")
gnc:pagename-general optname-capgains-acct "d" "Cap Gains Account"
#f #f (list ACCT-TYPE-INCOME)))
- ;; (add-option
- ;; (gnc:make-account-sel-limited-option
- ;; gnc:pagename-general optname-fees-acct "c" "Fees Account"
- ;; #f #f (list ACCT-TYPE-EXPENSE)))
+ (add-option
+ (gnc:make-account-sel-limited-option
+ gnc:pagename-general optname-fees-acct "c5" "Fees Account"
+ #f #f (list ACCT-TYPE-EXPENSE)))
+
+ (add-option
+ (gnc:make-string-option
+ gnc:pagename-general optname-cap-fee-action "d5" opthelp-cap-fee-action "Fee"))
(add-option
(gnc:make-simple-boolean-option
@@ -170,6 +179,161 @@ commissions in cumulative average cost and gain/loss after commission")
(define (trans-extract-amount trans account numfilter)
(trans-extract trans account numfilter xaccSplitGetAmount))
+(define-record-type :txn-info
+ (make-txn-info stock-amt stock-val proceeds-val
+ fees-cap-val fees-exp-val dividend-val capgains-val)
+ txn-info?
+ (stock-amt get-stock-amt set-stock-amt!)
+ (stock-val get-stock-val set-stock-val!)
+ (proceeds-val get-proceeds-val set-proceeds-val!)
+ (fees-cap-val get-fees-cap-val set-fees-cap-val!)
+ (fees-exp-val get-fees-exp-val set-fees-exp-val!)
+ (dividend-val get-dividend-val set-dividend-val!)
+ (capgains-val get-capgains-val set-capgains-val!))
+
+;; "bitfield" Nabc a=neg b=zero c=pos
+(define (N001 x) (if (number? x) (> x 0) #f))
+(define (N100 x) (if (number? x) (< x 0) #f))
+(define (N010 x) (if (number? x) (= x 0) #t))
+(define (N011 x) (if (number? x) (>= x 0) #t))
+(define (N110 x) (if (number? x) (<= x 0) #t))
+(define (N111 x) #t)
+;; N000 should be (not x) however we can accept a zero-amount split too
+(define (N000 x) (if (number? x) (= x 0) #t))
+
+;; --stock-- cash cap exp divi capg
+;; amt val fees fees
+
+(define open-types
+ (list
+ (list N001 N001 N100 N011 N000 N000 N000 "Open Long")
+ (list N100 N100 N001 N011 N000 N000 N000 "Open Short")))
+
+(define long-types
+ (list
+ (list N001 N001 N100 N011 N000 N000 N000 "Buy")
+ (list N100 N100 N011 N000 N011 N000 N111 "Sell")
+ (list N000 N000 N001 N000 N011 N100 N000 "Dividend")
+ (list N001 N001 N001 N011 N000 N100 N000 "Dividend reinvestment (w/ remainder)")
+ (list N001 N001 N000 N011 N000 N100 N000 "Dividend reinvestment (w/o remainder)")
+ (list N000 N100 N001 N011 N000 N000 N000 "Return of Capital")
+ (list N000 N001 N000 N000 N011 N100 N000 "Notional distribution")
+ (list N001 N000 N000 N011 N000 N000 N000 "Stock split")
+ (list N100 N000 N000 N011 N000 N000 N000 "Reverse split")
+ (list N100 N100 N001 N000 N011 N000 N111 "Reverse split w/ cash in lieu for fractionals")))
+
+(define short-types
+ (list
+ (list N100 N100 N001 N011 N000 N000 N000 "Short Sell")
+ (list N001 N001 N110 N000 N011 N000 N111 "Cover Buy")
+ (list N000 N000 N100 N000 N011 N001 N000 "Compensatory dividend")
+ (list N000 N000 N000 N011 N000 N000 N000 "Dividend reinvestment (w remainder)")
+ (list N000 N000 N000 N011 N000 N000 N000 "Dividend reinvestment (w/o remainder)")
+ (list N000 N001 N100 N011 N000 N000 N000 "Compensatory return of capital")
+ (list N000 N100 N000 N000 N011 N001 N000 "Compensatory notional distribution")
+ (list N100 N000 N000 N011 N000 N000 N000 "Stock split")
+ (list N001 N000 N000 N011 N000 N000 N000 "Reverse split")
+ (list N001 N001 N100 N000 N011 N000 N111 "Reverse split w/ cash in lieu for fractionals")))
+
+(define (cmp amt neg zero pos)
+ (cond ((< amt 0) neg)
+ ((= amt 0) zero)
+ (else pos)))
+
+(define shown-headers? #f)
+(define (txn-identify trans txn-info cumul-units)
+ (define (get-amount mon)
+ (and (gnc:gnc-monetary? mon)
+ (gnc:gnc-monetary-amount mon)))
+ (define trans-units (get-amount (get-stock-amt txn-info)))
+ (define trans-value (get-amount (get-stock-val txn-info)))
+ (define cash-value (get-amount (get-proceeds-val txn-info)))
+ (define fees-stock (get-amount (get-fees-cap-val txn-info)))
+ (define fees-expense (get-amount (get-fees-exp-val txn-info)))
+ (define dividend (get-amount (get-dividend-val txn-info)))
+ (define capgains (get-amount (get-capgains-val txn-info)))
+ (let lp ((types (cmp cumul-units short-types open-types long-types)))
+ (match types
+ (()
+ ;; (gnc:pk (qof-print-date (xaccTransGetDate trans)) txn-info)
+ "Unknown")
+ (((amt-fn val-fn proc-fn fee-cap-fn fee-exp-fn div-fn capg-fn res) . tail)
+ (if (and (amt-fn trans-units)
+ (val-fn trans-value)
+ (proc-fn cash-value)
+ (fee-cap-fn fees-stock)
+ (fee-exp-fn fees-expense)
+ (div-fn dividend)
+ (capg-fn capgains))
+ res
+ (lp tail))))))
+
+(define (txn->info txn stock-acct cap-fee-action
+ proceeds-acct capgains-acct expenses-acct dividend-acct)
+ (define (from-acct? acct)
+ (lambda (split)
+ (equal? (xaccSplitGetAccount split) acct)))
+ (define (cap-expenses? split)
+ (and ((from-acct? stock-acct) split)
+ (equal? (gnc-get-action-num txn split) cap-fee-action)))
+ (define (make-monetary account amount)
+ (and amount (gnc:make-gnc-monetary (xaccAccountGetCommodity account) amount)))
+ (define (make-parent-monetary amount)
+ (and amount (gnc:make-gnc-monetary (gnc-account-get-currency-or-parent stock-acct) amount)))
+ (let lp ((splits (xaccTransGetSplitList txn))
+ (stock-amt #f)
+ (stock-val #f)
+ (proceeds-val #f)
+ (fees-cap-val #f)
+ (fees-exp-val #f)
+ (dividend-val #f)
+ (capgains-val #f))
+ (match splits
+ (() (make-txn-info
+ (make-monetary stock-acct stock-amt)
+ (make-parent-monetary stock-val)
+ (make-monetary proceeds-acct proceeds-val)
+ (make-parent-monetary fees-cap-val)
+ (make-monetary expenses-acct fees-exp-val)
+ (make-monetary dividend-acct dividend-val)
+ (make-monetary capgains-acct capgains-val)))
+
+ (((? (from-acct? proceeds-acct) split) . rest)
+ (lp rest stock-amt stock-val
+ (M+ proceeds-val (xaccSplitGetAmount split))
+ fees-cap-val fees-exp-val dividend-val capgains-val))
+
+ (((? (from-acct? capgains-acct) split) . rest)
+ (lp rest stock-amt stock-val proceeds-val fees-cap-val fees-exp-val dividend-val
+ (M+ capgains-val (xaccSplitGetAmount split))))
+
+ (((? (from-acct? expenses-acct) split) . rest)
+ (lp rest stock-amt stock-val proceeds-val fees-cap-val
+ (M+ fees-exp-val (xaccSplitGetAmount split))
+ dividend-val capgains-val))
+
+ (((? (from-acct? dividend-acct) split) . rest)
+ (lp rest stock-amt stock-val proceeds-val fees-cap-val fees-exp-val
+ (M+ dividend-val (xaccSplitGetAmount split))
+ capgains-val))
+
+ ;; testing capitalized fees must take place *before* processing
+ ;; stock amt/val because it belongs to the stock account.
+ (((? cap-expenses? split) . rest)
+ (lp rest stock-amt stock-val proceeds-val
+ (M+ fees-cap-val (xaccSplitGetValue split))
+ fees-exp-val dividend-val capgains-val))
+
+ (((? (from-acct? stock-acct) split) . rest)
+ (lp rest
+ (M+ stock-amt (xaccSplitGetAmount split))
+ (M+ stock-val (xaccSplitGetValue split))
+ proceeds-val fees-cap-val fees-exp-val dividend-val capgains-val))
+
+ ((_ . rest)
+ (lp rest stock-amt stock-val proceeds-val fees-cap-val fees-exp-val
+ dividend-val capgains-val)))))
+
(define (ifrs-cost-basis-renderer report-obj)
(define (opt-val section name)
(gnc:option-value
@@ -187,11 +351,12 @@ commissions in cumulative average cost and gain/loss after commission")
(define proceeds-acct (opt-val gnc:pagename-general optname-proceeds-acct))
(define dividend-acct (opt-val gnc:pagename-general optname-dividend-acct))
(define capgains-acct (opt-val gnc:pagename-general optname-capgains-acct))
- ;; (define fees-acct (opt-val gnc:pagename-general optname-fees-acct))
+ (define fees-acct (opt-val gnc:pagename-general optname-fees-acct))
(define report-currency (opt-val gnc:pagename-general optname-report-currency))
(define format-cells (opt-val gnc:pagename-general optname-format-cells))
(define short-alternate-format? (opt-val gnc:pagename-general optname-format-short))
(define cap-purch-costs? (opt-val gnc:pagename-general optname-cap-purch-costs))
+ (define cap-fee-action (opt-val gnc:pagename-general optname-cap-fee-action))
(define document (gnc:make-html-document))
(define large 10000000)
@@ -199,18 +364,9 @@ commissions in cumulative average cost and gain/loss after commission")
(/ (gnc-pricedb-convert-balance-nearest-price-t64 db large from to time)
large))
- (define (stock-split prev delta)
- (let ((exact (/ (+ delta prev) prev)))
- (format #f "~a:~a Split" (numerator exact) (denominator exact))))
-
(define (to-cell elt)
(gnc:make-html-table-cell/markup "number-cell" elt))
- (define (cmp amt neg zero pos)
- (cond ((< amt 0) neg)
- ((= amt 0) zero)
- (else pos)))
-
(gnc:html-document-set-title! document "IFRS weighted average cost basis Report")
(cond
@@ -250,10 +406,10 @@ commissions in cumulative average cost and gain/loss after commission")
(gnc:html-document-set-title!
document
- (format #f "Average-Cost (Basis) Report: From ~a to ~a. Report-currency ~a"
- (qof-print-date startdate)
- (qof-print-date enddate)
- (gnc-commodity-get-mnemonic report-currency)))
+ (gnc:format "Average-Cost (Basis) Report: From ${startdate} to ${enddate}. Report-currency ${currency}"
+ 'startdate (qof-print-date startdate)
+ 'enddate (qof-print-date enddate)
+ 'currency (gnc-commodity-get-mnemonic report-currency)))
(gnc:html-table-set-col-headers!
table (list "date" "description" "trans-units" "cumul-units" "note"
@@ -279,12 +435,15 @@ commissions in cumulative average cost and gain/loss after commission")
((split . rest-splits)
(let* ((trans (xaccSplitGetParent split))
+ (txn-info (txn->info trans stock-acct cap-fee-action proceeds-acct
+ capgains-acct fees-acct dividend-acct))
(trans-units (trans-extract-amount trans stock-acct #f))
(trans-value (trans-extract-value trans stock-acct #f))
- (proceeds-val (trans-extract-value trans proceeds-acct #f))
+ (cash-value (trans-extract-value trans proceeds-acct #f))
(dividends-val (trans-extract-value trans dividend-acct #f))
(capgains-val (trans-extract-value trans capgains-acct #f))
- (fees-value (trans-extract-value trans #f "Fee"))
+ (fees-expense (trans-extract-value trans fees-acct #f))
+ (fees-value (trans-extract-value trans #f cap-fee-action))
(new-units (M+ cumul-units trans-units))
(sale?
@@ -296,7 +455,7 @@ commissions in cumulative average cost and gain/loss after commission")
(purchase?
(cond
((= trans-value 0) dividends-val) ;dividends
- ((= trans-units 0) proceeds-val) ;return of capital
+ ((= trans-units 0) cash-value) ;return of capital
((> trans-units 0) (< 0 new-units)) ;regular buy
((< trans-units 0) (< new-units 0)))) ;buy during short
@@ -307,7 +466,7 @@ commissions in cumulative average cost and gain/loss after commission")
(purchase-val (and purchase? (M- trans-value purchase-cost)))
(cash-dividends (M- dividends-val))
(proceeds-cost (and sale? fees-value))
- (proceeds-val (and sale? (M+ proceeds-val proceeds-cost)))
+ (proceeds-value (and sale? (M+ cash-value proceeds-cost)))
;; now convert to report-currency
(fx (get-fx pricedb currency report-currency
@@ -315,14 +474,14 @@ commissions in cumulative average cost and gain/loss after commission")
(conv-purchase-val (M* fx purchase-val))
(conv-purchase-cost (M* fx purchase-cost))
(conv-dividends (M* fx cash-dividends))
- (conv-proceeds-val (M* fx proceeds-val))
+ (conv-proceeds-value (M* fx proceeds-value))
(conv-proceeds-cost (M* fx proceeds-cost))
;; now perform AVERAGE-COST-BASIS calculations
(average-cost-basis/unit-for-sale
(M-abs (M/ cumul-average-cost-basis cumul-units)))
(average-cost-basis-of-sale
- (and proceeds-val (M* average-cost-basis/unit-for-sale
+ (and proceeds-value (M* average-cost-basis/unit-for-sale
trans-units)))
(cumul-average-cost-basis
(M+ cumul-average-cost-basis
@@ -330,11 +489,11 @@ commissions in cumulative average cost and gain/loss after commission")
(and cap-purch-costs? conv-purchase-cost)
average-cost-basis-of-sale))
- (net-proceeds (M- conv-proceeds-val conv-proceeds-cost))
+ (net-proceeds (M- conv-proceeds-value conv-proceeds-cost))
(gain-post-commission (M+ net-proceeds average-cost-basis-of-sale
(and (not cap-purch-costs?)
conv-purchase-cost)))
- (gain-pre-commission (M+ conv-proceeds-val
+ (gain-pre-commission (M+ conv-proceeds-value
average-cost-basis-of-sale))
(new-gross-profit (M+ cumul-gross-profit gain-pre-commission))
@@ -343,7 +502,7 @@ commissions in cumulative average cost and gain/loss after commission")
conv-dividends)))
;; (gnc:pk trans 'trans-units trans-units 'trans-value trans-value
- ;; 'cumul-units cumul-units 'proceeds-val proceeds-val
+ ;; 'cumul-units cumul-units 'proceeds-value proceeds-value
;; 'sale? sale? 'purchase? purchase?)
(cond
((not (< startdate (xaccTransGetDate (xaccSplitGetParent (car splits)))
@@ -368,37 +527,18 @@ commissions in cumulative average cost and gain/loss after commission")
(cond
((< new-units 0 cumul-units) "ERROR: longâshort")
((< cumul-units 0 new-units) "ERROR: shortâlong")
- ((= 0 cumul-units) (cmp new-units "Open Short" "1" "Open Long"))
- ((= 0 new-units) (cmp trans-units "Close Long" "2" "Close Short"))
- ((= 0 trans-units trans-value)
- (cmp cumul-units "Compensatory Dividend" "7" "Dividend"))
- ((= 0 trans-units)
- (cond (cash-dividends
- (cmp cumul-units
- "Compensatory Notional Distribution"
- "7"
- "Notional Distribution"))
- (purchase-val
- (cmp cumul-units
- "Compensatory Return Capital"
- "8"
- "Return Capital"))
- (else "3")))
- ((= 0 trans-value) (stock-split cumul-units trans-units))
- (purchase-val (cmp purchase-val "Short Sell" "5" "Buy"))
- (proceeds-val (cmp proceeds-val "Short Buy" "6" "Sell"))
- (else "4"))
+ (else (txn-identify trans txn-info cumul-units)))
(gnc-commodity-get-mnemonic currency)
(to-cell (gnc:default-price-renderer report-currency fx))
(to-cell (to-orig-currency purchase-val))
(to-cell (to-orig-currency purchase-cost))
(to-cell (to-orig-currency cash-dividends))
- (to-cell (to-orig-currency proceeds-val))
+ (to-cell (to-orig-currency proceeds-value))
(to-cell (to-orig-currency proceeds-cost))
(to-cell (to-report-currency conv-purchase-val))
(to-cell (to-report-currency conv-purchase-cost))
(to-cell (to-report-currency conv-dividends))
- (to-cell (to-report-currency conv-proceeds-val))
+ (to-cell (to-report-currency conv-proceeds-value))
(to-cell (to-report-currency conv-proceeds-cost))
(to-cell (to-report-currency average-cost-basis/unit-for-sale))
(to-cell (to-report-currency (M- average-cost-basis-of-sale)))
diff --git a/gnucash/report/reports/standard/test/test-ifrs-cost-basis.scm b/gnucash/report/reports/standard/test/test-ifrs-cost-basis.scm
index a37ed1d34..f4c7d2c2d 100644
--- a/gnucash/report/reports/standard/test/test-ifrs-cost-basis.scm
+++ b/gnucash/report/reports/standard/test/test-ifrs-cost-basis.scm
@@ -250,6 +250,8 @@
(set-option! options "General" "Report's currency" (mnemonic->commodity "CAD"))
(set-option! options "General" "Proceeds Account"
(assoc-ref account-alist "USD Cash"))
+ (set-option! options "General" "Fees Account"
+ (assoc-ref account-alist "USD Commissions"))
(set-option! options "General" "Start Date"
(cons 'absolute (gnc-dmy2time64 01 01 2019)))
(set-option! options "General" "End Date"
@@ -284,7 +286,7 @@
(sxml->table-row-col sxml 1 4 #f))
(test-equal "Return Capital $2500"
- '("04/16/20" "Return of Capital" "0 SPY" "325 SPY" "Return Capital"
+ '("04/16/20" "Return of Capital" "0 SPY" "325 SPY" "Return of Capital"
"CAD" "C$1.0000" "-C$2,500.00" "-C$2,500.00" "C$184.68"
"C$57,519.90" "-C$6,009.95" "-C$6,019.90" "-C$6,019.90")
(sxml->table-row-col sxml 1 5 #f))
@@ -296,7 +298,7 @@
(sxml->table-row-col sxml 1 6 #f))
(test-equal "2:1 split"
- '("05/11/20" "stock split" "450 SPY" "900 SPY" "2:1 Split"
+ '("05/11/20" "stock split" "450 SPY" "900 SPY" "Stock split"
"CAD" "C$1.0000" "C$233.38" "C$105,019.90" "-C$6,009.95"
"-C$6,019.90" "-C$6,019.90")
(sxml->table-row-col sxml 1 7 #f))
@@ -315,7 +317,7 @@
(sxml->table-row-col sxml 1 9 #f))
(test-equal "sell 915 SPY close long"
- '("06/10/20" "Sell SPY" "-915 SPY" "0 SPY" "Close Long" "CAD"
+ '("06/10/20" "Sell SPY" "-915 SPY" "0 SPY" "Sell" "CAD"
"C$1.0000" "C$128,100.00" "C$9.95" "C$128,100.00" "C$9.95"
"C$120.51" "C$110,266.92" "C$0.00" "C$17,823.14" "C$17,833.08"
"C$128,090.05" "C$17,570.15" "C$17,540.30" "C$17,540.30")
@@ -334,14 +336,14 @@
(sxml->table-row-col sxml 1 12 #f))
(test-equal "buy 50 SPY short"
- '("06/18/20" "Buy SPY Close Short" "50 SPY" "-100 SPY" "Short Buy"
+ '("06/18/20" "Buy SPY Close Short" "50 SPY" "-100 SPY" "Cover Buy"
"CAD" "C$1.0000" "-C$5,000.00" "C$9.95" "-C$5,000.00" "C$9.95"
"C$152.87" "-C$7,643.37" "-C$15,286.73" "C$2,633.42" "C$2,643.37"
"-C$5,009.95" "C$20,213.52" "C$20,173.72" "C$20,173.72")
(sxml->table-row-col sxml 1 13 #f))
(test-equal "BUY 100 SPY close short"
- '("06/20/20" "Buy SPY Close Short" "100 SPY" "0 SPY" "Close Short"
+ '("06/20/20" "Buy SPY Close Short" "100 SPY" "0 SPY" "Cover Buy"
"CAD" "C$1.0000" "-C$8,000.00" "C$4.98" "-C$8,000.00" "C$4.98"
"C$152.87" "-C$15,286.73" "C$0.00" "C$7,281.75" "C$7,286.73"
"-C$8,004.98" "C$27,500.25" "C$27,455.47" "C$27,455.47")
Summary of changes:
.../report/reports/standard/ifrs-cost-basis.scm | 240 ++++++++++++++++-----
.../reports/standard/test/test-ifrs-cost-basis.scm | 12 +-
2 files changed, 197 insertions(+), 55 deletions(-)
More information about the gnucash-changes
mailing list