Skip to content

Commit 9a18c32

Browse files
justingrantptomato
authored andcommitted
Add NYSE custom time zone cookbook sample
This commit includes a sample custom time zone that implements an "NYSE Time Zone" where there are no valid PlainDateTIme values except when the market is open. When the market is closed, instants are disambiguated to the opening bell of the next market day. This is a proof-of-concept only. A real production implementation would include holidays and additional helper functions, e.g. "isMarketOpen".
1 parent 515655e commit 9a18c32

File tree

4 files changed

+235
-1
lines changed

4 files changed

+235
-1
lines changed

docs/cookbook-nyse.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## New York Stock Exchange time zone
2+
3+
This is an example of using `Temporal.TimeZone` for a custom purpose that is not a standard time zone in use somewhere in the world.
4+
5+
`NYSETimeZone` is a time zone where there are no valid `Temporal.PlainDateTime` values except when the market is open.
6+
When the market is closed, instants are disambiguated to the opening bell of the next market day.
7+
8+
> **NOTE**: This is a very specialized use of Temporal and is not something you would normally need to do.
9+
10+
```javascript
11+
{{cookbook/stockExchangeTimeZone.mjs}}
12+
```

docs/cookbook.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,9 @@ Since they are generally larger than these cookbook recipes, they're on their ow
465465
Extend Temporal to support arbitrarily-large years (e.g., **+635427810-02-02**) for astronomical purposes.
466466

467467
[Extra-expanded years](cookbook-expandedyears.md)
468+
469+
### New York Stock Exchange time zone
470+
471+
An example of using `Temporal.TimeZone` for other purposes than a standard time zne.
472+
473+
[NYSE time zone](cookbook-nyse.md)
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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');

polyfill/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"scripts": {
1010
"coverage": "c8 report --reporter html",
1111
"test": "node --no-warnings --experimental-modules --icu-data-dir node_modules/full-icu --loader ./test/resolve.source.mjs ./test/all.mjs",
12-
"test-cookbook": "TEST=all npm run test-cookbook-one",
12+
"test-cookbook": "TEST=all npm run test-cookbook-one && TEST=stockExchangeTimeZone npm run test-cookbook-one",
1313
"test-cookbook-one": "node --no-warnings --experimental-modules --icu-data-dir node_modules/full-icu --loader ./test/resolve.cookbook.mjs ../docs/cookbook/$TEST.mjs",
1414
"test262": "./ci_test.sh",
1515
"codecov:tests": "NODE_V8_COVERAGE=coverage/tmp npm run test && c8 report --reporter=text-lcov > coverage/tests.lcov && codecov -F tests -f coverage/tests.lcov",

0 commit comments

Comments
 (0)