Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate function added #17

Merged
merged 21 commits into from Jun 6, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 68 additions & 3 deletions README.md
Expand Up @@ -21,7 +21,7 @@ which are as follows:
| ppmt | ✅ | Computes principal payment for a loan|
| nper | ✅ | Computes the number of periodic payments|
| pv | ✅ | Computes the present value of a payment|
| rate | | Computes the rate of interest per period|
| rate | | Computes the rate of interest per period|
| 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|
Expand All @@ -45,9 +45,11 @@ While the numpy-financial package contains a set of elementary financial functio
+ [Example(IPmt-Investment)](#exampleipmt-investment)
* [PPmt(Principal Payment)](#ppmt)
+ [Example(PPmt-Loan)](#exampleppmt-loan)
* [Nper(Number of payments)](#nper)
* [Nper(Number of payments)](#nper)
+ [Example(Nper-Loan)](#examplenper-loan)

* [Rate(Interest Rate)](#rate)
+ [Example(Rate-Investment)](#examplerate-investment)

Detailed documentation is available at [godoc](https://godoc.org/github.com/razorpay/go-financial).
## Amortisation(Generate Table)

Expand Down Expand Up @@ -573,3 +575,66 @@ func main() {
}
```
[Run on go-playground](https://play.golang.org/p/hm77MTPBGYg)

## Rate

```go
func Rate(pv, fv, pmt float64, nper int64, when paymentperiod.Type, params ...float64) (float64, bool)
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
```
Params:
```text
pv : a present value
fv : a future value
pmt : a (fixed) payment, paid either at the beginning (when = 1)
or the end (when = 0) of each period
pv : a present value
fv : a future value
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
nper : total number of periods to be compounded for
when : specification of whether payment is made at the beginning (when = 1)
or the end (when = 0) of each period
params : varadic variable with following specifications:
0th index -> total number of iterations for which function should run (default is 100)
1st index -> an initial guess amount to start from (default is 0.1)
2nd index -> tolerance threshold (default is 1e-6)
```

Returns:
```text
rate : a value for the corresponding rate
valid : returns true if rate difference is less than the threshold (returns false conversely)
```

Rate computes the interest rate to ensure a balanced cashflow equation

### Example(Rate-Investment)

If an investment of $2000 is done and an amount of $100 is added at the start of each period, for what periodic interest rate would the invester be able to withdraw $3000 after the end of 4 periods ?

```go
package main

import (
"fmt"
gofinancial "github.com/razorpay/go-financial"
"github.com/razorpay/go-financial/enums/paymentperiod"
"github.com/shopspring/decimal"
)

func main() {
fv := decimal.NewFromFloat(-3000)
pmt := decimal.NewFromFloat(100)
pv := decimal.NewFromFloat(2000)
when := paymentperiod.BEGINNING
nper := decimal.NewFromInt(4)

rate, ok := gofinancial.Rate(pv, fv, pmt, nper, when)
if ok {
fmt.Printf("rate:%v ", rate)
} else {
fmt.Printf("NaN")
}
// Output:
// rate: 0.06106257989825202
}
```
[Run on go-playground](https://play.golang.org/p/I655r9QWu0H)
94 changes: 94 additions & 0 deletions reducing_utils.go
Expand Up @@ -290,3 +290,97 @@ func Npv(rate decimal.Decimal, values []decimal.Decimal) decimal.Decimal {
}
return internalNpv
}

/*
This function computs the ratio that is used to find a single value that sets the non-liner equation to zero
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved

Params:
nper : number of compounding periods
pmt : a (fixed) payment, paid either
at the beginning (when = 1) or the end (when = 0) of each period
pv : a present value
fv : a future value
when : specification of whether payment is made
at the beginning (when = 1) or the end (when = 0) of each period
curRate: the rate compounded once per period rate
*/
func getRateRatio(pv, fv, pmt, curRate decimal.Decimal, nper int64, when paymentperiod.Type) decimal.Decimal {
oneInDecimal := decimal.NewFromInt(1)
whenInDecimal := decimal.NewFromInt(when.Value())
nperInDecimal := decimal.NewFromInt(nper)

f0 := curRate.Add(oneInDecimal).Pow(decimal.NewFromInt(nper)) // f0 := math.Pow((1 + curRate), float64(nper))
f1 := f0.Div(curRate.Add(oneInDecimal)) // f1 := f0 / (1 + curRate)

yP0 := pv.Mul(f0)
yP1 := pmt.Mul(oneInDecimal.Add(curRate.Mul(whenInDecimal))).Mul(f0.Sub(oneInDecimal)).Div(curRate)
y := fv.Add(yP0).Add(yP1) // y := fv + pv*f0 + pmt*(1.0+curRate*when.Value())*(f0-1)/curRate

derivativeP0 := nperInDecimal.Mul(f1).Mul(pv)
derivativeP1 := pmt.Mul(whenInDecimal).Mul(f0.Sub(oneInDecimal)).Div(curRate)
derivativeP2s0 := oneInDecimal.Add(curRate.Mul(whenInDecimal))
derivativeP2s1 := ((curRate.Mul((nperInDecimal)).Mul(f1)).Sub(f0).Add(oneInDecimal)).Div(curRate.Mul(curRate))
derivativeP2 := derivativeP2s0.Mul(derivativeP2s1)
derivative := derivativeP0.Add(derivativeP1).Add(derivativeP2)
// derivative := (float64(nper) * f1 * pv) + (pmt * ((when.Value() * (f0 - 1) / curRate) + ((1.0 + curRate*when.Value()) * ((curRate*float64(nper)*f1 - f0 + 1) / (curRate * curRate)))))

return y.Div(derivative)
}

/*
Rate computes the Interest rate per period by running Newton Rapson to find an approximate value for:

y = fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate*((1+rate)**nper-1)
(0 - y_previous) /(rate - rate_previous) = dy/drate {derivative of y w.r.t. rate}


Params:
nper : number of compounding periods
pmt : a (fixed) payment, paid either
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
at the beginning (when = 1) or the end (when = 0) of each period
pv : a present value
fv : a future value
when : specification of whether payment is made
at the beginning (when = 1) or the end (when = 0) of each period
params : optional parameters for maxIter, tolerance, and initialGuess
References:
Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May). Open Document
Format for Office Applications (OpenDocument)v1.2, Part 2: Recalculated
Formula (OpenFormula) Format - Annotated Version, Pre-Draft 12.
Organization for the Advancement of Structured Information Standards
(OASIS). Billerica, MA, USA. [ODT Document]. Available:
http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
OpenDocument-formula-20090508.odt
*/

thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, params ...decimal.Decimal) (decimal.Decimal, bool) {
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
initialGuess := decimal.NewFromFloat(0.1)
tolerance := decimal.NewFromFloat(1e-6)
maxIter := 100

for index, value := range params {
switch index {
case 0:
maxIter = int(value.IntPart())
case 1:
initialGuess = value
case 2:
tolerance = value
default:
// no more values to be read
}
}

var nextIterRate, currentIterRate decimal.Decimal = initialGuess, initialGuess

for iter := 0; iter < maxIter; iter++ {
currentIterRate = nextIterRate
nextIterRate = currentIterRate.Sub(getRateRatio(pv, fv, pmt, currentIterRate, nper, when))
}

if nextIterRate.Sub(currentIterRate).Abs().GreaterThan(tolerance) {
return nextIterRate, false
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
}

return nextIterRate, true
}
42 changes: 42 additions & 0 deletions reducing_utils_test.go
Expand Up @@ -355,3 +355,45 @@ func Test_Nper(t *testing.T) {
})
}
}

func Test_rate(t *testing.T) {
type args struct {
pv decimal.Decimal
fv decimal.Decimal
pmt decimal.Decimal
nper int64
when paymentperiod.Type
}
tests := []struct {
name string
args args
want decimal.Decimal
}{
{
name: "success", args: args{
pv: decimal.NewFromInt(2000),
fv: decimal.NewFromInt(-3000),
pmt: decimal.NewFromInt(100),
nper: 4,
when: paymentperiod.BEGINNING,
},
want: decimal.NewFromFloat(0.06106257989825202),
}, {
name: "success", args: args{
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
pv: decimal.NewFromInt(-3000),
fv: decimal.NewFromInt(1000),
pmt: decimal.NewFromInt(500),
nper: 2,
when: paymentperiod.BEGINNING,
},
want: decimal.NewFromFloat(-0.25968757625671507),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, isValid := Rate(tt.args.pv, tt.args.fv, tt.args.pmt, tt.args.nper, tt.args.when); !isValid || isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)) != nil {
t.Errorf("rate() = (%v,%v), want (%v,%v)", got, isValid, tt.want, true)
}
})
}
}