@discomedia/utils 1.0.54 → 1.0.56

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
@@ -11,6 +11,7 @@ import require$$0$1 from 'buffer';
11
11
  import require$$0$4 from 'fs';
12
12
  import require$$1$1 from 'path';
13
13
  import require$$2$1 from 'os';
14
+ import 'console';
14
15
 
15
16
  /**
16
17
  * Logs a message to the console.
@@ -146,12 +147,18 @@ const marketEarlyCloses = {
146
147
 
147
148
  // Constants for NY market times (Eastern Time)
148
149
  const MARKET_CONFIG = {
150
+ TIMEZONE: 'America/New_York',
149
151
  UTC_OFFSET_STANDARD: -5, // EST
150
152
  UTC_OFFSET_DST: -4, // EDT
151
153
  TIMES: {
154
+ EXTENDED_START: { hour: 4, minute: 0 },
152
155
  MARKET_OPEN: { hour: 9, minute: 30 },
156
+ EARLY_MARKET_END: { hour: 10, minute: 0 },
153
157
  MARKET_CLOSE: { hour: 16, minute: 0 },
154
- EARLY_CLOSE: { hour: 13, minute: 0 }},
158
+ EARLY_CLOSE: { hour: 13, minute: 0 },
159
+ EXTENDED_END: { hour: 20, minute: 0 },
160
+ EARLY_EXTENDED_END: { hour: 17, minute: 0 },
161
+ },
155
162
  };
156
163
  // Helper: Get NY offset for a given UTC date (DST rules for US)
157
164
  /**
@@ -219,6 +226,33 @@ function fromNYTime(date) {
219
226
  const utcMillis = nyMillis - offset * 60 * 60 * 1000;
220
227
  return new Date(utcMillis);
221
228
  }
229
+ // Helper: Format date in ISO, unix, etc.
230
+ /**
231
+ * Formats a date in ISO, unix-seconds, or unix-ms format.
232
+ * @param date - Date object
233
+ * @param outputFormat - Output format ('iso', 'unix-seconds', 'unix-ms')
234
+ * @returns Formatted date string or number
235
+ */
236
+ function formatDate(date, outputFormat = 'iso') {
237
+ switch (outputFormat) {
238
+ case 'unix-seconds':
239
+ return Math.floor(date.getTime() / 1000);
240
+ case 'unix-ms':
241
+ return date.getTime();
242
+ case 'iso':
243
+ default:
244
+ return date.toISOString();
245
+ }
246
+ }
247
+ // Helper: Format date in NY locale string
248
+ /**
249
+ * Formats a date in NY locale string.
250
+ * @param date - Date object
251
+ * @returns NY locale string
252
+ */
253
+ function formatNYLocale(date) {
254
+ return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
255
+ }
222
256
  // Market calendar logic
223
257
  /**
224
258
  * Market calendar logic for holidays, weekends, and market days.
@@ -314,6 +348,82 @@ class MarketCalendar {
314
348
  return prevDay;
315
349
  }
316
350
  }
351
+ // Market open/close times
352
+ /**
353
+ * Returns market open/close times for a given date, including extended and early closes.
354
+ * @param date - Date object
355
+ * @returns MarketOpenCloseResult
356
+ */
357
+ function getMarketTimes(date) {
358
+ const calendar = new MarketCalendar();
359
+ const nyDate = toNYTime(date);
360
+ if (!calendar.isMarketDay(date)) {
361
+ return {
362
+ marketOpen: false,
363
+ open: null,
364
+ close: null,
365
+ openExt: null,
366
+ closeExt: null,
367
+ };
368
+ }
369
+ const year = nyDate.getUTCFullYear();
370
+ const month = nyDate.getUTCMonth();
371
+ const day = nyDate.getUTCDate();
372
+ // Helper to build NY time for a given hour/minute
373
+ function buildNYTime(hour, minute) {
374
+ const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
375
+ return fromNYTime(d);
376
+ }
377
+ let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
378
+ let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
379
+ let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
380
+ let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
381
+ if (calendar.isEarlyCloseDay(date)) {
382
+ close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
383
+ closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
384
+ }
385
+ return {
386
+ marketOpen: true,
387
+ open,
388
+ close,
389
+ openExt,
390
+ closeExt,
391
+ };
392
+ }
393
+ // Is within market hours
394
+ /**
395
+ * Checks if a date/time is within market hours, extended hours, or continuous.
396
+ * @param date - Date object
397
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
398
+ * @returns true if within hours, false otherwise
399
+ */
400
+ function isWithinMarketHours(date, intradayReporting = 'market_hours') {
401
+ const calendar = new MarketCalendar();
402
+ if (!calendar.isMarketDay(date))
403
+ return false;
404
+ const nyDate = toNYTime(date);
405
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
406
+ switch (intradayReporting) {
407
+ case 'extended_hours': {
408
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
409
+ if (calendar.isEarlyCloseDay(date)) {
410
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
411
+ }
412
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
413
+ return minutes >= startMinutes && minutes <= endMinutes;
414
+ }
415
+ case 'continuous':
416
+ return true;
417
+ default: {
418
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
419
+ if (calendar.isEarlyCloseDay(date)) {
420
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
421
+ }
422
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
423
+ return minutes >= startMinutes && minutes <= endMinutes;
424
+ }
425
+ }
426
+ }
317
427
  // Get last full trading date
318
428
  /**
319
429
  * Returns the last full trading date (market close) for a given date.
@@ -354,6 +464,244 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
354
464
  const closeMinute = marketCloseMinutes % 60;
355
465
  return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
356
466
  }
467
+ // Get day boundaries
468
+ /**
469
+ * Returns the start and end boundaries for a market day, extended hours, or continuous.
470
+ * @param date - Date object
471
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
472
+ * @returns Object with start and end Date
473
+ */
474
+ function getDayBoundaries(date, intradayReporting = 'market_hours') {
475
+ const calendar = new MarketCalendar();
476
+ const nyDate = toNYTime(date);
477
+ const year = nyDate.getUTCFullYear();
478
+ const month = nyDate.getUTCMonth();
479
+ const day = nyDate.getUTCDate();
480
+ function buildNYTime(hour, minute, sec = 0, ms = 0) {
481
+ const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
482
+ return fromNYTime(d);
483
+ }
484
+ let start;
485
+ let end;
486
+ switch (intradayReporting) {
487
+ case 'extended_hours':
488
+ start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
489
+ end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
490
+ if (calendar.isEarlyCloseDay(date)) {
491
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
492
+ }
493
+ break;
494
+ case 'continuous':
495
+ start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
496
+ end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
497
+ break;
498
+ default:
499
+ start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
500
+ end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
501
+ if (calendar.isEarlyCloseDay(date)) {
502
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
503
+ }
504
+ break;
505
+ }
506
+ return { start, end };
507
+ }
508
+ // Period calculator
509
+ /**
510
+ * Calculates the start date for a given period ending at endDate.
511
+ * @param endDate - Date object
512
+ * @param period - Period string
513
+ * @returns Date object for period start
514
+ */
515
+ function calculatePeriodStartDate(endDate, period) {
516
+ const calendar = new MarketCalendar();
517
+ let startDate;
518
+ switch (period) {
519
+ case 'YTD':
520
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
521
+ break;
522
+ case '1D':
523
+ startDate = calendar.getPreviousMarketDay(endDate);
524
+ break;
525
+ case '3D':
526
+ startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
527
+ break;
528
+ case '1W':
529
+ startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
530
+ break;
531
+ case '2W':
532
+ startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
533
+ break;
534
+ case '1M':
535
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
536
+ break;
537
+ case '3M':
538
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
539
+ break;
540
+ case '6M':
541
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
542
+ break;
543
+ case '1Y':
544
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
545
+ break;
546
+ default:
547
+ throw new Error(`Invalid period: ${period}`);
548
+ }
549
+ // Ensure start date is a market day
550
+ while (!calendar.isMarketDay(startDate)) {
551
+ startDate = calendar.getNextMarketDay(startDate);
552
+ }
553
+ return startDate;
554
+ }
555
+ // Get market time period
556
+ /**
557
+ * Returns the start and end dates for a market time period.
558
+ * @param params - MarketTimeParams
559
+ * @returns PeriodDates object
560
+ */
561
+ function getMarketTimePeriod(params) {
562
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso' } = params;
563
+ if (!period)
564
+ throw new Error('Period is required');
565
+ const calendar = new MarketCalendar();
566
+ const nyEndDate = toNYTime(end);
567
+ let endDate;
568
+ const isCurrentMarketDay = calendar.isMarketDay(end);
569
+ const isWithinHours = isWithinMarketHours(end, intraday_reporting);
570
+ const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
571
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
572
+ if (isCurrentMarketDay) {
573
+ if (minutes < marketStartMinutes) {
574
+ // Before market open - use previous day's close
575
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
576
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
577
+ endDate = dayEnd;
578
+ }
579
+ else if (isWithinHours) {
580
+ // During market hours - use current time
581
+ endDate = end;
582
+ }
583
+ else {
584
+ // After market close - use today's close
585
+ const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
586
+ endDate = dayEnd;
587
+ }
588
+ }
589
+ else {
590
+ // Not a market day - use previous market day's close
591
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
592
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
593
+ endDate = dayEnd;
594
+ }
595
+ // Calculate start date
596
+ const periodStartDate = calculatePeriodStartDate(endDate, period);
597
+ const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
598
+ if (endDate.getTime() < dayStart.getTime()) {
599
+ throw new Error('Start date cannot be after end date');
600
+ }
601
+ return {
602
+ start: formatDate(dayStart, outputFormat),
603
+ end: formatDate(endDate, outputFormat),
604
+ };
605
+ }
606
+ // Market status
607
+ /**
608
+ * Returns the current market status for a given date.
609
+ * @param date - Date object (default: now)
610
+ * @returns MarketStatus object
611
+ */
612
+ function getMarketStatusImpl(date = new Date()) {
613
+ const calendar = new MarketCalendar();
614
+ const nyDate = toNYTime(date);
615
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
616
+ const isMarketDay = calendar.isMarketDay(date);
617
+ const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
618
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
619
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
620
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
621
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
622
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
623
+ if (isEarlyCloseDay) {
624
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
625
+ extendedEndMinutes =
626
+ MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
627
+ }
628
+ let status;
629
+ let nextStatus;
630
+ let nextStatusTime;
631
+ let marketPeriod;
632
+ if (!isMarketDay) {
633
+ status = 'closed';
634
+ nextStatus = 'extended hours';
635
+ marketPeriod = 'closed';
636
+ const nextMarketDay = calendar.getNextMarketDay(date);
637
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
638
+ }
639
+ else if (minutes < extendedStartMinutes) {
640
+ status = 'closed';
641
+ nextStatus = 'extended hours';
642
+ marketPeriod = 'closed';
643
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
644
+ }
645
+ else if (minutes < marketStartMinutes) {
646
+ status = 'extended hours';
647
+ nextStatus = 'open';
648
+ marketPeriod = 'preMarket';
649
+ nextStatusTime = getDayBoundaries(date, 'market_hours').start;
650
+ }
651
+ else if (minutes < marketCloseMinutes) {
652
+ status = 'open';
653
+ nextStatus = 'extended hours';
654
+ marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
655
+ nextStatusTime = getDayBoundaries(date, 'market_hours').end;
656
+ }
657
+ else if (minutes < extendedEndMinutes) {
658
+ status = 'extended hours';
659
+ nextStatus = 'closed';
660
+ marketPeriod = 'afterMarket';
661
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
662
+ }
663
+ else {
664
+ status = 'closed';
665
+ nextStatus = 'extended hours';
666
+ marketPeriod = 'closed';
667
+ const nextMarketDay = calendar.getNextMarketDay(date);
668
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
669
+ }
670
+ // I think using nyDate here may be wrong - should use current time? i.e. date.getTime()
671
+ const nextStatusTimeDifference = nextStatusTime.getTime() - date.getTime();
672
+ return {
673
+ time: date,
674
+ timeString: formatNYLocale(nyDate),
675
+ status,
676
+ nextStatus,
677
+ marketPeriod,
678
+ nextStatusTime,
679
+ nextStatusTimeDifference,
680
+ nextStatusTimeString: formatNYLocale(nextStatusTime),
681
+ };
682
+ }
683
+ // API exports
684
+ /**
685
+ * Returns market open/close times for a given date.
686
+ * @param options - { date?: Date }
687
+ * @returns MarketOpenCloseResult
688
+ */
689
+ function getMarketOpenClose(options = {}) {
690
+ const { date = new Date() } = options;
691
+ return getMarketTimes(date);
692
+ }
693
+ /**
694
+ * Returns the start and end dates for a market time period as Date objects.
695
+ * @param params - MarketTimeParams
696
+ * @returns Object with start and end Date
697
+ */
698
+ function getStartAndEndDates(params = {}) {
699
+ const { start, end } = getMarketTimePeriod(params);
700
+ return {
701
+ start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
702
+ end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
703
+ };
704
+ }
357
705
  /**
358
706
  * Returns the last full trading date as a Date object.
359
707
  */
@@ -365,6 +713,43 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
365
713
  function getLastFullTradingDate(currentDate = new Date()) {
366
714
  return getLastFullTradingDateImpl(currentDate);
367
715
  }
716
+ /**
717
+ * Returns the next market day after the reference date.
718
+ * @param referenceDate - Date object (default: now)
719
+ * @returns Object with date, yyyymmdd string, and ISO string
720
+ */
721
+ function getNextMarketDay({ referenceDate } = {}) {
722
+ const calendar = new MarketCalendar();
723
+ const startDate = referenceDate ?? new Date();
724
+ // Find the next trading day (UTC Date object)
725
+ const nextDate = calendar.getNextMarketDay(startDate);
726
+ // Convert to NY time before extracting Y-M-D parts
727
+ const nyNext = toNYTime(nextDate);
728
+ const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, '0')}-${String(nyNext.getUTCDate()).padStart(2, '0')}`;
729
+ return {
730
+ date: nextDate, // raw Date, unchanged
731
+ yyyymmdd, // correct trading date string
732
+ dateISOString: nextDate.toISOString(),
733
+ };
734
+ }
735
+ /**
736
+ * Returns the previous market day before the reference date.
737
+ * @param referenceDate - Date object (default: now)
738
+ * @returns Object with date, yyyymmdd string, and ISO string
739
+ */
740
+ function getPreviousMarketDay({ referenceDate } = {}) {
741
+ const calendar = new MarketCalendar();
742
+ const startDate = referenceDate || new Date();
743
+ const prevDate = calendar.getPreviousMarketDay(startDate);
744
+ // convert to NY time first
745
+ const nyPrev = toNYTime(prevDate); // ← already in this file
746
+ const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, '0')}-${String(nyPrev.getUTCDate()).padStart(2, '0')}`;
747
+ return {
748
+ date: prevDate,
749
+ yyyymmdd,
750
+ dateISOString: prevDate.toISOString(),
751
+ };
752
+ }
368
753
  /**
369
754
  * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
370
755
  * @param time - a string, number (unix timestamp), or Date object representing the time
@@ -380,6 +765,296 @@ function getTradingDate(time) {
380
765
  const nyDate = toNYTime(date);
381
766
  return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
382
767
  }
768
+ /**
769
+ * Returns the NY timezone offset string for a given date.
770
+ * @param date - Date object (default: now)
771
+ * @returns '-04:00' for EDT, '-05:00' for EST
772
+ */
773
+ function getNYTimeZone(date) {
774
+ const offset = getNYOffset(date || new Date());
775
+ return offset === -4 ? '-04:00' : '-05:00';
776
+ }
777
+ /**
778
+ * Returns the regular market open and close Date objects for a given trading day string in the
779
+ * America/New_York timezone (NYSE/NASDAQ calendar).
780
+ *
781
+ * This helper is convenient when you have a calendar date like '2025-10-03' and want the precise
782
+ * open and close Date values for that day. It internally:
783
+ * - Determines the NY offset for the day using `getNYTimeZone()`.
784
+ * - Anchors a noon-time Date on that day in NY time to avoid DST edge cases.
785
+ * - Verifies the day is a market day via `isMarketDay()`.
786
+ * - Fetches the open/close times via `getMarketOpenClose()`.
787
+ *
788
+ * Throws if the provided day is not a market day or if open/close times are unavailable.
789
+ *
790
+ * See also:
791
+ * - `getNYTimeZone(date?: Date)`
792
+ * - `isMarketDay(date: Date)`
793
+ * - `getMarketOpenClose(options?: { date?: Date })`
794
+ *
795
+ * @param dateStr - Trading day string in 'YYYY-MM-DD' format (Eastern Time date)
796
+ * @returns An object containing `{ open: Date; close: Date }`
797
+ * @example
798
+ * ```ts
799
+ * const { open, close } = disco.time.getOpenCloseForTradingDay('2025-10-03');
800
+ * ```
801
+ */
802
+ function getOpenCloseForTradingDay(dateStr) {
803
+ // Build a UTC midnight anchor for the date, then derive the NY offset for that day.
804
+ const utcAnchor = new Date(`${dateStr}T00:00:00Z`);
805
+ const nyOffset = getNYTimeZone(utcAnchor); // '-04:00' | '-05:00'
806
+ // Create a NY-local noon date to avoid DST midnight transitions.
807
+ const nyNoon = new Date(`${dateStr}T12:00:00${nyOffset}`);
808
+ if (!isMarketDay(nyNoon)) {
809
+ throw new Error(`Not a market day in ET: ${dateStr}`);
810
+ }
811
+ const { open, close } = getMarketOpenClose({ date: nyNoon });
812
+ if (!open || !close) {
813
+ throw new Error(`No market times available for ${dateStr}`);
814
+ }
815
+ return { open, close };
816
+ }
817
+ /**
818
+ * Converts any date to the market time zone (America/New_York, Eastern Time).
819
+ * Returns a new Date object representing the same moment in time but adjusted to NY/Eastern timezone.
820
+ * Automatically handles daylight saving time transitions (EST/EDT).
821
+ *
822
+ * @param date - Date object to convert to market time zone
823
+ * @returns Date object in NY/Eastern time zone
824
+ * @example
825
+ * ```typescript
826
+ * const utcDate = new Date('2024-01-15T15:30:00Z'); // 3:30 PM UTC
827
+ * const nyDate = convertDateToMarketTimeZone(utcDate); // 10:30 AM EST (winter) or 11:30 AM EDT (summer)
828
+ * ```
829
+ */
830
+ function convertDateToMarketTimeZone(date) {
831
+ return toNYTime(date);
832
+ }
833
+ /**
834
+ * Returns the current market status for a given date.
835
+ * @param options - { date?: Date }
836
+ * @returns MarketStatus object
837
+ */
838
+ function getMarketStatus(options = {}) {
839
+ const { date = new Date() } = options;
840
+ return getMarketStatusImpl(date);
841
+ }
842
+ /**
843
+ * Checks if a date is a market day.
844
+ * @param date - Date object
845
+ * @returns true if market day, false otherwise
846
+ */
847
+ function isMarketDay(date) {
848
+ const calendar = new MarketCalendar();
849
+ return calendar.isMarketDay(date);
850
+ }
851
+ /**
852
+ * Returns full trading days from market open to market close.
853
+ * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
854
+ * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
855
+ */
856
+ /**
857
+ * Returns full trading days from market open to market close.
858
+ * @param options - { endDate?: Date, days?: number }
859
+ * @returns Object with startDate and endDate
860
+ */
861
+ function getTradingStartAndEndDates(options = {}) {
862
+ const { endDate = new Date(), days = 1 } = options;
863
+ const calendar = new MarketCalendar();
864
+ // Find the most recent market close
865
+ let endMarketDay = endDate;
866
+ const nyEnd = toNYTime(endDate);
867
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
868
+ const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
869
+ const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
870
+ if (!calendar.isMarketDay(endDate) ||
871
+ minutes < marketOpenMinutes ||
872
+ (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
873
+ // Before market open, not a market day, or during market hours: use previous market day
874
+ endMarketDay = calendar.getPreviousMarketDay(endDate);
875
+ }
876
+ else {
877
+ // After market close: use today
878
+ endMarketDay = endDate;
879
+ }
880
+ // Get market close for endMarketDay
881
+ const endClose = getMarketOpenClose({ date: endMarketDay }).close;
882
+ // Find start market day by iterating back over market days
883
+ let startMarketDay = endMarketDay;
884
+ let count = Math.max(1, days);
885
+ for (let i = 1; i < count; i++) {
886
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
887
+ }
888
+ // If days > 1, we need to go back (days-1) market days from endMarketDay
889
+ if (days > 1) {
890
+ startMarketDay = endMarketDay;
891
+ for (let i = 1; i < days; i++) {
892
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
893
+ }
894
+ }
895
+ const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
896
+ return { startDate: startOpen, endDate: endClose };
897
+ }
898
+ /**
899
+ * 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.
900
+ *
901
+ * This function calculates the actual trading time between two dates by:
902
+ * 1. Iterating through each calendar day between startDate and endDate (inclusive)
903
+ * 2. For each day that is a market day (not weekend/holiday), getting market open/close times
904
+ * 3. Calculating the overlap between the time range and market hours for that day
905
+ * 4. Summing up all the trading minutes across all days
906
+ *
907
+ * The function automatically handles:
908
+ * - Weekends (Saturday/Sunday) - skipped entirely
909
+ * - Market holidays - skipped entirely
910
+ * - Early close days (e.g. day before holidays) - uses early close time
911
+ * - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
912
+ *
913
+ * Examples:
914
+ * - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
915
+ * - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
916
+ * - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
917
+ *
918
+ * @param startDate - Start date/time
919
+ * @param endDate - End date/time (default: now)
920
+ * @returns Object containing:
921
+ * - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
922
+ * - hours: Trading time in hours
923
+ * - minutes: Trading time in minutes
924
+ */
925
+ function countTradingDays(startDate, endDate = new Date()) {
926
+ const calendar = new MarketCalendar();
927
+ // Ensure start is before end
928
+ if (startDate.getTime() > endDate.getTime()) {
929
+ throw new Error('Start date must be before end date');
930
+ }
931
+ let totalMinutes = 0;
932
+ // Get the NY dates for iteration
933
+ const startNY = toNYTime(startDate);
934
+ const endNY = toNYTime(endDate);
935
+ // Create date at start of first day (in NY time)
936
+ const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
937
+ // Iterate through each calendar day
938
+ while (currentNY.getTime() <= endNY.getTime()) {
939
+ const currentUTC = fromNYTime(currentNY);
940
+ // Check if this is a market day
941
+ if (calendar.isMarketDay(currentUTC)) {
942
+ // Get market hours for this day
943
+ const marketTimes = getMarketTimes(currentUTC);
944
+ if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
945
+ // Calculate the overlap between our time range and market hours
946
+ const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
947
+ const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
948
+ // Only count if there's actual overlap
949
+ if (dayStart < dayEnd) {
950
+ totalMinutes += (dayEnd - dayStart) / (1000 * 60);
951
+ }
952
+ }
953
+ }
954
+ // Move to next day
955
+ currentNY.setUTCDate(currentNY.getUTCDate() + 1);
956
+ }
957
+ // Convert to days, hours, minutes
958
+ const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
959
+ const days = totalMinutes / MINUTES_PER_TRADING_DAY;
960
+ const hours = totalMinutes / 60;
961
+ const minutes = totalMinutes;
962
+ return {
963
+ days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
964
+ hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
965
+ minutes: Math.round(minutes),
966
+ };
967
+ }
968
+ /**
969
+ * Returns the trading day N days back from a reference date, along with its market open time.
970
+ * Trading days are counted as full or half trading days (days that end count as 1 full trading day).
971
+ * By default, the most recent completed trading day counts as day 1.
972
+ * Set includeMostRecentFullDay to false to count strictly before that day.
973
+ *
974
+ * @param options - Object with:
975
+ * - referenceDate: Date to count back from (default: now)
976
+ * - days: Number of trading days to go back (must be an integer >= 1)
977
+ * - includeMostRecentFullDay: Whether to include the most recent completed trading day (default: true)
978
+ * @returns Object containing:
979
+ * - date: Trading date in YYYY-MM-DD format
980
+ * - marketOpenISO: Market open time as ISO string (e.g., "2025-11-15T13:30:00.000Z")
981
+ * - unixTimestamp: Market open time as Unix timestamp in seconds
982
+ * @example
983
+ * ```typescript
984
+ * // Get the trading day 1 day back (most recent full trading day)
985
+ * const result = getTradingDaysBack({ days: 1 });
986
+ * console.log(result.date); // "2025-11-01"
987
+ * console.log(result.marketOpenISO); // "2025-11-01T13:30:00.000Z"
988
+ * console.log(result.unixTimestamp); // 1730466600
989
+ *
990
+ * // Get the trading day 5 days back from a specific date
991
+ * const result2 = getTradingDaysBack({
992
+ * referenceDate: new Date('2025-11-15T12:00:00-05:00'),
993
+ * days: 5
994
+ * });
995
+ * ```
996
+ */
997
+ function getTradingDaysBack(options) {
998
+ const calendar = new MarketCalendar();
999
+ const { referenceDate, days, includeMostRecentFullDay = true } = options;
1000
+ const refDate = referenceDate || new Date();
1001
+ const daysBack = days;
1002
+ if (!Number.isInteger(daysBack) || daysBack < 1) {
1003
+ throw new Error('days must be an integer >= 1');
1004
+ }
1005
+ // Start from the last full trading date relative to reference
1006
+ let targetDate = getLastFullTradingDateImpl(refDate);
1007
+ if (!includeMostRecentFullDay) {
1008
+ targetDate = calendar.getPreviousMarketDay(targetDate);
1009
+ }
1010
+ // Go back the specified number of days (we're already at day 1, so go back days-1 more)
1011
+ for (let i = 1; i < daysBack; i++) {
1012
+ targetDate = calendar.getPreviousMarketDay(targetDate);
1013
+ }
1014
+ // Get market open time for this date
1015
+ const marketTimes = getMarketTimes(targetDate);
1016
+ if (!marketTimes.open) {
1017
+ throw new Error(`No market open time for target date`);
1018
+ }
1019
+ // Format the date string (YYYY-MM-DD) in NY time
1020
+ const nyDate = toNYTime(marketTimes.open);
1021
+ const dateStr = `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
1022
+ const marketOpenISO = marketTimes.open.toISOString();
1023
+ const unixTimestamp = Math.floor(marketTimes.open.getTime() / 1000);
1024
+ return {
1025
+ date: dateStr,
1026
+ marketOpenISO,
1027
+ unixTimestamp,
1028
+ };
1029
+ }
1030
+ // Export MARKET_TIMES for compatibility
1031
+ const MARKET_TIMES = {
1032
+ TIMEZONE: MARKET_CONFIG.TIMEZONE,
1033
+ PRE: {
1034
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
1035
+ END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1036
+ },
1037
+ EARLY_MORNING: {
1038
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1039
+ END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
1040
+ },
1041
+ EARLY_CLOSE_BEFORE_HOLIDAY: {
1042
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1043
+ END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
1044
+ },
1045
+ EARLY_EXTENDED_BEFORE_HOLIDAY: {
1046
+ START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
1047
+ END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
1048
+ },
1049
+ REGULAR: {
1050
+ START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1051
+ END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
1052
+ },
1053
+ EXTENDED: {
1054
+ START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
1055
+ END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
1056
+ },
1057
+ };
383
1058
 
384
1059
  /*
385
1060
  How it works:
@@ -467,8 +1142,18 @@ class Queue {
467
1142
  }
468
1143
 
469
1144
  function pLimit(concurrency) {
1145
+ let rejectOnClear = false;
1146
+
1147
+ if (typeof concurrency === 'object') {
1148
+ ({concurrency, rejectOnClear = false} = concurrency);
1149
+ }
1150
+
470
1151
  validateConcurrency(concurrency);
471
1152
 
1153
+ if (typeof rejectOnClear !== 'boolean') {
1154
+ throw new TypeError('Expected `rejectOnClear` to be a boolean');
1155
+ }
1156
+
472
1157
  const queue = new Queue();
473
1158
  let activeCount = 0;
474
1159
 
@@ -476,7 +1161,7 @@ function pLimit(concurrency) {
476
1161
  // Process the next queued function if we're under the concurrency limit
477
1162
  if (activeCount < concurrency && queue.size > 0) {
478
1163
  activeCount++;
479
- queue.dequeue()();
1164
+ queue.dequeue().run();
480
1165
  }
481
1166
  };
482
1167
 
@@ -503,11 +1188,14 @@ function pLimit(concurrency) {
503
1188
  next();
504
1189
  };
505
1190
 
506
- const enqueue = (function_, resolve, arguments_) => {
1191
+ const enqueue = (function_, resolve, reject, arguments_) => {
1192
+ const queueItem = {reject};
1193
+
507
1194
  // Queue the internal resolve function instead of the run function
508
1195
  // to preserve the asynchronous execution context.
509
1196
  new Promise(internalResolve => { // eslint-disable-line promise/param-names
510
- queue.enqueue(internalResolve);
1197
+ queueItem.run = internalResolve;
1198
+ queue.enqueue(queueItem);
511
1199
  }).then(run.bind(undefined, function_, resolve, arguments_)); // eslint-disable-line promise/prefer-await-to-then
512
1200
 
513
1201
  // Start processing immediately if we haven't reached the concurrency limit
@@ -516,8 +1204,8 @@ function pLimit(concurrency) {
516
1204
  }
517
1205
  };
518
1206
 
519
- const generator = (function_, ...arguments_) => new Promise(resolve => {
520
- enqueue(function_, resolve, arguments_);
1207
+ const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
1208
+ enqueue(function_, resolve, reject, arguments_);
521
1209
  });
522
1210
 
523
1211
  Object.defineProperties(generator, {
@@ -529,7 +1217,16 @@ function pLimit(concurrency) {
529
1217
  },
530
1218
  clearQueue: {
531
1219
  value() {
532
- queue.clear();
1220
+ if (!rejectOnClear) {
1221
+ queue.clear();
1222
+ return;
1223
+ }
1224
+
1225
+ const abortError = AbortSignal.abort().reason;
1226
+
1227
+ while (queue.size > 0) {
1228
+ queue.dequeue().reject(abortError);
1229
+ }
533
1230
  },
534
1231
  },
535
1232
  concurrency: {
@@ -815,7 +1512,7 @@ const safeJSON = (text) => {
815
1512
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
816
1513
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
817
1514
 
818
- const VERSION = '6.15.0'; // x-release-please-version
1515
+ const VERSION = '6.17.0'; // x-release-please-version
819
1516
 
820
1517
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
821
1518
  const isRunningInBrowser = () => {
@@ -4108,12 +4805,7 @@ class Assistants extends APIResource {
4108
4805
  /**
4109
4806
  * Create an assistant with a model and instructions.
4110
4807
  *
4111
- * @example
4112
- * ```ts
4113
- * const assistant = await client.beta.assistants.create({
4114
- * model: 'gpt-4o',
4115
- * });
4116
- * ```
4808
+ * @deprecated
4117
4809
  */
4118
4810
  create(body, options) {
4119
4811
  return this._client.post('/assistants', {
@@ -4125,12 +4817,7 @@ class Assistants extends APIResource {
4125
4817
  /**
4126
4818
  * Retrieves an assistant.
4127
4819
  *
4128
- * @example
4129
- * ```ts
4130
- * const assistant = await client.beta.assistants.retrieve(
4131
- * 'assistant_id',
4132
- * );
4133
- * ```
4820
+ * @deprecated
4134
4821
  */
4135
4822
  retrieve(assistantID, options) {
4136
4823
  return this._client.get(path `/assistants/${assistantID}`, {
@@ -4141,12 +4828,7 @@ class Assistants extends APIResource {
4141
4828
  /**
4142
4829
  * Modifies an assistant.
4143
4830
  *
4144
- * @example
4145
- * ```ts
4146
- * const assistant = await client.beta.assistants.update(
4147
- * 'assistant_id',
4148
- * );
4149
- * ```
4831
+ * @deprecated
4150
4832
  */
4151
4833
  update(assistantID, body, options) {
4152
4834
  return this._client.post(path `/assistants/${assistantID}`, {
@@ -4158,13 +4840,7 @@ class Assistants extends APIResource {
4158
4840
  /**
4159
4841
  * Returns a list of assistants.
4160
4842
  *
4161
- * @example
4162
- * ```ts
4163
- * // Automatically fetches more pages as needed.
4164
- * for await (const assistant of client.beta.assistants.list()) {
4165
- * // ...
4166
- * }
4167
- * ```
4843
+ * @deprecated
4168
4844
  */
4169
4845
  list(query = {}, options) {
4170
4846
  return this._client.getAPIList('/assistants', (CursorPage), {
@@ -4176,11 +4852,7 @@ class Assistants extends APIResource {
4176
4852
  /**
4177
4853
  * Delete an assistant.
4178
4854
  *
4179
- * @example
4180
- * ```ts
4181
- * const assistantDeleted =
4182
- * await client.beta.assistants.delete('assistant_id');
4183
- * ```
4855
+ * @deprecated
4184
4856
  */
4185
4857
  delete(assistantID, options) {
4186
4858
  return this._client.delete(path `/assistants/${assistantID}`, {
@@ -5654,8 +6326,8 @@ Evals.Runs = Runs;
5654
6326
  let Files$1 = class Files extends APIResource {
5655
6327
  /**
5656
6328
  * Upload a file that can be used across various endpoints. Individual files can be
5657
- * up to 512 MB, and the size of all files uploaded by one organization can be up
5658
- * to 1 TB.
6329
+ * up to 512 MB, and each project can store up to 2.5 TB of files in total. There
6330
+ * is no organization-wide storage limit.
5659
6331
  *
5660
6332
  * - The Assistants API supports files up to 2 million tokens and of specific file
5661
6333
  * types. See the
@@ -13295,6 +13967,11 @@ class AlpacaMarketDataAPI extends EventEmitter {
13295
13967
  optionWs = null;
13296
13968
  stockSubscriptions = { trades: [], quotes: [], bars: [] };
13297
13969
  optionSubscriptions = { trades: [], quotes: [], bars: [] };
13970
+ autoReconnect = true;
13971
+ stockReconnectAttempts = 0;
13972
+ optionReconnectAttempts = 0;
13973
+ stockReconnectTimeout = null;
13974
+ optionReconnectTimeout = null;
13298
13975
  setMode(mode = 'production') {
13299
13976
  if (mode === 'sandbox') {
13300
13977
  // sandbox mode
@@ -13323,6 +14000,13 @@ class AlpacaMarketDataAPI extends EventEmitter {
13323
14000
  return 'production';
13324
14001
  }
13325
14002
  }
14003
+ setAutoReconnect(enabled) {
14004
+ this.autoReconnect = enabled;
14005
+ log(`Auto-reconnect ${enabled ? 'enabled' : 'disabled'}`);
14006
+ }
14007
+ getAutoReconnect() {
14008
+ return this.autoReconnect;
14009
+ }
13326
14010
  constructor() {
13327
14011
  super();
13328
14012
  this.dataURL = 'https://data.alpaca.markets/v2';
@@ -13345,6 +14029,36 @@ class AlpacaMarketDataAPI extends EventEmitter {
13345
14029
  }
13346
14030
  return AlpacaMarketDataAPI.instance;
13347
14031
  }
14032
+ getReconnectDelay(attempt) {
14033
+ // 0: instant (0ms), 1: 5s, 2: 10s, 3: 15s, 4+: 60s
14034
+ const delays = [0, 5000, 10000, 15000, 60000];
14035
+ return attempt < delays.length ? delays[attempt] : 60000;
14036
+ }
14037
+ scheduleReconnect(streamType) {
14038
+ if (!this.autoReconnect) {
14039
+ log(`Auto-reconnect disabled, not reconnecting ${streamType} stream`);
14040
+ return;
14041
+ }
14042
+ const attempts = streamType === 'stock' ? this.stockReconnectAttempts : this.optionReconnectAttempts;
14043
+ const delay = this.getReconnectDelay(attempts);
14044
+ log(`Scheduling ${streamType} stream reconnect (attempt ${attempts + 1}) in ${delay}ms`);
14045
+ const timeout = setTimeout(() => {
14046
+ log(`Attempting to reconnect ${streamType} stream (attempt ${attempts + 1})`);
14047
+ if (streamType === 'stock') {
14048
+ this.stockReconnectAttempts++;
14049
+ }
14050
+ else {
14051
+ this.optionReconnectAttempts++;
14052
+ }
14053
+ this.connect(streamType);
14054
+ }, delay);
14055
+ if (streamType === 'stock') {
14056
+ this.stockReconnectTimeout = timeout;
14057
+ }
14058
+ else {
14059
+ this.optionReconnectTimeout = timeout;
14060
+ }
14061
+ }
13348
14062
  on(event, listener) {
13349
14063
  return super.on(event, listener);
13350
14064
  }
@@ -13362,6 +14076,21 @@ class AlpacaMarketDataAPI extends EventEmitter {
13362
14076
  }
13363
14077
  ws.on('open', () => {
13364
14078
  log(`${streamType} stream connected`);
14079
+ // Reset reconnect attempts on successful connection
14080
+ if (streamType === 'stock') {
14081
+ this.stockReconnectAttempts = 0;
14082
+ if (this.stockReconnectTimeout) {
14083
+ clearTimeout(this.stockReconnectTimeout);
14084
+ this.stockReconnectTimeout = null;
14085
+ }
14086
+ }
14087
+ else {
14088
+ this.optionReconnectAttempts = 0;
14089
+ if (this.optionReconnectTimeout) {
14090
+ clearTimeout(this.optionReconnectTimeout);
14091
+ this.optionReconnectTimeout = null;
14092
+ }
14093
+ }
13365
14094
  const authMessage = {
13366
14095
  action: 'auth',
13367
14096
  key: process.env.ALPACA_API_KEY,
@@ -13386,20 +14115,42 @@ class AlpacaMarketDataAPI extends EventEmitter {
13386
14115
  }
13387
14116
  }
13388
14117
  });
13389
- ws.on('close', () => {
13390
- log(`${streamType} stream disconnected`, { type: 'warn' });
14118
+ ws.on('close', (code, reason) => {
14119
+ const reasonStr = reason.toString() || 'No reason provided';
14120
+ const codeDesc = this.getCloseCodeDescription(code);
14121
+ log(`${streamType} stream disconnected - Code: ${code} (${codeDesc}), Reason: ${reasonStr}`, { type: 'warn' });
13391
14122
  if (streamType === 'stock') {
13392
14123
  this.stockWs = null;
13393
14124
  }
13394
14125
  else {
13395
14126
  this.optionWs = null;
13396
14127
  }
13397
- // Optional: implement reconnect logic
14128
+ this.scheduleReconnect(streamType);
13398
14129
  });
13399
14130
  ws.on('error', (error) => {
13400
14131
  log(`${streamType} stream error: ${error.message}`, { type: 'error' });
13401
14132
  });
13402
14133
  }
14134
+ getCloseCodeDescription(code) {
14135
+ const codes = {
14136
+ 1000: 'Normal Closure',
14137
+ 1001: 'Going Away',
14138
+ 1002: 'Protocol Error',
14139
+ 1003: 'Unsupported Data',
14140
+ 1005: 'No Status Received',
14141
+ 1006: 'Abnormal Closure',
14142
+ 1007: 'Invalid Frame Payload Data',
14143
+ 1008: 'Policy Violation',
14144
+ 1009: 'Message Too Big',
14145
+ 1010: 'Mandatory Extension',
14146
+ 1011: 'Internal Server Error',
14147
+ 1012: 'Service Restart',
14148
+ 1013: 'Try Again Later',
14149
+ 1014: 'Bad Gateway',
14150
+ 1015: 'TLS Handshake',
14151
+ };
14152
+ return codes[code] || 'Unknown';
14153
+ }
13403
14154
  sendSubscription(streamType) {
13404
14155
  const ws = streamType === 'stock' ? this.stockWs : this.optionWs;
13405
14156
  const subscriptions = streamType === 'stock' ? this.stockSubscriptions : this.optionSubscriptions;
@@ -13434,11 +14185,21 @@ class AlpacaMarketDataAPI extends EventEmitter {
13434
14185
  }
13435
14186
  }
13436
14187
  disconnectStockStream() {
14188
+ if (this.stockReconnectTimeout) {
14189
+ clearTimeout(this.stockReconnectTimeout);
14190
+ this.stockReconnectTimeout = null;
14191
+ }
14192
+ this.stockReconnectAttempts = 0;
13437
14193
  if (this.stockWs) {
13438
14194
  this.stockWs.close();
13439
14195
  }
13440
14196
  }
13441
14197
  disconnectOptionStream() {
14198
+ if (this.optionReconnectTimeout) {
14199
+ clearTimeout(this.optionReconnectTimeout);
14200
+ this.optionReconnectTimeout = null;
14201
+ }
14202
+ this.optionReconnectAttempts = 0;
13442
14203
  if (this.optionWs) {
13443
14204
  this.optionWs.close();
13444
14205
  }
@@ -14429,1622 +15190,132 @@ class AlpacaMarketDataAPI extends EventEmitter {
14429
15190
  }
14430
15191
  }
14431
15192
  // Export the singleton instance
14432
- const marketDataAPI = AlpacaMarketDataAPI.getInstance();
15193
+ AlpacaMarketDataAPI.getInstance();
15194
+
15195
+ const disco = {
15196
+ time: {
15197
+ convertDateToMarketTimeZone: convertDateToMarketTimeZone,
15198
+ getStartAndEndDates: getStartAndEndDates,
15199
+ getMarketOpenClose: getMarketOpenClose,
15200
+ getOpenCloseForTradingDay: getOpenCloseForTradingDay,
15201
+ getLastFullTradingDate: getLastFullTradingDate,
15202
+ getNextMarketDay: getNextMarketDay,
15203
+ getPreviousMarketDay: getPreviousMarketDay,
15204
+ getMarketTimePeriod: getMarketTimePeriod,
15205
+ getMarketStatus: getMarketStatus,
15206
+ getNYTimeZone: getNYTimeZone,
15207
+ getTradingDate: getTradingDate,
15208
+ getTradingStartAndEndDates: getTradingStartAndEndDates,
15209
+ getTradingDaysBack: getTradingDaysBack,
15210
+ isMarketDay: isMarketDay,
15211
+ isWithinMarketHours: isWithinMarketHours,
15212
+ countTradingDays: countTradingDays,
15213
+ MARKET_TIMES: MARKET_TIMES,
15214
+ }};
14433
15215
 
14434
- const limitPriceSlippagePercent100 = 0.1; // 0.1%
14435
- /**
14436
- Websocket example
14437
- const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
14438
- alpacaAPI.onTradeUpdate((update: TradeUpdate) => {
14439
- this.log(`Received trade update: event ${update.event} for an order to ${update.order.side} ${update.order.qty} of ${update.order.symbol}`);
14440
- });
14441
- alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
14442
-
14443
- Portfolio History examples
14444
- // Get standard portfolio history
14445
- const portfolioHistory = await alpacaAPI.getPortfolioHistory({
14446
- timeframe: '1D',
14447
- period: '1M'
14448
- });
14449
-
14450
- // Get daily portfolio history with current day included (if available from hourly data)
14451
- const dailyHistory = await alpacaAPI.getPortfolioDailyHistory({
14452
- period: '1M'
14453
- });
14454
- */
14455
- class AlpacaTradingAPI {
14456
- static new(credentials) {
14457
- return new AlpacaTradingAPI(credentials);
14458
- }
14459
- static getInstance(credentials) {
14460
- return new AlpacaTradingAPI(credentials);
14461
- }
14462
- ws = null;
14463
- headers;
14464
- tradeUpdateCallback = null;
14465
- credentials;
14466
- apiBaseUrl;
14467
- wsUrl;
14468
- authenticated = false;
14469
- connecting = false;
14470
- reconnectDelay = 10000; // 10 seconds between reconnection attempts
14471
- reconnectTimeout = null;
14472
- messageHandlers = new Map();
14473
- debugLogging = false;
14474
- manualDisconnect = false;
14475
- /**
14476
- * Constructor for AlpacaTradingAPI
14477
- * @param credentials - Alpaca credentials,
14478
- * accountName: string; // The account identifier used inthis.logs and tracking
14479
- * apiKey: string; // Alpaca API key
14480
- * apiSecret: string; // Alpaca API secret
14481
- * type: AlpacaAccountType;
14482
- * orderType: AlpacaOrderType;
14483
- * @param options - Optional options
14484
- * debugLogging: boolean; // Whether to log messages of type 'debug'
14485
- */
14486
- constructor(credentials, options) {
14487
- this.credentials = credentials;
14488
- // Set URLs based on account type
14489
- this.apiBaseUrl =
14490
- credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
14491
- this.wsUrl =
14492
- credentials.type === 'PAPER' ? 'wss://paper-api.alpaca.markets/stream' : 'wss://api.alpaca.markets/stream';
14493
- this.headers = {
14494
- 'APCA-API-KEY-ID': credentials.apiKey,
14495
- 'APCA-API-SECRET-KEY': credentials.apiSecret,
14496
- 'Content-Type': 'application/json',
14497
- };
14498
- // Initialize message handlers
14499
- this.messageHandlers.set('authorization', this.handleAuthMessage.bind(this));
14500
- this.messageHandlers.set('listening', this.handleListenMessage.bind(this));
14501
- this.messageHandlers.set('trade_updates', this.handleTradeUpdate.bind(this));
14502
- this.debugLogging = options?.debugLogging || false;
14503
- }
14504
- log(message, options = { type: 'info' }) {
14505
- if (this.debugLogging && options.type === 'debug') {
14506
- return;
14507
- }
14508
- log$1(message, { ...options, source: 'AlpacaTradingAPI', account: this.credentials.accountName });
14509
- }
14510
- /**
14511
- * Round a price to the nearest 2 decimal places for Alpaca, or 4 decimal places for prices less than $1
14512
- * @param price - The price to round
14513
- * @returns The rounded price
14514
- */
14515
- roundPriceForAlpaca = (price) => {
14516
- return price >= 1 ? Math.round(price * 100) / 100 : Math.round(price * 10000) / 10000;
14517
- };
14518
- handleAuthMessage(data) {
14519
- if (data.status === 'authorized') {
14520
- this.authenticated = true;
14521
- this.log('WebSocket authenticated');
14522
- }
14523
- else {
14524
- this.log(`Authentication failed: ${data.message || 'Unknown error'}`, {
14525
- type: 'error',
14526
- });
14527
- }
14528
- }
14529
- handleListenMessage(data) {
14530
- if (data.streams?.includes('trade_updates')) {
14531
- this.log('Successfully subscribed to trade updates');
14532
- }
14533
- }
14534
- handleTradeUpdate(data) {
14535
- if (this.tradeUpdateCallback) {
14536
- this.log(`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares${data.event === 'partial_fill' ? ` (filled shares: ${data.order.filled_qty})` : ''}, type ${data.order.type}`, {
14537
- symbol: data.order.symbol,
14538
- type: 'debug',
14539
- });
14540
- this.tradeUpdateCallback(data);
14541
- }
14542
- }
14543
- handleMessage(message) {
14544
- try {
14545
- const data = JSON.parse(message);
14546
- const handler = this.messageHandlers.get(data.stream);
14547
- if (handler) {
14548
- handler(data.data);
14549
- }
14550
- else {
14551
- this.log(`Received message for unknown stream: ${data.stream}`, {
14552
- type: 'warn',
14553
- });
14554
- }
14555
- }
14556
- catch (error) {
14557
- this.log('Failed to parse WebSocket message', {
14558
- type: 'error',
14559
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14560
- });
14561
- }
14562
- }
14563
- connectWebsocket() {
14564
- // Reset manual disconnect flag to allow reconnection logic
14565
- this.manualDisconnect = false;
14566
- if (this.connecting) {
14567
- this.log('Connection attempt skipped - already connecting');
14568
- return;
14569
- }
14570
- if (this.ws?.readyState === WebSocket.OPEN) {
14571
- this.log('Connection attempt skipped - already connected');
14572
- return;
14573
- }
14574
- this.connecting = true;
14575
- if (this.ws) {
14576
- this.ws.removeAllListeners();
14577
- this.ws.terminate();
14578
- this.ws = null;
14579
- }
14580
- this.log(`Connecting to WebSocket at ${this.wsUrl}...`);
14581
- this.ws = new WebSocket(this.wsUrl);
14582
- this.ws.on('open', async () => {
14583
- try {
14584
- this.log('WebSocket connected');
14585
- await this.authenticate();
14586
- await this.subscribeToTradeUpdates();
14587
- this.connecting = false;
14588
- }
14589
- catch (error) {
14590
- this.log('Failed to setup WebSocket connection', {
14591
- type: 'error',
14592
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14593
- });
14594
- this.ws?.close();
14595
- }
14596
- });
14597
- this.ws.on('message', (data) => {
14598
- this.handleMessage(data.toString());
14599
- });
14600
- this.ws.on('error', (error) => {
14601
- this.log('WebSocket error', {
14602
- type: 'error',
14603
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14604
- });
14605
- this.connecting = false;
14606
- });
14607
- this.ws.on('close', () => {
14608
- this.log('WebSocket connection closed');
14609
- this.authenticated = false;
14610
- this.connecting = false;
14611
- // Clear any existing reconnect timeout
14612
- if (this.reconnectTimeout) {
14613
- clearTimeout(this.reconnectTimeout);
14614
- this.reconnectTimeout = null;
14615
- }
14616
- // Schedule reconnection unless this was a manual disconnect
14617
- if (!this.manualDisconnect) {
14618
- this.reconnectTimeout = setTimeout(() => {
14619
- this.log('Attempting to reconnect...');
14620
- this.connectWebsocket();
14621
- }, this.reconnectDelay);
14622
- }
14623
- });
14624
- }
14625
- /**
14626
- * Cleanly disconnect from the WebSocket and stop auto-reconnects
14627
- */
14628
- disconnect() {
14629
- // Prevent auto-reconnect scheduling
14630
- this.manualDisconnect = true;
14631
- // Clear any scheduled reconnect
14632
- if (this.reconnectTimeout) {
14633
- clearTimeout(this.reconnectTimeout);
14634
- this.reconnectTimeout = null;
14635
- }
14636
- if (this.ws) {
14637
- this.log('Disconnecting WebSocket...');
14638
- // Remove listeners first to avoid duplicate handlers after reconnects
14639
- this.ws.removeAllListeners();
14640
- try {
14641
- // Attempt graceful close
14642
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
14643
- this.ws.close(1000, 'Client disconnect');
14644
- }
14645
- else {
14646
- this.ws.terminate();
14647
- }
14648
- }
14649
- catch {
14650
- // Fallback terminate on any error
14651
- try {
14652
- this.ws.terminate();
14653
- }
14654
- catch {
14655
- /* no-op */
14656
- }
14657
- }
14658
- this.ws = null;
14659
- }
14660
- this.authenticated = false;
14661
- this.connecting = false;
14662
- this.log('WebSocket disconnected');
14663
- }
14664
- async authenticate() {
14665
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14666
- throw new Error('WebSocket not ready for authentication');
14667
- }
14668
- const authMessage = {
14669
- action: 'auth',
14670
- key: this.credentials.apiKey,
14671
- secret: this.credentials.apiSecret,
14672
- };
14673
- this.ws.send(JSON.stringify(authMessage));
14674
- return new Promise((resolve, reject) => {
14675
- const authTimeout = setTimeout(() => {
14676
- this.log('Authentication timeout', { type: 'error' });
14677
- reject(new Error('Authentication timed out'));
14678
- }, 10000);
14679
- const handleAuthResponse = (data) => {
14680
- try {
14681
- const message = JSON.parse(data.toString());
14682
- if (message.stream === 'authorization') {
14683
- this.ws?.removeListener('message', handleAuthResponse);
14684
- clearTimeout(authTimeout);
14685
- if (message.data?.status === 'authorized') {
14686
- this.authenticated = true;
14687
- resolve();
14688
- }
14689
- else {
14690
- const error = `Authentication failed: ${message.data?.message || 'Unknown error'}`;
14691
- this.log(error, { type: 'error' });
14692
- reject(new Error(error));
14693
- }
14694
- }
14695
- }
14696
- catch (error) {
14697
- this.log('Failed to parse auth response', {
14698
- type: 'error',
14699
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14700
- });
14701
- }
14702
- };
14703
- this.ws?.on('message', handleAuthResponse);
14704
- });
14705
- }
14706
- async subscribeToTradeUpdates() {
14707
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.authenticated) {
14708
- throw new Error('WebSocket not ready for subscription');
14709
- }
14710
- const listenMessage = {
14711
- action: 'listen',
14712
- data: {
14713
- streams: ['trade_updates'],
14714
- },
14715
- };
14716
- this.ws.send(JSON.stringify(listenMessage));
14717
- return new Promise((resolve, reject) => {
14718
- const listenTimeout = setTimeout(() => {
14719
- reject(new Error('Subscribe timeout'));
14720
- }, 10000);
14721
- const handleListenResponse = (data) => {
14722
- try {
14723
- const message = JSON.parse(data.toString());
14724
- if (message.stream === 'listening') {
14725
- this.ws?.removeListener('message', handleListenResponse);
14726
- clearTimeout(listenTimeout);
14727
- if (message.data?.streams?.includes('trade_updates')) {
14728
- resolve();
14729
- }
14730
- else {
14731
- reject(new Error('Failed to subscribe to trade updates'));
14732
- }
14733
- }
14734
- }
14735
- catch (error) {
14736
- this.log('Failed to parse listen response', {
14737
- type: 'error',
14738
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14739
- });
14740
- }
14741
- };
14742
- this.ws?.on('message', handleListenResponse);
14743
- });
14744
- }
14745
- async makeRequest(endpoint, method = 'GET', body, queryString = '') {
14746
- const url = `${this.apiBaseUrl}${endpoint}${queryString}`;
14747
- try {
14748
- const response = await fetch(url, {
14749
- method,
14750
- headers: this.headers,
14751
- body: body ? JSON.stringify(body) : undefined,
14752
- });
14753
- if (!response.ok) {
14754
- const errorText = await response.text();
14755
- this.log(`Alpaca API error (${response.status}): ${errorText}`, { type: 'error' });
14756
- throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
14757
- }
14758
- // Handle responses with no content (e.g., 204 No Content)
14759
- if (response.status === 204 || response.headers.get('content-length') === '0') {
14760
- return null;
14761
- }
14762
- const contentType = response.headers.get('content-type');
14763
- if (contentType && contentType.includes('application/json')) {
14764
- return await response.json();
14765
- }
14766
- // For non-JSON responses, return the text content
14767
- const textContent = await response.text();
14768
- return textContent || null;
14769
- }
14770
- catch (err) {
14771
- const error = err;
14772
- this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
14773
- source: 'AlpacaAPI',
14774
- type: 'error',
14775
- });
14776
- throw error;
14777
- }
14778
- }
14779
- async getPositions(assetClass) {
14780
- const positions = (await this.makeRequest('/positions'));
14781
- if (assetClass) {
14782
- return positions.filter((position) => position.asset_class === assetClass);
14783
- }
14784
- return positions;
14785
- }
14786
- /**
14787
- * Get all orders
14788
- * @param params (GetOrdersParams) - optional parameters to filter the orders
14789
- * - status: 'open' | 'closed' | 'all'
14790
- * - limit: number
14791
- * - after: string
14792
- * - until: string
14793
- * - direction: 'asc' | 'desc'
14794
- * - nested: boolean
14795
- * - symbols: string[], an array of all the symbols
14796
- * - side: 'buy' | 'sell'
14797
- * @returns all orders
14798
- */
14799
- async getOrders(params = {}) {
14800
- const queryParams = new URLSearchParams();
14801
- if (params.status)
14802
- queryParams.append('status', params.status);
14803
- if (params.limit)
14804
- queryParams.append('limit', params.limit.toString());
14805
- if (params.after)
14806
- queryParams.append('after', params.after);
14807
- if (params.until)
14808
- queryParams.append('until', params.until);
14809
- if (params.direction)
14810
- queryParams.append('direction', params.direction);
14811
- if (params.nested)
14812
- queryParams.append('nested', params.nested.toString());
14813
- if (params.symbols)
14814
- queryParams.append('symbols', params.symbols.join(','));
14815
- if (params.side)
14816
- queryParams.append('side', params.side);
14817
- const endpoint = `/orders${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
14818
- try {
14819
- return await this.makeRequest(endpoint);
14820
- }
14821
- catch (error) {
14822
- this.log(`Error getting orders: ${error}`, { type: 'error' });
14823
- throw error;
14824
- }
14825
- }
14826
- async getAccountDetails() {
14827
- try {
14828
- return await this.makeRequest('/account');
14829
- }
14830
- catch (error) {
14831
- this.log(`Error getting account details: ${error}`, { type: 'error' });
14832
- throw error;
14833
- }
14834
- }
14835
- /**
14836
- * Create a trailing stop order
14837
- * @param symbol (string) - the symbol of the order
14838
- * @param qty (number) - the quantity of the order
14839
- * @param side (string) - the side of the order
14840
- * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
14841
- * @param position_intent (string) - the position intent of the order
14842
- */
14843
- async createTrailingStop(symbol, qty, side, trailPercent100, position_intent) {
14844
- this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
14845
- symbol,
14846
- });
14847
- try {
14848
- await this.makeRequest(`/orders`, 'POST', {
14849
- symbol,
14850
- qty: Math.abs(qty),
14851
- side,
14852
- position_intent,
14853
- order_class: 'simple',
14854
- type: 'trailing_stop',
14855
- trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
14856
- time_in_force: 'gtc',
14857
- });
14858
- }
14859
- catch (error) {
14860
- this.log(`Error creating trailing stop: ${error}`, {
14861
- symbol,
14862
- type: 'error',
14863
- });
14864
- throw error;
14865
- }
14866
- }
14867
- /**
14868
- * Create a market order
14869
- * @param symbol (string) - the symbol of the order
14870
- * @param qty (number) - the quantity of the order
14871
- * @param side (string) - the side of the order
14872
- * @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
14873
- * @param client_order_id (string) - optional client order id
14874
- */
14875
- async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
14876
- this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14877
- symbol,
14878
- });
14879
- const body = {
14880
- symbol,
14881
- qty: Math.abs(qty).toString(),
14882
- side,
14883
- position_intent,
14884
- type: 'market',
14885
- time_in_force: 'day',
14886
- order_class: 'simple',
14887
- };
14888
- if (client_order_id !== undefined) {
14889
- body.client_order_id = client_order_id;
14890
- }
14891
- try {
14892
- return await this.makeRequest('/orders', 'POST', body);
14893
- }
14894
- catch (error) {
14895
- this.log(`Error creating market order: ${error}`, { type: 'error' });
14896
- throw error;
14897
- }
14898
- }
14899
- /**
14900
- * Create a Market on Open (MOO) order - executes in the opening auction
14901
- *
14902
- * IMPORTANT TIMING CONSTRAINTS:
14903
- * - Valid submission window: After 7:00pm ET and before 9:28am ET
14904
- * - Orders submitted between 9:28am and 7:00pm ET will be REJECTED
14905
- * - Orders submitted after 7:00pm ET are queued for the next trading day's opening auction
14906
- * - Example: An order at 8:00pm Monday will execute at Tuesday's market open (9:30am)
14907
- *
14908
- * @param symbol - The symbol of the order
14909
- * @param qty - The quantity of shares
14910
- * @param side - Buy or sell
14911
- * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
14912
- * @param client_order_id - Optional client order id
14913
- * @returns The created order
14914
- */
14915
- async createMOOOrder(symbol, qty, side, position_intent, client_order_id) {
14916
- this.log(`Creating Market on Open order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14917
- symbol,
14918
- });
14919
- const body = {
14920
- symbol,
14921
- qty: Math.abs(qty).toString(),
14922
- side,
14923
- position_intent,
14924
- type: 'market',
14925
- time_in_force: 'opg',
14926
- order_class: 'simple',
14927
- };
14928
- if (client_order_id !== undefined) {
14929
- body.client_order_id = client_order_id;
14930
- }
14931
- try {
14932
- return await this.makeRequest('/orders', 'POST', body);
14933
- }
14934
- catch (error) {
14935
- this.log(`Error creating MOO order: ${error}`, { type: 'error' });
14936
- throw error;
14937
- }
14938
- }
14939
- /**
14940
- * Create a Market on Close (MOC) order - executes in the closing auction
14941
- *
14942
- * IMPORTANT TIMING CONSTRAINTS:
14943
- * - Valid submission window: After 7:00pm ET (previous day) and before 3:50pm ET (same day)
14944
- * - Orders submitted between 3:50pm and 7:00pm ET will be REJECTED
14945
- * - Orders submitted after 7:00pm ET are queued for the next trading day's closing auction
14946
- * - Example: An order at 8:00pm Monday will execute at Tuesday's market close (4:00pm)
14947
- *
14948
- * @param symbol - The symbol of the order
14949
- * @param qty - The quantity of shares
14950
- * @param side - Buy or sell
14951
- * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
14952
- * @param client_order_id - Optional client order id
14953
- * @returns The created order
14954
- */
14955
- async createMOCOrder(symbol, qty, side, position_intent, client_order_id) {
14956
- this.log(`Creating Market on Close order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14957
- symbol,
14958
- });
14959
- const body = {
14960
- symbol,
14961
- qty: Math.abs(qty).toString(),
14962
- side,
14963
- position_intent,
14964
- type: 'market',
14965
- time_in_force: 'cls',
14966
- order_class: 'simple',
14967
- };
14968
- if (client_order_id !== undefined) {
14969
- body.client_order_id = client_order_id;
14970
- }
14971
- try {
14972
- return await this.makeRequest('/orders', 'POST', body);
14973
- }
14974
- catch (error) {
14975
- this.log(`Error creating MOC order: ${error}`, { type: 'error' });
14976
- throw error;
14977
- }
14978
- }
14979
- /**
14980
- * Get the current trail percent for a symbol, assuming that it has an open position and a trailing stop order to close it. Because this relies on an orders request for one symbol, you can't do it too often.
14981
- * @param symbol (string) - the symbol of the order
14982
- * @returns the current trail percent
14983
- */
14984
- async getCurrentTrailPercent(symbol) {
14985
- try {
14986
- const orders = await this.getOrders({
14987
- status: 'open',
14988
- symbols: [symbol],
14989
- });
14990
- const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop' &&
14991
- (order.position_intent === 'sell_to_close' || order.position_intent === 'buy_to_close'));
14992
- if (!trailingStopOrder) {
14993
- this.log(`No closing trailing stop order found for ${symbol}`, {
14994
- symbol,
14995
- });
14996
- return null;
14997
- }
14998
- if (!trailingStopOrder.trail_percent) {
14999
- this.log(`Trailing stop order found for ${symbol} but no trail_percent value`, {
15000
- symbol,
15001
- });
15002
- return null;
15003
- }
15004
- const trailPercent = parseFloat(trailingStopOrder.trail_percent);
15005
- return trailPercent;
15006
- }
15007
- catch (error) {
15008
- this.log(`Error getting current trail percent: ${error}`, {
15009
- symbol,
15010
- type: 'error',
15011
- });
15012
- throw error;
15013
- }
15014
- }
15015
- /**
15016
- * Update the trail percent for a trailing stop order
15017
- * @param symbol (string) - the symbol of the order
15018
- * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
15019
- */
15020
- async updateTrailingStop(symbol, trailPercent100) {
15021
- // First get all open orders for this symbol
15022
- const orders = await this.getOrders({
15023
- status: 'open',
15024
- symbols: [symbol],
15025
- });
15026
- // Find the trailing stop order
15027
- const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop');
15028
- if (!trailingStopOrder) {
15029
- this.log(`No open trailing stop order found for ${symbol}`, { type: 'error', symbol });
15030
- return;
15031
- }
15032
- // Check if the trail_percent is already set to the desired value
15033
- const currentTrailPercent = trailingStopOrder.trail_percent ? parseFloat(trailingStopOrder.trail_percent) : null;
15034
- // Compare with a small epsilon to handle floating point precision
15035
- const epsilon = 0.0001;
15036
- if (currentTrailPercent !== null && Math.abs(currentTrailPercent - trailPercent100) < epsilon) {
15037
- this.log(`Trailing stop for ${symbol} already set to ${trailPercent100}% (current: ${currentTrailPercent}%), skipping update`, {
15038
- symbol,
15039
- });
15040
- return;
15041
- }
15042
- this.log(`Updating trailing stop for ${symbol} from ${currentTrailPercent}% to ${trailPercent100}%`, {
15043
- symbol,
15044
- });
15045
- try {
15046
- await this.makeRequest(`/orders/${trailingStopOrder.id}`, 'PATCH', {
15047
- trail: trailPercent100.toString(), // Changed from trail_percent to trail
15048
- });
15049
- }
15050
- catch (error) {
15051
- this.log(`Error updating trailing stop: ${error}`, {
15052
- symbol,
15053
- type: 'error',
15054
- });
15055
- throw error;
15056
- }
15057
- }
15058
- /**
15059
- * Cancel all open orders
15060
- */
15061
- async cancelAllOrders() {
15062
- this.log(`Canceling all open orders`);
15063
- try {
15064
- await this.makeRequest('/orders', 'DELETE');
15065
- }
15066
- catch (error) {
15067
- this.log(`Error canceling all orders: ${error}`, { type: 'error' });
15068
- }
15069
- }
15070
- /**
15071
- * Cancel a specific order by its ID
15072
- * @param orderId The id of the order to cancel
15073
- * @throws Error if the order is not cancelable (status 422) or if the order doesn't exist
15074
- * @returns Promise that resolves when the order is successfully canceled
15075
- */
15076
- async cancelOrder(orderId) {
15077
- this.log(`Attempting to cancel order ${orderId}`);
15078
- try {
15079
- await this.makeRequest(`/orders/${orderId}`, 'DELETE');
15080
- this.log(`Successfully canceled order ${orderId}`);
15081
- }
15082
- catch (error) {
15083
- // If the error is a 422, it means the order is not cancelable
15084
- if (error instanceof Error && error.message.includes('422')) {
15085
- this.log(`Order ${orderId} is not cancelable`, {
15086
- type: 'error',
15087
- });
15088
- throw new Error(`Order ${orderId} is not cancelable`);
15089
- }
15090
- // Re-throw other errors
15091
- throw error;
15092
- }
15093
- }
15094
- /**
15095
- * Create a limit order
15096
- * @param symbol (string) - the symbol of the order
15097
- * @param qty (number) - the quantity of the order
15098
- * @param side (string) - the side of the order
15099
- * @param limitPrice (number) - the limit price of the order
15100
- * @param position_intent (string) - the position intent of the order
15101
- * @param extended_hours (boolean) - whether the order is in extended hours
15102
- * @param client_order_id (string) - the client order id of the order
15103
- */
15104
- async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
15105
- this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
15106
- symbol,
15107
- });
15108
- const body = {
15109
- symbol,
15110
- qty: Math.abs(qty).toString(),
15111
- side,
15112
- position_intent,
15113
- type: 'limit',
15114
- limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
15115
- time_in_force: 'day',
15116
- order_class: 'simple',
15117
- extended_hours,
15118
- };
15119
- if (client_order_id !== undefined) {
15120
- body.client_order_id = client_order_id;
15121
- }
15122
- try {
15123
- return await this.makeRequest('/orders', 'POST', body);
15124
- }
15125
- catch (error) {
15126
- this.log(`Error creating limit order: ${error}`, { type: 'error' });
15127
- throw error;
15128
- }
15129
- }
15130
- /**
15131
- * Close all equities positions
15132
- * @param options (object) - the options for closing the positions
15133
- * - cancel_orders (boolean) - whether to cancel related orders
15134
- * - useLimitOrders (boolean) - whether to use limit orders to close the positions
15135
- */
15136
- async closeAllPositions(options = { cancel_orders: true, useLimitOrders: false }) {
15137
- this.log(`Closing all positions${options.useLimitOrders ? ' using limit orders' : ''}${options.cancel_orders ? ' and canceling open orders' : ''}`);
15138
- if (options.useLimitOrders) {
15139
- // Get all positions
15140
- const positions = await this.getPositions('us_equity');
15141
- if (positions.length === 0) {
15142
- this.log('No positions to close');
15143
- return;
15144
- }
15145
- this.log(`Found ${positions.length} positions to close`);
15146
- // Get latest quotes for all positions
15147
- const symbols = positions.map((position) => position.symbol);
15148
- const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15149
- const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
15150
- if (lengthOfQuotes === 0) {
15151
- this.log('No quotes available for positions, received 0 quotes', {
15152
- type: 'error',
15153
- });
15154
- return;
15155
- }
15156
- if (lengthOfQuotes !== positions.length) {
15157
- this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: 'warn' });
15158
- return;
15159
- }
15160
- // Create limit orders to close each position
15161
- for (const position of positions) {
15162
- const quote = quotesResponse.quotes[position.symbol];
15163
- if (!quote) {
15164
- this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15165
- symbol: position.symbol,
15166
- type: 'warn',
15167
- });
15168
- continue;
15169
- }
15170
- const qty = Math.abs(parseFloat(position.qty));
15171
- const side = position.side === 'long' ? 'sell' : 'buy';
15172
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15173
- // Get the current price from the quote
15174
- const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15175
- if (!currentPrice) {
15176
- this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15177
- symbol: position.symbol,
15178
- type: 'warn',
15179
- });
15180
- continue;
15181
- }
15182
- // Apply slippage from config
15183
- const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15184
- const limitPrice = side === 'sell'
15185
- ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15186
- : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15187
- this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15188
- symbol: position.symbol,
15189
- });
15190
- await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
15191
- }
15192
- }
15193
- else {
15194
- await this.makeRequest('/positions', 'DELETE', undefined, options.cancel_orders ? '?cancel_orders=true' : '');
15195
- }
15196
- }
15197
- /**
15198
- * Close all equities positions using limit orders during extended hours trading
15199
- * @param cancelOrders Whether to cancel related orders (default: true)
15200
- * @returns Promise that resolves when all positions are closed
15201
- */
15202
- async closeAllPositionsAfterHours() {
15203
- this.log('Closing all positions using limit orders during extended hours trading');
15204
- // Get all positions
15205
- const positions = await this.getPositions();
15206
- this.log(`Found ${positions.length} positions to close`);
15207
- if (positions.length === 0) {
15208
- this.log('No positions to close');
15209
- return;
15210
- }
15211
- await this.cancelAllOrders();
15212
- this.log(`Cancelled all open orders`);
15213
- // Get latest quotes for all positions
15214
- const symbols = positions.map((position) => position.symbol);
15215
- const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15216
- // Create limit orders to close each position
15217
- for (const position of positions) {
15218
- const quote = quotesResponse.quotes[position.symbol];
15219
- if (!quote) {
15220
- this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15221
- symbol: position.symbol,
15222
- type: 'warn',
15223
- });
15224
- continue;
15225
- }
15226
- const qty = Math.abs(parseFloat(position.qty));
15227
- const side = position.side === 'long' ? 'sell' : 'buy';
15228
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15229
- // Get the current price from the quote
15230
- const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15231
- if (!currentPrice) {
15232
- this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15233
- symbol: position.symbol,
15234
- type: 'warn',
15235
- });
15236
- continue;
15237
- }
15238
- // Apply slippage from config
15239
- const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15240
- const limitPrice = side === 'sell'
15241
- ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15242
- : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15243
- this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15244
- symbol: position.symbol,
15245
- });
15246
- await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true // Enable extended hours trading
15247
- );
15248
- }
15249
- this.log(`All positions closed: ${positions.map((p) => p.symbol).join(', ')}`);
15250
- }
15251
- onTradeUpdate(callback) {
15252
- this.tradeUpdateCallback = callback;
15253
- }
15254
- /**
15255
- * Get portfolio history for the account
15256
- * @param params Parameters for the portfolio history request
15257
- * @returns Portfolio history data
15258
- */
15259
- async getPortfolioHistory(params) {
15260
- const queryParams = new URLSearchParams();
15261
- if (params.timeframe)
15262
- queryParams.append('timeframe', params.timeframe);
15263
- if (params.period)
15264
- queryParams.append('period', params.period);
15265
- if (params.extended_hours !== undefined)
15266
- queryParams.append('extended_hours', params.extended_hours.toString());
15267
- if (params.start)
15268
- queryParams.append('start', params.start);
15269
- if (params.end)
15270
- queryParams.append('end', params.end);
15271
- if (params.date_end)
15272
- queryParams.append('date_end', params.date_end);
15273
- const response = await this.makeRequest(`/account/portfolio/history?${queryParams.toString()}`);
15274
- return response;
15275
- }
15276
- /**
15277
- * Get portfolio daily history for the account, ensuring the most recent day is included
15278
- * by combining daily and hourly history if needed.
15279
- *
15280
- * This function performs two API calls:
15281
- * 1. Retrieves daily portfolio history
15282
- * 2. Retrieves hourly portfolio history to check for more recent data
15283
- *
15284
- * If hourly history has timestamps more recent than the last timestamp in daily history,
15285
- * it appends one additional day to the daily history using the most recent hourly values.
15286
- *
15287
- * @param params Parameters for the portfolio history request (same as getPortfolioHistory except timeframe is forced to '1D')
15288
- * @returns Portfolio history data with daily timeframe, including the most recent day if available from hourly data
15289
- */
15290
- async getPortfolioDailyHistory(params) {
15291
- // Get daily and hourly history in parallel
15292
- const dailyParams = { ...params, timeframe: '1D' };
15293
- const hourlyParams = { timeframe: '1Min', period: '1D' };
15294
- const [dailyHistory, hourlyHistory] = await Promise.all([
15295
- this.getPortfolioHistory(dailyParams),
15296
- this.getPortfolioHistory(hourlyParams),
15297
- ]);
15298
- // If no hourly history, return daily as-is
15299
- if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
15300
- return dailyHistory;
15301
- }
15302
- // Get the last timestamp from daily history
15303
- const lastDailyTimestamp = dailyHistory.timestamp[dailyHistory.timestamp.length - 1];
15304
- // Check if hourly history has more recent data
15305
- const recentHourlyData = hourlyHistory.timestamp
15306
- .map((timestamp, index) => ({ timestamp, index }))
15307
- .filter(({ timestamp }) => timestamp > lastDailyTimestamp);
15308
- // If no more recent hourly data, return daily history as-is
15309
- if (recentHourlyData.length === 0) {
15310
- return dailyHistory;
15311
- }
15312
- // Get the most recent hourly data point
15313
- const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
15314
- const mostRecentIndex = mostRecentHourly.index;
15315
- // Calculate the timestamp for the new daily entry.
15316
- // Alpaca's daily history timestamps are at 00:00:00Z for the calendar day
15317
- // following the NY trading date. Derive the trading date in NY time from the
15318
- // most recent intraday timestamp, then set the new daily timestamp to
15319
- // midnight UTC of the next calendar day.
15320
- const mostRecentMs = mostRecentHourly.timestamp * 1000; // hourly timestamps are seconds
15321
- const tradingDateStr = getTradingDate(new Date(mostRecentMs)); // e.g., '2025-09-05' (NY trading date)
15322
- const [yearStr, monthStr, dayStr] = tradingDateStr.split('-');
15323
- const year = Number(yearStr);
15324
- const month = Number(monthStr); // 1-based
15325
- const day = Number(dayStr);
15326
- const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1000);
15327
- // Create a new daily history entry with the most recent hourly values
15328
- const updatedDailyHistory = {
15329
- ...dailyHistory,
15330
- timestamp: [...dailyHistory.timestamp, newDailyTimestamp],
15331
- equity: [...dailyHistory.equity, hourlyHistory.equity[mostRecentIndex]],
15332
- profit_loss: [...dailyHistory.profit_loss, hourlyHistory.profit_loss[mostRecentIndex]],
15333
- profit_loss_pct: [...dailyHistory.profit_loss_pct, hourlyHistory.profit_loss_pct[mostRecentIndex]],
15334
- };
15335
- return updatedDailyHistory;
15336
- }
15337
- /**
15338
- * Get option contracts based on specified parameters
15339
- * @param params Parameters to filter option contracts
15340
- * @returns Option contracts matching the criteria
15341
- */
15342
- async getOptionContracts(params) {
15343
- const queryParams = new URLSearchParams();
15344
- queryParams.append('underlying_symbols', params.underlying_symbols.join(','));
15345
- if (params.expiration_date_gte)
15346
- queryParams.append('expiration_date_gte', params.expiration_date_gte);
15347
- if (params.expiration_date_lte)
15348
- queryParams.append('expiration_date_lte', params.expiration_date_lte);
15349
- if (params.strike_price_gte)
15350
- queryParams.append('strike_price_gte', params.strike_price_gte);
15351
- if (params.strike_price_lte)
15352
- queryParams.append('strike_price_lte', params.strike_price_lte);
15353
- if (params.type)
15354
- queryParams.append('type', params.type);
15355
- if (params.status)
15356
- queryParams.append('status', params.status);
15357
- if (params.limit)
15358
- queryParams.append('limit', params.limit.toString());
15359
- if (params.page_token)
15360
- queryParams.append('page_token', params.page_token);
15361
- this.log(`Fetching option contracts for ${params.underlying_symbols.join(', ')}`, {
15362
- symbol: params.underlying_symbols.join(', '),
15363
- });
15364
- const response = (await this.makeRequest(`/options/contracts?${queryParams.toString()}`));
15365
- this.log(`Found ${response.option_contracts.length} option contracts`, {
15366
- symbol: params.underlying_symbols.join(', '),
15367
- });
15368
- return response;
15369
- }
15370
- /**
15371
- * Get a specific option contract by symbol or ID
15372
- * @param symbolOrId The symbol or ID of the option contract
15373
- * @returns The option contract details
15374
- */
15375
- async getOptionContract(symbolOrId) {
15376
- this.log(`Fetching option contract details for ${symbolOrId}`, {
15377
- symbol: symbolOrId,
15378
- });
15379
- const response = (await this.makeRequest(`/options/contracts/${symbolOrId}`));
15380
- this.log(`Found option contract details for ${symbolOrId}: ${response.name}`, {
15381
- symbol: symbolOrId,
15382
- });
15383
- return response;
15384
- }
15385
- /**
15386
- * Create a simple option order (market or limit)
15387
- * @param symbol Option contract symbol
15388
- * @param qty Quantity of contracts (must be a whole number)
15389
- * @param side Buy or sell
15390
- * @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
15391
- * @param type Order type (market or limit)
15392
- * @param limitPrice Limit price (required for limit orders)
15393
- * @returns The created order
15394
- */
15395
- async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
15396
- if (!Number.isInteger(qty) || qty <= 0) {
15397
- this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15398
- }
15399
- if (type === 'limit' && limitPrice === undefined) {
15400
- this.log('Limit price is required for limit orders', { type: 'error' });
15401
- }
15402
- this.log(`Creating ${type} option order for ${symbol}: ${side} ${qty} contracts (${position_intent})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15403
- symbol,
15404
- });
15405
- const orderData = {
15406
- symbol,
15407
- qty: qty.toString(),
15408
- side,
15409
- position_intent,
15410
- type,
15411
- time_in_force: 'day',
15412
- order_class: 'simple',
15413
- extended_hours: false,
15414
- };
15415
- if (type === 'limit' && limitPrice !== undefined) {
15416
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15417
- }
15418
- return this.makeRequest('/orders', 'POST', orderData);
15419
- }
15420
- /**
15421
- * Create a multi-leg option order
15422
- * @param legs Array of order legs
15423
- * @param qty Quantity of the multi-leg order (must be a whole number)
15424
- * @param type Order type (market or limit)
15425
- * @param limitPrice Limit price (required for limit orders)
15426
- * @returns The created multi-leg order
15427
- */
15428
- async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
15429
- if (!Number.isInteger(qty) || qty <= 0) {
15430
- this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15431
- }
15432
- if (type === 'limit' && limitPrice === undefined) {
15433
- this.log('Limit price is required for limit orders', { type: 'error' });
15434
- }
15435
- if (legs.length < 2) {
15436
- this.log('Multi-leg orders require at least 2 legs', { type: 'error' });
15437
- }
15438
- const legSymbols = legs.map((leg) => leg.symbol).join(', ');
15439
- this.log(`Creating multi-leg ${type} option order with ${legs.length} legs (${legSymbols})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15440
- symbol: legSymbols,
15441
- });
15442
- const orderData = {
15443
- order_class: 'mleg',
15444
- qty: qty.toString(),
15445
- type,
15446
- time_in_force: 'day',
15447
- legs,
15448
- };
15449
- if (type === 'limit' && limitPrice !== undefined) {
15450
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15451
- }
15452
- return this.makeRequest('/orders', 'POST', orderData);
15453
- }
15454
- /**
15455
- * Exercise an option contract
15456
- * @param symbolOrContractId The symbol or ID of the option contract to exercise
15457
- * @returns Response from the exercise request
15458
- */
15459
- async exerciseOption(symbolOrContractId) {
15460
- this.log(`Exercising option contract ${symbolOrContractId}`, {
15461
- symbol: symbolOrContractId,
15462
- });
15463
- return this.makeRequest(`/positions/${symbolOrContractId}/exercise`, 'POST');
15464
- }
15465
- /**
15466
- * Get option positions
15467
- * @returns Array of option positions
15468
- */
15469
- async getOptionPositions() {
15470
- this.log('Fetching option positions');
15471
- const positions = await this.getPositions('us_option');
15472
- return positions;
15473
- }
15474
- async getOptionsOpenSpreadTrades() {
15475
- this.log('Fetching option open trades');
15476
- // this function will get all open positions, extract the symbol and see when they were created.
15477
- // figures out when the earliest date was (should be today)
15478
- // then it pulls all orders after the earliest date that were closed and that were of class 'mleg'
15479
- // Each of these contains two orders. they look like this:
15480
- }
15481
- /**
15482
- * Get option account activities (exercises, assignments, expirations)
15483
- * @param activityType Type of option activity to filter by
15484
- * @param date Date to filter activities (YYYY-MM-DD format)
15485
- * @returns Array of option account activities
15486
- */
15487
- async getOptionActivities(activityType, date) {
15488
- const queryParams = new URLSearchParams();
15489
- if (activityType) {
15490
- queryParams.append('activity_types', activityType);
15491
- }
15492
- else {
15493
- queryParams.append('activity_types', 'OPEXC,OPASN,OPEXP');
15494
- }
15495
- if (date) {
15496
- queryParams.append('date', date);
15497
- }
15498
- this.log(`Fetching option activities${activityType ? ` of type ${activityType}` : ''}${date ? ` for date ${date}` : ''}`);
15499
- return this.makeRequest(`/account/activities?${queryParams.toString()}`);
15500
- }
15501
- /**
15502
- * Create a long call spread (buy lower strike call, sell higher strike call)
15503
- * @param lowerStrikeCallSymbol Symbol of the lower strike call option
15504
- * @param higherStrikeCallSymbol Symbol of the higher strike call option
15505
- * @param qty Quantity of spreads to create (must be a whole number)
15506
- * @param limitPrice Limit price for the spread
15507
- * @returns The created multi-leg order
15508
- */
15509
- async createLongCallSpread(lowerStrikeCallSymbol, higherStrikeCallSymbol, qty, limitPrice) {
15510
- this.log(`Creating long call spread: Buy ${lowerStrikeCallSymbol}, Sell ${higherStrikeCallSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15511
- symbol: `${lowerStrikeCallSymbol},${higherStrikeCallSymbol}`,
15512
- });
15513
- const legs = [
15514
- {
15515
- symbol: lowerStrikeCallSymbol,
15516
- ratio_qty: '1',
15517
- side: 'buy',
15518
- position_intent: 'buy_to_open',
15519
- },
15520
- {
15521
- symbol: higherStrikeCallSymbol,
15522
- ratio_qty: '1',
15523
- side: 'sell',
15524
- position_intent: 'sell_to_open',
15525
- },
15526
- ];
15527
- return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15528
- }
15529
- /**
15530
- * Create a long put spread (buy higher strike put, sell lower strike put)
15531
- * @param higherStrikePutSymbol Symbol of the higher strike put option
15532
- * @param lowerStrikePutSymbol Symbol of the lower strike put option
15533
- * @param qty Quantity of spreads to create (must be a whole number)
15534
- * @param limitPrice Limit price for the spread
15535
- * @returns The created multi-leg order
15536
- */
15537
- async createLongPutSpread(higherStrikePutSymbol, lowerStrikePutSymbol, qty, limitPrice) {
15538
- this.log(`Creating long put spread: Buy ${higherStrikePutSymbol}, Sell ${lowerStrikePutSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15539
- symbol: `${higherStrikePutSymbol},${lowerStrikePutSymbol}`,
15540
- });
15541
- const legs = [
15542
- {
15543
- symbol: higherStrikePutSymbol,
15544
- ratio_qty: '1',
15545
- side: 'buy',
15546
- position_intent: 'buy_to_open',
15547
- },
15548
- {
15549
- symbol: lowerStrikePutSymbol,
15550
- ratio_qty: '1',
15551
- side: 'sell',
15552
- position_intent: 'sell_to_open',
15553
- },
15554
- ];
15555
- return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15556
- }
15557
- /**
15558
- * Create an iron condor (sell call spread and put spread)
15559
- * @param longPutSymbol Symbol of the lower strike put (long)
15560
- * @param shortPutSymbol Symbol of the higher strike put (short)
15561
- * @param shortCallSymbol Symbol of the lower strike call (short)
15562
- * @param longCallSymbol Symbol of the higher strike call (long)
15563
- * @param qty Quantity of iron condors to create (must be a whole number)
15564
- * @param limitPrice Limit price for the iron condor (credit)
15565
- * @returns The created multi-leg order
15566
- */
15567
- async createIronCondor(longPutSymbol, shortPutSymbol, shortCallSymbol, longCallSymbol, qty, limitPrice) {
15568
- this.log(`Creating iron condor with ${qty} contracts at $${limitPrice.toFixed(2)}`, {
15569
- symbol: `${longPutSymbol},${shortPutSymbol},${shortCallSymbol},${longCallSymbol}`,
15570
- });
15571
- const legs = [
15572
- {
15573
- symbol: longPutSymbol,
15574
- ratio_qty: '1',
15575
- side: 'buy',
15576
- position_intent: 'buy_to_open',
15577
- },
15578
- {
15579
- symbol: shortPutSymbol,
15580
- ratio_qty: '1',
15581
- side: 'sell',
15582
- position_intent: 'sell_to_open',
15583
- },
15584
- {
15585
- symbol: shortCallSymbol,
15586
- ratio_qty: '1',
15587
- side: 'sell',
15588
- position_intent: 'sell_to_open',
15589
- },
15590
- {
15591
- symbol: longCallSymbol,
15592
- ratio_qty: '1',
15593
- side: 'buy',
15594
- position_intent: 'buy_to_open',
15595
- },
15596
- ];
15597
- try {
15598
- return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15599
- }
15600
- catch (error) {
15601
- this.log(`Error creating iron condor: ${error}`, { type: 'error' });
15602
- throw error;
15603
- }
15604
- }
15605
- /**
15606
- * Create a covered call (sell call option against owned stock)
15607
- * @param stockSymbol Symbol of the underlying stock
15608
- * @param callOptionSymbol Symbol of the call option to sell
15609
- * @param qty Quantity of covered calls to create (must be a whole number)
15610
- * @param limitPrice Limit price for the call option
15611
- * @returns The created order
15612
- */
15613
- async createCoveredCall(stockSymbol, callOptionSymbol, qty, limitPrice) {
15614
- this.log(`Creating covered call: Sell ${callOptionSymbol} against ${stockSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15615
- symbol: `${stockSymbol},${callOptionSymbol}`,
15616
- });
15617
- // For covered calls, we don't need to include the stock leg if we already own the shares
15618
- // We just create a simple sell order for the call option
15619
- try {
15620
- return await this.createOptionOrder(callOptionSymbol, qty, 'sell', 'sell_to_open', 'limit', limitPrice);
15621
- }
15622
- catch (error) {
15623
- this.log(`Error creating covered call: ${error}`, { type: 'error' });
15624
- throw error;
15625
- }
15626
- }
15627
- /**
15628
- * Roll an option position to a new expiration or strike
15629
- * @param currentOptionSymbol Symbol of the current option position
15630
- * @param newOptionSymbol Symbol of the new option to roll to
15631
- * @param qty Quantity of options to roll (must be a whole number)
15632
- * @param currentPositionSide Side of the current position ('buy' or 'sell')
15633
- * @param limitPrice Net limit price for the roll
15634
- * @returns The created multi-leg order
15635
- */
15636
- async rollOptionPosition(currentOptionSymbol, newOptionSymbol, qty, currentPositionSide, limitPrice) {
15637
- this.log(`Rolling ${qty} ${currentOptionSymbol} to ${newOptionSymbol} at net price $${limitPrice.toFixed(2)}`, {
15638
- symbol: `${currentOptionSymbol},${newOptionSymbol}`,
15639
- });
15640
- // If current position is long, we need to sell to close and buy to open
15641
- // If current position is short, we need to buy to close and sell to open
15642
- const closePositionSide = currentPositionSide === 'buy' ? 'sell' : 'buy';
15643
- const openPositionSide = currentPositionSide;
15644
- const closePositionIntent = closePositionSide === 'buy' ? 'buy_to_close' : 'sell_to_close';
15645
- const openPositionIntent = openPositionSide === 'buy' ? 'buy_to_open' : 'sell_to_open';
15646
- const legs = [
15647
- {
15648
- symbol: currentOptionSymbol,
15649
- ratio_qty: '1',
15650
- side: closePositionSide,
15651
- position_intent: closePositionIntent,
15652
- },
15653
- {
15654
- symbol: newOptionSymbol,
15655
- ratio_qty: '1',
15656
- side: openPositionSide,
15657
- position_intent: openPositionIntent,
15658
- },
15659
- ];
15660
- try {
15661
- return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15662
- }
15663
- catch (error) {
15664
- this.log(`Error rolling option position: ${error}`, { type: 'error' });
15665
- throw error;
15666
- }
15667
- }
15668
- /**
15669
- * Get option chain for a specific underlying symbol and expiration date
15670
- * @param underlyingSymbol The underlying stock symbol
15671
- * @param expirationDate The expiration date (YYYY-MM-DD format)
15672
- * @returns Option contracts for the specified symbol and expiration date
15673
- */
15674
- async getOptionChain(underlyingSymbol, expirationDate) {
15675
- this.log(`Fetching option chain for ${underlyingSymbol} with expiration date ${expirationDate}`, {
15676
- symbol: underlyingSymbol,
15677
- });
15678
- try {
15679
- const params = {
15680
- underlying_symbols: [underlyingSymbol],
15681
- expiration_date_gte: expirationDate,
15682
- expiration_date_lte: expirationDate,
15683
- status: 'active',
15684
- limit: 500, // Get a large number to ensure we get all strikes
15685
- };
15686
- const response = await this.getOptionContracts(params);
15687
- return response.option_contracts || [];
15688
- }
15689
- catch (error) {
15690
- this.log(`Failed to fetch option chain for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15691
- type: 'error',
15692
- symbol: underlyingSymbol,
15693
- });
15694
- return [];
15695
- }
15696
- }
15697
- /**
15698
- * Get all available expiration dates for a specific underlying symbol
15699
- * @param underlyingSymbol The underlying stock symbol
15700
- * @returns Array of available expiration dates
15701
- */
15702
- async getOptionExpirationDates(underlyingSymbol) {
15703
- this.log(`Fetching available expiration dates for ${underlyingSymbol}`, {
15704
- symbol: underlyingSymbol,
15705
- });
15706
- try {
15707
- const params = {
15708
- underlying_symbols: [underlyingSymbol],
15709
- status: 'active',
15710
- limit: 1000, // Get a large number to ensure we get contracts with all expiration dates
15711
- };
15712
- const response = await this.getOptionContracts(params);
15713
- // Extract unique expiration dates
15714
- const expirationDates = new Set();
15715
- if (response.option_contracts) {
15716
- response.option_contracts.forEach((contract) => {
15717
- expirationDates.add(contract.expiration_date);
15718
- });
15719
- }
15720
- // Convert to array and sort
15721
- return Array.from(expirationDates).sort();
15722
- }
15723
- catch (error) {
15724
- this.log(`Failed to fetch expiration dates for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15725
- type: 'error',
15726
- symbol: underlyingSymbol,
15727
- });
15728
- return [];
15729
- }
15730
- }
15731
- /**
15732
- * Get the current options trading level for the account
15733
- * @returns The options trading level (0-3)
15734
- */
15735
- async getOptionsTradingLevel() {
15736
- this.log('Fetching options trading level');
15737
- const accountDetails = await this.getAccountDetails();
15738
- return accountDetails.options_trading_level || 0;
15739
- }
15740
- /**
15741
- * Check if the account has options trading enabled
15742
- * @returns Boolean indicating if options trading is enabled
15743
- */
15744
- async isOptionsEnabled() {
15745
- this.log('Checking if options trading is enabled');
15746
- const accountDetails = await this.getAccountDetails();
15747
- // Check if options trading level is 2 or higher (Level 2+ allows buying calls/puts)
15748
- // Level 0: Options disabled
15749
- // Level 1: Only covered calls and cash-secured puts
15750
- // Level 2+: Can buy calls and puts (required for executeOptionsOrder)
15751
- const optionsLevel = accountDetails.options_trading_level || 0;
15752
- const isEnabled = optionsLevel >= 2;
15753
- this.log(`Options trading level: ${optionsLevel}, enabled: ${isEnabled}`);
15754
- return isEnabled;
15755
- }
15756
- /**
15757
- * Close all option positions
15758
- * @param cancelOrders Whether to cancel related orders (default: true)
15759
- * @returns Response from the close positions request
15760
- */
15761
- async closeAllOptionPositions(cancelOrders = true) {
15762
- this.log(`Closing all option positions${cancelOrders ? ' and canceling related orders' : ''}`);
15763
- const optionPositions = await this.getOptionPositions();
15764
- if (optionPositions.length === 0) {
15765
- this.log('No option positions to close');
15766
- return;
15767
- }
15768
- // Create market orders to close each position
15769
- for (const position of optionPositions) {
15770
- const side = position.side === 'long' ? 'sell' : 'buy';
15771
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15772
- this.log(`Closing ${position.side} position of ${position.qty} contracts for ${position.symbol}`, {
15773
- symbol: position.symbol,
15774
- });
15775
- await this.createOptionOrder(position.symbol, parseInt(position.qty), side, positionIntent, 'market');
15776
- }
15777
- if (cancelOrders) {
15778
- // Get all open option orders
15779
- const orders = await this.getOrders({ status: 'open' });
15780
- const optionOrders = orders.filter((order) => order.asset_class === 'us_option');
15781
- // Cancel each open option order
15782
- for (const order of optionOrders) {
15783
- this.log(`Canceling open order for ${order.symbol}`, {
15784
- symbol: order.symbol,
15785
- });
15786
- await this.makeRequest(`/orders/${order.id}`, 'DELETE');
15787
- }
15788
- }
15789
- }
15790
- /**
15791
- * Close a specific option position
15792
- * @param symbol The option contract symbol
15793
- * @param qty Optional quantity to close (defaults to entire position)
15794
- * @returns The created order
15795
- */
15796
- async closeOptionPosition(symbol, qty) {
15797
- this.log(`Closing option position for ${symbol}${qty ? ` (${qty} contracts)` : ''}`, {
15798
- symbol,
15799
- });
15800
- // Get the position details
15801
- const positions = await this.getOptionPositions();
15802
- const position = positions.find((p) => p.symbol === symbol);
15803
- if (!position) {
15804
- throw new Error(`No position found for option contract ${symbol}`);
15805
- }
15806
- const quantityToClose = qty || parseInt(position.qty);
15807
- const side = position.side === 'long' ? 'sell' : 'buy';
15808
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15809
- try {
15810
- return await this.createOptionOrder(symbol, quantityToClose, side, positionIntent, 'market');
15811
- }
15812
- catch (error) {
15813
- this.log(`Error closing option position: ${error}`, { type: 'error' });
15814
- throw error;
15815
- }
15816
- }
15817
- /**
15818
- * Create a complete equities trade with optional stop loss and take profit
15819
- * @param params Trade parameters including symbol, qty, side, and optional referencePrice
15820
- * @param options Trade options including order type, extended hours, stop loss, and take profit settings
15821
- * @returns The created order
15822
- */
15823
- async createEquitiesTrade(params, options) {
15824
- const { symbol, qty, side, referencePrice } = params;
15825
- const { type = 'market', limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
15826
- // Validation: Extended hours + market order is not allowed
15827
- if (extendedHours && type === 'market') {
15828
- this.log('Cannot create market order with extended hours enabled', {
15829
- symbol,
15830
- type: 'error',
15831
- });
15832
- throw new Error('Cannot create market order with extended hours enabled');
15833
- }
15834
- // Validation: Limit orders require limit price
15835
- if (type === 'limit' && limitPrice === undefined) {
15836
- this.log('Limit price is required for limit orders', {
15837
- symbol,
15838
- type: 'error',
15839
- });
15840
- throw new Error('Limit price is required for limit orders');
15841
- }
15842
- let calculatedStopPrice;
15843
- let calculatedTakeProfitPrice;
15844
- // Handle stop loss validation and calculation
15845
- if (useStopLoss) {
15846
- if (stopPrice === undefined && stopPercent100 === undefined) {
15847
- this.log('Either stopPrice or stopPercent100 must be provided when useStopLoss is true', {
15848
- symbol,
15849
- type: 'error',
15850
- });
15851
- throw new Error('Either stopPrice or stopPercent100 must be provided when useStopLoss is true');
15852
- }
15853
- if (stopPercent100 !== undefined) {
15854
- if (referencePrice === undefined) {
15855
- this.log('referencePrice is required when using stopPercent100', {
15856
- symbol,
15857
- type: 'error',
15858
- });
15859
- throw new Error('referencePrice is required when using stopPercent100');
15860
- }
15861
- // Calculate stop price based on percentage and side
15862
- const stopPercentDecimal = stopPercent100 / 100;
15863
- if (side === 'buy') {
15864
- // For buy orders, stop loss is below the reference price
15865
- calculatedStopPrice = referencePrice * (1 - stopPercentDecimal);
15866
- }
15867
- else {
15868
- // For sell orders, stop loss is above the reference price
15869
- calculatedStopPrice = referencePrice * (1 + stopPercentDecimal);
15870
- }
15871
- }
15872
- else {
15873
- calculatedStopPrice = stopPrice;
15874
- }
15875
- }
15876
- // Handle take profit validation and calculation
15877
- if (useTakeProfit) {
15878
- if (takeProfitPrice === undefined && takeProfitPercent100 === undefined) {
15879
- this.log('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true', {
15880
- symbol,
15881
- type: 'error',
15882
- });
15883
- throw new Error('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true');
15884
- }
15885
- if (takeProfitPercent100 !== undefined) {
15886
- if (referencePrice === undefined) {
15887
- this.log('referencePrice is required when using takeProfitPercent100', {
15888
- symbol,
15889
- type: 'error',
15890
- });
15891
- throw new Error('referencePrice is required when using takeProfitPercent100');
15892
- }
15893
- // Calculate take profit price based on percentage and side
15894
- const takeProfitPercentDecimal = takeProfitPercent100 / 100;
15895
- if (side === 'buy') {
15896
- // For buy orders, take profit is above the reference price
15897
- calculatedTakeProfitPrice = referencePrice * (1 + takeProfitPercentDecimal);
15898
- }
15899
- else {
15900
- // For sell orders, take profit is below the reference price
15901
- calculatedTakeProfitPrice = referencePrice * (1 - takeProfitPercentDecimal);
15902
- }
15903
- }
15904
- else {
15905
- calculatedTakeProfitPrice = takeProfitPrice;
15906
- }
15907
- }
15908
- // Determine order class based on what's enabled
15909
- let orderClass = 'simple';
15910
- if (useStopLoss && useTakeProfit) {
15911
- orderClass = 'bracket';
15912
- }
15913
- else if (useStopLoss || useTakeProfit) {
15914
- orderClass = 'oto';
15915
- }
15916
- // Build the order request
15917
- const orderData = {
15918
- symbol,
15919
- qty: Math.abs(qty).toString(),
15920
- side,
15921
- type,
15922
- time_in_force: 'day',
15923
- order_class: orderClass,
15924
- extended_hours: extendedHours,
15925
- position_intent: side === 'buy' ? 'buy_to_open' : 'sell_to_open',
15926
- };
15927
- if (clientOrderId) {
15928
- orderData.client_order_id = clientOrderId;
15929
- }
15930
- // Add limit price for limit orders
15931
- if (type === 'limit' && limitPrice !== undefined) {
15932
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15933
- }
15934
- // Add stop loss if enabled
15935
- if (useStopLoss && calculatedStopPrice !== undefined) {
15936
- orderData.stop_loss = {
15937
- stop_price: this.roundPriceForAlpaca(calculatedStopPrice).toString(),
15938
- };
15939
- }
15940
- // Add take profit if enabled
15941
- if (useTakeProfit && calculatedTakeProfitPrice !== undefined) {
15942
- orderData.take_profit = {
15943
- limit_price: this.roundPriceForAlpaca(calculatedTakeProfitPrice).toString(),
15944
- };
15945
- }
15946
- const logMessage = `Creating ${orderClass} ${type} ${side} order for ${symbol}: ${qty} shares${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}${useStopLoss ? ` with stop loss at $${calculatedStopPrice?.toFixed(2)}` : ''}${useTakeProfit ? ` with take profit at $${calculatedTakeProfitPrice?.toFixed(2)}` : ''}${extendedHours ? ' (extended hours)' : ''}`;
15947
- this.log(logMessage, {
15948
- symbol,
15949
- });
15950
- try {
15951
- return await this.makeRequest('/orders', 'POST', orderData);
15952
- }
15953
- catch (error) {
15954
- this.log(`Error creating equities trade: ${error}`, {
15955
- symbol,
15956
- type: 'error',
15957
- });
15958
- throw error;
15959
- }
15216
+ // Test file for context functionality
15217
+ //testGetMarketStatus();
15218
+ // Test getTradingDate for grouping data
15219
+ function testGetTradingDate() {
15220
+ const testCases = [
15221
+ {
15222
+ label: 'Market day (regular)',
15223
+ input: '2025-07-10T10:00:00-04:00',
15224
+ expected: '2025-07-10',
15225
+ },
15226
+ {
15227
+ label: 'Weekend (Saturday, should return it anyway )',
15228
+ input: '2025-07-12T12:00:00-04:00',
15229
+ expected: '2025-07-12',
15230
+ },
15231
+ {
15232
+ label: 'Holiday (Independence Day, should return it anyway)',
15233
+ input: '2025-07-04T12:00:00-04:00',
15234
+ expected: '2025-07-04',
15235
+ },
15236
+ {
15237
+ label: 'Input as Date object',
15238
+ input: new Date('2025-07-10T10:00:00-04:00'),
15239
+ expected: '2025-07-10',
15240
+ },
15241
+ {
15242
+ label: 'Input as timestamp (number)',
15243
+ input: new Date('2025-07-10T10:00:00-04:00').getTime(),
15244
+ expected: '2025-07-10',
15245
+ },
15246
+ {
15247
+ label: 'Input as ISO string',
15248
+ input: '2025-07-10T10:00:00-04:00',
15249
+ expected: '2025-07-10',
15250
+ },
15251
+ {
15252
+ label: 'Edge case (Jan 24, 2026, early morning EST)',
15253
+ input: '2026-01-24T00:30:00-05:00',
15254
+ expected: '2026-01-24',
15255
+ },
15256
+ {
15257
+ label: 'Edge case (Jan 24, 2026, late night EST)',
15258
+ input: '2026-01-24T23:59:59-05:00',
15259
+ expected: '2026-01-24',
15260
+ },
15261
+ {
15262
+ label: 'Edge case (Jan 25, 2026, start of day EST)',
15263
+ input: '2026-01-25T00:00:00-05:00',
15264
+ expected: '2026-01-25',
15265
+ },
15266
+ {
15267
+ label: 'Edge case (Jan 25, 2026, late night EST)',
15268
+ input: '2026-01-25T23:59:59-05:00',
15269
+ expected: '2026-01-25',
15270
+ },
15271
+ {
15272
+ label: 'Edge case (Jan 26, 2026, start of day EST)',
15273
+ input: '2026-01-26T00:00:00-05:00',
15274
+ expected: '2026-01-26',
15275
+ },
15276
+ {
15277
+ label: 'Edge case (Jan 26, 2026, 02:00Z = Jan 25, 21:00 EST)',
15278
+ input: '2026-01-26T02:00:00Z',
15279
+ expected: '2026-01-25',
15280
+ },
15281
+ {
15282
+ label: 'Edge case (Jan 26, 2026, 04:59:59Z = Jan 25, 23:59:59 EST)',
15283
+ input: '2026-01-26T04:59:59Z',
15284
+ expected: '2026-01-25',
15285
+ },
15286
+ {
15287
+ label: 'Edge case (Jan 26, 2026, 05:00:00Z = Jan 26, 00:00:00 EST)',
15288
+ input: '2026-01-26T05:00:00Z',
15289
+ expected: '2026-01-26',
15290
+ },
15291
+ {
15292
+ label: 'Edge case (Jan 24, 2026, timestamp input EST)',
15293
+ input: new Date('2026-01-24T12:00:00-05:00').getTime(),
15294
+ expected: '2026-01-24',
15295
+ },
15296
+ ];
15297
+ for (const { label, input, expected } of testCases) {
15298
+ const result = disco.time.getTradingDate(input);
15299
+ const pass = result === expected;
15300
+ console.log(`\nTest: ${label}`);
15301
+ console.log(` Input: ${input instanceof Date ? input.toISOString() : input}`);
15302
+ console.log(` Result: ${result} ${pass ? '✓' : `✗ (expected ${expected})`}`);
15303
+ if (!pass) {
15304
+ console.error(` FAILED: Expected ${expected}, got ${result}`);
15305
+ }
15960
15306
  }
15961
15307
  }
15962
-
15963
- // Test file for context functionality
15964
- async function testMOOAndMOCOrders() {
15965
- console.log('\n--- Testing Market on Open and Market on Close Orders ---');
15966
- console.log('NOTE: MOO orders must be submitted after 7:00pm ET and before 9:28am ET');
15967
- console.log('NOTE: MOC orders must be submitted after 7:00pm ET and before 3:50pm ET');
15968
- console.log('Orders submitted outside these windows will be rejected.\n');
15969
- const log = (message, options = { type: 'info' }) => {
15970
- log$1(message, { ...options, source: 'Test' });
15971
- };
15972
- if (!process.env.ALPACA_TRADING_API_KEY ||
15973
- !process.env.ALPACA_TRADING_SECRET_KEY ||
15974
- !process.env.ALPACA_TRADING_ACCOUNT_TYPE) {
15975
- log('Missing required ALPACA_TRADING_* environment variables', { type: 'error' });
15976
- return;
15977
- }
15978
- const credentials = {
15979
- accountName: 'Test Account',
15980
- apiKey: process.env.ALPACA_TRADING_API_KEY,
15981
- apiSecret: process.env.ALPACA_TRADING_SECRET_KEY,
15982
- type: process.env.ALPACA_TRADING_ACCOUNT_TYPE,
15983
- orderType: 'limit',
15984
- engine: 'quant',
15985
- };
15986
- const tradingAPI = AlpacaTradingAPI.getInstance(credentials);
15987
- try {
15988
- // Test creating a Market on Open order
15989
- log('Creating Market on Open (MOO) order for SPY...');
15990
- const mooOrder = await tradingAPI.createMOOOrder('SPY', 1, 'buy', 'buy_to_open', 'test-moo-order');
15991
- log(`MOO order created successfully: ${mooOrder.id}`);
15992
- log(` Symbol: ${mooOrder.symbol}`);
15993
- log(` Qty: ${mooOrder.qty}`);
15994
- log(` Side: ${mooOrder.side}`);
15995
- log(` Type: ${mooOrder.type}`);
15996
- log(` Time in Force: ${mooOrder.time_in_force}`);
15997
- log(` Status: ${mooOrder.status}`);
15998
- // Wait a moment before canceling
15999
- await new Promise((resolve) => setTimeout(resolve, 1000));
16000
- // Cancel the MOO order
16001
- log(`Canceling MOO order ${mooOrder.id}...`);
16002
- await tradingAPI.cancelOrder(mooOrder.id);
16003
- log(`MOO order canceled successfully`);
16004
- // Wait a moment before next order
16005
- await new Promise((resolve) => setTimeout(resolve, 1000));
16006
- // Test creating a Market on Close order
16007
- log('Creating Market on Close (MOC) order for SPY...');
16008
- const mocOrder = await tradingAPI.createMOCOrder('SPY', 1, 'sell', 'sell_to_open', 'test-moc-order');
16009
- log(`MOC order created successfully: ${mocOrder.id}`);
16010
- log(` Symbol: ${mocOrder.symbol}`);
16011
- log(` Qty: ${mocOrder.qty}`);
16012
- log(` Side: ${mocOrder.side}`);
16013
- log(` Type: ${mocOrder.type}`);
16014
- log(` Time in Force: ${mocOrder.time_in_force}`);
16015
- log(` Status: ${mocOrder.status}`);
16016
- // Wait a moment before canceling
16017
- await new Promise((resolve) => setTimeout(resolve, 1000));
16018
- // Cancel the MOC order
16019
- log(`Canceling MOC order ${mocOrder.id}...`);
16020
- await tradingAPI.cancelOrder(mocOrder.id);
16021
- log(`MOC order canceled successfully`);
16022
- log('\nMOO/MOC order test completed successfully');
16023
- }
16024
- catch (error) {
16025
- log(`Error during MOO/MOC order test: ${error instanceof Error ? error.message : 'Unknown error'}`, {
16026
- type: 'error',
16027
- });
16028
- throw error;
16029
- }
16030
- }
16031
- // testGetTradingDate();
16032
- // testGetTradingStartAndEndDates();
16033
- // testGetLastFullTradingDate();
16034
- // testGetMarketOpenClose();
16035
- // testGetNYTimeZone();
16036
- // testGetNextMarketDay();
16037
- // testCountTradingDays();
16038
- // testGetPreviousMarketDay();
16039
- // testOpenRouter();
16040
- // testGetMarketStatus();
16041
- // testCryptoMarketData();
16042
15308
  // testGetPortfolioDailyHistory();
16043
15309
  // testWebSocketConnectAndDisconnect();
16044
15310
  // testGetAssetsShortableFilter();
16045
- // testMarketDataAPI();
16046
15311
  // testLLM();
16047
15312
  // testImageModelDefaults();
16048
15313
  // testGetTradingDaysBack();
16049
- testMOOAndMOCOrders();
15314
+ // testMOOAndMOCOrders();
15315
+ // testMarketDataAPI();
15316
+ // Test market data subscription with a real symbol or FAKEPACA
15317
+ // Uncomment one of the following to test:
15318
+ // testMarketDataSubscription('SPY');
15319
+ // testMarketDataSubscription('FAKEPACA');
15320
+ testGetTradingDate();
16050
15321
  //# sourceMappingURL=test.js.map