@discomedia/utils 1.0.52 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/test.js CHANGED
@@ -146,18 +146,12 @@ const marketEarlyCloses = {
146
146
 
147
147
  // Constants for NY market times (Eastern Time)
148
148
  const MARKET_CONFIG = {
149
- TIMEZONE: 'America/New_York',
150
149
  UTC_OFFSET_STANDARD: -5, // EST
151
150
  UTC_OFFSET_DST: -4, // EDT
152
151
  TIMES: {
153
- EXTENDED_START: { hour: 4, minute: 0 },
154
152
  MARKET_OPEN: { hour: 9, minute: 30 },
155
- EARLY_MARKET_END: { hour: 10, minute: 0 },
156
153
  MARKET_CLOSE: { hour: 16, minute: 0 },
157
- EARLY_CLOSE: { hour: 13, minute: 0 },
158
- EXTENDED_END: { hour: 20, minute: 0 },
159
- EARLY_EXTENDED_END: { hour: 17, minute: 0 },
160
- },
154
+ EARLY_CLOSE: { hour: 13, minute: 0 }},
161
155
  };
162
156
  // Helper: Get NY offset for a given UTC date (DST rules for US)
163
157
  /**
@@ -225,33 +219,6 @@ function fromNYTime(date) {
225
219
  const utcMillis = nyMillis - offset * 60 * 60 * 1000;
226
220
  return new Date(utcMillis);
227
221
  }
228
- // Helper: Format date in ISO, unix, etc.
229
- /**
230
- * Formats a date in ISO, unix-seconds, or unix-ms format.
231
- * @param date - Date object
232
- * @param outputFormat - Output format ('iso', 'unix-seconds', 'unix-ms')
233
- * @returns Formatted date string or number
234
- */
235
- function formatDate(date, outputFormat = 'iso') {
236
- switch (outputFormat) {
237
- case 'unix-seconds':
238
- return Math.floor(date.getTime() / 1000);
239
- case 'unix-ms':
240
- return date.getTime();
241
- case 'iso':
242
- default:
243
- return date.toISOString();
244
- }
245
- }
246
- // Helper: Format date in NY locale string
247
- /**
248
- * Formats a date in NY locale string.
249
- * @param date - Date object
250
- * @returns NY locale string
251
- */
252
- function formatNYLocale(date) {
253
- return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
254
- }
255
222
  // Market calendar logic
256
223
  /**
257
224
  * Market calendar logic for holidays, weekends, and market days.
@@ -347,82 +314,6 @@ class MarketCalendar {
347
314
  return prevDay;
348
315
  }
349
316
  }
350
- // Market open/close times
351
- /**
352
- * Returns market open/close times for a given date, including extended and early closes.
353
- * @param date - Date object
354
- * @returns MarketOpenCloseResult
355
- */
356
- function getMarketTimes(date) {
357
- const calendar = new MarketCalendar();
358
- const nyDate = toNYTime(date);
359
- if (!calendar.isMarketDay(date)) {
360
- return {
361
- marketOpen: false,
362
- open: null,
363
- close: null,
364
- openExt: null,
365
- closeExt: null,
366
- };
367
- }
368
- const year = nyDate.getUTCFullYear();
369
- const month = nyDate.getUTCMonth();
370
- const day = nyDate.getUTCDate();
371
- // Helper to build NY time for a given hour/minute
372
- function buildNYTime(hour, minute) {
373
- const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
374
- return fromNYTime(d);
375
- }
376
- let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
377
- let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
378
- let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
379
- let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
380
- if (calendar.isEarlyCloseDay(date)) {
381
- close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
382
- closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
383
- }
384
- return {
385
- marketOpen: true,
386
- open,
387
- close,
388
- openExt,
389
- closeExt,
390
- };
391
- }
392
- // Is within market hours
393
- /**
394
- * Checks if a date/time is within market hours, extended hours, or continuous.
395
- * @param date - Date object
396
- * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
397
- * @returns true if within hours, false otherwise
398
- */
399
- function isWithinMarketHours(date, intradayReporting = 'market_hours') {
400
- const calendar = new MarketCalendar();
401
- if (!calendar.isMarketDay(date))
402
- return false;
403
- const nyDate = toNYTime(date);
404
- const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
405
- switch (intradayReporting) {
406
- case 'extended_hours': {
407
- let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
408
- if (calendar.isEarlyCloseDay(date)) {
409
- endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
410
- }
411
- const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
412
- return minutes >= startMinutes && minutes <= endMinutes;
413
- }
414
- case 'continuous':
415
- return true;
416
- default: {
417
- let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
418
- if (calendar.isEarlyCloseDay(date)) {
419
- endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
420
- }
421
- const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
422
- return minutes >= startMinutes && minutes <= endMinutes;
423
- }
424
- }
425
- }
426
317
  // Get last full trading date
427
318
  /**
428
319
  * Returns the last full trading date (market close) for a given date.
@@ -463,244 +354,6 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
463
354
  const closeMinute = marketCloseMinutes % 60;
464
355
  return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
465
356
  }
466
- // Get day boundaries
467
- /**
468
- * Returns the start and end boundaries for a market day, extended hours, or continuous.
469
- * @param date - Date object
470
- * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
471
- * @returns Object with start and end Date
472
- */
473
- function getDayBoundaries(date, intradayReporting = 'market_hours') {
474
- const calendar = new MarketCalendar();
475
- const nyDate = toNYTime(date);
476
- const year = nyDate.getUTCFullYear();
477
- const month = nyDate.getUTCMonth();
478
- const day = nyDate.getUTCDate();
479
- function buildNYTime(hour, minute, sec = 0, ms = 0) {
480
- const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
481
- return fromNYTime(d);
482
- }
483
- let start;
484
- let end;
485
- switch (intradayReporting) {
486
- case 'extended_hours':
487
- start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
488
- end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
489
- if (calendar.isEarlyCloseDay(date)) {
490
- end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
491
- }
492
- break;
493
- case 'continuous':
494
- start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
495
- end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
496
- break;
497
- default:
498
- start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
499
- end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
500
- if (calendar.isEarlyCloseDay(date)) {
501
- end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
502
- }
503
- break;
504
- }
505
- return { start, end };
506
- }
507
- // Period calculator
508
- /**
509
- * Calculates the start date for a given period ending at endDate.
510
- * @param endDate - Date object
511
- * @param period - Period string
512
- * @returns Date object for period start
513
- */
514
- function calculatePeriodStartDate(endDate, period) {
515
- const calendar = new MarketCalendar();
516
- let startDate;
517
- switch (period) {
518
- case 'YTD':
519
- startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
520
- break;
521
- case '1D':
522
- startDate = calendar.getPreviousMarketDay(endDate);
523
- break;
524
- case '3D':
525
- startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
526
- break;
527
- case '1W':
528
- startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
529
- break;
530
- case '2W':
531
- startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
532
- break;
533
- case '1M':
534
- startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
535
- break;
536
- case '3M':
537
- startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
538
- break;
539
- case '6M':
540
- startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
541
- break;
542
- case '1Y':
543
- startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
544
- break;
545
- default:
546
- throw new Error(`Invalid period: ${period}`);
547
- }
548
- // Ensure start date is a market day
549
- while (!calendar.isMarketDay(startDate)) {
550
- startDate = calendar.getNextMarketDay(startDate);
551
- }
552
- return startDate;
553
- }
554
- // Get market time period
555
- /**
556
- * Returns the start and end dates for a market time period.
557
- * @param params - MarketTimeParams
558
- * @returns PeriodDates object
559
- */
560
- function getMarketTimePeriod(params) {
561
- const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso' } = params;
562
- if (!period)
563
- throw new Error('Period is required');
564
- const calendar = new MarketCalendar();
565
- const nyEndDate = toNYTime(end);
566
- let endDate;
567
- const isCurrentMarketDay = calendar.isMarketDay(end);
568
- const isWithinHours = isWithinMarketHours(end, intraday_reporting);
569
- const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
570
- const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
571
- if (isCurrentMarketDay) {
572
- if (minutes < marketStartMinutes) {
573
- // Before market open - use previous day's close
574
- const lastMarketDay = calendar.getPreviousMarketDay(end);
575
- const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
576
- endDate = dayEnd;
577
- }
578
- else if (isWithinHours) {
579
- // During market hours - use current time
580
- endDate = end;
581
- }
582
- else {
583
- // After market close - use today's close
584
- const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
585
- endDate = dayEnd;
586
- }
587
- }
588
- else {
589
- // Not a market day - use previous market day's close
590
- const lastMarketDay = calendar.getPreviousMarketDay(end);
591
- const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
592
- endDate = dayEnd;
593
- }
594
- // Calculate start date
595
- const periodStartDate = calculatePeriodStartDate(endDate, period);
596
- const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
597
- if (endDate.getTime() < dayStart.getTime()) {
598
- throw new Error('Start date cannot be after end date');
599
- }
600
- return {
601
- start: formatDate(dayStart, outputFormat),
602
- end: formatDate(endDate, outputFormat),
603
- };
604
- }
605
- // Market status
606
- /**
607
- * Returns the current market status for a given date.
608
- * @param date - Date object (default: now)
609
- * @returns MarketStatus object
610
- */
611
- function getMarketStatusImpl(date = new Date()) {
612
- const calendar = new MarketCalendar();
613
- const nyDate = toNYTime(date);
614
- const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
615
- const isMarketDay = calendar.isMarketDay(date);
616
- const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
617
- const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
618
- const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
619
- const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
620
- let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
621
- let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
622
- if (isEarlyCloseDay) {
623
- marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
624
- extendedEndMinutes =
625
- MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
626
- }
627
- let status;
628
- let nextStatus;
629
- let nextStatusTime;
630
- let marketPeriod;
631
- if (!isMarketDay) {
632
- status = 'closed';
633
- nextStatus = 'extended hours';
634
- marketPeriod = 'closed';
635
- const nextMarketDay = calendar.getNextMarketDay(date);
636
- nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
637
- }
638
- else if (minutes < extendedStartMinutes) {
639
- status = 'closed';
640
- nextStatus = 'extended hours';
641
- marketPeriod = 'closed';
642
- nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
643
- }
644
- else if (minutes < marketStartMinutes) {
645
- status = 'extended hours';
646
- nextStatus = 'open';
647
- marketPeriod = 'preMarket';
648
- nextStatusTime = getDayBoundaries(date, 'market_hours').start;
649
- }
650
- else if (minutes < marketCloseMinutes) {
651
- status = 'open';
652
- nextStatus = 'extended hours';
653
- marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
654
- nextStatusTime = getDayBoundaries(date, 'market_hours').end;
655
- }
656
- else if (minutes < extendedEndMinutes) {
657
- status = 'extended hours';
658
- nextStatus = 'closed';
659
- marketPeriod = 'afterMarket';
660
- nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
661
- }
662
- else {
663
- status = 'closed';
664
- nextStatus = 'extended hours';
665
- marketPeriod = 'closed';
666
- const nextMarketDay = calendar.getNextMarketDay(date);
667
- nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
668
- }
669
- // I think using nyDate here may be wrong - should use current time? i.e. date.getTime()
670
- const nextStatusTimeDifference = nextStatusTime.getTime() - date.getTime();
671
- return {
672
- time: date,
673
- timeString: formatNYLocale(nyDate),
674
- status,
675
- nextStatus,
676
- marketPeriod,
677
- nextStatusTime,
678
- nextStatusTimeDifference,
679
- nextStatusTimeString: formatNYLocale(nextStatusTime),
680
- };
681
- }
682
- // API exports
683
- /**
684
- * Returns market open/close times for a given date.
685
- * @param options - { date?: Date }
686
- * @returns MarketOpenCloseResult
687
- */
688
- function getMarketOpenClose(options = {}) {
689
- const { date = new Date() } = options;
690
- return getMarketTimes(date);
691
- }
692
- /**
693
- * Returns the start and end dates for a market time period as Date objects.
694
- * @param params - MarketTimeParams
695
- * @returns Object with start and end Date
696
- */
697
- function getStartAndEndDates(params = {}) {
698
- const { start, end } = getMarketTimePeriod(params);
699
- return {
700
- start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
701
- end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
702
- };
703
- }
704
357
  /**
705
358
  * Returns the last full trading date as a Date object.
706
359
  */
@@ -712,43 +365,6 @@ function getStartAndEndDates(params = {}) {
712
365
  function getLastFullTradingDate(currentDate = new Date()) {
713
366
  return getLastFullTradingDateImpl(currentDate);
714
367
  }
715
- /**
716
- * Returns the next market day after the reference date.
717
- * @param referenceDate - Date object (default: now)
718
- * @returns Object with date, yyyymmdd string, and ISO string
719
- */
720
- function getNextMarketDay({ referenceDate } = {}) {
721
- const calendar = new MarketCalendar();
722
- const startDate = referenceDate ?? new Date();
723
- // Find the next trading day (UTC Date object)
724
- const nextDate = calendar.getNextMarketDay(startDate);
725
- // Convert to NY time before extracting Y-M-D parts
726
- const nyNext = toNYTime(nextDate);
727
- const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, '0')}-${String(nyNext.getUTCDate()).padStart(2, '0')}`;
728
- return {
729
- date: nextDate, // raw Date, unchanged
730
- yyyymmdd, // correct trading date string
731
- dateISOString: nextDate.toISOString(),
732
- };
733
- }
734
- /**
735
- * Returns the previous market day before the reference date.
736
- * @param referenceDate - Date object (default: now)
737
- * @returns Object with date, yyyymmdd string, and ISO string
738
- */
739
- function getPreviousMarketDay({ referenceDate } = {}) {
740
- const calendar = new MarketCalendar();
741
- const startDate = referenceDate || new Date();
742
- const prevDate = calendar.getPreviousMarketDay(startDate);
743
- // convert to NY time first
744
- const nyPrev = toNYTime(prevDate); // ← already in this file
745
- const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, '0')}-${String(nyPrev.getUTCDate()).padStart(2, '0')}`;
746
- return {
747
- date: prevDate,
748
- yyyymmdd,
749
- dateISOString: prevDate.toISOString(),
750
- };
751
- }
752
368
  /**
753
369
  * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
754
370
  * @param time - a string, number (unix timestamp), or Date object representing the time
@@ -764,663 +380,25 @@ function getTradingDate(time) {
764
380
  const nyDate = toNYTime(date);
765
381
  return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
766
382
  }
767
- /**
768
- * Returns the NY timezone offset string for a given date.
769
- * @param date - Date object (default: now)
770
- * @returns '-04:00' for EDT, '-05:00' for EST
771
- */
772
- function getNYTimeZone(date) {
773
- const offset = getNYOffset(date || new Date());
774
- return offset === -4 ? '-04:00' : '-05:00';
775
- }
776
- /**
777
- * Returns the regular market open and close Date objects for a given trading day string in the
778
- * America/New_York timezone (NYSE/NASDAQ calendar).
779
- *
780
- * This helper is convenient when you have a calendar date like '2025-10-03' and want the precise
781
- * open and close Date values for that day. It internally:
782
- * - Determines the NY offset for the day using `getNYTimeZone()`.
783
- * - Anchors a noon-time Date on that day in NY time to avoid DST edge cases.
784
- * - Verifies the day is a market day via `isMarketDay()`.
785
- * - Fetches the open/close times via `getMarketOpenClose()`.
786
- *
787
- * Throws if the provided day is not a market day or if open/close times are unavailable.
788
- *
789
- * See also:
790
- * - `getNYTimeZone(date?: Date)`
791
- * - `isMarketDay(date: Date)`
792
- * - `getMarketOpenClose(options?: { date?: Date })`
793
- *
794
- * @param dateStr - Trading day string in 'YYYY-MM-DD' format (Eastern Time date)
795
- * @returns An object containing `{ open: Date; close: Date }`
796
- * @example
797
- * ```ts
798
- * const { open, close } = disco.time.getOpenCloseForTradingDay('2025-10-03');
799
- * ```
800
- */
801
- function getOpenCloseForTradingDay(dateStr) {
802
- // Build a UTC midnight anchor for the date, then derive the NY offset for that day.
803
- const utcAnchor = new Date(`${dateStr}T00:00:00Z`);
804
- const nyOffset = getNYTimeZone(utcAnchor); // '-04:00' | '-05:00'
805
- // Create a NY-local noon date to avoid DST midnight transitions.
806
- const nyNoon = new Date(`${dateStr}T12:00:00${nyOffset}`);
807
- if (!isMarketDay(nyNoon)) {
808
- throw new Error(`Not a market day in ET: ${dateStr}`);
809
- }
810
- const { open, close } = getMarketOpenClose({ date: nyNoon });
811
- if (!open || !close) {
812
- throw new Error(`No market times available for ${dateStr}`);
813
- }
814
- return { open, close };
815
- }
816
- /**
817
- * Converts any date to the market time zone (America/New_York, Eastern Time).
818
- * Returns a new Date object representing the same moment in time but adjusted to NY/Eastern timezone.
819
- * Automatically handles daylight saving time transitions (EST/EDT).
820
- *
821
- * @param date - Date object to convert to market time zone
822
- * @returns Date object in NY/Eastern time zone
823
- * @example
824
- * ```typescript
825
- * const utcDate = new Date('2024-01-15T15:30:00Z'); // 3:30 PM UTC
826
- * const nyDate = convertDateToMarketTimeZone(utcDate); // 10:30 AM EST (winter) or 11:30 AM EDT (summer)
827
- * ```
828
- */
829
- function convertDateToMarketTimeZone(date) {
830
- return toNYTime(date);
831
- }
832
- /**
833
- * Returns the current market status for a given date.
834
- * @param options - { date?: Date }
835
- * @returns MarketStatus object
836
- */
837
- function getMarketStatus(options = {}) {
838
- const { date = new Date() } = options;
839
- return getMarketStatusImpl(date);
840
- }
841
- /**
842
- * Checks if a date is a market day.
843
- * @param date - Date object
844
- * @returns true if market day, false otherwise
845
- */
846
- function isMarketDay(date) {
847
- const calendar = new MarketCalendar();
848
- return calendar.isMarketDay(date);
849
- }
850
- /**
851
- * Returns full trading days from market open to market close.
852
- * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
853
- * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
854
- */
855
- /**
856
- * Returns full trading days from market open to market close.
857
- * @param options - { endDate?: Date, days?: number }
858
- * @returns Object with startDate and endDate
859
- */
860
- function getTradingStartAndEndDates(options = {}) {
861
- const { endDate = new Date(), days = 1 } = options;
862
- const calendar = new MarketCalendar();
863
- // Find the most recent market close
864
- let endMarketDay = endDate;
865
- const nyEnd = toNYTime(endDate);
866
- const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
867
- const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
868
- const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
869
- if (!calendar.isMarketDay(endDate) ||
870
- minutes < marketOpenMinutes ||
871
- (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
872
- // Before market open, not a market day, or during market hours: use previous market day
873
- endMarketDay = calendar.getPreviousMarketDay(endDate);
874
- }
875
- else {
876
- // After market close: use today
877
- endMarketDay = endDate;
878
- }
879
- // Get market close for endMarketDay
880
- const endClose = getMarketOpenClose({ date: endMarketDay }).close;
881
- // Find start market day by iterating back over market days
882
- let startMarketDay = endMarketDay;
883
- let count = Math.max(1, days);
884
- for (let i = 1; i < count; i++) {
885
- startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
886
- }
887
- // If days > 1, we need to go back (days-1) market days from endMarketDay
888
- if (days > 1) {
889
- startMarketDay = endMarketDay;
890
- for (let i = 1; i < days; i++) {
891
- startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
892
- }
893
- }
894
- const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
895
- return { startDate: startOpen, endDate: endClose };
896
- }
897
- /**
898
- * 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.
899
- *
900
- * This function calculates the actual trading time between two dates by:
901
- * 1. Iterating through each calendar day between startDate and endDate (inclusive)
902
- * 2. For each day that is a market day (not weekend/holiday), getting market open/close times
903
- * 3. Calculating the overlap between the time range and market hours for that day
904
- * 4. Summing up all the trading minutes across all days
905
- *
906
- * The function automatically handles:
907
- * - Weekends (Saturday/Sunday) - skipped entirely
908
- * - Market holidays - skipped entirely
909
- * - Early close days (e.g. day before holidays) - uses early close time
910
- * - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
911
- *
912
- * Examples:
913
- * - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
914
- * - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
915
- * - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
916
- *
917
- * @param startDate - Start date/time
918
- * @param endDate - End date/time (default: now)
919
- * @returns Object containing:
920
- * - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
921
- * - hours: Trading time in hours
922
- * - minutes: Trading time in minutes
923
- */
924
- function countTradingDays(startDate, endDate = new Date()) {
925
- const calendar = new MarketCalendar();
926
- // Ensure start is before end
927
- if (startDate.getTime() > endDate.getTime()) {
928
- throw new Error('Start date must be before end date');
929
- }
930
- let totalMinutes = 0;
931
- // Get the NY dates for iteration
932
- const startNY = toNYTime(startDate);
933
- const endNY = toNYTime(endDate);
934
- // Create date at start of first day (in NY time)
935
- const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
936
- // Iterate through each calendar day
937
- while (currentNY.getTime() <= endNY.getTime()) {
938
- const currentUTC = fromNYTime(currentNY);
939
- // Check if this is a market day
940
- if (calendar.isMarketDay(currentUTC)) {
941
- // Get market hours for this day
942
- const marketTimes = getMarketTimes(currentUTC);
943
- if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
944
- // Calculate the overlap between our time range and market hours
945
- const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
946
- const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
947
- // Only count if there's actual overlap
948
- if (dayStart < dayEnd) {
949
- totalMinutes += (dayEnd - dayStart) / (1000 * 60);
950
- }
951
- }
952
- }
953
- // Move to next day
954
- currentNY.setUTCDate(currentNY.getUTCDate() + 1);
955
- }
956
- // Convert to days, hours, minutes
957
- const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
958
- const days = totalMinutes / MINUTES_PER_TRADING_DAY;
959
- const hours = totalMinutes / 60;
960
- const minutes = totalMinutes;
961
- return {
962
- days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
963
- hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
964
- minutes: Math.round(minutes),
965
- };
966
- }
967
- /**
968
- * Returns the trading day N days back from a reference date, along with its market open time.
969
- * Trading days are counted as full or half trading days (days that end count as 1 full trading day).
970
- * By default, the most recent completed trading day counts as day 1.
971
- * Set includeMostRecentFullDay to false to count strictly before that day.
972
- *
973
- * @param options - Object with:
974
- * - referenceDate: Date to count back from (default: now)
975
- * - days: Number of trading days to go back (must be an integer >= 1)
976
- * - includeMostRecentFullDay: Whether to include the most recent completed trading day (default: true)
977
- * @returns Object containing:
978
- * - date: Trading date in YYYY-MM-DD format
979
- * - marketOpenISO: Market open time as ISO string (e.g., "2025-11-15T13:30:00.000Z")
980
- * - unixTimestamp: Market open time as Unix timestamp in seconds
981
- * @example
982
- * ```typescript
983
- * // Get the trading day 1 day back (most recent full trading day)
984
- * const result = getTradingDaysBack({ days: 1 });
985
- * console.log(result.date); // "2025-11-01"
986
- * console.log(result.marketOpenISO); // "2025-11-01T13:30:00.000Z"
987
- * console.log(result.unixTimestamp); // 1730466600
988
- *
989
- * // Get the trading day 5 days back from a specific date
990
- * const result2 = getTradingDaysBack({
991
- * referenceDate: new Date('2025-11-15T12:00:00-05:00'),
992
- * days: 5
993
- * });
994
- * ```
995
- */
996
- function getTradingDaysBack(options) {
997
- const calendar = new MarketCalendar();
998
- const { referenceDate, days, includeMostRecentFullDay = true } = options;
999
- const refDate = referenceDate || new Date();
1000
- const daysBack = days;
1001
- if (!Number.isInteger(daysBack) || daysBack < 1) {
1002
- throw new Error('days must be an integer >= 1');
1003
- }
1004
- // Start from the last full trading date relative to reference
1005
- let targetDate = getLastFullTradingDateImpl(refDate);
1006
- if (!includeMostRecentFullDay) {
1007
- targetDate = calendar.getPreviousMarketDay(targetDate);
1008
- }
1009
- // Go back the specified number of days (we're already at day 1, so go back days-1 more)
1010
- for (let i = 1; i < daysBack; i++) {
1011
- targetDate = calendar.getPreviousMarketDay(targetDate);
1012
- }
1013
- // Get market open time for this date
1014
- const marketTimes = getMarketTimes(targetDate);
1015
- if (!marketTimes.open) {
1016
- throw new Error(`No market open time for target date`);
1017
- }
1018
- // Format the date string (YYYY-MM-DD) in NY time
1019
- const nyDate = toNYTime(marketTimes.open);
1020
- const dateStr = `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
1021
- const marketOpenISO = marketTimes.open.toISOString();
1022
- const unixTimestamp = Math.floor(marketTimes.open.getTime() / 1000);
1023
- return {
1024
- date: dateStr,
1025
- marketOpenISO,
1026
- unixTimestamp,
1027
- };
1028
- }
1029
- // Export MARKET_TIMES for compatibility
1030
- const MARKET_TIMES = {
1031
- TIMEZONE: MARKET_CONFIG.TIMEZONE,
1032
- PRE: {
1033
- START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
1034
- END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1035
- },
1036
- EARLY_MORNING: {
1037
- START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1038
- END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
1039
- },
1040
- EARLY_CLOSE_BEFORE_HOLIDAY: {
1041
- START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1042
- END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
1043
- },
1044
- EARLY_EXTENDED_BEFORE_HOLIDAY: {
1045
- START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
1046
- END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
1047
- },
1048
- REGULAR: {
1049
- START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
1050
- END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
1051
- },
1052
- EXTENDED: {
1053
- START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
1054
- END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
1055
- },
1056
- };
1057
383
 
1058
- // format-tools.ts
1059
- /**
1060
- * Capitalizes the first letter of a string
1061
- * @param {string} str - The string to capitalize
1062
- * @returns {string} The capitalized string, or original value if not a string
1063
- * @example
1064
- * capitalize('hello') // 'Hello'
1065
- * capitalize(123) // 123
1066
- */
1067
- function capFirstLetter(str) {
1068
- if (!str || typeof str !== 'string')
1069
- return str;
1070
- return str.charAt(0).toUpperCase() + str.slice(1);
1071
- }
1072
- /**
1073
- * Formats a number as US currency
1074
- * @param {number} value - The number to format
1075
- * @returns {string} The formatted currency string (e.g. '$1,234.56')
1076
- * @example
1077
- * formatCurrency(1234.56) // '$1,234.56'
1078
- * formatCurrency(NaN) // '$0.00'
1079
- */
1080
- function formatCurrency(value) {
1081
- if (isNaN(value)) {
1082
- return '$0.00';
1083
- }
1084
- return new Intl.NumberFormat('en-US', {
1085
- style: 'currency',
1086
- currency: 'USD',
1087
- }).format(value);
1088
- }
1089
- /**
1090
- * Formats a number with commas
1091
- * @param {number} value - The number to format
1092
- * @returns {string} The formatted number string (e.g. '1,234.56')
1093
- * @example
1094
- * formatNumber(1234.56) // '1,234.56'
1095
- * formatNumber(NaN) // '0'
1096
- */
1097
- function formatNumber(value) {
1098
- if (isNaN(value)) {
1099
- return '0';
1100
- }
1101
- return new Intl.NumberFormat('en-US').format(value);
1102
- }
1103
- /**
1104
- * Formats a number as a percentage
1105
- * @param {number} value - The number to format (e.g. 0.75 for 75%)
1106
- * @param {number} [decimalPlaces=2] - Number of decimal places to show
1107
- * @returns {string} The formatted percentage string (e.g. '75.00%')
1108
- * @example
1109
- * formatPercentage(0.75) // '75.00%'
1110
- * formatPercentage(0.753, 1) // '75.3%'
1111
- */
1112
- function formatPercentage(value, decimalPlaces = 2) {
1113
- if (isNaN(value)) {
1114
- return '0%';
1115
- }
1116
- return new Intl.NumberFormat('en-US', {
1117
- style: 'percent',
1118
- minimumFractionDigits: decimalPlaces,
1119
- }).format(value);
1120
- }
1121
- /**
1122
- * Formats a Date object to Australian datetime format for Google Sheets
1123
- * @param {Date} date - The date to format
1124
- * @returns {string} The formatted datetime string in 'DD/MM/YYYY HH:MM:SS' format
1125
- * @example
1126
- * dateTimeForGS(new Date('2025-01-01T12:34:56')) // '01/01/2025 12:34:56'
1127
- */
1128
- function dateTimeForGS(date) {
1129
- return date
1130
- .toLocaleString('en-AU', {
1131
- day: '2-digit',
1132
- month: '2-digit',
1133
- year: 'numeric',
1134
- hour: '2-digit',
1135
- minute: '2-digit',
1136
- second: '2-digit',
1137
- hour12: false,
1138
- })
1139
- .replace(/\./g, '/');
1140
- }
384
+ /*
385
+ How it works:
386
+ `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
387
+ */
1141
388
 
1142
- /**
1143
- * Type guard to check if a model is an OpenRouter model
1144
- */
1145
- function isOpenRouterModel(model) {
1146
- const openRouterModels = [
1147
- 'openai/gpt-5',
1148
- 'openai/gpt-5-mini',
1149
- 'openai/gpt-5-nano',
1150
- 'openai/gpt-5.1',
1151
- 'openai/gpt-5.2',
1152
- 'openai/gpt-5.2-pro',
1153
- 'openai/gpt-5.1-codex',
1154
- 'openai/gpt-5.1-codex-max',
1155
- 'openai/gpt-oss-120b',
1156
- 'z.ai/glm-4.5',
1157
- 'z.ai/glm-4.5-air',
1158
- 'google/gemini-2.5-flash',
1159
- 'google/gemini-2.5-flash-lite',
1160
- 'deepseek/deepseek-r1-0528',
1161
- 'deepseek/deepseek-chat-v3-0324',
1162
- ];
1163
- return openRouterModels.includes(model);
389
+ class Node {
390
+ value;
391
+ next;
392
+
393
+ constructor(value) {
394
+ this.value = value;
395
+ }
1164
396
  }
1165
397
 
1166
- var Types = /*#__PURE__*/Object.freeze({
1167
- __proto__: null,
1168
- isOpenRouterModel: isOpenRouterModel
1169
- });
1170
-
1171
- // Utility function for debug logging
1172
- // Define the possible log types as a const array for better type inference
1173
- /**
1174
- * Debug logging utility that respects environment debug flags.
1175
- * Logs messages to the console based on the specified log level.
1176
- *
1177
- * @param message - The message to log.
1178
- * @param data - Optional data to log alongside the message. This can be any type of data.
1179
- * @param type - Log level. One of: 'info' | 'warn' | 'error' | 'debug' | 'trace'. Defaults to 'info'.
1180
- *
1181
- * @example
1182
- * logIfDebug("User login failed", { userId: 123 }, "error");
1183
- * logIfDebug("Cache miss", undefined, "warn");
1184
- * logIfDebug("Processing request", { requestId: "abc" }, "debug");
1185
- */
1186
- const logIfDebug = (message, data, type = 'info') => {
1187
- const prefix = `[DEBUG][${type.toUpperCase()}]`;
1188
- const formattedData = data !== undefined ? JSON.stringify(data, null, 2) : '';
1189
- switch (type) {
1190
- case 'error':
1191
- console.error(prefix, message, formattedData);
1192
- break;
1193
- case 'warn':
1194
- console.warn(prefix, message, formattedData);
1195
- break;
1196
- case 'debug':
1197
- console.debug(prefix, message, formattedData);
1198
- break;
1199
- case 'trace':
1200
- console.trace(prefix, message, formattedData);
1201
- break;
1202
- case 'info':
1203
- default:
1204
- console.info(prefix, message, formattedData);
1205
- }
1206
- };
1207
- /**
1208
- * Masks the middle part of an API key, returning only the first 2 and last 2 characters.
1209
- * If the API key is very short (<= 4 characters), it will be returned as is.
1210
- *
1211
- * @param keyValue - The API key to mask.
1212
- * @returns The masked API key.
1213
- *
1214
- * @example
1215
- * maskApiKey("12341239856677"); // Returns "12****77"
1216
- */
1217
- function maskApiKey(keyValue) {
1218
- if (keyValue.length <= 4) {
1219
- return keyValue;
1220
- }
1221
- const firstTwo = keyValue.slice(0, 2);
1222
- const lastTwo = keyValue.slice(-2);
1223
- return `${firstTwo}****${lastTwo}`;
1224
- }
1225
- /**
1226
- * Hides (masks) the value of any query parameter that is "apiKey" (case-insensitive),
1227
- * replacing the middle part with **** and keeping only the first 2 and last 2 characters.
1228
- *
1229
- * @param url - The URL containing the query parameters.
1230
- * @returns The URL with the masked API key.
1231
- *
1232
- * @example
1233
- * hideApiKeyFromurl("https://xxx.com/s/23/fdsa/?apiKey=12341239856677");
1234
- * // Returns "https://xxx.com/s/23/fdsa/?apiKey=12****77"
1235
- */
1236
- function hideApiKeyFromurl(url) {
1237
- try {
1238
- const parsedUrl = new URL(url);
1239
- // We iterate over all search params and look for one named 'apikey' (case-insensitive)
1240
- for (const [key, value] of parsedUrl.searchParams.entries()) {
1241
- if (key.toLowerCase() === 'apikey') {
1242
- const masked = maskApiKey(value);
1243
- parsedUrl.searchParams.set(key, masked);
1244
- }
1245
- }
1246
- return parsedUrl.toString();
1247
- }
1248
- catch {
1249
- // If we can't parse it as a valid URL, just return the original string
1250
- return url;
1251
- }
1252
- }
1253
- /**
1254
- * Extracts meaningful error information from various error types.
1255
- * @param error - The error to analyze.
1256
- * @param response - Optional response object for HTTP errors.
1257
- * @returns Structured error details.
1258
- */
1259
- function extractErrorDetails(error, response) {
1260
- const errMsg = error instanceof Error ? error.message : String(error);
1261
- const errName = error instanceof Error ? error.name : 'Error';
1262
- if (errName === 'TypeError' && errMsg.includes('fetch')) {
1263
- return { type: 'NETWORK_ERROR', reason: 'Network connectivity issue', status: null };
1264
- }
1265
- if (errMsg.includes('HTTP error: 429')) {
1266
- const match = errMsg.match(/RATE_LIMIT: 429:(\d+)/);
1267
- const retryAfter = match ? parseInt(match[1]) : undefined;
1268
- return { type: 'RATE_LIMIT', reason: 'Rate limit exceeded', status: 429, retryAfter };
1269
- }
1270
- if (errMsg.includes('HTTP error: 401') || errMsg.includes('AUTH_ERROR: 401')) {
1271
- return { type: 'AUTH_ERROR', reason: 'Authentication failed - invalid API key', status: 401 };
1272
- }
1273
- if (errMsg.includes('HTTP error: 403') || errMsg.includes('AUTH_ERROR: 403')) {
1274
- return { type: 'AUTH_ERROR', reason: 'Access forbidden - insufficient permissions', status: 403 };
1275
- }
1276
- if (errMsg.includes('SERVER_ERROR:')) {
1277
- const status = parseInt(errMsg.split('SERVER_ERROR: ')[1]) || 500;
1278
- return { type: 'SERVER_ERROR', reason: `Server error (${status})`, status };
1279
- }
1280
- if (errMsg.includes('CLIENT_ERROR:')) {
1281
- const status = parseInt(errMsg.split('CLIENT_ERROR: ')[1]) || 400;
1282
- return { type: 'CLIENT_ERROR', reason: `Client error (${status})`, status };
1283
- }
1284
- return { type: 'UNKNOWN', reason: errMsg || 'Unknown error', status: null };
1285
- }
1286
- /**
1287
- * Fetches a resource with intelligent retry logic for handling transient errors.
1288
- * Features enhanced error logging, rate limit detection, and adaptive backoff.
1289
- *
1290
- * @param url - The URL to fetch.
1291
- * @param options - Optional fetch options.
1292
- * @param retries - The number of retry attempts. Defaults to 3.
1293
- * @param initialBackoff - The initial backoff time in milliseconds. Defaults to 1000.
1294
- * @returns A promise that resolves to the response.
1295
- *
1296
- * @throws Will throw an error if the fetch fails after the specified number of retries.
1297
- */
1298
- async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1000) {
1299
- let backoff = initialBackoff;
1300
- for (let attempt = 1; attempt <= retries; attempt++) {
1301
- try {
1302
- const response = await fetch(url, options);
1303
- if (!response.ok) {
1304
- // Enhanced HTTP error handling with specific error types
1305
- if (response.status === 429) {
1306
- // Check for Retry-After header
1307
- const retryAfter = response.headers.get('Retry-After');
1308
- const retryDelay = retryAfter ? parseInt(retryAfter) * 1000 : null;
1309
- throw new Error(`RATE_LIMIT: ${response.status}${retryDelay ? `:${retryDelay}` : ''}`);
1310
- }
1311
- if ([500, 502, 503, 504].includes(response.status)) {
1312
- throw new Error(`SERVER_ERROR: ${response.status}`);
1313
- }
1314
- if ([401, 403].includes(response.status)) {
1315
- throw new Error(`AUTH_ERROR: ${response.status}`);
1316
- }
1317
- if (response.status >= 400 && response.status < 500) {
1318
- // Don't retry most 4xx client errors
1319
- throw new Error(`CLIENT_ERROR: ${response.status}`);
1320
- }
1321
- throw new Error(`HTTP_ERROR: ${response.status}`);
1322
- }
1323
- return response;
1324
- }
1325
- catch (error) {
1326
- if (attempt === retries) {
1327
- throw error;
1328
- }
1329
- // Extract meaningful error information
1330
- const errorDetails = extractErrorDetails(error);
1331
- let adaptiveBackoff = backoff;
1332
- // Adaptive backoff based on error type
1333
- if (errorDetails.type === 'RATE_LIMIT') {
1334
- // Use Retry-After header if available, otherwise use minimum 5s for rate limits
1335
- if (errorDetails.retryAfter) {
1336
- adaptiveBackoff = errorDetails.retryAfter;
1337
- }
1338
- else {
1339
- adaptiveBackoff = Math.max(backoff, 5000);
1340
- }
1341
- }
1342
- else if (errorDetails.type === 'AUTH_ERROR') {
1343
- // Don't retry auth errors - fail fast
1344
- console.error(`Authentication error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
1345
- attemptNumber: attempt,
1346
- errorType: errorDetails.type,
1347
- httpStatus: errorDetails.status,
1348
- url: hideApiKeyFromurl(url),
1349
- source: 'fetchWithRetry',
1350
- timestamp: new Date().toISOString(),
1351
- });
1352
- throw error;
1353
- }
1354
- else if (errorDetails.type === 'CLIENT_ERROR') {
1355
- // Don't retry client errors (except 429 which is handled above)
1356
- console.error(`Client error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
1357
- attemptNumber: attempt,
1358
- errorType: errorDetails.type,
1359
- httpStatus: errorDetails.status,
1360
- url: hideApiKeyFromurl(url),
1361
- source: 'fetchWithRetry',
1362
- timestamp: new Date().toISOString(),
1363
- });
1364
- throw error;
1365
- }
1366
- // Enhanced error logging with structured data
1367
- console.warn(`Fetch attempt ${attempt} of ${retries} for ${hideApiKeyFromurl(url)} failed: ${errorDetails.reason}. Retrying in ${adaptiveBackoff}ms...`, {
1368
- attemptNumber: attempt,
1369
- totalRetries: retries,
1370
- errorType: errorDetails.type,
1371
- httpStatus: errorDetails.status,
1372
- retryDelay: adaptiveBackoff,
1373
- url: hideApiKeyFromurl(url),
1374
- source: 'fetchWithRetry',
1375
- timestamp: new Date().toISOString(),
1376
- });
1377
- await new Promise((resolve) => setTimeout(resolve, adaptiveBackoff));
1378
- backoff = Math.min(backoff * 2, 30000); // Cap at 30 seconds
1379
- }
1380
- }
1381
- throw new Error('Failed to fetch after multiple attempts');
1382
- }
1383
- /**
1384
- * Validates a Polygon.io API key by making a test request.
1385
- * @param apiKey - The API key to validate.
1386
- * @returns Promise that resolves to true if valid, false otherwise.
1387
- */
1388
- async function validatePolygonApiKey(apiKey) {
1389
- try {
1390
- const response = await fetch(`https://api.polygon.io/v1/meta/symbols?apikey=${apiKey}&limit=1`);
1391
- if (response.status === 401) {
1392
- throw new Error('Invalid or expired Polygon.io API key');
1393
- }
1394
- if (response.status === 403) {
1395
- throw new Error('Polygon.io API key lacks required permissions');
1396
- }
1397
- return response.ok;
1398
- }
1399
- catch (error) {
1400
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1401
- console.error('Polygon.io API key validation failed:', errorMessage);
1402
- return false;
1403
- }
1404
- }
1405
-
1406
- /*
1407
- How it works:
1408
- `this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
1409
- */
1410
-
1411
- class Node {
1412
- value;
1413
- next;
1414
-
1415
- constructor(value) {
1416
- this.value = value;
1417
- }
1418
- }
1419
-
1420
- class Queue {
1421
- #head;
1422
- #tail;
1423
- #size;
398
+ class Queue {
399
+ #head;
400
+ #tail;
401
+ #size;
1424
402
 
1425
403
  constructor() {
1426
404
  this.clear();
@@ -1590,473 +568,10 @@ function validateConcurrency(concurrency) {
1590
568
  * Polygon.io calls
1591
569
  **********************************************************************************/
1592
570
  // Constants from environment variables
1593
- const POLYGON_API_KEY = process.env.POLYGON_API_KEY;
571
+ process.env.POLYGON_API_KEY;
1594
572
  // Define concurrency limits per API
1595
573
  const POLYGON_CONCURRENCY_LIMIT = 100;
1596
- const polygonLimit = pLimit(POLYGON_CONCURRENCY_LIMIT);
1597
- // Use to update general information about stocks
1598
- /**
1599
- * Fetches general information about a stock ticker.
1600
- * @param {string} symbol - The stock ticker symbol to fetch information for.
1601
- * @param {Object} [options] - Optional parameters.
1602
- * @param {string} [options.apiKey] - The API key to use for the request.
1603
- * @returns {Promise<PolygonTickerInfo | null>} The ticker information or null if not found.
1604
- */
1605
- const fetchTickerInfo = async (symbol, options) => {
1606
- if (!options?.apiKey && !POLYGON_API_KEY) {
1607
- throw new Error('Polygon API key is missing');
1608
- }
1609
- const baseUrl = `https://api.polygon.io/v3/reference/tickers/${encodeURIComponent(symbol)}`;
1610
- const params = new URLSearchParams({
1611
- apiKey: options?.apiKey || POLYGON_API_KEY,
1612
- });
1613
- return polygonLimit(async () => {
1614
- try {
1615
- const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
1616
- const data = await response.json();
1617
- // Check for "NOT_FOUND" status and return null
1618
- if (data.status === 'NOT_FOUND') {
1619
- console.warn(`Ticker not found: ${symbol}`);
1620
- return null;
1621
- }
1622
- // Map the results to the required structure
1623
- const results = data.results;
1624
- if (!results) {
1625
- throw new Error('No results in Polygon API response');
1626
- }
1627
- // Validate required fields
1628
- const requiredFields = [
1629
- 'active',
1630
- 'currency_name',
1631
- 'locale',
1632
- 'market',
1633
- 'name',
1634
- 'primary_exchange',
1635
- 'ticker',
1636
- 'type',
1637
- ];
1638
- for (const field of requiredFields) {
1639
- if (results[field] === undefined) {
1640
- throw new Error(`Missing required field in Polygon API response: ${field}`);
1641
- }
1642
- }
1643
- // Handle optional share_class_shares_outstanding field
1644
- if (results.share_class_shares_outstanding === undefined) {
1645
- results.share_class_shares_outstanding = null;
1646
- }
1647
- return {
1648
- ticker: results.ticker,
1649
- type: results.type,
1650
- active: results.active,
1651
- currency_name: results.currency_name,
1652
- description: results.description ?? 'No description available',
1653
- locale: results.locale,
1654
- market: results.market,
1655
- market_cap: results.market_cap ?? 0,
1656
- name: results.name,
1657
- primary_exchange: results.primary_exchange,
1658
- share_class_shares_outstanding: results.share_class_shares_outstanding,
1659
- };
1660
- }
1661
- catch (error) {
1662
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1663
- const contextualMessage = `Error fetching ticker info for ${symbol}`;
1664
- console.error(`${contextualMessage}: ${errorMessage}`, {
1665
- symbol,
1666
- errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
1667
- ? 'AUTH_ERROR'
1668
- : error instanceof Error && error.message.includes('RATE_LIMIT')
1669
- ? 'RATE_LIMIT'
1670
- : error instanceof Error && error.message.includes('NETWORK_ERROR')
1671
- ? 'NETWORK_ERROR'
1672
- : 'UNKNOWN',
1673
- url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
1674
- source: 'PolygonAPI.fetchTickerInfo',
1675
- timestamp: new Date().toISOString(),
1676
- });
1677
- throw new Error(`${contextualMessage}: ${errorMessage}`);
1678
- }
1679
- });
1680
- };
1681
- // Fetch last trade using Polygon.io
1682
- /**
1683
- * Fetches the last trade for a given stock ticker.
1684
- * @param {string} symbol - The stock ticker symbol to fetch the last trade for.
1685
- * @param {Object} [options] - Optional parameters.
1686
- * @param {string} [options.apiKey] - The API key to use for the request.
1687
- * @returns {Promise<PolygonQuote>} The last trade information.
1688
- */
1689
- const fetchLastTrade = async (symbol, options) => {
1690
- if (!options?.apiKey && !POLYGON_API_KEY) {
1691
- throw new Error('Polygon API key is missing');
1692
- }
1693
- const baseUrl = `https://api.polygon.io/v2/last/trade/${encodeURIComponent(symbol)}`;
1694
- const params = new URLSearchParams({
1695
- apiKey: options?.apiKey || POLYGON_API_KEY,
1696
- });
1697
- return polygonLimit(async () => {
1698
- try {
1699
- const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
1700
- const data = await response.json();
1701
- if (data.status !== 'OK' || !data.results) {
1702
- throw new Error(`Polygon.io API error: ${data.status || 'No results'} ${data.error || ''}`);
1703
- }
1704
- const { p: price, s: vol, t: timestamp } = data.results;
1705
- if (typeof price !== 'number' || typeof vol !== 'number' || typeof timestamp !== 'number') {
1706
- throw new Error('Invalid trade data received from Polygon.io API');
1707
- }
1708
- return {
1709
- price,
1710
- vol,
1711
- time: new Date(Math.floor(timestamp / 1000000)), // Convert nanoseconds to milliseconds
1712
- };
1713
- }
1714
- catch (error) {
1715
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1716
- const contextualMessage = `Error fetching last trade for ${symbol}`;
1717
- console.error(`${contextualMessage}: ${errorMessage}`, {
1718
- symbol,
1719
- errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
1720
- ? 'AUTH_ERROR'
1721
- : error instanceof Error && error.message.includes('RATE_LIMIT')
1722
- ? 'RATE_LIMIT'
1723
- : error instanceof Error && error.message.includes('NETWORK_ERROR')
1724
- ? 'NETWORK_ERROR'
1725
- : 'UNKNOWN',
1726
- url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
1727
- source: 'PolygonAPI.fetchLastTrade',
1728
- timestamp: new Date().toISOString(),
1729
- });
1730
- throw new Error(`${contextualMessage}: ${errorMessage}`);
1731
- }
1732
- });
1733
- };
1734
- // use Polygon for all price data fetching
1735
- /**
1736
- * Fetches price data for a given stock ticker.
1737
- * @param {Object} params - The parameters for fetching price data.
1738
- * @param {string} params.ticker - The stock ticker symbol.
1739
- * @param {number} params.start - The start timestamp for fetching price data.
1740
- * @param {number} [params.end] - The end timestamp for fetching price data.
1741
- * @param {number} params.multiplier - The multiplier for the price data.
1742
- * @param {string} params.timespan - The timespan for the price data.
1743
- * @param {number} [params.limit] - The maximum number of price data points to fetch.
1744
- * @param {Object} [options] - Optional parameters.
1745
- * @param {string} [options.apiKey] - The API key to use for the request.
1746
- * @returns {Promise<PolygonPriceData[]>} The fetched price data.
1747
- */
1748
- const fetchPrices = async (params, options) => {
1749
- if (!options?.apiKey && !POLYGON_API_KEY) {
1750
- throw new Error('Polygon API key is missing');
1751
- }
1752
- const { ticker, start, end = Date.now().valueOf(), multiplier, timespan, limit = 1000 } = params;
1753
- const baseUrl = `https://api.polygon.io/v2/aggs/ticker/${encodeURIComponent(ticker)}/range/${multiplier}/${timespan}/${start}/${end}`;
1754
- const urlParams = new URLSearchParams({
1755
- apiKey: options?.apiKey || POLYGON_API_KEY,
1756
- adjusted: 'true',
1757
- sort: 'asc',
1758
- limit: limit.toString(),
1759
- });
1760
- return polygonLimit(async () => {
1761
- try {
1762
- let allResults = [];
1763
- let nextUrl = `${baseUrl}?${urlParams.toString()}`;
1764
- while (nextUrl) {
1765
- //console.log(`Debug: Fetching ${nextUrl}`);
1766
- const response = await fetchWithRetry(nextUrl, {}, 3, 1000);
1767
- const data = await response.json();
1768
- if (data.status !== 'OK') {
1769
- throw new Error(`Polygon.io API responded with status: ${data.status}`);
1770
- }
1771
- if (data.results) {
1772
- allResults = [...allResults, ...data.results];
1773
- }
1774
- // Check if there's a next page and append API key
1775
- nextUrl = data.next_url ? `${data.next_url}&apiKey=${options?.apiKey || POLYGON_API_KEY}` : '';
1776
- }
1777
- return allResults.map((entry) => ({
1778
- date: new Date(entry.t).toLocaleString('en-US', {
1779
- year: 'numeric',
1780
- month: 'short',
1781
- day: '2-digit',
1782
- hour: '2-digit',
1783
- minute: '2-digit',
1784
- second: '2-digit',
1785
- timeZone: 'America/New_York',
1786
- timeZoneName: 'short',
1787
- hourCycle: 'h23',
1788
- }),
1789
- timeStamp: entry.t,
1790
- open: entry.o,
1791
- high: entry.h,
1792
- low: entry.l,
1793
- close: entry.c,
1794
- vol: entry.v,
1795
- vwap: entry.vw,
1796
- trades: entry.n,
1797
- }));
1798
- }
1799
- catch (error) {
1800
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1801
- const contextualMessage = `Error fetching price data for ${ticker}`;
1802
- console.error(`${contextualMessage}: ${errorMessage}`, {
1803
- ticker,
1804
- errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
1805
- ? 'AUTH_ERROR'
1806
- : error instanceof Error && error.message.includes('RATE_LIMIT')
1807
- ? 'RATE_LIMIT'
1808
- : error instanceof Error && error.message.includes('NETWORK_ERROR')
1809
- ? 'NETWORK_ERROR'
1810
- : 'UNKNOWN',
1811
- source: 'PolygonAPI.fetchPrices',
1812
- timestamp: new Date().toISOString(),
1813
- });
1814
- throw new Error(`${contextualMessage}: ${errorMessage}`);
1815
- }
1816
- });
1817
- };
1818
- /**
1819
- * Analyzes the price data for a given stock.
1820
- * @param {PolygonPriceData[]} priceData - The price data to analyze.
1821
- * @returns {string} The analysis report.
1822
- */
1823
- function analysePolygonPriceData(priceData) {
1824
- if (!priceData || priceData.length === 0) {
1825
- return 'No price data available for analysis.';
1826
- }
1827
- // Parse the dates into Date objects
1828
- const parsedData = priceData.map((entry) => ({
1829
- ...entry,
1830
- date: new Date(entry.date),
1831
- }));
1832
- // Sort the data by date
1833
- parsedData.sort((a, b) => a.date.getTime() - b.date.getTime());
1834
- // Extract start and end times
1835
- const startTime = parsedData[0].date;
1836
- const endTime = parsedData[parsedData.length - 1].date;
1837
- // Calculate the total time in hours
1838
- (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
1839
- // Calculate the interval between data points
1840
- const intervals = parsedData
1841
- .slice(1)
1842
- .map((_, i) => (parsedData[i + 1].date.getTime() - parsedData[i].date.getTime()) / 1000); // in seconds
1843
- const avgInterval = intervals.length > 0 ? intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length : 0;
1844
- // Format the report
1845
- const report = `
1846
- Report:
1847
- * Start time of data (US Eastern): ${startTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
1848
- * End time of data (US Eastern): ${endTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
1849
- * Number of data points: ${priceData.length}
1850
- * Average interval between data points (seconds): ${avgInterval.toFixed(2)}
1851
- `;
1852
- return report.trim();
1853
- }
1854
- /**
1855
- * Fetches grouped daily price data for a specific date.
1856
- * @param {string} date - The date to fetch grouped daily data for.
1857
- * @param {Object} [options] - Optional parameters.
1858
- * @param {string} [options.apiKey] - The API key to use for the request.
1859
- * @param {boolean} [options.adjusted] - Whether to adjust the data.
1860
- * @param {boolean} [options.includeOTC] - Whether to include OTC data.
1861
- * @returns {Promise<PolygonGroupedDailyResponse>} The grouped daily response.
1862
- */
1863
- const fetchGroupedDaily = async (date, options) => {
1864
- if (!options?.apiKey && !POLYGON_API_KEY) {
1865
- throw new Error('Polygon API key is missing');
1866
- }
1867
- const baseUrl = `https://api.polygon.io/v2/aggs/grouped/locale/us/market/stocks/${date}`;
1868
- const params = new URLSearchParams({
1869
- apiKey: options?.apiKey || POLYGON_API_KEY,
1870
- adjusted: options?.adjusted !== false ? 'true' : 'false',
1871
- include_otc: options?.includeOTC ? 'true' : 'false',
1872
- });
1873
- return polygonLimit(async () => {
1874
- try {
1875
- const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
1876
- const data = await response.json();
1877
- if (data.status !== 'OK') {
1878
- throw new Error(`Polygon.io API responded with status: ${data.status}`);
1879
- }
1880
- return {
1881
- adjusted: data.adjusted,
1882
- queryCount: data.queryCount,
1883
- request_id: data.request_id,
1884
- resultsCount: data.resultsCount,
1885
- status: data.status,
1886
- results: data.results.map((result) => ({
1887
- symbol: result.T,
1888
- timeStamp: result.t,
1889
- open: result.o,
1890
- high: result.h,
1891
- low: result.l,
1892
- close: result.c,
1893
- vol: result.v,
1894
- vwap: result.vw,
1895
- trades: result.n,
1896
- })),
1897
- };
1898
- }
1899
- catch (error) {
1900
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1901
- const contextualMessage = `Error fetching grouped daily data for ${date}`;
1902
- console.error(`${contextualMessage}: ${errorMessage}`, {
1903
- date,
1904
- errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
1905
- ? 'AUTH_ERROR'
1906
- : error instanceof Error && error.message.includes('RATE_LIMIT')
1907
- ? 'RATE_LIMIT'
1908
- : error instanceof Error && error.message.includes('NETWORK_ERROR')
1909
- ? 'NETWORK_ERROR'
1910
- : 'UNKNOWN',
1911
- url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
1912
- source: 'PolygonAPI.fetchGroupedDaily',
1913
- timestamp: new Date().toISOString(),
1914
- });
1915
- throw new Error(`${contextualMessage}: ${errorMessage}`);
1916
- }
1917
- });
1918
- };
1919
- /**
1920
- * Formats the price data into a readable string.
1921
- * @param {PolygonPriceData[]} priceData - The price data to format.
1922
- * @returns {string} The formatted price data.
1923
- */
1924
- function formatPriceData(priceData) {
1925
- if (!priceData || priceData.length === 0)
1926
- return 'No price data available';
1927
- return priceData
1928
- .map((d) => {
1929
- // For daily data, remove the time portion if it's all zeros
1930
- const dateStr = d.date.includes(', 00:00:00') ? d.date.split(', 00:00:00')[0] : d.date;
1931
- return [
1932
- dateStr,
1933
- `O: ${formatCurrency(d.open)}`,
1934
- `H: ${formatCurrency(d.high)}`,
1935
- `L: ${formatCurrency(d.low)}`,
1936
- `C: ${formatCurrency(d.close)}`,
1937
- `Vol: ${d.vol}`,
1938
- ].join(' | ');
1939
- })
1940
- .join('\n');
1941
- }
1942
- const fetchDailyOpenClose = async (
1943
- /**
1944
- * Fetches the daily open and close data for a given stock ticker.
1945
- * @param {string} symbol - The stock ticker symbol to fetch data for.
1946
- * @param {Date} [date=new Date()] - The date to fetch data for.
1947
- * @param {Object} [options] - Optional parameters.
1948
- * @param {string} [options.apiKey] - The API key to use for the request.
1949
- * @param {boolean} [options.adjusted] - Whether to adjust the data.
1950
- * @returns {Promise<PolygonDailyOpenClose>} The daily open and close data.
1951
- */
1952
- symbol, date = new Date(), options) => {
1953
- if (!options?.apiKey && !POLYGON_API_KEY) {
1954
- throw new Error('Polygon API key is missing');
1955
- }
1956
- const formattedDate = date.toISOString().split('T')[0]; // Format as YYYY-MM-DD
1957
- const baseUrl = `https://api.polygon.io/v1/open-close/${encodeURIComponent(symbol)}/${formattedDate}`;
1958
- const params = new URLSearchParams({
1959
- apiKey: options?.apiKey || POLYGON_API_KEY,
1960
- adjusted: (options?.adjusted ?? true).toString(),
1961
- });
1962
- return polygonLimit(async () => {
1963
- const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
1964
- const data = await response.json();
1965
- if (data.status !== 'OK') {
1966
- throw new Error(`Failed to fetch daily open/close data for ${symbol}: ${data.status}`);
1967
- }
1968
- return data;
1969
- });
1970
- };
1971
- /**
1972
- * Gets the previous close price for a given stock ticker.
1973
- * @param {string} symbol - The stock ticker symbol to fetch the previous close for.
1974
- * @param {Date} [referenceDate] - The reference date to use for fetching the previous close.
1975
- * @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
1976
- */
1977
- async function getPreviousClose(symbol, referenceDate, options) {
1978
- const previousDate = getLastFullTradingDate(referenceDate);
1979
- const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
1980
- if (!lastOpenClose) {
1981
- throw new Error(`Could not fetch last trade price for ${symbol}`);
1982
- }
1983
- return {
1984
- close: lastOpenClose.close,
1985
- date: previousDate,
1986
- };
1987
- }
1988
- /**
1989
- * Fetches trade data for a given stock ticker.
1990
- * @param {string} symbol - The stock ticker symbol to fetch trades for.
1991
- * @param {Object} [options] - Optional parameters.
1992
- * @param {string} [options.apiKey] - The API key to use for the request.
1993
- * @param {string | number} [options.timestamp] - The timestamp for fetching trades.
1994
- * @param {string | number} [options.timestampgt] - Greater than timestamp for fetching trades.
1995
- * @param {string | number} [options.timestampgte] - Greater than or equal to timestamp for fetching trades.
1996
- * @param {string | number} [options.timestamplt] - Less than timestamp for fetching trades.
1997
- * @param {string | number} [options.timestamplte] - Less than or equal to timestamp for fetching trades.
1998
- * @param {'asc' | 'desc'} [options.order] - The order of the trades.
1999
- * @param {number} [options.limit] - The maximum number of trades to fetch.
2000
- * @param {string} [options.sort] - The sort order for the trades.
2001
- * @returns {Promise<PolygonTradesResponse>} The fetched trades response.
2002
- */
2003
- const fetchTrades = async (symbol, options) => {
2004
- if (!options?.apiKey && !POLYGON_API_KEY) {
2005
- throw new Error('Polygon API key is missing');
2006
- }
2007
- const baseUrl = `https://api.polygon.io/v3/trades/${encodeURIComponent(symbol)}`;
2008
- const params = new URLSearchParams({
2009
- apiKey: options?.apiKey || POLYGON_API_KEY,
2010
- });
2011
- // Add optional parameters if they exist
2012
- if (options?.timestamp)
2013
- params.append('timestamp', options.timestamp.toString());
2014
- if (options?.timestampgt)
2015
- params.append('timestamp.gt', options.timestampgt.toString());
2016
- if (options?.timestampgte)
2017
- params.append('timestamp.gte', options.timestampgte.toString());
2018
- if (options?.timestamplt)
2019
- params.append('timestamp.lt', options.timestamplt.toString());
2020
- if (options?.timestamplte)
2021
- params.append('timestamp.lte', options.timestamplte.toString());
2022
- if (options?.order)
2023
- params.append('order', options.order);
2024
- if (options?.limit)
2025
- params.append('limit', options.limit.toString());
2026
- if (options?.sort)
2027
- params.append('sort', options.sort);
2028
- return polygonLimit(async () => {
2029
- const url = `${baseUrl}?${params.toString()}`;
2030
- try {
2031
- console.log(`[DEBUG] Fetching trades for ${symbol} from ${url}`);
2032
- const response = await fetchWithRetry(url, {}, 3, 1000);
2033
- const data = (await response.json());
2034
- if ('message' in data) {
2035
- // This is an error response
2036
- throw new Error(`Polygon API Error: ${data.message}`);
2037
- }
2038
- return data;
2039
- }
2040
- catch (error) {
2041
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2042
- const contextualMessage = `Error fetching trades for ${symbol}`;
2043
- console.error(`${contextualMessage}: ${errorMessage}`, {
2044
- symbol,
2045
- errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
2046
- ? 'AUTH_ERROR'
2047
- : error instanceof Error && error.message.includes('RATE_LIMIT')
2048
- ? 'RATE_LIMIT'
2049
- : error instanceof Error && error.message.includes('NETWORK_ERROR')
2050
- ? 'NETWORK_ERROR'
2051
- : 'UNKNOWN',
2052
- url: hideApiKeyFromurl(url),
2053
- source: 'PolygonAPI.fetchTrades',
2054
- timestamp: new Date().toISOString(),
2055
- });
2056
- throw new Error(`${contextualMessage}: ${errorMessage}`);
2057
- }
2058
- });
2059
- };
574
+ pLimit(POLYGON_CONCURRENCY_LIMIT);
2060
575
 
2061
576
  /**
2062
577
  * Polygon Indices API Implementation
@@ -2067,224 +582,7 @@ const fetchTrades = async (symbol, options) => {
2067
582
  const { ALPACA_INDICES_API_KEY } = process.env;
2068
583
  // Define concurrency limits for API
2069
584
  const POLYGON_INDICES_CONCURRENCY_LIMIT = 5;
2070
- const polygonIndicesLimit = pLimit(POLYGON_INDICES_CONCURRENCY_LIMIT);
2071
- // Base URL for Polygon API
2072
- const POLYGON_API_BASE_URL = 'https://api.polygon.io';
2073
- /**
2074
- * Validates that an API key is available
2075
- * @param {string | undefined} apiKey - Optional API key to use
2076
- * @throws {Error} If no API key is available
2077
- */
2078
- const validateApiKey = (apiKey) => {
2079
- const key = apiKey || ALPACA_INDICES_API_KEY;
2080
- if (!key) {
2081
- throw new Error('Polygon Indices API key is missing');
2082
- }
2083
- return key;
2084
- };
2085
- /**
2086
- * Fetches aggregate bars for an index over a given date range in custom time window sizes.
2087
- *
2088
- * @param {PolygonIndicesAggregatesParams} params - Parameters for the aggregates request
2089
- * @param {Object} [options] - Optional parameters
2090
- * @param {string} [options.apiKey] - API key to use for the request
2091
- * @returns {Promise<PolygonIndicesAggregatesResponse>} The aggregates response
2092
- */
2093
- const fetchIndicesAggregates = async (params, options) => {
2094
- const apiKey = validateApiKey(options?.apiKey);
2095
- const { indicesTicker, multiplier, timespan, from, to, sort = 'asc', limit } = params;
2096
- const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/range/${multiplier}/${timespan}/${from}/${to}`);
2097
- const queryParams = new URLSearchParams();
2098
- queryParams.append('apiKey', apiKey);
2099
- if (sort) {
2100
- queryParams.append('sort', sort);
2101
- }
2102
- if (limit) {
2103
- queryParams.append('limit', limit.toString());
2104
- }
2105
- url.search = queryParams.toString();
2106
- return polygonIndicesLimit(async () => {
2107
- try {
2108
- const response = await fetchWithRetry(url.toString(), {}, 3, 300);
2109
- const data = await response.json();
2110
- if (data.status === 'ERROR') {
2111
- throw new Error(`Polygon API Error: ${data.error}`);
2112
- }
2113
- return data;
2114
- }
2115
- catch (error) {
2116
- console.error('Error fetching indices aggregates:', error);
2117
- throw error;
2118
- }
2119
- });
2120
- };
2121
- /**
2122
- * Gets the previous day's open, high, low, and close (OHLC) for the specified index.
2123
- *
2124
- * @param {string} indicesTicker - The ticker symbol of the index
2125
- * @param {Object} [options] - Optional parameters
2126
- * @param {string} [options.apiKey] - API key to use for the request
2127
- * @returns {Promise<PolygonIndicesPrevCloseResponse>} The previous close response
2128
- */
2129
- const fetchIndicesPreviousClose = async (indicesTicker, options) => {
2130
- const apiKey = validateApiKey(options?.apiKey);
2131
- const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/prev`);
2132
- const queryParams = new URLSearchParams();
2133
- queryParams.append('apiKey', apiKey);
2134
- url.search = queryParams.toString();
2135
- return polygonIndicesLimit(async () => {
2136
- try {
2137
- const response = await fetchWithRetry(url.toString(), {}, 3, 300);
2138
- const data = await response.json();
2139
- if (data.status === 'ERROR') {
2140
- throw new Error(`Polygon API Error: ${data.error}`);
2141
- }
2142
- return data;
2143
- }
2144
- catch (error) {
2145
- console.error('Error fetching indices previous close:', error);
2146
- throw error;
2147
- }
2148
- });
2149
- };
2150
- /**
2151
- * Gets the open, close and afterhours values of an index symbol on a certain date.
2152
- *
2153
- * @param {string} indicesTicker - The ticker symbol of the index
2154
- * @param {string} date - The date in YYYY-MM-DD format
2155
- * @param {Object} [options] - Optional parameters
2156
- * @param {string} [options.apiKey] - API key to use for the request
2157
- * @returns {Promise<PolygonIndicesDailyOpenCloseResponse>} The daily open/close response
2158
- */
2159
- const fetchIndicesDailyOpenClose = async (indicesTicker, date, options) => {
2160
- const apiKey = validateApiKey(options?.apiKey);
2161
- const url = new URL(`${POLYGON_API_BASE_URL}/v1/open-close/${encodeURIComponent(indicesTicker)}/${date}`);
2162
- const queryParams = new URLSearchParams();
2163
- queryParams.append('apiKey', apiKey);
2164
- url.search = queryParams.toString();
2165
- return polygonIndicesLimit(async () => {
2166
- try {
2167
- const response = await fetchWithRetry(url.toString(), {}, 3, 300);
2168
- const data = await response.json();
2169
- if (data.status === 'ERROR') {
2170
- throw new Error(`Polygon API Error: ${data.error}`);
2171
- }
2172
- return data;
2173
- }
2174
- catch (error) {
2175
- console.error('Error fetching indices daily open/close:', error);
2176
- throw error;
2177
- }
2178
- });
2179
- };
2180
- /**
2181
- * Gets a snapshot of indices data for specified tickers.
2182
- *
2183
- * @param {PolygonIndicesSnapshotParams} [params] - Parameters for the snapshot request
2184
- * @param {Object} [options] - Optional parameters
2185
- * @param {string} [options.apiKey] - API key to use for the request
2186
- * @returns {Promise<PolygonIndicesSnapshotResponse>} The indices snapshot response
2187
- */
2188
- const fetchIndicesSnapshot = async (params, options) => {
2189
- const apiKey = validateApiKey(options?.apiKey);
2190
- const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot/indices`);
2191
- const queryParams = new URLSearchParams();
2192
- queryParams.append('apiKey', apiKey);
2193
- if (params?.tickers?.length) {
2194
- queryParams.append('ticker.any_of', params.tickers.join(','));
2195
- }
2196
- if (params?.order) {
2197
- queryParams.append('order', params.order);
2198
- }
2199
- if (params?.limit) {
2200
- queryParams.append('limit', params.limit.toString());
2201
- }
2202
- if (params?.sort) {
2203
- queryParams.append('sort', params.sort);
2204
- }
2205
- url.search = queryParams.toString();
2206
- return polygonIndicesLimit(async () => {
2207
- try {
2208
- const response = await fetchWithRetry(url.toString(), {}, 3, 300);
2209
- const data = await response.json();
2210
- if (data.status === 'ERROR') {
2211
- throw new Error(`Polygon API Error: ${data.error}`);
2212
- }
2213
- return data;
2214
- }
2215
- catch (error) {
2216
- console.error('Error fetching indices snapshot:', error);
2217
- throw error;
2218
- }
2219
- });
2220
- };
2221
- /**
2222
- * Gets snapshots for assets of all types, including indices.
2223
- *
2224
- * @param {string[]} tickers - Array of tickers to fetch snapshots for
2225
- * @param {Object} [options] - Optional parameters
2226
- * @param {string} [options.apiKey] - API key to use for the request
2227
- * @param {string} [options.type] - Filter by asset type
2228
- * @param {string} [options.order] - Order results
2229
- * @param {number} [options.limit] - Limit the number of results
2230
- * @param {string} [options.sort] - Sort field
2231
- * @returns {Promise<any>} The universal snapshot response
2232
- */
2233
- const fetchUniversalSnapshot = async (tickers, options) => {
2234
- const apiKey = validateApiKey(options?.apiKey);
2235
- const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot`);
2236
- const queryParams = new URLSearchParams();
2237
- queryParams.append('apiKey', apiKey);
2238
- if (tickers.length) {
2239
- queryParams.append('ticker.any_of', tickers.join(','));
2240
- }
2241
- if (options?.type) {
2242
- queryParams.append('type', options.type);
2243
- }
2244
- if (options?.order) {
2245
- queryParams.append('order', options.order);
2246
- }
2247
- if (options?.limit) {
2248
- queryParams.append('limit', options.limit.toString());
2249
- }
2250
- if (options?.sort) {
2251
- queryParams.append('sort', options.sort);
2252
- }
2253
- url.search = queryParams.toString();
2254
- return polygonIndicesLimit(async () => {
2255
- try {
2256
- const response = await fetchWithRetry(url.toString(), {}, 3, 300);
2257
- const data = await response.json();
2258
- if (data.status === 'ERROR') {
2259
- throw new Error(`Polygon API Error: ${data.error}`);
2260
- }
2261
- return data;
2262
- }
2263
- catch (error) {
2264
- console.error('Error fetching universal snapshot:', error);
2265
- throw error;
2266
- }
2267
- });
2268
- };
2269
- /**
2270
- * Converts Polygon Indices bar data to a more standardized format
2271
- *
2272
- * @param {PolygonIndicesAggregatesResponse} data - The raw aggregates response
2273
- * @returns {Array<{date: string, open: number, high: number, low: number, close: number, timestamp: number}>} Formatted bar data
2274
- */
2275
- const formatIndicesBarData = (data) => {
2276
- return data.results.map((bar) => {
2277
- const date = new Date(bar.t);
2278
- return {
2279
- date: date.toISOString().split('T')[0],
2280
- open: bar.o,
2281
- high: bar.h,
2282
- low: bar.l,
2283
- close: bar.c,
2284
- timestamp: bar.t,
2285
- };
2286
- });
2287
- };
585
+ pLimit(POLYGON_INDICES_CONCURRENCY_LIMIT);
2288
586
 
2289
587
  function __classPrivateFieldSet(receiver, state, value, kind, f) {
2290
588
  if (typeof state === "function" ? receiver !== state || true : !state.has(receiver))
@@ -7901,9 +6199,9 @@ function maybeParseResponse(response, params) {
7901
6199
  }),
7902
6200
  };
7903
6201
  }
7904
- return parseResponse$1(response, params);
6202
+ return parseResponse(response, params);
7905
6203
  }
7906
- function parseResponse$1(response, params) {
6204
+ function parseResponse(response, params) {
7907
6205
  const output = response.output.map((item) => {
7908
6206
  if (item.type === 'function_call') {
7909
6207
  return {
@@ -8335,7 +6633,7 @@ class Responses extends APIResource {
8335
6633
  parse(body, options) {
8336
6634
  return this._client.responses
8337
6635
  .create(body, options)
8338
- ._thenUnwrap((response) => parseResponse$1(response, body));
6636
+ ._thenUnwrap((response) => parseResponse(response, body));
8339
6637
  }
8340
6638
  /**
8341
6639
  * Creates a model response stream
@@ -9336,2030 +7634,126 @@ class OpenAI {
9336
7634
  return sleepSeconds * jitter * 1000;
9337
7635
  }
9338
7636
  async buildRequest(inputOptions, { retryCount = 0 } = {}) {
9339
- const options = { ...inputOptions };
9340
- const { method, path, query, defaultBaseURL } = options;
9341
- const url = this.buildURL(path, query, defaultBaseURL);
9342
- if ('timeout' in options)
9343
- validatePositiveInteger('timeout', options.timeout);
9344
- options.timeout = options.timeout ?? this.timeout;
9345
- const { bodyHeaders, body } = this.buildBody({ options });
9346
- const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
9347
- const req = {
9348
- method,
9349
- headers: reqHeaders,
9350
- ...(options.signal && { signal: options.signal }),
9351
- ...(globalThis.ReadableStream &&
9352
- body instanceof globalThis.ReadableStream && { duplex: 'half' }),
9353
- ...(body && { body }),
9354
- ...(this.fetchOptions ?? {}),
9355
- ...(options.fetchOptions ?? {}),
9356
- };
9357
- return { req, url, timeout: options.timeout };
9358
- }
9359
- async buildHeaders({ options, method, bodyHeaders, retryCount, }) {
9360
- let idempotencyHeaders = {};
9361
- if (this.idempotencyHeader && method !== 'get') {
9362
- if (!options.idempotencyKey)
9363
- options.idempotencyKey = this.defaultIdempotencyKey();
9364
- idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
9365
- }
9366
- const headers = buildHeaders([
9367
- idempotencyHeaders,
9368
- {
9369
- Accept: 'application/json',
9370
- 'User-Agent': this.getUserAgent(),
9371
- 'X-Stainless-Retry-Count': String(retryCount),
9372
- ...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
9373
- ...getPlatformHeaders(),
9374
- 'OpenAI-Organization': this.organization,
9375
- 'OpenAI-Project': this.project,
9376
- },
9377
- await this.authHeaders(options),
9378
- this._options.defaultHeaders,
9379
- bodyHeaders,
9380
- options.headers,
9381
- ]);
9382
- this.validateHeaders(headers);
9383
- return headers.values;
9384
- }
9385
- buildBody({ options: { body, headers: rawHeaders } }) {
9386
- if (!body) {
9387
- return { bodyHeaders: undefined, body: undefined };
9388
- }
9389
- const headers = buildHeaders([rawHeaders]);
9390
- if (
9391
- // Pass raw type verbatim
9392
- ArrayBuffer.isView(body) ||
9393
- body instanceof ArrayBuffer ||
9394
- body instanceof DataView ||
9395
- (typeof body === 'string' &&
9396
- // Preserve legacy string encoding behavior for now
9397
- headers.values.has('content-type')) ||
9398
- // `Blob` is superset of `File`
9399
- (globalThis.Blob && body instanceof globalThis.Blob) ||
9400
- // `FormData` -> `multipart/form-data`
9401
- body instanceof FormData ||
9402
- // `URLSearchParams` -> `application/x-www-form-urlencoded`
9403
- body instanceof URLSearchParams ||
9404
- // Send chunked stream (each chunk has own `length`)
9405
- (globalThis.ReadableStream && body instanceof globalThis.ReadableStream)) {
9406
- return { bodyHeaders: undefined, body: body };
9407
- }
9408
- else if (typeof body === 'object' &&
9409
- (Symbol.asyncIterator in body ||
9410
- (Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))) {
9411
- return { bodyHeaders: undefined, body: ReadableStreamFrom(body) };
9412
- }
9413
- else {
9414
- return __classPrivateFieldGet(this, _OpenAI_encoder, "f").call(this, { body, headers });
9415
- }
9416
- }
9417
- }
9418
- _a = OpenAI, _OpenAI_encoder = new WeakMap(), _OpenAI_instances = new WeakSet(), _OpenAI_baseURLOverridden = function _OpenAI_baseURLOverridden() {
9419
- return this.baseURL !== 'https://api.openai.com/v1';
9420
- };
9421
- OpenAI.OpenAI = _a;
9422
- OpenAI.DEFAULT_TIMEOUT = 600000; // 10 minutes
9423
- OpenAI.OpenAIError = OpenAIError;
9424
- OpenAI.APIError = APIError;
9425
- OpenAI.APIConnectionError = APIConnectionError;
9426
- OpenAI.APIConnectionTimeoutError = APIConnectionTimeoutError;
9427
- OpenAI.APIUserAbortError = APIUserAbortError;
9428
- OpenAI.NotFoundError = NotFoundError;
9429
- OpenAI.ConflictError = ConflictError;
9430
- OpenAI.RateLimitError = RateLimitError;
9431
- OpenAI.BadRequestError = BadRequestError;
9432
- OpenAI.AuthenticationError = AuthenticationError;
9433
- OpenAI.InternalServerError = InternalServerError;
9434
- OpenAI.PermissionDeniedError = PermissionDeniedError;
9435
- OpenAI.UnprocessableEntityError = UnprocessableEntityError;
9436
- OpenAI.InvalidWebhookSignatureError = InvalidWebhookSignatureError;
9437
- OpenAI.toFile = toFile;
9438
- OpenAI.Completions = Completions;
9439
- OpenAI.Chat = Chat;
9440
- OpenAI.Embeddings = Embeddings;
9441
- OpenAI.Files = Files$1;
9442
- OpenAI.Images = Images;
9443
- OpenAI.Audio = Audio;
9444
- OpenAI.Moderations = Moderations;
9445
- OpenAI.Models = Models;
9446
- OpenAI.FineTuning = FineTuning;
9447
- OpenAI.Graders = Graders;
9448
- OpenAI.VectorStores = VectorStores;
9449
- OpenAI.Webhooks = Webhooks;
9450
- OpenAI.Beta = Beta;
9451
- OpenAI.Batches = Batches;
9452
- OpenAI.Uploads = Uploads;
9453
- OpenAI.Responses = Responses;
9454
- OpenAI.Realtime = Realtime;
9455
- OpenAI.Conversations = Conversations;
9456
- OpenAI.Evals = Evals;
9457
- OpenAI.Containers = Containers;
9458
- OpenAI.Videos = Videos;
9459
-
9460
- // llm-openai-config.ts
9461
- const DEFAULT_MODEL = 'gpt-4.1-mini';
9462
- /** Token costs in USD per 1M tokens. Last updated Feb 2025. */
9463
- const openAiModelCosts = {
9464
- 'gpt-4o': {
9465
- inputCost: 2.5 / 1_000_000,
9466
- outputCost: 10 / 1_000_000,
9467
- },
9468
- 'gpt-4o-mini': {
9469
- inputCost: 0.15 / 1_000_000,
9470
- outputCost: 0.6 / 1_000_000,
9471
- },
9472
- 'o1-mini': {
9473
- inputCost: 1.1 / 1_000_000,
9474
- outputCost: 4.4 / 1_000_000,
9475
- },
9476
- 'o1': {
9477
- inputCost: 15 / 1_000_000,
9478
- outputCost: 60 / 1_000_000,
9479
- },
9480
- 'o3-mini': {
9481
- inputCost: 1.1 / 1_000_000,
9482
- outputCost: 4.4 / 1_000_000,
9483
- },
9484
- 'o3': {
9485
- inputCost: 2 / 1_000_000,
9486
- outputCost: 8 / 1_000_000,
9487
- },
9488
- 'gpt-4.1': {
9489
- inputCost: 2 / 1_000_000,
9490
- outputCost: 8 / 1_000_000,
9491
- },
9492
- 'gpt-4.1-mini': {
9493
- inputCost: 0.4 / 1_000_000,
9494
- outputCost: 1.6 / 1_000_000,
9495
- },
9496
- 'gpt-4.1-nano': {
9497
- inputCost: 0.1 / 1_000_000,
9498
- outputCost: 0.4 / 1_000_000,
9499
- },
9500
- 'gpt-5': {
9501
- inputCost: 1.25 / 1_000_000,
9502
- outputCost: 10 / 1_000_000,
9503
- },
9504
- 'gpt-5-mini': {
9505
- inputCost: 0.25 / 1_000_000,
9506
- outputCost: 2 / 1_000_000,
9507
- },
9508
- 'gpt-5-nano': {
9509
- inputCost: 0.05 / 1_000_000,
9510
- outputCost: 0.4 / 1_000_000,
9511
- },
9512
- 'gpt-5.1': {
9513
- inputCost: 1.25 / 1_000_000,
9514
- outputCost: 10 / 1_000_000,
9515
- },
9516
- 'gpt-5.2': {
9517
- inputCost: 1.5 / 1_000_000,
9518
- outputCost: 12 / 1_000_000,
9519
- },
9520
- 'gpt-5.2-pro': {
9521
- inputCost: 3 / 1_000_000,
9522
- outputCost: 24 / 1_000_000,
9523
- },
9524
- 'gpt-5.1-codex': {
9525
- inputCost: 1.1 / 1_000_000,
9526
- outputCost: 8.8 / 1_000_000,
9527
- },
9528
- 'gpt-5.1-codex-max': {
9529
- inputCost: 1.8 / 1_000_000,
9530
- outputCost: 14.4 / 1_000_000,
9531
- },
9532
- 'o4-mini': {
9533
- inputCost: 1.1 / 1_000_000,
9534
- outputCost: 4.4 / 1_000_000,
9535
- },
9536
- };
9537
- const deepseekModelCosts = {
9538
- 'deepseek-chat': {
9539
- inputCost: 0.27 / 1_000_000, // $0.27 per 1M tokens (Cache miss price)
9540
- cacheHitCost: 0.07 / 1_000_000, // $0.07 per 1M tokens (Cache hit price)
9541
- outputCost: 1.1 / 1_000_000, // $1.10 per 1M tokens
9542
- },
9543
- 'deepseek-reasoner': {
9544
- inputCost: 0.55 / 1_000_000, // $0.55 per 1M tokens (Cache miss price)
9545
- cacheHitCost: 0.14 / 1_000_000, // $0.14 per 1M tokens (Cache hit price)
9546
- outputCost: 2.19 / 1_000_000, // $2.19 per 1M tokens
9547
- },
9548
- };
9549
- /** Image generation costs in USD per image. Based on OpenAI pricing as of Feb 2025. */
9550
- const openAiImageCosts = {
9551
- 'gpt-image-1': 0.0075, // $0.0075 per image for gpt-image-1
9552
- 'gpt-image-1.5': 0.0075, // Assumes parity pricing with gpt-image-1 until OpenAI publishes updated rates
9553
- };
9554
- /**
9555
- * Calculates the cost of generating images using OpenAI's Images API.
9556
- *
9557
- * @param model The image generation model name.
9558
- * @param imageCount The number of images generated.
9559
- * @returns The cost of generating the images in USD.
9560
- */
9561
- function calculateImageCost(model, imageCount) {
9562
- if (typeof model !== 'string' || typeof imageCount !== 'number' || imageCount <= 0) {
9563
- return 0;
9564
- }
9565
- const costPerImage = openAiImageCosts[model];
9566
- if (!costPerImage)
9567
- return 0;
9568
- return imageCount * costPerImage;
9569
- }
9570
- /**
9571
- * Calculates the cost of calling a language model in USD based on the provider and model, tokens, and given costs per 1M tokens.
9572
- *
9573
- * @param provider The provider of the language model. Supported providers are 'openai' and 'deepseek'.
9574
- * @param model The name of the language model. Supported models are listed in the `openAiModelCosts` and `deepseekModelCosts` objects.
9575
- * @param inputTokens The number of input tokens passed to the language model.
9576
- * @param outputTokens The number of output tokens generated by the language model.
9577
- * @param reasoningTokens The number of output tokens generated by the language model for reasoning. This is only used for Deepseek models.
9578
- * @param cacheHitTokens The number of input tokens that were cache hits for Deepseek models.
9579
- * @returns The cost of calling the language model in USD.
9580
- */
9581
- function calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens) {
9582
- if (typeof provider !== 'string' ||
9583
- typeof model !== 'string' ||
9584
- typeof inputTokens !== 'number' ||
9585
- typeof outputTokens !== 'number' ||
9586
- (reasoningTokens !== undefined && typeof reasoningTokens !== 'number') ||
9587
- (cacheHitTokens !== undefined && typeof cacheHitTokens !== 'number')) {
9588
- return 0;
9589
- }
9590
- const modelCosts = provider === 'deepseek' ? deepseekModelCosts[model] : openAiModelCosts[model];
9591
- if (!modelCosts)
9592
- return 0;
9593
- // Calculate input cost based on cache hit/miss for Deepseek
9594
- const inputCost = provider === 'deepseek' && modelCosts.cacheHitCost
9595
- ? (cacheHitTokens || 0) * modelCosts.cacheHitCost + (inputTokens - (cacheHitTokens || 0)) * modelCosts.inputCost
9596
- : inputTokens * modelCosts.inputCost;
9597
- const outputCost = outputTokens * modelCosts.outputCost;
9598
- const reasoningCost = (reasoningTokens || 0) * modelCosts.outputCost;
9599
- return inputCost + outputCost + reasoningCost;
9600
- }
9601
-
9602
- /**
9603
- * Fix a broken JSON string by attempting to extract and parse valid JSON content. This function is very lenient and will attempt to fix many types of JSON errors, including unbalanced brackets, missing or extra commas, improperly escaped $ signs, unquoted strings, trailing commas, missing closing brackets or braces, etc.
9604
- * @param {string} jsonStr - The broken JSON string to fix
9605
- * @returns {JsonValue} - The parsed JSON value
9606
- */
9607
- function fixBrokenJson(jsonStr) {
9608
- // Pre-process: Fix improperly escaped $ signs
9609
- jsonStr = jsonStr.replace(/\\\$/g, '$');
9610
- let index = 0;
9611
- function parse() {
9612
- const results = [];
9613
- while (index < jsonStr.length) {
9614
- skipWhitespace();
9615
- const value = parseValue();
9616
- if (value !== undefined) {
9617
- results.push(value);
9618
- }
9619
- else {
9620
- index++; // Skip invalid character
9621
- }
9622
- }
9623
- return results.length === 1 ? results[0] : results;
9624
- }
9625
- function parseValue() {
9626
- skipWhitespace();
9627
- const char = getChar();
9628
- if (!char)
9629
- return undefined;
9630
- if (char === '{')
9631
- return parseObject();
9632
- if (char === '[')
9633
- return parseArray();
9634
- if (char === '"' || char === "'")
9635
- return parseString();
9636
- if (char === 't' && jsonStr.slice(index, index + 4).toLowerCase() === 'true') {
9637
- index += 4;
9638
- return true;
9639
- }
9640
- if (char === 'f' && jsonStr.slice(index, index + 5).toLowerCase() === 'false') {
9641
- index += 5;
9642
- return false;
9643
- }
9644
- if (char === 'n' && jsonStr.slice(index, index + 4).toLowerCase() === 'null') {
9645
- index += 4;
9646
- return null;
9647
- }
9648
- if (/[a-zA-Z]/.test(char))
9649
- return parseString(); // Unquoted string
9650
- if (char === '-' || char === '.' || /\d/.test(char))
9651
- return parseNumber();
9652
- return undefined; // Unknown character
9653
- }
9654
- function parseObject() {
9655
- const obj = {};
9656
- index++; // Skip opening brace
9657
- skipWhitespace();
9658
- while (index < jsonStr.length && getChar() !== '}') {
9659
- skipWhitespace();
9660
- const key = parseString();
9661
- if (key === undefined) {
9662
- console.warn(`Expected key at position ${index}`);
9663
- index++;
9664
- continue;
9665
- }
9666
- skipWhitespace();
9667
- if (getChar() === ':') {
9668
- index++; // Skip colon
9669
- }
9670
- else {
9671
- console.warn(`Missing colon after key "${key}" at position ${index}`);
9672
- }
9673
- skipWhitespace();
9674
- const value = parseValue();
9675
- if (value === undefined) {
9676
- console.warn(`Expected value for key "${key}" at position ${index}`);
9677
- index++;
9678
- continue;
9679
- }
9680
- obj[key] = value;
9681
- skipWhitespace();
9682
- if (getChar() === ',') {
9683
- index++; // Skip comma
9684
- }
9685
- else {
9686
- break;
9687
- }
9688
- skipWhitespace();
9689
- }
9690
- if (getChar() === '}') {
9691
- index++; // Skip closing brace
9692
- }
9693
- else {
9694
- // Add a closing brace if it's missing
9695
- jsonStr += '}';
9696
- }
9697
- return obj;
9698
- }
9699
- function parseArray() {
9700
- const arr = [];
9701
- index++; // Skip opening bracket
9702
- skipWhitespace();
9703
- while (index < jsonStr.length && getChar() !== ']') {
9704
- const value = parseValue();
9705
- if (value === undefined) {
9706
- console.warn(`Expected value at position ${index}`);
9707
- index++;
9708
- }
9709
- else {
9710
- arr.push(value);
9711
- }
9712
- skipWhitespace();
9713
- if (getChar() === ',') {
9714
- index++; // Skip comma
9715
- }
9716
- else {
9717
- break;
9718
- }
9719
- skipWhitespace();
9720
- }
9721
- if (getChar() === ']') {
9722
- index++; // Skip closing bracket
9723
- }
9724
- else {
9725
- console.warn(`Missing closing bracket for array at position ${index}`);
9726
- }
9727
- return arr;
9728
- }
9729
- function parseString() {
9730
- const startChar = getChar();
9731
- let result = '';
9732
- let isQuoted = false;
9733
- let delimiter = '';
9734
- if (startChar === '"' || startChar === "'") {
9735
- isQuoted = true;
9736
- delimiter = startChar;
9737
- index++; // Skip opening quote
9738
- }
9739
- while (index < jsonStr.length) {
9740
- const char = getChar();
9741
- if (isQuoted) {
9742
- if (char === delimiter) {
9743
- index++; // Skip closing quote
9744
- return result;
9745
- }
9746
- else if (char === '\\') {
9747
- index++;
9748
- const escapeChar = getChar();
9749
- const escapeSequences = {
9750
- n: '\n',
9751
- r: '\r',
9752
- t: '\t',
9753
- b: '\b',
9754
- f: '\f',
9755
- '\\': '\\',
9756
- '/': '/',
9757
- '"': '"',
9758
- "'": "'",
9759
- $: '$',
9760
- };
9761
- result += escapeSequences[escapeChar] || escapeChar;
9762
- }
9763
- else {
9764
- result += char;
9765
- }
9766
- index++;
9767
- }
9768
- else {
9769
- if (/\s|,|}|]|\:/.test(char)) {
9770
- return result;
9771
- }
9772
- else {
9773
- result += char;
9774
- index++;
9775
- }
9776
- }
9777
- }
9778
- if (isQuoted) {
9779
- console.warn(`Missing closing quote for string starting at position ${index}`);
9780
- }
9781
- return result;
9782
- }
9783
- function parseNumber() {
9784
- const numberRegex = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/;
9785
- const match = numberRegex.exec(jsonStr.slice(index));
9786
- if (match) {
9787
- index += match[0].length;
9788
- return parseFloat(match[0]);
9789
- }
9790
- else {
9791
- console.warn(`Invalid number at position ${index}`);
9792
- index++;
9793
- return undefined;
9794
- }
9795
- }
9796
- function getChar() {
9797
- return jsonStr[index];
9798
- }
9799
- function skipWhitespace() {
9800
- while (/\s/.test(getChar()))
9801
- index++;
9802
- }
9803
- try {
9804
- return parse();
9805
- }
9806
- catch (error) {
9807
- const msg = error instanceof Error ? error.message : String(error);
9808
- console.error(`Error parsing JSON at position ${index}: ${msg}`);
9809
- return null;
9810
- }
9811
- }
9812
- /**
9813
- * Returns true if the given JSON string is valid, false otherwise.
9814
- *
9815
- * This function simply attempts to parse the given string as JSON and returns true if successful, or false if an error is thrown.
9816
- *
9817
- * @param {string} jsonString The JSON string to validate.
9818
- * @returns {boolean} True if the JSON string is valid, false otherwise.
9819
- */
9820
- function isValidJson(jsonString) {
9821
- try {
9822
- JSON.parse(jsonString);
9823
- return true;
9824
- }
9825
- catch {
9826
- return false;
9827
- }
9828
- }
9829
- let openai;
9830
- /**
9831
- * Initializes an instance of the OpenAI client, using the provided API key or the value of the OPENAI_API_KEY environment variable if no key is provided.
9832
- * @param apiKey - the API key to use, or undefined to use the value of the OPENAI_API_KEY environment variable
9833
- * @returns an instance of the OpenAI client
9834
- * @throws an error if the API key is not provided and the OPENAI_API_KEY environment variable is not set
9835
- */
9836
- function initializeOpenAI(apiKey) {
9837
- const key = process.env.OPENAI_API_KEY;
9838
- if (!key) {
9839
- throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
9840
- }
9841
- return new OpenAI({
9842
- apiKey: key,
9843
- defaultHeaders: { 'User-Agent': 'My Server-side Application (compatible; OpenAI API Client)' },
9844
- });
9845
- }
9846
- /**
9847
- * Fixes broken JSON by sending it to OpenAI to fix it.
9848
- * If the model fails to return valid JSON, an error is thrown.
9849
- * @param jsonStr - the broken JSON to fix
9850
- * @param apiKey - the OpenAI API key to use, or undefined to use the value of the OPENAI_API_KEY environment variable
9851
- * @returns the fixed JSON
9852
- * @throws an error if the model fails to return valid JSON
9853
- */
9854
- async function fixJsonWithAI(jsonStr, apiKey) {
9855
- try {
9856
- if (!openai) {
9857
- openai = initializeOpenAI();
9858
- }
9859
- const completion = await openai.chat.completions.create({
9860
- model: DEFAULT_MODEL,
9861
- messages: [
9862
- {
9863
- role: 'system',
9864
- content: 'You are a JSON fixer. Return only valid JSON without any additional text or explanation.',
9865
- },
9866
- {
9867
- role: 'user',
9868
- content: `Fix this broken JSON:\n${jsonStr}`,
9869
- },
9870
- ],
9871
- response_format: { type: 'json_object' },
9872
- });
9873
- const fixedJson = completion.choices[0]?.message?.content;
9874
- if (fixedJson && isValidJson(fixedJson)) {
9875
- return JSON.parse(fixedJson);
9876
- }
9877
- throw new Error('Failed to fix JSON with AI');
9878
- }
9879
- catch (err) {
9880
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
9881
- throw new Error(`Error fixing JSON with AI: ${errorMessage}`);
9882
- }
9883
- }
9884
-
9885
- // llm-utils.ts
9886
- function normalizeModelName(model) {
9887
- return model.replace(/_/g, '-').toLowerCase();
9888
- }
9889
- const CODE_BLOCK_TYPES = ['javascript', 'js', 'graphql', 'json', 'typescript', 'python', 'markdown', 'yaml'];
9890
- /**
9891
- * Tries to parse JSON from the given content string according to the specified
9892
- * response format. If the response format is 'json' or a JSON schema object, it
9893
- * will attempt to parse the content using multiple strategies. If any of the
9894
- * strategies succeed, it will return the parsed JSON object. If all of them fail,
9895
- * it will throw an error.
9896
- *
9897
- * @param content The content string to parse
9898
- * @param responseFormat The desired format of the response
9899
- * @returns The parsed JSON object or null if it fails
9900
- */
9901
- async function parseResponse(content, responseFormat) {
9902
- if (!content)
9903
- return null;
9904
- if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema')) {
9905
- let cleanedContent = content.trim();
9906
- let detectedType = null;
9907
- // Remove code block markers if present
9908
- const startsWithCodeBlock = CODE_BLOCK_TYPES.some((type) => {
9909
- if (cleanedContent.startsWith(`\`\`\`${type}`)) {
9910
- detectedType = type;
9911
- return true;
9912
- }
9913
- return false;
9914
- });
9915
- if (startsWithCodeBlock && cleanedContent.endsWith('```')) {
9916
- const firstLineEndIndex = cleanedContent.indexOf('\n');
9917
- if (firstLineEndIndex !== -1) {
9918
- cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
9919
- console.log(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
9920
- }
9921
- }
9922
- // Multiple JSON parsing strategies
9923
- const jsonParsingStrategies = [
9924
- // Strategy 1: Direct parsing
9925
- () => {
9926
- try {
9927
- return JSON.parse(cleanedContent);
9928
- }
9929
- catch {
9930
- return null;
9931
- }
9932
- },
9933
- // Strategy 2: Extract JSON from between first {} or []
9934
- () => {
9935
- const jsonMatch = cleanedContent.match(/[\{\[].*[\}\]]/s);
9936
- if (jsonMatch) {
9937
- try {
9938
- return JSON.parse(jsonMatch[0]);
9939
- }
9940
- catch {
9941
- return null;
9942
- }
9943
- }
9944
- return null;
9945
- },
9946
- // Strategy 3: Remove leading/trailing text and try parsing
9947
- () => {
9948
- const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, '');
9949
- try {
9950
- return JSON.parse(jsonMatch);
9951
- }
9952
- catch {
9953
- return null;
9954
- }
9955
- },
9956
- // Strategy 4: Use fixJsonWithAI
9957
- () => {
9958
- return fixJsonWithAI(cleanedContent);
9959
- },
9960
- // Strategy 5: Use fixBrokenJson
9961
- () => {
9962
- return fixBrokenJson(cleanedContent);
9963
- },
9964
- ];
9965
- // Try each parsing strategy
9966
- for (const strategy of jsonParsingStrategies) {
9967
- const result = await strategy();
9968
- if (result !== null) {
9969
- return result;
9970
- }
9971
- }
9972
- // If all strategies fail, throw an error
9973
- console.error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
9974
- throw new Error('Unable to parse JSON response');
9975
- }
9976
- else {
9977
- return content;
9978
- }
9979
- }
9980
-
9981
- // llm-openai.ts
9982
- const isSupportedModel = (model) => {
9983
- return [
9984
- 'gpt-4o-mini',
9985
- 'gpt-4o',
9986
- 'o1-mini',
9987
- 'o1',
9988
- 'o3-mini',
9989
- 'gpt-4.1',
9990
- 'gpt-4.1-mini',
9991
- 'gpt-4.1-nano',
9992
- 'gpt-5',
9993
- 'gpt-5-mini',
9994
- 'gpt-5-nano',
9995
- 'gpt-5.1',
9996
- 'gpt-5.2',
9997
- 'gpt-5.2-pro',
9998
- 'gpt-5.1-codex',
9999
- 'gpt-5.1-codex-max',
10000
- 'o4-mini',
10001
- 'o3',
10002
- ].includes(model);
10003
- };
10004
- /**
10005
- * Checks if the given model supports the temperature parameter. Reasoning models (o1*, o3*, o4*) do not support temperature.
10006
- * @param model The model to check.
10007
- * @returns True if the model supports the temperature parameter, false otherwise.
10008
- */
10009
- function supportsTemperature(model) {
10010
- // Reasoning models don't support temperature
10011
- // GPT-5 models also do not support temperature
10012
- const reasoningAndGPT5Models = [
10013
- 'o1',
10014
- 'o1-mini',
10015
- 'o3-mini',
10016
- 'o4-mini',
10017
- 'o3',
10018
- 'gpt-5',
10019
- 'gpt-5-mini',
10020
- 'gpt-5-nano',
10021
- 'gpt-5.1',
10022
- 'gpt-5.2',
10023
- 'gpt-5.2-pro',
10024
- 'gpt-5.1-codex',
10025
- 'gpt-5.1-codex-max',
10026
- ];
10027
- return !reasoningAndGPT5Models.includes(model);
10028
- }
10029
- /**
10030
- * Checks if the given model is a reasoning model. Reasoning models have different tool choice constraints.
10031
- * @param model The model to check.
10032
- * @returns True if the model is a reasoning model, false otherwise.
10033
- */
10034
- function isReasoningModel(model) {
10035
- const reasoningModels = ['o1', 'o1-mini', 'o3-mini', 'o4-mini', 'o3'];
10036
- return reasoningModels.includes(model);
10037
- }
10038
- /**
10039
- * Checks if the given model is a GPT-5 model. GPT-5 models don't support tool_choice other than 'auto'.
10040
- * @param model The model to check.
10041
- * @returns True if the model is a GPT-5 model, false otherwise.
10042
- */
10043
- function isGPT5Model(model) {
10044
- const gpt5Models = [
10045
- 'gpt-5',
10046
- 'gpt-5-mini',
10047
- 'gpt-5-nano',
10048
- 'gpt-5.1',
10049
- 'gpt-5.2',
10050
- 'gpt-5.2-pro',
10051
- 'gpt-5.1-codex',
10052
- 'gpt-5.1-codex-max',
10053
- ];
10054
- return gpt5Models.includes(model);
10055
- }
10056
- /**
10057
- * Makes a call to OpenAI's Responses API for more advanced use cases with built-in tools.
10058
- *
10059
- * This function provides access to the Responses API which supports:
10060
- * - Built-in tools (web search, file search, computer use, code interpreter, image generation)
10061
- * - Background processing
10062
- * - Conversation state management
10063
- * - Reasoning support for o-series models
10064
- *
10065
- * @example
10066
- * // Basic text response
10067
- * const response = await makeResponsesAPICall('What is the weather like?');
10068
- *
10069
- * @example
10070
- * // With web search tool
10071
- * const response = await makeResponsesAPICall('Latest news about AI', {
10072
- * tools: [{ type: 'web_search_preview' }]
10073
- * });
10074
- *
10075
- * @param input - The input content. Can be:
10076
- * - A string for simple text prompts
10077
- * - An array of input items for complex/multi-modal content
10078
- * @param options - Configuration options for the Responses API
10079
- * @returns Promise<LLMResponse<T>> - The response in the same format as makeLLMCall
10080
- * @throws Error if the API call fails
10081
- */
10082
- const makeResponsesAPICall = async (input, options = {}) => {
10083
- const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
10084
- const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
10085
- if (!apiKey) {
10086
- throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
10087
- }
10088
- const openai = new OpenAI({
10089
- apiKey: apiKey,
10090
- });
10091
- // Remove apiKey from options before creating request body
10092
- const { apiKey: _, model: __, ...cleanOptions } = options;
10093
- const requestBody = {
10094
- model: normalizedModel,
10095
- input,
10096
- ...cleanOptions,
10097
- };
10098
- // Make the API call to the Responses endpoint
10099
- const response = await openai.responses.create(requestBody);
10100
- // Extract tool calls from the output
10101
- const toolCalls = response.output
10102
- ?.filter((item) => item.type === 'function_call')
10103
- .map((toolCall) => ({
10104
- id: toolCall.call_id,
10105
- type: 'function',
10106
- function: {
10107
- name: toolCall.name,
10108
- arguments: toolCall.arguments,
10109
- },
10110
- }));
10111
- // Extract code interpreter outputs if present
10112
- const codeInterpreterCalls = response.output?.filter((item) => item.type === 'code_interpreter_call');
10113
- let codeInterpreterOutputs = undefined;
10114
- if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
10115
- // Each code_interpreter_call has an 'outputs' array property
10116
- codeInterpreterOutputs = codeInterpreterCalls.flatMap((ci) => {
10117
- // Return outputs if present, otherwise empty array
10118
- if (ci.outputs)
10119
- return ci.outputs;
10120
- return [];
10121
- });
10122
- }
10123
- // Handle tool calls differently
10124
- if (toolCalls && toolCalls.length > 0) {
10125
- return {
10126
- response: {
10127
- tool_calls: toolCalls.map((tc) => ({
10128
- id: tc.id,
10129
- name: tc.function.name,
10130
- arguments: JSON.parse(tc.function.arguments),
10131
- })),
10132
- },
10133
- usage: {
10134
- prompt_tokens: response.usage?.input_tokens || 0,
10135
- completion_tokens: response.usage?.output_tokens || 0,
10136
- reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
10137
- provider: 'openai',
10138
- model: normalizedModel,
10139
- cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0),
10140
- },
10141
- tool_calls: toolCalls,
10142
- ...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
10143
- };
10144
- }
10145
- // Extract text content from the response output
10146
- const textContent = response.output
10147
- ?.filter((item) => item.type === 'message')
10148
- .map((item) => item.content
10149
- .filter((content) => content.type === 'output_text')
10150
- .map((content) => content.text)
10151
- .join(''))
10152
- .join('') || '';
10153
- // Determine the format for parsing the response
10154
- let parsingFormat = 'text';
10155
- if (requestBody.text?.format?.type === 'json_object') {
10156
- parsingFormat = 'json';
10157
- }
10158
- // Handle regular responses
10159
- const parsedResponse = await parseResponse(textContent, parsingFormat);
10160
- if (parsedResponse === null) {
10161
- throw new Error('Failed to parse response from Responses API');
10162
- }
10163
- return {
10164
- response: parsedResponse,
10165
- usage: {
10166
- prompt_tokens: response.usage?.input_tokens || 0,
10167
- completion_tokens: response.usage?.output_tokens || 0,
10168
- reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
10169
- provider: 'openai',
10170
- model: normalizedModel,
10171
- cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0),
10172
- },
10173
- tool_calls: toolCalls,
10174
- ...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
10175
- };
10176
- };
10177
- /**
10178
- * Makes a call to the OpenAI Responses API for advanced use cases with built-in tools.
10179
- *
10180
- * @param input The text prompt to send to the model (e.g., "What's in this image?")
10181
- * @param options The options for the Responses API call, including optional image data and context.
10182
- * @return A promise that resolves to the response from the Responses API.
10183
- *
10184
- * @example
10185
- * // With conversation context
10186
- * const response = await makeLLMCall("What did I ask about earlier?", {
10187
- * context: [
10188
- * { role: 'user', content: 'What is the capital of France?' },
10189
- * { role: 'assistant', content: 'The capital of France is Paris.' }
10190
- * ]
10191
- * });
10192
- */
10193
- async function makeLLMCall(input, options = {}) {
10194
- const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', tools, useCodeInterpreter = false, useWebSearch = false, imageBase64, imageDetail = 'high', context, } = options;
10195
- // Validate model
10196
- const normalizedModel = normalizeModelName(model);
10197
- if (!isSupportedModel(normalizedModel)) {
10198
- throw new Error(`Unsupported model: ${normalizedModel}. Please use one of the supported models.`);
10199
- }
10200
- // Process input for conversation context and image analysis
10201
- let processedInput;
10202
- if (context && context.length > 0) {
10203
- // Build conversation array with context
10204
- const conversationMessages = [];
10205
- // Add context messages
10206
- for (const contextMsg of context) {
10207
- conversationMessages.push({
10208
- role: contextMsg.role,
10209
- content: contextMsg.content,
10210
- type: 'message',
10211
- });
10212
- }
10213
- // Add current input message
10214
- if (imageBase64) {
10215
- // Current message includes both text and image
10216
- conversationMessages.push({
10217
- role: 'user',
10218
- content: [
10219
- { type: 'input_text', text: input },
10220
- {
10221
- type: 'input_image',
10222
- detail: imageDetail,
10223
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
10224
- },
10225
- ],
10226
- type: 'message',
10227
- });
10228
- }
10229
- else {
10230
- // Current message is just text
10231
- conversationMessages.push({
10232
- role: 'user',
10233
- content: input,
10234
- type: 'message',
10235
- });
10236
- }
10237
- processedInput = conversationMessages;
10238
- }
10239
- else if (imageBase64) {
10240
- // No context, but has image - use the original image logic
10241
- processedInput = [
10242
- {
10243
- role: 'user',
10244
- content: [
10245
- { type: 'input_text', text: input },
10246
- {
10247
- type: 'input_image',
10248
- detail: imageDetail,
10249
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
10250
- },
10251
- ],
10252
- type: 'message',
10253
- },
10254
- ];
10255
- }
10256
- else {
10257
- // No context, no image - simple string input
10258
- processedInput = input;
10259
- }
10260
- // Build the options object for makeResponsesAPICall
10261
- let responsesOptions = {
10262
- apiKey,
10263
- model: normalizedModel,
10264
- parallel_tool_calls: false,
10265
- tools,
10266
- };
10267
- // Only include temperature if the model supports it
10268
- if (supportsTemperature(normalizedModel)) {
10269
- responsesOptions.temperature = 0.2;
10270
- }
10271
- // Configure response format
10272
- if (responseFormat === 'json') {
10273
- responsesOptions.text = { format: { type: 'json_object' } };
10274
- }
10275
- // Configure built-in tools
10276
- if (useCodeInterpreter) {
10277
- responsesOptions.tools = [{ type: 'code_interpreter', container: { type: 'auto' } }];
10278
- // For reasoning models, we can't force tool choice - they only support 'auto'
10279
- if (!isReasoningModel(normalizedModel)) {
10280
- responsesOptions.tool_choice = { type: 'code_interpreter' };
10281
- }
10282
- responsesOptions.include = ['code_interpreter_call.outputs'];
10283
- }
10284
- if (useWebSearch) {
10285
- responsesOptions.tools = [{ type: 'web_search_preview' }];
10286
- // For reasoning models and GPT-5 models, we can't force tool choice - they only support 'auto'
10287
- if (!isReasoningModel(normalizedModel) && !isGPT5Model(normalizedModel)) {
10288
- responsesOptions.tool_choice = { type: 'web_search_preview' };
10289
- }
10290
- }
10291
- return await makeResponsesAPICall(processedInput, responsesOptions);
10292
- }
10293
-
10294
- const DEFAULT_IMAGE_MODEL = 'gpt-image-1.5';
10295
- const resolveImageModel = (model) => model ?? DEFAULT_IMAGE_MODEL;
10296
- const MULTIMODAL_VISION_MODELS = new Set([
10297
- 'gpt-4o-mini',
10298
- 'gpt-4o',
10299
- 'gpt-5',
10300
- 'gpt-5-mini',
10301
- 'gpt-5-nano',
10302
- 'gpt-5.1',
10303
- 'gpt-5.2',
10304
- 'gpt-5.2-pro',
10305
- 'gpt-5.1-codex',
10306
- 'gpt-5.1-codex-max',
10307
- ]);
10308
- /**
10309
- * Makes a call to the OpenAI Images API to generate images based on a text prompt.
10310
- *
10311
- * This function provides access to OpenAI's image generation capabilities with support for:
10312
- * - Different output formats (JPEG, PNG, WebP)
10313
- * - Various image sizes and quality settings
10314
- * - Multiple image generation in a single call
10315
- * - Base64 encoded image data return
10316
- *
10317
- * @example
10318
- * // Basic image generation
10319
- * const response = await makeImagesCall('A beautiful sunset over mountains');
10320
- *
10321
- * @example
10322
- * // Custom options
10323
- * const response = await makeImagesCall('A birthday cake', {
10324
- * size: '1024x1024',
10325
- * outputFormat: 'png',
10326
- * quality: 'high',
10327
- * count: 2
10328
- * });
10329
- *
10330
- * @param prompt - The text prompt describing the image to generate
10331
- * @param options - Configuration options for image generation. Includes:
10332
- * - size: Image dimensions (uses OpenAI's size options)
10333
- * - outputFormat: Image format ('jpeg', 'png', 'webp') - defaults to 'webp'
10334
- * - compression: Compression level (number) - defaults to 50
10335
- * - quality: Quality setting (uses OpenAI's quality options) - defaults to 'high'
10336
- * - count: Number of images to generate - defaults to 1
10337
- * - background: Background setting for transparency support
10338
- * - moderation: Content moderation level
10339
- * - apiKey: OpenAI API key (optional, falls back to environment variable)
10340
- * @returns Promise<ImageResponseWithUsage> - The image generation response with cost information
10341
- * @throws Error if the API call fails or invalid parameters are provided
10342
- */
10343
- async function makeImagesCall(prompt, options = {}) {
10344
- const { model, size = 'auto', outputFormat = 'webp', compression = 50, quality = 'high', count = 1, background = 'auto', moderation = 'auto', apiKey, visionModel, } = options;
10345
- const imageModel = resolveImageModel(model);
10346
- const supportedVisionModel = visionModel && MULTIMODAL_VISION_MODELS.has(visionModel) ? visionModel : undefined;
10347
- if (visionModel && !supportedVisionModel) {
10348
- console.warn(`Vision model ${visionModel} is not recognized as a multimodal OpenAI model. Ignoring for image usage metadata.`);
10349
- }
10350
- // Get API key
10351
- const effectiveApiKey = apiKey || process.env.OPENAI_API_KEY;
10352
- if (!effectiveApiKey) {
10353
- throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
10354
- }
10355
- // Validate inputs
10356
- if (!prompt || typeof prompt !== 'string') {
10357
- throw new Error('Prompt must be a non-empty string');
10358
- }
10359
- if (count && (count < 1 || count > 10)) {
10360
- throw new Error('Count must be between 1 and 10');
10361
- }
10362
- if (compression && (compression < 0 || compression > 100)) {
10363
- throw new Error('Compression must be between 0 and 100');
10364
- }
10365
- const openai = new OpenAI({
10366
- apiKey: effectiveApiKey,
10367
- });
10368
- // Build the request parameters using OpenAI's type
10369
- const requestParams = {
10370
- model: imageModel,
10371
- prompt,
10372
- n: count || 1,
10373
- size: size || 'auto',
10374
- quality: quality || 'auto',
10375
- background: background || 'auto',
10376
- moderation: moderation || 'auto',
10377
- };
10378
- // Add output format if specified
10379
- if (outputFormat) {
10380
- requestParams.output_format = outputFormat;
10381
- }
10382
- // Add compression if specified
10383
- if (compression !== undefined) {
10384
- requestParams.output_compression = compression;
10385
- }
10386
- try {
10387
- // Make the API call
10388
- const response = await openai.images.generate(requestParams);
10389
- // Validate response
10390
- if (!response.data || response.data.length === 0) {
10391
- throw new Error('No images returned from OpenAI Images API');
10392
- }
10393
- // Calculate cost
10394
- const cost = calculateImageCost(imageModel, count || 1);
10395
- // Return the response with enhanced usage information
10396
- const enhancedResponse = {
10397
- ...response,
10398
- usage: {
10399
- // OpenAI Images response may not include usage details per image; preserve if present
10400
- ...(response.usage ?? {
10401
- input_tokens: 0,
10402
- input_tokens_details: { image_tokens: 0, text_tokens: 0 },
10403
- output_tokens: 0,
10404
- total_tokens: 0,
10405
- }),
10406
- provider: 'openai',
10407
- model: imageModel,
10408
- cost,
10409
- ...(supportedVisionModel ? { visionModel: supportedVisionModel } : {}),
10410
- },
10411
- };
10412
- return enhancedResponse;
10413
- }
10414
- catch (error) {
10415
- const message = error instanceof Error ? error.message : 'Unknown error';
10416
- throw new Error(`OpenAI Images API call failed: ${message}`);
10417
- }
10418
- }
10419
-
10420
- /**
10421
- * Default options for Deepseek API calls
10422
- */
10423
- const DEFAULT_DEEPSEEK_OPTIONS = {
10424
- defaultModel: 'deepseek-chat',
10425
- };
10426
- /**
10427
- * Checks if the given model is a supported Deepseek model
10428
- * @param model The model to check
10429
- * @returns True if the model is supported, false otherwise
10430
- */
10431
- const isSupportedDeepseekModel = (model) => {
10432
- return ['deepseek-chat', 'deepseek-reasoner'].includes(model);
10433
- };
10434
- /**
10435
- * Checks if the given Deepseek model supports JSON output format
10436
- * @param model The model to check
10437
- * @returns True if JSON output is supported, false otherwise
10438
- */
10439
- const supportsJsonOutput = (model) => {
10440
- return model === 'deepseek-chat';
10441
- };
10442
- /**
10443
- * Checks if the given Deepseek model supports tool calling
10444
- * @param model The model to check
10445
- * @returns True if tool calling is supported, false otherwise
10446
- */
10447
- const supportsToolCalling = (model) => {
10448
- return model === 'deepseek-chat';
10449
- };
10450
- /**
10451
- * Gets the appropriate response format option for Deepseek API
10452
- *
10453
- * @param responseFormat The desired response format
10454
- * @param model The model being used
10455
- * @returns Object representing the Deepseek response format option
10456
- */
10457
- function getDeepseekResponseFormatOption(responseFormat, model) {
10458
- // Check if the model supports JSON output
10459
- if (responseFormat !== 'text' && !supportsJsonOutput(model)) {
10460
- console.warn(`Model ${model} does not support JSON output. Using text format instead.`);
10461
- return { type: 'text' };
10462
- }
10463
- if (responseFormat === 'text') {
10464
- return { type: 'text' };
10465
- }
10466
- if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
10467
- return { type: 'json_object' };
10468
- }
10469
- return { type: 'text' };
10470
- }
10471
- /**
10472
- * Creates a completion using the Deepseek API
10473
- *
10474
- * @param content The content to pass to the API
10475
- * @param responseFormat The desired format of the response
10476
- * @param options Additional options for the API call
10477
- * @returns Promise resolving to the API response
10478
- */
10479
- async function createDeepseekCompletion(content, responseFormat, options = {}) {
10480
- // Extract the model or use default
10481
- const modelOption = options.model || DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
10482
- const normalizedModel = normalizeModelName(modelOption);
10483
- // Validate model
10484
- if (!isSupportedDeepseekModel(normalizedModel)) {
10485
- throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
10486
- }
10487
- // Check if tools are requested with a model that doesn't support them
10488
- if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
10489
- throw new Error(`Model ${normalizedModel} does not support tool calling.`);
10490
- }
10491
- const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY;
10492
- if (!apiKey) {
10493
- throw new Error('Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set');
10494
- }
10495
- // Initialize OpenAI client with Deepseek API URL
10496
- const openai = new OpenAI({
10497
- apiKey,
10498
- baseURL: 'https://api.deepseek.com',
10499
- });
10500
- const messages = [];
10501
- // Add system message with developer prompt if present
10502
- if (options.developerPrompt) {
10503
- messages.push({
10504
- role: 'system',
10505
- content: options.developerPrompt,
10506
- });
10507
- }
10508
- // Add context if present
10509
- if (options.context) {
10510
- messages.push(...options.context);
10511
- }
10512
- // Add user content
10513
- if (typeof content === 'string') {
10514
- messages.push({
10515
- role: 'user',
10516
- content,
10517
- });
10518
- }
10519
- else {
10520
- messages.push({
10521
- role: 'user',
10522
- content,
10523
- });
10524
- }
10525
- // If JSON response format, include a hint in the system prompt
10526
- if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema'))
10527
- && supportsJsonOutput(normalizedModel)) {
10528
- // If there's no system message yet, add one
10529
- if (!messages.some(m => m.role === 'system')) {
10530
- messages.unshift({
10531
- role: 'system',
10532
- content: 'Please respond in valid JSON format.',
10533
- });
10534
- }
10535
- else {
10536
- // Append to existing system message
10537
- const systemMsgIndex = messages.findIndex(m => m.role === 'system');
10538
- const systemMsg = messages[systemMsgIndex];
10539
- if (typeof systemMsg.content === 'string') {
10540
- messages[systemMsgIndex] = {
10541
- ...systemMsg,
10542
- content: `${systemMsg.content} Please respond in valid JSON format.`,
10543
- };
10544
- }
10545
- }
10546
- }
10547
- const queryOptions = {
10548
- model: normalizedModel,
10549
- messages,
10550
- response_format: getDeepseekResponseFormatOption(responseFormat, normalizedModel),
10551
- temperature: options.temperature,
10552
- top_p: options.top_p,
10553
- frequency_penalty: options.frequency_penalty,
10554
- presence_penalty: options.presence_penalty,
10555
- max_tokens: options.max_completion_tokens,
10556
- };
10557
- // Only add tools if the model supports them
10558
- if (options.tools && supportsToolCalling(normalizedModel)) {
10559
- queryOptions.tools = options.tools;
10560
- }
10561
- try {
10562
- const completion = await openai.chat.completions.create(queryOptions);
10563
- return {
10564
- id: completion.id,
10565
- content: completion.choices[0]?.message?.content || '',
10566
- tool_calls: completion.choices[0]?.message?.tool_calls,
10567
- usage: completion.usage || {
10568
- prompt_tokens: 0,
10569
- completion_tokens: 0,
10570
- total_tokens: 0,
10571
- },
10572
- system_fingerprint: completion.system_fingerprint,
10573
- provider: 'deepseek',
10574
- model: normalizedModel,
10575
- };
10576
- }
10577
- catch (error) {
10578
- console.error('Error calling Deepseek API:', error);
10579
- throw error;
10580
- }
10581
- }
10582
- /**
10583
- * Makes a call to the Deepseek AI API.
10584
- *
10585
- * @param content The content to pass to the LLM. Can be a string or an array of content parts.
10586
- * @param responseFormat The format of the response. Defaults to 'json'.
10587
- * @param options Configuration options including model ('deepseek-chat' or 'deepseek-reasoner'), tools, and apiKey.
10588
- * @return A promise that resolves to the response from the Deepseek API.
10589
- */
10590
- const makeDeepseekCall = async (content, responseFormat = 'json', options = {}) => {
10591
- // Set default model if not provided
10592
- const mergedOptions = {
10593
- ...options,
10594
- };
10595
- if (!mergedOptions.model) {
10596
- mergedOptions.model = DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
10597
- }
10598
- const modelName = normalizeModelName(mergedOptions.model);
10599
- // Check if the requested response format is compatible with the model
10600
- if (responseFormat !== 'text' && !supportsJsonOutput(modelName)) {
10601
- console.warn(`Model ${modelName} does not support JSON output. Will return error in the response.`);
10602
- return {
10603
- response: {
10604
- error: `Model ${modelName} does not support JSON output format.`
10605
- },
10606
- usage: {
10607
- prompt_tokens: 0,
10608
- completion_tokens: 0,
10609
- reasoning_tokens: 0,
10610
- provider: 'deepseek',
10611
- model: modelName,
10612
- cache_hit_tokens: 0,
10613
- cost: 0,
10614
- },
10615
- tool_calls: undefined,
10616
- };
10617
- }
10618
- // Check if tools are requested with a model that doesn't support them
10619
- if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
10620
- console.warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
10621
- return {
10622
- response: {
10623
- error: `Model ${modelName} does not support tool calling.`
10624
- },
10625
- usage: {
10626
- prompt_tokens: 0,
10627
- completion_tokens: 0,
10628
- reasoning_tokens: 0,
10629
- provider: 'deepseek',
10630
- model: modelName,
10631
- cache_hit_tokens: 0,
10632
- cost: 0,
10633
- },
10634
- tool_calls: undefined,
10635
- };
10636
- }
10637
- try {
10638
- const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
10639
- // Handle tool calls similarly to OpenAI
10640
- if (completion.tool_calls && completion.tool_calls.length > 0) {
10641
- const fnCalls = completion.tool_calls
10642
- .filter((tc) => tc.type === 'function')
10643
- .map((tc) => ({
10644
- id: tc.id,
10645
- name: tc.function.name,
10646
- arguments: JSON.parse(tc.function.arguments),
10647
- }));
10648
- return {
10649
- response: { tool_calls: fnCalls },
10650
- usage: {
10651
- prompt_tokens: completion.usage.prompt_tokens,
10652
- completion_tokens: completion.usage.completion_tokens,
10653
- reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
10654
- provider: 'deepseek',
10655
- model: completion.model,
10656
- cache_hit_tokens: 0, // Not provided directly in API response
10657
- cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, 0 // Cache hit tokens (not provided in the response)
10658
- ),
10659
- },
10660
- tool_calls: completion.tool_calls,
10661
- };
10662
- }
10663
- // Handle regular responses
10664
- const parsedResponse = await parseResponse(completion.content, responseFormat);
10665
- if (parsedResponse === null) {
10666
- throw new Error('Failed to parse Deepseek response');
10667
- }
10668
- return {
10669
- response: parsedResponse,
10670
- usage: {
10671
- prompt_tokens: completion.usage.prompt_tokens,
10672
- completion_tokens: completion.usage.completion_tokens,
10673
- reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
10674
- provider: 'deepseek',
10675
- model: completion.model,
10676
- cache_hit_tokens: 0, // Not provided directly in API response
10677
- cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, 0 // Cache hit tokens (not provided in the response)
10678
- ),
10679
- },
10680
- tool_calls: completion.tool_calls,
10681
- };
10682
- }
10683
- catch (error) {
10684
- // If there's an error due to incompatible features, return a structured error
10685
- console.error('Error in Deepseek API call:', error);
10686
- return {
10687
- response: {
10688
- error: error instanceof Error ? error.message : 'Unknown error'
10689
- },
10690
- usage: {
10691
- prompt_tokens: 0,
10692
- completion_tokens: 0,
10693
- reasoning_tokens: 0,
10694
- provider: 'deepseek',
10695
- model: modelName,
10696
- cache_hit_tokens: 0,
10697
- cost: 0,
10698
- },
10699
- tool_calls: undefined,
10700
- };
10701
- }
10702
- };
10703
-
10704
- // llm-openrouter.ts
10705
- // Map our ContextMessage to OpenAI chat message
10706
- function mapContextToMessages(context) {
10707
- return context.map((msg) => {
10708
- const role = msg.role === 'developer' ? 'system' : msg.role;
10709
- return { role, content: msg.content };
10710
- });
10711
- }
10712
- function toOpenRouterModel(model) {
10713
- if (model && model.includes('/'))
10714
- return model;
10715
- const base = normalizeModelName(model || DEFAULT_MODEL);
10716
- return `openai/${base}`;
10717
- }
10718
- // Normalize model name for pricing
10719
- function normalizeModelForPricing(model) {
10720
- if (!model)
10721
- return { provider: 'openai', coreModel: normalizeModelName(DEFAULT_MODEL) };
10722
- const [maybeProvider, maybeModel] = model.includes('/') ? model.split('/') : ['openai', model];
10723
- const provider = (maybeProvider === 'deepseek' ? 'deepseek' : 'openai');
10724
- const coreModel = normalizeModelName(maybeModel || model);
10725
- return { provider, coreModel };
10726
- }
10727
- /**
10728
- * Make a call through OpenRouter using the OpenAI Chat Completions-compatible API.
10729
- * Supports: JSON mode, model selection, message history, and tools.
10730
- */
10731
- async function makeOpenRouterCall(input, options = {}) {
10732
- const { apiKey = process.env.OPENROUTER_API_KEY, model, responseFormat = 'text', tools, toolChoice, context, developerPrompt, temperature = 0.2, max_tokens, top_p, frequency_penalty, presence_penalty, stop, seed, referer = process.env.OPENROUTER_SITE_URL, title = process.env.OPENROUTER_SITE_NAME, } = options;
10733
- if (!apiKey) {
10734
- throw new Error('OpenRouter API key is not provided and OPENROUTER_API_KEY is not set');
10735
- }
10736
- const client = new OpenAI({
10737
- apiKey,
10738
- baseURL: 'https://openrouter.ai/api/v1',
10739
- defaultHeaders: {
10740
- ...(referer ? { 'HTTP-Referer': referer } : {}),
10741
- ...(title ? { 'X-Title': title } : {}),
10742
- },
10743
- });
10744
- const messages = [];
10745
- if (developerPrompt && developerPrompt.trim()) {
10746
- messages.push({ role: 'system', content: developerPrompt });
10747
- }
10748
- if (context && context.length > 0) {
10749
- messages.push(...mapContextToMessages(context));
10750
- }
10751
- messages.push({ role: 'user', content: input });
10752
- // Configure response_format
10753
- let response_format;
10754
- let parsingFormat = 'text';
10755
- if (responseFormat === 'json') {
10756
- response_format = { type: 'json_object' };
10757
- parsingFormat = 'json';
10758
- }
10759
- else if (typeof responseFormat === 'object') {
10760
- response_format = { type: 'json_object' };
10761
- parsingFormat = responseFormat;
10762
- }
10763
- const modelId = toOpenRouterModel(model);
10764
- const completion = await client.chat.completions.create({
10765
- model: modelId,
10766
- messages,
10767
- response_format,
10768
- tools,
10769
- tool_choice: toolChoice,
10770
- temperature,
10771
- max_tokens,
10772
- top_p,
10773
- frequency_penalty,
10774
- presence_penalty,
10775
- stop,
10776
- seed,
10777
- });
10778
- const choice = completion.choices && completion.choices.length > 0 ? completion.choices[0] : undefined;
10779
- const message = (choice && 'message' in choice ? choice.message : undefined);
10780
- const { provider: pricingProvider, coreModel } = normalizeModelForPricing(modelId);
10781
- const promptTokens = completion.usage?.prompt_tokens ?? 0;
10782
- const completionTokens = completion.usage?.completion_tokens ?? 0;
10783
- const cost = calculateCost(pricingProvider, coreModel, promptTokens, completionTokens);
10784
- // Tool calls branch: return empty string response and expose tool_calls on LLMResponse
10785
- const hasToolCalls = Array.isArray(message?.tool_calls) && message.tool_calls.length > 0;
10786
- if (hasToolCalls) {
10787
- const usageModel = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
10788
- return {
10789
- response: '',
10790
- usage: {
10791
- prompt_tokens: promptTokens,
10792
- completion_tokens: completionTokens,
10793
- provider: 'openrouter',
10794
- model: usageModel,
10795
- cost,
10796
- },
10797
- tool_calls: message.tool_calls,
10798
- };
10799
- }
10800
- const rawText = typeof message?.content === 'string' ? message.content : '';
10801
- const parsed = await parseResponse(rawText, parsingFormat);
10802
- if (parsed === null) {
10803
- throw new Error('Failed to parse OpenRouter response');
10804
- }
10805
- // Ensure the model value conforms to LLMModel; otherwise fall back to DEFAULT_MODEL
10806
- const usageModel = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
10807
- return {
10808
- response: parsed,
10809
- usage: {
10810
- prompt_tokens: promptTokens,
10811
- completion_tokens: completionTokens,
10812
- provider: 'openrouter',
10813
- model: usageModel,
10814
- cost,
10815
- },
10816
- ...(hasToolCalls ? { tool_calls: message.tool_calls } : {}),
10817
- };
10818
- }
10819
-
10820
- /**
10821
- * A class to measure performance of code execution.
10822
- *
10823
- * This utility records elapsed time during execution and provides a detailed
10824
- * breakdown of elapsed time between checkpoints.
10825
- *
10826
- * Usage example:
10827
- * const timer = new PerformanceTimer();
10828
- * timer.checkpoint('Start Processing');
10829
- * // ... code for processing
10830
- * timer.checkpoint('After Task 1');
10831
- * // ... additional processing code
10832
- * timer.stop();
10833
- * console.log(timer.generateReport());
10834
- *
10835
- * Methods:
10836
- * - checkpoint(label: string): Record a timestamped checkpoint.
10837
- * - stop(): Stop the timer and compute the total elapsed time.
10838
- * - generateReport(): Return a performance report with total time and phases.
10839
- * - formatPerformanceReport(): Return a formatted string of the performance report.
10840
- * - getElapsedSeconds(): Get the current elapsed time in seconds.
10841
- */
10842
- class PerformanceTimer {
10843
- startTime;
10844
- checkpoints;
10845
- totalTime;
10846
- constructor() {
10847
- this.startTime = performance.now();
10848
- this.checkpoints = new Map();
10849
- this.totalTime = null;
10850
- }
10851
- /**
10852
- * Gets the current elapsed time in seconds.
10853
- *
10854
- * @returns The elapsed time in seconds with 3 decimal places of precision.
10855
- */
10856
- getElapsedSeconds() {
10857
- const elapsedMs = this.totalTime ?? (performance.now() - this.startTime);
10858
- return Number((elapsedMs / 1000).toFixed(3));
10859
- }
10860
- /**
10861
- * Records a checkpoint with the given label.
10862
- *
10863
- * @param label - A descriptive label for the checkpoint.
10864
- */
10865
- checkpoint(label) {
10866
- this.checkpoints.set(label, performance.now());
10867
- }
10868
- /**
10869
- * Stops the timer and sets the total elapsed time.
10870
- */
10871
- stop() {
10872
- this.totalTime = performance.now() - this.startTime;
10873
- }
10874
- /**
10875
- * Generates a performance report containing the total elapsed time and
10876
- * breakdown of phases between checkpoints.
10877
- *
10878
- * @returns A performance report object.
10879
- * @throws Error if the timer is not stopped before generating the report.
10880
- */
10881
- analyseReportData() {
10882
- if (this.totalTime === null) {
10883
- throw new Error('Timer must be stopped before generating report');
10884
- }
10885
- const report = {
10886
- totalTime: this.totalTime,
10887
- phases: []
10888
- };
10889
- let lastTime = this.startTime;
10890
- // Convert checkpoints to a sorted array by time
10891
- const sortedCheckpoints = Array.from(this.checkpoints.entries())
10892
- .sort((a, b) => a[1] - b[1]);
10893
- for (const [label, time] of sortedCheckpoints) {
10894
- const duration = time - lastTime;
10895
- if (duration < 0) {
10896
- throw new Error(`Negative duration detected for checkpoint: ${label}`);
10897
- }
10898
- report.phases.push({
10899
- label,
10900
- duration
10901
- });
10902
- lastTime = time;
10903
- }
10904
- // Add final phase from last checkpoint to stop
10905
- if (sortedCheckpoints.length > 0) {
10906
- const finalDuration = this.totalTime - lastTime;
10907
- report.phases.push({
10908
- label: 'Final Processing',
10909
- duration: Math.max(finalDuration, 0) // Prevent negative duration
10910
- });
10911
- }
10912
- return report;
10913
- }
10914
- /**
10915
- * Returns a formatted string of the performance report.
10916
- *
10917
- * @returns A string detailing the total execution time and phase breakdown.
10918
- */
10919
- generateReport() {
10920
- const report = this.analyseReportData();
10921
- // Format numbers with thousands separators and 2 decimal places
10922
- const formatNumber = (num) => num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
10923
- // Calculate column widths
10924
- const maxLabelLength = Math.max(...report.phases.map(p => p.label.length), 30);
10925
- const maxDurationLength = Math.max(...report.phases.map(p => formatNumber(p.duration).length), 10);
10926
- // Create table header
10927
- let output = `╔${'═'.repeat(maxLabelLength + 2)}╦${'═'.repeat(maxDurationLength + 2)}╗\n`;
10928
- output += `║ ${'Phase'.padEnd(maxLabelLength)} ║ ${'Time (ms)'.padEnd(maxDurationLength)} ║\n`;
10929
- output += `╠${'═'.repeat(maxLabelLength + 2)}╬${'═'.repeat(maxDurationLength + 2)}╣\n`;
10930
- // Add table rows
10931
- report.phases.forEach(phase => {
10932
- output += `║ ${phase.label.padEnd(maxLabelLength)} ║ ${formatNumber(phase.duration).padStart(maxDurationLength)} ║\n`;
10933
- });
10934
- // Add table footer with total
10935
- output += `╠${'═'.repeat(maxLabelLength + 2)}╬${'═'.repeat(maxDurationLength + 2)}╣\n`;
10936
- output += `║ ${'Total'.padEnd(maxLabelLength)} ║ ${formatNumber(report.totalTime).padStart(maxDurationLength)} ║\n`;
10937
- output += `╚${'═'.repeat(maxLabelLength + 2)}╩${'═'.repeat(maxDurationLength + 2)}╝\n`;
10938
- return output;
10939
- }
10940
- }
10941
-
10942
- /**
10943
- * Calculates Bollinger Bands for a given set of price data.
10944
- * Bollinger Bands consist of a middle band (SMA) and two outer bands
10945
- * that are standard deviations away from the middle band.
10946
- *
10947
- * @param priceData - An array of price data objects containing closing prices.
10948
- * @param params - An object containing optional parameters for the calculation.
10949
- * @param params.period - The number of periods to use for the SMA (default is 20).
10950
- * @param params.standardDeviations - The number of standard deviations for the outer bands (default is 2).
10951
- * @returns An array of BollingerBandsData objects containing the calculated bands.
10952
- */
10953
- function calculateBollingerBands(priceData, { period = 20, standardDeviations = 2 } = {}) {
10954
- if (priceData.length < period) {
10955
- logIfDebug(`Insufficient data for Bollinger Bands calculation: required periods: ${period}, but only received ${priceData.length} periods of data`);
10956
- return [];
10957
- }
10958
- const result = [];
10959
- for (let i = period - 1; i < priceData.length; i++) {
10960
- const periodSlice = priceData.slice(i - period + 1, i + 1);
10961
- const prices = periodSlice.map((d) => d.close);
10962
- // Calculate middle band (SMA)
10963
- const sum = prices.reduce((acc, price) => acc + price, 0);
10964
- const sma = sum / period;
10965
- // Calculate standard deviation
10966
- const squaredDifferences = prices.map((price) => Math.pow(price - sma, 2));
10967
- const variance = squaredDifferences.reduce((acc, val) => acc + val, 0) / period;
10968
- const standardDeviation = Math.sqrt(variance);
10969
- // Calculate bands
10970
- const upperBand = sma + standardDeviation * standardDeviations;
10971
- const lowerBand = sma - standardDeviation * standardDeviations;
10972
- result.push({
10973
- date: priceData[i].date,
10974
- middle: parseFloat(sma.toFixed(2)),
10975
- upper: parseFloat(upperBand.toFixed(2)),
10976
- lower: parseFloat(lowerBand.toFixed(2)),
10977
- close: priceData[i].close,
10978
- });
10979
- }
10980
- // logIfDebug(`Calculated Bollinger Bands for ${result.length} periods`);
10981
- return result;
10982
- }
10983
- /**
10984
- * Calculates the Exponential Moving Average (EMA) for a given set of price data.
10985
- * The EMA gives more weight to recent prices, making it more responsive to new information.
10986
- *
10987
- * @param priceData - An array of price data objects containing closing prices.
10988
- * @param params - An object containing optional parameters for the calculation.
10989
- * @param params.period - The number of periods to use for the EMA (default is 20).
10990
- * @param params.period2 - An optional second period for a second EMA (default is 9).
10991
- * @returns An array of EMAData objects containing the calculated EMA values.
10992
- */
10993
- function calculateEMA(priceData, { period = 20, period2 = 9 } = {}) {
10994
- if (priceData.length < period || (period2 && priceData.length < period2)) {
10995
- logIfDebug(`Insufficient data for EMA calculation: required periods: ${period}, ${period2}, but only received ${priceData.length} periods of data`);
10996
- return [];
10997
- }
10998
- const result = [];
10999
- const multiplier = 2 / (period + 1);
11000
- const multiplier2 = period2 ? 2 / (period2 + 1) : 0;
11001
- // Calculate initial SMA for first period
11002
- let sum = 0;
11003
- for (let i = 0; i < period; i++) {
11004
- sum += priceData[i].close;
11005
- }
11006
- let prevEMA = sum / period;
11007
- // Calculate initial SMA for second period if needed
11008
- let prevEMA2;
11009
- if (period2) {
11010
- sum = 0;
11011
- for (let i = 0; i < period2; i++) {
11012
- sum += priceData[i].close;
11013
- }
11014
- prevEMA2 = sum / period2;
11015
- }
11016
- // Add first EMA(s)
11017
- const firstEntry = {
11018
- date: priceData[Math.max(period, period2 || 0) - 1].date,
11019
- ema: parseFloat(prevEMA.toFixed(2)),
11020
- close: priceData[Math.max(period, period2 || 0) - 1].close,
11021
- };
11022
- if (period2) {
11023
- firstEntry.ema2 = parseFloat(prevEMA2.toFixed(2));
11024
- }
11025
- result.push(firstEntry);
11026
- // Calculate EMA for remaining periods
11027
- for (let i = Math.max(period, period2 || 0); i < priceData.length; i++) {
11028
- const currentClose = priceData[i].close;
11029
- const currentEMA = (currentClose - prevEMA) * multiplier + prevEMA;
11030
- prevEMA = currentEMA;
11031
- const entry = {
11032
- date: priceData[i].date,
11033
- ema: parseFloat(currentEMA.toFixed(2)),
11034
- close: currentClose,
11035
- };
11036
- if (period2) {
11037
- const currentEMA2 = (currentClose - prevEMA2) * multiplier2 + prevEMA2;
11038
- prevEMA2 = currentEMA2;
11039
- entry.ema2 = parseFloat(currentEMA2.toFixed(2));
11040
- }
11041
- result.push(entry);
11042
- }
11043
- // logIfDebug(`Calculated EMA for ${result.length} periods`);
11044
- return result;
11045
- }
11046
- /**
11047
- * Calculates Fibonacci retracement and extension levels based on price data.
11048
- * Fibonacci levels are used to identify potential support and resistance levels.
11049
- *
11050
- * @param priceData - An array of price data objects containing high and low prices.
11051
- * @param params - An object containing optional parameters for the calculation.
11052
- * @param params.lookbackPeriod - The number of periods to look back for swing high/low (default is 20).
11053
- * @param params.retracementLevels - An array of retracement levels to calculate (default is [0.236, 0.382, 0.5, 0.618, 0.786]).
11054
- * @param params.extensionLevels - An array of extension levels to calculate (default is [1.272, 1.618, 2.618]).
11055
- * @param params.reverseDirection - A boolean indicating if the trend is reversed (default is false).
11056
- * @returns An array of FibonacciData objects containing the calculated levels.
11057
- */
11058
- function calculateFibonacciLevels(priceData, { lookbackPeriod = 20, retracementLevels = [0.236, 0.382, 0.5, 0.618, 0.786], extensionLevels = [1.272, 1.618, 2.618], reverseDirection = false, } = {}) {
11059
- const result = [];
11060
- for (let i = 0; i < priceData.length; i++) {
11061
- const periodSlice = priceData.slice(Math.max(0, i - lookbackPeriod + 1), i + 1);
11062
- const swingHigh = Math.max(...periodSlice.map((d) => d.high));
11063
- const swingLow = Math.min(...periodSlice.map((d) => d.low));
11064
- const priceRange = swingHigh - swingLow;
11065
- const trend = reverseDirection ? 'downtrend' : 'uptrend';
11066
- let levels = [];
11067
- if (priceRange > 0) {
11068
- // Calculate retracement levels
11069
- retracementLevels.forEach((level) => {
11070
- const price = reverseDirection ? swingLow + priceRange * level : swingHigh - priceRange * level;
11071
- levels.push({
11072
- level,
11073
- price: parseFloat(price.toFixed(2)),
11074
- type: 'retracement',
11075
- });
11076
- });
11077
- // Calculate extension levels
11078
- extensionLevels.forEach((level) => {
11079
- const price = reverseDirection
11080
- ? swingHigh - priceRange * (level - 1) // For downtrend
11081
- : swingHigh + priceRange * (level - 1); // For uptrend
11082
- levels.push({
11083
- level,
11084
- price: parseFloat(price.toFixed(2)),
11085
- type: 'extension',
11086
- });
11087
- });
11088
- // Sort levels by price
11089
- levels.sort((a, b) => (reverseDirection ? b.price - a.price : a.price - b.price));
11090
- }
11091
- else {
11092
- logIfDebug(`Price range is zero on date ${priceData[i].date}; no levels calculated.`);
11093
- }
11094
- result.push({
11095
- date: priceData[i].date,
11096
- levels,
11097
- swingHigh,
11098
- swingLow,
11099
- trend,
11100
- close: priceData[i].close,
11101
- });
11102
- }
11103
- // logIfDebug(`Calculated Fibonacci levels for ${result.length} periods`);
11104
- return result;
11105
- }
11106
- /**
11107
- * Calculates the Moving Average Convergence Divergence (MACD) for a given set of price data.
11108
- * MACD is a trend-following momentum indicator that shows the relationship between two EMAs.
11109
- *
11110
- * @param priceData - An array of price data objects containing closing prices.
11111
- * @param params - An object containing optional parameters for the calculation.
11112
- * @param params.shortPeriod - The short EMA period (default is 12).
11113
- * @param params.longPeriod - The long EMA period (default is 26).
11114
- * @param params.signalPeriod - The signal line period (default is 9).
11115
- * @returns An array of MACDData objects containing the calculated MACD values.
11116
- */
11117
- function calculateMACD(priceData, { shortPeriod = 12, longPeriod = 26, signalPeriod = 9 } = {}) {
11118
- if (priceData.length < longPeriod + signalPeriod) {
11119
- logIfDebug(`Insufficient data for MACD calculation: required periods: ${longPeriod + signalPeriod}, but only received ${priceData.length} periods of data`);
11120
- return [];
11121
- }
11122
- const emaShort = calculateEMA(priceData, { period: shortPeriod });
11123
- const emaLong = calculateEMA(priceData, { period: longPeriod });
11124
- // Align EMAs by trimming the beginning of emaShort to match emaLong length
11125
- if (emaShort.length < emaLong.length) {
11126
- logIfDebug('Short EMA length is less than Long EMA length for MACD calculation');
11127
- return [];
11128
- }
11129
- const emaShortAligned = emaShort.slice(emaShort.length - emaLong.length);
11130
- const macdLine = emaShortAligned.map((short, i) => short.ema - emaLong[i].ema);
11131
- const result = [];
11132
- if (macdLine.length < signalPeriod) {
11133
- logIfDebug(`Insufficient MACD data for Signal Line calculation: required periods: ${signalPeriod}, but only received ${macdLine.length} periods of data`);
11134
- return [];
11135
- }
11136
- const signalMultiplier = 2 / (signalPeriod + 1);
11137
- let signalEMA = macdLine.slice(0, signalPeriod).reduce((sum, val) => sum + val, 0) / signalPeriod;
11138
- for (let i = signalPeriod; i < macdLine.length; i++) {
11139
- const macdValue = macdLine[i];
11140
- signalEMA = (macdValue - signalEMA) * signalMultiplier + signalEMA;
11141
- const hist = macdValue - signalEMA;
11142
- result.push({
11143
- date: emaLong[i].date, // Use emaLong's date for alignment
11144
- macd: parseFloat(macdValue.toFixed(2)),
11145
- signal: parseFloat(signalEMA.toFixed(2)),
11146
- histogram: parseFloat(hist.toFixed(2)),
11147
- close: emaLong[i].close,
11148
- });
7637
+ const options = { ...inputOptions };
7638
+ const { method, path, query, defaultBaseURL } = options;
7639
+ const url = this.buildURL(path, query, defaultBaseURL);
7640
+ if ('timeout' in options)
7641
+ validatePositiveInteger('timeout', options.timeout);
7642
+ options.timeout = options.timeout ?? this.timeout;
7643
+ const { bodyHeaders, body } = this.buildBody({ options });
7644
+ const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
7645
+ const req = {
7646
+ method,
7647
+ headers: reqHeaders,
7648
+ ...(options.signal && { signal: options.signal }),
7649
+ ...(globalThis.ReadableStream &&
7650
+ body instanceof globalThis.ReadableStream && { duplex: 'half' }),
7651
+ ...(body && { body }),
7652
+ ...(this.fetchOptions ?? {}),
7653
+ ...(options.fetchOptions ?? {}),
7654
+ };
7655
+ return { req, url, timeout: options.timeout };
11149
7656
  }
11150
- // logIfDebug(`Calculated MACD for ${result.length} periods`);
11151
- return result;
11152
- }
11153
- /**
11154
- * Calculates the Relative Strength Index (RSI) for a given set of price data.
11155
- * RSI is a momentum oscillator that measures the speed and change of price movements.
11156
- *
11157
- * @param priceData - An array of price data objects containing closing prices.
11158
- * @param params - An object containing optional parameters for the calculation.
11159
- * @param params.period - The number of periods to use for the RSI (default is 14).
11160
- * @returns An array of RSIData objects containing the calculated RSI values.
11161
- */
11162
- function calculateRSI(priceData, { period = 14 } = {}) {
11163
- if (priceData.length < period + 1) {
11164
- logIfDebug(`Insufficient data for RSI calculation: required periods: ${period + 1}, but only received ${priceData.length} periods of data`);
11165
- return [];
11166
- }
11167
- const result = [];
11168
- let avgGain = 0;
11169
- let avgLoss = 0;
11170
- // Calculate first average gain and loss
11171
- for (let i = 1; i <= period; i++) {
11172
- const change = priceData[i].close - priceData[i - 1].close;
11173
- if (change >= 0) {
11174
- avgGain += change;
7657
+ async buildHeaders({ options, method, bodyHeaders, retryCount, }) {
7658
+ let idempotencyHeaders = {};
7659
+ if (this.idempotencyHeader && method !== 'get') {
7660
+ if (!options.idempotencyKey)
7661
+ options.idempotencyKey = this.defaultIdempotencyKey();
7662
+ idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
11175
7663
  }
11176
- else {
11177
- avgLoss += Math.abs(change);
11178
- }
11179
- }
11180
- avgGain = avgGain / period;
11181
- avgLoss = avgLoss / period;
11182
- // Calculate RSI for the first period
11183
- let rs = avgGain / avgLoss;
11184
- let rsi = 100 - 100 / (1 + rs);
11185
- result.push({
11186
- date: priceData[period].date,
11187
- rsi: parseFloat(rsi.toFixed(2)),
11188
- close: priceData[period].close,
11189
- });
11190
- // Calculate subsequent periods using smoothed averages
11191
- for (let i = period + 1; i < priceData.length; i++) {
11192
- const change = priceData[i].close - priceData[i - 1].close;
11193
- const gain = change >= 0 ? change : 0;
11194
- const loss = change < 0 ? Math.abs(change) : 0;
11195
- // Use smoothed averages
11196
- avgGain = (avgGain * (period - 1) + gain) / period;
11197
- avgLoss = (avgLoss * (period - 1) + loss) / period;
11198
- rs = avgGain / avgLoss;
11199
- rsi = 100 - 100 / (1 + rs);
11200
- result.push({
11201
- date: priceData[i].date,
11202
- rsi: parseFloat(rsi.toFixed(2)),
11203
- close: priceData[i].close,
11204
- });
7664
+ const headers = buildHeaders([
7665
+ idempotencyHeaders,
7666
+ {
7667
+ Accept: 'application/json',
7668
+ 'User-Agent': this.getUserAgent(),
7669
+ 'X-Stainless-Retry-Count': String(retryCount),
7670
+ ...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
7671
+ ...getPlatformHeaders(),
7672
+ 'OpenAI-Organization': this.organization,
7673
+ 'OpenAI-Project': this.project,
7674
+ },
7675
+ await this.authHeaders(options),
7676
+ this._options.defaultHeaders,
7677
+ bodyHeaders,
7678
+ options.headers,
7679
+ ]);
7680
+ this.validateHeaders(headers);
7681
+ return headers.values;
11205
7682
  }
11206
- // logIfDebug(`Calculated RSI for ${result.length} periods`);
11207
- return result;
11208
- }
11209
- /**
11210
- * Calculates the Stochastic Oscillator for a given set of price data.
11211
- * The Stochastic Oscillator compares a particular closing price of a security to a range of its prices over a certain period of time.
11212
- *
11213
- * @param priceData - An array of price data objects containing high, low, and closing prices.
11214
- * @param params - An object containing optional parameters for the calculation.
11215
- * @param params.lookbackPeriod - The number of periods to look back for the calculation of %K (default is 5).
11216
- * @param params.signalPeriod - The number of periods for the %D signal line (default is 3).
11217
- * @param params.smoothingFactor - The smoothing factor for %K (default is 3).
11218
- * @returns An array of StochData objects containing the calculated %K and %D values.
11219
- */
11220
- function calculateStochasticOscillator(priceData, { lookbackPeriod = 5, signalPeriod = 3, smoothingFactor = 3 } = {}) {
11221
- if (priceData.length < lookbackPeriod) {
11222
- logIfDebug(`Insufficient data for Stochastic Oscillator calculation: required periods: ${lookbackPeriod}, but only received ${priceData.length} periods of data`);
11223
- return [];
11224
- }
11225
- const kValues = [];
11226
- const result = [];
11227
- let kSum = 0;
11228
- let dSum = 0;
11229
- for (let i = lookbackPeriod - 1; i < priceData.length; i++) {
11230
- const periodSlice = priceData.slice(i - lookbackPeriod + 1, i + 1);
11231
- const currentClose = periodSlice[periodSlice.length - 1].close;
11232
- const highPrices = periodSlice.map((d) => d.high);
11233
- const lowPrices = periodSlice.map((d) => d.low);
11234
- const highestHigh = Math.max(...highPrices);
11235
- const lowestLow = Math.min(...lowPrices);
11236
- const k = highestHigh === lowestLow ? 0 : ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100;
11237
- kValues.push(k);
11238
- kSum += k;
11239
- if (kValues.length > smoothingFactor)
11240
- kSum -= kValues[kValues.length - smoothingFactor - 1];
11241
- const smoothedK = kSum / Math.min(kValues.length, smoothingFactor);
11242
- dSum += smoothedK;
11243
- if (kValues.length > smoothingFactor + signalPeriod - 1)
11244
- dSum -= kValues[kValues.length - smoothingFactor - signalPeriod];
11245
- const smoothedD = dSum / Math.min(kValues.length - smoothingFactor + 1, signalPeriod);
11246
- if (kValues.length >= smoothingFactor + signalPeriod - 1) {
11247
- result.push({
11248
- date: priceData[i].date,
11249
- slowK: parseFloat(smoothedK.toFixed(2)),
11250
- slowD: parseFloat(smoothedD.toFixed(2)),
11251
- close: currentClose,
11252
- });
7683
+ buildBody({ options: { body, headers: rawHeaders } }) {
7684
+ if (!body) {
7685
+ return { bodyHeaders: undefined, body: undefined };
11253
7686
  }
11254
- }
11255
- // logIfDebug(`Calculated Stochastic Oscillator for ${result.length} periods`);
11256
- return result;
11257
- }
11258
- /**
11259
- * Calculates support and resistance levels based on price data.
11260
- * Support and resistance levels are price levels at which a stock tends to stop and reverse.
11261
- *
11262
- * @param priceData - An array of price data objects containing high, low, and closing prices.
11263
- * @param params - An object containing optional parameters for the calculation.
11264
- * @param params.maxLevels - The maximum number of support/resistance levels to return (default is 5).
11265
- * @param params.lookbackPeriod - The number of periods to look back for pivot points (default is 10).
11266
- * @returns An array of SupportResistanceData objects containing the calculated levels.
11267
- */
11268
- function calculateSupportAndResistance(priceData, { maxLevels = 5, lookbackPeriod = 10 } = {}) {
11269
- const result = [];
11270
- for (let i = 0; i < priceData.length; i++) {
11271
- const startIdx = Math.max(0, i - lookbackPeriod);
11272
- const analysisWindow = priceData.slice(startIdx, i + 1);
11273
- const pivotPoints = [];
11274
- // **Compute Volatility Metrics**
11275
- const priceChanges = analysisWindow.slice(1).map((bar, idx) => Math.abs(bar.close - analysisWindow[idx].close));
11276
- const avgPriceChange = priceChanges.reduce((sum, change) => sum + change, 0) / priceChanges.length;
11277
- const volatility = avgPriceChange / analysisWindow[0].close; // Relative volatility
11278
- // **Adjust Sensitivity and minGapBetweenLevels Dynamically**
11279
- const sensitivity = volatility * 2; // Adjust the multiplier as needed
11280
- const minGapBetweenLevels = volatility * 100; // Convert to percentage
11281
- // Analyze each point in window for pivot status
11282
- for (let j = 1; j < analysisWindow.length - 1; j++) {
11283
- const curr = analysisWindow[j];
11284
- const prevBar = analysisWindow[j - 1];
11285
- const nextBar = analysisWindow[j + 1];
11286
- // Check for high pivot
11287
- if (curr.high > prevBar.high && curr.high > nextBar.high) {
11288
- const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.high) / curr.high < sensitivity);
11289
- if (existingPivot) {
11290
- existingPivot.count++;
11291
- existingPivot.volume += curr.vol; // **Include Volume**
11292
- }
11293
- else {
11294
- pivotPoints.push({ price: curr.high, count: 1, volume: curr.vol });
11295
- }
11296
- }
11297
- // Check for low pivot
11298
- if (curr.low < prevBar.low && curr.low < nextBar.low) {
11299
- const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.low) / curr.low < sensitivity);
11300
- if (existingPivot) {
11301
- existingPivot.count++;
11302
- existingPivot.volume += curr.vol; // **Include Volume**
11303
- }
11304
- else {
11305
- pivotPoints.push({ price: curr.low, count: 1, volume: curr.vol });
11306
- }
11307
- }
7687
+ const headers = buildHeaders([rawHeaders]);
7688
+ if (
7689
+ // Pass raw type verbatim
7690
+ ArrayBuffer.isView(body) ||
7691
+ body instanceof ArrayBuffer ||
7692
+ body instanceof DataView ||
7693
+ (typeof body === 'string' &&
7694
+ // Preserve legacy string encoding behavior for now
7695
+ headers.values.has('content-type')) ||
7696
+ // `Blob` is superset of `File`
7697
+ (globalThis.Blob && body instanceof globalThis.Blob) ||
7698
+ // `FormData` -> `multipart/form-data`
7699
+ body instanceof FormData ||
7700
+ // `URLSearchParams` -> `application/x-www-form-urlencoded`
7701
+ body instanceof URLSearchParams ||
7702
+ // Send chunked stream (each chunk has own `length`)
7703
+ (globalThis.ReadableStream && body instanceof globalThis.ReadableStream)) {
7704
+ return { bodyHeaders: undefined, body: body };
11308
7705
  }
11309
- // Group nearby levels
11310
- const currentPrice = priceData[i].close;
11311
- const levels = [];
11312
- // Sort pivots by price
11313
- pivotPoints.sort((a, b) => a.price - b.price);
11314
- // Group close pivots
11315
- let currentGroup = [];
11316
- for (let j = 0; j < pivotPoints.length; j++) {
11317
- if (currentGroup.length === 0) {
11318
- currentGroup.push(pivotPoints[j]);
11319
- }
11320
- else {
11321
- const lastPrice = currentGroup[currentGroup.length - 1].price;
11322
- if ((Math.abs(pivotPoints[j].price - lastPrice) / lastPrice) * 100 <= minGapBetweenLevels) {
11323
- currentGroup.push(pivotPoints[j]);
11324
- }
11325
- else {
11326
- // Process current group
11327
- if (currentGroup.length > 0) {
11328
- const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
11329
- const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
11330
- const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
11331
- levels.push({
11332
- price: parseFloat(avgPrice.toFixed(2)),
11333
- strength: parseFloat(totalStrength.toFixed(2)),
11334
- type: avgPrice > currentPrice ? 'resistance' : 'support',
11335
- });
11336
- }
11337
- currentGroup = [pivotPoints[j]];
11338
- }
11339
- }
7706
+ else if (typeof body === 'object' &&
7707
+ (Symbol.asyncIterator in body ||
7708
+ (Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))) {
7709
+ return { bodyHeaders: undefined, body: ReadableStreamFrom(body) };
11340
7710
  }
11341
- // Process final group
11342
- if (currentGroup.length > 0) {
11343
- const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
11344
- const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
11345
- const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
11346
- levels.push({
11347
- price: parseFloat(avgPrice.toFixed(2)),
11348
- strength: parseFloat(totalStrength.toFixed(2)),
11349
- type: avgPrice > currentPrice ? 'resistance' : 'support',
11350
- });
7711
+ else {
7712
+ return __classPrivateFieldGet(this, _OpenAI_encoder, "f").call(this, { body, headers });
11351
7713
  }
11352
- // Sort by strength and limit
11353
- const finalLevels = levels.sort((a, b) => b.strength - a.strength).slice(0, maxLevels);
11354
- result.push({
11355
- date: priceData[i].date,
11356
- levels: finalLevels,
11357
- close: currentPrice,
11358
- });
11359
7714
  }
11360
- logIfDebug(`Found ${result.reduce((sum, r) => sum + r.levels.length, 0)} support/resistance levels across ${result.length} periods`);
11361
- return result;
11362
7715
  }
7716
+ _a = OpenAI, _OpenAI_encoder = new WeakMap(), _OpenAI_instances = new WeakSet(), _OpenAI_baseURLOverridden = function _OpenAI_baseURLOverridden() {
7717
+ return this.baseURL !== 'https://api.openai.com/v1';
7718
+ };
7719
+ OpenAI.OpenAI = _a;
7720
+ OpenAI.DEFAULT_TIMEOUT = 600000; // 10 minutes
7721
+ OpenAI.OpenAIError = OpenAIError;
7722
+ OpenAI.APIError = APIError;
7723
+ OpenAI.APIConnectionError = APIConnectionError;
7724
+ OpenAI.APIConnectionTimeoutError = APIConnectionTimeoutError;
7725
+ OpenAI.APIUserAbortError = APIUserAbortError;
7726
+ OpenAI.NotFoundError = NotFoundError;
7727
+ OpenAI.ConflictError = ConflictError;
7728
+ OpenAI.RateLimitError = RateLimitError;
7729
+ OpenAI.BadRequestError = BadRequestError;
7730
+ OpenAI.AuthenticationError = AuthenticationError;
7731
+ OpenAI.InternalServerError = InternalServerError;
7732
+ OpenAI.PermissionDeniedError = PermissionDeniedError;
7733
+ OpenAI.UnprocessableEntityError = UnprocessableEntityError;
7734
+ OpenAI.InvalidWebhookSignatureError = InvalidWebhookSignatureError;
7735
+ OpenAI.toFile = toFile;
7736
+ OpenAI.Completions = Completions;
7737
+ OpenAI.Chat = Chat;
7738
+ OpenAI.Embeddings = Embeddings;
7739
+ OpenAI.Files = Files$1;
7740
+ OpenAI.Images = Images;
7741
+ OpenAI.Audio = Audio;
7742
+ OpenAI.Moderations = Moderations;
7743
+ OpenAI.Models = Models;
7744
+ OpenAI.FineTuning = FineTuning;
7745
+ OpenAI.Graders = Graders;
7746
+ OpenAI.VectorStores = VectorStores;
7747
+ OpenAI.Webhooks = Webhooks;
7748
+ OpenAI.Beta = Beta;
7749
+ OpenAI.Batches = Batches;
7750
+ OpenAI.Uploads = Uploads;
7751
+ OpenAI.Responses = Responses;
7752
+ OpenAI.Realtime = Realtime;
7753
+ OpenAI.Conversations = Conversations;
7754
+ OpenAI.Evals = Evals;
7755
+ OpenAI.Containers = Containers;
7756
+ OpenAI.Videos = Videos;
11363
7757
 
11364
7758
  function getDefaultExportFromCjs (x) {
11365
7759
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -11381,6 +7775,7 @@ function requireConstants () {
11381
7775
 
11382
7776
  constants = {
11383
7777
  BINARY_TYPES,
7778
+ CLOSE_TIMEOUT: 30000,
11384
7779
  EMPTY_BUFFER: Buffer.alloc(0),
11385
7780
  GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
11386
7781
  hasBlob,
@@ -14151,6 +10546,7 @@ function requireWebsocket () {
14151
10546
 
14152
10547
  const {
14153
10548
  BINARY_TYPES,
10549
+ CLOSE_TIMEOUT,
14154
10550
  EMPTY_BUFFER,
14155
10551
  GUID,
14156
10552
  kForOnEventAttribute,
@@ -14165,7 +10561,6 @@ function requireWebsocket () {
14165
10561
  const { format, parse } = requireExtension();
14166
10562
  const { toBuffer } = requireBufferUtil();
14167
10563
 
14168
- const closeTimeout = 30 * 1000;
14169
10564
  const kAborted = Symbol('kAborted');
14170
10565
  const protocolVersions = [8, 13];
14171
10566
  const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
@@ -14221,6 +10616,7 @@ function requireWebsocket () {
14221
10616
  initAsClient(this, address, protocols, options);
14222
10617
  } else {
14223
10618
  this._autoPong = options.autoPong;
10619
+ this._closeTimeout = options.closeTimeout;
14224
10620
  this._isServer = true;
14225
10621
  }
14226
10622
  }
@@ -14762,6 +11158,8 @@ function requireWebsocket () {
14762
11158
  * times in the same tick
14763
11159
  * @param {Boolean} [options.autoPong=true] Specifies whether or not to
14764
11160
  * automatically send a pong in response to a ping
11161
+ * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to wait
11162
+ * for the closing handshake to finish after `websocket.close()` is called
14765
11163
  * @param {Function} [options.finishRequest] A function which can be used to
14766
11164
  * customize the headers of each http request before it is sent
14767
11165
  * @param {Boolean} [options.followRedirects=false] Whether or not to follow
@@ -14788,6 +11186,7 @@ function requireWebsocket () {
14788
11186
  const opts = {
14789
11187
  allowSynchronousEvents: true,
14790
11188
  autoPong: true,
11189
+ closeTimeout: CLOSE_TIMEOUT,
14791
11190
  protocolVersion: protocolVersions[1],
14792
11191
  maxPayload: 100 * 1024 * 1024,
14793
11192
  skipUTF8Validation: false,
@@ -14806,6 +11205,7 @@ function requireWebsocket () {
14806
11205
  };
14807
11206
 
14808
11207
  websocket._autoPong = opts.autoPong;
11208
+ websocket._closeTimeout = opts.closeTimeout;
14809
11209
 
14810
11210
  if (!protocolVersions.includes(opts.protocolVersion)) {
14811
11211
  throw new RangeError(
@@ -15423,7 +11823,7 @@ function requireWebsocket () {
15423
11823
  function setCloseTimer(websocket) {
15424
11824
  websocket._closeTimer = setTimeout(
15425
11825
  websocket._socket.destroy.bind(websocket._socket),
15426
- closeTimeout
11826
+ websocket._closeTimeout
15427
11827
  );
15428
11828
  }
15429
11829
 
@@ -15441,23 +11841,23 @@ function requireWebsocket () {
15441
11841
 
15442
11842
  websocket._readyState = WebSocket.CLOSING;
15443
11843
 
15444
- let chunk;
15445
-
15446
11844
  //
15447
11845
  // The close frame might not have been received or the `'end'` event emitted,
15448
11846
  // for example, if the socket was destroyed due to an error. Ensure that the
15449
11847
  // `receiver` stream is closed after writing any remaining buffered data to
15450
11848
  // it. If the readable side of the socket is in flowing mode then there is no
15451
- // buffered data as everything has been already written and `readable.read()`
15452
- // will return `null`. If instead, the socket is paused, any possible buffered
15453
- // data will be read as a single chunk.
11849
+ // buffered data as everything has been already written. If instead, the
11850
+ // socket is paused, any possible buffered data will be read as a single
11851
+ // chunk.
15454
11852
  //
15455
11853
  if (
15456
11854
  !this._readableState.endEmitted &&
15457
11855
  !websocket._closeFrameReceived &&
15458
11856
  !websocket._receiver._writableState.errorEmitted &&
15459
- (chunk = websocket._socket.read()) !== null
11857
+ this._readableState.length !== 0
15460
11858
  ) {
11859
+ const chunk = this.read(this._readableState.length);
11860
+
15461
11861
  websocket._receiver.write(chunk);
15462
11862
  }
15463
11863
 
@@ -15789,7 +12189,7 @@ function requireWebsocketServer () {
15789
12189
  const PerMessageDeflate = requirePermessageDeflate();
15790
12190
  const subprotocol = requireSubprotocol();
15791
12191
  const WebSocket = requireWebsocket();
15792
- const { GUID, kWebSocket } = requireConstants();
12192
+ const { CLOSE_TIMEOUT, GUID, kWebSocket } = requireConstants();
15793
12193
 
15794
12194
  const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
15795
12195
 
@@ -15816,6 +12216,9 @@ function requireWebsocketServer () {
15816
12216
  * pending connections
15817
12217
  * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
15818
12218
  * track clients
12219
+ * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
12220
+ * wait for the closing handshake to finish after `websocket.close()` is
12221
+ * called
15819
12222
  * @param {Function} [options.handleProtocols] A hook to handle protocols
15820
12223
  * @param {String} [options.host] The hostname where to bind the server
15821
12224
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
@@ -15845,6 +12248,7 @@ function requireWebsocketServer () {
15845
12248
  perMessageDeflate: false,
15846
12249
  handleProtocols: null,
15847
12250
  clientTracking: true,
12251
+ closeTimeout: CLOSE_TIMEOUT,
15848
12252
  verifyClient: null,
15849
12253
  noServer: false,
15850
12254
  backlog: null, // use default (511 as implemented in net.js)
@@ -18129,7 +14533,7 @@ class AlpacaTradingAPI {
18129
14533
  }
18130
14534
  handleTradeUpdate(data) {
18131
14535
  if (this.tradeUpdateCallback) {
18132
- this.log(`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares, type ${data.order.type}`, {
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}`, {
18133
14537
  symbol: data.order.symbol,
18134
14538
  type: 'debug',
18135
14539
  });
@@ -18247,7 +14651,9 @@ class AlpacaTradingAPI {
18247
14651
  try {
18248
14652
  this.ws.terminate();
18249
14653
  }
18250
- catch { /* no-op */ }
14654
+ catch {
14655
+ /* no-op */
14656
+ }
18251
14657
  }
18252
14658
  this.ws = null;
18253
14659
  }
@@ -18464,6 +14870,7 @@ class AlpacaTradingAPI {
18464
14870
  * @param qty (number) - the quantity of the order
18465
14871
  * @param side (string) - the side of the order
18466
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
18467
14874
  */
18468
14875
  async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
18469
14876
  this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
@@ -18489,6 +14896,86 @@ class AlpacaTradingAPI {
18489
14896
  throw error;
18490
14897
  }
18491
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
+ }
18492
14979
  /**
18493
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.
18494
14981
  * @param symbol (string) - the symbol of the order
@@ -18806,7 +15293,7 @@ class AlpacaTradingAPI {
18806
15293
  const hourlyParams = { timeframe: '1Min', period: '1D' };
18807
15294
  const [dailyHistory, hourlyHistory] = await Promise.all([
18808
15295
  this.getPortfolioHistory(dailyParams),
18809
- this.getPortfolioHistory(hourlyParams)
15296
+ this.getPortfolioHistory(hourlyParams),
18810
15297
  ]);
18811
15298
  // If no hourly history, return daily as-is
18812
15299
  if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
@@ -19473,250 +15960,73 @@ class AlpacaTradingAPI {
19473
15960
  }
19474
15961
  }
19475
15962
 
19476
- const disco = {
19477
- types: Types,
19478
- alpaca: {
19479
- TradingAPI: AlpacaTradingAPI,
19480
- MarketDataAPI: AlpacaMarketDataAPI,
19481
- },
19482
- format: {
19483
- capFirstLetter: capFirstLetter,
19484
- currency: formatCurrency,
19485
- number: formatNumber,
19486
- pct: formatPercentage,
19487
- dateTimeForGS: dateTimeForGS,
19488
- },
19489
- indices: {
19490
- fetchAggregates: fetchIndicesAggregates,
19491
- fetchPreviousClose: fetchIndicesPreviousClose,
19492
- fetchDailyOpenClose: fetchIndicesDailyOpenClose,
19493
- fetchSnapshot: fetchIndicesSnapshot,
19494
- fetchUniversalSnapshot: fetchUniversalSnapshot,
19495
- formatBarData: formatIndicesBarData,
19496
- },
19497
- llm: {
19498
- call: makeLLMCall,
19499
- seek: makeDeepseekCall,
19500
- images: makeImagesCall,
19501
- open: makeOpenRouterCall,
19502
- },
19503
- polygon: {
19504
- fetchTickerInfo: fetchTickerInfo,
19505
- fetchGroupedDaily: fetchGroupedDaily,
19506
- fetchLastTrade: fetchLastTrade,
19507
- fetchTrades: fetchTrades,
19508
- fetchPrices: fetchPrices,
19509
- analysePolygonPriceData: analysePolygonPriceData,
19510
- formatPriceData: formatPriceData,
19511
- fetchDailyOpenClose: fetchDailyOpenClose,
19512
- getPreviousClose: getPreviousClose,
19513
- },
19514
- ta: {
19515
- calculateEMA: calculateEMA,
19516
- calculateMACD: calculateMACD,
19517
- calculateRSI: calculateRSI,
19518
- calculateStochasticOscillator: calculateStochasticOscillator,
19519
- calculateBollingerBands: calculateBollingerBands,
19520
- calculateSupportAndResistance: calculateSupportAndResistance,
19521
- calculateFibonacciLevels: calculateFibonacciLevels,
19522
- },
19523
- time: {
19524
- convertDateToMarketTimeZone: convertDateToMarketTimeZone,
19525
- getStartAndEndDates: getStartAndEndDates,
19526
- getMarketOpenClose: getMarketOpenClose,
19527
- getOpenCloseForTradingDay: getOpenCloseForTradingDay,
19528
- getLastFullTradingDate: getLastFullTradingDate,
19529
- getNextMarketDay: getNextMarketDay,
19530
- getPreviousMarketDay: getPreviousMarketDay,
19531
- getMarketTimePeriod: getMarketTimePeriod,
19532
- getMarketStatus: getMarketStatus,
19533
- getNYTimeZone: getNYTimeZone,
19534
- getTradingDate: getTradingDate,
19535
- getTradingStartAndEndDates: getTradingStartAndEndDates,
19536
- getTradingDaysBack: getTradingDaysBack,
19537
- isMarketDay: isMarketDay,
19538
- isWithinMarketHours: isWithinMarketHours,
19539
- countTradingDays: countTradingDays,
19540
- MARKET_TIMES: MARKET_TIMES,
19541
- },
19542
- utils: {
19543
- logIfDebug: logIfDebug,
19544
- fetchWithRetry: fetchWithRetry,
19545
- validatePolygonApiKey: validatePolygonApiKey,
19546
- Timer: PerformanceTimer,
19547
- },
19548
- };
19549
-
19550
15963
  // Test file for context functionality
19551
- function testGetTradingDaysBack() {
19552
- const testCases = [
19553
- {
19554
- label: '1 day back from Friday after close',
19555
- referenceDate: '2025-07-11T18:00:00-04:00',
19556
- days: 1,
19557
- expected: {
19558
- date: '2025-07-11',
19559
- marketOpenISO: '2025-07-11T13:30:00.000Z',
19560
- },
19561
- },
19562
- {
19563
- label: '1 day back from Friday during market',
19564
- referenceDate: '2025-07-11T10:00:00-04:00',
19565
- days: 1,
19566
- expected: {
19567
- date: '2025-07-10',
19568
- marketOpenISO: '2025-07-10T13:30:00.000Z',
19569
- },
19570
- },
19571
- {
19572
- label: '2 days back from Friday after close',
19573
- referenceDate: '2025-07-11T18:00:00-04:00',
19574
- days: 2,
19575
- expected: {
19576
- date: '2025-07-10',
19577
- marketOpenISO: '2025-07-10T13:30:00.000Z',
19578
- },
19579
- },
19580
- {
19581
- label: '5 days back from Friday after close',
19582
- referenceDate: '2025-07-11T18:00:00-04:00',
19583
- days: 5,
19584
- expected: {
19585
- date: '2025-07-07',
19586
- marketOpenISO: '2025-07-07T13:30:00.000Z',
19587
- },
19588
- },
19589
- {
19590
- label: '6 days back from Friday after close (skips weekend and July 4)',
19591
- referenceDate: '2025-07-11T18:00:00-04:00',
19592
- days: 6,
19593
- expected: {
19594
- date: '2025-07-03',
19595
- marketOpenISO: '2025-07-03T13:30:00.000Z',
19596
- },
19597
- },
19598
- {
19599
- label: '1 day back from Monday after close (should be Monday)',
19600
- referenceDate: '2025-07-14T18:00:00-04:00',
19601
- days: 1,
19602
- expected: {
19603
- date: '2025-07-14',
19604
- marketOpenISO: '2025-07-14T13:30:00.000Z',
19605
- },
19606
- },
19607
- {
19608
- label: '1 day back from Saturday (should be Friday)',
19609
- referenceDate: '2025-07-12T12:00:00-04:00',
19610
- days: 1,
19611
- expected: {
19612
- date: '2025-07-11',
19613
- marketOpenISO: '2025-07-11T13:30:00.000Z',
19614
- },
19615
- },
19616
- {
19617
- label: '3 days back from Monday (skips weekend)',
19618
- referenceDate: '2025-07-14T18:00:00-04:00',
19619
- days: 3,
19620
- expected: {
19621
- date: '2025-07-10',
19622
- marketOpenISO: '2025-07-10T13:30:00.000Z',
19623
- },
19624
- },
19625
- {
19626
- label: '1 day back from day after holiday (July 7, after July 4)',
19627
- referenceDate: '2025-07-07T18:00:00-04:00',
19628
- days: 1,
19629
- expected: {
19630
- date: '2025-07-07',
19631
- marketOpenISO: '2025-07-07T13:30:00.000Z',
19632
- },
19633
- },
19634
- {
19635
- label: '2 days back from day after holiday (skips July 4)',
19636
- referenceDate: '2025-07-07T18:00:00-04:00',
19637
- days: 2,
19638
- expected: {
19639
- date: '2025-07-03',
19640
- marketOpenISO: '2025-07-03T13:30:00.000Z',
19641
- },
19642
- },
19643
- {
19644
- label: 'Early close day (July 3) - 1 day back from July 7',
19645
- referenceDate: '2025-07-07T18:00:00-04:00',
19646
- days: 2,
19647
- expected: {
19648
- date: '2025-07-03',
19649
- marketOpenISO: '2025-07-03T13:30:00.000Z', // Still opens at normal time
19650
- },
19651
- },
19652
- {
19653
- label: '1 day back from Friday after close (exclude most recent full day)',
19654
- referenceDate: '2025-07-11T18:00:00-04:00',
19655
- days: 1,
19656
- includeMostRecentFullDay: false,
19657
- expected: {
19658
- date: '2025-07-10',
19659
- marketOpenISO: '2025-07-10T13:30:00.000Z',
19660
- },
19661
- },
19662
- {
19663
- label: '3 days back from Monday after close (exclude most recent full day)',
19664
- referenceDate: '2025-07-14T18:00:00-04:00',
19665
- days: 3,
19666
- includeMostRecentFullDay: false,
19667
- expected: {
19668
- date: '2025-07-09',
19669
- marketOpenISO: '2025-07-09T13:30:00.000Z',
19670
- },
19671
- },
19672
- ];
19673
- console.log('\n=== Testing getTradingDaysBack ===\n');
19674
- for (const { label, referenceDate, days, includeMostRecentFullDay, expected } of testCases) {
19675
- try {
19676
- const refDate = new Date(referenceDate);
19677
- const result = disco.time.getTradingDaysBack({ referenceDate: refDate, days, includeMostRecentFullDay });
19678
- const dateMatches = result.date === expected.date;
19679
- const isoMatches = result.marketOpenISO === expected.marketOpenISO;
19680
- const unixValid = typeof result.unixTimestamp === 'number' && result.unixTimestamp > 0;
19681
- // Verify unix timestamp matches ISO
19682
- const dateFromUnix = new Date(result.unixTimestamp * 1000).toISOString();
19683
- const unixMatches = dateFromUnix === result.marketOpenISO;
19684
- const allPass = dateMatches && isoMatches && unixValid && unixMatches;
19685
- console.log(`${allPass ? '✅' : '❌'} ${label}`);
19686
- if (!allPass) {
19687
- console.log(` Expected: ${JSON.stringify(expected)}`);
19688
- console.log(` Got: ${JSON.stringify(result)}`);
19689
- if (!dateMatches)
19690
- console.log(` ❌ Date mismatch`);
19691
- if (!isoMatches)
19692
- console.log(` ❌ ISO mismatch`);
19693
- if (!unixValid)
19694
- console.log(` ❌ Unix timestamp invalid`);
19695
- if (!unixMatches)
19696
- console.log(` ❌ Unix timestamp doesn't match ISO`);
19697
- }
19698
- }
19699
- catch (error) {
19700
- console.log(`❌ ${label}`);
19701
- console.log(` Error: ${error instanceof Error ? error.message : String(error)}`);
19702
- }
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;
19703
16029
  }
19704
- const totalTests = testCases.length;
19705
- const passedTests = testCases.filter((tc) => {
19706
- try {
19707
- const refDate = new Date(tc.referenceDate);
19708
- const result = disco.time.getTradingDaysBack({
19709
- referenceDate: refDate,
19710
- days: tc.days,
19711
- includeMostRecentFullDay: tc.includeMostRecentFullDay,
19712
- });
19713
- return result.date === tc.expected.date && result.marketOpenISO === tc.expected.marketOpenISO;
19714
- }
19715
- catch {
19716
- return false;
19717
- }
19718
- }).length;
19719
- console.log(`\n=== Summary: ${passedTests}/${totalTests} tests passed ===\n`);
19720
16030
  }
19721
16031
  // testGetTradingDate();
19722
16032
  // testGetTradingStartAndEndDates();
@@ -19735,5 +16045,6 @@ function testGetTradingDaysBack() {
19735
16045
  // testMarketDataAPI();
19736
16046
  // testLLM();
19737
16047
  // testImageModelDefaults();
19738
- testGetTradingDaysBack();
16048
+ // testGetTradingDaysBack();
16049
+ testMOOAndMOCOrders();
19739
16050
  //# sourceMappingURL=test.js.map