@pyverret/ratejs 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dist/index.cjs +395 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +388 -72
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -28,14 +28,14 @@ function cagr(params) {
|
|
|
28
28
|
|
|
29
29
|
// src/interest/compound.ts
|
|
30
30
|
function compound(params) {
|
|
31
|
-
const { principal, rate, timesPerYear, years } = params;
|
|
31
|
+
const { principal, rate: rate2, timesPerYear, years } = params;
|
|
32
32
|
assertNonNegative(principal, "principal");
|
|
33
33
|
assertPositive(timesPerYear, "timesPerYear");
|
|
34
34
|
assertNonNegative(years, "years");
|
|
35
|
-
if (
|
|
35
|
+
if (rate2 === 0 || years === 0) return principal;
|
|
36
36
|
const n = timesPerYear;
|
|
37
37
|
const t = years;
|
|
38
|
-
return principal * (1 +
|
|
38
|
+
return principal * (1 + rate2 / n) ** (n * t);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// src/interest/effectiveAnnualRate.ts
|
|
@@ -49,14 +49,14 @@ function effectiveAnnualRate(params) {
|
|
|
49
49
|
|
|
50
50
|
// src/interest/futureValue.ts
|
|
51
51
|
function futureValue(params) {
|
|
52
|
-
const { presentValue: presentValue2, rate, timesPerYear, years } = params;
|
|
52
|
+
const { presentValue: presentValue2, rate: rate2, timesPerYear, years } = params;
|
|
53
53
|
assertNonNegative(presentValue2, "presentValue");
|
|
54
54
|
assertPositive(timesPerYear, "timesPerYear");
|
|
55
55
|
assertNonNegative(years, "years");
|
|
56
|
-
if (
|
|
56
|
+
if (rate2 === 0 || years === 0) return presentValue2;
|
|
57
57
|
const n = timesPerYear;
|
|
58
58
|
const t = years;
|
|
59
|
-
return presentValue2 * (1 +
|
|
59
|
+
return presentValue2 * (1 + rate2 / n) ** (n * t);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// src/interest/inflationAdjustedAmount.ts
|
|
@@ -74,7 +74,7 @@ function investmentGrowth(params) {
|
|
|
74
74
|
const {
|
|
75
75
|
initial,
|
|
76
76
|
contributionPerPeriod = 0,
|
|
77
|
-
rate,
|
|
77
|
+
rate: rate2,
|
|
78
78
|
timesPerYear,
|
|
79
79
|
years,
|
|
80
80
|
contributionTiming = "end"
|
|
@@ -91,11 +91,11 @@ function investmentGrowth(params) {
|
|
|
91
91
|
totalInterest: 0
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
-
const r =
|
|
95
|
-
const fvInitial =
|
|
94
|
+
const r = rate2 / timesPerYear;
|
|
95
|
+
const fvInitial = rate2 === 0 ? initial : initial * (1 + r) ** periods;
|
|
96
96
|
let fvContrib = 0;
|
|
97
97
|
if (contributionPerPeriod !== 0) {
|
|
98
|
-
if (
|
|
98
|
+
if (rate2 === 0) {
|
|
99
99
|
fvContrib = contributionPerPeriod * periods;
|
|
100
100
|
} else {
|
|
101
101
|
const ordinary = contributionPerPeriod * (((1 + r) ** periods - 1) / r);
|
|
@@ -108,38 +108,180 @@ function investmentGrowth(params) {
|
|
|
108
108
|
return { futureValue: futureValue2, totalContributions, totalInterest };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// src/utils/solvers.ts
|
|
112
|
+
function newtonRaphson(params) {
|
|
113
|
+
const {
|
|
114
|
+
initialGuess,
|
|
115
|
+
fn,
|
|
116
|
+
derivative,
|
|
117
|
+
tolerance = 1e-10,
|
|
118
|
+
maxIterations = 100,
|
|
119
|
+
min = Number.NEGATIVE_INFINITY,
|
|
120
|
+
max = Number.POSITIVE_INFINITY
|
|
121
|
+
} = params;
|
|
122
|
+
let x = initialGuess;
|
|
123
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
124
|
+
const value = fn(x);
|
|
125
|
+
if (!Number.isFinite(value)) return void 0;
|
|
126
|
+
if (Math.abs(value) <= tolerance) return x;
|
|
127
|
+
const slope = derivative(x);
|
|
128
|
+
if (!Number.isFinite(slope) || Math.abs(slope) < 1e-14) return void 0;
|
|
129
|
+
x -= value / slope;
|
|
130
|
+
if (x < min) x = min;
|
|
131
|
+
if (x > max) x = max;
|
|
132
|
+
}
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
function bisection(params) {
|
|
136
|
+
const { fn, lower, upper, tolerance = 1e-10, maxIterations = 200 } = params;
|
|
137
|
+
let a = lower;
|
|
138
|
+
let b = upper;
|
|
139
|
+
let fa = fn(a);
|
|
140
|
+
let fb = fn(b);
|
|
141
|
+
if (!Number.isFinite(fa) || !Number.isFinite(fb) || fa * fb > 0) return void 0;
|
|
142
|
+
if (Math.abs(fa) <= tolerance) return a;
|
|
143
|
+
if (Math.abs(fb) <= tolerance) return b;
|
|
144
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
145
|
+
const c = (a + b) / 2;
|
|
146
|
+
const fc = fn(c);
|
|
147
|
+
if (!Number.isFinite(fc)) return void 0;
|
|
148
|
+
if (Math.abs(fc) <= tolerance || Math.abs(b - a) <= tolerance) return c;
|
|
149
|
+
if (fa * fc < 0) {
|
|
150
|
+
b = c;
|
|
151
|
+
fb = fc;
|
|
152
|
+
} else {
|
|
153
|
+
a = c;
|
|
154
|
+
fa = fc;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return (a + b) / 2;
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
// src/interest/irr.ts
|
|
112
|
-
function
|
|
161
|
+
function npvAt(rate2, cashFlows) {
|
|
113
162
|
let sum = 0;
|
|
114
163
|
for (let t = 0; t < cashFlows.length; t++) {
|
|
115
164
|
const cf = cashFlows[t];
|
|
116
|
-
if (cf !== void 0) sum += cf / (1 +
|
|
165
|
+
if (cf !== void 0) sum += cf / (1 + rate2) ** t;
|
|
117
166
|
}
|
|
118
167
|
return sum;
|
|
119
168
|
}
|
|
120
|
-
function
|
|
169
|
+
function npvDerivativeAt(rate2, cashFlows) {
|
|
121
170
|
let sum = 0;
|
|
122
171
|
for (let t = 1; t < cashFlows.length; t++) {
|
|
123
172
|
const cf = cashFlows[t];
|
|
124
|
-
if (cf !== void 0) sum -= t * cf / (1 +
|
|
173
|
+
if (cf !== void 0) sum -= t * cf / (1 + rate2) ** (t + 1);
|
|
125
174
|
}
|
|
126
175
|
return sum;
|
|
127
176
|
}
|
|
177
|
+
function hasPositiveAndNegative(cashFlows) {
|
|
178
|
+
let hasPositive = false;
|
|
179
|
+
let hasNegative = false;
|
|
180
|
+
for (const cf of cashFlows) {
|
|
181
|
+
if (cf > 0) hasPositive = true;
|
|
182
|
+
if (cf < 0) hasNegative = true;
|
|
183
|
+
if (hasPositive && hasNegative) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function findBracket(fn, lowerBound, upperBound) {
|
|
188
|
+
const fLower = fn(lowerBound);
|
|
189
|
+
const fUpper = fn(upperBound);
|
|
190
|
+
if (!Number.isFinite(fLower) || !Number.isFinite(fUpper)) return void 0;
|
|
191
|
+
if (fLower === 0) return { lower: lowerBound, upper: lowerBound };
|
|
192
|
+
if (fUpper === 0) return { lower: upperBound, upper: upperBound };
|
|
193
|
+
if (fLower * fUpper < 0) return { lower: lowerBound, upper: upperBound };
|
|
194
|
+
let lower = lowerBound;
|
|
195
|
+
let upper = upperBound;
|
|
196
|
+
let fLo = fLower;
|
|
197
|
+
let fHi = fUpper;
|
|
198
|
+
for (let i = 0; i < 20; i++) {
|
|
199
|
+
const nextLower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
200
|
+
const fNextLower = fn(nextLower);
|
|
201
|
+
if (Number.isFinite(fNextLower) && fNextLower * fHi < 0) {
|
|
202
|
+
return { lower: nextLower, upper };
|
|
203
|
+
}
|
|
204
|
+
if (Number.isFinite(fNextLower)) {
|
|
205
|
+
lower = nextLower;
|
|
206
|
+
fLo = fNextLower;
|
|
207
|
+
}
|
|
208
|
+
upper = upper * 2 + 1;
|
|
209
|
+
fHi = fn(upper);
|
|
210
|
+
if (!Number.isFinite(fHi)) continue;
|
|
211
|
+
if (fLo * fHi < 0) return { lower, upper };
|
|
212
|
+
}
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
128
215
|
function irr(params) {
|
|
129
|
-
const {
|
|
130
|
-
|
|
216
|
+
const {
|
|
217
|
+
cashFlows,
|
|
218
|
+
guess = 0.1,
|
|
219
|
+
maxIterations = 100,
|
|
220
|
+
lowerBound = -0.99,
|
|
221
|
+
upperBound = 10
|
|
222
|
+
} = params;
|
|
223
|
+
if (cashFlows.length === 0) {
|
|
224
|
+
throw new RangeError("cashFlows must contain at least one value");
|
|
225
|
+
}
|
|
226
|
+
assertFiniteNumber(guess, "guess");
|
|
227
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
228
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
229
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
230
|
+
if (maxIterations <= 0 || !Number.isInteger(maxIterations)) {
|
|
231
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
232
|
+
}
|
|
233
|
+
if (lowerBound <= -1) {
|
|
234
|
+
throw new RangeError("lowerBound must be > -1");
|
|
235
|
+
}
|
|
236
|
+
if (upperBound <= lowerBound) {
|
|
237
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
238
|
+
}
|
|
131
239
|
for (const cf of cashFlows) assertFiniteNumber(cf, "cashFlows[]");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const val = npv(r, cashFlows);
|
|
135
|
-
if (Math.abs(val) < 1e-10) return r;
|
|
136
|
-
const der = npvDerivative(r, cashFlows);
|
|
137
|
-
if (!Number.isFinite(der) || Math.abs(der) < 1e-15) break;
|
|
138
|
-
r = r - val / der;
|
|
139
|
-
if (r < -0.99) r = -0.99;
|
|
140
|
-
if (r > 10) r = 10;
|
|
240
|
+
if (!hasPositiveAndNegative(cashFlows)) {
|
|
241
|
+
throw new RangeError("cashFlows must include at least one positive and one negative value");
|
|
141
242
|
}
|
|
142
|
-
|
|
243
|
+
const fn = (rate2) => npvAt(rate2, cashFlows);
|
|
244
|
+
const derivative = (rate2) => npvDerivativeAt(rate2, cashFlows);
|
|
245
|
+
const newton = newtonRaphson({
|
|
246
|
+
initialGuess: guess,
|
|
247
|
+
fn,
|
|
248
|
+
derivative,
|
|
249
|
+
tolerance: 1e-10,
|
|
250
|
+
maxIterations,
|
|
251
|
+
min: lowerBound,
|
|
252
|
+
max: upperBound
|
|
253
|
+
});
|
|
254
|
+
if (newton !== void 0) return newton;
|
|
255
|
+
const bracket = findBracket(fn, lowerBound, upperBound);
|
|
256
|
+
if (bracket === void 0) {
|
|
257
|
+
throw new RangeError("IRR did not converge within search bounds");
|
|
258
|
+
}
|
|
259
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
260
|
+
const bisected = bisection({
|
|
261
|
+
fn,
|
|
262
|
+
lower: bracket.lower,
|
|
263
|
+
upper: bracket.upper,
|
|
264
|
+
tolerance: 1e-10,
|
|
265
|
+
maxIterations: maxIterations * 2
|
|
266
|
+
});
|
|
267
|
+
if (bisected !== void 0) return bisected;
|
|
268
|
+
throw new RangeError("IRR did not converge");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/interest/npv.ts
|
|
272
|
+
function npv(params) {
|
|
273
|
+
const { rate: rate2, cashFlows } = params;
|
|
274
|
+
assertFiniteNumber(rate2, "rate");
|
|
275
|
+
if (rate2 <= -1) throw new RangeError("rate must be > -1");
|
|
276
|
+
if (cashFlows.length === 0) return 0;
|
|
277
|
+
let sum = 0;
|
|
278
|
+
for (let t = 0; t < cashFlows.length; t++) {
|
|
279
|
+
const cf = cashFlows[t];
|
|
280
|
+
if (cf === void 0) continue;
|
|
281
|
+
assertFiniteNumber(cf, `cashFlows[${t}]`);
|
|
282
|
+
sum += cf / (1 + rate2) ** t;
|
|
283
|
+
}
|
|
284
|
+
return sum;
|
|
143
285
|
}
|
|
144
286
|
|
|
145
287
|
// src/interest/paymentFromPresentValue.ts
|
|
@@ -149,12 +291,12 @@ function paymentFromPresentValue(params) {
|
|
|
149
291
|
assertNonNegative(periods, "periods");
|
|
150
292
|
if (presentValue2 === 0 || periods === 0) return 0;
|
|
151
293
|
if (ratePerPeriod === 0) {
|
|
152
|
-
const
|
|
153
|
-
return timing === "begin" ?
|
|
294
|
+
const pmt3 = presentValue2 / periods;
|
|
295
|
+
return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
|
|
154
296
|
}
|
|
155
|
-
let
|
|
156
|
-
if (timing === "begin")
|
|
157
|
-
return
|
|
297
|
+
let pmt2 = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
|
|
298
|
+
if (timing === "begin") pmt2 /= 1 + ratePerPeriod;
|
|
299
|
+
return pmt2;
|
|
158
300
|
}
|
|
159
301
|
|
|
160
302
|
// src/interest/periodsToReachGoal.ts
|
|
@@ -162,54 +304,54 @@ function periodsToReachGoal(params) {
|
|
|
162
304
|
const {
|
|
163
305
|
principal,
|
|
164
306
|
targetFutureValue,
|
|
165
|
-
rate,
|
|
307
|
+
rate: rate2,
|
|
166
308
|
timesPerYear,
|
|
167
309
|
contributionPerPeriod = 0,
|
|
168
310
|
contributionTiming = "end"
|
|
169
311
|
} = params;
|
|
170
312
|
assertNonNegative(principal, "principal");
|
|
171
313
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
172
|
-
assertFiniteNumber(
|
|
314
|
+
assertFiniteNumber(rate2, "rate");
|
|
173
315
|
assertPositive(timesPerYear, "timesPerYear");
|
|
174
316
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
175
317
|
if (targetFutureValue <= principal) return 0;
|
|
176
|
-
const r =
|
|
318
|
+
const r = rate2 / timesPerYear;
|
|
177
319
|
if (r <= -1) {
|
|
178
320
|
throw new RangeError("rate / timesPerYear must be > -1");
|
|
179
321
|
}
|
|
180
322
|
if (contributionPerPeriod === 0) {
|
|
181
323
|
if (principal === 0) return Number.POSITIVE_INFINITY;
|
|
182
|
-
if (
|
|
324
|
+
if (rate2 === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
|
|
183
325
|
if (r < 0) return Number.POSITIVE_INFINITY;
|
|
184
326
|
const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
|
|
185
327
|
return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
|
|
186
328
|
}
|
|
187
|
-
if (
|
|
329
|
+
if (rate2 === 0) {
|
|
188
330
|
const needed = targetFutureValue - principal;
|
|
189
331
|
if (needed <= 0) return 0;
|
|
190
332
|
return Math.ceil(needed / contributionPerPeriod);
|
|
191
333
|
}
|
|
192
|
-
let
|
|
334
|
+
let fv2 = principal;
|
|
193
335
|
let periods = 0;
|
|
194
336
|
const maxPeriods = 1e4;
|
|
195
|
-
while (
|
|
196
|
-
if (contributionTiming === "begin")
|
|
197
|
-
|
|
337
|
+
while (fv2 < targetFutureValue && periods < maxPeriods) {
|
|
338
|
+
if (contributionTiming === "begin") fv2 += contributionPerPeriod;
|
|
339
|
+
fv2 = fv2 * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
|
|
198
340
|
periods++;
|
|
199
341
|
}
|
|
200
|
-
return
|
|
342
|
+
return fv2 >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
|
|
201
343
|
}
|
|
202
344
|
|
|
203
345
|
// src/interest/presentValue.ts
|
|
204
346
|
function presentValue(params) {
|
|
205
|
-
const { futureValue:
|
|
206
|
-
assertNonNegative(
|
|
347
|
+
const { futureValue: fv2, rate: rate2, timesPerYear, years } = params;
|
|
348
|
+
assertNonNegative(fv2, "futureValue");
|
|
207
349
|
assertPositive(timesPerYear, "timesPerYear");
|
|
208
350
|
assertNonNegative(years, "years");
|
|
209
|
-
if (
|
|
351
|
+
if (rate2 === 0 || years === 0) return fv2;
|
|
210
352
|
const n = timesPerYear;
|
|
211
353
|
const t = years;
|
|
212
|
-
return
|
|
354
|
+
return fv2 / (1 + rate2 / n) ** (n * t);
|
|
213
355
|
}
|
|
214
356
|
|
|
215
357
|
// src/interest/presentValueOfAnnuity.ts
|
|
@@ -219,11 +361,11 @@ function presentValueOfAnnuity(params) {
|
|
|
219
361
|
assertNonNegative(periods, "periods");
|
|
220
362
|
if (periods === 0) return 0;
|
|
221
363
|
if (ratePerPeriod === 0) {
|
|
222
|
-
const
|
|
223
|
-
return timing === "begin" ?
|
|
364
|
+
const pv3 = paymentPerPeriod * periods;
|
|
365
|
+
return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
|
|
224
366
|
}
|
|
225
|
-
const
|
|
226
|
-
return timing === "begin" ?
|
|
367
|
+
const pv2 = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
|
|
368
|
+
return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
|
|
227
369
|
}
|
|
228
370
|
|
|
229
371
|
// src/interest/rateToReachGoal.ts
|
|
@@ -246,27 +388,37 @@ function rateToReachGoal(params) {
|
|
|
246
388
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
247
389
|
}
|
|
248
390
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
249
|
-
|
|
250
|
-
if (r <= -1) r = 0.01;
|
|
251
|
-
for (let i = 0; i < 100; i++) {
|
|
391
|
+
const fn = (r) => {
|
|
252
392
|
const onePlusR = 1 + r;
|
|
253
393
|
const onePlusRN = onePlusR ** periods;
|
|
254
394
|
const fvLump = principal * onePlusRN;
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
395
|
+
const annuityBase = r === 0 ? periods : (onePlusRN - 1) / r;
|
|
396
|
+
const fvAnnuity = contributionPerPeriod * annuityBase * (1 + dueFactor * r);
|
|
397
|
+
return fvLump + fvAnnuity - targetFutureValue;
|
|
398
|
+
};
|
|
399
|
+
const derivative = (r) => {
|
|
400
|
+
const delta = Math.abs(r) > 1e-6 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
401
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
402
|
+
};
|
|
403
|
+
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
404
|
+
const newton = newtonRaphson({
|
|
405
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, -0.99) : 0.01,
|
|
406
|
+
fn,
|
|
407
|
+
derivative,
|
|
408
|
+
tolerance: 1e-10,
|
|
409
|
+
maxIterations: 100,
|
|
410
|
+
min: -0.99,
|
|
411
|
+
max: 10
|
|
412
|
+
});
|
|
413
|
+
if (newton !== void 0) return newton;
|
|
414
|
+
const bisected = bisection({
|
|
415
|
+
fn,
|
|
416
|
+
lower: -0.99,
|
|
417
|
+
upper: 10,
|
|
418
|
+
tolerance: 1e-10,
|
|
419
|
+
maxIterations: 200
|
|
420
|
+
});
|
|
421
|
+
return bisected ?? Number.NaN;
|
|
270
422
|
}
|
|
271
423
|
|
|
272
424
|
// src/interest/realReturn.ts
|
|
@@ -280,10 +432,174 @@ function realReturn(params) {
|
|
|
280
432
|
|
|
281
433
|
// src/interest/ruleOf72.ts
|
|
282
434
|
function ruleOf72(params) {
|
|
283
|
-
const { rate, constant = 72 } = params;
|
|
284
|
-
assertFiniteNumber(
|
|
285
|
-
assertPositive(
|
|
286
|
-
return constant / 100 /
|
|
435
|
+
const { rate: rate2, constant = 72 } = params;
|
|
436
|
+
assertFiniteNumber(rate2, "rate");
|
|
437
|
+
assertPositive(rate2, "rate");
|
|
438
|
+
return constant / 100 / rate2;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/interest/tvom.ts
|
|
442
|
+
function assertTiming(value, name) {
|
|
443
|
+
if (value !== "end" && value !== "begin") {
|
|
444
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function annuityDueFactor(ratePerPeriod, timing) {
|
|
448
|
+
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
449
|
+
}
|
|
450
|
+
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
451
|
+
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
452
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
453
|
+
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
454
|
+
return -(presentValue2 * growth + paymentFv);
|
|
455
|
+
}
|
|
456
|
+
function fv(params) {
|
|
457
|
+
const { ratePerPeriod, periods, payment = 0, presentValue: presentValue2, timing = "end" } = params;
|
|
458
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
459
|
+
assertPositive(periods, "periods");
|
|
460
|
+
assertFiniteNumber(payment, "payment");
|
|
461
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
462
|
+
assertTiming(timing, "timing");
|
|
463
|
+
return futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
464
|
+
}
|
|
465
|
+
function pv(params) {
|
|
466
|
+
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
467
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
468
|
+
assertPositive(periods, "periods");
|
|
469
|
+
assertFiniteNumber(payment, "payment");
|
|
470
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
471
|
+
assertTiming(timing, "timing");
|
|
472
|
+
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
473
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
474
|
+
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
475
|
+
return -futureValue2 / growth - paymentPv;
|
|
476
|
+
}
|
|
477
|
+
function pmt(params) {
|
|
478
|
+
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
479
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
480
|
+
assertPositive(periods, "periods");
|
|
481
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
482
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
483
|
+
assertTiming(timing, "timing");
|
|
484
|
+
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
485
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
486
|
+
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
487
|
+
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
488
|
+
return numerator / denominator;
|
|
489
|
+
}
|
|
490
|
+
function nper(params) {
|
|
491
|
+
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
492
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
493
|
+
assertFiniteNumber(payment, "payment");
|
|
494
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
495
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
496
|
+
assertTiming(timing, "timing");
|
|
497
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
498
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
499
|
+
}
|
|
500
|
+
if (ratePerPeriod === 0) {
|
|
501
|
+
const linear = -(presentValue2 + futureValue2) / payment;
|
|
502
|
+
return Number.isFinite(linear) && linear >= 0 ? linear : Number.NaN;
|
|
503
|
+
}
|
|
504
|
+
const adjustedPayment = payment * annuityDueFactor(ratePerPeriod, timing);
|
|
505
|
+
const numerator = adjustedPayment - futureValue2 * ratePerPeriod;
|
|
506
|
+
const denominator = adjustedPayment + presentValue2 * ratePerPeriod;
|
|
507
|
+
if (numerator === 0 || denominator === 0) return Number.NaN;
|
|
508
|
+
const ratio = numerator / denominator;
|
|
509
|
+
if (ratio <= 0) return Number.NaN;
|
|
510
|
+
const periods = Math.log(ratio) / Math.log(1 + ratePerPeriod);
|
|
511
|
+
return Number.isFinite(periods) ? periods : Number.NaN;
|
|
512
|
+
}
|
|
513
|
+
function rate(params) {
|
|
514
|
+
const {
|
|
515
|
+
periods,
|
|
516
|
+
payment,
|
|
517
|
+
presentValue: presentValue2,
|
|
518
|
+
futureValue: futureValue2 = 0,
|
|
519
|
+
timing = "end",
|
|
520
|
+
guess = 0.1,
|
|
521
|
+
tolerance = 1e-10,
|
|
522
|
+
maxIterations = 100,
|
|
523
|
+
lowerBound = -0.999999999,
|
|
524
|
+
upperBound = 10
|
|
525
|
+
} = params;
|
|
526
|
+
assertPositive(periods, "periods");
|
|
527
|
+
assertFiniteNumber(payment, "payment");
|
|
528
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
529
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
530
|
+
assertFiniteNumber(guess, "guess");
|
|
531
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
532
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
533
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
534
|
+
assertTiming(timing, "timing");
|
|
535
|
+
if (ratePerPeriodIsInvalidForDomain(lowerBound)) {
|
|
536
|
+
throw new RangeError("lowerBound must be > -1");
|
|
537
|
+
}
|
|
538
|
+
if (upperBound <= lowerBound) {
|
|
539
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
540
|
+
}
|
|
541
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
542
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
543
|
+
}
|
|
544
|
+
const fn = (r) => futureValueFromCashFlows(r, periods, payment, presentValue2, timing) - futureValue2;
|
|
545
|
+
const derivative = (r) => {
|
|
546
|
+
const delta = Math.abs(r) > 1e-5 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
547
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
548
|
+
};
|
|
549
|
+
const newton = newtonRaphson({
|
|
550
|
+
initialGuess: guess,
|
|
551
|
+
fn,
|
|
552
|
+
derivative,
|
|
553
|
+
tolerance,
|
|
554
|
+
maxIterations,
|
|
555
|
+
min: lowerBound,
|
|
556
|
+
max: upperBound
|
|
557
|
+
});
|
|
558
|
+
if (newton !== void 0) return newton;
|
|
559
|
+
const bracket = findRateBracket(fn, lowerBound, upperBound);
|
|
560
|
+
if (bracket === void 0) {
|
|
561
|
+
throw new RangeError("RATE did not converge within search bounds");
|
|
562
|
+
}
|
|
563
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
564
|
+
const bisected = bisection({
|
|
565
|
+
fn,
|
|
566
|
+
lower: bracket.lower,
|
|
567
|
+
upper: bracket.upper,
|
|
568
|
+
tolerance,
|
|
569
|
+
maxIterations: maxIterations * 2
|
|
570
|
+
});
|
|
571
|
+
if (bisected !== void 0) return bisected;
|
|
572
|
+
throw new RangeError("RATE did not converge");
|
|
573
|
+
}
|
|
574
|
+
function ratePerPeriodIsInvalidForDomain(ratePerPeriod) {
|
|
575
|
+
return ratePerPeriod <= -1;
|
|
576
|
+
}
|
|
577
|
+
function findRateBracket(fn, lowerBound, upperBound) {
|
|
578
|
+
const scan = (start, end, segments = 200) => {
|
|
579
|
+
let prevX;
|
|
580
|
+
let prevValue;
|
|
581
|
+
for (let i = 0; i <= segments; i++) {
|
|
582
|
+
const x = start + (end - start) * i / segments;
|
|
583
|
+
const value = fn(x);
|
|
584
|
+
if (!Number.isFinite(value)) continue;
|
|
585
|
+
if (value === 0) return { lower: x, upper: x };
|
|
586
|
+
if (prevValue !== void 0 && prevX !== void 0 && prevValue * value < 0) {
|
|
587
|
+
return { lower: prevX, upper: x };
|
|
588
|
+
}
|
|
589
|
+
prevX = x;
|
|
590
|
+
prevValue = value;
|
|
591
|
+
}
|
|
592
|
+
return void 0;
|
|
593
|
+
};
|
|
594
|
+
let lower = lowerBound;
|
|
595
|
+
let upper = upperBound;
|
|
596
|
+
for (let i = 0; i < 20; i++) {
|
|
597
|
+
const bracket = scan(lower, upper);
|
|
598
|
+
if (bracket !== void 0) return bracket;
|
|
599
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
600
|
+
upper = upper * 2 + 1;
|
|
601
|
+
}
|
|
602
|
+
return scan(lower, upper, 400);
|
|
287
603
|
}
|
|
288
604
|
|
|
289
605
|
// src/loans/loanPayment.ts
|
|
@@ -405,6 +721,6 @@ function roundToCurrency(params) {
|
|
|
405
721
|
return Math.round(value * factor) / factor;
|
|
406
722
|
}
|
|
407
723
|
|
|
408
|
-
export { amortizationSchedule, cagr, compound, effectiveAnnualRate, futureValue, inflationAdjustedAmount, investmentGrowth, irr, loanPayment, paymentFromPresentValue, payoffPeriodWithExtra, periodsToReachGoal, presentValue, presentValueOfAnnuity, rateToReachGoal, realReturn, remainingBalance, roundToCurrency, ruleOf72 };
|
|
724
|
+
export { amortizationSchedule, bisection, cagr, compound, effectiveAnnualRate, futureValue, fv, inflationAdjustedAmount, investmentGrowth, irr, loanPayment, newtonRaphson, nper, npv, paymentFromPresentValue, payoffPeriodWithExtra, periodsToReachGoal, pmt, presentValue, presentValueOfAnnuity, pv, rate, rateToReachGoal, realReturn, remainingBalance, roundToCurrency, ruleOf72 };
|
|
409
725
|
//# sourceMappingURL=index.js.map
|
|
410
726
|
//# sourceMappingURL=index.js.map
|