@discomedia/utils 1.0.18 → 1.0.19
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/dist/index-frontend.cjs +104 -304
- package/dist/index-frontend.cjs.map +1 -1
- package/dist/index-frontend.mjs +104 -304
- package/dist/index-frontend.mjs.map +1 -1
- package/dist/index.cjs +410 -579
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +410 -579
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +1 -1
- package/dist/test.js +13086 -475
- package/dist/test.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/market-time.d.ts +15 -154
- package/dist/types/market-time.d.ts.map +1 -1
- package/dist/types/test.d.ts +1 -1
- package/dist/types/test.d.ts.map +1 -1
- package/dist/types-frontend/index.d.ts.map +1 -1
- package/dist/types-frontend/market-time.d.ts +15 -154
- package/dist/types-frontend/market-time.d.ts.map +1 -1
- package/dist/types-frontend/test.d.ts +1 -1
- package/dist/types-frontend/test.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var dateFns = require('date-fns');
|
|
4
|
-
var dateFnsTz = require('date-fns-tz');
|
|
5
3
|
var require$$0$3 = require('events');
|
|
6
4
|
var require$$1 = require('https');
|
|
7
5
|
var require$$2 = require('http');
|
|
@@ -148,13 +146,11 @@ const marketEarlyCloses = {
|
|
|
148
146
|
},
|
|
149
147
|
};
|
|
150
148
|
|
|
151
|
-
//
|
|
152
|
-
// ===== CONFIGURATION =====
|
|
153
|
-
/**
|
|
154
|
-
* Market configuration constants
|
|
155
|
-
*/
|
|
149
|
+
// Constants for NY market times (Eastern Time)
|
|
156
150
|
const MARKET_CONFIG = {
|
|
157
151
|
TIMEZONE: 'America/New_York',
|
|
152
|
+
UTC_OFFSET_STANDARD: -5, // EST
|
|
153
|
+
UTC_OFFSET_DST: -4, // EDT
|
|
158
154
|
TIMES: {
|
|
159
155
|
EXTENDED_START: { hour: 4, minute: 0 },
|
|
160
156
|
MARKET_OPEN: { hour: 9, minute: 30 },
|
|
@@ -165,48 +161,97 @@ const MARKET_CONFIG = {
|
|
|
165
161
|
EARLY_EXTENDED_END: { hour: 17, minute: 0 },
|
|
166
162
|
},
|
|
167
163
|
};
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
// Helper: Get NY offset for a given UTC date (DST rules for US)
|
|
165
|
+
function getNYOffset(date) {
|
|
166
|
+
// US DST starts 2nd Sunday in March, ends 1st Sunday in November
|
|
167
|
+
const year = date.getUTCFullYear();
|
|
168
|
+
const dstStart = getNthWeekdayOfMonth(year, 3, 0, 2); // March, Sunday, 2nd
|
|
169
|
+
const dstEnd = getNthWeekdayOfMonth(year, 11, 0, 1); // November, Sunday, 1st
|
|
170
|
+
const utcTime = date.getTime();
|
|
171
|
+
if (utcTime >= dstStart.getTime() && utcTime < dstEnd.getTime()) {
|
|
172
|
+
return MARKET_CONFIG.UTC_OFFSET_DST;
|
|
173
|
+
}
|
|
174
|
+
return MARKET_CONFIG.UTC_OFFSET_STANDARD;
|
|
175
|
+
}
|
|
176
|
+
// Helper: Get nth weekday of month in UTC
|
|
177
|
+
function getNthWeekdayOfMonth(year, month, weekday, n) {
|
|
178
|
+
let count = 0;
|
|
179
|
+
for (let d = 1; d <= 31; d++) {
|
|
180
|
+
const date = new Date(Date.UTC(year, month - 1, d));
|
|
181
|
+
if (date.getUTCMonth() !== month - 1)
|
|
182
|
+
break;
|
|
183
|
+
if (date.getUTCDay() === weekday) {
|
|
184
|
+
count++;
|
|
185
|
+
if (count === n)
|
|
186
|
+
return date;
|
|
187
|
+
}
|
|
176
188
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
// fallback: last day of month
|
|
190
|
+
return new Date(Date.UTC(year, month - 1, 28));
|
|
191
|
+
}
|
|
192
|
+
// Helper: Convert UTC date to NY time (returns new Date object)
|
|
193
|
+
function toNYTime(date) {
|
|
194
|
+
const offset = getNYOffset(date);
|
|
195
|
+
// NY offset in hours
|
|
196
|
+
const utcMillis = date.getTime();
|
|
197
|
+
const nyMillis = utcMillis + offset * 60 * 60 * 1000;
|
|
198
|
+
return new Date(nyMillis);
|
|
199
|
+
}
|
|
200
|
+
// Helper: Convert NY time to UTC (returns new Date object)
|
|
201
|
+
function fromNYTime(date) {
|
|
202
|
+
const offset = getNYOffset(date);
|
|
203
|
+
const nyMillis = date.getTime();
|
|
204
|
+
const utcMillis = nyMillis - offset * 60 * 60 * 1000;
|
|
205
|
+
return new Date(utcMillis);
|
|
206
|
+
}
|
|
207
|
+
// Helper: Format date in ISO, unix, etc.
|
|
208
|
+
function formatDate(date, outputFormat = 'iso') {
|
|
209
|
+
switch (outputFormat) {
|
|
210
|
+
case 'unix-seconds':
|
|
211
|
+
return Math.floor(date.getTime() / 1000);
|
|
212
|
+
case 'unix-ms':
|
|
213
|
+
return date.getTime();
|
|
214
|
+
case 'iso':
|
|
215
|
+
default:
|
|
216
|
+
return date.toISOString();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Helper: Format date in NY locale string
|
|
220
|
+
function formatNYLocale(date) {
|
|
221
|
+
return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
|
|
222
|
+
}
|
|
223
|
+
// Market calendar logic
|
|
224
|
+
class MarketCalendar {
|
|
180
225
|
isWeekend(date) {
|
|
181
|
-
const day = date.
|
|
182
|
-
return day === 0 || day === 6;
|
|
226
|
+
const day = toNYTime(date).getUTCDay();
|
|
227
|
+
return day === 0 || day === 6;
|
|
183
228
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Check if a date is a market holiday
|
|
186
|
-
*/
|
|
187
229
|
isHoliday(date) {
|
|
188
|
-
const
|
|
189
|
-
const year =
|
|
230
|
+
const nyDate = toNYTime(date);
|
|
231
|
+
const year = nyDate.getUTCFullYear();
|
|
232
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
233
|
+
const day = nyDate.getUTCDate();
|
|
234
|
+
const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
190
235
|
const yearHolidays = marketHolidays[year];
|
|
191
236
|
if (!yearHolidays)
|
|
192
237
|
return false;
|
|
193
238
|
return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
|
|
194
239
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Check if a date is an early close day
|
|
197
|
-
*/
|
|
198
240
|
isEarlyCloseDay(date) {
|
|
199
|
-
const
|
|
200
|
-
const year =
|
|
241
|
+
const nyDate = toNYTime(date);
|
|
242
|
+
const year = nyDate.getUTCFullYear();
|
|
243
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
244
|
+
const day = nyDate.getUTCDate();
|
|
245
|
+
const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
201
246
|
const yearEarlyCloses = marketEarlyCloses[year];
|
|
202
247
|
return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
|
|
203
248
|
}
|
|
204
|
-
/**
|
|
205
|
-
* Get the early close time for a date (in minutes from midnight)
|
|
206
|
-
*/
|
|
207
249
|
getEarlyCloseTime(date) {
|
|
208
|
-
const
|
|
209
|
-
const year =
|
|
250
|
+
const nyDate = toNYTime(date);
|
|
251
|
+
const year = nyDate.getUTCFullYear();
|
|
252
|
+
const month = nyDate.getUTCMonth() + 1;
|
|
253
|
+
const day = nyDate.getUTCDate();
|
|
254
|
+
const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
210
255
|
const yearEarlyCloses = marketEarlyCloses[year];
|
|
211
256
|
if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
|
|
212
257
|
const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
|
|
@@ -214,618 +259,404 @@ class MarketCalendar {
|
|
|
214
259
|
}
|
|
215
260
|
return null;
|
|
216
261
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Check if a date is a market day (not weekend or holiday)
|
|
219
|
-
*/
|
|
220
262
|
isMarketDay(date) {
|
|
221
263
|
return !this.isWeekend(date) && !this.isHoliday(date);
|
|
222
264
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Get the next market day from a given date
|
|
225
|
-
*/
|
|
226
265
|
getNextMarketDay(date) {
|
|
227
|
-
let nextDay =
|
|
266
|
+
let nextDay = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
|
228
267
|
while (!this.isMarketDay(nextDay)) {
|
|
229
|
-
nextDay =
|
|
268
|
+
nextDay = new Date(nextDay.getTime() + 24 * 60 * 60 * 1000);
|
|
230
269
|
}
|
|
231
270
|
return nextDay;
|
|
232
271
|
}
|
|
233
|
-
/**
|
|
234
|
-
* Get the previous market day from a given date
|
|
235
|
-
*/
|
|
236
272
|
getPreviousMarketDay(date) {
|
|
237
|
-
let prevDay =
|
|
273
|
+
let prevDay = new Date(date.getTime() - 24 * 60 * 60 * 1000);
|
|
238
274
|
while (!this.isMarketDay(prevDay)) {
|
|
239
|
-
prevDay =
|
|
275
|
+
prevDay = new Date(prevDay.getTime() - 24 * 60 * 60 * 1000);
|
|
240
276
|
}
|
|
241
277
|
return prevDay;
|
|
242
278
|
}
|
|
243
279
|
}
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
timezone;
|
|
250
|
-
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
251
|
-
this.timezone = timezone;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Format a date based on the output format
|
|
255
|
-
*/
|
|
256
|
-
formatDate(date, outputFormat = 'iso') {
|
|
257
|
-
switch (outputFormat) {
|
|
258
|
-
case 'unix-seconds':
|
|
259
|
-
return Math.floor(date.getTime() / 1000);
|
|
260
|
-
case 'unix-ms':
|
|
261
|
-
return date.getTime();
|
|
262
|
-
case 'iso':
|
|
263
|
-
default:
|
|
264
|
-
return dateFnsTz.formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Get New York timezone offset
|
|
269
|
-
*/
|
|
270
|
-
getNYTimeZone(date = new Date()) {
|
|
271
|
-
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
272
|
-
timeZone: this.timezone,
|
|
273
|
-
timeZoneName: 'shortOffset',
|
|
274
|
-
});
|
|
275
|
-
const parts = dtf.formatToParts(date);
|
|
276
|
-
const tz = parts.find(p => p.type === 'timeZoneName')?.value;
|
|
277
|
-
if (!tz) {
|
|
278
|
-
throw new Error('Could not determine New York offset');
|
|
279
|
-
}
|
|
280
|
-
const shortOffset = tz.replace('GMT', '');
|
|
281
|
-
if (shortOffset === '-4') {
|
|
282
|
-
return '-04:00';
|
|
283
|
-
}
|
|
284
|
-
else if (shortOffset === '-5') {
|
|
285
|
-
return '-05:00';
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
throw new Error(`Unexpected timezone offset: ${shortOffset}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Get trading date in YYYY-MM-DD format
|
|
293
|
-
*/
|
|
294
|
-
getTradingDate(time) {
|
|
295
|
-
let date;
|
|
296
|
-
if (typeof time === 'number') {
|
|
297
|
-
date = new Date(time);
|
|
298
|
-
}
|
|
299
|
-
else if (typeof time === 'string') {
|
|
300
|
-
date = new Date(time);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
date = time;
|
|
304
|
-
}
|
|
305
|
-
return dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// ===== MARKET TIME CALCULATOR =====
|
|
309
|
-
/**
|
|
310
|
-
* Service for core market time calculations
|
|
311
|
-
*/
|
|
312
|
-
class MarketTimeCalculator {
|
|
313
|
-
calendar;
|
|
314
|
-
timezone;
|
|
315
|
-
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
316
|
-
this.timezone = timezone;
|
|
317
|
-
this.calendar = new MarketCalendar(timezone);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Get market open/close times for a date
|
|
321
|
-
*/
|
|
322
|
-
getMarketTimes(date) {
|
|
323
|
-
const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
|
|
324
|
-
// Market closed on weekends and holidays
|
|
325
|
-
if (!this.calendar.isMarketDay(zonedDate)) {
|
|
326
|
-
return {
|
|
327
|
-
marketOpen: false,
|
|
328
|
-
open: null,
|
|
329
|
-
close: null,
|
|
330
|
-
openExt: null,
|
|
331
|
-
closeExt: null,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
const dayStart = dateFns.startOfDay(zonedDate);
|
|
335
|
-
// Regular market times
|
|
336
|
-
const open = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour, minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute }), this.timezone);
|
|
337
|
-
let close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute }), this.timezone);
|
|
338
|
-
// Extended hours
|
|
339
|
-
const openExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute }), this.timezone);
|
|
340
|
-
let closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute }), this.timezone);
|
|
341
|
-
// Handle early close days
|
|
342
|
-
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
343
|
-
close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute }), this.timezone);
|
|
344
|
-
closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute }), this.timezone);
|
|
345
|
-
}
|
|
280
|
+
// Market open/close times
|
|
281
|
+
function getMarketTimes(date) {
|
|
282
|
+
const calendar = new MarketCalendar();
|
|
283
|
+
const nyDate = toNYTime(date);
|
|
284
|
+
if (!calendar.isMarketDay(date)) {
|
|
346
285
|
return {
|
|
347
|
-
marketOpen:
|
|
348
|
-
open,
|
|
349
|
-
close,
|
|
350
|
-
openExt,
|
|
351
|
-
closeExt,
|
|
286
|
+
marketOpen: false,
|
|
287
|
+
open: null,
|
|
288
|
+
close: null,
|
|
289
|
+
openExt: null,
|
|
290
|
+
closeExt: null,
|
|
352
291
|
};
|
|
353
292
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
370
|
-
endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
371
|
-
}
|
|
372
|
-
return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
|
|
373
|
-
}
|
|
374
|
-
case 'continuous':
|
|
375
|
-
return true;
|
|
376
|
-
default: {
|
|
377
|
-
// 'market_hours'
|
|
378
|
-
const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
379
|
-
let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
380
|
-
// Handle early close
|
|
381
|
-
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
382
|
-
endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
383
|
-
}
|
|
384
|
-
return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
293
|
+
const year = nyDate.getUTCFullYear();
|
|
294
|
+
const month = nyDate.getUTCMonth();
|
|
295
|
+
const day = nyDate.getUTCDate();
|
|
296
|
+
// Helper to build NY time for a given hour/minute
|
|
297
|
+
function buildNYTime(hour, minute) {
|
|
298
|
+
const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
|
|
299
|
+
return fromNYTime(d);
|
|
300
|
+
}
|
|
301
|
+
let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
|
|
302
|
+
let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
|
|
303
|
+
let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
|
|
304
|
+
let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
|
|
305
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
306
|
+
close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
|
|
307
|
+
closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
|
|
387
308
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
309
|
+
return {
|
|
310
|
+
marketOpen: true,
|
|
311
|
+
open,
|
|
312
|
+
close,
|
|
313
|
+
openExt,
|
|
314
|
+
closeExt,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Is within market hours
|
|
318
|
+
function isWithinMarketHoursImpl(date, intradayReporting = 'market_hours') {
|
|
319
|
+
const calendar = new MarketCalendar();
|
|
320
|
+
if (!calendar.isMarketDay(date))
|
|
321
|
+
return false;
|
|
322
|
+
const nyDate = toNYTime(date);
|
|
323
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
324
|
+
switch (intradayReporting) {
|
|
325
|
+
case 'extended_hours': {
|
|
326
|
+
let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
327
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
328
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
407
329
|
}
|
|
408
|
-
|
|
409
|
-
|
|
330
|
+
const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
331
|
+
return minutes >= startMinutes && minutes <= endMinutes;
|
|
332
|
+
}
|
|
333
|
+
case 'continuous':
|
|
334
|
+
return true;
|
|
335
|
+
default: {
|
|
336
|
+
let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
337
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
338
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
410
339
|
}
|
|
340
|
+
const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
341
|
+
return minutes >= startMinutes && minutes <= endMinutes;
|
|
411
342
|
}
|
|
412
|
-
// Return the last completed trading day
|
|
413
|
-
const lastMarketDay = this.calendar.getPreviousMarketDay(nowET);
|
|
414
|
-
return dateFnsTz.fromZonedTime(dateFns.set(lastMarketDay, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
|
|
415
343
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
344
|
+
}
|
|
345
|
+
// Get last full trading date
|
|
346
|
+
function getLastFullTradingDateImpl(currentDate = new Date()) {
|
|
347
|
+
const calendar = new MarketCalendar();
|
|
348
|
+
const nyDate = toNYTime(currentDate);
|
|
349
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
350
|
+
const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
351
|
+
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
352
|
+
if (calendar.isEarlyCloseDay(currentDate)) {
|
|
353
|
+
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
354
|
+
}
|
|
355
|
+
// If not a market day, or before open, or during market hours, return previous market day's close
|
|
356
|
+
if (!calendar.isMarketDay(currentDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
|
|
357
|
+
const prevMarketDay = calendar.getPreviousMarketDay(currentDate);
|
|
358
|
+
let prevCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
359
|
+
if (calendar.isEarlyCloseDay(prevMarketDay)) {
|
|
360
|
+
prevCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
361
|
+
}
|
|
362
|
+
const year = prevMarketDay.getUTCFullYear();
|
|
363
|
+
const month = prevMarketDay.getUTCMonth();
|
|
364
|
+
const day = prevMarketDay.getUTCDate();
|
|
365
|
+
const closeHour = Math.floor(prevCloseMinutes / 60);
|
|
366
|
+
const closeMinute = prevCloseMinutes % 60;
|
|
367
|
+
return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
|
|
368
|
+
}
|
|
369
|
+
// After market close or after extended hours, return today's close
|
|
370
|
+
const year = nyDate.getUTCFullYear();
|
|
371
|
+
const month = nyDate.getUTCMonth();
|
|
372
|
+
const day = nyDate.getUTCDate();
|
|
373
|
+
const closeHour = Math.floor(marketCloseMinutes / 60);
|
|
374
|
+
const closeMinute = marketCloseMinutes % 60;
|
|
375
|
+
return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
|
|
376
|
+
}
|
|
377
|
+
// Get day boundaries
|
|
378
|
+
function getDayBoundaries(date, intradayReporting = 'market_hours') {
|
|
379
|
+
const calendar = new MarketCalendar();
|
|
380
|
+
const nyDate = toNYTime(date);
|
|
381
|
+
const year = nyDate.getUTCFullYear();
|
|
382
|
+
const month = nyDate.getUTCMonth();
|
|
383
|
+
const day = nyDate.getUTCDate();
|
|
384
|
+
function buildNYTime(hour, minute, sec = 0, ms = 0) {
|
|
385
|
+
const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
|
|
386
|
+
return fromNYTime(d);
|
|
387
|
+
}
|
|
388
|
+
let start;
|
|
389
|
+
let end;
|
|
390
|
+
switch (intradayReporting) {
|
|
391
|
+
case 'extended_hours':
|
|
392
|
+
start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
|
|
393
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
|
|
394
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
395
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
|
|
452
396
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
|
|
464
|
-
seconds: 59,
|
|
465
|
-
milliseconds: 999,
|
|
466
|
-
});
|
|
467
|
-
// Handle early close
|
|
468
|
-
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
469
|
-
end = dateFns.set(zonedDate, {
|
|
470
|
-
hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour,
|
|
471
|
-
minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute,
|
|
472
|
-
seconds: 59,
|
|
473
|
-
milliseconds: 999,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
break;
|
|
397
|
+
break;
|
|
398
|
+
case 'continuous':
|
|
399
|
+
start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
|
400
|
+
end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
|
|
401
|
+
break;
|
|
402
|
+
default:
|
|
403
|
+
start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
|
|
404
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
|
|
405
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
406
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
|
|
477
407
|
}
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
start: dateFnsTz.fromZonedTime(start, this.timezone),
|
|
481
|
-
end: dateFnsTz.fromZonedTime(end, this.timezone),
|
|
482
|
-
};
|
|
408
|
+
break;
|
|
483
409
|
}
|
|
410
|
+
return { start, end };
|
|
484
411
|
}
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
412
|
+
// Period calculator
|
|
413
|
+
function calculatePeriodStartDate(endDate, period) {
|
|
414
|
+
const calendar = new MarketCalendar();
|
|
415
|
+
let startDate;
|
|
416
|
+
switch (period) {
|
|
417
|
+
case 'YTD':
|
|
418
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
|
|
419
|
+
break;
|
|
420
|
+
case '1D':
|
|
421
|
+
startDate = calendar.getPreviousMarketDay(endDate);
|
|
422
|
+
break;
|
|
423
|
+
case '3D':
|
|
424
|
+
startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
425
|
+
break;
|
|
426
|
+
case '1W':
|
|
427
|
+
startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
428
|
+
break;
|
|
429
|
+
case '2W':
|
|
430
|
+
startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
431
|
+
break;
|
|
432
|
+
case '1M':
|
|
433
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
|
|
434
|
+
break;
|
|
435
|
+
case '3M':
|
|
436
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
|
|
437
|
+
break;
|
|
438
|
+
case '6M':
|
|
439
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
|
|
440
|
+
break;
|
|
441
|
+
case '1Y':
|
|
442
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
throw new Error(`Invalid period: ${period}`);
|
|
497
446
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
calculatePeriodStartDate(endDate, period) {
|
|
502
|
-
let startDate;
|
|
503
|
-
switch (period) {
|
|
504
|
-
case 'YTD':
|
|
505
|
-
startDate = dateFns.set(endDate, { month: 0, date: 1 });
|
|
506
|
-
break;
|
|
507
|
-
case '1D':
|
|
508
|
-
startDate = this.calendar.getPreviousMarketDay(endDate);
|
|
509
|
-
break;
|
|
510
|
-
case '3D':
|
|
511
|
-
startDate = dateFns.sub(endDate, { days: 3 });
|
|
512
|
-
break;
|
|
513
|
-
case '1W':
|
|
514
|
-
startDate = dateFns.sub(endDate, { weeks: 1 });
|
|
515
|
-
break;
|
|
516
|
-
case '2W':
|
|
517
|
-
startDate = dateFns.sub(endDate, { weeks: 2 });
|
|
518
|
-
break;
|
|
519
|
-
case '1M':
|
|
520
|
-
startDate = dateFns.sub(endDate, { months: 1 });
|
|
521
|
-
break;
|
|
522
|
-
case '3M':
|
|
523
|
-
startDate = dateFns.sub(endDate, { months: 3 });
|
|
524
|
-
break;
|
|
525
|
-
case '6M':
|
|
526
|
-
startDate = dateFns.sub(endDate, { months: 6 });
|
|
527
|
-
break;
|
|
528
|
-
case '1Y':
|
|
529
|
-
startDate = dateFns.sub(endDate, { years: 1 });
|
|
530
|
-
break;
|
|
531
|
-
default:
|
|
532
|
-
throw new Error(`Invalid period: ${period}`);
|
|
533
|
-
}
|
|
534
|
-
// Ensure start date is a market day
|
|
535
|
-
while (!this.calendar.isMarketDay(startDate)) {
|
|
536
|
-
startDate = this.calendar.getNextMarketDay(startDate);
|
|
537
|
-
}
|
|
538
|
-
return startDate;
|
|
447
|
+
// Ensure start date is a market day
|
|
448
|
+
while (!calendar.isMarketDay(startDate)) {
|
|
449
|
+
startDate = calendar.getNextMarketDay(startDate);
|
|
539
450
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
endDate = end;
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
// After market close - use today's close
|
|
568
|
-
const { end: dayEnd } = this.timeCalculator.getDayBoundaries(zonedEndDate, intraday_reporting);
|
|
569
|
-
endDate = dayEnd;
|
|
570
|
-
}
|
|
451
|
+
return startDate;
|
|
452
|
+
}
|
|
453
|
+
// Get market time period
|
|
454
|
+
function getMarketTimePeriod(params) {
|
|
455
|
+
const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
|
|
456
|
+
if (!period)
|
|
457
|
+
throw new Error('Period is required');
|
|
458
|
+
const calendar = new MarketCalendar();
|
|
459
|
+
const nyEndDate = toNYTime(end);
|
|
460
|
+
let endDate;
|
|
461
|
+
const isCurrentMarketDay = calendar.isMarketDay(end);
|
|
462
|
+
const isWithinHours = isWithinMarketHours(end, intraday_reporting);
|
|
463
|
+
const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
|
|
464
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
465
|
+
if (isCurrentMarketDay) {
|
|
466
|
+
if (minutes < marketStartMinutes) {
|
|
467
|
+
// Before market open - use previous day's close
|
|
468
|
+
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
469
|
+
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
470
|
+
endDate = dayEnd;
|
|
471
|
+
}
|
|
472
|
+
else if (isWithinHours) {
|
|
473
|
+
// During market hours - use current time
|
|
474
|
+
endDate = end;
|
|
571
475
|
}
|
|
572
476
|
else {
|
|
573
|
-
//
|
|
574
|
-
const
|
|
575
|
-
const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
477
|
+
// After market close - use today's close
|
|
478
|
+
const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
|
|
576
479
|
endDate = dayEnd;
|
|
577
480
|
}
|
|
578
|
-
// Calculate start date
|
|
579
|
-
const periodStartDate = this.calculatePeriodStartDate(endDate, period);
|
|
580
|
-
const { start: dayStart } = this.timeCalculator.getDayBoundaries(periodStartDate, intraday_reporting);
|
|
581
|
-
// Ensure start is not after end
|
|
582
|
-
if (dateFns.isBefore(endDate, dayStart)) {
|
|
583
|
-
throw new Error('Start date cannot be after end date');
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
start: this.formatter.formatDate(dayStart, outputFormat),
|
|
587
|
-
end: this.formatter.formatDate(endDate, outputFormat),
|
|
588
|
-
};
|
|
589
481
|
}
|
|
482
|
+
else {
|
|
483
|
+
// Not a market day - use previous market day's close
|
|
484
|
+
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
485
|
+
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
486
|
+
endDate = dayEnd;
|
|
487
|
+
}
|
|
488
|
+
// Calculate start date
|
|
489
|
+
const periodStartDate = calculatePeriodStartDate(endDate, period);
|
|
490
|
+
const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
|
|
491
|
+
if (endDate.getTime() < dayStart.getTime()) {
|
|
492
|
+
throw new Error('Start date cannot be after end date');
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
start: formatDate(dayStart, outputFormat),
|
|
496
|
+
end: formatDate(endDate, outputFormat),
|
|
497
|
+
};
|
|
590
498
|
}
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
calendar;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
499
|
+
// Market status
|
|
500
|
+
function getMarketStatusImpl(date = new Date()) {
|
|
501
|
+
const calendar = new MarketCalendar();
|
|
502
|
+
const nyDate = toNYTime(date);
|
|
503
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
504
|
+
const isMarketDay = calendar.isMarketDay(date);
|
|
505
|
+
const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
|
|
506
|
+
const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
507
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
508
|
+
const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
|
|
509
|
+
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
510
|
+
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
511
|
+
if (isEarlyCloseDay) {
|
|
512
|
+
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
513
|
+
extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
514
|
+
}
|
|
515
|
+
let status;
|
|
516
|
+
let nextStatus;
|
|
517
|
+
let nextStatusTime;
|
|
518
|
+
let marketPeriod;
|
|
519
|
+
if (!isMarketDay) {
|
|
520
|
+
status = 'closed';
|
|
521
|
+
nextStatus = 'extended hours';
|
|
522
|
+
marketPeriod = 'closed';
|
|
523
|
+
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
524
|
+
nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
|
|
525
|
+
}
|
|
526
|
+
else if (minutes < extendedStartMinutes) {
|
|
527
|
+
status = 'closed';
|
|
528
|
+
nextStatus = 'extended hours';
|
|
529
|
+
marketPeriod = 'closed';
|
|
530
|
+
nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
|
|
531
|
+
}
|
|
532
|
+
else if (minutes < marketStartMinutes) {
|
|
533
|
+
status = 'extended hours';
|
|
534
|
+
nextStatus = 'open';
|
|
535
|
+
marketPeriod = 'preMarket';
|
|
536
|
+
nextStatusTime = getDayBoundaries(date, 'market_hours').start;
|
|
537
|
+
}
|
|
538
|
+
else if (minutes < marketCloseMinutes) {
|
|
539
|
+
status = 'open';
|
|
540
|
+
nextStatus = 'extended hours';
|
|
541
|
+
marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
|
|
542
|
+
nextStatusTime = getDayBoundaries(date, 'market_hours').end;
|
|
543
|
+
}
|
|
544
|
+
else if (minutes < extendedEndMinutes) {
|
|
545
|
+
status = 'extended hours';
|
|
546
|
+
nextStatus = 'closed';
|
|
547
|
+
marketPeriod = 'afterMarket';
|
|
548
|
+
nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
|
|
603
549
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
const isMarketDay = this.calendar.isMarketDay(nyTime);
|
|
611
|
-
const isEarlyCloseDay = this.calendar.isEarlyCloseDay(nyTime);
|
|
612
|
-
// Time boundaries
|
|
613
|
-
const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
614
|
-
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
615
|
-
const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
|
|
616
|
-
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
617
|
-
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
618
|
-
if (isEarlyCloseDay) {
|
|
619
|
-
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
620
|
-
extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
621
|
-
}
|
|
622
|
-
let status;
|
|
623
|
-
let nextStatus;
|
|
624
|
-
let nextStatusTime;
|
|
625
|
-
let marketPeriod;
|
|
626
|
-
if (!isMarketDay) {
|
|
627
|
-
// Market is closed (holiday/weekend)
|
|
628
|
-
status = 'closed';
|
|
629
|
-
nextStatus = 'extended hours';
|
|
630
|
-
marketPeriod = 'closed';
|
|
631
|
-
const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
|
|
632
|
-
nextStatusTime = dateFns.set(nextMarketDay, {
|
|
633
|
-
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
634
|
-
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
else if (timeInMinutes < extendedStartMinutes) {
|
|
638
|
-
// Before extended hours
|
|
639
|
-
status = 'closed';
|
|
640
|
-
nextStatus = 'extended hours';
|
|
641
|
-
marketPeriod = 'closed';
|
|
642
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
643
|
-
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
644
|
-
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
else if (timeInMinutes < marketStartMinutes) {
|
|
648
|
-
// Pre-market extended hours
|
|
649
|
-
status = 'extended hours';
|
|
650
|
-
nextStatus = 'open';
|
|
651
|
-
marketPeriod = 'preMarket';
|
|
652
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
653
|
-
hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
|
|
654
|
-
minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
else if (timeInMinutes < marketCloseMinutes) {
|
|
658
|
-
// Market is open
|
|
659
|
-
status = 'open';
|
|
660
|
-
nextStatus = 'extended hours';
|
|
661
|
-
marketPeriod = timeInMinutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
|
|
662
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
663
|
-
hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.hour : MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
|
|
664
|
-
minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.minute : MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
else if (timeInMinutes < extendedEndMinutes) {
|
|
668
|
-
// After-market extended hours
|
|
669
|
-
status = 'extended hours';
|
|
670
|
-
nextStatus = 'closed';
|
|
671
|
-
marketPeriod = 'afterMarket';
|
|
672
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
673
|
-
hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour : MARKET_CONFIG.TIMES.EXTENDED_END.hour,
|
|
674
|
-
minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute : MARKET_CONFIG.TIMES.EXTENDED_END.minute,
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
// After extended hours
|
|
679
|
-
status = 'closed';
|
|
680
|
-
nextStatus = 'extended hours';
|
|
681
|
-
marketPeriod = 'closed';
|
|
682
|
-
const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
|
|
683
|
-
nextStatusTime = dateFns.set(nextMarketDay, {
|
|
684
|
-
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
685
|
-
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
const nextStatusTimeUTC = dateFnsTz.fromZonedTime(nextStatusTime, this.timezone);
|
|
689
|
-
const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
|
|
690
|
-
return {
|
|
691
|
-
time: date,
|
|
692
|
-
timeString: dateFns.format(nyTime, dateFormat),
|
|
693
|
-
status,
|
|
694
|
-
nextStatus,
|
|
695
|
-
marketPeriod,
|
|
696
|
-
nextStatusTime: nextStatusTimeUTC,
|
|
697
|
-
nextStatusTimeDifference: dateFns.differenceInMilliseconds(nextStatusTime, nyTime),
|
|
698
|
-
nextStatusTimeString: dateFns.format(nextStatusTime, dateFormat),
|
|
699
|
-
};
|
|
550
|
+
else {
|
|
551
|
+
status = 'closed';
|
|
552
|
+
nextStatus = 'extended hours';
|
|
553
|
+
marketPeriod = 'closed';
|
|
554
|
+
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
555
|
+
nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
|
|
700
556
|
}
|
|
557
|
+
const nextStatusTimeDifference = nextStatusTime.getTime() - nyDate.getTime();
|
|
558
|
+
return {
|
|
559
|
+
time: date,
|
|
560
|
+
timeString: formatNYLocale(nyDate),
|
|
561
|
+
status,
|
|
562
|
+
nextStatus,
|
|
563
|
+
marketPeriod,
|
|
564
|
+
nextStatusTime,
|
|
565
|
+
nextStatusTimeDifference,
|
|
566
|
+
nextStatusTimeString: formatNYLocale(nextStatusTime),
|
|
567
|
+
};
|
|
701
568
|
}
|
|
702
|
-
//
|
|
703
|
-
/**
|
|
704
|
-
* Simple functional API that uses the services above
|
|
705
|
-
*/
|
|
706
|
-
// Create service instances
|
|
707
|
-
const marketCalendar = new MarketCalendar();
|
|
708
|
-
const marketTimeCalculator = new MarketTimeCalculator();
|
|
709
|
-
const periodCalculator = new PeriodCalculator();
|
|
710
|
-
const marketStatusService = new MarketStatusService();
|
|
711
|
-
const timeFormatter = new TimeFormatter();
|
|
712
|
-
/**
|
|
713
|
-
* Get market open/close times for a given date
|
|
714
|
-
*/
|
|
569
|
+
// API exports
|
|
715
570
|
function getMarketOpenClose(options = {}) {
|
|
716
571
|
const { date = new Date() } = options;
|
|
717
|
-
return
|
|
572
|
+
return getMarketTimes(date);
|
|
718
573
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Get start and end dates for a given market time period
|
|
721
|
-
*/
|
|
722
574
|
function getStartAndEndDates(params = {}) {
|
|
723
|
-
const { start, end } =
|
|
575
|
+
const { start, end } = getMarketTimePeriod(params);
|
|
724
576
|
return {
|
|
725
|
-
start: new Date(start),
|
|
726
|
-
end: new Date(end),
|
|
577
|
+
start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
|
|
578
|
+
end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
|
|
727
579
|
};
|
|
728
580
|
}
|
|
729
581
|
/**
|
|
730
|
-
*
|
|
582
|
+
* Returns the last full trading date as a Date object.
|
|
731
583
|
*/
|
|
732
584
|
function getLastFullTradingDate(currentDate = new Date()) {
|
|
733
|
-
|
|
734
|
-
return {
|
|
735
|
-
date,
|
|
736
|
-
YYYYMMDD: timeFormatter.getTradingDate(date),
|
|
737
|
-
};
|
|
585
|
+
return getLastFullTradingDateImpl(currentDate);
|
|
738
586
|
}
|
|
739
|
-
/**
|
|
740
|
-
* Get the next market day
|
|
741
|
-
*/
|
|
742
587
|
function getNextMarketDay({ referenceDate } = {}) {
|
|
588
|
+
const calendar = new MarketCalendar();
|
|
743
589
|
const startDate = referenceDate || new Date();
|
|
744
|
-
const nextDate =
|
|
745
|
-
|
|
746
|
-
const startOfDayNY = dateFns.startOfDay(dateFnsTz.toZonedTime(nextDate, MARKET_CONFIG.TIMEZONE));
|
|
747
|
-
const dateInET = dateFnsTz.fromZonedTime(startOfDayNY, MARKET_CONFIG.TIMEZONE);
|
|
590
|
+
const nextDate = calendar.getNextMarketDay(startDate);
|
|
591
|
+
const yyyymmdd = `${nextDate.getUTCFullYear()}-${String(nextDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nextDate.getUTCDate()).padStart(2, '0')}`;
|
|
748
592
|
return {
|
|
749
|
-
date:
|
|
750
|
-
yyyymmdd
|
|
751
|
-
dateISOString:
|
|
593
|
+
date: nextDate,
|
|
594
|
+
yyyymmdd,
|
|
595
|
+
dateISOString: nextDate.toISOString(),
|
|
752
596
|
};
|
|
753
597
|
}
|
|
754
598
|
/**
|
|
755
|
-
*
|
|
599
|
+
* Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
|
|
600
|
+
* @param time - a string, number (unix timestamp), or Date object representing the time
|
|
601
|
+
* @returns the trading date as a string in YYYY-MM-DD format
|
|
756
602
|
*/
|
|
757
603
|
function getTradingDate(time) {
|
|
758
|
-
|
|
604
|
+
const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
|
|
605
|
+
const nyDate = toNYTime(date);
|
|
606
|
+
return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
|
|
759
607
|
}
|
|
760
|
-
/**
|
|
761
|
-
* Get New York timezone offset
|
|
762
|
-
*/
|
|
763
608
|
function getNYTimeZone(date) {
|
|
764
|
-
|
|
609
|
+
const offset = getNYOffset(date || new Date());
|
|
610
|
+
return offset === -4 ? '-04:00' : '-05:00';
|
|
765
611
|
}
|
|
766
|
-
/**
|
|
767
|
-
* Get current market status
|
|
768
|
-
*/
|
|
769
612
|
function getMarketStatus(options = {}) {
|
|
770
613
|
const { date = new Date() } = options;
|
|
771
|
-
return
|
|
614
|
+
return getMarketStatusImpl(date);
|
|
772
615
|
}
|
|
773
|
-
/**
|
|
774
|
-
* Check if a date is a market day
|
|
775
|
-
*/
|
|
776
|
-
function isMarketDay(date) {
|
|
777
|
-
return marketCalendar.isMarketDay(date);
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Check if a date is within market hours
|
|
781
|
-
*/
|
|
782
616
|
function isWithinMarketHours(date, intradayReporting = 'market_hours') {
|
|
783
|
-
return
|
|
617
|
+
return isWithinMarketHoursImpl(date, intradayReporting);
|
|
784
618
|
}
|
|
785
619
|
/**
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
* and to go back x days before, e.g. 7 would just subtract raw 7 days, giving us 4-5 trading days (depending on holidays).
|
|
790
|
-
* @param options.endDate - The end date to use, defaults to today
|
|
791
|
-
* @param options.days - The number of days to go back, defaults to 1
|
|
792
|
-
* @returns The start and end dates with proper market open/close times
|
|
620
|
+
* Returns full trading days from market open to market close.
|
|
621
|
+
* endDate is always the most recent market close (previous day's close if before open, today's close if after open).
|
|
622
|
+
* days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
|
|
793
623
|
*/
|
|
794
624
|
function getTradingStartAndEndDates(options = {}) {
|
|
795
625
|
const { endDate = new Date(), days = 1 } = options;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
let startDate;
|
|
808
|
-
if (days <= 1) {
|
|
809
|
-
// For 1 day, start is market open of the same trading day as end
|
|
810
|
-
startDate = getMarketOpenClose({ date: endTradingDate }).open;
|
|
626
|
+
const calendar = new MarketCalendar();
|
|
627
|
+
// Find the most recent market close
|
|
628
|
+
let endMarketDay = endDate;
|
|
629
|
+
const nyEnd = toNYTime(endDate);
|
|
630
|
+
const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
631
|
+
const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
632
|
+
const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
|
|
633
|
+
if (!calendar.isMarketDay(endDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
|
|
634
|
+
// Before market open, not a market day, or during market hours: use previous market day
|
|
635
|
+
endMarketDay = calendar.getPreviousMarketDay(endDate);
|
|
811
636
|
}
|
|
812
637
|
else {
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
638
|
+
// After market close: use today
|
|
639
|
+
endMarketDay = endDate;
|
|
640
|
+
}
|
|
641
|
+
// Get market close for endMarketDay
|
|
642
|
+
const endClose = getMarketOpenClose({ date: endMarketDay }).close;
|
|
643
|
+
// Find start market day by iterating back over market days
|
|
644
|
+
let startMarketDay = endMarketDay;
|
|
645
|
+
let count = Math.max(1, days);
|
|
646
|
+
for (let i = 1; i < count; i++) {
|
|
647
|
+
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
648
|
+
}
|
|
649
|
+
// If days > 1, we need to go back (days-1) market days from endMarketDay
|
|
650
|
+
if (days > 1) {
|
|
651
|
+
startMarketDay = endMarketDay;
|
|
652
|
+
for (let i = 1; i < days; i++) {
|
|
653
|
+
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
|
|
657
|
+
return { startDate: startOpen, endDate: endClose };
|
|
827
658
|
}
|
|
828
|
-
// Export
|
|
659
|
+
// Export MARKET_TIMES for compatibility
|
|
829
660
|
const MARKET_TIMES = {
|
|
830
661
|
TIMEZONE: MARKET_CONFIG.TIMEZONE,
|
|
831
662
|
PRE: {
|
|
@@ -1739,7 +1570,7 @@ symbol, date = new Date(), options) => {
|
|
|
1739
1570
|
* @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
|
|
1740
1571
|
*/
|
|
1741
1572
|
async function getPreviousClose(symbol, referenceDate, options) {
|
|
1742
|
-
const previousDate = getLastFullTradingDate(referenceDate)
|
|
1573
|
+
const previousDate = getLastFullTradingDate(referenceDate);
|
|
1743
1574
|
const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
|
|
1744
1575
|
if (!lastOpenClose) {
|
|
1745
1576
|
throw new Error(`Could not fetch last trade price for ${symbol}`);
|
|
@@ -16320,8 +16151,8 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16320
16151
|
const response = await this.getHistoricalBars({
|
|
16321
16152
|
symbols: [symbol],
|
|
16322
16153
|
timeframe: '1Day',
|
|
16323
|
-
start: prevMarketDate.
|
|
16324
|
-
end: prevMarketDate.
|
|
16154
|
+
start: prevMarketDate.toISOString(),
|
|
16155
|
+
end: prevMarketDate.toISOString(),
|
|
16325
16156
|
limit: 1,
|
|
16326
16157
|
});
|
|
16327
16158
|
if (!response.bars[symbol] || response.bars[symbol].length === 0) {
|