@discomedia/utils 1.0.19 → 1.0.21

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/test.js CHANGED
@@ -146,14 +146,25 @@ const marketEarlyCloses = {
146
146
 
147
147
  // Constants for NY market times (Eastern Time)
148
148
  const MARKET_CONFIG = {
149
+ TIMEZONE: 'America/New_York',
149
150
  UTC_OFFSET_STANDARD: -5, // EST
150
151
  UTC_OFFSET_DST: -4, // EDT
151
152
  TIMES: {
153
+ EXTENDED_START: { hour: 4, minute: 0 },
152
154
  MARKET_OPEN: { hour: 9, minute: 30 },
155
+ EARLY_MARKET_END: { hour: 10, minute: 0 },
153
156
  MARKET_CLOSE: { hour: 16, minute: 0 },
154
- EARLY_CLOSE: { hour: 13, minute: 0 }},
157
+ EARLY_CLOSE: { hour: 13, minute: 0 },
158
+ EXTENDED_END: { hour: 20, minute: 0 },
159
+ EARLY_EXTENDED_END: { hour: 17, minute: 0 },
160
+ },
155
161
  };
156
162
  // Helper: Get NY offset for a given UTC date (DST rules for US)
163
+ /**
164
+ * Returns the NY timezone offset (in hours) for a given UTC date, accounting for US DST rules.
165
+ * @param date - UTC date
166
+ * @returns offset in hours (-5 for EST, -4 for EDT)
167
+ */
157
168
  function getNYOffset(date) {
158
169
  // US DST starts 2nd Sunday in March, ends 1st Sunday in November
159
170
  const year = date.getUTCFullYear();
@@ -166,6 +177,14 @@ function getNYOffset(date) {
166
177
  return MARKET_CONFIG.UTC_OFFSET_STANDARD;
167
178
  }
168
179
  // Helper: Get nth weekday of month in UTC
180
+ /**
181
+ * Returns the nth weekday of a given month in UTC.
182
+ * @param year - Year
183
+ * @param month - 1-based month (e.g. March = 3)
184
+ * @param weekday - 0=Sunday, 1=Monday, ...
185
+ * @param n - nth occurrence
186
+ * @returns Date object for the nth weekday
187
+ */
169
188
  function getNthWeekdayOfMonth(year, month, weekday, n) {
170
189
  let count = 0;
171
190
  for (let d = 1; d <= 31; d++) {
@@ -182,6 +201,11 @@ function getNthWeekdayOfMonth(year, month, weekday, n) {
182
201
  return new Date(Date.UTC(year, month - 1, 28));
183
202
  }
184
203
  // Helper: Convert UTC date to NY time (returns new Date object)
204
+ /**
205
+ * Converts a UTC date to NY time (returns a new Date object).
206
+ * @param date - UTC date
207
+ * @returns Date object in NY time
208
+ */
185
209
  function toNYTime(date) {
186
210
  const offset = getNYOffset(date);
187
211
  // NY offset in hours
@@ -190,18 +214,63 @@ function toNYTime(date) {
190
214
  return new Date(nyMillis);
191
215
  }
192
216
  // Helper: Convert NY time to UTC (returns new Date object)
217
+ /**
218
+ * Converts a NY time date to UTC (returns a new Date object).
219
+ * @param date - NY time date
220
+ * @returns Date object in UTC
221
+ */
193
222
  function fromNYTime(date) {
194
223
  const offset = getNYOffset(date);
195
224
  const nyMillis = date.getTime();
196
225
  const utcMillis = nyMillis - offset * 60 * 60 * 1000;
197
226
  return new Date(utcMillis);
198
227
  }
228
+ // Helper: Format date in ISO, unix, etc.
229
+ /**
230
+ * Formats a date in ISO, unix-seconds, or unix-ms format.
231
+ * @param date - Date object
232
+ * @param outputFormat - Output format ('iso', 'unix-seconds', 'unix-ms')
233
+ * @returns Formatted date string or number
234
+ */
235
+ function formatDate(date, outputFormat = 'iso') {
236
+ switch (outputFormat) {
237
+ case 'unix-seconds':
238
+ return Math.floor(date.getTime() / 1000);
239
+ case 'unix-ms':
240
+ return date.getTime();
241
+ case 'iso':
242
+ default:
243
+ return date.toISOString();
244
+ }
245
+ }
246
+ // Helper: Format date in NY locale string
247
+ /**
248
+ * Formats a date in NY locale string.
249
+ * @param date - Date object
250
+ * @returns NY locale string
251
+ */
252
+ function formatNYLocale(date) {
253
+ return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
254
+ }
199
255
  // Market calendar logic
256
+ /**
257
+ * Market calendar logic for holidays, weekends, and market days.
258
+ */
200
259
  class MarketCalendar {
260
+ /**
261
+ * Checks if a date is a weekend in NY time.
262
+ * @param date - Date object
263
+ * @returns true if weekend, false otherwise
264
+ */
201
265
  isWeekend(date) {
202
266
  const day = toNYTime(date).getUTCDay();
203
267
  return day === 0 || day === 6;
204
268
  }
269
+ /**
270
+ * Checks if a date is a market holiday in NY time.
271
+ * @param date - Date object
272
+ * @returns true if holiday, false otherwise
273
+ */
205
274
  isHoliday(date) {
206
275
  const nyDate = toNYTime(date);
207
276
  const year = nyDate.getUTCFullYear();
@@ -211,8 +280,13 @@ class MarketCalendar {
211
280
  const yearHolidays = marketHolidays[year];
212
281
  if (!yearHolidays)
213
282
  return false;
214
- return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
283
+ return Object.values(yearHolidays).some((holiday) => holiday.date === formattedDate);
215
284
  }
285
+ /**
286
+ * Checks if a date is an early close day in NY time.
287
+ * @param date - Date object
288
+ * @returns true if early close, false otherwise
289
+ */
216
290
  isEarlyCloseDay(date) {
217
291
  const nyDate = toNYTime(date);
218
292
  const year = nyDate.getUTCFullYear();
@@ -222,6 +296,11 @@ class MarketCalendar {
222
296
  const yearEarlyCloses = marketEarlyCloses[year];
223
297
  return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
224
298
  }
299
+ /**
300
+ * Gets the early close time (in minutes from midnight) for a given date.
301
+ * @param date - Date object
302
+ * @returns minutes from midnight or null
303
+ */
225
304
  getEarlyCloseTime(date) {
226
305
  const nyDate = toNYTime(date);
227
306
  const year = nyDate.getUTCFullYear();
@@ -235,9 +314,19 @@ class MarketCalendar {
235
314
  }
236
315
  return null;
237
316
  }
317
+ /**
318
+ * Checks if a date is a market day (not weekend or holiday).
319
+ * @param date - Date object
320
+ * @returns true if market day, false otherwise
321
+ */
238
322
  isMarketDay(date) {
239
323
  return !this.isWeekend(date) && !this.isHoliday(date);
240
324
  }
325
+ /**
326
+ * Gets the next market day after the given date.
327
+ * @param date - Date object
328
+ * @returns Date object for next market day
329
+ */
241
330
  getNextMarketDay(date) {
242
331
  let nextDay = new Date(date.getTime() + 24 * 60 * 60 * 1000);
243
332
  while (!this.isMarketDay(nextDay)) {
@@ -245,6 +334,11 @@ class MarketCalendar {
245
334
  }
246
335
  return nextDay;
247
336
  }
337
+ /**
338
+ * Gets the previous market day before the given date.
339
+ * @param date - Date object
340
+ * @returns Date object for previous market day
341
+ */
248
342
  getPreviousMarketDay(date) {
249
343
  let prevDay = new Date(date.getTime() - 24 * 60 * 60 * 1000);
250
344
  while (!this.isMarketDay(prevDay)) {
@@ -253,7 +347,88 @@ class MarketCalendar {
253
347
  return prevDay;
254
348
  }
255
349
  }
350
+ // Market open/close times
351
+ /**
352
+ * Returns market open/close times for a given date, including extended and early closes.
353
+ * @param date - Date object
354
+ * @returns MarketOpenCloseResult
355
+ */
356
+ function getMarketTimes(date) {
357
+ const calendar = new MarketCalendar();
358
+ const nyDate = toNYTime(date);
359
+ if (!calendar.isMarketDay(date)) {
360
+ return {
361
+ marketOpen: false,
362
+ open: null,
363
+ close: null,
364
+ openExt: null,
365
+ closeExt: null,
366
+ };
367
+ }
368
+ const year = nyDate.getUTCFullYear();
369
+ const month = nyDate.getUTCMonth();
370
+ const day = nyDate.getUTCDate();
371
+ // Helper to build NY time for a given hour/minute
372
+ function buildNYTime(hour, minute) {
373
+ const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
374
+ return fromNYTime(d);
375
+ }
376
+ let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
377
+ let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
378
+ let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
379
+ let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
380
+ if (calendar.isEarlyCloseDay(date)) {
381
+ close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
382
+ closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
383
+ }
384
+ return {
385
+ marketOpen: true,
386
+ open,
387
+ close,
388
+ openExt,
389
+ closeExt,
390
+ };
391
+ }
392
+ // Is within market hours
393
+ /**
394
+ * Checks if a date/time is within market hours, extended hours, or continuous.
395
+ * @param date - Date object
396
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
397
+ * @returns true if within hours, false otherwise
398
+ */
399
+ function isWithinMarketHours(date, intradayReporting = 'market_hours') {
400
+ const calendar = new MarketCalendar();
401
+ if (!calendar.isMarketDay(date))
402
+ return false;
403
+ const nyDate = toNYTime(date);
404
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
405
+ switch (intradayReporting) {
406
+ case 'extended_hours': {
407
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
408
+ if (calendar.isEarlyCloseDay(date)) {
409
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
410
+ }
411
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
412
+ return minutes >= startMinutes && minutes <= endMinutes;
413
+ }
414
+ case 'continuous':
415
+ return true;
416
+ default: {
417
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
418
+ if (calendar.isEarlyCloseDay(date)) {
419
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
420
+ }
421
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
422
+ return minutes >= startMinutes && minutes <= endMinutes;
423
+ }
424
+ }
425
+ }
256
426
  // Get last full trading date
427
+ /**
428
+ * Returns the last full trading date (market close) for a given date.
429
+ * @param currentDate - Date object (default: now)
430
+ * @returns Date object for last full trading date
431
+ */
257
432
  function getLastFullTradingDateImpl(currentDate = new Date()) {
258
433
  const calendar = new MarketCalendar();
259
434
  const nyDate = toNYTime(currentDate);
@@ -264,7 +439,9 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
264
439
  marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
265
440
  }
266
441
  // If not a market day, or before open, or during market hours, return previous market day's close
267
- if (!calendar.isMarketDay(currentDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
442
+ if (!calendar.isMarketDay(currentDate) ||
443
+ minutes < marketOpenMinutes ||
444
+ (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
268
445
  const prevMarketDay = calendar.getPreviousMarketDay(currentDate);
269
446
  let prevCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
270
447
  if (calendar.isEarlyCloseDay(prevMarketDay)) {
@@ -285,23 +462,478 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
285
462
  const closeMinute = marketCloseMinutes % 60;
286
463
  return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
287
464
  }
465
+ // Get day boundaries
466
+ /**
467
+ * Returns the start and end boundaries for a market day, extended hours, or continuous.
468
+ * @param date - Date object
469
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
470
+ * @returns Object with start and end Date
471
+ */
472
+ function getDayBoundaries(date, intradayReporting = 'market_hours') {
473
+ const calendar = new MarketCalendar();
474
+ const nyDate = toNYTime(date);
475
+ const year = nyDate.getUTCFullYear();
476
+ const month = nyDate.getUTCMonth();
477
+ const day = nyDate.getUTCDate();
478
+ function buildNYTime(hour, minute, sec = 0, ms = 0) {
479
+ const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
480
+ return fromNYTime(d);
481
+ }
482
+ let start;
483
+ let end;
484
+ switch (intradayReporting) {
485
+ case 'extended_hours':
486
+ start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
487
+ end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
488
+ if (calendar.isEarlyCloseDay(date)) {
489
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
490
+ }
491
+ break;
492
+ case 'continuous':
493
+ start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
494
+ end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
495
+ break;
496
+ default:
497
+ start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
498
+ end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
499
+ if (calendar.isEarlyCloseDay(date)) {
500
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
501
+ }
502
+ break;
503
+ }
504
+ return { start, end };
505
+ }
506
+ // Period calculator
507
+ /**
508
+ * Calculates the start date for a given period ending at endDate.
509
+ * @param endDate - Date object
510
+ * @param period - Period string
511
+ * @returns Date object for period start
512
+ */
513
+ function calculatePeriodStartDate(endDate, period) {
514
+ const calendar = new MarketCalendar();
515
+ let startDate;
516
+ switch (period) {
517
+ case 'YTD':
518
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
519
+ break;
520
+ case '1D':
521
+ startDate = calendar.getPreviousMarketDay(endDate);
522
+ break;
523
+ case '3D':
524
+ startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
525
+ break;
526
+ case '1W':
527
+ startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
528
+ break;
529
+ case '2W':
530
+ startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
531
+ break;
532
+ case '1M':
533
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
534
+ break;
535
+ case '3M':
536
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
537
+ break;
538
+ case '6M':
539
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
540
+ break;
541
+ case '1Y':
542
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
543
+ break;
544
+ default:
545
+ throw new Error(`Invalid period: ${period}`);
546
+ }
547
+ // Ensure start date is a market day
548
+ while (!calendar.isMarketDay(startDate)) {
549
+ startDate = calendar.getNextMarketDay(startDate);
550
+ }
551
+ return startDate;
552
+ }
553
+ // Get market time period
554
+ /**
555
+ * Returns the start and end dates for a market time period.
556
+ * @param params - MarketTimeParams
557
+ * @returns PeriodDates object
558
+ */
559
+ function getMarketTimePeriod(params) {
560
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso' } = params;
561
+ if (!period)
562
+ throw new Error('Period is required');
563
+ const calendar = new MarketCalendar();
564
+ const nyEndDate = toNYTime(end);
565
+ let endDate;
566
+ const isCurrentMarketDay = calendar.isMarketDay(end);
567
+ const isWithinHours = isWithinMarketHours(end, intraday_reporting);
568
+ const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
569
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
570
+ if (isCurrentMarketDay) {
571
+ if (minutes < marketStartMinutes) {
572
+ // Before market open - use previous day's close
573
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
574
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
575
+ endDate = dayEnd;
576
+ }
577
+ else if (isWithinHours) {
578
+ // During market hours - use current time
579
+ endDate = end;
580
+ }
581
+ else {
582
+ // After market close - use today's close
583
+ const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
584
+ endDate = dayEnd;
585
+ }
586
+ }
587
+ else {
588
+ // Not a market day - use previous market day's close
589
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
590
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
591
+ endDate = dayEnd;
592
+ }
593
+ // Calculate start date
594
+ const periodStartDate = calculatePeriodStartDate(endDate, period);
595
+ const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
596
+ if (endDate.getTime() < dayStart.getTime()) {
597
+ throw new Error('Start date cannot be after end date');
598
+ }
599
+ return {
600
+ start: formatDate(dayStart, outputFormat),
601
+ end: formatDate(endDate, outputFormat),
602
+ };
603
+ }
604
+ // Market status
605
+ /**
606
+ * Returns the current market status for a given date.
607
+ * @param date - Date object (default: now)
608
+ * @returns MarketStatus object
609
+ */
610
+ function getMarketStatusImpl(date = new Date()) {
611
+ const calendar = new MarketCalendar();
612
+ const nyDate = toNYTime(date);
613
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
614
+ const isMarketDay = calendar.isMarketDay(date);
615
+ const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
616
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
617
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
618
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
619
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
620
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
621
+ if (isEarlyCloseDay) {
622
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
623
+ extendedEndMinutes =
624
+ MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
625
+ }
626
+ let status;
627
+ let nextStatus;
628
+ let nextStatusTime;
629
+ let marketPeriod;
630
+ if (!isMarketDay) {
631
+ status = 'closed';
632
+ nextStatus = 'extended hours';
633
+ marketPeriod = 'closed';
634
+ const nextMarketDay = calendar.getNextMarketDay(date);
635
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
636
+ }
637
+ else if (minutes < extendedStartMinutes) {
638
+ status = 'closed';
639
+ nextStatus = 'extended hours';
640
+ marketPeriod = 'closed';
641
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
642
+ }
643
+ else if (minutes < marketStartMinutes) {
644
+ status = 'extended hours';
645
+ nextStatus = 'open';
646
+ marketPeriod = 'preMarket';
647
+ nextStatusTime = getDayBoundaries(date, 'market_hours').start;
648
+ }
649
+ else if (minutes < marketCloseMinutes) {
650
+ status = 'open';
651
+ nextStatus = 'extended hours';
652
+ marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
653
+ nextStatusTime = getDayBoundaries(date, 'market_hours').end;
654
+ }
655
+ else if (minutes < extendedEndMinutes) {
656
+ status = 'extended hours';
657
+ nextStatus = 'closed';
658
+ marketPeriod = 'afterMarket';
659
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
660
+ }
661
+ else {
662
+ status = 'closed';
663
+ nextStatus = 'extended hours';
664
+ marketPeriod = 'closed';
665
+ const nextMarketDay = calendar.getNextMarketDay(date);
666
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
667
+ }
668
+ const nextStatusTimeDifference = nextStatusTime.getTime() - nyDate.getTime();
669
+ return {
670
+ time: date,
671
+ timeString: formatNYLocale(nyDate),
672
+ status,
673
+ nextStatus,
674
+ marketPeriod,
675
+ nextStatusTime,
676
+ nextStatusTimeDifference,
677
+ nextStatusTimeString: formatNYLocale(nextStatusTime),
678
+ };
679
+ }
680
+ // API exports
681
+ /**
682
+ * Returns market open/close times for a given date.
683
+ * @param options - { date?: Date }
684
+ * @returns MarketOpenCloseResult
685
+ */
686
+ function getMarketOpenClose(options = {}) {
687
+ const { date = new Date() } = options;
688
+ return getMarketTimes(date);
689
+ }
690
+ /**
691
+ * Returns the start and end dates for a market time period as Date objects.
692
+ * @param params - MarketTimeParams
693
+ * @returns Object with start and end Date
694
+ */
695
+ function getStartAndEndDates(params = {}) {
696
+ const { start, end } = getMarketTimePeriod(params);
697
+ return {
698
+ start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
699
+ end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
700
+ };
701
+ }
702
+ /**
703
+ * Returns the last full trading date as a Date object.
704
+ */
288
705
  /**
289
706
  * Returns the last full trading date as a Date object.
707
+ * @param currentDate - Date object (default: now)
708
+ * @returns Date object for last full trading date
290
709
  */
291
710
  function getLastFullTradingDate(currentDate = new Date()) {
292
711
  return getLastFullTradingDateImpl(currentDate);
293
712
  }
713
+ /**
714
+ * Returns the next market day after the reference date.
715
+ * @param referenceDate - Date object (default: now)
716
+ * @returns Object with date, yyyymmdd string, and ISO string
717
+ */
294
718
  function getNextMarketDay({ referenceDate } = {}) {
295
719
  const calendar = new MarketCalendar();
296
- const startDate = referenceDate || new Date();
720
+ const startDate = referenceDate ?? new Date();
721
+ // Find the next trading day (UTC Date object)
297
722
  const nextDate = calendar.getNextMarketDay(startDate);
298
- const yyyymmdd = `${nextDate.getUTCFullYear()}-${String(nextDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nextDate.getUTCDate()).padStart(2, '0')}`;
723
+ // Convert to NY time before extracting Y-M-D parts
724
+ const nyNext = toNYTime(nextDate);
725
+ const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, '0')}-${String(nyNext.getUTCDate()).padStart(2, '0')}`;
299
726
  return {
300
- date: nextDate,
301
- yyyymmdd,
727
+ date: nextDate, // raw Date, unchanged
728
+ yyyymmdd, // correct trading date string
302
729
  dateISOString: nextDate.toISOString(),
303
730
  };
304
731
  }
732
+ /**
733
+ * Returns the previous market day before the reference date.
734
+ * @param referenceDate - Date object (default: now)
735
+ * @returns Object with date, yyyymmdd string, and ISO string
736
+ */
737
+ function getPreviousMarketDay({ referenceDate } = {}) {
738
+ const calendar = new MarketCalendar();
739
+ const startDate = referenceDate || new Date();
740
+ const prevDate = calendar.getPreviousMarketDay(startDate);
741
+ // convert to NY time first
742
+ const nyPrev = toNYTime(prevDate); // ← already in this file
743
+ const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, '0')}-${String(nyPrev.getUTCDate()).padStart(2, '0')}`;
744
+ return {
745
+ date: prevDate,
746
+ yyyymmdd,
747
+ dateISOString: prevDate.toISOString(),
748
+ };
749
+ }
750
+ /**
751
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
752
+ * @param time - a string, number (unix timestamp), or Date object representing the time
753
+ * @returns the trading date as a string in YYYY-MM-DD format
754
+ */
755
+ /**
756
+ * Returns the trading date for a given time in YYYY-MM-DD format (NY time).
757
+ * @param time - string, number, or Date
758
+ * @returns trading date string
759
+ */
760
+ function getTradingDate(time) {
761
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
762
+ const nyDate = toNYTime(date);
763
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
764
+ }
765
+ /**
766
+ * Returns the NY timezone offset string for a given date.
767
+ * @param date - Date object (default: now)
768
+ * @returns '-04:00' for EDT, '-05:00' for EST
769
+ */
770
+ function getNYTimeZone(date) {
771
+ const offset = getNYOffset(date || new Date());
772
+ return offset === -4 ? '-04:00' : '-05:00';
773
+ }
774
+ /**
775
+ * Returns the current market status for a given date.
776
+ * @param options - { date?: Date }
777
+ * @returns MarketStatus object
778
+ */
779
+ function getMarketStatus(options = {}) {
780
+ const { date = new Date() } = options;
781
+ return getMarketStatusImpl(date);
782
+ }
783
+ /**
784
+ * Checks if a date is a market day.
785
+ * @param date - Date object
786
+ * @returns true if market day, false otherwise
787
+ */
788
+ function isMarketDay(date) {
789
+ const calendar = new MarketCalendar();
790
+ return calendar.isMarketDay(date);
791
+ }
792
+ /**
793
+ * Returns full trading days from market open to market close.
794
+ * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
795
+ * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
796
+ */
797
+ /**
798
+ * Returns full trading days from market open to market close.
799
+ * @param options - { endDate?: Date, days?: number }
800
+ * @returns Object with startDate and endDate
801
+ */
802
+ function getTradingStartAndEndDates(options = {}) {
803
+ const { endDate = new Date(), days = 1 } = options;
804
+ const calendar = new MarketCalendar();
805
+ // Find the most recent market close
806
+ let endMarketDay = endDate;
807
+ const nyEnd = toNYTime(endDate);
808
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
809
+ const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
810
+ const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
811
+ if (!calendar.isMarketDay(endDate) ||
812
+ minutes < marketOpenMinutes ||
813
+ (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
814
+ // Before market open, not a market day, or during market hours: use previous market day
815
+ endMarketDay = calendar.getPreviousMarketDay(endDate);
816
+ }
817
+ else {
818
+ // After market close: use today
819
+ endMarketDay = endDate;
820
+ }
821
+ // Get market close for endMarketDay
822
+ const endClose = getMarketOpenClose({ date: endMarketDay }).close;
823
+ // Find start market day by iterating back over market days
824
+ let startMarketDay = endMarketDay;
825
+ let count = Math.max(1, days);
826
+ for (let i = 1; i < count; i++) {
827
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
828
+ }
829
+ // If days > 1, we need to go back (days-1) market days from endMarketDay
830
+ if (days > 1) {
831
+ startMarketDay = endMarketDay;
832
+ for (let i = 1; i < days; i++) {
833
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
834
+ }
835
+ }
836
+ const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
837
+ return { startDate: startOpen, endDate: endClose };
838
+ }
839
+ /**
840
+ * Counts trading time between two dates (passed as standard Date objects), excluding weekends and holidays, and closed market hours, using other functions in this library.
841
+ *
842
+ * This function calculates the actual trading time between two dates by:
843
+ * 1. Iterating through each calendar day between startDate and endDate (inclusive)
844
+ * 2. For each day that is a market day (not weekend/holiday), getting market open/close times
845
+ * 3. Calculating the overlap between the time range and market hours for that day
846
+ * 4. Summing up all the trading minutes across all days
847
+ *
848
+ * The function automatically handles:
849
+ * - Weekends (Saturday/Sunday) - skipped entirely
850
+ * - Market holidays - skipped entirely
851
+ * - Early close days (e.g. day before holidays) - uses early close time
852
+ * - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
853
+ *
854
+ * Examples:
855
+ * - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
856
+ * - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
857
+ * - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
858
+ *
859
+ * @param startDate - Start date/time
860
+ * @param endDate - End date/time (default: now)
861
+ * @returns Object containing:
862
+ * - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
863
+ * - hours: Trading time in hours
864
+ * - minutes: Trading time in minutes
865
+ */
866
+ function countTradingDays(startDate, endDate = new Date()) {
867
+ const calendar = new MarketCalendar();
868
+ // Ensure start is before end
869
+ if (startDate.getTime() > endDate.getTime()) {
870
+ throw new Error('Start date must be before end date');
871
+ }
872
+ let totalMinutes = 0;
873
+ // Get the NY dates for iteration
874
+ const startNY = toNYTime(startDate);
875
+ const endNY = toNYTime(endDate);
876
+ // Create date at start of first day (in NY time)
877
+ const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
878
+ // Iterate through each calendar day
879
+ while (currentNY.getTime() <= endNY.getTime()) {
880
+ const currentUTC = fromNYTime(currentNY);
881
+ // Check if this is a market day
882
+ if (calendar.isMarketDay(currentUTC)) {
883
+ // Get market hours for this day
884
+ const marketTimes = getMarketTimes(currentUTC);
885
+ if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
886
+ // Calculate the overlap between our time range and market hours
887
+ const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
888
+ const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
889
+ // Only count if there's actual overlap
890
+ if (dayStart < dayEnd) {
891
+ totalMinutes += (dayEnd - dayStart) / (1000 * 60);
892
+ }
893
+ }
894
+ }
895
+ // Move to next day
896
+ currentNY.setUTCDate(currentNY.getUTCDate() + 1);
897
+ }
898
+ // Convert to days, hours, minutes
899
+ const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
900
+ const days = totalMinutes / MINUTES_PER_TRADING_DAY;
901
+ const hours = totalMinutes / 60;
902
+ const minutes = totalMinutes;
903
+ return {
904
+ days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
905
+ hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
906
+ minutes: Math.round(minutes),
907
+ };
908
+ }
909
+ // Export MARKET_TIMES for compatibility
910
+ const MARKET_TIMES = {
911
+ TIMEZONE: MARKET_CONFIG.TIMEZONE,
912
+ PRE: {
913
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
914
+ END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
915
+ },
916
+ EARLY_MORNING: {
917
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
918
+ END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
919
+ },
920
+ EARLY_CLOSE_BEFORE_HOLIDAY: {
921
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
922
+ END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
923
+ },
924
+ EARLY_EXTENDED_BEFORE_HOLIDAY: {
925
+ START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
926
+ END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
927
+ },
928
+ REGULAR: {
929
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
930
+ END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
931
+ },
932
+ EXTENDED: {
933
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
934
+ END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
935
+ },
936
+ };
305
937
 
306
938
  /*
307
939
  How it works:
@@ -380,6 +1012,12 @@ class Queue {
380
1012
  current = current.next;
381
1013
  }
382
1014
  }
1015
+
1016
+ * drain() {
1017
+ while (this.#head) {
1018
+ yield this.dequeue();
1019
+ }
1020
+ }
383
1021
  }
384
1022
 
385
1023
  function pLimit(concurrency) {
@@ -729,7 +1367,7 @@ const safeJSON = (text) => {
729
1367
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
730
1368
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
731
1369
 
732
- const VERSION = '5.10.1'; // x-release-please-version
1370
+ const VERSION = '5.10.2'; // x-release-please-version
733
1371
 
734
1372
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
735
1373
  const isRunningInBrowser = () => {
@@ -12163,7 +12801,7 @@ var config = {};
12163
12801
 
12164
12802
  var main = {exports: {}};
12165
12803
 
12166
- var version = "17.2.0";
12804
+ var version = "17.2.1";
12167
12805
  var require$$4 = {
12168
12806
  version: version};
12169
12807
 
@@ -12182,9 +12820,12 @@ function requireMain () {
12182
12820
 
12183
12821
  // Array of tips to display randomly
12184
12822
  const TIPS = [
12185
- '🔐 encrypt with dotenvx: https://dotenvx.com',
12823
+ '🔐 encrypt with Dotenvx: https://dotenvx.com',
12186
12824
  '🔐 prevent committing .env to code: https://dotenvx.com/precommit',
12187
12825
  '🔐 prevent building .env in docker: https://dotenvx.com/prebuild',
12826
+ '📡 observe env with Radar: https://dotenvx.com/radar',
12827
+ '📡 auto-backup env with Radar: https://dotenvx.com/radar',
12828
+ '📡 version env with Radar: https://dotenvx.com/radar',
12188
12829
  '🛠️ run anywhere with `dotenvx run -- yourcommand`',
12189
12830
  '⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }',
12190
12831
  '⚙️ enable debug logging with { debug: true }',
@@ -12485,7 +13126,7 @@ function requireMain () {
12485
13126
  }
12486
13127
  }
12487
13128
 
12488
- _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`(tip: ${_getRandomTip()})`)}`);
13129
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`-- tip: ${_getRandomTip()}`)}`);
12489
13130
  }
12490
13131
 
12491
13132
  if (lastError) {
@@ -13588,12 +14229,25 @@ AlpacaMarketDataAPI.getInstance();
13588
14229
 
13589
14230
  const disco = {
13590
14231
  time: {
13591
- getNextMarketDay: getNextMarketDay}};
14232
+ getStartAndEndDates: getStartAndEndDates,
14233
+ getMarketOpenClose: getMarketOpenClose,
14234
+ getLastFullTradingDate: getLastFullTradingDate,
14235
+ getNextMarketDay: getNextMarketDay,
14236
+ getPreviousMarketDay: getPreviousMarketDay,
14237
+ getMarketTimePeriod: getMarketTimePeriod,
14238
+ getMarketStatus: getMarketStatus,
14239
+ getNYTimeZone: getNYTimeZone,
14240
+ getTradingDate: getTradingDate,
14241
+ getTradingStartAndEndDates: getTradingStartAndEndDates,
14242
+ isMarketDay: isMarketDay,
14243
+ isWithinMarketHours: isWithinMarketHours,
14244
+ countTradingDays: countTradingDays,
14245
+ MARKET_TIMES: MARKET_TIMES,
14246
+ }};
13592
14247
 
13593
14248
  // Test file for context functionality
13594
14249
  // Test getNextMarketDay for various scenarios
13595
14250
  function testGetNextMarketDay() {
13596
- const { getNextMarketDay } = disco.time;
13597
14251
  const testCases = [
13598
14252
  {
13599
14253
  label: 'Weekday (Wednesday)',
@@ -13614,9 +14268,27 @@ function testGetNextMarketDay() {
13614
14268
  date: '2025-07-03T15:00:00-04:00',
13615
14269
  expected: '2025-07-07', // Should skip to next market day
13616
14270
  },
14271
+ // Late on 3 July, should skip to next market day
14272
+ {
14273
+ label: 'Late on 3 July (after early close)',
14274
+ date: '2025-07-03T18:00:00-04:00',
14275
+ expected: '2025-07-07', // Should skip to next market day
14276
+ },
14277
+ // during 3 july market hours, should return next monday
14278
+ {
14279
+ label: 'During 3 July market hours',
14280
+ date: '2025-07-03T10:00:00-04:00',
14281
+ expected: '2025-07-07', // Should skip to next market day
14282
+ },
14283
+ // saturday morning, should return next monday
14284
+ {
14285
+ label: 'Saturday morning',
14286
+ date: '2025-07-12T09:00:00-04:00',
14287
+ expected: '2025-07-14', // Should skip to next market day
14288
+ },
13617
14289
  ];
13618
14290
  for (const { label, date, expected } of testCases) {
13619
- const result = getNextMarketDay({ referenceDate: new Date(date) });
14291
+ const result = disco.time.getNextMarketDay({ referenceDate: new Date(date) });
13620
14292
  const yyyymmdd = result.yyyymmdd;
13621
14293
  const pass = yyyymmdd === expected;
13622
14294
  console.log(`\nTest: ${label}`);
@@ -13631,6 +14303,9 @@ function testGetNextMarketDay() {
13631
14303
  // testGetTradingStartAndEndDates();
13632
14304
  // testGetLastFullTradingDate();
13633
14305
  //testGetMarketOpenClose();
14306
+ // Test countTradingDays function
13634
14307
  //testGetNYTimeZone();
13635
14308
  testGetNextMarketDay();
14309
+ //testCountTradingDays();
14310
+ // testGetPreviousMarketDay();
13636
14311
  //# sourceMappingURL=test.js.map