|
| 1 | +/** |
| 2 | + * This sample implements a custom time zone that only allows PlainDateTime |
| 3 | + * values that are during the times that the New York Stock Exchange is open, |
| 4 | + * which is usually Monday through Friday 9:30 a.m. to 4:00 p.m. in |
| 5 | + * America/New_York. A more complete implementation would include market |
| 6 | + * holidays. |
| 7 | + * |
| 8 | + * `Temporal.Instants` when the market is closed will be disambiguated to the |
| 9 | + * start of the next day that the market is open. This makes it easy to |
| 10 | + * determine, for any instant, what market day that instant corresponds to, by |
| 11 | + * simply converting the instant to a ZonedDateTime in the 'NYSE' time zone. |
| 12 | + * |
| 13 | + * All `Temporal.Instant` values after market close on a particular market |
| 14 | + * display should be considered to be executed as of the market open on the next |
| 15 | + * market day. |
| 16 | + * */ |
| 17 | + |
| 18 | +const tz = Temporal.TimeZone.from('America/New_York'); |
| 19 | +const openTime = Temporal.PlainTime.from('09:30'); |
| 20 | +const closeTime = Temporal.PlainTime.from('16:00'); |
| 21 | +function isMarketOpenDate(date) { |
| 22 | + return date.dayOfWeek < 6; // not a weekend |
| 23 | +} |
| 24 | +function isDuringMarketHours(dt) { |
| 25 | + return isMarketOpenDate(dt) && !isBeforeMarketOpen(dt) && !isAfterMarketClose(dt); |
| 26 | +} |
| 27 | +function isBeforeMarketOpen(dt) { |
| 28 | + return isMarketOpenDate(dt) && Temporal.PlainTime.compare(dt, openTime) < 0; |
| 29 | +} |
| 30 | +function isAfterMarketClose(dt) { |
| 31 | + return isMarketOpenDate(dt) && Temporal.PlainTime.compare(dt, closeTime) >= 0; |
| 32 | +} |
| 33 | +function getNextMarketOpen(instant) { |
| 34 | + let zdt = instant.toZonedDateTimeISO(tz); |
| 35 | + |
| 36 | + // keep adding days until we get to a market day, unless today is a market day |
| 37 | + // before the market opens. |
| 38 | + if (!isBeforeMarketOpen(zdt)) { |
| 39 | + do { |
| 40 | + zdt = zdt.add({ days: 1 }); |
| 41 | + } while (!isMarketOpenDate(zdt)); |
| 42 | + } |
| 43 | + return zdt.toPlainDate().toZonedDateTime({ timeZone: tz, plainTime: openTime }); |
| 44 | +} |
| 45 | +function getNextMarketClose(instant) { |
| 46 | + let zdt = instant.toZonedDateTimeISO(tz); |
| 47 | + |
| 48 | + // keep adding days until we get to a market day, unless today is a market day |
| 49 | + // before the market closes. |
| 50 | + if (isAfterMarketClose(zdt)) { |
| 51 | + do { |
| 52 | + zdt = zdt.add({ days: 1 }); |
| 53 | + } while (!isMarketOpenDate(zdt)); |
| 54 | + } |
| 55 | + return zdt.toPlainDate().toZonedDateTime({ timeZone: tz, plainTime: closeTime }); |
| 56 | +} |
| 57 | +function getPreviousMarketOpen(instant) { |
| 58 | + let zdt = instant.toZonedDateTimeISO(tz); |
| 59 | + |
| 60 | + // keep subtracting days until we get to a market day, unless today is a market day |
| 61 | + // after the market opened. |
| 62 | + if (!isBeforeMarketOpen(zdt)) { |
| 63 | + do { |
| 64 | + zdt = zdt.subtract({ days: 1 }); |
| 65 | + } while (!isMarketOpenDate(zdt)); |
| 66 | + } |
| 67 | + return zdt.toPlainDate().toZonedDateTime({ timeZone: tz, plainTime: openTime }); |
| 68 | +} |
| 69 | +function getPreviousMarketClose(instant) { |
| 70 | + let zdt = instant.toZonedDateTimeISO(tz); |
| 71 | + |
| 72 | + // keep adding days until we get to a market day, unless today is a market day |
| 73 | + // after the market closed. |
| 74 | + if (!isAfterMarketClose(zdt)) { |
| 75 | + do { |
| 76 | + zdt = zdt.subtract({ days: 1 }); |
| 77 | + } while (!isMarketOpenDate(zdt)); |
| 78 | + } |
| 79 | + return zdt.toPlainDate().toZonedDateTime({ timeZone: tz, plainTime: closeTime }); |
| 80 | +} |
| 81 | + |
| 82 | +class NYSETimeZone extends Temporal.TimeZone { |
| 83 | + constructor() { |
| 84 | + super('America/New_York'); |
| 85 | + } |
| 86 | + getPossibleInstantsFor(dt) { |
| 87 | + dt = Temporal.PlainDateTime.from(dt); |
| 88 | + const zdt = dt.toZonedDateTime(tz); |
| 89 | + const zdtWhenMarketIsOpen = isDuringMarketHours(zdt) ? zdt : getNextMarketOpen(zdt.toInstant()); |
| 90 | + return [zdtWhenMarketIsOpen.toInstant()]; |
| 91 | + } |
| 92 | + getInstantFor(dt) { |
| 93 | + dt = Temporal.PlainDateTime.from(dt); |
| 94 | + // `disambiguation` option is ignored. If the market is closed, then return the |
| 95 | + // opening time of the next market day. |
| 96 | + const zdt = dt.toZonedDateTime(tz); |
| 97 | + const zdtWhenMarketIsOpen = isDuringMarketHours(zdt) ? zdt : getNextMarketOpen(zdt.toInstant()); |
| 98 | + return zdtWhenMarketIsOpen.toInstant(); |
| 99 | + } |
| 100 | + getPlainDateTimeFor(instant) { |
| 101 | + instant = Temporal.Instant.from(instant); |
| 102 | + const zdt = instant.toZonedDateTimeISO(tz); |
| 103 | + const zdtWhenMarketIsOpen = isDuringMarketHours(zdt) ? zdt : getNextMarketOpen(zdt.toInstant()); |
| 104 | + return zdtWhenMarketIsOpen.toPlainDateTime(); |
| 105 | + } |
| 106 | + getNextTransition(instant) { |
| 107 | + instant = Temporal.Instant.from(instant); |
| 108 | + const nextOpen = getNextMarketOpen(instant); |
| 109 | + const nextClose = getNextMarketClose(instant); |
| 110 | + const zdtTransition = [nextOpen, nextClose].sort(Temporal.ZonedDateTime.compare)[0]; |
| 111 | + return zdtTransition.toInstant(); |
| 112 | + } |
| 113 | + getPreviousTransition(instant) { |
| 114 | + instant = Temporal.Instant.from(instant); |
| 115 | + const prevOpen = getPreviousMarketOpen(instant); |
| 116 | + const prevClose = getPreviousMarketClose(instant); |
| 117 | + const zdtTransition = [prevOpen, prevClose].sort(Temporal.ZonedDateTime.compare)[1]; |
| 118 | + return zdtTransition.toInstant(); |
| 119 | + } |
| 120 | + getOffsetNanosecondsFor(instant) { |
| 121 | + instant = Temporal.Instant.from(instant); |
| 122 | + const zdt = instant.toZonedDateTimeISO(tz); |
| 123 | + const zdtWhenMarketIsOpen = isDuringMarketHours(zdt) ? zdt : getNextMarketOpen(zdt.toInstant()); |
| 124 | + const ns = zdt.offsetNanoseconds + zdt.until(zdtWhenMarketIsOpen, { largestUnit: 'nanoseconds' }).nanoseconds; |
| 125 | + return ns; |
| 126 | + } |
| 127 | + toString() { |
| 128 | + return 'NYSE'; |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +const tzNYSE = Object.freeze(new NYSETimeZone()); |
| 133 | + |
| 134 | +// Monkeypatch Temporal.TimeZone.from to handle our custom time zone |
| 135 | +let oldTzFrom; |
| 136 | +const replacement = (item) => { |
| 137 | + if (item === 'NYSE') return tzNYSE; |
| 138 | + return oldTzFrom.call(Temporal.TimeZone, item); |
| 139 | +}; |
| 140 | +if (Temporal.TimeZone.from !== replacement) { |
| 141 | + oldTzFrom = Temporal.TimeZone.from; |
| 142 | + Temporal.TimeZone.from = replacement; |
| 143 | +} |
| 144 | + |
| 145 | +let zdt; |
| 146 | +let isOpen; |
| 147 | +let date; |
| 148 | +let inNYSE; |
| 149 | +let nextOpen; |
| 150 | +let todayClose; |
| 151 | +let newDate; |
| 152 | +let openInstant; |
| 153 | +let closeInstant; |
| 154 | + |
| 155 | +// 1. What is the market day associated with the Instant of a financial transaction? |
| 156 | +zdt = Temporal.ZonedDateTime.from('2020-11-12T18:50-08:00[America/Los_Angeles]'); |
| 157 | +date = tzNYSE.getPlainDateTimeFor(zdt.toInstant()).toPlainDate(); |
| 158 | +assert.equal(date.toString(), '2020-11-13'); |
| 159 | +zdt = Temporal.ZonedDateTime.from('2020-11-12T06:50-08:00[America/Los_Angeles]'); |
| 160 | +date = tzNYSE.getPlainDateTimeFor(zdt.toInstant()).toPlainDate(); |
| 161 | +assert.equal(date.toString(), '2020-11-12'); |
| 162 | +zdt = Temporal.ZonedDateTime.from('2020-11-12T01:50-08:00[America/Los_Angeles]'); |
| 163 | +date = tzNYSE.getPlainDateTimeFor(zdt.toInstant()).toPlainDate(); |
| 164 | +assert.equal(date.toString(), '2020-11-12'); |
| 165 | + |
| 166 | +// 2. Is the stock market open on a particular date? |
| 167 | +date = Temporal.PlainDate.from('2020-11-12'); |
| 168 | +isOpen = date.toZonedDateTime('NYSE').toPlainDate().equals(date); |
| 169 | +assert.equal(isOpen, true); |
| 170 | +date = Temporal.PlainDate.from('2020-11-14'); |
| 171 | +isOpen = date.toZonedDateTime('NYSE').toPlainDate().equals(date); |
| 172 | +assert.equal(isOpen, false); |
| 173 | + |
| 174 | +// 3. For a particular date, when is the next market day? |
| 175 | +const getNextMarketDay = (date) => { |
| 176 | + date = Temporal.PlainDate.from(date); |
| 177 | + const zdt = date.toZonedDateTime('NYSE'); |
| 178 | + if (zdt.toPlainDate().equals(date)) { |
| 179 | + // It's a market day, so find the next one |
| 180 | + return zdt.add({ days: 1 }).toPlainDate(); |
| 181 | + } else { |
| 182 | + // the original date wasn't a market day, so we're already on the next one! |
| 183 | + return zdt.toPlainDate(); |
| 184 | + } |
| 185 | +}; |
| 186 | +date = Temporal.PlainDate.from('2020-11-09'); |
| 187 | +newDate = getNextMarketDay(date); |
| 188 | +assert.equal(newDate.equals('2020-11-10'), true); |
| 189 | +date = Temporal.PlainDate.from('2020-11-14'); |
| 190 | +newDate = getNextMarketDay(date); |
| 191 | +assert.equal(newDate.equals('2020-11-16'), true); |
| 192 | + |
| 193 | +// 4. For a particular date and time somewhere in the world, is the market open? |
| 194 | +// If it's open, then when will it close? |
| 195 | +// If it's closed, then when will it open next? |
| 196 | +// Return a result in the local time zone, not NYC's time zone. |
| 197 | +zdt = Temporal.ZonedDateTime.from('2020-11-12T18:50-08:00[America/Los_Angeles]'); |
| 198 | +inNYSE = zdt.withTimeZone('NYSE'); |
| 199 | +isOpen = inNYSE.toPlainDateTime().toZonedDateTime('NYSE').equals(inNYSE); |
| 200 | +assert.equal(isOpen, false); |
| 201 | +nextOpen = inNYSE.timeZone.getNextTransition(zdt.toInstant()).toZonedDateTimeISO(zdt.timeZone); |
| 202 | +assert.equal(nextOpen.toString(), '2020-11-13T06:30:00-08:00[America/Los_Angeles]'); |
| 203 | + |
| 204 | +zdt = Temporal.ZonedDateTime.from('2020-11-12T12:50-08:00[America/Los_Angeles]'); |
| 205 | +inNYSE = zdt.withTimeZone('NYSE'); |
| 206 | +isOpen = inNYSE.toPlainDateTime().toZonedDateTime('NYSE').equals(inNYSE); |
| 207 | +assert.equal(isOpen, true); |
| 208 | +todayClose = inNYSE.timeZone.getNextTransition(zdt.toInstant()).toZonedDateTimeISO(zdt.timeZone); |
| 209 | +assert.equal(todayClose.toString(), '2020-11-12T13:00:00-08:00[America/Los_Angeles]'); |
| 210 | + |
| 211 | +// 5. For any particular market date, what were the opening and closing clock times in NYC? |
| 212 | +date = Temporal.PlainDate.from('2020-11-09'); |
| 213 | +openInstant = date.toZonedDateTime('NYSE').toInstant(); |
| 214 | +closeInstant = date.toZonedDateTime('NYSE').timeZone.getNextTransition(openInstant); |
| 215 | +assert.equal(openInstant.toZonedDateTimeISO('America/New_York').toPlainTime().toString(), '09:30:00'); |
| 216 | +assert.equal(closeInstant.toZonedDateTimeISO('America/New_York').toPlainTime().toString(), '16:00:00'); |
0 commit comments