Skip to content

Commit

Permalink
Added mirr()
Browse files Browse the repository at this point in the history
  • Loading branch information
lmammino committed Jul 2, 2020
1 parent 2203c3e commit bc54c9a
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 14 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,16 @@ There's no `default` export in the ESM implementation, so you have to explicitel

## Implemented functions

- [X] `fv`
- [X] `pmt`
- [X] `nper`
- [X] `ipmt`
- [X] `ppmt`
- [X] `pv`
- [X] `rate`
- [X] `irr`
- [X] `npv`
- [ ] `mirr`
- [X] `fv` (since v0.0.12)
- [X] `pmt` (since v0.0.12)
- [X] `nper` (since v0.0.12)
- [X] `ipmt` (since v0.0.12)
- [X] `ppmt` (since v0.0.14)
- [X] `pv` (since v0.0.15)
- [X] `rate` (since v0.0.16)
- [X] `irr` (since v0.0.17)
- [X] `npv` (since v0.0.18)
- [X] `mirr` (since 0.1.0)


## Local Development
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "financial",
"description": "A Zero-dependency TypeScript/JavaScript port of numpy-financial",
"author": "Luciano Mammino <[email protected]> (https://loige.co)",
"version": "0.0.18",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/lmammino/financial.git"
Expand Down
61 changes: 59 additions & 2 deletions src/financial.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/**
* When payments are due
*
* @since v0.0.12
*/
export enum PaymentDueTime {
/** Payments due at the beginning of a period (1) */
Expand All @@ -19,6 +21,8 @@ export enum PaymentDueTime {
*
* @returns The value at the end of the `nper` periods
*
* @since v0.0.12
*
* ## Examples
*
* What is the future value after 10 years of saving $100 now, with
Expand Down Expand Up @@ -76,6 +80,8 @@ export function fv (rate: number, nper: number, pmt: number, pv: number, when :
*
* @returns the (fixed) periodic payment
*
* @since v0.0.12
*
* ## Examples
*
* What is the monthly payment needed to pay off a $200,000 loan in 15
Expand Down Expand Up @@ -141,6 +147,8 @@ export function pmt (rate: number, nper: number, pv: number, fv = 0, when = Paym
*
* @returns The number of periodic payments
*
* @since v0.0.12
*
* ## Examples
*
* If you only had $150/month to pay towards the loan, how long would it take
Expand Down Expand Up @@ -191,6 +199,8 @@ export function nper (rate: number, pmt: number, pv: number, fv = 0, when = Paym
*
* @returns Interest portion of payment
*
* @since v0.0.12
*
* ## Examples
*
* What is the amortization schedule for a 1 year loan of $2500 at
Expand Down Expand Up @@ -263,6 +273,8 @@ export function ipmt (rate: number, per: number, nper: number, pv: number, fv =
* @param when - When payments are due
*
* @returns the payment against loan principal
*
* @since v0.0.14
*/
export function ppmt (rate: number, per: number, nper: number, pv: number, fv = 0, when = PaymentDueTime.End) : number {
const total = pmt(rate, nper, pv, fv, when)
Expand All @@ -281,6 +293,8 @@ export function ppmt (rate: number, per: number, nper: number, pv: number, fv =
*
* @returns the present value of a payment or investment
*
* @since v0.0.15
*
* ## Examples
*
* What is the present value (e.g., the initial investment)
Expand Down Expand Up @@ -344,6 +358,8 @@ export function pv (rate: number, nper: number, pmt: number, fv = 0, when = Paym
* @returns the rate of interest per period (or `NaN` if it could
* not be computed within the number of iterations provided)
*
* @since v0.0.16
*
* ## Notes
*
* Use Newton's iteration until the change is less than 1e-6
Expand Down Expand Up @@ -413,6 +429,8 @@ export function rate (nper: number, pmt: number, pv: number, fv: number, when =
*
* @returns Internal Rate of Return for periodic input values
*
* @since v0.0.17
*
* ## Notes
*
* The IRR is perhaps best understood through an example (illustrated
Expand Down Expand Up @@ -464,7 +482,7 @@ export function irr (values: number[], guess = 0.1, tol = 1e-6, maxIter = 100):
const dates : number[] = []
let positive = false
let negative = false
for (var i = 0; i < values.length; i++) {
for (let i = 0; i < values.length; i++) {
dates[i] = (i === 0) ? 0 : dates[i - 1] + 365
if (values[i] > 0) {
positive = true
Expand All @@ -474,7 +492,8 @@ export function irr (values: number[], guess = 0.1, tol = 1e-6, maxIter = 100):
}
}

// Return error if values does not contain at least one positive value and one negative value
// Return error if values does not contain at least one positive
// value and one negative value
if (!positive || !negative) {
return Number.NaN
}
Expand Down Expand Up @@ -515,6 +534,8 @@ export function irr (values: number[], guess = 0.1, tol = 1e-6, maxIter = 100):
* investment, thus `values[0]` will typically be negative.
* @returns The NPV of the input cash flow series `values` at the discount `rate`.
*
* @since v0.0.18
*
* ## Warnings
*
* `npv considers a series of cashflows starting in the present (t = 0).
Expand Down Expand Up @@ -571,6 +592,42 @@ export function npv (rate: number, values: number[]) : number {
)
}

/**
* Calculates the Modified Internal Rate of Return.
*
* @param values - Cash flows (must contain at least one positive and one negative
* value) or nan is returned. The first value is considered a sunk
* cost at time zero.
* @param financeRate - Interest rate paid on the cash flows
* @param reinvestRate - Interest rate received on the cash flows upon reinvestment
*
* @returns Modified internal rate of return
*
* @since v0.1.0
*/
export function mirr (values: number[], financeRate: number, reinvestRate: number): number {
let positive = false
let negative = false
for (let i = 0; i < values.length; i++) {
if (values[i] > 0) {
positive = true
}
if (values[i] < 0) {
negative = true
}
}

// Return error if values does not contain at least one
// positive value and one negative value
if (!positive || !negative) {
return Number.NaN
}

const numer = Math.abs(npv(reinvestRate, values.map((x) => x > 0 ? x : 0)))
const denom = Math.abs(npv(financeRate, values.map(x => x < 0 ? x : 0)))
return (numer / denom) ** (1 / (values.length - 1)) * (1 + reinvestRate) - 1
}

/**
* This function is here to simply have a different name for the 'fv'
* function to not interfere with the 'fv' keyword argument within the 'ipmt'
Expand Down
15 changes: 14 additions & 1 deletion test/financial.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fv, pmt, nper, ipmt, ppmt, pv, rate, irr, npv, PaymentDueTime } from '../src/financial'
import { fv, pmt, nper, ipmt, ppmt, pv, rate, irr, npv, mirr, PaymentDueTime } from '../src/financial'

// Mostly based on
// https://github.com/numpy/numpy-financial/blob/master/numpy_financial/tests/test_financial.py
Expand Down Expand Up @@ -161,3 +161,16 @@ describe('npv()', () => {
expect(npv(0.05, [-15000, 1500, 2500, 3500, 4500, 6000])).toBeCloseTo(122.894855, 6)
})
})

describe('mirr()', () => {
it('calculates float', () => {
expect(mirr([-4500, -800, 800, 800, 600, 600, 800, 800, 700, 3000], 0.08, 0.055)).toBeCloseTo(0.066597, 6)
expect(mirr([-120000, 39000, 30000, 21000, 37000, 46000], 0.10, 0.12)).toBeCloseTo(0.126094, 6)
expect(mirr([100, 200, -50, 300, -200], 0.05, 0.06)).toBeCloseTo(0.342823, 6)
})

it('returns NaN if mirr() cannot be calculated', () => {
expect(mirr([39000, 30000, 21000, 37000, 46000], 0.10, 0.12)).toBeNaN()
expect(mirr([-39000, -30000, -21000, -37000, -46000], 0.10, 0.12)).toBeNaN()
})
})

0 comments on commit bc54c9a

Please sign in to comment.