diff --git a/README.md b/README.md index 28ca3ee..cee43ba 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ which are as follows: | nper | ✅ | Computes the number of periodic payments| | pv | ✅ | Computes the present value of a payment| | rate | ✅ | Computes the rate of interest per period| -| irr | | Computes the internal rate of return| +| irr | ✅ | Computes the internal rate of return| | npv | ✅ | Computes the net present value of a series of cash flow| -| mirr | | Computes the modified internal rate of return| +| mirr | ✅ | Computes the modified internal rate of return| # Index While the numpy-financial package contains a set of elementary financial functions, this pkg also contains some helper functions on top of it. Their usage and description can be found below: @@ -49,6 +49,10 @@ While the numpy-financial package contains a set of elementary financial functio + [Example(Nper-Loan)](#examplenper-loan) * [Rate(Interest Rate)](#rate) + [Example(Rate-Investment)](#examplerate-investment) + * [Irr(Internal Rate of Return)](#irr) + + [Example(Irr)](#exampleirr) + * [Mirr(Modified irr)](#mirr) + + [Example(Mirr)](#examplemirr) Detailed documentation is available at [godoc](https://godoc.org/github.com/razorpay/go-financial). ## Amortisation(Generate Table) @@ -638,3 +642,104 @@ func main() { } ``` [Run on go-playground](https://play.golang.org/p/H2uybe1dbRj) + +## Irr + +```go +func Irr(values []decimal.Decimal, maxIter int64, tolerance, prev_point, next_point decimal.Decimal) (decimal.Decimal, error) +``` + +Params: +```text +values : the value of the cash flow for that time period. Values provided here must be an array of float64 +maxIter : total number of iterations for which the function should run +tolerance : accept result only if the difference in iteration values is less than the tolerance provided +prev_point : an initial point to start approximating from +next_point : next point to use for secant +``` + +Returns: +```text +rate : a rate for the corresponding values +error : returns nil if NPV is close to zero (returns an error conversely) +``` + +Irr computes the rate to ensure a net zero cashflow + +### Example(Irr) + +If an initial inflow of $123400 is done followed by successive outflows of $36200, $54800 and $48100; for what value of rate does the npv evalute to zero ? (assuming 100 iterations, 1e-6 threshold and 0.1 and 0.2 as initial guessing points) + +```go +package main + +import ( + "fmt" + gofinancial "github.com/razorpay/go-financial" + "github.com/shopspring/decimal" +) + +func main() { + cashflow := []decimal.Decimal{decimal.NewFromFloat(-123400), decimal.NewFromFloat(36200), decimal.NewFromFloat(54800), decimal.NewFromFloat(48100)} + maxIter := 100 + tolerance := decimal.NewFromFloat(1e-6) + prev_point := decimal.NewFromFloat(0.1) + next_point := decimal.NewFromFloat(0.2) + + irr, err := gofinancial.Irr(cashflow, maxIter, tolerance, prev_point, next_point) + if err != nil { + fmt.Printf(err) + } else { + fmt.Printf("irr: %v ", irr) + } + // Output: + // irr: 0.05961637856732953787613704103503 +} +``` +[Run on go-playground](https://play.golang.org/p/izqvtBWEOqJ) + +## Mirr + +```go +func Mirr(cashflows []decimal.Decimal, financeRate, reinvestRate decimal.Decimal) decimal.Decimal +``` + +Params: +```text + cashflows: periodic chasflow statement + financeRate: interest rate for inflows + reinvestRate: interst received for reinvesting outflows +``` + +Returns: +```text +mirr : a value for the corresponding mirr +``` + +Mirr calculates the Modified internal rate of return for a given cashflow and finance and reinvestment rates + +### Example(Mirr) + + +```go +package main + +import ( + "fmt" + gofinancial "github.com/razorpay/go-financial" + "github.com/shopspring/decimal" +) + +func main() { + cashflow := []decimal.Decimal{decimal.NewFromInt(-1000), decimal.NewFromInt(-4000), decimal.NewFromInt(5000), decimal.NewFromInt(2000)} + financeRate := decimal.NewFromFloat(0.1) + reinvestRate := decimal.NewFromFloat(0.12) + + mirr := gofinancial.Mirr(cashflow,financeRate,reinvestRate) + + fmt.Printf("mirr: %v",mirr) + // Output: + // mirr: 0.1790856860348926 +} +``` +[Run on go-playground](https://play.golang.org/p/ci6pBUkX4jy) diff --git a/reducing_utils.go b/reducing_utils.go index 1220ecc..98e0265 100644 --- a/reducing_utils.go +++ b/reducing_utils.go @@ -371,3 +371,75 @@ func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, maxI } return nextIterRate, nil } + +/* +IRR computes the rate of return for a balanced cashflow (if feasible!) using the secant method of approximation. + +Params: + values : the value of the cash flow for that time period. Values provided here must be an array of float64 + maxIter : total number of iterations for which the function should run + tolerance : accept result only if the difference in iteration values is less than the tolerance provided + prevPoint : an initial point to start approximating from + nextPoint : next point to use for secant + +References: + [G] L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., + Addison-Wesley, 2003, pg. 348. +*/ +func Irr(values []decimal.Decimal, maxIter int64, tolerance, prevPoint, nextPoint decimal.Decimal) (decimal.Decimal, error) { + xP := prevPoint + xN := nextPoint + + // https://en.wikipedia.org/wiki/Secant_method + for i := int64(0); i < maxIter; i++ { + yP := Npv(xP, values) + yN := Npv(xN, values) + _v1 := xN.Sub(xP).Div(yN.Sub(yP).Add(decimal.NewFromFloat(0.0000001))) + _v2 := yN.Neg().Mul(_v1) + xN, xP = xN.Add(_v2), xN + + } + + if Npv(xN, values).LessThan(tolerance) { + return xN, nil + } + + return decimal.Zero, ErrOutOfBounds +} + +/* +Mirr calculates the Modified internal rate of return for a given cashflow and finance and reinvestment rates + +Params: + cashFlows : periodic chasflow statement + financeRate : interest rate for inflows + reinvestRate : interst received for reinvesting outflows +*/ +func Mirr(cashFlows []decimal.Decimal, financeRate, reinvestRate decimal.Decimal) decimal.Decimal { + var inflow, outflow []decimal.Decimal + + for _, v := range cashFlows { + if v.IsNegative() { + inflow = append(inflow, v) + outflow = append(outflow, decimal.Zero) + } + if v.IsPositive() { + outflow = append(outflow, v) + inflow = append(inflow, decimal.Zero) + } + } + + one := decimal.NewFromFloat(1) + n_1 := decimal.NewFromInt(int64(len(cashFlows) - 1)) + _pv := Npv(financeRate, inflow) + _fv := Npv(reinvestRate, outflow).Mul(one.Add(reinvestRate).Pow(n_1)) + + // ! shopspring decimal struct based underroot is imprecise (hence math library) + _x1 := _fv.Div(_pv.Neg()) + _go_x1, _ := _x1.Float64() + _go_x2, _ := one.Div(n_1).Float64() + _x3 := math.Pow(_go_x1, _go_x2) + + _mirr := decimal.NewFromFloat(_x3).Sub(one) + return _mirr +} diff --git a/reducing_utils_test.go b/reducing_utils_test.go index 36764c5..3cfd3a9 100644 --- a/reducing_utils_test.go +++ b/reducing_utils_test.go @@ -422,3 +422,103 @@ func Test_Rate(t *testing.T) { }) } } + +func Test_Irr(t *testing.T) { + type args struct { + cashFlow []decimal.Decimal + maxIter int64 + tolerance decimal.Decimal + prevPoint decimal.Decimal + nextPoint decimal.Decimal + } + tests := []struct { + name string + args args + want decimal.Decimal + anyErr error + }{ + { + name: "success", + args: args{ + cashFlow: []decimal.Decimal{decimal.NewFromFloat(1000), decimal.NewFromFloat(-900), decimal.NewFromFloat(200), decimal.NewFromFloat(-400)}, + maxIter: 100, + tolerance: decimal.NewFromFloat(1e-6), + prevPoint: decimal.NewFromFloat(0.1), + nextPoint: decimal.NewFromFloat(0.2), + }, + want: decimal.NewFromFloat(0.06491271005017), + anyErr: nil, + }, + { + name: "success", + args: args{ + cashFlow: []decimal.Decimal{decimal.NewFromFloat(-123400), decimal.NewFromFloat(36200), decimal.NewFromFloat(54800), decimal.NewFromFloat(48100)}, + maxIter: 100, + tolerance: decimal.NewFromFloat(1e-6), + prevPoint: decimal.NewFromFloat(0.1), + nextPoint: decimal.NewFromFloat(0.2), + }, + want: decimal.NewFromFloat(0.0596163785673), + anyErr: nil, + }, + { + name: "failure", + args: args{ + cashFlow: []decimal.Decimal{decimal.NewFromFloat(1000), decimal.NewFromFloat(-900), decimal.NewFromFloat(200)}, + maxIter: 100, + tolerance: decimal.NewFromFloat(1e-6), + prevPoint: decimal.NewFromFloat(0.1), + nextPoint: decimal.NewFromFloat(0.2), + }, + want: decimal.Zero, + anyErr: ErrOutOfBounds, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, err := Irr(tt.args.cashFlow, tt.args.maxIter, tt.args.tolerance, tt.args.prevPoint, tt.args.nextPoint); err != tt.anyErr || isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)) != nil { + t.Errorf("IRR returned (%v,%v), wanted (%v,%v)", got, err, tt.want, tt.anyErr) + } + }) + } +} + +func Test_Mirr(t *testing.T) { + type args struct { + cashFlows []decimal.Decimal + financeRate decimal.Decimal + reinvestRate decimal.Decimal + } + tests := []struct { + name string + args args + want decimal.Decimal + }{ + { + name: "success", args: args{ + cashFlows: []decimal.Decimal{decimal.NewFromInt(-1000), decimal.NewFromInt(-4000), decimal.NewFromInt(5000), decimal.NewFromInt(2000)}, + financeRate: decimal.NewFromFloat(0.1), + reinvestRate: decimal.NewFromFloat(0.12), + }, + want: decimal.NewFromFloat(0.1790856860), + }, + { + name: "success", args: args{ + cashFlows: []decimal.Decimal{decimal.NewFromInt(-2000), decimal.NewFromInt(2000), decimal.NewFromInt(2000)}, + financeRate: decimal.NewFromFloat(0.3), + reinvestRate: decimal.NewFromFloat(0.1), + }, + want: decimal.NewFromFloat(0.4491376746), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if res := Mirr(tt.args.cashFlows, tt.args.financeRate, tt.args.reinvestRate); isAlmostEqual(res, tt.want, decimal.NewFromFloat(precision)) != nil { + t.Errorf("Mirr returned (%v), wanted (%v)", res, tt.want) + } + }) + } +} +