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