diff --git a/README.md b/README.md index f07ccae..e9d45c6 100644 --- a/README.md +++ b/README.md @@ -24,64 +24,6 @@ Two things in one repo: go get github.com/shanehull/dozen/engine ``` -Two layers: pure functions for direct math, and `Engine` for stack/keystroke -behaviour that the app is built on. - -## Functional API - -Rates are per-period fractions (`0.05` = 5%). Inflows positive, outflows -negative. `engine.End` (ordinary annuity) or `engine.Begin` (annuity due). - -```go -import "github.com/shanehull/dozen/engine" - -pmt := engine.PMT(0.06/12, 360, 300000, 0, engine.End) -fmt.Printf("%.2f\n", -pmt) // 1798.65 - -npv := engine.NPV(0.10, -100000, 30000, 40000, 50000, 60000) -fmt.Printf("%.2f\n", npv) // 38877.13 - -irr, _ := engine.IRR(-100000, 40000, 50000, 30000) -fmt.Printf("%.2f%%\n", irr*100) // 24.89% -``` - -### TVM - -```go -FV(rate, n, pv, pmt float64, when Timing) float64 -PV(rate, n, pmt, fv float64, when Timing) float64 -PMT(rate, n, pv, fv float64, when Timing) float64 -NPer(rate, pmt, pv, fv float64, when Timing) (float64, error) -Rate(n, pmt, pv, fv float64, when Timing) (float64, error) -SolveTVM(n, i, pv, pmt, fv float64, when Timing) (float64, error) -``` - -`SolveTVM` is the unified solver — pass `math.NaN()` for exactly one variable. - -```go -i, _ := engine.SolveTVM(360, math.NaN(), 300000, -1798.65, 0, engine.End) -fmt.Printf("%.4f%%\n", i*12*100) // 6.00% APR -``` - -### Cash flows - -```go -NPV(rate float64, cashflows ...float64) float64 -IRR(cashflows ...float64) (float64, error) -``` - -### Depreciation - -`factorPct` = declining-balance factor as percent (200 = double-declining). - -```go -DepSL(cost, salvage, life, period float64) (dep, remaining float64) -DepSOYD(cost, salvage, life, period float64) (dep, remaining float64) -DepDB(cost, salvage, life, period, factorPct float64) (dep, remaining float64) -``` - -## Engine - `Engine` is an RPN calculator with a four-level stack, 20 memory registers, TVM and cash-flow slots, statistics accumulation, and program memory. @@ -91,9 +33,9 @@ c := engine.New() c.X = 2; c.Enter(); c.X = 3 c.Add() // c.X == 5 -c.X = 360; c.SetN() // FinN = 360 -c.X = 5; c.SetI() // FinI = 5 -c.X = -1000; c.SetPV() // FinPV = -1000 +c.SetN(360) // FinN = 360 +c.SetI(5) // FinI = 5 +c.SetPV(-1000) // FinPV = -1000 c.SolvePMT() // c.X == monthly payment c.X = 4; c.Sqrt() // c.X == 2 @@ -109,7 +51,7 @@ c.Restore(state) | ------------ | ---------------------------------------------------------------------------------------------- | | Stack | `Enter`, `Clx`, `Chs`, `XY`, `RollDown`, `RollUp`, `LastXRecall` | | Arithmetic | `Add`, `Sub`, `Mul`, `Div`, `YPowX` | -| TVM store | `SetN`, `SetI`, `SetPV`, `SetPMT`, `SetFV` | +| TVM store | `SetN(n)`, `SetI(i)`, `SetPV(pv)`, `SetPMT(pmt)`, `SetFV(fv)` | | TVM solve | `SolveN`, `SolveI`, `SolvePV`, `SolvePMT`, `SolveFV` | | Cash flow | `NPV`, `IRR` | | Amortization | `Amortize` | @@ -121,8 +63,7 @@ c.Restore(state) | Percent | `Pct`, `PctChg`, `PctTotal` | | Utility | `Abs`, `Intg`, `Frac`, `Exp`, `Exp10` | | Date | `DaysBetween`, `DateAdd` | -| Memory | `Store(n)`, `Recall(n)` | -| Program | `SST`, `BST`, `Goto(line)` | +| Memory | `Store(n)`, `Recall(n)` || Program | `SST`, `BST`, `Goto(line)` | | State | `Snapshot`, `Restore` | | Clear | `ClearFin`, `ClearReg`, `ClearStats`, `ClearPgm` | diff --git a/calculator.go b/calculator.go index 83eb848..92f11f4 100644 --- a/calculator.go +++ b/calculator.go @@ -158,15 +158,15 @@ func (c *CalcService) PressKey(input KeyInput) KeyResult { func (c *CalcService) storeTVM(op string) { switch op { case "n": - c.e.SetN() + c.e.SetN(c.e.X) case "i": - c.e.SetI() + c.e.SetI(c.e.X) case "PV": - c.e.SetPV() + c.e.SetPV(c.e.X) case "PMT": - c.e.SetPMT() + c.e.SetPMT(c.e.X) case "FV": - c.e.SetFV() + c.e.SetFV(c.e.X) } c.e.Flags.StackLift = false } diff --git a/engine/engine.go b/engine/engine.go index c842860..e0ebc3a 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -2,8 +2,6 @@ package engine import "math" -const MaxDigits = 10 - type AngleMode int const ( diff --git a/engine/engine_test.go b/engine/engine_test.go index 033f923..4418578 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -425,8 +425,7 @@ func TestClearRegs(t *testing.T) { func TestPrefixF(t *testing.T) { e := New() - e.X = 42 - e.SetN() + e.SetN(42) if e.FinN != 42 { t.Fatalf("f+n: want FinN=42, got %v", e.FinN) } diff --git a/engine/finance.go b/engine/finance.go deleted file mode 100644 index a6219e7..0000000 --- a/engine/finance.go +++ /dev/null @@ -1,227 +0,0 @@ -package engine - -import ( - "errors" - "math" -) - -// This file is the library API: pure financial functions in HP-12C -// terminology, with no dependency on the calculator's stack, prefixes, or -// keystroke model. The Engine (Step, X/Y/Z/T, ...) is a calculator front-end -// that delegates to these functions; use these directly when you just want the -// math. -// -// Sign convention (same as the HP-12C): cash inflows are positive and outflows -// negative, so a loan taken out has a positive PV and negative PMT. Rates are -// per period, expressed as a fraction (0.05 == 5%). n is the number of periods. - -// Timing selects whether payments occur at the end of each period (an ordinary -// annuity) or the beginning (an annuity due, "BEGIN" mode on the HP-12C). -type Timing bool - -const ( - End Timing = false - Begin Timing = true -) - -var ( - // ErrNoConvergence is returned by Rate/IRR when the iterative solver fails - // to converge (e.g. a cash-flow stream with no valid internal rate). - ErrNoConvergence = errors.New("engine: solution did not converge") - // ErrNoSolution is returned when the inputs admit no finite answer. - ErrNoSolution = errors.New("engine: no solution for the given inputs") -) - -// FV returns the future value of a series of cash flows. -func FV(rate, n, pv, pmt float64, when Timing) float64 { - return tvmFV(rate, n, pv, pmt, bool(when)) -} - -// PV returns the present value of a series of cash flows. -func PV(rate, n, pmt, fv float64, when Timing) float64 { - return tvmPV(rate, n, pmt, fv, bool(when)) -} - -// PMT returns the periodic payment for a loan or annuity. -func PMT(rate, n, pv, fv float64, when Timing) float64 { - return tvmPMT(rate, n, pv, fv, bool(when)) -} - -// NPer solves for the number of periods. It returns ErrNoSolution when the -// cash flows never reach the target future value. -func NPer(rate, pmt, pv, fv float64, when Timing) (float64, error) { - if isZero(rate) { - if isZero(pmt) { - return 0, ErrNoSolution - } - return clamp(-(fv + pv) / pmt), nil - } - b := 0.0 - if when { - b = 1 - } - adj := pmt * (1 + rate*b) / rate - den := pv + adj - if isZero(den) { - return 0, ErrNoSolution - } - num := adj - fv - if num/den <= 0 { - return 0, ErrNoSolution - } - return clamp(math.Log(num/den) / math.Log(1+rate)), nil -} - -// Rate solves for the periodic interest rate (as a fraction) using -// Newton-Raphson iteration. It returns ErrNoConvergence if no rate is found. -func Rate(n, pmt, pv, fv float64, when Timing) (float64, error) { - begin := bool(when) - guess := 0.001 - for iter := 0; iter < 100; iter++ { - f := tvmEq(guess, n, pv, pmt, fv, begin) - fp := tvmEqDeriv(guess, n, pv, pmt, fv, begin) - if isZero(fp) { - return 0, ErrNoConvergence - } - next := guess - f/fp - if math.Abs(next-guess) < 1e-10 { - return clamp(next), nil - } - guess = next - } - return 0, ErrNoConvergence -} - -// SolveTVM solves for the missing time-value-of-money variable. Pass -// math.NaN() for exactly one of n, i, pv, pmt, or fv. i is the periodic -// rate as a fraction (0.05 = 5%). The result is the solved value or an -// error if the solver fails to converge. -func SolveTVM(n, i, pv, pmt, fv float64, when Timing) (float64, error) { - var missing int - if math.IsNaN(n) { - missing++ - } - if math.IsNaN(i) { - missing++ - } - if math.IsNaN(pv) { - missing++ - } - if math.IsNaN(pmt) { - missing++ - } - if math.IsNaN(fv) { - missing++ - } - if missing != 1 { - return 0, ErrNoSolution - } - switch { - case math.IsNaN(n): - return NPer(i, pmt, pv, fv, when) - case math.IsNaN(i): - return Rate(n, pmt, pv, fv, when) - case math.IsNaN(pv): - return PV(i, n, pmt, fv, when), nil - case math.IsNaN(pmt): - return PMT(i, n, pv, fv, when), nil - case math.IsNaN(fv): - return FV(i, n, pv, pmt, when), nil - } - return 0, ErrNoSolution -} - -// NPV returns the net present value of a cash-flow stream discounted at rate -// (a fraction). cashflows[0] is the period-0 flow (typically the initial -// outlay); cashflows[k] occurs at the end of period k. -func NPV(rate float64, cashflows ...float64) float64 { - if len(cashflows) == 0 { - return 0 - } - npv := cashflows[0] - for k := 1; k < len(cashflows); k++ { - npv += cashflows[k] / math.Pow(1+rate, float64(k)) - } - return clamp(npv) -} - -// IRR returns the internal rate of return (as a fraction) of a cash-flow -// stream, i.e. the rate at which NPV is zero. cashflows[0] is the period-0 -// flow. It returns ErrNoConvergence if the solver does not converge. -func IRR(cashflows ...float64) (float64, error) { - guess := 0.1 - for iter := 0; iter < 100; iter++ { - f := NPV(guess, cashflows...) - d := npvDeriv(guess, cashflows) - if isZero(d) { - return 0, ErrNoConvergence - } - next := guess - f/d - if math.Abs(next-guess) < 1e-10 { - return clamp(next), nil - } - guess = next - } - return 0, ErrNoConvergence -} - -func npvDeriv(rate float64, cashflows []float64) float64 { - d := 0.0 - for k := 1; k < len(cashflows); k++ { - d -= float64(k) * cashflows[k] / math.Pow(1+rate, float64(k+1)) - } - return d -} - -// DepSL returns straight-line depreciation for the given period and the -// remaining depreciable value after that period. -func DepSL(cost, salvage, life, period float64) (dep, remaining float64) { - if isZero(life) { - return math.NaN(), math.NaN() - } - dep = (cost - salvage) / life - remaining = (cost - salvage) - dep*period - return clamp(dep), clamp(remaining) -} - -// DepSOYD returns sum-of-the-years'-digits depreciation for the given period -// and the remaining depreciable value after that period. -func DepSOYD(cost, salvage, life, period float64) (dep, remaining float64) { - if isZero(life) { - return math.NaN(), math.NaN() - } - soyd := life * (life + 1) / 2 - dep = (cost - salvage) * (life - period + 1) / soyd - acc := 0.0 - for k := 1.0; k <= period; k++ { - acc += (cost - salvage) * (life - k + 1) / soyd - } - return clamp(dep), clamp((cost - salvage) - acc) -} - -// DepDB returns declining-balance depreciation for the given period and the -// remaining book value (above salvage) after that period. factorPct is the -// declining-balance factor as a percent (200 == double-declining balance); -// zero defaults to 200%. -func DepDB(cost, salvage, life, period, factorPct float64) (dep, remaining float64) { - if isZero(life) { - return math.NaN(), math.NaN() - } - factor := factorPct / 100 - if isZero(factor) { - factor = 2 - } - rate := factor / life - book := cost - for k := 1.0; k <= period; k++ { - dep = book * rate - if book-dep < salvage { - dep = book - salvage - } - if dep < 0 { - dep = 0 - } - book -= dep - } - return clamp(dep), clamp(book - salvage) -} diff --git a/engine/finance_example_test.go b/engine/finance_example_test.go deleted file mode 100644 index 0ee2c26..0000000 --- a/engine/finance_example_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package engine_test - -import ( - "fmt" - - "github.com/shanehull/dozen/engine" -) - -// The functional API needs no calculator, stack, or keystrokes — just call the -// function with the values you already have. - -func ExamplePMT() { - // A $300,000 mortgage at 6% APR over 30 years, paid monthly. - pmt := engine.PMT(0.06/12, 360, 300000, 0, engine.End) - fmt.Printf("Monthly payment: $%.2f\n", -pmt) - // Output: Monthly payment: $1798.65 -} - -func ExampleFV() { - // $10,000 invested for 20 years at 8% (outflow now, so PV is negative). - fv := engine.FV(0.08, 20, -10000, 0, engine.End) - fmt.Printf("Future value: $%.2f\n", fv) - // Output: Future value: $46609.57 -} - -func ExampleRate() { - // What rate turns $1,000 today into $2,000 after 12 periods? - r, _ := engine.Rate(12, 0, -1000, 2000, engine.End) - fmt.Printf("Rate: %.3f%%\n", r*100) - // Output: Rate: 5.946% -} - -func ExampleNPV() { - npv := engine.NPV(0.10, -100000, 30000, 40000, 50000, 60000) - fmt.Printf("NPV: $%.2f\n", npv) - // Output: NPV: $38877.13 -} - -func ExampleIRR() { - irr, _ := engine.IRR(-100000, 40000, 50000, 30000) - fmt.Printf("IRR: %.2f%%\n", irr*100) - // Output: IRR: 10.13% -} - -func ExampleDepSL() { - // $50,000 asset, $5,000 salvage, 10-year life, first year. - dep, remaining := engine.DepSL(50000, 5000, 10, 1) - fmt.Printf("Year 1 depreciation: $%.2f, remaining: $%.2f\n", dep, remaining) - // Output: Year 1 depreciation: $4500.00, remaining: $40500.00 -} diff --git a/engine/financial.go b/engine/financial.go index cb094ac..a8d81db 100644 --- a/engine/financial.go +++ b/engine/financial.go @@ -2,15 +2,15 @@ package engine import "math" -func (e *Engine) SetN() { e.FinN = e.X } -func (e *Engine) SetI() { e.FinI = e.X } -func (e *Engine) SetPV() { e.FinPV = e.X } -func (e *Engine) SetPMT() { e.FinPMT = e.X } -func (e *Engine) SetFV() { e.FinFV = e.X } +func (e *Engine) SetN(n float64) { e.FinN = n } +func (e *Engine) SetI(i float64) { e.FinI = i } +func (e *Engine) SetPV(pv float64) { e.FinPV = pv } +func (e *Engine) SetPMT(pmt float64) { e.FinPMT = pmt } +func (e *Engine) SetFV(fv float64) { e.FinFV = fv } -func (e *Engine) SolveN() { e.solve(NaNf64(), e.FinI/100, e.FinPV, e.FinPMT, e.FinFV) } +func (e *Engine) SolveN() { e.solve(math.NaN(), e.FinI/100, e.FinPV, e.FinPMT, e.FinFV) } func (e *Engine) SolveI() { - v, err := SolveTVM(e.FinN, NaNf64(), e.FinPV, e.FinPMT, e.FinFV, Timing(e.Flags.Begin)) + v, err := solveTVM(e.FinN, math.NaN(), e.FinPV, e.FinPMT, e.FinFV, timing(e.Flags.Begin)) if err != nil { e.X = math.NaN() return @@ -19,12 +19,12 @@ func (e *Engine) SolveI() { e.X = clamp(v * 100) e.Flags.StackLift = true } -func (e *Engine) SolvePV() { e.solve(e.FinN, e.FinI/100, NaNf64(), e.FinPMT, e.FinFV) } -func (e *Engine) SolvePMT() { e.solve(e.FinN, e.FinI/100, e.FinPV, NaNf64(), e.FinFV) } -func (e *Engine) SolveFV() { e.solve(e.FinN, e.FinI/100, e.FinPV, e.FinPMT, NaNf64()) } +func (e *Engine) SolvePV() { e.solve(e.FinN, e.FinI/100, math.NaN(), e.FinPMT, e.FinFV) } +func (e *Engine) SolvePMT() { e.solve(e.FinN, e.FinI/100, e.FinPV, math.NaN(), e.FinFV) } +func (e *Engine) SolveFV() { e.solve(e.FinN, e.FinI/100, e.FinPV, e.FinPMT, math.NaN()) } func (e *Engine) solve(n, i, pv, pmt, fv float64) { - v, err := SolveTVM(n, i, pv, pmt, fv, Timing(e.Flags.Begin)) + v, err := solveTVM(n, i, pv, pmt, fv, timing(e.Flags.Begin)) if err != nil { e.X = math.NaN() return @@ -34,8 +34,6 @@ func (e *Engine) solve(n, i, pv, pmt, fv float64) { e.Flags.StackLift = true } -func NaNf64() float64 { return math.NaN() } - func tvmPV(i, n, pmt, fv float64, begin bool) float64 { if isZero(i) { return clamp(-(fv + pmt*n)) @@ -100,39 +98,20 @@ func (e *Engine) cashflows() []float64 { } func (e *Engine) NPV() { - e.X = clamp(NPV(e.X/100, e.cashflows()...)) + e.X = clamp(npv(e.X/100, e.cashflows()...)) e.Flags.StackLift = true } func (e *Engine) IRR() { - r, _ := IRR(e.cashflows()...) + r, _ := irr(e.cashflows()...) e.result(r * 100) } func (e *Engine) Amortize() { - n := e.X - i := e.FinI / 100 - pv := e.FinPV - pmt := e.FinPMT - begin := e.Flags.Begin - totalInt, totalPrin := 0.0, 0.0 - for k := 1; k <= int(n); k++ { - interest := pv * i - if begin { - interest = 0 - pv -= pmt - if pv < 0 { - pv = 0 - } - } else { - pv -= pmt - interest - } - totalInt += interest - totalPrin += pmt - interest - } - e.AmortInt = clamp(totalInt) - e.AmortPrin = clamp(totalPrin) - e.AmortN = n + totalInt, totalPrin := amort(e.FinI/100, e.X, e.FinPV, e.FinPMT, bool(e.Flags.Begin)) + e.AmortInt = totalInt + e.AmortPrin = totalPrin + e.AmortN = e.X e.X = e.AmortInt e.Push() e.X = e.AmortPrin @@ -141,52 +120,21 @@ func (e *Engine) Amortize() { } func (e *Engine) BondPrice() { - ytm := e.Y / 100 - coupon := e.X - n := e.FinN - price := 0.0 - for k := 1.0; k <= n; k++ { - price += coupon / 2 / math.Pow(1+ytm/2, k) - } - price += 100 / math.Pow(1+ytm/2, n) + price := bondPrice(e.Y/100, e.X, e.FinN) e.LastX = e.X - e.X = clamp(price) + e.X = price e.Tuck() e.Flags.StackLift = true } func (e *Engine) BondYield() { - price := e.X - coupon := e.Y - n := e.FinN - guess := coupon / price - for iter := 0; iter < 100; iter++ { - p := 0.0 - for k := 1.0; k <= n; k++ { - p += coupon / 2 / math.Pow(1+guess/2, k) - } - p += 100 / math.Pow(1+guess/2, n) - f := p - price - d := 0.0 - for k := 1.0; k <= n; k++ { - d -= float64(k) * coupon / (4 * math.Pow(1+guess/2, k+1)) - } - d -= n * 100 / (4 * math.Pow(1+guess/2, n+1)) - if isZero(d) { - break - } - next := guess - f/d - if math.Abs(next-guess) < 1e-10 { - e.LastX = e.X - e.X = clamp(next * 100) - e.Tuck() - e.Flags.StackLift = true - return - } - guess = next + ytm, err := bondYield(e.X, e.Y, e.FinN) + if err != nil { + e.X = math.NaN() + return } e.LastX = e.X - e.X = clamp(guess * 100) + e.X = clamp(ytm * 100) e.Tuck() e.Flags.StackLift = true } @@ -199,7 +147,7 @@ func (e *Engine) depResult(dep, remaining float64) { } func (e *Engine) DepreciationSL() { - dep, rem := DepSL(e.FinPV, e.FinFV, e.FinN, e.X) + dep, rem := depSL(e.FinPV, e.FinFV, e.FinN, e.X) if math.IsNaN(dep) { e.X = math.NaN() return @@ -208,7 +156,7 @@ func (e *Engine) DepreciationSL() { } func (e *Engine) DepreciationSOYD() { - dep, rem := DepSOYD(e.FinPV, e.FinFV, e.FinN, e.X) + dep, rem := depSOYD(e.FinPV, e.FinFV, e.FinN, e.X) if math.IsNaN(dep) { e.X = math.NaN() return @@ -217,7 +165,7 @@ func (e *Engine) DepreciationSOYD() { } func (e *Engine) DepreciationDB() { - dep, rem := DepDB(e.FinPV, e.FinFV, e.FinN, e.X, e.FinI) + dep, rem := depDB(e.FinPV, e.FinFV, e.FinN, e.X, e.FinI) if math.IsNaN(dep) { e.X = math.NaN() return diff --git a/engine/math.go b/engine/math.go new file mode 100644 index 0000000..160c00a --- /dev/null +++ b/engine/math.go @@ -0,0 +1,238 @@ +package engine + +import ( + "errors" + "math" +) + +// TVM and cash-flow math, shared by the Engine methods. All functions in +// this file are package‑private — the Engine is the public RPN interface. + +// Sign convention (same as the HP-12C): cash inflows are positive and outflows +// negative, so a loan taken out has a positive PV and negative PMT. Rates are +// per period, expressed as a fraction (0.05 == 5%). n is the number of periods. + +type timing bool + +var ( + errNoConvergence = errors.New("engine: solution did not converge") + errNoSolution = errors.New("engine: no solution for the given inputs") +) + +func fv(rate, n, pv, pmt float64, when timing) float64 { + return tvmFV(rate, n, pv, pmt, bool(when)) +} + +func pv(rate, n, pmt, fv float64, when timing) float64 { + return tvmPV(rate, n, pmt, fv, bool(when)) +} + +func pmt(rate, n, pv, fv float64, when timing) float64 { + return tvmPMT(rate, n, pv, fv, bool(when)) +} + +func nper(rate, pmt, pv, fv float64, when timing) (float64, error) { + if isZero(rate) { + if isZero(pmt) { + return 0, errNoSolution + } + return clamp(-(fv + pv) / pmt), nil + } + b := 0.0 + if when { + b = 1 + } + adj := pmt * (1 + rate*b) / rate + den := pv + adj + if isZero(den) { + return 0, errNoSolution + } + num := adj - fv + if num/den <= 0 { + return 0, errNoSolution + } + return clamp(math.Log(num/den) / math.Log(1+rate)), nil +} + +func rate(n, pmt, pv, fv float64, when timing) (float64, error) { + beg := bool(when) + guess := 0.001 + for iter := 0; iter < 100; iter++ { + f := tvmEq(guess, n, pv, pmt, fv, beg) + fp := tvmEqDeriv(guess, n, pv, pmt, fv, beg) + if isZero(fp) { + return 0, errNoConvergence + } + next := guess - f/fp + if math.Abs(next-guess) < 1e-10 { + return clamp(next), nil + } + guess = next + } + return 0, errNoConvergence +} + +func solveTVM(N, I, PV, PMT, FV float64, when timing) (float64, error) { + var missing int + if math.IsNaN(N) { + missing++ + } + if math.IsNaN(I) { + missing++ + } + if math.IsNaN(PV) { + missing++ + } + if math.IsNaN(PMT) { + missing++ + } + if math.IsNaN(FV) { + missing++ + } + if missing != 1 { + return 0, errNoSolution + } + switch { + case math.IsNaN(N): + return nper(I, PMT, PV, FV, when) + case math.IsNaN(I): + return rate(N, PMT, PV, FV, when) + case math.IsNaN(PV): + return pv(I, N, PMT, FV, when), nil + case math.IsNaN(PMT): + return pmt(I, N, PV, FV, when), nil + case math.IsNaN(FV): + return fv(I, N, PV, PMT, when), nil + } + return 0, errNoSolution +} + +func npv(rate float64, cashflows ...float64) float64 { + if len(cashflows) == 0 { + return 0 + } + v := cashflows[0] + for k := 1; k < len(cashflows); k++ { + v += cashflows[k] / math.Pow(1+rate, float64(k)) + } + return clamp(v) +} + +func irr(cashflows ...float64) (float64, error) { + guess := 0.1 + for iter := 0; iter < 100; iter++ { + f := npv(guess, cashflows...) + d := npvDeriv(guess, cashflows) + if isZero(d) { + return 0, errNoConvergence + } + next := guess - f/d + if math.Abs(next-guess) < 1e-10 { + return clamp(next), nil + } + guess = next + } + return 0, errNoConvergence +} + +func npvDeriv(rate float64, cashflows []float64) float64 { + d := 0.0 + for k := 1; k < len(cashflows); k++ { + d -= float64(k) * cashflows[k] / math.Pow(1+rate, float64(k+1)) + } + return d +} + +func depSL(cost, salvage, life, period float64) (dep, remaining float64) { + if isZero(life) { + return math.NaN(), math.NaN() + } + dep = (cost - salvage) / life + remaining = (cost - salvage) - dep*period + return clamp(dep), clamp(remaining) +} + +func depSOYD(cost, salvage, life, period float64) (dep, remaining float64) { + if isZero(life) { + return math.NaN(), math.NaN() + } + soyd := life * (life + 1) / 2 + dep = (cost - salvage) * (life - period + 1) / soyd + acc := 0.0 + for k := 1.0; k <= period; k++ { + acc += (cost - salvage) * (life - k + 1) / soyd + } + return clamp(dep), clamp((cost - salvage) - acc) +} + +func depDB(cost, salvage, life, period, factorPct float64) (dep, remaining float64) { + if isZero(life) { + return math.NaN(), math.NaN() + } + factor := factorPct / 100 + if isZero(factor) { + factor = 2 + } + rate := factor / life + book := cost + for k := 1.0; k <= period; k++ { + dep = book * rate + if book-dep < salvage { + dep = book - salvage + } + if dep < 0 { + dep = 0 + } + book -= dep + } + return clamp(dep), clamp(book - salvage) +} + +func amort(i, n, pv, pmt float64, begin bool) (totalInt, totalPrin float64) { + for k := 1; k <= int(n); k++ { + interest := pv * i + if begin { + interest = 0 + pv += pmt + if pv < 0 { + pv = 0 + } + } else { + pv += pmt + interest + } + totalInt += interest + totalPrin += -pmt - interest + } + return clamp(totalInt), clamp(totalPrin) +} + +func bondPrice(ytm, coupon float64, n float64) float64 { + p := 0.0 + for k := 1.0; k <= n; k++ { + p += coupon / 2 / math.Pow(1+ytm/2, k) + } + p += 100 / math.Pow(1+ytm/2, n) + return clamp(p) +} + +func bondYield(price, coupon float64, n float64) (float64, error) { + guess := coupon / price + for iter := 0; iter < 100; iter++ { + p := bondPrice(guess, coupon, n) + f := p - price + d := 0.0 + for k := 1.0; k <= n; k++ { + d -= float64(k) * coupon / (4 * math.Pow(1+guess/2, k+1)) + } + d -= n * 100 / (4 * math.Pow(1+guess/2, n+1)) + if isZero(d) { + break + } + next := guess - f/d + if math.Abs(next-guess) < 1e-10 { + return clamp(next), nil + } + guess = next + } + return 0, errNoConvergence +} diff --git a/examples/fv/main.go b/examples/fv/main.go index 4cb32a1..ecc9750 100644 --- a/examples/fv/main.go +++ b/examples/fv/main.go @@ -7,7 +7,10 @@ import ( ) func main() { - // $10K invested for 20 years at 8% (outflow now → negative PV). - fv := engine.FV(0.08, 20, -10000, 0, engine.End) - fmt.Printf("$10K @ 8%% / 20yr → $%.2f\n", fv) + e := engine.New() + e.SetN(20) // 20 years + e.SetI(8) // 8% per period + e.SetPV(-10000) // $10,000 invested (outflow) + e.SolveFV() + fmt.Printf("$10K @ 8%% / 20yr → $%.2f\n", e.X) } diff --git a/examples/mortgage/main.go b/examples/mortgage/main.go index 4cf9386..d412e10 100644 --- a/examples/mortgage/main.go +++ b/examples/mortgage/main.go @@ -7,7 +7,11 @@ import ( ) func main() { - // $300K at 6% APR over 30 years, paid monthly. - pmt := engine.PMT(0.06/12, 360, 300000, 0, engine.End) - fmt.Printf("$300K @ 6%% / 30yr → $%.2f/mo\n", -pmt) + e := engine.New() + e.SetN(360) // 30 years × 12 months + e.SetI(0.5) // 6% APR ÷ 12 = 0.5% per month + e.SetPV(300000) // $300,000 loan + e.SetFV(0) // paid off at end + e.SolvePMT() + fmt.Printf("$300K @ 6%% / 30yr → $%.2f/mo\n", -e.X) } diff --git a/examples/npv/main.go b/examples/npv/main.go index 04ae04b..88c68be 100644 --- a/examples/npv/main.go +++ b/examples/npv/main.go @@ -7,9 +7,18 @@ import ( ) func main() { - // Initial outlay followed by four years of returns, discounted at 10%. - npv := engine.NPV(0.10, -100000, 30000, 40000, 50000, 60000) - irr, _ := engine.IRR(-100000, 30000, 40000, 50000, 60000) - fmt.Printf("NPV @ 10%%: $%.2f\n", npv) - fmt.Printf("IRR: %.2f%%\n", irr*100) + e := engine.New() + e.FinCF0 = -100000 + e.FinCFj[0] = 30000 + e.FinCFj[1] = 40000 + e.FinCFj[2] = 50000 + e.FinCFj[3] = 60000 + e.FinCfCnt = 4 + + e.X = 10 + e.NPV() + fmt.Printf("NPV @ 10%%: $%.2f\n", e.X) + + e.IRR() + fmt.Printf("IRR: %.2f%%\n", e.X) } diff --git a/examples/solve-tvm/main.go b/examples/solve-tvm/main.go index 074ad47..e7b7fe2 100644 --- a/examples/solve-tvm/main.go +++ b/examples/solve-tvm/main.go @@ -2,22 +2,31 @@ package main import ( "fmt" - "math" "github.com/shanehull/dozen/engine" ) func main() { - // $300K mortgage, 30 years, monthly payment of $1,798.65. - // What's the APR? Pass NaN for the unknown. - i, _ := engine.SolveTVM(360, math.NaN(), 300000, -1798.65, 0, engine.End) - fmt.Printf("Rate: %.2f%% APR\n", i*12*100) + e := engine.New() - // How many months to pay off with $2,000/mo? - n, _ := engine.SolveTVM(math.NaN(), 0.06/12, 300000, -2000, 0, engine.End) - fmt.Printf("Term: %.0f months at $2,000/mo\n", n) + // $300K mortgage, 30 years, $1,798.65/mo → what APR? + e.SetN(360) + e.SetPV(300000) + e.SetPMT(-1798.65) + e.SolveI() + fmt.Printf("Rate: %.2f%% APR\n", e.X*12) - // What loan can I afford at $1,500/mo over 30 years? - pv, _ := engine.SolveTVM(360, 0.06/12, math.NaN(), -1500, 0, engine.End) - fmt.Printf("Loan: $%.0f\n", pv) + // $300K at 6% APR with $2,000/mo → how many months? + e.SetI(0.5) // 6% APR ÷ 12 + e.SetPV(300000) + e.SetPMT(-2000) + e.SolveN() + fmt.Printf("Term: %.0f months at $2,000/mo\n", e.X) + + // 6% APR over 30 years, $1,500/mo → what loan? + e.SetN(360) + e.SetI(0.5) // 6% APR ÷ 12 + e.SetPMT(-1500) + e.SolvePV() + fmt.Printf("Loan: $%.0f\n", e.X) }