@travishorn/financejs 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.17.1",
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/index.js CHANGED
@@ -25,3 +25,4 @@ export { syd } from "./syd.js";
25
25
  export { vdb } from "./vdb.js";
26
26
  export { xirr } from "./xirr.js";
27
27
  export { xnpv } from "./xnpv.js";
28
+ export { yield_ } from "./yield_.js";
package/src/yield_.js ADDED
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Calculates the yield on a security that pays periodic interest. Use to
3
+ * calculate bond yield.
4
+ *
5
+ * Remarks:
6
+ * - The settlement date is the date a buyer purchases a coupon, such as a bond.
7
+ * The maturity date is the date when a coupon expires. For example, suppose a
8
+ * 30-year bond is issued on January 1, 2008, and is purchased by a buyer six
9
+ * months later. The issue date would be January 1, 2008, the settlement date
10
+ * would be July 1, 2008, and the maturity date would be January 1, 2038,
11
+ * which is 30 years after the January 1, 2008, issue date.
12
+ * - `settlement`, `maturity`, `frequency`, and `basis` are truncated to
13
+ * integers.
14
+ * - If `settlement` or `maturity` is not a valid date, an error is thrown.
15
+ * - If `rate` < `0`, an error is thrown.
16
+ * - If `pr` ≤ `0` or if `redemption` ≤ `0`, an error is thrown.
17
+ * - If `frequency` is any number other than `1`, `2`, or `4`, an error is
18
+ * thrown.
19
+ * - If `basis` < `0` or if `basis` > `4` and error is thrown.
20
+ * - If `settlement` ≥ `maturity`, an error is thrown.
21
+ * - If there is one coupon period or less until redemption, the yield is
22
+ * calculated as follows: `yield = ((redemption/100 + rate/frequency) -
23
+ * (par/100 + (A/E * rate/frequency))) / (par/100 + (A/E * rate/frequency)) *
24
+ * (frequency * E) / DSR`, where:
25
+ * - `A` = number of days from the beginning of the coupon period to the
26
+ * settlement date (accrued days).
27
+ * - `DSR` = number of days from the settlement date to the redemption date.
28
+ * - `E` = number of days in the coupon period.
29
+ * - If there is more than one coupon period until redemption, the yield is
30
+ * calculated through a hundred iterations. The resolution uses the Newton
31
+ * method. The yield is changed until the estimated price given the yield is
32
+ * close to price.
33
+ *
34
+ * @param {Date} settlement - The security's settlement date. The security
35
+ * settlement date is the date after the issue date when the security is traded
36
+ * to the buyer.
37
+ * @param {Date} maturity - The security's maturity date. The maturity date is
38
+ * the date when the security expires.
39
+ * @param {number} rate - The security's annual coupon rate.
40
+ * @param {number} pr - The security's price per $100 face value.
41
+ * @param {number} redemption - The security's redemption value per $100 face
42
+ * value.
43
+ * @param {1|2|4} frequency - The number of coupon payments per year. For
44
+ * annual payments, frequency = `1`; for semiannual, frequency = `2`; for
45
+ * quarterly, frequency = `4`.
46
+ * @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
47
+ * omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
48
+ * actual/365, `4` = European 30/360.
49
+ * @returns {number} The yield
50
+ *
51
+ * @example
52
+ * yield_(new Date("2008-02-15"), new Date("2016-11-15"), 0.0575, 95.04287, 100, 2, 0); // 0.06500001
53
+ */
54
+ export function yield_(
55
+ settlement,
56
+ maturity,
57
+ rate,
58
+ pr,
59
+ redemption,
60
+ frequency,
61
+ basis = 0,
62
+ ) {
63
+ const settlementDate = toUtcDate(settlement);
64
+ const maturityDate = toUtcDate(maturity);
65
+
66
+ frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
67
+ const basisNumber = Math.trunc(basis ?? 0);
68
+
69
+ if (rate < 0) {
70
+ throw new RangeError("Invalid rate.");
71
+ }
72
+
73
+ if (pr <= 0 || redemption <= 0) {
74
+ throw new RangeError("Price and redemption must be greater than zero.");
75
+ }
76
+
77
+ if (![1, 2, 4].includes(frequency)) {
78
+ throw new RangeError("Invalid frequency.");
79
+ }
80
+
81
+ if (basisNumber < 0 || basisNumber > 4) {
82
+ throw new RangeError("Invalid basis.");
83
+ }
84
+
85
+ /** @type {0|1|2|3|4} */
86
+ const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
87
+
88
+ if (settlementDate >= maturityDate) {
89
+ throw new RangeError("Settlement must be before maturity.");
90
+ }
91
+
92
+ const monthsPerCoupon = 12 / frequency;
93
+ const { previousCouponDate, nextCouponDate } = getCouponBounds(
94
+ settlementDate,
95
+ maturityDate,
96
+ monthsPerCoupon,
97
+ );
98
+
99
+ const a = coupdaybs(previousCouponDate, settlementDate, normalizedBasis);
100
+ let dsc = coupdaysnc(settlementDate, nextCouponDate, normalizedBasis);
101
+ const e = coupdays(
102
+ previousCouponDate,
103
+ nextCouponDate,
104
+ frequency,
105
+ normalizedBasis,
106
+ );
107
+
108
+ if (normalizedBasis === 3) {
109
+ dsc = e - a;
110
+ }
111
+ const n = couponsRemaining(nextCouponDate, maturityDate, monthsPerCoupon);
112
+ const coupon = (100 * rate) / frequency;
113
+
114
+ if (n <= 1) {
115
+ const numerator =
116
+ redemption / 100 +
117
+ rate / frequency -
118
+ (pr / 100 + (a / e) * (rate / frequency));
119
+ const denominator = pr / 100 + (a / e) * (rate / frequency);
120
+ const result = (numerator / denominator) * ((frequency * e) / dsc);
121
+ return normalizeNegativeZero(result);
122
+ }
123
+
124
+ /** @param {number} candidateYield */
125
+ const priceFromYield = (candidateYield) => {
126
+ const base = 1 + candidateYield / frequency;
127
+ const firstExponent = dsc / e;
128
+
129
+ let presentValue = 0;
130
+ for (let k = 1; k <= n; k += 1) {
131
+ presentValue += coupon / Math.pow(base, k - 1 + firstExponent);
132
+ }
133
+
134
+ presentValue += redemption / Math.pow(base, n - 1 + firstExponent);
135
+ presentValue -= (coupon * a) / e;
136
+
137
+ return presentValue;
138
+ };
139
+
140
+ const epsilon = 1e-11;
141
+ const maxIterations = 100;
142
+ const minYield = -frequency + 1e-10;
143
+ const step = 0.01;
144
+
145
+ let y0 = Math.max(rate, minYield + step);
146
+ let f0 = priceFromYield(y0) - pr;
147
+ let y1 = y0 + (f0 > 0 ? step : -step);
148
+ y1 = Math.max(y1, minYield);
149
+ let f1 = priceFromYield(y1) - pr;
150
+
151
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
152
+ if (Math.abs(f1) < epsilon) {
153
+ return normalizeNegativeZero(y1);
154
+ }
155
+
156
+ if (f1 === f0) {
157
+ y0 = Math.max(y0 + step, minYield);
158
+ f0 = priceFromYield(y0) - pr;
159
+ if (f1 === f0) {
160
+ throw new RangeError(
161
+ "Cannot calculate YIELD with the provided values.",
162
+ );
163
+ }
164
+ }
165
+
166
+ let y2 = y1 - ((y1 - y0) * f1) / (f1 - f0);
167
+ y2 = Math.max(y2, minYield);
168
+ const f2 = priceFromYield(y2) - pr;
169
+
170
+ if (Math.abs(f2) < epsilon) {
171
+ return normalizeNegativeZero(y2);
172
+ }
173
+
174
+ y0 = y1;
175
+ f0 = f1;
176
+ y1 = y2;
177
+ f1 = f2;
178
+ }
179
+
180
+ throw new RangeError("Maximum iterations exceeded while calculating YIELD.");
181
+ }
182
+
183
+ /** @param {Date} date */
184
+ function lastDayOfMonthUtc(date) {
185
+ return new Date(
186
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0),
187
+ ).getUTCDate();
188
+ }
189
+
190
+ /**
191
+ * Month arithmetic that preserves end-of-month behavior for coupon schedules.
192
+ *
193
+ * @param {Date} date
194
+ * @param {number} months
195
+ */
196
+ function addMonthsUtc(date, months) {
197
+ const year = date.getUTCFullYear();
198
+ const month = date.getUTCMonth();
199
+ const day = date.getUTCDate();
200
+ const isEndOfMonth = day === lastDayOfMonthUtc(date);
201
+
202
+ const monthIndex = month + months;
203
+ const newYear = year + Math.floor(monthIndex / 12);
204
+ const newMonth = ((monthIndex % 12) + 12) % 12;
205
+ const monthEndDay = new Date(Date.UTC(newYear, newMonth + 1, 0)).getUTCDate();
206
+ const newDay = isEndOfMonth ? monthEndDay : Math.min(day, monthEndDay);
207
+
208
+ return new Date(Date.UTC(newYear, newMonth, newDay));
209
+ }
210
+
211
+ /**
212
+ * @param {Date} settlementDate
213
+ * @param {Date} maturityDate
214
+ * @param {number} monthsPerCoupon
215
+ */
216
+ function getCouponBounds(settlementDate, maturityDate, monthsPerCoupon) {
217
+ let nextCouponDate = new Date(maturityDate.getTime());
218
+ let previousCouponDate = addMonthsUtc(nextCouponDate, -monthsPerCoupon);
219
+
220
+ while (settlementDate < previousCouponDate) {
221
+ nextCouponDate = previousCouponDate;
222
+ previousCouponDate = addMonthsUtc(nextCouponDate, -monthsPerCoupon);
223
+ }
224
+
225
+ // Excel treats settlement on a coupon date as the start of the next period.
226
+ if (settlementDate >= nextCouponDate) {
227
+ previousCouponDate = nextCouponDate;
228
+ nextCouponDate = addMonthsUtc(nextCouponDate, monthsPerCoupon);
229
+ }
230
+
231
+ return { previousCouponDate, nextCouponDate };
232
+ }
233
+
234
+ /**
235
+ * @param {Date} nextCouponDate
236
+ * @param {Date} maturityDate
237
+ * @param {number} monthsPerCoupon
238
+ */
239
+ function couponsRemaining(nextCouponDate, maturityDate, monthsPerCoupon) {
240
+ let n = 1;
241
+ let current = new Date(nextCouponDate.getTime());
242
+
243
+ while (current < maturityDate) {
244
+ current = addMonthsUtc(current, monthsPerCoupon);
245
+ n += 1;
246
+ }
247
+
248
+ return n;
249
+ }
250
+
251
+ /**
252
+ * @param {Date} settlementDate
253
+ * @param {Date} nextCouponDate
254
+ * @param {0|1|2|3|4} basis
255
+ */
256
+ function coupdaysnc(settlementDate, nextCouponDate, basis) {
257
+ if (basis === 0) {
258
+ return days360Us(settlementDate, nextCouponDate);
259
+ }
260
+
261
+ if (basis === 4) {
262
+ return days360Eu(settlementDate, nextCouponDate);
263
+ }
264
+
265
+ return actualDays(settlementDate, nextCouponDate);
266
+ }
267
+
268
+ /**
269
+ * @param {Date} previousCouponDate
270
+ * @param {Date} settlementDate
271
+ * @param {0|1|2|3|4} basis
272
+ */
273
+ function coupdaybs(previousCouponDate, settlementDate, basis) {
274
+ if (basis === 0) {
275
+ return days360Us(previousCouponDate, settlementDate);
276
+ }
277
+
278
+ if (basis === 4) {
279
+ return days360Eu(previousCouponDate, settlementDate);
280
+ }
281
+
282
+ return actualDays(previousCouponDate, settlementDate);
283
+ }
284
+
285
+ /**
286
+ * @param {Date} previousCouponDate
287
+ * @param {Date} nextCouponDate
288
+ * @param {number} frequency
289
+ * @param {0|1|2|3|4} basis
290
+ */
291
+ function coupdays(previousCouponDate, nextCouponDate, frequency, basis) {
292
+ if (basis === 0 || basis === 4) {
293
+ return 360 / frequency;
294
+ }
295
+
296
+ if (basis === 3) {
297
+ return 365 / frequency;
298
+ }
299
+
300
+ return actualDays(previousCouponDate, nextCouponDate);
301
+ }
302
+
303
+ /**
304
+ * Excel/NASD 30/360 day count.
305
+ *
306
+ * @param {Date} start
307
+ * @param {Date} end
308
+ */
309
+ function days360Us(start, end) {
310
+ let d1 = start.getUTCDate();
311
+ let d2 = end.getUTCDate();
312
+ const m1 = start.getUTCMonth() + 1;
313
+ const m2 = end.getUTCMonth() + 1;
314
+ const y1 = start.getUTCFullYear();
315
+ const y2 = end.getUTCFullYear();
316
+
317
+ const startIsMonthEnd = d1 === lastDayOfMonthUtc(start);
318
+ const endIsMonthEnd = d2 === lastDayOfMonthUtc(end);
319
+
320
+ if (m1 === 2 && startIsMonthEnd) {
321
+ d1 = 30;
322
+ }
323
+ if (m2 === 2 && endIsMonthEnd && m1 === 2 && startIsMonthEnd) {
324
+ d2 = 30;
325
+ }
326
+
327
+ if (d2 === 31 && d1 >= 30) {
328
+ d2 = 30;
329
+ }
330
+ if (d1 === 31) {
331
+ d1 = 30;
332
+ }
333
+
334
+ return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
335
+ }
336
+
337
+ /**
338
+ * European 30/360 day count.
339
+ *
340
+ * @param {Date} start
341
+ * @param {Date} end
342
+ */
343
+ function days360Eu(start, end) {
344
+ let d1 = start.getUTCDate();
345
+ let d2 = end.getUTCDate();
346
+
347
+ if (d1 === 31) {
348
+ d1 = 30;
349
+ }
350
+ if (d2 === 31) {
351
+ d2 = 30;
352
+ }
353
+
354
+ const m1 = start.getUTCMonth() + 1;
355
+ const m2 = end.getUTCMonth() + 1;
356
+ const y1 = start.getUTCFullYear();
357
+ const y2 = end.getUTCFullYear();
358
+
359
+ return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
360
+ }
361
+
362
+ /**
363
+ * @param {Date} start
364
+ * @param {Date} end
365
+ */
366
+ function actualDays(start, end) {
367
+ const msPerDay = 24 * 60 * 60 * 1000;
368
+ return (end.getTime() - start.getTime()) / msPerDay;
369
+ }
370
+
371
+ /** @param {Date} value */
372
+ function toUtcDate(value) {
373
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
374
+ throw new RangeError("Settlement and maturity must be valid Date objects.");
375
+ }
376
+
377
+ return new Date(
378
+ Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()),
379
+ );
380
+ }
381
+
382
+ /** @param {number} value */
383
+ function normalizeNegativeZero(value) {
384
+ return Object.is(value, -0) ? 0 : value;
385
+ }