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 17 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 decimal.Decimal, nper int64, when paymentperiod.Type, maxIter int64, tolerance, initialGuess decimal.Decimal) (decimal.Decimal, bool)
```
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
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
maxIter : total number of iterations for which function should run
tolerance : tolerance threshold for acceptable result
initialGuess : an initial guess amount to start from
```

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 ? (assuming 100 iterations, 1e-6 threshold and 0.1 as initial guessing point)

```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)
maxIter := 100
tolerance := decimal.NewFromFloat(1e-6)
initialGuess := decimal.NewFromFloat(0.1),

rate, ok := gofinancial.Rate(pv, fv, pmt, nper, when, maxIter, tolerance, initialGuess)
if ok {
fmt.Printf("rate:%v ", rate)
} else {
fmt.Printf("NaN")
}
// Output:
// rate: 0.06106257989825202
}
```
[Run on go-playground](https://play.golang.org/p/9khVcHwjkh5)
79 changes: 79 additions & 0 deletions reducing_utils.go
Expand Up @@ -290,3 +290,82 @@ 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
maxIter : total number of iterations to perform calculation
tolerance : accept result only if the difference in iteration values is less than the tolerance provided
initialGuess : an initial point to start approximating from
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
*/

thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, maxIter int64, tolerance, initialGuess decimal.Decimal) (decimal.Decimal, bool) {
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
var nextIterRate, currentIterRate decimal.Decimal = initialGuess, initialGuess

for iter := int64(0); iter < maxIter; iter++ {
thsubaku9 marked this conversation as resolved.
Show resolved Hide resolved
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
}
67 changes: 67 additions & 0 deletions reducing_utils_test.go
Expand Up @@ -355,3 +355,70 @@ 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
maxIter int64
tolerance decimal.Decimal
initialGuess decimal.Decimal
}
tests := []struct {
name string
args args
want decimal.Decimal
validity bool
}{
{
name: "success", args: args{
pv: decimal.NewFromInt(2000),
fv: decimal.NewFromInt(-3000),
pmt: decimal.NewFromInt(100),
nper: 4,
when: paymentperiod.BEGINNING,
maxIter: 100,
tolerance: decimal.NewFromFloat(1e-7),
initialGuess: decimal.NewFromFloat(0.1),
},
want: decimal.NewFromFloat(0.06106257989825202),
validity: true,
}, {
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,
maxIter: 100,
tolerance: decimal.NewFromFloat(1e-7),
initialGuess: decimal.NewFromFloat(0.1),
},
want: decimal.NewFromFloat(-0.25968757625671507),
validity: true,
}, {
name: "failure", args: args{
pv: decimal.NewFromInt(3000),
fv: decimal.NewFromInt(1000),
pmt: decimal.NewFromInt(100),
nper: 2,
when: paymentperiod.BEGINNING,
maxIter: 100,
tolerance: decimal.NewFromFloat(1e-7),
initialGuess: decimal.NewFromFloat(0.1),
},
want: decimal.NewFromFloat(0.4907342754506849),
validity: false,
},
}
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, tt.args.maxIter, tt.args.tolerance, tt.args.initialGuess); isValid != tt.validity || isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)) != nil {
t.Errorf("Rate returned (%v,%v), wanted (%v,%v)", got, isValid, tt.want, tt.validity)
}
})
}
}