Skip to content

Commit

Permalink
Added irr()
Browse files Browse the repository at this point in the history
  • Loading branch information
lmammino committed Jul 1, 2020
1 parent a282a65 commit 3314056
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ There's no `default` export in the ESM implementation, so you have to explicitel
- [X] `ppmt`
- [X] `pv`
- [X] `rate`
- [ ] `irr`
- [X] `irr`
- [ ] `npv`
- [ ] `mirr`

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.16",
"version": "0.0.17",
"repository": {
"type": "git",
"url": "https://github.com/lmammino/financial.git"
Expand Down
142 changes: 142 additions & 0 deletions src/financial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,113 @@ export function rate (nper: number, pmt: number, pv: number, fv: number, when =
return rn
}

/**
* Return the Internal Rate of Return (IRR).
*
* This is the "average" periodically compounded rate of return
* that gives a net present value of 0.0; for a more complete
* explanation, see Notes below.
*
* @param values - Input cash flows per time period.
* By convention, net "deposits"
* are negative and net "withdrawals" are positive. Thus, for
* example, at least the first element of `values`, which represents
* the initial investment, will typically be negative.
* @param guess - Starting guess for solving the Internal Rate of Return
* @param tol - Required tolerance for the solution
* @param maxIter - Maximum iterations in finding the solution
*
* @returns Internal Rate of Return for periodic input values
*
* ## Notes
*
* The IRR is perhaps best understood through an example (illustrated
* using `irr` in the Examples section below).
*
* Suppose one invests 100
* units and then makes the following withdrawals at regular (fixed)
* intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100
* unit investment yields 173 units; however, due to the combination of
* compounding and the periodic withdrawals, the "average" rate of return
* is neither simply 0.73/4 nor (1.73)^0.25-1.
* Rather, it is the solution (for `r`) of the equation:
*
* ```
* -100 + 39/(1+r) + 59/((1+r)^2) + 55/((1+r)^3) + 20/((1+r)^4) = 0
* ```
*
* In general, for `values` = `[0, 1, ... M]`,
* `irr` is the solution of the equation:
*
* ```
* \\sum_{t=0}^M{\\frac{v_t}{(1+irr)^{t}}} = 0
* ```
*
* ## Example
*
* ```javascript
* import { irr } from 'financial'
*
* irr([-100, 39, 59, 55, 20]) // 0.28095
* irr([-100, 0, 0, 74]) // -0.0955
* irr([-100, 100, 0, -7]) // -0.0833
* irr([-100, 100, 0, 7]) // 0.06206
* irr([-5, 10.5, 1, -8, 1]) // 0.0886
* ```
*
* ## References
*
* - L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed.,
* Addison-Wesley, 2003, pg. 348.
*/
export function irr (values: number[], guess = 0.1, tol = 1e-6, maxIter = 100): number {
// Based on https://gist.github.com/ghalimi/4591338 by @ghalimi
// ASF licensed (check the link for the full license)
// Credits: algorithm inspired by Apache OpenOffice

// Initialize dates and check that values contains at
// least one positive value and one negative value
const dates : number[] = []
let positive = false
let negative = false
for (var i = 0; i < values.length; i++) {
dates[i] = (i === 0) ? 0 : dates[i - 1] + 365
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
}

// Initialize guess and resultRate
let resultRate = guess

// Implement Newton's method
let newRate, epsRate, resultValue
let iteration = 0
let contLoop = true
do {
resultValue = _irrResult(values, dates, resultRate)
newRate = resultRate - resultValue / _irrResultDeriv(values, dates, resultRate)
epsRate = Math.abs(newRate - resultRate)
resultRate = newRate
contLoop = (epsRate > tol) && (Math.abs(resultValue) > tol)
} while (contLoop && (++iteration < maxIter))

if (contLoop) {
return Number.NaN
}

// Return internal rate of return
return resultRate
}

/**
* 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 Expand Up @@ -428,3 +535,38 @@ function _gDivGp (r: number, n: number, p: number, x: number, y: number, when: P
p * (t1 - 1) * w / r)
return g / gp
}

/**
* Calculates the resulting amount.
*
* Based on https://gist.github.com/ghalimi/4591338 by @ghalimi
* ASF licensed (check the link for the full license)
*
* @private
*/
function _irrResult (values: number[], dates: number[], rate: number): number {
const r = rate + 1
let result = values[0]
for (let i = 1; i < values.length; i++) {
result += values[i] / Math.pow(r, (dates[i] - dates[0]) / 365)
}
return result
}

/**
* Calculates the first derivation
*
* Based on https://gist.github.com/ghalimi/4591338 by @ghalimi
* ASF licensed (check the link for the full license)
*
* @private
*/
function _irrResultDeriv (values: number[], dates: number[], rate: number) : number {
const r = rate + 1
let result = 0
for (let i = 1; i < values.length; i++) {
const frac = (dates[i] - dates[0]) / 365
result -= frac * values[i] / Math.pow(r, frac + 1)
}
return result
}
8 changes: 8 additions & 0 deletions test/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ describe('Source code docs examples', () => {
test('pv()', () => {
expect(f.pv(0.05 / 12, 10 * 12, -100, 15692.93)).toBeCloseTo(-100.00067131625819, 6)
})

test('irr()', () => {
expect(f.irr([-100, 39, 59, 55, 20])).toBeCloseTo(0.2809484, 6)
expect(f.irr([-100, 0, 0, 74])).toBeCloseTo(-0.0954958, 6)
expect(f.irr([-100, 100, 0, -7])).toBeCloseTo(-0.083300, 6)
expect(f.irr([-100, 100, 0, 7])).toBeCloseTo(0.0620584, 6)
expect(f.irr([-5, 10.5, 1, -8, 1])).toBeCloseTo(0.088598, 6)
})
})
29 changes: 28 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, PaymentDueTime } from '../src/financial'
import { fv, pmt, nper, ipmt, ppmt, pv, rate, irr, PaymentDueTime } from '../src/financial'

// Based on https://github.com/numpy/numpy-financial/blob/master/numpy_financial/tests/test_financial.py

Expand Down Expand Up @@ -127,3 +127,30 @@ describe('rate()', () => {
expect(rate(10, 0, -3500, 10000, PaymentDueTime.Begin, 0.2, 1e-5, 200)).toBeCloseTo(0.1106908, 6)
})
})

describe('irr()', () => {
it('calculates basic values', () => {
expect(irr([-150000, 15000, 25000, 35000, 45000, 60000])).toBeCloseTo(0.052433, 6)
expect(irr([-100, 0, 0, 74])).toBeCloseTo(-0.095496, 6)
expect(irr([-100, 39, 59, 55, 20])).toBeCloseTo(0.2809484, 6)
expect(irr([-100, 100, 0, -7])).toBeCloseTo(-0.083300, 6)
expect(irr([-100, 100, 0, 7])).toBeCloseTo(0.0620585, 6)
expect(irr([-5, 10.5, 1, -8, 1])).toBeCloseTo(0.088598, 6)
})

it('calculates trailing zeroes correctly', () => {
expect(irr([-5, 10.5, 1, -8, 1, 0, 0, 0])).toBeCloseTo(0.088598, 6)
})

it('returns NaN if there is no solution', () => {
expect(irr([-1, -2, -3])).toBeNaN()
})

it('calculates with custom guess, tol and maxIter', () => {
expect(irr([-5, 10.5, 1, -8, 1], 0.1, 1e-10, 10)).toBeCloseTo(0.08859833852439172, 9)
})

it('returns null if can\'t calculate the result within the given number of iterations', () => {
expect(irr([-5, 10.5, 1, -8, 1], 0.1, 1e-10, 2)).toBeNaN()
})
})

0 comments on commit 3314056

Please sign in to comment.