@discomedia/utils 1.0.30 → 1.0.31

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,12 +146,18 @@ 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)
157
163
  /**
@@ -219,6 +225,33 @@ function fromNYTime(date) {
219
225
  const utcMillis = nyMillis - offset * 60 * 60 * 1000;
220
226
  return new Date(utcMillis);
221
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
+ }
222
255
  // Market calendar logic
223
256
  /**
224
257
  * Market calendar logic for holidays, weekends, and market days.
@@ -314,6 +347,82 @@ class MarketCalendar {
314
347
  return prevDay;
315
348
  }
316
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
+ }
317
426
  // Get last full trading date
318
427
  /**
319
428
  * Returns the last full trading date (market close) for a given date.
@@ -353,6 +462,244 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
353
462
  const closeMinute = marketCloseMinutes % 60;
354
463
  return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
355
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
+ // I think using nyDate here may be wrong - should use current time? i.e. date.getTime()
669
+ const nextStatusTimeDifference = nextStatusTime.getTime() - date.getTime();
670
+ return {
671
+ time: date,
672
+ timeString: formatNYLocale(nyDate),
673
+ status,
674
+ nextStatus,
675
+ marketPeriod,
676
+ nextStatusTime,
677
+ nextStatusTimeDifference,
678
+ nextStatusTimeString: formatNYLocale(nextStatusTime),
679
+ };
680
+ }
681
+ // API exports
682
+ /**
683
+ * Returns market open/close times for a given date.
684
+ * @param options - { date?: Date }
685
+ * @returns MarketOpenCloseResult
686
+ */
687
+ function getMarketOpenClose(options = {}) {
688
+ const { date = new Date() } = options;
689
+ return getMarketTimes(date);
690
+ }
691
+ /**
692
+ * Returns the start and end dates for a market time period as Date objects.
693
+ * @param params - MarketTimeParams
694
+ * @returns Object with start and end Date
695
+ */
696
+ function getStartAndEndDates(params = {}) {
697
+ const { start, end } = getMarketTimePeriod(params);
698
+ return {
699
+ start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
700
+ end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
701
+ };
702
+ }
356
703
  /**
357
704
  * Returns the last full trading date as a Date object.
358
705
  */
@@ -364,6 +711,230 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
364
711
  function getLastFullTradingDate(currentDate = new Date()) {
365
712
  return getLastFullTradingDateImpl(currentDate);
366
713
  }
714
+ /**
715
+ * Returns the next market day after the reference date.
716
+ * @param referenceDate - Date object (default: now)
717
+ * @returns Object with date, yyyymmdd string, and ISO string
718
+ */
719
+ function getNextMarketDay({ referenceDate } = {}) {
720
+ const calendar = new MarketCalendar();
721
+ const startDate = referenceDate ?? new Date();
722
+ // Find the next trading day (UTC Date object)
723
+ const nextDate = calendar.getNextMarketDay(startDate);
724
+ // Convert to NY time before extracting Y-M-D parts
725
+ const nyNext = toNYTime(nextDate);
726
+ const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, '0')}-${String(nyNext.getUTCDate()).padStart(2, '0')}`;
727
+ return {
728
+ date: nextDate, // raw Date, unchanged
729
+ yyyymmdd, // correct trading date string
730
+ dateISOString: nextDate.toISOString(),
731
+ };
732
+ }
733
+ /**
734
+ * Returns the previous market day before the reference date.
735
+ * @param referenceDate - Date object (default: now)
736
+ * @returns Object with date, yyyymmdd string, and ISO string
737
+ */
738
+ function getPreviousMarketDay({ referenceDate } = {}) {
739
+ const calendar = new MarketCalendar();
740
+ const startDate = referenceDate || new Date();
741
+ const prevDate = calendar.getPreviousMarketDay(startDate);
742
+ // convert to NY time first
743
+ const nyPrev = toNYTime(prevDate); // ← already in this file
744
+ const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, '0')}-${String(nyPrev.getUTCDate()).padStart(2, '0')}`;
745
+ return {
746
+ date: prevDate,
747
+ yyyymmdd,
748
+ dateISOString: prevDate.toISOString(),
749
+ };
750
+ }
751
+ /**
752
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
753
+ * @param time - a string, number (unix timestamp), or Date object representing the time
754
+ * @returns the trading date as a string in YYYY-MM-DD format
755
+ */
756
+ /**
757
+ * Returns the trading date for a given time in YYYY-MM-DD format (NY time).
758
+ * @param time - string, number, or Date
759
+ * @returns trading date string
760
+ */
761
+ function getTradingDate(time) {
762
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
763
+ const nyDate = toNYTime(date);
764
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
765
+ }
766
+ /**
767
+ * Returns the NY timezone offset string for a given date.
768
+ * @param date - Date object (default: now)
769
+ * @returns '-04:00' for EDT, '-05:00' for EST
770
+ */
771
+ function getNYTimeZone(date) {
772
+ const offset = getNYOffset(date || new Date());
773
+ return offset === -4 ? '-04:00' : '-05:00';
774
+ }
775
+ /**
776
+ * Returns the current market status for a given date.
777
+ * @param options - { date?: Date }
778
+ * @returns MarketStatus object
779
+ */
780
+ function getMarketStatus(options = {}) {
781
+ const { date = new Date() } = options;
782
+ return getMarketStatusImpl(date);
783
+ }
784
+ /**
785
+ * Checks if a date is a market day.
786
+ * @param date - Date object
787
+ * @returns true if market day, false otherwise
788
+ */
789
+ function isMarketDay(date) {
790
+ const calendar = new MarketCalendar();
791
+ return calendar.isMarketDay(date);
792
+ }
793
+ /**
794
+ * Returns full trading days from market open to market close.
795
+ * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
796
+ * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
797
+ */
798
+ /**
799
+ * Returns full trading days from market open to market close.
800
+ * @param options - { endDate?: Date, days?: number }
801
+ * @returns Object with startDate and endDate
802
+ */
803
+ function getTradingStartAndEndDates(options = {}) {
804
+ const { endDate = new Date(), days = 1 } = options;
805
+ const calendar = new MarketCalendar();
806
+ // Find the most recent market close
807
+ let endMarketDay = endDate;
808
+ const nyEnd = toNYTime(endDate);
809
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
810
+ const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
811
+ const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
812
+ if (!calendar.isMarketDay(endDate) ||
813
+ minutes < marketOpenMinutes ||
814
+ (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
815
+ // Before market open, not a market day, or during market hours: use previous market day
816
+ endMarketDay = calendar.getPreviousMarketDay(endDate);
817
+ }
818
+ else {
819
+ // After market close: use today
820
+ endMarketDay = endDate;
821
+ }
822
+ // Get market close for endMarketDay
823
+ const endClose = getMarketOpenClose({ date: endMarketDay }).close;
824
+ // Find start market day by iterating back over market days
825
+ let startMarketDay = endMarketDay;
826
+ let count = Math.max(1, days);
827
+ for (let i = 1; i < count; i++) {
828
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
829
+ }
830
+ // If days > 1, we need to go back (days-1) market days from endMarketDay
831
+ if (days > 1) {
832
+ startMarketDay = endMarketDay;
833
+ for (let i = 1; i < days; i++) {
834
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
835
+ }
836
+ }
837
+ const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
838
+ return { startDate: startOpen, endDate: endClose };
839
+ }
840
+ /**
841
+ * 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.
842
+ *
843
+ * This function calculates the actual trading time between two dates by:
844
+ * 1. Iterating through each calendar day between startDate and endDate (inclusive)
845
+ * 2. For each day that is a market day (not weekend/holiday), getting market open/close times
846
+ * 3. Calculating the overlap between the time range and market hours for that day
847
+ * 4. Summing up all the trading minutes across all days
848
+ *
849
+ * The function automatically handles:
850
+ * - Weekends (Saturday/Sunday) - skipped entirely
851
+ * - Market holidays - skipped entirely
852
+ * - Early close days (e.g. day before holidays) - uses early close time
853
+ * - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
854
+ *
855
+ * Examples:
856
+ * - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
857
+ * - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
858
+ * - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
859
+ *
860
+ * @param startDate - Start date/time
861
+ * @param endDate - End date/time (default: now)
862
+ * @returns Object containing:
863
+ * - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
864
+ * - hours: Trading time in hours
865
+ * - minutes: Trading time in minutes
866
+ */
867
+ function countTradingDays(startDate, endDate = new Date()) {
868
+ const calendar = new MarketCalendar();
869
+ // Ensure start is before end
870
+ if (startDate.getTime() > endDate.getTime()) {
871
+ throw new Error('Start date must be before end date');
872
+ }
873
+ let totalMinutes = 0;
874
+ // Get the NY dates for iteration
875
+ const startNY = toNYTime(startDate);
876
+ const endNY = toNYTime(endDate);
877
+ // Create date at start of first day (in NY time)
878
+ const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
879
+ // Iterate through each calendar day
880
+ while (currentNY.getTime() <= endNY.getTime()) {
881
+ const currentUTC = fromNYTime(currentNY);
882
+ // Check if this is a market day
883
+ if (calendar.isMarketDay(currentUTC)) {
884
+ // Get market hours for this day
885
+ const marketTimes = getMarketTimes(currentUTC);
886
+ if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
887
+ // Calculate the overlap between our time range and market hours
888
+ const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
889
+ const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
890
+ // Only count if there's actual overlap
891
+ if (dayStart < dayEnd) {
892
+ totalMinutes += (dayEnd - dayStart) / (1000 * 60);
893
+ }
894
+ }
895
+ }
896
+ // Move to next day
897
+ currentNY.setUTCDate(currentNY.getUTCDate() + 1);
898
+ }
899
+ // Convert to days, hours, minutes
900
+ const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
901
+ const days = totalMinutes / MINUTES_PER_TRADING_DAY;
902
+ const hours = totalMinutes / 60;
903
+ const minutes = totalMinutes;
904
+ return {
905
+ days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
906
+ hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
907
+ minutes: Math.round(minutes),
908
+ };
909
+ }
910
+ // Export MARKET_TIMES for compatibility
911
+ const MARKET_TIMES = {
912
+ TIMEZONE: MARKET_CONFIG.TIMEZONE,
913
+ PRE: {
914
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
915
+ END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
916
+ },
917
+ EARLY_MORNING: {
918
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
919
+ END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
920
+ },
921
+ EARLY_CLOSE_BEFORE_HOLIDAY: {
922
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
923
+ END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
924
+ },
925
+ EARLY_EXTENDED_BEFORE_HOLIDAY: {
926
+ START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
927
+ END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
928
+ },
929
+ REGULAR: {
930
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
931
+ END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
932
+ },
933
+ EXTENDED: {
934
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
935
+ END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
936
+ },
937
+ };
367
938
 
368
939
  /*
369
940
  How it works:
@@ -13675,87 +14246,124 @@ class AlpacaMarketDataAPI extends EventEmitter {
13675
14246
  }
13676
14247
  }
13677
14248
  // Export the singleton instance
13678
- const marketDataAPI = AlpacaMarketDataAPI.getInstance();
14249
+ AlpacaMarketDataAPI.getInstance();
14250
+
14251
+ const disco = {
14252
+ time: {
14253
+ getStartAndEndDates: getStartAndEndDates,
14254
+ getMarketOpenClose: getMarketOpenClose,
14255
+ getLastFullTradingDate: getLastFullTradingDate,
14256
+ getNextMarketDay: getNextMarketDay,
14257
+ getPreviousMarketDay: getPreviousMarketDay,
14258
+ getMarketTimePeriod: getMarketTimePeriod,
14259
+ getMarketStatus: getMarketStatus,
14260
+ getNYTimeZone: getNYTimeZone,
14261
+ getTradingDate: getTradingDate,
14262
+ getTradingStartAndEndDates: getTradingStartAndEndDates,
14263
+ isMarketDay: isMarketDay,
14264
+ isWithinMarketHours: isWithinMarketHours,
14265
+ countTradingDays: countTradingDays,
14266
+ MARKET_TIMES: MARKET_TIMES,
14267
+ }};
13679
14268
 
13680
14269
  // Test file for context functionality
13681
- async function testGetPortfolioDailyHistory() {
13682
- console.log('\n--- Testing getPortfolioDailyHistory with Real API ---');
13683
- const log = (message, options = { type: 'info' }) => {
13684
- log$1(message, { ...options, source: 'Test' });
13685
- };
13686
- if (!process.env.ALPACA_API_KEY || !process.env.ALPACA_SECRET_KEY || !process.env.ALPACA_ACCOUNT_TYPE) {
13687
- console.log('Skipping Alpaca tests: Missing environment variables (ALPACA_API_KEY, ALPACA_SECRET_KEY, ALPACA_ACCOUNT_TYPE)');
13688
- return;
13689
- }
13690
- try {
13691
- // Create real Alpaca API instance with credentials from .env
13692
- const { AlpacaTradingAPI } = await import('./alpaca-trading-api-CPaXTnjf.js');
13693
- const credentials = {
13694
- accountName: 'TestAccount',
13695
- apiKey: process.env.ALPACA_API_KEY,
13696
- apiSecret: process.env.ALPACA_SECRET_KEY,
13697
- type: process.env.ALPACA_ACCOUNT_TYPE,
13698
- orderType: 'limit',
13699
- engine: 'brain'
13700
- };
13701
- const alpacaAPI = new AlpacaTradingAPI(credentials);
13702
- log('Created Alpaca Trading API instance');
13703
- // Test getPortfolioDailyHistory with a short period to get recent data
13704
- log('Calling getPortfolioDailyHistory...');
13705
- const dailyHistoryResult = await alpacaAPI.getPortfolioDailyHistory({
13706
- period: '7D' // Last 7 days
13707
- });
13708
- log('✓ Successfully called getPortfolioDailyHistory');
13709
- console.log('Daily history result:');
13710
- console.log(` Timestamps: ${dailyHistoryResult.timestamp.length} entries`);
13711
- console.log(` First timestamp: ${new Date(dailyHistoryResult.timestamp[0] * 1000).toLocaleString('en-US', { timeZone: 'America/New_York' })}`);
13712
- console.log(` Last timestamp: ${new Date(dailyHistoryResult.timestamp[dailyHistoryResult.timestamp.length - 1] * 1000).toLocaleString('en-US', { timeZone: 'America/New_York' })}`);
13713
- console.log(` Base value: $${dailyHistoryResult.base_value}`);
13714
- console.log(` Latest equity: $${dailyHistoryResult.equity[dailyHistoryResult.equity.length - 1]}`);
13715
- console.log(` Latest P&L: $${dailyHistoryResult.profit_loss[dailyHistoryResult.profit_loss.length - 1]}`);
13716
- console.log(` Latest P&L %: ${dailyHistoryResult.profit_loss_pct[dailyHistoryResult.profit_loss_pct.length - 1]}%`);
13717
- console.log(` Timeframe: ${dailyHistoryResult.timeframe}`);
13718
- // Compare with regular daily history to see if there's a difference
13719
- log('Comparing with regular getPortfolioHistory (daily)...');
13720
- const regularDailyHistory = await alpacaAPI.getPortfolioHistory({
13721
- timeframe: '1D',
13722
- period: '7D'
13723
- });
13724
- const dailyHistoryCount = dailyHistoryResult.timestamp.length;
13725
- const regularDailyCount = regularDailyHistory.timestamp.length;
13726
- console.log(`Regular daily history: ${regularDailyCount} entries`);
13727
- console.log(`Enhanced daily history: ${dailyHistoryCount} entries`);
13728
- if (dailyHistoryCount > regularDailyCount) {
13729
- log('✓ getPortfolioDailyHistory successfully added recent data from hourly history');
13730
- console.log(` Added ${dailyHistoryCount - regularDailyCount} additional day(s)`);
13731
- }
13732
- else if (dailyHistoryCount === regularDailyCount) {
13733
- log('✓ No recent hourly data to merge - both histories have same length');
13734
- }
13735
- else {
13736
- log('⚠ Unexpected: Enhanced history has fewer entries than regular history');
13737
- }
13738
- // Test with a longer period to ensure it works with more data
13739
- // Note: For periods > 30 days, Alpaca API only allows 1D timeframe for hourly data
13740
- // So the function will gracefully handle this by only merging when hourly data is available
13741
- log('Testing with longer period (2 weeks)...');
13742
- const longerResult = await alpacaAPI.getPortfolioDailyHistory({
13743
- period: '14D'
13744
- });
13745
- console.log(`2-week daily history: ${longerResult.timestamp.length} entries`);
13746
- log('✓ Successfully retrieved 2-week daily history');
13747
- log('✓ All getPortfolioDailyHistory tests completed successfully!');
13748
- }
13749
- catch (error) {
13750
- console.error('✗ Test failed:', error);
13751
- if (error instanceof Error) {
13752
- console.error('Error message:', error.message);
13753
- console.error('Stack trace:', error.stack);
13754
- }
13755
- }
13756
- }
13757
- testGetPortfolioDailyHistory();
14270
+ // Test getMarketStatus with various scenarios
14271
+ function testGetMarketStatus() {
14272
+ const testCases = [
14273
+ {
14274
+ label: 'Weekend (Saturday, market closed)',
14275
+ date: '2025-07-12T12:00:00-04:00',
14276
+ expected: {
14277
+ status: 'closed',
14278
+ nextStatus: 'extended hours',
14279
+ marketPeriod: 'closed',
14280
+ },
14281
+ },
14282
+ // do before preMarket
14283
+ {
14284
+ label: 'Before pre-market (market closed)',
14285
+ date: '2025-07-10T03:00:00-04:00',
14286
+ expected: {
14287
+ status: 'closed',
14288
+ nextStatus: 'extended hours',
14289
+ marketPeriod: 'closed',
14290
+ },
14291
+ },
14292
+ {
14293
+ label: 'Pre-market (before open)',
14294
+ date: '2025-07-10T08:00:00-04:00', // 8am EDT
14295
+ expected: {
14296
+ status: 'extended hours',
14297
+ nextStatus: 'open',
14298
+ marketPeriod: 'preMarket',
14299
+ },
14300
+ },
14301
+ {
14302
+ label: 'Extended hours (pre-market)',
14303
+ date: '2025-07-10T05:00:00-04:00',
14304
+ expected: {
14305
+ status: 'extended hours',
14306
+ nextStatus: 'open',
14307
+ marketPeriod: 'preMarket',
14308
+ },
14309
+ },
14310
+ {
14311
+ label: 'Market open (regular)',
14312
+ date: '2025-07-10T10:00:00-04:00',
14313
+ expected: {
14314
+ status: 'open',
14315
+ nextStatus: 'extended hours',
14316
+ marketPeriod: 'regularMarket',
14317
+ },
14318
+ },
14319
+ {
14320
+ label: 'Market open (early market)',
14321
+ date: '2025-07-03T09:45:00-04:00', // Early close day
14322
+ expected: {
14323
+ status: 'open',
14324
+ nextStatus: 'extended hours',
14325
+ marketPeriod: 'earlyMarket',
14326
+ },
14327
+ },
14328
+ {
14329
+ label: 'After market close (extended hours)',
14330
+ date: '2025-07-10T17:00:00-04:00',
14331
+ expected: {
14332
+ status: 'extended hours',
14333
+ nextStatus: 'closed',
14334
+ marketPeriod: 'afterMarket',
14335
+ },
14336
+ },
14337
+ {
14338
+ label: 'After extended hours (market closed)',
14339
+ date: '2025-07-10T21:00:00-04:00',
14340
+ expected: {
14341
+ status: 'closed',
14342
+ nextStatus: 'extended hours',
14343
+ marketPeriod: 'closed',
14344
+ },
14345
+ },
14346
+ ];
14347
+ for (const { label, date, expected } of testCases) {
14348
+ const result = disco.time.getMarketStatus({ date: new Date(date) });
14349
+ const pass = result.status === expected.status &&
14350
+ result.nextStatus === expected.nextStatus &&
14351
+ result.marketPeriod === expected.marketPeriod;
14352
+ console.log(`\nTest: ${label}`);
14353
+ console.log(` Input date: ${date}`);
14354
+ console.log(` Status: ${result.status} ${pass ? '✓' : `✗ (expected ${expected.status})`}`);
14355
+ console.log(` NextStatus: ${result.nextStatus} ${pass ? '✓' : `✗ (expected ${expected.nextStatus})`}`);
14356
+ console.log(` MarketPeriod: ${result.marketPeriod} ${pass ? '✓' : `✗ (expected ${expected.marketPeriod})`}`);
14357
+ const nextStatusTimeDifferenceMs = result.nextStatusTimeDifference;
14358
+ // convert that to a string in the form 'xxh xxm'
14359
+ const nextStatusTimeDifferenceStr = `${Math.floor(nextStatusTimeDifferenceMs / (1000 * 60 * 60))}h ${Math.floor((nextStatusTimeDifferenceMs % (1000 * 60 * 60)) / (1000 * 60))}m`;
14360
+ console.log(` Next market status: ${result.nextStatus} at ${result.nextStatusTime.toLocaleString('en-US', { timeZone: 'America/New_York' })} in ${nextStatusTimeDifferenceStr}`);
14361
+ if (!pass) {
14362
+ console.error(` FAILED: Expected status=${expected.status}, nextStatus=${expected.nextStatus}, marketPeriod=${expected.marketPeriod}, got status=${result.status}, nextStatus=${result.nextStatus}, marketPeriod=${result.marketPeriod}`);
14363
+ }
14364
+ }
14365
+ }
14366
+ //testGetPortfolioDailyHistory();
13758
14367
  //testOpenRouter();
13759
-
13760
- export { WebSocket as W, log$1 as l, marketDataAPI as m };
14368
+ testGetMarketStatus();
13761
14369
  //# sourceMappingURL=test.js.map