├── .vscode ├── extensions.json └── settings.json ├── deno.json ├── jsr.json ├── .gitignore ├── test ├── utils.ts ├── examples.test.ts └── financial.test.ts ├── .eslintrc.js ├── tsconfig.json ├── biome.json ├── LICENSE ├── .github └── workflows │ ├── release.yml │ └── main.yml ├── package.json ├── README.md └── src └── index.ts /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lmammino/financial", 3 | "version": "0.2.4", 4 | "exports": "./src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lmammino/financial", 3 | "version": "0.2.4", 4 | "exports": "./src/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | dist/ 5 | docs/ 6 | coverage/ 7 | .npmrc 8 | src/*.js 9 | test/*.js -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'poku' 2 | 3 | export function assertEqualApprox(a: number, b: number, digits = 6) { 4 | assert.equal(a.toFixed(digits), b.toFixed(digits)) 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2020: true, 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: ['standard'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 11, 11 | sourceType: 'module', 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | ignorePatterns: ['dist/**.js'], 15 | rules: { 16 | 'no-unused-vars': 'off', 17 | '@typescript-eslint/no-unused-vars': 'error', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "types" 5 | ], 6 | "compilerOptions": { 7 | "module": "esnext", 8 | "lib": [ 9 | "dom", 10 | "esnext" 11 | ], 12 | "importHelpers": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "rootDir": "./src", 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": [ 25 | "src/*" 26 | ] 27 | }, 28 | "jsx": "react", 29 | "esModuleInterop": true, 30 | "outDir": "dist", 31 | } 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "[javascript]": { 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "always", 10 | "quickfix.biome": "always", 11 | "source.organizeImports.biome": "always" 12 | } 13 | }, 14 | "[json]": { 15 | "editor.defaultFormatter": "biomejs.biome", 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll": "always", 18 | "quickfix.biome": "always", 19 | "source.organizeImports.biome": "always" 20 | } 21 | }, 22 | "[typescript]": { 23 | "editor.defaultFormatter": "biomejs.biome", 24 | "editor.codeActionsOnSave": { 25 | "source.fixAll": "always", 26 | "quickfix.biome": "always", 27 | "source.organizeImports.biome": "always" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "nursery": { 11 | "all": true, 12 | "noNodejsModules": "off", 13 | "noUnusedImports": "warn", 14 | "useImportRestrictions": "off" 15 | }, 16 | "correctness": { 17 | "all": true 18 | } 19 | }, 20 | "ignore": ["coverage/", "dist/", "node_modules/", "docs/"] 21 | }, 22 | "formatter": { 23 | "enabled": true, 24 | "formatWithErrors": false, 25 | "indentStyle": "space", 26 | "indentWidth": 2, 27 | "lineWidth": 80, 28 | "ignore": ["coverage/", "dist/", "node_modules/", "docs/"] 29 | }, 30 | "javascript": { 31 | "formatter": { 32 | "semicolons": "asNeeded", 33 | "quoteStyle": "single" 34 | }, 35 | "globals": ["Deno", "describe", "test", "expect", "it"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luciano Mammino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - name: Begin CI... 17 | uses: actions/checkout@v4 18 | 19 | - uses: pnpm/action-setup@v4 20 | 21 | - name: Use Node 20.x 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | cache: 'pnpm' 26 | cache-dependency-path: 'pnpm-lock.yaml' 27 | 28 | - name: Install dependencies 29 | run: pnpm install --frozen-lockfile 30 | 31 | - name: Build (lib) 32 | run: pnpm run build:ts 33 | env: 34 | CI: true 35 | 36 | - name: Build (docs) 37 | run: pnpm run build:docs 38 | env: 39 | CI: true 40 | 41 | - name: Publish on NPM 42 | run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc && pnpm publish --no-git-checks 43 | 44 | - name: Publish on JSR 45 | run: npx jsr publish 46 | 47 | - name: Publish docs to Netlify 48 | uses: jsmrcaga/action-netlify-deploy@v1.1.0 49 | with: 50 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }} 51 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 52 | NETLIFY_DEPLOY_TO_PROD: true 53 | build_directory: docs 54 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Begin CI... 10 | uses: actions/checkout@v4 11 | 12 | - uses: pnpm/action-setup@v4 13 | 14 | - name: Use Node 20.x 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20.x 18 | cache: 'pnpm' 19 | cache-dependency-path: 'pnpm-lock.yaml' 20 | 21 | - name: Install dependencies 22 | run: pnpm install --frozen-lockfile 23 | 24 | - name: Use Deno 1.x 25 | uses: denolib/setup-deno@v2 26 | with: 27 | deno-version: v1.x 28 | 29 | - uses: oven-sh/setup-bun@v1 30 | with: 31 | bun-version: latest 32 | 33 | - name: Lint (Biome) 34 | run: pnpm run lint:biome 35 | env: 36 | CI: true 37 | 38 | - name: Lint (TypeScript) 39 | run: pnpm run lint:ts 40 | env: 41 | CI: true 42 | 43 | - name: Test (Node.js) 44 | run: pnpm run test:node 45 | env: 46 | CI: true 47 | 48 | - name: Send coverage to Codecov 49 | uses: codecov/codecov-action@v1 50 | with: 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | file: ./coverage/lcov.info 53 | 54 | - name: Test (Deno) 55 | run: pnpm run test:deno 56 | env: 57 | CI: true 58 | 59 | - name: Test (Bun) 60 | run: pnpm run test:bun 61 | env: 62 | CI: true 63 | 64 | - name: Build 65 | run: pnpm run build:ts 66 | env: 67 | CI: true 68 | 69 | - name: Build (docs) 70 | run: pnpm run build:docs 71 | env: 72 | CI: true 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "financial", 3 | "description": "A Zero-dependency TypeScript/JavaScript port of numpy-financial", 4 | "author": "Luciano Mammino (https://loige.co)", 5 | "version": "0.2.4", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lmammino/financial.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/lmammino/financial/issues" 12 | }, 13 | "homepage": "https://financialjs.netlify.app", 14 | "license": "MIT", 15 | "main": "dist/index.js", 16 | "typings": "dist/index.d.ts", 17 | "module": "dist/financial.esm.js", 18 | "engines": { 19 | "node": ">=18" 20 | }, 21 | "files": ["dist", "src"], 22 | "scripts": { 23 | "build:ts": "dts build", 24 | "build:docs": "typedoc --out docs/ src/", 25 | "lint:biome": "biome lint . && biome format .", 26 | "lint:ts": "tsc --noEmit --incremental false --diagnostics", 27 | "test:node": "c8 --reporter lcov tsx test/*.test.ts", 28 | "test:deno": "deno run --allow-env --allow-read --allow-run npm:poku --parallel test/*.test.ts", 29 | "test:bun": "bunx poku --parallel test/*.test.ts" 30 | }, 31 | "devDependencies": { 32 | "@biomejs/biome": "^1.5.3", 33 | "@types/jest": "^29.5.12", 34 | "c8": "^10.1.2", 35 | "dts-cli": "^2.0.4", 36 | "poku": "^2.1.0", 37 | "tsx": "^4.16.2", 38 | "typedoc": "^0.25.9", 39 | "typescript": "^5.3.3" 40 | }, 41 | "keywords": [ 42 | "financial", 43 | "numpy", 44 | "numpy-financial", 45 | "mortgage", 46 | "fv", 47 | "pmt", 48 | "nper", 49 | "ipmt", 50 | "ppmt", 51 | "pv", 52 | "rate", 53 | "irr", 54 | "npv", 55 | "mirr" 56 | ], 57 | "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903" 58 | } 59 | -------------------------------------------------------------------------------- /test/examples.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, describe, it } from 'poku' 2 | import { fv, ipmt, irr, nper, npv, pmt, pv } from '../src/index.ts' 3 | import { assertEqualApprox } from './utils.ts' 4 | 5 | describe('Source code docs examples', async () => { 6 | await it('fv()', () => { 7 | assertEqualApprox(fv(0.05 / 12, 10 * 12, -100, -100), 15692.928894335748) 8 | }) 9 | 10 | await it('pmt()', () => { 11 | assertEqualApprox(pmt(0.075 / 12, 12 * 15, 200000), -1854.0247200054619) 12 | }) 13 | 14 | await it('nper()', () => { 15 | assertEqualApprox(nper(0.07 / 12, -150, 8000), 64.07334877066185) 16 | }) 17 | 18 | await it('ipmt()', () => { 19 | const principal = 2500 20 | const periods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 21 | const ipmts = periods.map((per) => 22 | ipmt(0.0824 / 12, per, 1 * 12, principal), 23 | ) 24 | assert.deepEqual( 25 | ipmts, 26 | [ 27 | -17.166666666666668, -15.789337457350777, -14.402550587464257, 28 | -13.006241114404524, -11.600343649629737, -10.18479235559687, 29 | -8.759520942678298, -7.324462666057678, -5.879550322604295, 30 | -4.424716247725826, -2.9598923121998877, -1.4850099189833388, 31 | ], 32 | ) 33 | const interestpd = ipmts.reduce((a, b) => a + b, 0) 34 | assertEqualApprox(interestpd, -112.98308424136215) 35 | }) 36 | 37 | await it('pv()', () => { 38 | assertEqualApprox( 39 | pv(0.05 / 12, 10 * 12, -100, 15692.93), 40 | -100.00067131625819, 41 | ) 42 | }) 43 | 44 | await it('irr()', () => { 45 | assertEqualApprox(irr([-100, 39, 59, 55, 20]), 0.2809484) 46 | assertEqualApprox(irr([-100, 0, 0, 74]), -0.0954958) 47 | assertEqualApprox(irr([-100, 100, 0, -7]), -0.0833) 48 | assertEqualApprox(irr([-100, 100, 0, 7]), 0.0620584) 49 | assertEqualApprox(irr([-5, 10.5, 1, -8, 1]), 0.088598) 50 | }) 51 | 52 | await it('npv()', () => { 53 | const rate = 0.08 54 | const cashflows = [-40_000, 5000, 8000, 12000, 30000] 55 | assertEqualApprox(npv(rate, cashflows), 3065.22266817) 56 | 57 | const initialCashflow = cashflows[0] 58 | cashflows[0] = 0 59 | assertEqualApprox(npv(rate, cashflows) + initialCashflow, 3065.22266817) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Financial 2 | 3 | [![npm version](https://badge.fury.io/js/financial.svg)](https://badge.fury.io/js/financial) 4 | [![CI](https://github.com/lmammino/financial/workflows/CI/badge.svg)](https://github.com/lmammino/financial/actions?query=workflow%3ACI) 5 | [![codecov](https://codecov.io/gh/lmammino/financial/branch/main/graph/badge.svg)](https://codecov.io/gh/lmammino/financial) 6 | [![Documentation](https://api.netlify.com/api/v1/badges/eca2653e-dcaa-41db-865c-ab635687e69d/deploy-status)](https://financialjs.netlify.app/) 7 | 8 | A Zero-Dependency TypeScript / JavaScript financial utility library inspired by [numpy-financial](https://github.com/numpy/numpy-financial/) that can be used on **Node.js**, **Deno**, **Bun** and **the browser**. 9 | 10 | It does support the same functionality offered by `numpy-financial` but it only supports scalar JavaScript `number` values (NO numpy-like array values) and it does NOT support arbitrary-precision signed decimal numbers (such as decimal.js, big.js or bignumber.js). 11 | 12 | 13 | 📖 **API DOCS** 📖 : [financialjs.netlify.app](https://financialjs.netlify.app) 14 | 15 | 16 | [![Example usage in a picture](https://repository-images.githubusercontent.com/275629272/b295b880-bd3e-11ea-8860-705f2f6427ec)](https://repository-images.githubusercontent.com/275629272/b295b880-bd3e-11ea-8860-705f2f6427ec) 17 | 18 | 19 | ## Install 20 | 21 | With `npm`: 22 | 23 | ```bash 24 | npm install --save-dev financial 25 | ``` 26 | 27 | Or, with `yarn`: 28 | 29 | ```bash 30 | yarn add financial 31 | ``` 32 | 33 | 34 | ## Example usage 35 | 36 | ```javascript 37 | import { fv } from 'financial' 38 | 39 | fv(0.05 / 12, 10 * 12, -100, -100) // 15692.928894335748 40 | ``` 41 | 42 | ## Module formats 43 | 44 | This library exports its functionality using different module formats. 45 | 46 | 47 | ### Commonjs 48 | 49 | ```javascript 50 | const financial = require('financial') // ./index.js 51 | 52 | // use `financial.fv`, `financial.pmt`, etc. 53 | ``` 54 | 55 | or, leveraging destructuring 56 | 57 | ```javascript 58 | const { fv, pmt } = require('financial') // ./index.js 59 | 60 | // use `fv`, `pmt`, etc. 61 | ``` 62 | 63 | An optimized Commonjs for browsers can be imported directly from the web: 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | **Note**: make sure you replace the `x.y.z` with the correct version you want to use. 70 | 71 | 72 | ### ESM (EcmaScript Modules) 73 | 74 | Also working with Typescript 75 | 76 | ```javascript 77 | import { fv, pmt } from 'financial' 78 | 79 | // use `fv`, `pmt`, etc. 80 | ``` 81 | 82 | There's no `default` export in the ESM implementation, so you have to explicitely import the functionality you need, one by one. 83 | 84 | 85 | ### Use with Deno 86 | 87 | Make sure you specify the version you prefer in the import URL: 88 | 89 | ```typescript 90 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 91 | import * as f from 'https://deno.land/x/npm:financial@0.1.1/src/financial.ts' 92 | 93 | assertEquals(f.fv(0.05 / 12, 10 * 12, -100, -100), 15692.928894335755) 94 | ``` 95 | 96 | 97 | ## Implemented functions and Documentation 98 | 99 | Click on the function name to get the full documentation for every function, or check out the full programmatic documentation at [financialjs.netlify.app](https://financialjs.netlify.app). 100 | 101 | - [X] [`fv()`: Future Value](https://financialjs.netlify.app/modules/_financial_.html#fv) (since v0.0.12) 102 | - [X] [`pmt()`: Total payment](https://financialjs.netlify.app/modules/_financial_.html#pmt) (since v0.0.12) 103 | - [X] [`nper()`: Number of period payments](https://financialjs.netlify.app/modules/_financial_.html#nper) (since v0.0.12) 104 | - [X] [`ipmt()`: Interest portion of a payment](https://financialjs.netlify.app/modules/_financial_.html#ipmt) (since v0.0.12) 105 | - [X] [`ppmt()`: Payment against loan principal](https://financialjs.netlify.app/modules/_financial_.html#ppmt) (since v0.0.14) 106 | - [X] [`pv()`: Present Value](https://financialjs.netlify.app/modules/_financial_.html#pv) (since v0.0.15) 107 | - [X] [`rate()`: Rate of interest per period](https://financialjs.netlify.app/modules/_financial_.html#rate) (since v0.0.16) 108 | - [X] [`irr()`: Internal Rate of Return](https://financialjs.netlify.app/modules/_financial_.html#irr) (since v0.0.17) 109 | - [X] [`npv()`: Net Present Value](https://financialjs.netlify.app/modules/_financial_.html#npv) (since v0.0.18) 110 | - [X] [`mirr()`: Modified Internal Rate of Return](https://financialjs.netlify.app/modules/_financial_.html#mirr) (since 0.1.0) 111 | 112 | 113 | ## Local Development 114 | 115 | Below is a list of commands you will probably find useful. 116 | 117 | - `npm start` or `yarn start`: Runs the project in development/watch mode. Your project will be rebuilt upon changes. 118 | - `npm run build` or `yarn build`: Bundles the package to the `dist` folder. The package is optimized and bundled with Rollup into multiple format (CommonJS, UMD, and ES Module). 119 | - `npm run build:docs` or `yarn build:docs`: Builds the API documentation in the `docs` folder using `typedoc`. 120 | - `npm test` or `yarn test`: Runs the test watcher (Jest) in an interactive mode. it runs tests related to files changed since the last commit. 121 | - `npm run test:watch` or `yarn test:watch`: runs the tests in watch mode 122 | 123 | 124 | ### Test with Deno 125 | 126 | To test with Deno, run: 127 | 128 | ```bash 129 | deno test test/deno.ts 130 | ``` 131 | 132 | 133 | ## Contributing 134 | 135 | Everyone is very welcome to contribute to this project. You can contribute just by submitting bugs or 136 | suggesting improvements by [opening an issue on GitHub](https://github.com/lmammino/financial/issues). 137 | 138 | You can also submit PRs as long as you adhere with the code standards and write tests for the proposed changes. 139 | 140 | ## License 141 | 142 | Licensed under [MIT License](LICENSE). © Luciano Mammino. 143 | -------------------------------------------------------------------------------- /test/financial.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, describe, it } from 'poku' 2 | import { 3 | PaymentDueTime, 4 | fv, 5 | ipmt, 6 | irr, 7 | mirr, 8 | nper, 9 | npv, 10 | pmt, 11 | ppmt, 12 | pv, 13 | rate, 14 | } from '../src/index.ts' 15 | import { assertEqualApprox } from './utils.ts' 16 | 17 | // Mostly based on 18 | // https://github.com/numpy/numpy-financial/blob/master/numpy_financial/tests/test_financial.py 19 | 20 | describe('fv()', async () => { 21 | await it('calculates float when is end', () => { 22 | assertEqualApprox( 23 | fv(0.075, 20, -2000, 0, PaymentDueTime.End), 24 | 86609.362673042924, 25 | ) 26 | }) 27 | 28 | await it('calculates float when is begin', () => { 29 | assertEqualApprox( 30 | fv(0.075, 20, -2000, 0, PaymentDueTime.Begin), 31 | 93105.064874, 32 | ) 33 | }) 34 | 35 | await it('calculates float with rate 0', () => { 36 | assertEqualApprox(fv(0, 5, 100, 0), -500) 37 | assertEqualApprox(fv(0.1, 5, 100, 0), -610.51) 38 | }) 39 | }) 40 | 41 | describe('pmt()', async () => { 42 | await it('calculates float when is end', () => { 43 | assertEqualApprox(pmt(0.08 / 12, 5 * 12, 15000), -304.145914) 44 | }) 45 | 46 | await it('calculates float when is begin', () => { 47 | assertEqualApprox( 48 | pmt(0.08 / 12, 5 * 12, 15000, 0, PaymentDueTime.Begin), 49 | -302.13170297305413, 50 | ) 51 | }) 52 | 53 | await it('calculates float with rate 0', () => { 54 | assertEqualApprox(pmt(0.0, 5 * 12, 15000), -250) 55 | }) 56 | }) 57 | 58 | describe('nper()', async () => { 59 | await it('calculates float when is end', () => { 60 | assertEqualApprox(nper(0, -2000, 0, 100000), 50) 61 | assertEqualApprox(nper(0.075, -2000, 0, 100000), 21.544944) 62 | assertEqualApprox(nper(0.1, 0, -500, 1500), 11.52670461) 63 | assertEqualApprox(nper(0.075, -2000, 0, 100000), 21.5449442) 64 | }) 65 | 66 | await it('calculates float when is begin', () => { 67 | assertEqualApprox( 68 | nper(0.075, -2000, 0, 100000, PaymentDueTime.Begin), 69 | 20.76156441, 70 | ) 71 | }) 72 | 73 | await it('deals with infinite payments', () => { 74 | assert.equal(nper(0, -0.0, 1000), Number.POSITIVE_INFINITY) 75 | }) 76 | 77 | await it('calculates float with rate 0', () => { 78 | assertEqualApprox(nper(0, -100, 1000), 10) 79 | }) 80 | }) 81 | 82 | describe('ipmt()', async () => { 83 | await it('calculates float when is end', () => { 84 | assertEqualApprox(ipmt(0.1 / 12, 1, 24, 2000), -16.666667) 85 | assertEqualApprox(ipmt(0.1 / 12, 2, 24, 2000), -16.03647345) 86 | assertEqualApprox(ipmt(0.1 / 12, 3, 24, 2000), -15.40102862) 87 | assertEqualApprox(ipmt(0.1 / 12, 4, 24, 2000), -14.76028842) 88 | }) 89 | 90 | await it('calculates float when is begin', () => { 91 | assert.equal(ipmt(0.1 / 12, 1, 24, 2000, 0, PaymentDueTime.Begin), 0) 92 | assert.equal( 93 | ipmt(0.001988079518355057, 0, 360, 300000, 0, PaymentDueTime.Begin), 94 | Number.NaN, 95 | ) 96 | assert.equal( 97 | ipmt(0.001988079518355057, 1, 360, 300000, 0, PaymentDueTime.Begin), 98 | 0, 99 | ) 100 | assertEqualApprox( 101 | ipmt(0.001988079518355057, 2, 360, 300000, 0, PaymentDueTime.Begin), 102 | -594.107158, 103 | ) 104 | assertEqualApprox( 105 | ipmt(0.001988079518355057, 3, 360, 300000, 0, PaymentDueTime.Begin), 106 | -592.971592, 107 | ) 108 | }) 109 | }) 110 | 111 | describe('ppmt()', async () => { 112 | await it('calculates float when is end', () => { 113 | assertEqualApprox(ppmt(0.1 / 12, 1, 60, 55000), -710.25412578642) 114 | assertEqualApprox(ppmt(0.08 / 12, 1, 60, 15000), -204.145914) 115 | }) 116 | 117 | await it('calculates float when is begin', () => { 118 | assertEqualApprox( 119 | ppmt(0.1 / 12, 1, 60, 55000, 0, PaymentDueTime.Begin), 120 | -1158.929712, 121 | ) 122 | assertEqualApprox( 123 | ppmt(0.08 / 12, 1, 60, 15000, 0, PaymentDueTime.Begin), 124 | -302.131703, 125 | ) 126 | }) 127 | }) 128 | 129 | describe('pv()', async () => { 130 | await it('calculates float when is end', () => { 131 | assertEqualApprox(pv(0.07, 20, 12000), -127128.1709461939) 132 | assertEqualApprox(pv(0.07, 21, 12000), -130026.327987097) 133 | assertEqualApprox(pv(0.07, 22, 12000), -132734.88596925) 134 | assertEqualApprox(pv(0.07, 23, 12000), -135266.248569392) 135 | assertEqualApprox(pv(0.07, 24, 12000), -137632.008008778) 136 | assertEqualApprox(pv(0.07, 25, 12000), -139842.998139045) 137 | }) 138 | 139 | await it('calculates float when is begin', () => { 140 | assertEqualApprox( 141 | pv(0.07, 20, 12000, 0, PaymentDueTime.Begin), 142 | -136027.1429124, 143 | ) 144 | assertEqualApprox( 145 | pv(0.07, 21, 12000, 0, PaymentDueTime.Begin), 146 | -139128.1709461, 147 | ) 148 | assertEqualApprox( 149 | pv(0.07, 22, 12000, 0, PaymentDueTime.Begin), 150 | -142026.327987, 151 | ) 152 | assertEqualApprox( 153 | pv(0.07, 23, 12000, 0, PaymentDueTime.Begin), 154 | -144734.8859692, 155 | ) 156 | assertEqualApprox( 157 | pv(0.07, 24, 12000, 0, PaymentDueTime.Begin), 158 | -147266.2485693, 159 | ) 160 | assertEqualApprox( 161 | pv(0.07, 25, 12000, 0, PaymentDueTime.Begin), 162 | -149632.0080087, 163 | ) 164 | }) 165 | 166 | await it('calculates float when the rate is 0', () => { 167 | assertEqualApprox(pv(0, 20, 12000, 0), -240000) 168 | }) 169 | 170 | await it('calculates float when fv != 0', () => { 171 | assertEqualApprox(pv(0, 20, 12000, 1000), -241000) 172 | }) 173 | }) 174 | 175 | describe('rate()', async () => { 176 | await it('calculates float when is end', () => { 177 | assertEqualApprox(rate(10, 0, -3500, 10000), 0.1106908) 178 | }) 179 | 180 | await it('calculates float when is begin', () => { 181 | assertEqualApprox( 182 | rate(10, 0, -3500, 10000, PaymentDueTime.Begin), 183 | 0.1106908, 184 | ) 185 | }) 186 | 187 | await it('Should return NaN for infeasible solution', () => { 188 | assert.equal(rate(12, 400, 10000, 5000, PaymentDueTime.End), Number.NaN) 189 | assert.equal(rate(12, 400, 10000, 5000, PaymentDueTime.Begin), Number.NaN) 190 | }) 191 | 192 | await it('calculates float with a custom guess, tolerance and maxIter', () => { 193 | assertEqualApprox( 194 | rate(10, 0, -3500, 10000, PaymentDueTime.Begin, 0.2, 1e-5, 200), 195 | 0.1106908, 196 | ) 197 | }) 198 | }) 199 | 200 | describe('irr()', async () => { 201 | await it('calculates basic values', () => { 202 | assertEqualApprox( 203 | irr([-150000, 15000, 25000, 35000, 45000, 60000]), 204 | 0.052432889, 205 | 9, 206 | ) 207 | assertEqualApprox(irr([-100, 0, 0, 74]), -0.095496) 208 | assertEqualApprox(irr([-100, 39, 59, 55, 20]), 0.2809484) 209 | assertEqualApprox(irr([-100, 100, 0, -7]), -0.0833) 210 | assertEqualApprox(irr([-100, 100, 0, 7]), 0.062058) 211 | assertEqualApprox(irr([-5, 10.5, 1, -8, 1]), 0.088598) 212 | }) 213 | 214 | await it('calculates trailing zeroes correctly', () => { 215 | assertEqualApprox(irr([-5, 10.5, 1, -8, 1, 0, 0, 0]), 0.088598) 216 | }) 217 | 218 | await it('returns NaN if there is no solution', () => { 219 | assert.equal(irr([-1, -2, -3]), Number.NaN) 220 | }) 221 | 222 | await it('calculates with custom guess, tol and maxIter', () => { 223 | assertEqualApprox( 224 | irr([-5, 10.5, 1, -8, 1], 0.1, 1e-10, 10), 225 | 0.08859833852439172, 226 | 9, 227 | ) 228 | }) 229 | 230 | await it("returns null if can't calculate the result within the given number of iterations", () => { 231 | assert.equal(irr([-5, 10.5, 1, -8, 1], 0.1, 1e-10, 2), Number.NaN) 232 | }) 233 | }) 234 | 235 | describe('npv()', async () => { 236 | await it('calculates float', () => { 237 | assertEqualApprox( 238 | npv(0.05, [-15000, 1500, 2500, 3500, 4500, 6000]), 239 | 122.894855, 240 | ) 241 | }) 242 | }) 243 | 244 | describe('mirr()', async () => { 245 | await it('calculates float', () => { 246 | assertEqualApprox( 247 | mirr([-4500, -800, 800, 800, 600, 600, 800, 800, 700, 3000], 0.08, 0.055), 248 | 0.066597, 249 | ) 250 | assertEqualApprox( 251 | mirr([-120000, 39000, 30000, 21000, 37000, 46000], 0.1, 0.12), 252 | 0.126094, 253 | ) 254 | assertEqualApprox(mirr([100, 200, -50, 300, -200], 0.05, 0.06), 0.342823) 255 | }) 256 | 257 | await it('returns NaN if mirr() cannot be calculated', () => { 258 | assert.equal( 259 | mirr([39000, 30000, 21000, 37000, 46000], 0.1, 0.12), 260 | Number.NaN, 261 | ) 262 | assert.equal( 263 | mirr([-39000, -30000, -21000, -37000, -46000], 0.1, 0.12), 264 | Number.NaN, 265 | ) 266 | }) 267 | }) 268 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * When payments are due 3 | * 4 | * @since v0.0.12 5 | */ 6 | export enum PaymentDueTime { 7 | /** Payments due at the beginning of a period (1) */ 8 | Begin = 'begin', // 1 9 | /** Payments are due at the end of a period (0) */ 10 | End = 'end', // 0 11 | } 12 | 13 | /** 14 | * Compute the future value. 15 | * 16 | * @param rate - Rate of interest as decimal (not per cent) per period 17 | * @param nper - Number of compounding periods 18 | * @param pmt - A fixed payment, paid either at the beginning or ar the end (specified by `when`) 19 | * @param pv - Present value 20 | * @param when - When payment was made 21 | * 22 | * @returns The value at the end of the `nper` periods 23 | * 24 | * @since v0.0.12 25 | * 26 | * ## Examples 27 | * 28 | * What is the future value after 10 years of saving $100 now, with 29 | * an additional monthly savings of $100. Assume the interest rate is 30 | * 5% (annually) compounded monthly? 31 | * 32 | * ```javascript 33 | * import { fv } from 'financial' 34 | * 35 | * fv(0.05 / 12, 10 * 12, -100, -100) // 15692.928894335748 36 | * ``` 37 | * 38 | * By convention, the negative sign represents cash flow out (i.e. money not 39 | * available today). Thus, saving $100 a month at 5% annual interest leads 40 | * to $15,692.93 available to spend in 10 years. 41 | * 42 | * ## Notes 43 | * 44 | * The future value is computed by solving the equation: 45 | * 46 | * ``` 47 | * fv + pv * (1+rate) ** nper + pmt * (1 + rate * when) / rate * ((1 + rate) ** nper - 1) == 0 48 | * ``` 49 | * 50 | * or, when `rate == 0`: 51 | * 52 | * ``` 53 | * fv + pv + pmt * nper == 0 54 | * ``` 55 | * 56 | * ## References 57 | * 58 | * [Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May)](http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt). 59 | */ 60 | export function fv( 61 | rate: number, 62 | nper: number, 63 | pmt: number, 64 | pv: number, 65 | when: PaymentDueTime = PaymentDueTime.End, 66 | ): number { 67 | const isRateZero = rate === 0 68 | 69 | if (isRateZero) { 70 | return -(pv + pmt * nper) 71 | } 72 | 73 | const temp = (1 + rate) ** nper 74 | const whenMult = when === PaymentDueTime.Begin ? 1 : 0 75 | return -pv * temp - ((pmt * (1 + rate * whenMult)) / rate) * (temp - 1) 76 | } 77 | 78 | /** 79 | * Compute the payment against loan principal plus interest. 80 | * 81 | * @param rate - Rate of interest (per period) 82 | * @param nper - Number of compounding periods (e.g., number of payments) 83 | * @param pv - Present value (e.g., an amount borrowed) 84 | * @param fv - Future value (e.g., 0) 85 | * @param when - When payments are due 86 | * 87 | * @returns the (fixed) periodic payment 88 | * 89 | * @since v0.0.12 90 | * 91 | * ## Examples 92 | * 93 | * What is the monthly payment needed to pay off a $200,000 loan in 15 94 | * years at an annual interest rate of 7.5%? 95 | * 96 | * ```javascript 97 | * import { pmt } from 'financial' 98 | * 99 | * pmt(0.075/12, 12*15, 200000) // -1854.0247200054619 100 | * ``` 101 | * 102 | * In order to pay-off (i.e., have a future-value of 0) the $200,000 obtained 103 | * today, a monthly payment of $1,854.02 would be required. Note that this 104 | * example illustrates usage of `fv` having a default value of 0. 105 | * 106 | * ## Notes 107 | * 108 | * The payment is computed by solving the equation: 109 | * 110 | * ``` 111 | * fv + pv * (1 + rate) ** nper + pmt * (1 + rate*when) / rate * ((1 + rate) ** nper - 1) == 0 112 | * ``` 113 | * 114 | * or, when `rate == 0`: 115 | * 116 | * ``` 117 | * fv + pv + pmt * nper == 0 118 | * ``` 119 | * 120 | * for `pmt`. 121 | * 122 | * Note that computing a monthly mortgage payment is only 123 | * one use for this function. For example, `pmt` returns the 124 | * periodic deposit one must make to achieve a specified 125 | * future balance given an initial deposit, a fixed, 126 | * periodically compounded interest rate, and the total 127 | * number of periods. 128 | * 129 | * ## References 130 | * 131 | * [Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May)](http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt). 132 | */ 133 | export function pmt( 134 | rate: number, 135 | nper: number, 136 | pv: number, 137 | fv = 0, 138 | when = PaymentDueTime.End, 139 | ): number { 140 | const isRateZero = rate === 0 141 | const temp = (1 + rate) ** nper 142 | const whenMult = when === PaymentDueTime.Begin ? 1 : 0 143 | const maskedRate = isRateZero ? 1 : rate 144 | const fact = isRateZero 145 | ? nper 146 | : ((1 + maskedRate * whenMult) * (temp - 1)) / maskedRate 147 | 148 | return -(fv + pv * temp) / fact 149 | } 150 | 151 | /** 152 | * Compute the number of periodic payments. 153 | * 154 | * @param rate - Rate of interest (per period) 155 | * @param pmt - Payment 156 | * @param pv - Present value 157 | * @param fv - Future value 158 | * @param when - When payments are due 159 | * 160 | * @returns The number of periodic payments 161 | * 162 | * @since v0.0.12 163 | * 164 | * ## Examples 165 | * 166 | * If you only had $150/month to pay towards the loan, how long would it take 167 | * to pay-off a loan of $8,000 at 7% annual interest? 168 | * 169 | * ```javascript 170 | * import { nper } from 'financial' 171 | * 172 | * Math.round(nper(0.07/12, -150, 8000), 5) // 64.07335 173 | * ``` 174 | * 175 | * So, over 64 months would be required to pay off the loan. 176 | * 177 | * ## Notes 178 | * 179 | * The number of periods `nper` is computed by solving the equation: 180 | * 181 | * ``` 182 | * fv + pv * (1+rate) ** nper + pmt * (1+rate * when) / rate * ((1+rate) ** nper-1) = 0 183 | * ``` 184 | * 185 | * but if `rate = 0` then: 186 | * 187 | * ``` 188 | * fv + pv + pmt * nper = 0 189 | * ``` 190 | */ 191 | export function nper( 192 | rate: number, 193 | pmt: number, 194 | pv: number, 195 | fv = 0, 196 | when = PaymentDueTime.End, 197 | ): number { 198 | const isRateZero = rate === 0 199 | if (isRateZero) { 200 | return -(fv + pv) / pmt 201 | } 202 | 203 | const whenMult = when === PaymentDueTime.Begin ? 1 : 0 204 | const z = (pmt * (1 + rate * whenMult)) / rate 205 | return Math.log((-fv + z) / (pv + z)) / Math.log(1 + rate) 206 | } 207 | 208 | /** 209 | * Compute the interest portion of a payment. 210 | * 211 | * @param rate - Rate of interest as decimal (not per cent) per period 212 | * @param per - Interest paid against the loan changes during the life or the loan. The `per` is the payment period to calculate the interest amount 213 | * @param nper - Number of compounding periods 214 | * @param pv - Present value 215 | * @param fv - Future value 216 | * @param when - When payments are due 217 | * 218 | * @returns Interest portion of payment 219 | * 220 | * @since v0.0.12 221 | * 222 | * ## Examples 223 | * 224 | * What is the amortization schedule for a 1 year loan of $2500 at 225 | * 8.24% interest per year compounded monthly? 226 | * 227 | * ```javascript 228 | * const principal = 2500 229 | * const periods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 230 | * const ipmts = periods.map((per) => f.ipmt(0.0824 / 12, per, 1 * 12, principal)) 231 | * expect(ipmts).toEqual([ 232 | * -17.166666666666668, 233 | * -15.789337457350777, 234 | * -14.402550587464257, 235 | * -13.006241114404524, 236 | * -11.600343649629737, 237 | * -10.18479235559687, 238 | * -8.759520942678298, 239 | * -7.324462666057678, 240 | * -5.879550322604295, 241 | * -4.424716247725826, 242 | * -2.9598923121998877, 243 | * -1.4850099189833388 244 | * ]) 245 | * const interestpd = ipmts.reduce((a, b) => a + b, 0) 246 | * expect(interestpd).toBeCloseTo(-112.98308424136215, 6) 247 | * ``` 248 | * 249 | * The `periods` variable represents the periods of the loan. Remember that financial equations start the period count at 1! 250 | * 251 | * ## Notes 252 | * 253 | * The total payment is made up of payment against principal plus interest. 254 | * 255 | * ``` 256 | * pmt = ppmt + ipmt 257 | * ``` 258 | */ 259 | export function ipmt( 260 | rate: number, 261 | per: number, 262 | nper: number, 263 | pv: number, 264 | fv = 0, 265 | when = PaymentDueTime.End, 266 | ): number { 267 | // Payments start at the first period, so payments before that 268 | // don't make any sense. 269 | if (per < 1) { 270 | return Number.NaN 271 | } 272 | 273 | // If payments occur at the beginning of a period and this is the 274 | // first period, then no interest has accrued. 275 | if (when === PaymentDueTime.Begin && per === 1) { 276 | return 0 277 | } 278 | 279 | const totalPmt = pmt(rate, nper, pv, fv, when) 280 | let ipmtVal = _rbl(rate, per, totalPmt, pv, when) * rate 281 | 282 | // If paying at the beginning we need to discount by one period 283 | if (when === PaymentDueTime.Begin && per > 1) { 284 | ipmtVal = ipmtVal / (1 + rate) 285 | } 286 | 287 | return ipmtVal 288 | } 289 | 290 | /** 291 | * Compute the payment against loan principal. 292 | * 293 | * @param rate - Rate of interest (per period) 294 | * @param per - Amount paid against the loan changes. The `per` is the period of interest. 295 | * @param nper - Number of compounding periods 296 | * @param pv - Present value 297 | * @param fv - Future value 298 | * @param when - When payments are due 299 | * 300 | * @returns the payment against loan principal 301 | * 302 | * @since v0.0.14 303 | */ 304 | export function ppmt( 305 | rate: number, 306 | per: number, 307 | nper: number, 308 | pv: number, 309 | fv = 0, 310 | when = PaymentDueTime.End, 311 | ): number { 312 | const total = pmt(rate, nper, pv, fv, when) 313 | return total - ipmt(rate, per, nper, pv, fv, when) 314 | } 315 | 316 | /** 317 | * Calculates the present value of an annuity investment based on constant-amount 318 | * periodic payments and a constant interest rate. 319 | * 320 | * @param rate - Rate of interest (per period) 321 | * @param nper - Number of compounding periods 322 | * @param pmt - Payment 323 | * @param fv - Future value 324 | * @param when - When payments are due 325 | * 326 | * @returns the present value of a payment or investment 327 | * 328 | * @since v0.0.15 329 | * 330 | * ## Examples 331 | * 332 | * What is the present value (e.g., the initial investment) 333 | * of an investment that needs to total $15692.93 334 | * after 10 years of saving $100 every month? Assume the 335 | * interest rate is 5% (annually) compounded monthly. 336 | * 337 | * ```javascript 338 | * import { pv } from 'financial' 339 | * 340 | * pv(0.05/12, 10*12, -100, 15692.93) // -100.00067131625819 341 | * ``` 342 | * 343 | * By convention, the negative sign represents cash flow out 344 | * (i.e., money not available today). Thus, to end up with 345 | * $15,692.93 in 10 years saving $100 a month at 5% annual 346 | * interest, one's initial deposit should also be $100. 347 | * 348 | * ## Notes 349 | * 350 | * The present value is computed by solving the equation: 351 | * 352 | * ``` 353 | * fv + pv * (1 + rate) ** nper + pmt * (1 + rate * when) / rate * ((1 + rate) ** nper - 1) = 0 354 | * ``` 355 | * 356 | * or, when `rate = 0`: 357 | * 358 | * ``` 359 | * fv + pv + pmt * nper = 0 360 | * ``` 361 | * 362 | * for `pv`, which is then returned. 363 | * 364 | * ## References 365 | * 366 | * [Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May)](http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt). 367 | */ 368 | export function pv( 369 | rate: number, 370 | nper: number, 371 | pmt: number, 372 | fv = 0, 373 | when = PaymentDueTime.End, 374 | ): number { 375 | const whenMult = when === PaymentDueTime.Begin ? 1 : 0 376 | const isRateZero = rate === 0 377 | const temp = (1 + rate) ** nper 378 | const fact = isRateZero ? nper : ((1 + rate * whenMult) * (temp - 1)) / rate 379 | return -(fv + pmt * fact) / temp 380 | } 381 | 382 | /** 383 | * Compute the rate of interest per period 384 | * 385 | * @param nper - Number of compounding periods 386 | * @param pmt - Payment 387 | * @param pv - Present value 388 | * @param fv - Future value 389 | * @param when - When payments are due ('begin' or 'end') 390 | * @param guess - Starting guess for solving the rate of interest 391 | * @param tol - Required tolerance for the solution 392 | * @param maxIter - Maximum iterations in finding the solution 393 | * 394 | * @returns the rate of interest per period (or `NaN` if it could 395 | * not be computed within the number of iterations provided) 396 | * 397 | * @since v0.0.16 398 | * 399 | * ## Notes 400 | * 401 | * Use Newton's iteration until the change is less than 1e-6 402 | * for all values or a maximum of 100 iterations is reached. 403 | * Newton's rule is: 404 | * 405 | * ``` 406 | * r_{n+1} = r_{n} - g(r_n)/g'(r_n) 407 | * ``` 408 | * 409 | * where: 410 | * 411 | * - `g(r)` is the formula 412 | * - `g'(r)` is the derivative with respect to r. 413 | * 414 | * 415 | * The rate of interest is computed by iteratively solving the 416 | * (non-linear) equation: 417 | * 418 | * ``` 419 | * fv + pv * (1+rate) ** nper + pmt * (1+rate * when) / rate * ((1+rate) ** nper - 1) = 0 420 | * ``` 421 | * 422 | * for `rate. 423 | * 424 | * ## References 425 | * 426 | * [Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May)](http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formulaOpenDocument-formula-20090508.odt). 427 | */ 428 | export function rate( 429 | nper: number, 430 | pmt: number, 431 | pv: number, 432 | fv: number, 433 | when = PaymentDueTime.End, 434 | guess = 0.1, 435 | tol = 1e-6, 436 | maxIter = 100, 437 | ): number { 438 | let rn = guess 439 | let iterator = 0 440 | let close = false 441 | 442 | while (iterator < maxIter && !close) { 443 | const rnp1 = rn - _gDivGp(rn, nper, pmt, pv, fv, when) 444 | const diff = Math.abs(rnp1 - rn) 445 | close = diff < tol 446 | iterator++ 447 | rn = rnp1 448 | } 449 | 450 | // if exausted all the iterations and the result is not 451 | // close enough, returns `NaN` 452 | if (!close) { 453 | return Number.NaN 454 | } 455 | 456 | return rn 457 | } 458 | 459 | /** 460 | * Return the Internal Rate of Return (IRR). 461 | * 462 | * This is the "average" periodically compounded rate of return 463 | * that gives a net present value of 0.0; for a more complete 464 | * explanation, see Notes below. 465 | * 466 | * @param values - Input cash flows per time period. 467 | * By convention, net "deposits" 468 | * are negative and net "withdrawals" are positive. Thus, for 469 | * example, at least the first element of `values`, which represents 470 | * the initial investment, will typically be negative. 471 | * @param guess - Starting guess for solving the Internal Rate of Return 472 | * @param tol - Required tolerance for the solution 473 | * @param maxIter - Maximum iterations in finding the solution 474 | * 475 | * @returns Internal Rate of Return for periodic input values 476 | * 477 | * @since v0.0.17 478 | * 479 | * ## Notes 480 | * 481 | * The IRR is perhaps best understood through an example (illustrated 482 | * using `irr` in the Examples section below). 483 | * 484 | * Suppose one invests 100 485 | * units and then makes the following withdrawals at regular (fixed) 486 | * intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100 487 | * unit investment yields 173 units; however, due to the combination of 488 | * compounding and the periodic withdrawals, the "average" rate of return 489 | * is neither simply 0.73/4 nor (1.73)^0.25-1. 490 | * Rather, it is the solution (for `r`) of the equation: 491 | * 492 | * ``` 493 | * -100 + 39/(1+r) + 59/((1+r)^2) + 55/((1+r)^3) + 20/((1+r)^4) = 0 494 | * ``` 495 | * 496 | * In general, for `values` = `[0, 1, ... M]`, 497 | * `irr` is the solution of the equation: 498 | * 499 | * ``` 500 | * \\sum_{t=0}^M{\\frac{v_t}{(1+irr)^{t}}} = 0 501 | * ``` 502 | * 503 | * ## Example 504 | * 505 | * ```javascript 506 | * import { irr } from 'financial' 507 | * 508 | * irr([-100, 39, 59, 55, 20]) // 0.28095 509 | * irr([-100, 0, 0, 74]) // -0.0955 510 | * irr([-100, 100, 0, -7]) // -0.0833 511 | * irr([-100, 100, 0, 7]) // 0.06206 512 | * irr([-5, 10.5, 1, -8, 1]) // 0.0886 513 | * ``` 514 | * 515 | * ## References 516 | * 517 | * - L. J. Gitman, "Principles of Managerial Finance, Brief," 3rd ed., 518 | * Addison-Wesley, 2003, pg. 348. 519 | */ 520 | export function irr( 521 | values: number[], 522 | guess = 0.1, 523 | tol = 1e-6, 524 | maxIter = 100, 525 | ): number { 526 | // Based on https://gist.github.com/ghalimi/4591338 by @ghalimi 527 | // ASF licensed (check the link for the full license) 528 | // Credits: algorithm inspired by Apache OpenOffice 529 | 530 | // Initialize dates and check that values contains at 531 | // least one positive value and one negative value 532 | const dates: number[] = [] 533 | let positive = false 534 | let negative = false 535 | for (let i = 0; i < values.length; i++) { 536 | dates[i] = i === 0 ? 0 : dates[i - 1] + 365 537 | if (values[i] > 0) { 538 | positive = true 539 | } 540 | if (values[i] < 0) { 541 | negative = true 542 | } 543 | } 544 | 545 | // Return error if values does not contain at least one positive 546 | // value and one negative value 547 | if (!positive || !negative) { 548 | return Number.NaN 549 | } 550 | 551 | // Initialize guess and resultRate 552 | let resultRate = guess 553 | 554 | // Implement Newton's method 555 | let newRate = 0 556 | let epsRate = 0 557 | let resultValue = 0 558 | let iteration = 0 559 | let contLoop = true 560 | do { 561 | resultValue = _irrResult(values, dates, resultRate) 562 | newRate = 563 | resultRate - resultValue / _irrResultDeriv(values, dates, resultRate) 564 | epsRate = Math.abs(newRate - resultRate) 565 | resultRate = newRate 566 | contLoop = epsRate > tol && Math.abs(resultValue) > tol 567 | } while (contLoop && ++iteration < maxIter) 568 | 569 | if (contLoop) { 570 | return Number.NaN 571 | } 572 | 573 | // Return internal rate of return 574 | return resultRate 575 | } 576 | 577 | /** 578 | * Returns the NPV (Net Present Value) of a cash flow series. 579 | * 580 | * @param rate - The discount rate 581 | * @param values - The values of the time series of cash flows. The (fixed) time 582 | * interval between cash flow "events" must be the same as that for 583 | * which `rate` is given (i.e., if `rate` is per year, then precisely 584 | * a year is understood to elapse between each cash flow event). By 585 | * convention, investments or "deposits" are negative, income or 586 | * "withdrawals" are positive; `values` must begin with the initial 587 | * investment, thus `values[0]` will typically be negative. 588 | * @returns The NPV of the input cash flow series `values` at the discount `rate`. 589 | * 590 | * @since v0.0.18 591 | * 592 | * ## Warnings 593 | * 594 | * `npv considers a series of cashflows starting in the present (t = 0). 595 | * NPV can also be defined with a series of future cashflows, paid at the 596 | * end, rather than the start, of each period. If future cashflows are used, 597 | * the first cashflow `values[0]` must be zeroed and added to the net 598 | * present value of the future cashflows. This is demonstrated in the 599 | * examples. 600 | * 601 | * ## Notes 602 | * 603 | * Returns the result of: 604 | * 605 | * ``` 606 | * \\sum_{t=0}^{M-1}{\\frac{values_t}{(1+rate)^{t}}} 607 | * ``` 608 | * 609 | * ## Examples 610 | * 611 | * Consider a potential project with an initial investment of $40 000 and 612 | * projected cashflows of $5 000, $8 000, $12 000 and $30 000 at the end of 613 | * each period discounted at a rate of 8% per period. To find the project's 614 | * net present value: 615 | * 616 | * ```javascript 617 | * import {npv} from 'financial' 618 | * 619 | * const rate = 0.08 620 | * const cashflows = [-40_000, 5000, 8000, 12000, 30000] 621 | * npv(rate, cashflows) // 3065.2226681795255 622 | * ``` 623 | * 624 | * It may be preferable to split the projected cashflow into an initial 625 | * investment and expected future cashflows. In this case, the value of 626 | * the initial cashflow is zero and the initial investment is later added 627 | * to the future cashflows net present value: 628 | * 629 | * ```javascript 630 | * const initialCashflow = cashflows[0] 631 | * cashflows[0] = 0 632 | * 633 | * npv(rate, cashflows) + initialCashflow // 3065.2226681795255 634 | * ``` 635 | * 636 | * ## References 637 | * 638 | * L. J. Gitman, "Principles of Managerial Finance, Brief," 639 | * 3rd ed., Addison-Wesley, 2003, pg. 346. 640 | */ 641 | export function npv(rate: number, values: number[]): number { 642 | return values.reduce((acc, curr, i) => acc + curr / (1 + rate) ** i, 0) 643 | } 644 | 645 | /** 646 | * Calculates the Modified Internal Rate of Return. 647 | * 648 | * @param values - Cash flows (must contain at least one positive and one negative 649 | * value) or nan is returned. The first value is considered a sunk 650 | * cost at time zero. 651 | * @param financeRate - Interest rate paid on the cash flows 652 | * @param reinvestRate - Interest rate received on the cash flows upon reinvestment 653 | * 654 | * @returns Modified internal rate of return 655 | * 656 | * @since v0.1.0 657 | */ 658 | export function mirr( 659 | values: number[], 660 | financeRate: number, 661 | reinvestRate: number, 662 | ): number { 663 | let positive = false 664 | let negative = false 665 | for (const value of values) { 666 | if (value > 0) { 667 | positive = true 668 | } 669 | if (value < 0) { 670 | negative = true 671 | } 672 | } 673 | 674 | // Return error if values does not contain at least one 675 | // positive value and one negative value 676 | if (!positive || !negative) { 677 | return Number.NaN 678 | } 679 | 680 | const numer = Math.abs( 681 | npv( 682 | reinvestRate, 683 | values.map((x) => (x > 0 ? x : 0)), 684 | ), 685 | ) 686 | const denom = Math.abs( 687 | npv( 688 | financeRate, 689 | values.map((x) => (x < 0 ? x : 0)), 690 | ), 691 | ) 692 | return (numer / denom) ** (1 / (values.length - 1)) * (1 + reinvestRate) - 1 693 | } 694 | 695 | /** 696 | * This function is here to simply have a different name for the 'fv' 697 | * function to not interfere with the 'fv' keyword argument within the 'ipmt' 698 | * function. It is the 'remaining balance on loan' which might be useful as 699 | * it's own function, but is easily calculated with the 'fv' function. 700 | * 701 | * @private 702 | */ 703 | function _rbl( 704 | rate: number, 705 | per: number, 706 | pmt: number, 707 | pv: number, 708 | when: PaymentDueTime, 709 | ) { 710 | return fv(rate, per - 1, pmt, pv, when) 711 | } 712 | 713 | /** 714 | * Evaluates `g(r_n)/g'(r_n)`, where: 715 | * 716 | * ``` 717 | * g = fv + pv * (1+rate) ** nper + pmt * (1+rate * when)/rate * ((1+rate) ** nper - 1) 718 | * ``` 719 | * 720 | * @private 721 | */ 722 | function _gDivGp( 723 | r: number, 724 | n: number, 725 | p: number, 726 | x: number, 727 | y: number, 728 | when: PaymentDueTime, 729 | ): number { 730 | const w = when === PaymentDueTime.Begin ? 1 : 0 731 | 732 | const t1 = (r + 1) ** n 733 | const t2 = (r + 1) ** (n - 1) 734 | const g = y + t1 * x + (p * (t1 - 1) * (r * w + 1)) / r 735 | const gp = 736 | n * t2 * x - 737 | (p * (t1 - 1) * (r * w + 1)) / r ** 2 + 738 | (n * p * t2 * (r * w + 1)) / r + 739 | (p * (t1 - 1) * w) / r 740 | return g / gp 741 | } 742 | 743 | /** 744 | * Calculates the resulting amount. 745 | * 746 | * Based on https://gist.github.com/ghalimi/4591338 by @ghalimi 747 | * ASF licensed (check the link for the full license) 748 | * 749 | * @private 750 | */ 751 | function _irrResult(values: number[], dates: number[], rate: number): number { 752 | const r = rate + 1 753 | let result = values[0] 754 | for (let i = 1; i < values.length; i++) { 755 | result += values[i] / r ** ((dates[i] - dates[0]) / 365) 756 | } 757 | return result 758 | } 759 | 760 | /** 761 | * Calculates the first derivation 762 | * 763 | * Based on https://gist.github.com/ghalimi/4591338 by @ghalimi 764 | * ASF licensed (check the link for the full license) 765 | * 766 | * @private 767 | */ 768 | function _irrResultDeriv( 769 | values: number[], 770 | dates: number[], 771 | rate: number, 772 | ): number { 773 | const r = rate + 1 774 | let result = 0 775 | for (let i = 1; i < values.length; i++) { 776 | const frac = (dates[i] - dates[0]) / 365 777 | result -= (frac * values[i]) / r ** (frac + 1) 778 | } 779 | return result 780 | } 781 | --------------------------------------------------------------------------------