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 all 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, error)
```
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
error : returns nil if rate difference is less than the threshold (returns an error 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, err := gofinancial.Rate(pv, fv, pmt, nper, when, maxIter, tolerance, initialGuess)
if err != nil {
fmt.Printf(err)
} else {
fmt.Printf("rate: %v ", rate)
}
// Output:
// rate: 0.06106257989825202
}
```
[Run on go-playground](https://play.golang.org/p/H2uybe1dbRj)
1 change: 1 addition & 0 deletions error_codes.go
Expand Up @@ -8,4 +8,5 @@ var (
ErrInvalidFrequency = errors.New("invalid frequency")
ErrNotEqual = errors.New("input values are not equal")
ErrOutOfBounds = errors.New("error in representing data as it is out of bounds")
ErrTolerence = errors.New("nan error as tolerence level exceeded")
)
81 changes: 81 additions & 0 deletions reducing_utils.go
Expand Up @@ -290,3 +290,84 @@ func Npv(rate decimal.Decimal, values []decimal.Decimal) decimal.Decimal {
}
return internalNpv
}

/*
This function computes the ratio that is used to find a single value that sets the non-liner equation to zero

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:
[WRW] 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
*/
func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, maxIter int64, tolerance, initialGuess decimal.Decimal) (decimal.Decimal, error) {
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))
// skip further loops if |nextIterRate-currentIterRate| < tolerance
if nextIterRate.Sub(currentIterRate).Abs().LessThan(tolerance) {
break
}
}

if nextIterRate.Sub(currentIterRate).Abs().GreaterThanOrEqual(tolerance) {
return decimal.Zero, ErrTolerence
}
return nextIterRate, nil
}
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
anyErr error
}{
{
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),
anyErr: nil,
}, {
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),
anyErr: nil,
}, {
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.Zero,
anyErr: ErrTolerence,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, err := 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); err != tt.anyErr || isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)) != nil {
t.Errorf("Rate returned (%v,%v), wanted (%v,%v)", got, err, tt.want, tt.anyErr)
}
})
}
}