@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 +1 -1
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/yield_.js +385 -0
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
package/src/index.js
CHANGED
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
|
+
}
|