@pyverret/ratejs 1.0.0 → 1.1.1
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 +74 -2
- package/dist/index.cjs +497 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -1
- package/dist/index.d.ts +99 -1
- package/dist/index.js +490 -82
- 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,179 @@ 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 scan = (start, end, segments = 200) => {
|
|
189
|
+
let prevX;
|
|
190
|
+
let prevValue;
|
|
191
|
+
for (let i = 0; i <= segments; i++) {
|
|
192
|
+
const x = start + (end - start) * i / segments;
|
|
193
|
+
const value = fn(x);
|
|
194
|
+
if (!Number.isFinite(value)) continue;
|
|
195
|
+
if (value === 0) return { lower: x, upper: x };
|
|
196
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
197
|
+
return { lower: prevX, upper: x };
|
|
198
|
+
}
|
|
199
|
+
prevX = x;
|
|
200
|
+
prevValue = value;
|
|
201
|
+
}
|
|
202
|
+
return void 0;
|
|
203
|
+
};
|
|
204
|
+
let lower = lowerBound;
|
|
205
|
+
let upper = upperBound;
|
|
206
|
+
for (let i = 0; i < 20; i++) {
|
|
207
|
+
const bracket = scan(lower, upper);
|
|
208
|
+
if (bracket !== void 0) return bracket;
|
|
209
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
210
|
+
upper = upper * 2 + 1;
|
|
211
|
+
}
|
|
212
|
+
return scan(lower, upper, 400);
|
|
213
|
+
}
|
|
128
214
|
function irr(params) {
|
|
129
|
-
const {
|
|
130
|
-
|
|
215
|
+
const {
|
|
216
|
+
cashFlows,
|
|
217
|
+
guess = 0.1,
|
|
218
|
+
maxIterations = 100,
|
|
219
|
+
lowerBound = -0.99,
|
|
220
|
+
upperBound = 10
|
|
221
|
+
} = params;
|
|
222
|
+
if (cashFlows.length === 0) {
|
|
223
|
+
throw new RangeError("cashFlows must contain at least one value");
|
|
224
|
+
}
|
|
225
|
+
assertFiniteNumber(guess, "guess");
|
|
226
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
227
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
228
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
229
|
+
if (maxIterations <= 0 || !Number.isInteger(maxIterations)) {
|
|
230
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
231
|
+
}
|
|
232
|
+
if (lowerBound <= -1) {
|
|
233
|
+
throw new RangeError("lowerBound must be > -1");
|
|
234
|
+
}
|
|
235
|
+
if (upperBound <= lowerBound) {
|
|
236
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
237
|
+
}
|
|
131
238
|
for (const cf of cashFlows) assertFiniteNumber(cf, "cashFlows[]");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
239
|
+
if (!hasPositiveAndNegative(cashFlows)) {
|
|
240
|
+
throw new RangeError("cashFlows must include at least one positive and one negative value");
|
|
241
|
+
}
|
|
242
|
+
const fn = (rate2) => npvAt(rate2, cashFlows);
|
|
243
|
+
const derivative = (rate2) => npvDerivativeAt(rate2, cashFlows);
|
|
244
|
+
const newton = newtonRaphson({
|
|
245
|
+
initialGuess: guess,
|
|
246
|
+
fn,
|
|
247
|
+
derivative,
|
|
248
|
+
tolerance: 1e-10,
|
|
249
|
+
maxIterations,
|
|
250
|
+
min: lowerBound,
|
|
251
|
+
max: upperBound
|
|
252
|
+
});
|
|
253
|
+
if (newton !== void 0) return newton;
|
|
254
|
+
const bracket = findBracket(fn, lowerBound, upperBound);
|
|
255
|
+
if (bracket === void 0) {
|
|
256
|
+
throw new RangeError("IRR did not converge within search bounds");
|
|
141
257
|
}
|
|
142
|
-
return
|
|
258
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
259
|
+
const bisected = bisection({
|
|
260
|
+
fn,
|
|
261
|
+
lower: bracket.lower,
|
|
262
|
+
upper: bracket.upper,
|
|
263
|
+
tolerance: 1e-10,
|
|
264
|
+
maxIterations: maxIterations * 2
|
|
265
|
+
});
|
|
266
|
+
if (bisected !== void 0) return bisected;
|
|
267
|
+
throw new RangeError("IRR did not converge");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/interest/npv.ts
|
|
271
|
+
function npv(params) {
|
|
272
|
+
const { rate: rate2, cashFlows } = params;
|
|
273
|
+
assertFiniteNumber(rate2, "rate");
|
|
274
|
+
if (rate2 <= -1) throw new RangeError("rate must be > -1");
|
|
275
|
+
if (cashFlows.length === 0) return 0;
|
|
276
|
+
let sum = 0;
|
|
277
|
+
for (let t = 0; t < cashFlows.length; t++) {
|
|
278
|
+
const cf = cashFlows[t];
|
|
279
|
+
if (cf === void 0) continue;
|
|
280
|
+
assertFiniteNumber(cf, `cashFlows[${t}]`);
|
|
281
|
+
sum += cf / (1 + rate2) ** t;
|
|
282
|
+
}
|
|
283
|
+
return sum;
|
|
143
284
|
}
|
|
144
285
|
|
|
145
286
|
// src/interest/paymentFromPresentValue.ts
|
|
@@ -149,12 +290,12 @@ function paymentFromPresentValue(params) {
|
|
|
149
290
|
assertNonNegative(periods, "periods");
|
|
150
291
|
if (presentValue2 === 0 || periods === 0) return 0;
|
|
151
292
|
if (ratePerPeriod === 0) {
|
|
152
|
-
const
|
|
153
|
-
return timing === "begin" ?
|
|
293
|
+
const pmt3 = presentValue2 / periods;
|
|
294
|
+
return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
|
|
154
295
|
}
|
|
155
|
-
let
|
|
156
|
-
if (timing === "begin")
|
|
157
|
-
return
|
|
296
|
+
let pmt2 = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
|
|
297
|
+
if (timing === "begin") pmt2 /= 1 + ratePerPeriod;
|
|
298
|
+
return pmt2;
|
|
158
299
|
}
|
|
159
300
|
|
|
160
301
|
// src/interest/periodsToReachGoal.ts
|
|
@@ -162,54 +303,56 @@ function periodsToReachGoal(params) {
|
|
|
162
303
|
const {
|
|
163
304
|
principal,
|
|
164
305
|
targetFutureValue,
|
|
165
|
-
rate,
|
|
306
|
+
rate: rate2,
|
|
166
307
|
timesPerYear,
|
|
167
308
|
contributionPerPeriod = 0,
|
|
168
|
-
contributionTiming = "end"
|
|
309
|
+
contributionTiming = "end",
|
|
310
|
+
maxPeriods = 1e5
|
|
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");
|
|
317
|
+
assertPositive(maxPeriods, "maxPeriods");
|
|
175
318
|
if (targetFutureValue <= principal) return 0;
|
|
176
|
-
const r =
|
|
319
|
+
const r = rate2 / timesPerYear;
|
|
177
320
|
if (r <= -1) {
|
|
178
321
|
throw new RangeError("rate / timesPerYear must be > -1");
|
|
179
322
|
}
|
|
180
323
|
if (contributionPerPeriod === 0) {
|
|
181
324
|
if (principal === 0) return Number.POSITIVE_INFINITY;
|
|
182
|
-
if (
|
|
325
|
+
if (rate2 === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
|
|
183
326
|
if (r < 0) return Number.POSITIVE_INFINITY;
|
|
184
327
|
const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
|
|
185
328
|
return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
|
|
186
329
|
}
|
|
187
|
-
if (
|
|
330
|
+
if (rate2 === 0) {
|
|
188
331
|
const needed = targetFutureValue - principal;
|
|
189
332
|
if (needed <= 0) return 0;
|
|
190
333
|
return Math.ceil(needed / contributionPerPeriod);
|
|
191
334
|
}
|
|
192
|
-
let
|
|
335
|
+
let fv2 = principal;
|
|
193
336
|
let periods = 0;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
fv = fv * (1 + r) + (contributionTiming === "end" ? contributionPerPeriod : 0);
|
|
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
|
-
|
|
342
|
+
if (fv2 >= targetFutureValue) return periods;
|
|
343
|
+
throw new RangeError("maxPeriods exceeded before reaching target");
|
|
201
344
|
}
|
|
202
345
|
|
|
203
346
|
// src/interest/presentValue.ts
|
|
204
347
|
function presentValue(params) {
|
|
205
|
-
const { futureValue:
|
|
206
|
-
assertNonNegative(
|
|
348
|
+
const { futureValue: fv2, rate: rate2, timesPerYear, years } = params;
|
|
349
|
+
assertNonNegative(fv2, "futureValue");
|
|
207
350
|
assertPositive(timesPerYear, "timesPerYear");
|
|
208
351
|
assertNonNegative(years, "years");
|
|
209
|
-
if (
|
|
352
|
+
if (rate2 === 0 || years === 0) return fv2;
|
|
210
353
|
const n = timesPerYear;
|
|
211
354
|
const t = years;
|
|
212
|
-
return
|
|
355
|
+
return fv2 / (1 + rate2 / n) ** (n * t);
|
|
213
356
|
}
|
|
214
357
|
|
|
215
358
|
// src/interest/presentValueOfAnnuity.ts
|
|
@@ -219,11 +362,11 @@ function presentValueOfAnnuity(params) {
|
|
|
219
362
|
assertNonNegative(periods, "periods");
|
|
220
363
|
if (periods === 0) return 0;
|
|
221
364
|
if (ratePerPeriod === 0) {
|
|
222
|
-
const
|
|
223
|
-
return timing === "begin" ?
|
|
365
|
+
const pv3 = paymentPerPeriod * periods;
|
|
366
|
+
return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
|
|
224
367
|
}
|
|
225
|
-
const
|
|
226
|
-
return timing === "begin" ?
|
|
368
|
+
const pv2 = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
|
|
369
|
+
return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
|
|
227
370
|
}
|
|
228
371
|
|
|
229
372
|
// src/interest/rateToReachGoal.ts
|
|
@@ -233,40 +376,102 @@ function rateToReachGoal(params) {
|
|
|
233
376
|
targetFutureValue,
|
|
234
377
|
periods,
|
|
235
378
|
contributionPerPeriod = 0,
|
|
236
|
-
contributionTiming = "end"
|
|
379
|
+
contributionTiming = "end",
|
|
380
|
+
lowerBound = -0.99,
|
|
381
|
+
upperBound = 10,
|
|
382
|
+
maxIterations = 100,
|
|
383
|
+
tolerance = 1e-10
|
|
237
384
|
} = params;
|
|
238
385
|
assertNonNegative(principal, "principal");
|
|
239
386
|
assertNonNegative(targetFutureValue, "targetFutureValue");
|
|
240
387
|
assertPositive(periods, "periods");
|
|
241
388
|
assertNonNegative(contributionPerPeriod, "contributionPerPeriod");
|
|
389
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
390
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
391
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
392
|
+
assertFiniteNumber(tolerance, "tolerance");
|
|
393
|
+
if (lowerBound <= -1) throw new RangeError("lowerBound must be > -1");
|
|
394
|
+
if (upperBound <= lowerBound) throw new RangeError("upperBound must be greater than lowerBound");
|
|
395
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
396
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
397
|
+
}
|
|
398
|
+
if (tolerance <= 0) throw new RangeError("tolerance must be > 0");
|
|
242
399
|
if (periods === 0) return 0;
|
|
243
400
|
if (targetFutureValue <= principal && contributionPerPeriod === 0) return 0;
|
|
244
401
|
if (contributionPerPeriod === 0) {
|
|
245
402
|
if (targetFutureValue <= principal) return 0;
|
|
403
|
+
if (principal === 0) {
|
|
404
|
+
throw new RangeError(
|
|
405
|
+
"rateToReachGoal is undefined when principal is 0 and contributions are 0"
|
|
406
|
+
);
|
|
407
|
+
}
|
|
246
408
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
247
409
|
}
|
|
248
410
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
249
|
-
|
|
250
|
-
if (r <= -1) r = 0.01;
|
|
251
|
-
for (let i = 0; i < 100; i++) {
|
|
411
|
+
const fn = (r) => {
|
|
252
412
|
const onePlusR = 1 + r;
|
|
253
413
|
const onePlusRN = onePlusR ** periods;
|
|
254
414
|
const fvLump = principal * onePlusRN;
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
415
|
+
const annuityBase = r === 0 ? periods : (onePlusRN - 1) / r;
|
|
416
|
+
const fvAnnuity = contributionPerPeriod * annuityBase * (1 + dueFactor * r);
|
|
417
|
+
return fvLump + fvAnnuity - targetFutureValue;
|
|
418
|
+
};
|
|
419
|
+
const derivative = (r) => {
|
|
420
|
+
const delta = Math.abs(r) > 1e-6 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
421
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
422
|
+
};
|
|
423
|
+
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
424
|
+
const newton = newtonRaphson({
|
|
425
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, lowerBound) : 0.01,
|
|
426
|
+
fn,
|
|
427
|
+
derivative,
|
|
428
|
+
tolerance,
|
|
429
|
+
maxIterations,
|
|
430
|
+
min: lowerBound,
|
|
431
|
+
max: upperBound
|
|
432
|
+
});
|
|
433
|
+
if (newton !== void 0) return newton;
|
|
434
|
+
const bracket = findBracket2(fn, lowerBound, upperBound);
|
|
435
|
+
if (bracket === void 0) {
|
|
436
|
+
throw new RangeError("rateToReachGoal did not converge within search bounds");
|
|
437
|
+
}
|
|
438
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
439
|
+
const bisected = bisection({
|
|
440
|
+
fn,
|
|
441
|
+
lower: bracket.lower,
|
|
442
|
+
upper: bracket.upper,
|
|
443
|
+
tolerance,
|
|
444
|
+
maxIterations: maxIterations * 2
|
|
445
|
+
});
|
|
446
|
+
if (bisected !== void 0) return bisected;
|
|
447
|
+
throw new RangeError("rateToReachGoal did not converge");
|
|
448
|
+
}
|
|
449
|
+
function findBracket2(fn, lowerBound, upperBound) {
|
|
450
|
+
const scan = (start, end, segments = 200) => {
|
|
451
|
+
let prevX;
|
|
452
|
+
let prevValue;
|
|
453
|
+
for (let i = 0; i <= segments; i++) {
|
|
454
|
+
const x = start + (end - start) * i / segments;
|
|
455
|
+
const value = fn(x);
|
|
456
|
+
if (!Number.isFinite(value)) continue;
|
|
457
|
+
if (value === 0) return { lower: x, upper: x };
|
|
458
|
+
if (prevX !== void 0 && prevValue !== void 0 && prevValue * value < 0) {
|
|
459
|
+
return { lower: prevX, upper: x };
|
|
460
|
+
}
|
|
461
|
+
prevX = x;
|
|
462
|
+
prevValue = value;
|
|
463
|
+
}
|
|
464
|
+
return void 0;
|
|
465
|
+
};
|
|
466
|
+
let lower = lowerBound;
|
|
467
|
+
let upper = upperBound;
|
|
468
|
+
for (let i = 0; i < 20; i++) {
|
|
469
|
+
const bracket = scan(lower, upper);
|
|
470
|
+
if (bracket !== void 0) return bracket;
|
|
471
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
472
|
+
upper = upper * 2 + 1;
|
|
473
|
+
}
|
|
474
|
+
return scan(lower, upper, 400);
|
|
270
475
|
}
|
|
271
476
|
|
|
272
477
|
// src/interest/realReturn.ts
|
|
@@ -280,23 +485,223 @@ function realReturn(params) {
|
|
|
280
485
|
|
|
281
486
|
// src/interest/ruleOf72.ts
|
|
282
487
|
function ruleOf72(params) {
|
|
283
|
-
const { rate, constant = 72 } = params;
|
|
284
|
-
assertFiniteNumber(
|
|
285
|
-
assertPositive(
|
|
286
|
-
return constant / 100 /
|
|
488
|
+
const { rate: rate2, constant = 72 } = params;
|
|
489
|
+
assertFiniteNumber(rate2, "rate");
|
|
490
|
+
assertPositive(rate2, "rate");
|
|
491
|
+
return constant / 100 / rate2;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/interest/tvom.ts
|
|
495
|
+
function assertTiming(value, name) {
|
|
496
|
+
if (value !== "end" && value !== "begin") {
|
|
497
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function annuityDueFactor(ratePerPeriod, timing) {
|
|
501
|
+
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
502
|
+
}
|
|
503
|
+
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
504
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
505
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
506
|
+
}
|
|
507
|
+
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
508
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
509
|
+
if (!Number.isFinite(growth) || growth <= 0) return Number.NaN;
|
|
510
|
+
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
511
|
+
return -(presentValue2 * growth + paymentFv);
|
|
512
|
+
}
|
|
513
|
+
function fv(params) {
|
|
514
|
+
const { ratePerPeriod, periods, payment = 0, presentValue: presentValue2, timing = "end" } = params;
|
|
515
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
516
|
+
assertPositive(periods, "periods");
|
|
517
|
+
assertFiniteNumber(payment, "payment");
|
|
518
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
519
|
+
assertTiming(timing, "timing");
|
|
520
|
+
const value = futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
521
|
+
if (!Number.isFinite(value)) {
|
|
522
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
523
|
+
}
|
|
524
|
+
return value;
|
|
525
|
+
}
|
|
526
|
+
function pv(params) {
|
|
527
|
+
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
528
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
529
|
+
assertPositive(periods, "periods");
|
|
530
|
+
assertFiniteNumber(payment, "payment");
|
|
531
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
532
|
+
assertTiming(timing, "timing");
|
|
533
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
534
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
535
|
+
}
|
|
536
|
+
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
537
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
538
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
539
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
540
|
+
}
|
|
541
|
+
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
542
|
+
const value = -futureValue2 / growth - paymentPv;
|
|
543
|
+
if (!Number.isFinite(value)) {
|
|
544
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
545
|
+
}
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
function pmt(params) {
|
|
549
|
+
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
550
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
551
|
+
assertPositive(periods, "periods");
|
|
552
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
553
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
554
|
+
assertTiming(timing, "timing");
|
|
555
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
556
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
557
|
+
}
|
|
558
|
+
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
559
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
560
|
+
if (!Number.isFinite(growth) || growth <= 0) {
|
|
561
|
+
throw new RangeError("Numerical instability for given ratePerPeriod/periods");
|
|
562
|
+
}
|
|
563
|
+
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
564
|
+
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
565
|
+
const value = numerator / denominator;
|
|
566
|
+
if (!Number.isFinite(value)) {
|
|
567
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
568
|
+
}
|
|
569
|
+
return value;
|
|
570
|
+
}
|
|
571
|
+
function nper(params) {
|
|
572
|
+
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
573
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
574
|
+
assertFiniteNumber(payment, "payment");
|
|
575
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
576
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
577
|
+
assertTiming(timing, "timing");
|
|
578
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
579
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
580
|
+
}
|
|
581
|
+
if (ratePerPeriod === 0) {
|
|
582
|
+
const linear = -(presentValue2 + futureValue2) / payment;
|
|
583
|
+
return Number.isFinite(linear) && linear >= 0 ? linear : Number.NaN;
|
|
584
|
+
}
|
|
585
|
+
const adjustedPayment = payment * annuityDueFactor(ratePerPeriod, timing);
|
|
586
|
+
const numerator = adjustedPayment - futureValue2 * ratePerPeriod;
|
|
587
|
+
const denominator = adjustedPayment + presentValue2 * ratePerPeriod;
|
|
588
|
+
if (numerator === 0 || denominator === 0) return Number.NaN;
|
|
589
|
+
const ratio = numerator / denominator;
|
|
590
|
+
if (ratio <= 0) return Number.NaN;
|
|
591
|
+
const periods = Math.log(ratio) / Math.log(1 + ratePerPeriod);
|
|
592
|
+
return Number.isFinite(periods) ? periods : Number.NaN;
|
|
593
|
+
}
|
|
594
|
+
function rate(params) {
|
|
595
|
+
const {
|
|
596
|
+
periods,
|
|
597
|
+
payment,
|
|
598
|
+
presentValue: presentValue2,
|
|
599
|
+
futureValue: futureValue2 = 0,
|
|
600
|
+
timing = "end",
|
|
601
|
+
guess = 0.1,
|
|
602
|
+
tolerance = 1e-10,
|
|
603
|
+
maxIterations = 100,
|
|
604
|
+
lowerBound = -0.999999999,
|
|
605
|
+
upperBound = 10
|
|
606
|
+
} = params;
|
|
607
|
+
assertPositive(periods, "periods");
|
|
608
|
+
assertFiniteNumber(payment, "payment");
|
|
609
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
610
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
611
|
+
assertFiniteNumber(guess, "guess");
|
|
612
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
613
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
614
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
615
|
+
assertTiming(timing, "timing");
|
|
616
|
+
if (ratePerPeriodIsInvalidForDomain(lowerBound)) {
|
|
617
|
+
throw new RangeError("lowerBound must be > -1");
|
|
618
|
+
}
|
|
619
|
+
if (upperBound <= lowerBound) {
|
|
620
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
621
|
+
}
|
|
622
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
623
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
624
|
+
}
|
|
625
|
+
const fn = (r) => futureValueFromCashFlows(r, periods, payment, presentValue2, timing) - futureValue2;
|
|
626
|
+
const derivative = (r) => {
|
|
627
|
+
const delta = Math.abs(r) > 1e-5 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
628
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
629
|
+
};
|
|
630
|
+
const newton = newtonRaphson({
|
|
631
|
+
initialGuess: guess,
|
|
632
|
+
fn,
|
|
633
|
+
derivative,
|
|
634
|
+
tolerance,
|
|
635
|
+
maxIterations,
|
|
636
|
+
min: lowerBound,
|
|
637
|
+
max: upperBound
|
|
638
|
+
});
|
|
639
|
+
if (newton !== void 0) return newton;
|
|
640
|
+
const bracket = findRateBracket(fn, lowerBound, upperBound);
|
|
641
|
+
if (bracket === void 0) {
|
|
642
|
+
throw new RangeError("RATE did not converge within search bounds");
|
|
643
|
+
}
|
|
644
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
645
|
+
const bisected = bisection({
|
|
646
|
+
fn,
|
|
647
|
+
lower: bracket.lower,
|
|
648
|
+
upper: bracket.upper,
|
|
649
|
+
tolerance,
|
|
650
|
+
maxIterations: maxIterations * 2
|
|
651
|
+
});
|
|
652
|
+
if (bisected !== void 0) return bisected;
|
|
653
|
+
throw new RangeError("RATE did not converge");
|
|
654
|
+
}
|
|
655
|
+
function ratePerPeriodIsInvalidForDomain(ratePerPeriod) {
|
|
656
|
+
return ratePerPeriod <= -1;
|
|
657
|
+
}
|
|
658
|
+
function findRateBracket(fn, lowerBound, upperBound) {
|
|
659
|
+
const scan = (start, end, segments = 200) => {
|
|
660
|
+
let prevX;
|
|
661
|
+
let prevValue;
|
|
662
|
+
for (let i = 0; i <= segments; i++) {
|
|
663
|
+
const x = start + (end - start) * i / segments;
|
|
664
|
+
const value = fn(x);
|
|
665
|
+
if (!Number.isFinite(value)) continue;
|
|
666
|
+
if (value === 0) return { lower: x, upper: x };
|
|
667
|
+
if (prevValue !== void 0 && prevX !== void 0 && prevValue * value < 0) {
|
|
668
|
+
return { lower: prevX, upper: x };
|
|
669
|
+
}
|
|
670
|
+
prevX = x;
|
|
671
|
+
prevValue = value;
|
|
672
|
+
}
|
|
673
|
+
return void 0;
|
|
674
|
+
};
|
|
675
|
+
let lower = lowerBound;
|
|
676
|
+
let upper = upperBound;
|
|
677
|
+
for (let i = 0; i < 20; i++) {
|
|
678
|
+
const bracket = scan(lower, upper);
|
|
679
|
+
if (bracket !== void 0) return bracket;
|
|
680
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
681
|
+
upper = upper * 2 + 1;
|
|
682
|
+
}
|
|
683
|
+
return scan(lower, upper, 400);
|
|
287
684
|
}
|
|
288
685
|
|
|
289
686
|
// src/loans/loanPayment.ts
|
|
290
687
|
function loanPayment(params) {
|
|
291
688
|
const { principal, annualRate, paymentsPerYear, years } = params;
|
|
292
689
|
assertNonNegative(principal, "principal");
|
|
690
|
+
assertFiniteNumber(annualRate, "annualRate");
|
|
293
691
|
assertPositive(paymentsPerYear, "paymentsPerYear");
|
|
294
692
|
assertNonNegative(years, "years");
|
|
295
693
|
const n = Math.round(paymentsPerYear * years);
|
|
296
694
|
if (n === 0) return 0;
|
|
297
695
|
const r = annualRate / paymentsPerYear;
|
|
696
|
+
if (r <= -1) {
|
|
697
|
+
throw new RangeError("annualRate / paymentsPerYear must be > -1");
|
|
698
|
+
}
|
|
298
699
|
if (r === 0) return principal / n;
|
|
299
|
-
|
|
700
|
+
const value = r * principal / (1 - (1 + r) ** -n);
|
|
701
|
+
if (!Number.isFinite(value)) {
|
|
702
|
+
throw new RangeError("Numerical instability for given inputs");
|
|
703
|
+
}
|
|
704
|
+
return value;
|
|
300
705
|
}
|
|
301
706
|
|
|
302
707
|
// src/loans/amortizationSchedule.ts
|
|
@@ -394,17 +799,20 @@ function roundToCurrency(params) {
|
|
|
394
799
|
const factor = 10 ** d;
|
|
395
800
|
if (mode === "half-even") {
|
|
396
801
|
const scaled = value * factor;
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
802
|
+
const sign = scaled < 0 ? -1 : 1;
|
|
803
|
+
const absScaled = Math.abs(scaled);
|
|
804
|
+
const floorAbs = Math.floor(absScaled);
|
|
805
|
+
const fraction = absScaled - floorAbs;
|
|
806
|
+
const epsilon = 1e-12;
|
|
807
|
+
if (Math.abs(fraction - 0.5) <= epsilon) {
|
|
808
|
+
const nearestEven = floorAbs % 2 === 0 ? floorAbs : floorAbs + 1;
|
|
809
|
+
return sign * nearestEven / factor;
|
|
402
810
|
}
|
|
403
|
-
return
|
|
811
|
+
return Math.round(scaled) / factor;
|
|
404
812
|
}
|
|
405
813
|
return Math.round(value * factor) / factor;
|
|
406
814
|
}
|
|
407
815
|
|
|
408
|
-
export { amortizationSchedule, cagr, compound, effectiveAnnualRate, futureValue, inflationAdjustedAmount, investmentGrowth, irr, loanPayment, paymentFromPresentValue, payoffPeriodWithExtra, periodsToReachGoal, presentValue, presentValueOfAnnuity, rateToReachGoal, realReturn, remainingBalance, roundToCurrency, ruleOf72 };
|
|
816
|
+
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
817
|
//# sourceMappingURL=index.js.map
|
|
410
818
|
//# sourceMappingURL=index.js.map
|