@travishorn/financejs 1.17.1 → 1.19.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 +2 -2
- package/package.json +1 -1
- package/src/coupdaybs.js +64 -0
- package/src/coupdays.js +69 -0
- package/src/coupdaysnc.js +63 -0
- package/src/fv.js +1 -1
- package/src/index.js +4 -0
- package/src/nper.js +1 -1
- package/src/pmt.js +1 -1
- package/src/ppmt.js +1 -1
- package/src/price.js +149 -0
- package/src/pv.js +1 -1
- package/src/rate.js +1 -1
- package/src/util.js +342 -0
- package/src/yield_.js +19 -213
- package/src/normalizeZero.js +0 -22
package/README.md
CHANGED
|
@@ -111,9 +111,9 @@ 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
|
-
- **Tier 5:** all others
|
|
116
|
+
- **Tier 5:** ✓coupdaybs, ✓coupdays, ✓coupdaysnc, all others
|
|
117
117
|
|
|
118
118
|
## License
|
|
119
119
|
|
package/package.json
CHANGED
package/src/coupdaybs.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coupdaybs as coupdaybsUtil,
|
|
3
|
+
getCouponBounds,
|
|
4
|
+
toUtcDate,
|
|
5
|
+
} from "./util.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the number of days from the beginning of the coupon period to the
|
|
9
|
+
* settlement date.
|
|
10
|
+
*
|
|
11
|
+
* Remarks:
|
|
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 `frequency` is any number other than `1`, `2`, or `4`, an error is
|
|
16
|
+
* thrown.
|
|
17
|
+
* - If `basis` < `0` or if `basis` > `4`, an error is thrown.
|
|
18
|
+
* - If `settlement` >= `maturity`, an error is thrown.
|
|
19
|
+
*
|
|
20
|
+
* @param {Date} settlement - The security's settlement date.
|
|
21
|
+
* @param {Date} maturity - The security's maturity date.
|
|
22
|
+
* @param {1|2|4} frequency - The number of coupon payments per year. For annual
|
|
23
|
+
* payments, frequency = `1`; for semiannual, frequency = `2`; for quarterly,
|
|
24
|
+
* frequency = `4`.
|
|
25
|
+
* @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
|
|
26
|
+
* omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
|
|
27
|
+
* actual/365, `4` = European 30/360.
|
|
28
|
+
* @returns {number} The number of days from the beginning of the coupon period
|
|
29
|
+
* to the settlement date.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* coupdaybs(new Date("2011-01-25"), new Date("2011-11-15"), 2, 1); // 71
|
|
33
|
+
*/
|
|
34
|
+
export function coupdaybs(settlement, maturity, frequency, basis = 0) {
|
|
35
|
+
const settlementDate = toUtcDate(settlement);
|
|
36
|
+
const maturityDate = toUtcDate(maturity);
|
|
37
|
+
|
|
38
|
+
frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
|
|
39
|
+
const basisNumber = Math.trunc(basis ?? 0);
|
|
40
|
+
|
|
41
|
+
if (![1, 2, 4].includes(frequency)) {
|
|
42
|
+
throw new RangeError("Invalid frequency.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (basisNumber < 0 || basisNumber > 4) {
|
|
46
|
+
throw new RangeError("Invalid basis.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @type {0|1|2|3|4} */
|
|
50
|
+
const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
|
|
51
|
+
|
|
52
|
+
if (settlementDate >= maturityDate) {
|
|
53
|
+
throw new RangeError("Settlement must be before maturity.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const monthsPerCoupon = 12 / frequency;
|
|
57
|
+
const { previousCouponDate } = getCouponBounds(
|
|
58
|
+
settlementDate,
|
|
59
|
+
maturityDate,
|
|
60
|
+
monthsPerCoupon,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return coupdaybsUtil(previousCouponDate, settlementDate, normalizedBasis);
|
|
64
|
+
}
|
package/src/coupdays.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coupdays as coupdaysUtil,
|
|
3
|
+
getCouponBounds,
|
|
4
|
+
toUtcDate,
|
|
5
|
+
} from "./util.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the number of days in the coupon period that contains the settlement
|
|
9
|
+
* date.
|
|
10
|
+
*
|
|
11
|
+
* Remarks:
|
|
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 `frequency` is any number other than `1`, `2`, or `4`, an error is
|
|
16
|
+
* thrown.
|
|
17
|
+
* - If `basis` < `0` or if `basis` > `4`, an error is thrown.
|
|
18
|
+
* - If `settlement` >= `maturity`, an error is thrown.
|
|
19
|
+
*
|
|
20
|
+
* @param {Date} settlement - The security's settlement date.
|
|
21
|
+
* @param {Date} maturity - The security's maturity date.
|
|
22
|
+
* @param {1|2|4} frequency - The number of coupon payments per year. For annual
|
|
23
|
+
* payments, frequency = `1`; for semiannual, frequency = `2`; for quarterly,
|
|
24
|
+
* frequency = `4`.
|
|
25
|
+
* @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
|
|
26
|
+
* omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
|
|
27
|
+
* actual/365, `4` = European 30/360.
|
|
28
|
+
* @returns {number} The number of days in the coupon period that contains the
|
|
29
|
+
* settlement date.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* coupdays(new Date("2011-01-25"), new Date("2011-11-15"), 2, 1); // 184
|
|
33
|
+
*/
|
|
34
|
+
export function coupdays(settlement, maturity, frequency, basis = 0) {
|
|
35
|
+
const settlementDate = toUtcDate(settlement);
|
|
36
|
+
const maturityDate = toUtcDate(maturity);
|
|
37
|
+
|
|
38
|
+
frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
|
|
39
|
+
const basisNumber = Math.trunc(basis ?? 0);
|
|
40
|
+
|
|
41
|
+
if (![1, 2, 4].includes(frequency)) {
|
|
42
|
+
throw new RangeError("Invalid frequency.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (basisNumber < 0 || basisNumber > 4) {
|
|
46
|
+
throw new RangeError("Invalid basis.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @type {0|1|2|3|4} */
|
|
50
|
+
const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
|
|
51
|
+
|
|
52
|
+
if (settlementDate >= maturityDate) {
|
|
53
|
+
throw new RangeError("Settlement must be before maturity.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const monthsPerCoupon = 12 / frequency;
|
|
57
|
+
const { previousCouponDate, nextCouponDate } = getCouponBounds(
|
|
58
|
+
settlementDate,
|
|
59
|
+
maturityDate,
|
|
60
|
+
monthsPerCoupon,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return coupdaysUtil(
|
|
64
|
+
previousCouponDate,
|
|
65
|
+
nextCouponDate,
|
|
66
|
+
frequency,
|
|
67
|
+
normalizedBasis,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coupdaysnc as coupdaysncUtil,
|
|
3
|
+
getCouponBounds,
|
|
4
|
+
toUtcDate,
|
|
5
|
+
} from "./util.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the number of days from the settlement date to the next coupon date.
|
|
9
|
+
*
|
|
10
|
+
* Remarks:
|
|
11
|
+
* - `settlement`, `maturity`, `frequency`, and `basis` are truncated to
|
|
12
|
+
* integers.
|
|
13
|
+
* - If `settlement` or `maturity` is not a valid date, an error is thrown.
|
|
14
|
+
* - If `frequency` is any number other than `1`, `2`, or `4`, an error is
|
|
15
|
+
* thrown.
|
|
16
|
+
* - If `basis` < `0` or if `basis` > `4`, an error is thrown.
|
|
17
|
+
* - If `settlement` >= `maturity`, an error is thrown.
|
|
18
|
+
*
|
|
19
|
+
* @param {Date} settlement - The security's settlement date.
|
|
20
|
+
* @param {Date} maturity - The security's maturity date.
|
|
21
|
+
* @param {1|2|4} frequency - The number of coupon payments per year. For annual
|
|
22
|
+
* payments, frequency = `1`; for semiannual, frequency = `2`; for quarterly,
|
|
23
|
+
* frequency = `4`.
|
|
24
|
+
* @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
|
|
25
|
+
* omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
|
|
26
|
+
* actual/365, `4` = European 30/360.
|
|
27
|
+
* @returns {number} The number of days from the settlement date to the next
|
|
28
|
+
* coupon date.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* coupdaysnc(new Date("2011-01-25"), new Date("2011-11-15"), 2, 1); // 113
|
|
32
|
+
*/
|
|
33
|
+
export function coupdaysnc(settlement, maturity, frequency, basis = 0) {
|
|
34
|
+
const settlementDate = toUtcDate(settlement);
|
|
35
|
+
const maturityDate = toUtcDate(maturity);
|
|
36
|
+
|
|
37
|
+
frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
|
|
38
|
+
const basisNumber = Math.trunc(basis ?? 0);
|
|
39
|
+
|
|
40
|
+
if (![1, 2, 4].includes(frequency)) {
|
|
41
|
+
throw new RangeError("Invalid frequency.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (basisNumber < 0 || basisNumber > 4) {
|
|
45
|
+
throw new RangeError("Invalid basis.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {0|1|2|3|4} */
|
|
49
|
+
const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
|
|
50
|
+
|
|
51
|
+
if (settlementDate >= maturityDate) {
|
|
52
|
+
throw new RangeError("Settlement must be before maturity.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const monthsPerCoupon = 12 / frequency;
|
|
56
|
+
const { nextCouponDate } = getCouponBounds(
|
|
57
|
+
settlementDate,
|
|
58
|
+
maturityDate,
|
|
59
|
+
monthsPerCoupon,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return coupdaysncUtil(settlementDate, nextCouponDate, normalizedBasis);
|
|
63
|
+
}
|
package/src/fv.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { coupdaybs } from "./coupdaybs.js";
|
|
2
|
+
export { coupdays } from "./coupdays.js";
|
|
3
|
+
export { coupdaysnc } from "./coupdaysnc.js";
|
|
1
4
|
export { cumipmt } from "./cumipmt.js";
|
|
2
5
|
export { cumprinc } from "./cumprinc.js";
|
|
3
6
|
export { db } from "./db.js";
|
|
@@ -15,6 +18,7 @@ export { nominal } from "./nominal.js";
|
|
|
15
18
|
export { nper } from "./nper.js";
|
|
16
19
|
export { npv } from "./npv.js";
|
|
17
20
|
export { pduration } from "./pduration.js";
|
|
21
|
+
export { price } from "./price.js";
|
|
18
22
|
export { pmt } from "./pmt.js";
|
|
19
23
|
export { ppmt } from "./ppmt.js";
|
|
20
24
|
export { pv } from "./pv.js";
|
package/src/nper.js
CHANGED
package/src/pmt.js
CHANGED
package/src/ppmt.js
CHANGED
package/src/price.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
actualDays,
|
|
3
|
+
coupdaybs,
|
|
4
|
+
coupdays,
|
|
5
|
+
coupdaysnc,
|
|
6
|
+
couponsRemaining,
|
|
7
|
+
getCouponBounds,
|
|
8
|
+
normalizeZero,
|
|
9
|
+
toUtcDate,
|
|
10
|
+
} from "./util.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculates the price per $100 face value of a security that pays periodic
|
|
14
|
+
* interest.
|
|
15
|
+
*
|
|
16
|
+
* Remarks:
|
|
17
|
+
* - The settlement date is the date a buyer purchases a coupon, such as a bond.
|
|
18
|
+
* The maturity date is the date when a coupon expires. For example, suppose a
|
|
19
|
+
* 30-year bond is issued on January 1, 2008, and is purchased by a buyer six
|
|
20
|
+
* months later. The issue date would be January 1, 2008, the settlement date
|
|
21
|
+
* would be July 1, 2008, and the maturity date would be January 1, 2038,
|
|
22
|
+
* which is 30 years after the January 1, 2008, issue date.
|
|
23
|
+
* - `settlement`, `maturity`, `frequency`, and `basis` are truncated to
|
|
24
|
+
* integers.
|
|
25
|
+
* - If `settlement` or `maturity` is not a valid date, an error is thrown.
|
|
26
|
+
* - If `yld` < `0` or if `rate` < `0`, an error is thrown.
|
|
27
|
+
* - If `redemption` <= `0`, an error is thrown.
|
|
28
|
+
* - If `frequency` is any number other than `1`, `2`, or `4`, an error is
|
|
29
|
+
* thrown.
|
|
30
|
+
* - If `basis` < `0` or if `basis` > `4` and error is thrown.
|
|
31
|
+
* - If `settlement` >= `maturity`, an error is thrown.
|
|
32
|
+
* - When N > 1 (N is the number of coupons payable between the settlement date
|
|
33
|
+
* and redemption date), the calculation is: `PRICE =[redemption / (1 +
|
|
34
|
+
* yld/frequency)^(N-1 + DSC/E)] +[SUM(k=1 to N) (100 * rate/frequency) / (1 +
|
|
35
|
+
* yld/frequency)^(k-1 + DSC/E)] - (100 * rate/frequency * A/E)`, where:
|
|
36
|
+
* - DSC = number of days from settlement to next coupon date.
|
|
37
|
+
* - E = number of days in coupon period in which the settlement date falls.
|
|
38
|
+
* - A = number of days from beginning of coupon period to settlement date.
|
|
39
|
+
* - When N = 1, the calculation is: `DSR = E - A; T1 = 100 * (rate / frequency)
|
|
40
|
+
* + redemption; T2 = (yld / frequency) * (DSR / E) + 1; T3 = 100 * (rate /
|
|
41
|
+
* frequency) * (A / E); Price = (T1 / T2) - T3`.
|
|
42
|
+
*
|
|
43
|
+
* @param {Date} settlement - The security's settlement date. The security
|
|
44
|
+
* settlement date is the date after the issue date when the security is traded
|
|
45
|
+
* to the buyer.
|
|
46
|
+
* @param {Date} maturity - The security's maturity date. The maturity date is
|
|
47
|
+
* the date when the security expires.
|
|
48
|
+
* @param {number} rate - The security's annual coupon rate.
|
|
49
|
+
* @param {number} yld - The security's annual yield.
|
|
50
|
+
* @param {number} redemption - The security's redemption value per $100 face
|
|
51
|
+
* value.
|
|
52
|
+
* @param {1|2|4} frequency - The number of coupon payments per year. For annual
|
|
53
|
+
* payments, frequency = `1`; for semiannual, frequency = `2`; for quarterly,
|
|
54
|
+
* frequency = `4`.
|
|
55
|
+
* @param {0|1|2|3|4} [basis=0] - The type of day count basis to use. `0` or
|
|
56
|
+
* omitted = US (NASD 30/360), `1` = actual/actual, `2` = actual/360, `3` =
|
|
57
|
+
* actual/365, `4` = European 30/360.
|
|
58
|
+
* @returns {number} the price per $100 face value
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* price(new Date("2008-02-15"), new Date("2017-11-15"), 0.0575, 0.065, 100, 2, 0); // 94.63436162
|
|
62
|
+
*/
|
|
63
|
+
export function price(
|
|
64
|
+
settlement,
|
|
65
|
+
maturity,
|
|
66
|
+
rate,
|
|
67
|
+
yld,
|
|
68
|
+
redemption,
|
|
69
|
+
frequency,
|
|
70
|
+
basis = 0,
|
|
71
|
+
) {
|
|
72
|
+
const settlementDate = toUtcDate(settlement);
|
|
73
|
+
const maturityDate = toUtcDate(maturity);
|
|
74
|
+
|
|
75
|
+
frequency = /** @type {1|2|4} */ (Math.trunc(frequency));
|
|
76
|
+
const basisNumber = Math.trunc(basis ?? 0);
|
|
77
|
+
|
|
78
|
+
if (rate < 0 || yld < 0) {
|
|
79
|
+
throw new RangeError(
|
|
80
|
+
"Rate and yield must be greater than or equal to zero.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (redemption <= 0) {
|
|
85
|
+
throw new RangeError("Redemption must be greater than zero.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (![1, 2, 4].includes(frequency)) {
|
|
89
|
+
throw new RangeError("Invalid frequency.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (basisNumber < 0 || basisNumber > 4) {
|
|
93
|
+
throw new RangeError("Invalid basis.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @type {0|1|2|3|4} */
|
|
97
|
+
const normalizedBasis = /** @type {0|1|2|3|4} */ (basisNumber);
|
|
98
|
+
|
|
99
|
+
if (settlementDate >= maturityDate) {
|
|
100
|
+
throw new RangeError("Settlement must be before maturity.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const monthsPerCoupon = 12 / frequency;
|
|
104
|
+
const { previousCouponDate, nextCouponDate } = getCouponBounds(
|
|
105
|
+
settlementDate,
|
|
106
|
+
maturityDate,
|
|
107
|
+
monthsPerCoupon,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const a = coupdaybs(previousCouponDate, settlementDate, normalizedBasis);
|
|
111
|
+
let dsc = coupdaysnc(settlementDate, nextCouponDate, normalizedBasis);
|
|
112
|
+
let e = coupdays(
|
|
113
|
+
previousCouponDate,
|
|
114
|
+
nextCouponDate,
|
|
115
|
+
frequency,
|
|
116
|
+
normalizedBasis,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (normalizedBasis === 2) {
|
|
120
|
+
e = actualDays(previousCouponDate, nextCouponDate);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (normalizedBasis === 3) {
|
|
124
|
+
dsc = e - a;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const n = couponsRemaining(nextCouponDate, maturityDate, monthsPerCoupon);
|
|
128
|
+
const coupon = (100 * rate) / frequency;
|
|
129
|
+
|
|
130
|
+
if (n <= 1) {
|
|
131
|
+
const t1 = coupon + redemption;
|
|
132
|
+
const t2 = (yld / frequency) * (dsc / e) + 1;
|
|
133
|
+
const t3 = coupon * (a / e);
|
|
134
|
+
return normalizeZero(t1 / t2 - t3);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const base = 1 + yld / frequency;
|
|
138
|
+
const firstExponent = dsc / e;
|
|
139
|
+
|
|
140
|
+
let presentValue = 0;
|
|
141
|
+
for (let k = 1; k <= n; k += 1) {
|
|
142
|
+
presentValue += coupon / Math.pow(base, k - 1 + firstExponent);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
presentValue += redemption / Math.pow(base, n - 1 + firstExponent);
|
|
146
|
+
presentValue -= (coupon * a) / e;
|
|
147
|
+
|
|
148
|
+
return normalizeZero(presentValue);
|
|
149
|
+
}
|
package/src/pv.js
CHANGED
package/src/rate.js
CHANGED
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 === 2 || 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
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
actualDays,
|
|
3
|
+
coupdaybs,
|
|
4
|
+
coupdays,
|
|
5
|
+
coupdaysnc,
|
|
6
|
+
couponsRemaining,
|
|
7
|
+
getCouponBounds,
|
|
8
|
+
normalizeZero,
|
|
9
|
+
toUtcDate,
|
|
10
|
+
} from "./util.js";
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* Calculates the yield on a security that pays periodic interest. Use to
|
|
3
14
|
* calculate bond yield.
|
|
@@ -98,13 +109,17 @@ export function yield_(
|
|
|
98
109
|
|
|
99
110
|
const a = coupdaybs(previousCouponDate, settlementDate, normalizedBasis);
|
|
100
111
|
let dsc = coupdaysnc(settlementDate, nextCouponDate, normalizedBasis);
|
|
101
|
-
|
|
112
|
+
let e = coupdays(
|
|
102
113
|
previousCouponDate,
|
|
103
114
|
nextCouponDate,
|
|
104
115
|
frequency,
|
|
105
116
|
normalizedBasis,
|
|
106
117
|
);
|
|
107
118
|
|
|
119
|
+
if (normalizedBasis === 2) {
|
|
120
|
+
e = actualDays(previousCouponDate, nextCouponDate);
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
if (normalizedBasis === 3) {
|
|
109
124
|
dsc = e - a;
|
|
110
125
|
}
|
|
@@ -118,7 +133,7 @@ export function yield_(
|
|
|
118
133
|
(pr / 100 + (a / e) * (rate / frequency));
|
|
119
134
|
const denominator = pr / 100 + (a / e) * (rate / frequency);
|
|
120
135
|
const result = (numerator / denominator) * ((frequency * e) / dsc);
|
|
121
|
-
return
|
|
136
|
+
return normalizeZero(result);
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
/** @param {number} candidateYield */
|
|
@@ -150,17 +165,12 @@ export function yield_(
|
|
|
150
165
|
|
|
151
166
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
152
167
|
if (Math.abs(f1) < epsilon) {
|
|
153
|
-
return
|
|
168
|
+
return normalizeZero(y1);
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
if (f1 === f0) {
|
|
157
172
|
y0 = Math.max(y0 + step, minYield);
|
|
158
173
|
f0 = priceFromYield(y0) - pr;
|
|
159
|
-
if (f1 === f0) {
|
|
160
|
-
throw new RangeError(
|
|
161
|
-
"Cannot calculate YIELD with the provided values.",
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
let y2 = y1 - ((y1 - y0) * f1) / (f1 - f0);
|
|
@@ -168,7 +178,7 @@ export function yield_(
|
|
|
168
178
|
const f2 = priceFromYield(y2) - pr;
|
|
169
179
|
|
|
170
180
|
if (Math.abs(f2) < epsilon) {
|
|
171
|
-
return
|
|
181
|
+
return normalizeZero(y2);
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
y0 = y1;
|
|
@@ -179,207 +189,3 @@ export function yield_(
|
|
|
179
189
|
|
|
180
190
|
throw new RangeError("Maximum iterations exceeded while calculating YIELD.");
|
|
181
191
|
}
|
|
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
|
-
}
|
package/src/normalizeZero.js
DELETED
|
@@ -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
|
-
}
|