@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/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 (rate === 0 || years === 0) return principal;
37
+ if (rate2 === 0 || years === 0) return principal;
38
38
  const n = timesPerYear;
39
39
  const t = years;
40
- return principal * (1 + rate / n) ** (n * t);
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 (rate === 0 || years === 0) return presentValue2;
58
+ if (rate2 === 0 || years === 0) return presentValue2;
59
59
  const n = timesPerYear;
60
60
  const t = years;
61
- return presentValue2 * (1 + rate / n) ** (n * t);
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 = rate / timesPerYear;
97
- const fvInitial = rate === 0 ? initial : initial * (1 + r) ** periods;
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 (rate === 0) {
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 npv(rate, cashFlows) {
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 + rate) ** t;
167
+ if (cf !== void 0) sum += cf / (1 + rate2) ** t;
119
168
  }
120
169
  return sum;
121
170
  }
122
- function npvDerivative(rate, cashFlows) {
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 + rate) ** (t + 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 { cashFlows, guess = 0.1, maxIterations = 100 } = params;
132
- if (cashFlows.length === 0) return NaN;
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
- let r = guess;
135
- for (let i = 0; i < maxIterations; i++) {
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;
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 r;
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 pmt2 = presentValue2 / periods;
155
- return timing === "begin" ? pmt2 / (1 + ratePerPeriod) : pmt2;
295
+ const pmt3 = presentValue2 / periods;
296
+ return timing === "begin" ? pmt3 / (1 + ratePerPeriod) : pmt3;
156
297
  }
157
- let pmt = ratePerPeriod * presentValue2 / (1 - (1 + ratePerPeriod) ** -periods);
158
- if (timing === "begin") pmt /= 1 + ratePerPeriod;
159
- return pmt;
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(rate, "rate");
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 = rate / timesPerYear;
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 (rate === 0) return targetFutureValue <= principal ? 0 : Number.POSITIVE_INFINITY;
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 (rate === 0) {
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 fv = principal;
337
+ let fv2 = principal;
195
338
  let periods = 0;
196
- const maxPeriods = 1e4;
197
- while (fv < targetFutureValue && periods < maxPeriods) {
198
- if (contributionTiming === "begin") fv += contributionPerPeriod;
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
- return fv >= targetFutureValue ? periods : Number.POSITIVE_INFINITY;
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: fv, rate, timesPerYear, years } = params;
208
- assertNonNegative(fv, "futureValue");
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 (rate === 0 || years === 0) return fv;
354
+ if (rate2 === 0 || years === 0) return fv2;
212
355
  const n = timesPerYear;
213
356
  const t = years;
214
- return fv / (1 + rate / n) ** (n * t);
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 pv2 = paymentPerPeriod * periods;
225
- return timing === "begin" ? pv2 * (1 + ratePerPeriod) : pv2;
367
+ const pv3 = paymentPerPeriod * periods;
368
+ return timing === "begin" ? pv3 * (1 + ratePerPeriod) : pv3;
226
369
  }
227
- const pv = paymentPerPeriod * (1 - (1 + ratePerPeriod) ** -periods) / ratePerPeriod;
228
- return timing === "begin" ? pv * (1 + ratePerPeriod) : pv;
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
- let r = (targetFutureValue / (principal + contributionPerPeriod * periods)) ** (1 / periods) - 1;
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 annuity = contributionPerPeriod * ((onePlusRN - 1) / (r || 1e-14)) * (1 + dueFactor * r);
258
- const fv = fvLump + annuity;
259
- const err = fv - targetFutureValue;
260
- if (Math.abs(err) < 1e-10) return r;
261
- const dr = r * 1e-6 || 1e-10;
262
- const r2 = r + dr;
263
- const onePlusR2 = 1 + r2;
264
- const onePlusR2N = onePlusR2 ** periods;
265
- const fv2 = principal * onePlusR2N + contributionPerPeriod * ((onePlusR2N - 1) / r2) * (1 + dueFactor * r2);
266
- const dFvDr = (fv2 - fv) / dr;
267
- r = r - err / dFvDr;
268
- if (r <= -1) r = 0.01;
269
- if (r > 10) r = 10;
270
- }
271
- return r;
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(rate, "rate");
287
- assertPositive(rate, "rate");
288
- return constant / 100 / rate;
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
- return r * principal / (1 - (1 + r) ** -n);
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 rounded = Math.round(scaled);
400
- const remainder = Math.abs(scaled - rounded);
401
- if (remainder === 0.5) {
402
- const down = Math.floor(scaled);
403
- return (down % 2 === 0 ? down : down + 1) / factor;
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 rounded / factor;
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;