@travishorn/financejs 1.16.0 → 1.18.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 CHANGED
@@ -111,7 +111,7 @@ Tiers 1-3 are complete.
111
111
  - **Tier 2:** ✓ipmt, ✓ppmt, ✓cumipmt, ✓cumprinc, ✓sln, ✓db, ✓ddb, ✓effect,
112
112
  ✓nominal, ✓syd, ✓mirr
113
113
  - **Tier 3:** ✓rri, ✓pduration, ✓vdb, ✓fvschedule, ✓dollarde, ✓dollarfr, ✓ispmt
114
- - **Tier 4:** yield, price, duration, mduration, disc, intrate, received,
114
+ - **Tier 4:** yield, price, duration, mduration, disc, intrate, received,
115
115
  pricedisc, pricemat, yielddisc, yieldmat
116
116
  - **Tier 5:** all others
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travishorn/financejs",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Modern JavaScript time value of money and cash-flow financial formulas with Excel-style behavior.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/fv.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeZero } from "./normalizeZero.js";
1
+ import { normalizeZero } from "./util.js";
2
2
 
3
3
  /**
4
4
  * Calculates the future value of an investment based on a constant interest
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ export { nominal } from "./nominal.js";
15
15
  export { nper } from "./nper.js";
16
16
  export { npv } from "./npv.js";
17
17
  export { pduration } from "./pduration.js";
18
+ export { price } from "./price.js";
18
19
  export { pmt } from "./pmt.js";
19
20
  export { ppmt } from "./ppmt.js";
20
21
  export { pv } from "./pv.js";
@@ -25,3 +26,4 @@ export { syd } from "./syd.js";
25
26
  export { vdb } from "./vdb.js";
26
27
  export { xirr } from "./xirr.js";
27
28
  export { xnpv } from "./xnpv.js";
29
+ export { yield_ } from "./yield_.js";
package/src/nper.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeZero } from "./normalizeZero.js";
1
+ import { normalizeZero } from "./util.js";
2
2
 
3
3
  /**
4
4
  * Calculates the number of periods for an investment based on periodic,
package/src/pmt.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeZero } from "./normalizeZero.js";
1
+ import { normalizeZero } from "./util.js";
2
2
 
3
3
  /**
4
4
  * Calculates the payment for a loan based on constant payments and a constant
package/src/ppmt.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ipmt } from "./ipmt.js";
2
- import { normalizeZero } from "./normalizeZero.js";
3
2
  import { pmt } from "./pmt.js";
3
+ import { normalizeZero } from "./util.js";
4
4
 
5
5
  /**
6
6
  * Calculates the payment on the principal for a given period for an investment
package/src/price.js ADDED
@@ -0,0 +1,144 @@
1
+ import {
2
+ coupdaybs,
3
+ coupdays,
4
+ coupdaysnc,
5
+ couponsRemaining,
6
+ getCouponBounds,
7
+ normalizeZero,
8
+ toUtcDate,
9
+ } from "./util.js";
10
+
11
+ /**
12
+ * Calculates the price per $100 face value of a security that pays periodic
13
+ * interest.
14
+ *
15
+ * Remarks:
16
+ * - The settlement date is the date a buyer purchases a coupon, such as a bond.
17
+ * The maturity date is the date when a coupon expires. For example, suppose a
18
+ * 30-year bond is issued on January 1, 2008, and is purchased by a buyer six
19
+ * months later. The issue date would be January 1, 2008, the settlement date
20
+ * would be July 1, 2008, and the maturity date would be January 1, 2038,
21
+ * which is 30 years after the January 1, 2008, issue date.
22
+ * - `settlement`, `maturity`, `frequency`, and `basis` are truncated to
23
+ * integers.
24
+ * - If `settlement` or `maturity` is not a valid date, an error is thrown.
25
+ * - If `yld` < `0` or if `rate` < `0`, an error is thrown.
26
+ * - If `redemption` <= `0`, an error is thrown.
27
+ * - If `frequency` is any number other than `1`, `2`, or `4`, an error is
28
+ * thrown.
29
+ * - If `basis` < `0` or if `basis` > `4` and error is thrown.
30
+ * - If `settlement` >= `maturity`, an error is thrown.
31
+ * - When N > 1 (N is the number of coupons payable between the settlement date
32
+ * and redemption date), the calculation is: `PRICE =[redemption / (1 +
33
+ * yld/frequency)^(N-1 + DSC/E)] +[SUM(k=1 to N) (100 * rate/frequency) / (1 +
34
+ * yld/frequency)^(k-1 + DSC/E)] - (100 * rate/frequency * A/E)`, where:
35
+ * - DSC = number of days from settlement to next coupon date.
36
+ * - E = number of days in coupon period in which the settlement date falls.
37
+ * - A = number of days from beginning of coupon period to settlement date.
38
+ * - When N = 1, the calculation is: `DSR = E - A; T1 = 100 * (rate / frequency)
39
+ * + redemption; T2 = (yld / frequency) * (DSR / E) + 1; T3 = 100 * (rate /
40
+ * frequency) * (A / E); Price = (T1 / T2) - T3`.
41
+ *
42
+ * @param {Date} settlement - The security's settlement date. The security
43
+ * settlement date is the date after the issue date when the security is traded
44
+ * to the buyer.
45
+ * @param {Date} maturity - The security's maturity date. The maturity date is
46
+ * the date when the security expires.
47
+ * @param {number} rate - The security's annual coupon rate.
48
+ * @param {number} yld - The security's annual yield.
49
+ * @param {number} redemption - The security's redemption value per $100 face
50
+ * value.
51
+ * @param {1|2|4} frequency - The number of coupon payments per year. For annual
52
+ * payments, frequency = `1`; for semiannual, frequency = `2`; for quarterly,
53
+ * frequency = `4`.
54
+ * @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
55
+ * omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
56
+ * actual/365, `4` = European 30/360.
57
+ * @returns {number} the price per $100 face value
58
+ *
59
+ * @example
60
+ * price(new Date("2008-02-15"), new Date("2017-11-15"), 0.0575, 0.065, 100, 2, 0); // 94.63436162
61
+ */
62
+ export function price(
63
+ settlement,
64
+ maturity,
65
+ rate,
66
+ yld,
67
+ redemption,
68
+ frequency,
69
+ basis = 0,
70
+ ) {
71
+ const settlementDate = toUtcDate(settlement);
72
+ const maturityDate = toUtcDate(maturity);
73
+
74
+ frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
75
+ const basisNumber = Math.trunc(basis ?? 0);
76
+
77
+ if (rate < 0 || yld < 0) {
78
+ throw new RangeError(
79
+ "Rate and yield must be greater than or equal to zero.",
80
+ );
81
+ }
82
+
83
+ if (redemption <= 0) {
84
+ throw new RangeError("Redemption must be greater than zero.");
85
+ }
86
+
87
+ if (![1, 2, 4].includes(frequency)) {
88
+ throw new RangeError("Invalid frequency.");
89
+ }
90
+
91
+ if (basisNumber < 0 || basisNumber > 4) {
92
+ throw new RangeError("Invalid basis.");
93
+ }
94
+
95
+ /** @type {0|1|2|3|4} */
96
+ const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
97
+
98
+ if (settlementDate >= maturityDate) {
99
+ throw new RangeError("Settlement must be before maturity.");
100
+ }
101
+
102
+ const monthsPerCoupon = 12 / frequency;
103
+ const { previousCouponDate, nextCouponDate } = getCouponBounds(
104
+ settlementDate,
105
+ maturityDate,
106
+ monthsPerCoupon,
107
+ );
108
+
109
+ const a = coupdaybs(previousCouponDate, settlementDate, normalizedBasis);
110
+ let dsc = coupdaysnc(settlementDate, nextCouponDate, normalizedBasis);
111
+ const e = coupdays(
112
+ previousCouponDate,
113
+ nextCouponDate,
114
+ frequency,
115
+ normalizedBasis,
116
+ );
117
+
118
+ if (normalizedBasis === 3) {
119
+ dsc = e - a;
120
+ }
121
+
122
+ const n = couponsRemaining(nextCouponDate, maturityDate, monthsPerCoupon);
123
+ const coupon = (100 * rate) / frequency;
124
+
125
+ if (n <= 1) {
126
+ const t1 = coupon + redemption;
127
+ const t2 = (yld / frequency) * (dsc / e) + 1;
128
+ const t3 = coupon * (a / e);
129
+ return normalizeZero(t1 / t2 - t3);
130
+ }
131
+
132
+ const base = 1 + yld / frequency;
133
+ const firstExponent = dsc / e;
134
+
135
+ let presentValue = 0;
136
+ for (let k = 1; k <= n; k += 1) {
137
+ presentValue += coupon / Math.pow(base, k - 1 + firstExponent);
138
+ }
139
+
140
+ presentValue += redemption / Math.pow(base, n - 1 + firstExponent);
141
+ presentValue -= (coupon * a) / e;
142
+
143
+ return normalizeZero(presentValue);
144
+ }
package/src/pv.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeZero } from "./normalizeZero.js";
1
+ import { normalizeZero } from "./util.js";
2
2
 
3
3
  /**
4
4
  * Calculates the present value of a loan or an investment, based on a constant
package/src/rate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeZero } from "./normalizeZero.js";
1
+ import { normalizeZero } from "./util.js";
2
2
 
3
3
  /**
4
4
  * Evaluates the annuity equation for a candidate interest rate.
package/src/util.js ADDED
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Gets the last calendar day number for the month that contains `date`.
3
+ *
4
+ * This helper is UTC-based, so results are not affected by local timezone or
5
+ * daylight-saving transitions.
6
+ *
7
+ * @param {Date} date - The date whose month should be inspected.
8
+ * @returns {number} The month-end day of month, from 28 to 31.
9
+ *
10
+ * @example
11
+ * lastDayOfMonthUtc(new Date("2024-02-10")); // 29
12
+ * lastDayOfMonthUtc(new Date("2023-02-10")); // 28
13
+ */
14
+ export function lastDayOfMonthUtc(date) {
15
+ return new Date(
16
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0),
17
+ ).getUTCDate();
18
+ }
19
+
20
+ /**
21
+ * Adds or subtracts whole months from a UTC date while preserving end-of-month
22
+ * behavior.
23
+ *
24
+ * If the input date is the last day of its month, the result is also forced to
25
+ * the last day of the destination month. Otherwise, the day is clamped to the
26
+ * destination month when needed (for example, Jan 30 + 1 month => Feb 28/29).
27
+ *
28
+ * @param {Date} date - The starting date.
29
+ * @param {number} months - Number of months to add. Use negative values to
30
+ * subtract months.
31
+ * @returns {Date} A new UTC date shifted by `months`.
32
+ *
33
+ * @example
34
+ * addMonthsUtc(new Date("2021-01-31"), 1); // 2021-02-28T00:00:00.000Z
35
+ * addMonthsUtc(new Date("2024-01-31"), 1); // 2024-02-29T00:00:00.000Z
36
+ */
37
+ export function addMonthsUtc(date, months) {
38
+ const year = date.getUTCFullYear();
39
+ const month = date.getUTCMonth();
40
+ const day = date.getUTCDate();
41
+ const isEndOfMonth = day === lastDayOfMonthUtc(date);
42
+
43
+ const monthIndex = month + months;
44
+ const newYear = year + Math.floor(monthIndex / 12);
45
+ const newMonth = ((monthIndex % 12) + 12) % 12;
46
+ const monthEndDay = new Date(Date.UTC(newYear, newMonth + 1, 0)).getUTCDate();
47
+ const newDay = isEndOfMonth ? monthEndDay : Math.min(day, monthEndDay);
48
+
49
+ return new Date(Date.UTC(newYear, newMonth, newDay));
50
+ }
51
+
52
+ /**
53
+ * Computes day count using Excel/NASD 30/360 convention.
54
+ *
55
+ * This convention applies special normalization around month-end boundaries,
56
+ * especially for February and day 31 handling, to produce a synthetic
57
+ * 360-day-year day count.
58
+ *
59
+ * @param {Date} start - Start date (inclusive boundary for convention logic).
60
+ * @param {Date} end - End date.
61
+ * @returns {number} Day count between `start` and `end` under US 30/360 rules.
62
+ *
63
+ * @example
64
+ * days360Us(new Date("2024-01-30"), new Date("2024-03-31")); // 60
65
+ */
66
+ export function days360Us(start, end) {
67
+ let d1 = start.getUTCDate();
68
+ let d2 = end.getUTCDate();
69
+ const m1 = start.getUTCMonth() + 1;
70
+ const m2 = end.getUTCMonth() + 1;
71
+ const y1 = start.getUTCFullYear();
72
+ const y2 = end.getUTCFullYear();
73
+
74
+ const startIsMonthEnd = d1 === lastDayOfMonthUtc(start);
75
+ const endIsMonthEnd = d2 === lastDayOfMonthUtc(end);
76
+
77
+ if (m1 === 2 && startIsMonthEnd) {
78
+ d1 = 30;
79
+ }
80
+ if (m2 === 2 && endIsMonthEnd && m1 === 2 && startIsMonthEnd) {
81
+ d2 = 30;
82
+ }
83
+
84
+ if (d2 === 31 && d1 >= 30) {
85
+ d2 = 30;
86
+ }
87
+ if (d1 === 31) {
88
+ d1 = 30;
89
+ }
90
+
91
+ return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
92
+ }
93
+
94
+ /**
95
+ * Computes day count using European 30/360 convention.
96
+ *
97
+ * Unlike NASD 30/360, this method simply converts day 31 to day 30 for both
98
+ * dates, then calculates using a 360-day year basis.
99
+ *
100
+ * @param {Date} start - Start date.
101
+ * @param {Date} end - End date.
102
+ * @returns {number} Day count between `start` and `end` under EU 30/360 rules.
103
+ *
104
+ * @example
105
+ * days360Eu(new Date("2024-01-31"), new Date("2024-03-31")); // 60
106
+ */
107
+ export function days360Eu(start, end) {
108
+ let d1 = start.getUTCDate();
109
+ let d2 = end.getUTCDate();
110
+
111
+ if (d1 === 31) {
112
+ d1 = 30;
113
+ }
114
+ if (d2 === 31) {
115
+ d2 = 30;
116
+ }
117
+
118
+ const m1 = start.getUTCMonth() + 1;
119
+ const m2 = end.getUTCMonth() + 1;
120
+ const y1 = start.getUTCFullYear();
121
+ const y2 = end.getUTCFullYear();
122
+
123
+ return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
124
+ }
125
+
126
+ /**
127
+ * Computes the actual elapsed days between two dates using UTC timestamps.
128
+ *
129
+ * @param {Date} start - Start date.
130
+ * @param {Date} end - End date.
131
+ * @returns {number} Exact day difference as `(end - start)` in days.
132
+ *
133
+ * @example
134
+ * actualDays(new Date("2024-01-01"), new Date("2024-01-11")); // 10
135
+ */
136
+ export function actualDays(start, end) {
137
+ const msPerDay = 24 * 60 * 60 * 1000;
138
+ return (end.getTime() - start.getTime()) / msPerDay;
139
+ }
140
+
141
+ /**
142
+ * Finds coupon period boundaries around a settlement date.
143
+ *
144
+ * Starting from maturity and stepping backward by coupon interval, this helper
145
+ * returns the coupon date immediately before settlement and the next coupon
146
+ * date after settlement. If settlement lands exactly on a coupon date, it is
147
+ * treated as the start of the next period (Excel-compatible behavior).
148
+ *
149
+ * @param {Date} settlementDate - Security settlement date.
150
+ * @param {Date} maturityDate - Security maturity date.
151
+ * @param {number} monthsPerCoupon - Coupon interval in months (e.g., 12, 6, or
152
+ * 3).
153
+ * @returns {{ previousCouponDate: Date, nextCouponDate: Date }} Coupon bounds
154
+ * containing settlement.
155
+ *
156
+ * @example
157
+ * getCouponBounds(new Date("2021-01-15"), new Date("2022-01-31"), 6);
158
+ */
159
+ export function getCouponBounds(settlementDate, maturityDate, monthsPerCoupon) {
160
+ let nextCouponDate = new Date(maturityDate.getTime());
161
+ let previousCouponDate = addMonthsUtc(nextCouponDate, -monthsPerCoupon);
162
+
163
+ while (settlementDate < previousCouponDate) {
164
+ nextCouponDate = previousCouponDate;
165
+ previousCouponDate = addMonthsUtc(nextCouponDate, -monthsPerCoupon);
166
+ }
167
+
168
+ // Excel treats settlement on a coupon date as the start of the next period.
169
+ if (settlementDate >= nextCouponDate) {
170
+ previousCouponDate = nextCouponDate;
171
+ nextCouponDate = addMonthsUtc(nextCouponDate, monthsPerCoupon);
172
+ }
173
+
174
+ return { previousCouponDate, nextCouponDate };
175
+ }
176
+
177
+ /**
178
+ * Counts remaining coupon payments from `nextCouponDate` through maturity.
179
+ *
180
+ * The count includes the coupon on `nextCouponDate` itself, then steps by the
181
+ * coupon interval until the maturity date is reached.
182
+ *
183
+ * @param {Date} nextCouponDate - Next coupon date after settlement.
184
+ * @param {Date} maturityDate - Security maturity date.
185
+ * @param {number} monthsPerCoupon - Coupon interval in months.
186
+ * @returns {number} Number of remaining coupon payments.
187
+ *
188
+ * @example
189
+ * couponsRemaining(new Date("2025-01-01"), new Date("2026-01-01"), 6); // 3
190
+ */
191
+ export function couponsRemaining(
192
+ nextCouponDate,
193
+ maturityDate,
194
+ monthsPerCoupon,
195
+ ) {
196
+ let n = 1;
197
+ let current = new Date(nextCouponDate.getTime());
198
+
199
+ while (current < maturityDate) {
200
+ current = addMonthsUtc(current, monthsPerCoupon);
201
+ n += 1;
202
+ }
203
+
204
+ return n;
205
+ }
206
+
207
+ /**
208
+ * Computes days from settlement to next coupon date (`DSC`) by day-count basis.
209
+ *
210
+ * Basis mapping:
211
+ * - `0`: US (NASD) 30/360
212
+ * - `1`: Actual/actual
213
+ * - `2`: Actual/360
214
+ * - `3`: Actual/365
215
+ * - `4`: European 30/360
216
+ *
217
+ * @param {Date} settlementDate - Settlement date.
218
+ * @param {Date} nextCouponDate - Next coupon date.
219
+ * @param {0|1|2|3|4} basis - Day-count basis code.
220
+ * @returns {number} Days between settlement and next coupon under `basis`.
221
+ *
222
+ * @example
223
+ * coupdaysnc(new Date("2024-01-15"), new Date("2024-04-15"), 0);
224
+ */
225
+ export function coupdaysnc(settlementDate, nextCouponDate, basis) {
226
+ if (basis === 0) {
227
+ return days360Us(settlementDate, nextCouponDate);
228
+ }
229
+
230
+ if (basis === 4) {
231
+ return days360Eu(settlementDate, nextCouponDate);
232
+ }
233
+
234
+ return actualDays(settlementDate, nextCouponDate);
235
+ }
236
+
237
+ /**
238
+ * Computes days from previous coupon date to settlement (`A`) by day-count
239
+ * basis.
240
+ *
241
+ * Basis mapping:
242
+ * - `0`: US (NASD) 30/360
243
+ * - `1`: Actual/actual
244
+ * - `2`: Actual/360
245
+ * - `3`: Actual/365
246
+ * - `4`: European 30/360
247
+ *
248
+ * @param {Date} previousCouponDate - Coupon date immediately before settlement.
249
+ * @param {Date} settlementDate - Settlement date.
250
+ * @param {0|1|2|3|4} basis - Day-count basis code.
251
+ * @returns {number} Days between previous coupon and settlement under `basis`.
252
+ *
253
+ * @example
254
+ * coupdaybs(new Date("2023-10-15"), new Date("2024-01-15"), 1);
255
+ */
256
+ export function coupdaybs(previousCouponDate, settlementDate, basis) {
257
+ if (basis === 0) {
258
+ return days360Us(previousCouponDate, settlementDate);
259
+ }
260
+
261
+ if (basis === 4) {
262
+ return days360Eu(previousCouponDate, settlementDate);
263
+ }
264
+
265
+ return actualDays(previousCouponDate, settlementDate);
266
+ }
267
+
268
+ /**
269
+ * Computes total days in the coupon period (`E`) by basis and payment
270
+ * frequency.
271
+ *
272
+ * For 30/360 bases (`0` and `4`), period length is fixed at `360/frequency`.
273
+ * For basis `3` (Actual/365), period length is fixed at `365/frequency`.
274
+ * Otherwise, calendar days between coupon boundaries are used.
275
+ *
276
+ * @param {Date} previousCouponDate - Coupon date before settlement.
277
+ * @param {Date} nextCouponDate - Coupon date after settlement.
278
+ * @param {number} frequency - Coupon payments per year.
279
+ * @param {0|1|2|3|4} basis - Day-count basis code.
280
+ * @returns {number} Coupon period length in days.
281
+ *
282
+ * @example
283
+ * coupdays(new Date("2024-01-01"), new Date("2024-07-01"), 2, 3); // 182.5
284
+ */
285
+ export function coupdays(previousCouponDate, nextCouponDate, frequency, basis) {
286
+ if (basis === 0 || basis === 4) {
287
+ return 360 / frequency;
288
+ }
289
+
290
+ if (basis === 3) {
291
+ return 365 / frequency;
292
+ }
293
+
294
+ return actualDays(previousCouponDate, nextCouponDate);
295
+ }
296
+
297
+ /**
298
+ * Converts a `Date` to a UTC-midnight date-only representation.
299
+ *
300
+ * This helper is used to remove time-of-day components so financial date
301
+ * calculations operate on whole UTC dates.
302
+ *
303
+ * @param {Date} value - Input date value.
304
+ * @returns {Date} A new date at `00:00:00.000Z` for the same UTC
305
+ * year/month/day.
306
+ * @throws {RangeError} If `value` is not a valid `Date` instance.
307
+ *
308
+ * @example
309
+ * toUtcDate(new Date("2024-04-03T15:45:12.250Z")); // 2024-04-03T00:00:00.000Z
310
+ */
311
+ export function toUtcDate(value) {
312
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
313
+ throw new RangeError("Settlement and maturity must be valid Date objects.");
314
+ }
315
+
316
+ return new Date(
317
+ Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()),
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Normalizes zero values to ensure consistent handling of positive and negative
323
+ * zero in calculations.
324
+ *
325
+ * In JavaScript, +0 and -0 are distinct values that can yield different results
326
+ * in equality checks (e.g., Object.is(+0, -0) is false), sign checks
327
+ * (Math.sign), and division (1/0 vs 1/-0). This function coerces any zero input
328
+ * (either +0 or -0) to positive zero (+0), providing a consistent
329
+ * representation for downstream calculations, comparisons, and serialization.
330
+ *
331
+ * @param {number} value - The numeric value to normalize. If the value is +0 or
332
+ * -0, returns +0; otherwise, returns the original value.
333
+ * @returns {number} The normalized value, with all zeroes represented as +0.
334
+ *
335
+ * @example
336
+ * normalizeZero(-0); // 0
337
+ * normalizeZero(+0); // 0
338
+ * normalizeZero(5); // 5
339
+ */
340
+ export function normalizeZero(value) {
341
+ return value === 0 ? 0 : value;
342
+ }
package/src/yield_.js ADDED
@@ -0,0 +1,186 @@
1
+ import {
2
+ coupdaybs,
3
+ coupdays,
4
+ coupdaysnc,
5
+ couponsRemaining,
6
+ getCouponBounds,
7
+ normalizeZero,
8
+ toUtcDate,
9
+ } from "./util.js";
10
+
11
+ /**
12
+ * Calculates the yield on a security that pays periodic interest. Use to
13
+ * calculate bond yield.
14
+ *
15
+ * Remarks:
16
+ * - The settlement date is the date a buyer purchases a coupon, such as a bond.
17
+ * The maturity date is the date when a coupon expires. For example, suppose a
18
+ * 30-year bond is issued on January 1, 2008, and is purchased by a buyer six
19
+ * months later. The issue date would be January 1, 2008, the settlement date
20
+ * would be July 1, 2008, and the maturity date would be January 1, 2038,
21
+ * which is 30 years after the January 1, 2008, issue date.
22
+ * - `settlement`, `maturity`, `frequency`, and `basis` are truncated to
23
+ * integers.
24
+ * - If `settlement` or `maturity` is not a valid date, an error is thrown.
25
+ * - If `rate` < `0`, an error is thrown.
26
+ * - If `pr` ≤ `0` or if `redemption` ≤ `0`, an error is thrown.
27
+ * - If `frequency` is any number other than `1`, `2`, or `4`, an error is
28
+ * thrown.
29
+ * - If `basis` < `0` or if `basis` > `4` and error is thrown.
30
+ * - If `settlement` ≥ `maturity`, an error is thrown.
31
+ * - If there is one coupon period or less until redemption, the yield is
32
+ * calculated as follows: `yield = ((redemption/100 + rate/frequency) -
33
+ * (par/100 + (A/E * rate/frequency))) / (par/100 + (A/E * rate/frequency)) *
34
+ * (frequency * E) / DSR`, where:
35
+ * - `A` = number of days from the beginning of the coupon period to the
36
+ * settlement date (accrued days).
37
+ * - `DSR` = number of days from the settlement date to the redemption date.
38
+ * - `E` = number of days in the coupon period.
39
+ * - If there is more than one coupon period until redemption, the yield is
40
+ * calculated through a hundred iterations. The resolution uses the Newton
41
+ * method. The yield is changed until the estimated price given the yield is
42
+ * close to price.
43
+ *
44
+ * @param {Date} settlement - The security's settlement date. The security
45
+ * settlement date is the date after the issue date when the security is traded
46
+ * to the buyer.
47
+ * @param {Date} maturity - The security's maturity date. The maturity date is
48
+ * the date when the security expires.
49
+ * @param {number} rate - The security's annual coupon rate.
50
+ * @param {number} pr - The security's price per $100 face value.
51
+ * @param {number} redemption - The security's redemption value per $100 face
52
+ * value.
53
+ * @param {1|2|4} frequency - The number of coupon payments per year. For
54
+ * annual payments, frequency = `1`; for semiannual, frequency = `2`; for
55
+ * quarterly, frequency = `4`.
56
+ * @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
57
+ * omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
58
+ * actual/365, `4` = European 30/360.
59
+ * @returns {number} The yield
60
+ *
61
+ * @example
62
+ * yield_(new Date("2008-02-15"), new Date("2016-11-15"), 0.0575, 95.04287, 100, 2, 0); // 0.06500001
63
+ */
64
+ export function yield_(
65
+ settlement,
66
+ maturity,
67
+ rate,
68
+ pr,
69
+ redemption,
70
+ frequency,
71
+ basis = 0,
72
+ ) {
73
+ const settlementDate = toUtcDate(settlement);
74
+ const maturityDate = toUtcDate(maturity);
75
+
76
+ frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
77
+ const basisNumber = Math.trunc(basis ?? 0);
78
+
79
+ if (rate < 0) {
80
+ throw new RangeError("Invalid rate.");
81
+ }
82
+
83
+ if (pr <= 0 || redemption <= 0) {
84
+ throw new RangeError("Price and redemption must be greater than zero.");
85
+ }
86
+
87
+ if (![1, 2, 4].includes(frequency)) {
88
+ throw new RangeError("Invalid frequency.");
89
+ }
90
+
91
+ if (basisNumber < 0 || basisNumber > 4) {
92
+ throw new RangeError("Invalid basis.");
93
+ }
94
+
95
+ /** @type {0|1|2|3|4} */
96
+ const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
97
+
98
+ if (settlementDate >= maturityDate) {
99
+ throw new RangeError("Settlement must be before maturity.");
100
+ }
101
+
102
+ const monthsPerCoupon = 12 / frequency;
103
+ const { previousCouponDate, nextCouponDate } = getCouponBounds(
104
+ settlementDate,
105
+ maturityDate,
106
+ monthsPerCoupon,
107
+ );
108
+
109
+ const a = coupdaybs(previousCouponDate, settlementDate, normalizedBasis);
110
+ let dsc = coupdaysnc(settlementDate, nextCouponDate, normalizedBasis);
111
+ const e = coupdays(
112
+ previousCouponDate,
113
+ nextCouponDate,
114
+ frequency,
115
+ normalizedBasis,
116
+ );
117
+
118
+ if (normalizedBasis === 3) {
119
+ dsc = e - a;
120
+ }
121
+ const n = couponsRemaining(nextCouponDate, maturityDate, monthsPerCoupon);
122
+ const coupon = (100 * rate) / frequency;
123
+
124
+ if (n <= 1) {
125
+ const numerator =
126
+ redemption / 100 +
127
+ rate / frequency -
128
+ (pr / 100 + (a / e) * (rate / frequency));
129
+ const denominator = pr / 100 + (a / e) * (rate / frequency);
130
+ const result = (numerator / denominator) * ((frequency * e) / dsc);
131
+ return normalizeZero(result);
132
+ }
133
+
134
+ /** @param {number} candidateYield */
135
+ const priceFromYield = (candidateYield) => {
136
+ const base = 1 + candidateYield / frequency;
137
+ const firstExponent = dsc / e;
138
+
139
+ let presentValue = 0;
140
+ for (let k = 1; k <= n; k += 1) {
141
+ presentValue += coupon / Math.pow(base, k - 1 + firstExponent);
142
+ }
143
+
144
+ presentValue += redemption / Math.pow(base, n - 1 + firstExponent);
145
+ presentValue -= (coupon * a) / e;
146
+
147
+ return presentValue;
148
+ };
149
+
150
+ const epsilon = 1e-11;
151
+ const maxIterations = 100;
152
+ const minYield = -frequency + 1e-10;
153
+ const step = 0.01;
154
+
155
+ let y0 = Math.max(rate, minYield + step);
156
+ let f0 = priceFromYield(y0) - pr;
157
+ let y1 = y0 + (f0 > 0 ? step : -step);
158
+ y1 = Math.max(y1, minYield);
159
+ let f1 = priceFromYield(y1) - pr;
160
+
161
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
162
+ if (Math.abs(f1) < epsilon) {
163
+ return normalizeZero(y1);
164
+ }
165
+
166
+ if (f1 === f0) {
167
+ y0 = Math.max(y0 + step, minYield);
168
+ f0 = priceFromYield(y0) - pr;
169
+ }
170
+
171
+ let y2 = y1 - ((y1 - y0) * f1) / (f1 - f0);
172
+ y2 = Math.max(y2, minYield);
173
+ const f2 = priceFromYield(y2) - pr;
174
+
175
+ if (Math.abs(f2) < epsilon) {
176
+ return normalizeZero(y2);
177
+ }
178
+
179
+ y0 = y1;
180
+ f0 = f1;
181
+ y1 = y2;
182
+ f1 = f2;
183
+ }
184
+
185
+ throw new RangeError("Maximum iterations exceeded while calculating YIELD.");
186
+ }
@@ -1,22 +0,0 @@
1
- /**
2
- * Normalizes zero values to ensure consistent handling of positive and negative
3
- * zero in calculations.
4
- *
5
- * In JavaScript, +0 and -0 are distinct values that can yield different results
6
- * in equality checks (e.g., Object.is(+0, -0) is false), sign checks
7
- * (Math.sign), and division (1/0 vs 1/-0). This function coerces any zero input
8
- * (either +0 or -0) to positive zero (+0), providing a consistent
9
- * representation for downstream calculations, comparisons, and serialization.
10
- *
11
- * @param {number} value - The numeric value to normalize. If the value is +0 or
12
- * -0, returns +0; otherwise, returns the original value.
13
- * @returns {number} The normalized value, with all zeroes represented as +0.
14
- *
15
- * @example
16
- * normalizeZero(-0); // 0
17
- * normalizeZero(+0); // 0
18
- * normalizeZero(5); // 5
19
- */
20
- export function normalizeZero(value) {
21
- return value === 0 ? 0 : value;
22
- }