@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.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,180 @@ 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 fLower = fn(lowerBound);
|
|
191
|
+
const fUpper = fn(upperBound);
|
|
192
|
+
if (!Number.isFinite(fLower) || !Number.isFinite(fUpper)) return void 0;
|
|
193
|
+
if (fLower === 0) return { lower: lowerBound, upper: lowerBound };
|
|
194
|
+
if (fUpper === 0) return { lower: upperBound, upper: upperBound };
|
|
195
|
+
if (fLower * fUpper < 0) return { lower: lowerBound, upper: upperBound };
|
|
196
|
+
let lower = lowerBound;
|
|
197
|
+
let upper = upperBound;
|
|
198
|
+
let fLo = fLower;
|
|
199
|
+
let fHi = fUpper;
|
|
200
|
+
for (let i = 0; i < 20; i++) {
|
|
201
|
+
const nextLower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
202
|
+
const fNextLower = fn(nextLower);
|
|
203
|
+
if (Number.isFinite(fNextLower) && fNextLower * fHi < 0) {
|
|
204
|
+
return { lower: nextLower, upper };
|
|
205
|
+
}
|
|
206
|
+
if (Number.isFinite(fNextLower)) {
|
|
207
|
+
lower = nextLower;
|
|
208
|
+
fLo = fNextLower;
|
|
209
|
+
}
|
|
210
|
+
upper = upper * 2 + 1;
|
|
211
|
+
fHi = fn(upper);
|
|
212
|
+
if (!Number.isFinite(fHi)) continue;
|
|
213
|
+
if (fLo * fHi < 0) return { lower, upper };
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
130
217
|
function irr(params) {
|
|
131
|
-
const {
|
|
132
|
-
|
|
218
|
+
const {
|
|
219
|
+
cashFlows,
|
|
220
|
+
guess = 0.1,
|
|
221
|
+
maxIterations = 100,
|
|
222
|
+
lowerBound = -0.99,
|
|
223
|
+
upperBound = 10
|
|
224
|
+
} = params;
|
|
225
|
+
if (cashFlows.length === 0) {
|
|
226
|
+
throw new RangeError("cashFlows must contain at least one value");
|
|
227
|
+
}
|
|
228
|
+
assertFiniteNumber(guess, "guess");
|
|
229
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
230
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
231
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
232
|
+
if (maxIterations <= 0 || !Number.isInteger(maxIterations)) {
|
|
233
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
234
|
+
}
|
|
235
|
+
if (lowerBound <= -1) {
|
|
236
|
+
throw new RangeError("lowerBound must be > -1");
|
|
237
|
+
}
|
|
238
|
+
if (upperBound <= lowerBound) {
|
|
239
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
240
|
+
}
|
|
133
241
|
for (const cf of cashFlows) assertFiniteNumber(cf, "cashFlows[]");
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const val = npv(r, cashFlows);
|
|
137
|
-
if (Math.abs(val) < 1e-10) return r;
|
|
138
|
-
const der = npvDerivative(r, cashFlows);
|
|
139
|
-
if (!Number.isFinite(der) || Math.abs(der) < 1e-15) break;
|
|
140
|
-
r = r - val / der;
|
|
141
|
-
if (r < -0.99) r = -0.99;
|
|
142
|
-
if (r > 10) r = 10;
|
|
242
|
+
if (!hasPositiveAndNegative(cashFlows)) {
|
|
243
|
+
throw new RangeError("cashFlows must include at least one positive and one negative value");
|
|
143
244
|
}
|
|
144
|
-
|
|
245
|
+
const fn = (rate2) => npvAt(rate2, cashFlows);
|
|
246
|
+
const derivative = (rate2) => npvDerivativeAt(rate2, cashFlows);
|
|
247
|
+
const newton = newtonRaphson({
|
|
248
|
+
initialGuess: guess,
|
|
249
|
+
fn,
|
|
250
|
+
derivative,
|
|
251
|
+
tolerance: 1e-10,
|
|
252
|
+
maxIterations,
|
|
253
|
+
min: lowerBound,
|
|
254
|
+
max: upperBound
|
|
255
|
+
});
|
|
256
|
+
if (newton !== void 0) return newton;
|
|
257
|
+
const bracket = findBracket(fn, lowerBound, upperBound);
|
|
258
|
+
if (bracket === void 0) {
|
|
259
|
+
throw new RangeError("IRR did not converge within search bounds");
|
|
260
|
+
}
|
|
261
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
262
|
+
const bisected = bisection({
|
|
263
|
+
fn,
|
|
264
|
+
lower: bracket.lower,
|
|
265
|
+
upper: bracket.upper,
|
|
266
|
+
tolerance: 1e-10,
|
|
267
|
+
maxIterations: maxIterations * 2
|
|
268
|
+
});
|
|
269
|
+
if (bisected !== void 0) return bisected;
|
|
270
|
+
throw new RangeError("IRR did not converge");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/interest/npv.ts
|
|
274
|
+
function npv(params) {
|
|
275
|
+
const { rate: rate2, cashFlows } = params;
|
|
276
|
+
assertFiniteNumber(rate2, "rate");
|
|
277
|
+
if (rate2 <= -1) throw new RangeError("rate must be > -1");
|
|
278
|
+
if (cashFlows.length === 0) return 0;
|
|
279
|
+
let sum = 0;
|
|
280
|
+
for (let t = 0; t < cashFlows.length; t++) {
|
|
281
|
+
const cf = cashFlows[t];
|
|
282
|
+
if (cf === void 0) continue;
|
|
283
|
+
assertFiniteNumber(cf, `cashFlows[${t}]`);
|
|
284
|
+
sum += cf / (1 + rate2) ** t;
|
|
285
|
+
}
|
|
286
|
+
return sum;
|
|
145
287
|
}
|
|
146
288
|
|
|
147
289
|
// src/interest/paymentFromPresentValue.ts
|
|
@@ -151,12 +293,12 @@ function paymentFromPresentValue(params) {
|
|
|
151
293
|
assertNonNegative(periods, "periods");
|
|
152
294
|
if (presentValue2 === 0 || periods === 0) return 0;
|
|
153
295
|
if (ratePerPeriod === 0) {
|
|
154
|
-
const
|
|
155
|
-
return timing === "begin" ?
|
|
296
|
+
const pmt3 = presentValue2 / periods;
|
|
297
|
+
return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
|
|
156
298
|
}
|
|
157
|
-
let
|
|
158
|
-
if (timing === "begin")
|
|
159
|
-
return
|
|
299
|
+
let pmt2 = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
|
|
300
|
+
if (timing === "begin") pmt2 /= 1 + ratePerPeriod;
|
|
301
|
+
return pmt2;
|
|
160
302
|
}
|
|
161
303
|
|
|
162
304
|
// src/interest/periodsToReachGoal.ts
|
|
@@ -164,54 +306,54 @@ function periodsToReachGoal(params) {
|
|
|
164
306
|
const {
|
|
165
307
|
principal,
|
|
166
308
|
targetFutureValue,
|
|
167
|
-
rate,
|
|
309
|
+
rate: rate2,
|
|
168
310
|
timesPerYear,
|
|
169
311
|
contributionPerPeriod = 0,
|
|
170
312
|
contributionTiming = "end"
|
|
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");
|
|
177
319
|
if (targetFutureValue <= principal) return 0;
|
|
178
|
-
const r =
|
|
320
|
+
const r = rate2 / timesPerYear;
|
|
179
321
|
if (r <= -1) {
|
|
180
322
|
throw new RangeError("rate / timesPerYear must be > -1");
|
|
181
323
|
}
|
|
182
324
|
if (contributionPerPeriod === 0) {
|
|
183
325
|
if (principal === 0) return Number.POSITIVE_INFINITY;
|
|
184
|
-
if (
|
|
326
|
+
if (rate2 === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
|
|
185
327
|
if (r < 0) return Number.POSITIVE_INFINITY;
|
|
186
328
|
const n = Math.log(targetFutureValue / principal) / Math.log(1 + r);
|
|
187
329
|
return Number.isFinite(n) && n >= 0 ? Math.ceil(n) : Number.POSITIVE_INFINITY;
|
|
188
330
|
}
|
|
189
|
-
if (
|
|
331
|
+
if (rate2 === 0) {
|
|
190
332
|
const needed = targetFutureValue - principal;
|
|
191
333
|
if (needed <= 0) return 0;
|
|
192
334
|
return Math.ceil(needed / contributionPerPeriod);
|
|
193
335
|
}
|
|
194
|
-
let
|
|
336
|
+
let fv2 = principal;
|
|
195
337
|
let periods = 0;
|
|
196
338
|
const maxPeriods = 1e4;
|
|
197
|
-
while (
|
|
198
|
-
if (contributionTiming === "begin")
|
|
199
|
-
|
|
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
|
-
return
|
|
344
|
+
return fv2 >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
|
|
203
345
|
}
|
|
204
346
|
|
|
205
347
|
// src/interest/presentValue.ts
|
|
206
348
|
function presentValue(params) {
|
|
207
|
-
const { futureValue:
|
|
208
|
-
assertNonNegative(
|
|
349
|
+
const { futureValue: fv2, rate: rate2, timesPerYear, years } = params;
|
|
350
|
+
assertNonNegative(fv2, "futureValue");
|
|
209
351
|
assertPositive(timesPerYear, "timesPerYear");
|
|
210
352
|
assertNonNegative(years, "years");
|
|
211
|
-
if (
|
|
353
|
+
if (rate2 === 0 || years === 0) return fv2;
|
|
212
354
|
const n = timesPerYear;
|
|
213
355
|
const t = years;
|
|
214
|
-
return
|
|
356
|
+
return fv2 / (1 + rate2 / n) ** (n * t);
|
|
215
357
|
}
|
|
216
358
|
|
|
217
359
|
// src/interest/presentValueOfAnnuity.ts
|
|
@@ -221,11 +363,11 @@ function presentValueOfAnnuity(params) {
|
|
|
221
363
|
assertNonNegative(periods, "periods");
|
|
222
364
|
if (periods === 0) return 0;
|
|
223
365
|
if (ratePerPeriod === 0) {
|
|
224
|
-
const
|
|
225
|
-
return timing === "begin" ?
|
|
366
|
+
const pv3 = paymentPerPeriod * periods;
|
|
367
|
+
return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
|
|
226
368
|
}
|
|
227
|
-
const
|
|
228
|
-
return timing === "begin" ?
|
|
369
|
+
const pv2 = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
|
|
370
|
+
return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
|
|
229
371
|
}
|
|
230
372
|
|
|
231
373
|
// src/interest/rateToReachGoal.ts
|
|
@@ -248,27 +390,37 @@ function rateToReachGoal(params) {
|
|
|
248
390
|
return (targetFutureValue / principal) ** (1 / periods) - 1;
|
|
249
391
|
}
|
|
250
392
|
const dueFactor = contributionTiming === "begin" ? 1 : 0;
|
|
251
|
-
|
|
252
|
-
if (r <= -1) r = 0.01;
|
|
253
|
-
for (let i = 0; i < 100; i++) {
|
|
393
|
+
const fn = (r) => {
|
|
254
394
|
const onePlusR = 1 + r;
|
|
255
395
|
const onePlusRN = onePlusR ** periods;
|
|
256
396
|
const fvLump = principal * onePlusRN;
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
397
|
+
const annuityBase = r === 0 ? periods : (onePlusRN - 1) / r;
|
|
398
|
+
const fvAnnuity = contributionPerPeriod * annuityBase * (1 + dueFactor * r);
|
|
399
|
+
return fvLump + fvAnnuity - targetFutureValue;
|
|
400
|
+
};
|
|
401
|
+
const derivative = (r) => {
|
|
402
|
+
const delta = Math.abs(r) > 1e-6 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
403
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
404
|
+
};
|
|
405
|
+
const initialGuess = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
|
|
406
|
+
const newton = newtonRaphson({
|
|
407
|
+
initialGuess: Number.isFinite(initialGuess) ? Math.max(initialGuess, -0.99) : 0.01,
|
|
408
|
+
fn,
|
|
409
|
+
derivative,
|
|
410
|
+
tolerance: 1e-10,
|
|
411
|
+
maxIterations: 100,
|
|
412
|
+
min: -0.99,
|
|
413
|
+
max: 10
|
|
414
|
+
});
|
|
415
|
+
if (newton !== void 0) return newton;
|
|
416
|
+
const bisected = bisection({
|
|
417
|
+
fn,
|
|
418
|
+
lower: -0.99,
|
|
419
|
+
upper: 10,
|
|
420
|
+
tolerance: 1e-10,
|
|
421
|
+
maxIterations: 200
|
|
422
|
+
});
|
|
423
|
+
return bisected ?? Number.NaN;
|
|
272
424
|
}
|
|
273
425
|
|
|
274
426
|
// src/interest/realReturn.ts
|
|
@@ -282,10 +434,174 @@ function realReturn(params) {
|
|
|
282
434
|
|
|
283
435
|
// src/interest/ruleOf72.ts
|
|
284
436
|
function ruleOf72(params) {
|
|
285
|
-
const { rate, constant = 72 } = params;
|
|
286
|
-
assertFiniteNumber(
|
|
287
|
-
assertPositive(
|
|
288
|
-
return constant / 100 /
|
|
437
|
+
const { rate: rate2, constant = 72 } = params;
|
|
438
|
+
assertFiniteNumber(rate2, "rate");
|
|
439
|
+
assertPositive(rate2, "rate");
|
|
440
|
+
return constant / 100 / rate2;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/interest/tvom.ts
|
|
444
|
+
function assertTiming(value, name) {
|
|
445
|
+
if (value !== "end" && value !== "begin") {
|
|
446
|
+
throw new RangeError(`${name} must be "end" or "begin"`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function annuityDueFactor(ratePerPeriod, timing) {
|
|
450
|
+
return timing === "begin" ? 1 + ratePerPeriod : 1;
|
|
451
|
+
}
|
|
452
|
+
function futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing) {
|
|
453
|
+
if (ratePerPeriod === 0) return -(presentValue2 + payment * periods);
|
|
454
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
455
|
+
const paymentFv = payment * ((growth - 1) / ratePerPeriod) * annuityDueFactor(ratePerPeriod, timing);
|
|
456
|
+
return -(presentValue2 * growth + paymentFv);
|
|
457
|
+
}
|
|
458
|
+
function fv(params) {
|
|
459
|
+
const { ratePerPeriod, periods, payment = 0, presentValue: presentValue2, timing = "end" } = params;
|
|
460
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
461
|
+
assertPositive(periods, "periods");
|
|
462
|
+
assertFiniteNumber(payment, "payment");
|
|
463
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
464
|
+
assertTiming(timing, "timing");
|
|
465
|
+
return futureValueFromCashFlows(ratePerPeriod, periods, payment, presentValue2, timing);
|
|
466
|
+
}
|
|
467
|
+
function pv(params) {
|
|
468
|
+
const { ratePerPeriod, periods, payment = 0, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
469
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
470
|
+
assertPositive(periods, "periods");
|
|
471
|
+
assertFiniteNumber(payment, "payment");
|
|
472
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
473
|
+
assertTiming(timing, "timing");
|
|
474
|
+
if (ratePerPeriod === 0) return -(futureValue2 + payment * periods);
|
|
475
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
476
|
+
const paymentPv = payment * (1 - 1 / growth) / ratePerPeriod * annuityDueFactor(ratePerPeriod, timing);
|
|
477
|
+
return -futureValue2 / growth - paymentPv;
|
|
478
|
+
}
|
|
479
|
+
function pmt(params) {
|
|
480
|
+
const { ratePerPeriod, periods, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
481
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
482
|
+
assertPositive(periods, "periods");
|
|
483
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
484
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
485
|
+
assertTiming(timing, "timing");
|
|
486
|
+
if (ratePerPeriod === 0) return -(presentValue2 + futureValue2) / periods;
|
|
487
|
+
const growth = (1 + ratePerPeriod) ** periods;
|
|
488
|
+
const numerator = -(futureValue2 + presentValue2 * growth) * ratePerPeriod;
|
|
489
|
+
const denominator = (growth - 1) * annuityDueFactor(ratePerPeriod, timing);
|
|
490
|
+
return numerator / denominator;
|
|
491
|
+
}
|
|
492
|
+
function nper(params) {
|
|
493
|
+
const { ratePerPeriod, payment, presentValue: presentValue2, futureValue: futureValue2 = 0, timing = "end" } = params;
|
|
494
|
+
assertFiniteNumber(ratePerPeriod, "ratePerPeriod");
|
|
495
|
+
assertFiniteNumber(payment, "payment");
|
|
496
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
497
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
498
|
+
assertTiming(timing, "timing");
|
|
499
|
+
if (ratePerPeriodIsInvalidForDomain(ratePerPeriod)) {
|
|
500
|
+
throw new RangeError("ratePerPeriod must be > -1");
|
|
501
|
+
}
|
|
502
|
+
if (ratePerPeriod === 0) {
|
|
503
|
+
const linear = -(presentValue2 + futureValue2) / payment;
|
|
504
|
+
return Number.isFinite(linear) && linear >= 0 ? linear : Number.NaN;
|
|
505
|
+
}
|
|
506
|
+
const adjustedPayment = payment * annuityDueFactor(ratePerPeriod, timing);
|
|
507
|
+
const numerator = adjustedPayment - futureValue2 * ratePerPeriod;
|
|
508
|
+
const denominator = adjustedPayment + presentValue2 * ratePerPeriod;
|
|
509
|
+
if (numerator === 0 || denominator === 0) return Number.NaN;
|
|
510
|
+
const ratio = numerator / denominator;
|
|
511
|
+
if (ratio <= 0) return Number.NaN;
|
|
512
|
+
const periods = Math.log(ratio) / Math.log(1 + ratePerPeriod);
|
|
513
|
+
return Number.isFinite(periods) ? periods : Number.NaN;
|
|
514
|
+
}
|
|
515
|
+
function rate(params) {
|
|
516
|
+
const {
|
|
517
|
+
periods,
|
|
518
|
+
payment,
|
|
519
|
+
presentValue: presentValue2,
|
|
520
|
+
futureValue: futureValue2 = 0,
|
|
521
|
+
timing = "end",
|
|
522
|
+
guess = 0.1,
|
|
523
|
+
tolerance = 1e-10,
|
|
524
|
+
maxIterations = 100,
|
|
525
|
+
lowerBound = -0.999999999,
|
|
526
|
+
upperBound = 10
|
|
527
|
+
} = params;
|
|
528
|
+
assertPositive(periods, "periods");
|
|
529
|
+
assertFiniteNumber(payment, "payment");
|
|
530
|
+
assertFiniteNumber(presentValue2, "presentValue");
|
|
531
|
+
assertFiniteNumber(futureValue2, "futureValue");
|
|
532
|
+
assertFiniteNumber(guess, "guess");
|
|
533
|
+
assertFiniteNumber(maxIterations, "maxIterations");
|
|
534
|
+
assertFiniteNumber(lowerBound, "lowerBound");
|
|
535
|
+
assertFiniteNumber(upperBound, "upperBound");
|
|
536
|
+
assertTiming(timing, "timing");
|
|
537
|
+
if (ratePerPeriodIsInvalidForDomain(lowerBound)) {
|
|
538
|
+
throw new RangeError("lowerBound must be > -1");
|
|
539
|
+
}
|
|
540
|
+
if (upperBound <= lowerBound) {
|
|
541
|
+
throw new RangeError("upperBound must be greater than lowerBound");
|
|
542
|
+
}
|
|
543
|
+
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
544
|
+
throw new RangeError("maxIterations must be a positive integer");
|
|
545
|
+
}
|
|
546
|
+
const fn = (r) => futureValueFromCashFlows(r, periods, payment, presentValue2, timing) - futureValue2;
|
|
547
|
+
const derivative = (r) => {
|
|
548
|
+
const delta = Math.abs(r) > 1e-5 ? Math.abs(r) * 1e-6 : 1e-6;
|
|
549
|
+
return (fn(r + delta) - fn(r - delta)) / (2 * delta);
|
|
550
|
+
};
|
|
551
|
+
const newton = newtonRaphson({
|
|
552
|
+
initialGuess: guess,
|
|
553
|
+
fn,
|
|
554
|
+
derivative,
|
|
555
|
+
tolerance,
|
|
556
|
+
maxIterations,
|
|
557
|
+
min: lowerBound,
|
|
558
|
+
max: upperBound
|
|
559
|
+
});
|
|
560
|
+
if (newton !== void 0) return newton;
|
|
561
|
+
const bracket = findRateBracket(fn, lowerBound, upperBound);
|
|
562
|
+
if (bracket === void 0) {
|
|
563
|
+
throw new RangeError("RATE did not converge within search bounds");
|
|
564
|
+
}
|
|
565
|
+
if (bracket.lower === bracket.upper) return bracket.lower;
|
|
566
|
+
const bisected = bisection({
|
|
567
|
+
fn,
|
|
568
|
+
lower: bracket.lower,
|
|
569
|
+
upper: bracket.upper,
|
|
570
|
+
tolerance,
|
|
571
|
+
maxIterations: maxIterations * 2
|
|
572
|
+
});
|
|
573
|
+
if (bisected !== void 0) return bisected;
|
|
574
|
+
throw new RangeError("RATE did not converge");
|
|
575
|
+
}
|
|
576
|
+
function ratePerPeriodIsInvalidForDomain(ratePerPeriod) {
|
|
577
|
+
return ratePerPeriod <= -1;
|
|
578
|
+
}
|
|
579
|
+
function findRateBracket(fn, lowerBound, upperBound) {
|
|
580
|
+
const scan = (start, end, segments = 200) => {
|
|
581
|
+
let prevX;
|
|
582
|
+
let prevValue;
|
|
583
|
+
for (let i = 0; i <= segments; i++) {
|
|
584
|
+
const x = start + (end - start) * i / segments;
|
|
585
|
+
const value = fn(x);
|
|
586
|
+
if (!Number.isFinite(value)) continue;
|
|
587
|
+
if (value === 0) return { lower: x, upper: x };
|
|
588
|
+
if (prevValue !== void 0 && prevX !== void 0 && prevValue * value < 0) {
|
|
589
|
+
return { lower: prevX, upper: x };
|
|
590
|
+
}
|
|
591
|
+
prevX = x;
|
|
592
|
+
prevValue = value;
|
|
593
|
+
}
|
|
594
|
+
return void 0;
|
|
595
|
+
};
|
|
596
|
+
let lower = lowerBound;
|
|
597
|
+
let upper = upperBound;
|
|
598
|
+
for (let i = 0; i < 20; i++) {
|
|
599
|
+
const bracket = scan(lower, upper);
|
|
600
|
+
if (bracket !== void 0) return bracket;
|
|
601
|
+
lower = Math.max(-0.999999999, (lower - 1) / 2);
|
|
602
|
+
upper = upper * 2 + 1;
|
|
603
|
+
}
|
|
604
|
+
return scan(lower, upper, 400);
|
|
289
605
|
}
|
|
290
606
|
|
|
291
607
|
// src/loans/loanPayment.ts
|
|
@@ -408,19 +724,27 @@ function roundToCurrency(params) {
|
|
|
408
724
|
}
|
|
409
725
|
|
|
410
726
|
exports.amortizationSchedule = amortizationSchedule;
|
|
727
|
+
exports.bisection = bisection;
|
|
411
728
|
exports.cagr = cagr;
|
|
412
729
|
exports.compound = compound;
|
|
413
730
|
exports.effectiveAnnualRate = effectiveAnnualRate;
|
|
414
731
|
exports.futureValue = futureValue;
|
|
732
|
+
exports.fv = fv;
|
|
415
733
|
exports.inflationAdjustedAmount = inflationAdjustedAmount;
|
|
416
734
|
exports.investmentGrowth = investmentGrowth;
|
|
417
735
|
exports.irr = irr;
|
|
418
736
|
exports.loanPayment = loanPayment;
|
|
737
|
+
exports.newtonRaphson = newtonRaphson;
|
|
738
|
+
exports.nper = nper;
|
|
739
|
+
exports.npv = npv;
|
|
419
740
|
exports.paymentFromPresentValue = paymentFromPresentValue;
|
|
420
741
|
exports.payoffPeriodWithExtra = payoffPeriodWithExtra;
|
|
421
742
|
exports.periodsToReachGoal = periodsToReachGoal;
|
|
743
|
+
exports.pmt = pmt;
|
|
422
744
|
exports.presentValue = presentValue;
|
|
423
745
|
exports.presentValueOfAnnuity = presentValueOfAnnuity;
|
|
746
|
+
exports.pv = pv;
|
|
747
|
+
exports.rate = rate;
|
|
424
748
|
exports.rateToReachGoal = rateToReachGoal;
|
|
425
749
|
exports.realReturn = realReturn;
|
|
426
750
|
exports.remainingBalance = remainingBalance;
|