From bc54c9a5d453bd73a02b412d259e86b6aba09622 Mon Sep 17 00:00:00 2001 From: Luciano Date: Thu, 2 Jul 2020 23:28:02 +0100 Subject: [PATCH] Added mirr() --- README.md | 20 +++++++------- package.json | 2 +- src/financial.ts | 61 ++++++++++++++++++++++++++++++++++++++++-- test/financial.test.ts | 15 ++++++++++- 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index becb97a..ac924c4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index d7edf95..522a68f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "financial", "description": "A Zero-dependency TypeScript/JavaScript port of numpy-financial", "author": "Luciano Mammino (https://loige.co)", - "version": "0.0.18", + "version": "0.1.0", "repository": { "type": "git", "url": "https://github.com/lmammino/financial.git" diff --git a/src/financial.ts b/src/financial.ts index 374bbd5..70a7f3e 100644 --- a/src/financial.ts +++ b/src/financial.ts @@ -1,5 +1,7 @@ /** * When payments are due + * + * @since v0.0.12 */ export enum PaymentDueTime { /** Payments due at the beginning of a period (1) */ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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). @@ -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' diff --git a/test/financial.test.ts b/test/financial.test.ts index 65cc69b..8c9edc2 100644 --- a/test/financial.test.ts +++ b/test/financial.test.ts @@ -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 @@ -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() + }) +})