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