cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Add finance functions

Dami committed 9ebb048
PMT, PV, FV, NPER, RATE, NPV and IRR.
commit
9ebb04866dc7bc515b18fececd10ee030177b70b
parent
c72f0d1ab0f2d5ff1da79248ddf1deb1e555d202
author
Dami <dami.etoile@gmail.com>
2 files changed, +242 -0
Ainternal/sheet/functions_finance.go216++++++++++++++++++++++++++++++
Ainternal/sheet/functions_finance_test.go26++++
A internal/sheet/functions_finance.go +216 -0
@@ -0,0 +1,216 @@
1+package sheet2+3+import (4+ "math"5+)6+7+// Finance functions follow the usual cash-flow sign convention: money8+// paid out is negative. type (end_or_beginning) is 0 for payments at the9+// end of each period and 1 for the beginning.10+11+func init() {12+ define(13+ &FuncDef{Name: "PMT", Args: "rate, number_of_periods, present_value, [future_value], [end_or_beginning]", Desc: "Payment per period of a loan or investment", Min: 3, Max: 5,14+ eval: finance(func(x []float64) Value {15+ r, n, pv, fv, t := x[0], x[1], x[2], x[3], x[4]16+ if n == 0 {17+ return ErrNum18+ }19+ if r == 0 {20+ return num(-(pv + fv) / n)21+ }22+ g := math.Pow(1+r, n)23+ return num(-r * (fv + pv*g) / ((1 + r*t) * (g - 1)))24+ })},25+ &FuncDef{Name: "PV", Args: "rate, number_of_periods, payment_amount, [future_value], [end_or_beginning]", Desc: "Present value of a series of payments", Min: 3, Max: 5,26+ eval: finance(func(x []float64) Value {27+ r, n, pmt, fv, t := x[0], x[1], x[2], x[3], x[4]28+ if r == 0 {29+ return num(-(fv + pmt*n))30+ }31+ g := math.Pow(1+r, n)32+ return num(-(fv + pmt*(1+r*t)*(g-1)/r) / g)33+ })},34+ &FuncDef{Name: "FV", Args: "rate, number_of_periods, payment_amount, [present_value], [end_or_beginning]", Desc: "Future value of a series of payments", Min: 3, Max: 5,35+ eval: finance(func(x []float64) Value {36+ r, n, pmt, pv, t := x[0], x[1], x[2], x[3], x[4]37+ if r == 0 {38+ return num(-(pv + pmt*n))39+ }40+ g := math.Pow(1+r, n)41+ return num(-(pv*g + pmt*(1+r*t)*(g-1)/r))42+ })},43+ &FuncDef{Name: "NPER", Args: "rate, payment_amount, present_value, [future_value], [end_or_beginning]", Desc: "Number of periods to pay off a loan or reach a value", Min: 3, Max: 5,44+ eval: finance(func(x []float64) Value {45+ r, pmt, pv, fv, t := x[0], x[1], x[2], x[3], x[4]46+ if r == 0 {47+ if pmt == 0 {48+ return ErrNum49+ }50+ return num(-(pv + fv) / pmt)51+ }52+ a := pmt * (1 + r*t)53+ ratio := (a - fv*r) / (a + pv*r)54+ if ratio <= 0 || r <= -1 {55+ return ErrNum56+ }57+ return num(math.Log(ratio) / math.Log(1+r))58+ })},59+ &FuncDef{Name: "RATE", Args: "number_of_periods, payment_per_period, present_value, [future_value], [end_or_beginning], [rate_guess]", Desc: "Interest rate per period of an annuity", Min: 3, Max: 6,60+ eval: rate, format: returns(Preset(FmtPercent))},61+ &FuncDef{Name: "NPV", Args: "discount, cashflow1, [cashflow2, ...]", Desc: "Net present value of periodic cash flows", Min: 2, Max: -1,62+ eval: func(args []Node, get lookup) Value {63+ r, err := numArg(args[0], get)64+ if err != nil {65+ return *err66+ }67+ flows, err := nums(args[1:], get)68+ if err != nil {69+ return *err70+ }71+ if r == -1 {72+ return ErrDiv073+ }74+ return num(npv(r, flows, 1))75+ }},76+ &FuncDef{Name: "IRR", Args: "cashflow_amounts, [rate_guess]", Desc: "Internal rate of return of periodic cash flows", Min: 1, Max: 2,77+ eval: irr, format: returns(Preset(FmtPercent))},78+ )79+}80+81+// finance evaluates arguments as numbers, defaulting the optional82+// future/present value and type to 0 (type is 1 if non-zero).83+func finance(f func(x []float64) Value) func([]Node, lookup) Value {84+ return func(args []Node, get lookup) Value {85+ x := make([]float64, 5)86+ for i := range x {87+ v, err := optNum(args, i, 0, get)88+ if err != nil {89+ return *err90+ }91+ x[i] = v92+ }93+ if x[4] != 0 {94+ x[4] = 195+ }96+ return f(x)97+ }98+}99+100+// npv discounts flows, the first at period first.101+func npv(r float64, flows []float64, first int) float64 {102+ total := 0.0103+ for i, v := range flows {104+ total += v / math.Pow(1+r, float64(i+first))105+ }106+ return total107+}108+109+// rate solves the annuity equation for the rate with Newton's method, as110+// Sheets does, starting from the guess (10% by default).111+func rate(args []Node, get lookup) Value {112+ x := make([]float64, 6)113+ for i := range x {114+ def := 0.0115+ if i == 5 {116+ def = 0.1117+ }118+ v, err := optNum(args, i, def, get)119+ if err != nil {120+ return *err121+ }122+ x[i] = v123+ }124+ n, pmt, pv, fv, t, r := x[0], x[1], x[2], x[3], x[4], x[5]125+ if t != 0 {126+ t = 1127+ }128+ if n <= 0 {129+ return ErrNum130+ }131+ f := func(r float64) float64 {132+ if math.Abs(r) < 1e-12 {133+ return pv + pmt*n + fv134+ }135+ g := math.Pow(1+r, n)136+ return pv*g + pmt*(1+r*t)*(g-1)/r + fv137+ }138+ for range 100 {139+ y := f(r)140+ h := 1e-7 * math.Max(1, math.Abs(r))141+ d := (f(r+h) - f(r-h)) / (2 * h)142+ if d == 0 || math.IsNaN(d) {143+ return ErrNum144+ }145+ next := r - y/d146+ if next <= -1 {147+ next = (r - 1) / 2148+ }149+ if math.Abs(next-r) < 1e-12 {150+ return num(next)151+ }152+ r = next153+ }154+ return ErrNum155+}156+157+// irr finds the rate where the cash flows' present value is zero: Newton158+// from the guess, falling back to bisection.159+func irr(args []Node, get lookup) Value {160+ flows, err := nums(args[:1], get)161+ if err != nil {162+ return *err163+ }164+ guess, err := optNum(args, 1, 0.1, get)165+ if err != nil {166+ return *err167+ }168+ pos, neg := false, false169+ for _, v := range flows {170+ pos = pos || v > 0171+ neg = neg || v < 0172+ }173+ if !pos || !neg {174+ return ErrNum175+ }176+ f := func(r float64) float64 { return npv(r, flows, 0) }177+ df := func(r float64) float64 {178+ total := 0.0179+ for i, v := range flows {180+ total -= float64(i) * v / math.Pow(1+r, float64(i+1))181+ }182+ return total183+ }184+ r := guess185+ for range 50 {186+ d := df(r)187+ if d == 0 || math.IsNaN(d) {188+ break189+ }190+ next := r - f(r)/d191+ if next <= -1 || math.IsNaN(next) || math.IsInf(next, 0) {192+ break193+ }194+ if math.Abs(next-r) < 1e-12 {195+ return num(next)196+ }197+ r = next198+ }199+ // Bisection over a bracket where the sign changes.200+ lo, hi := -0.9999999, 1.0201+ for f(hi)*f(lo) > 0 && hi < 1e6 {202+ hi *= 2203+ }204+ if f(hi)*f(lo) > 0 {205+ return ErrNum206+ }207+ for range 200 {208+ mid := (lo + hi) / 2209+ if f(lo)*f(mid) <= 0 {210+ hi = mid211+ } else {212+ lo = mid213+ }214+ }215+ return num((lo + hi) / 2)216+}
A internal/sheet/functions_finance_test.go +26 -0