@discomedia/utils 1.0.51 → 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index-frontend.cjs +12 -5
- package/dist/index-frontend.cjs.map +1 -1
- package/dist/index-frontend.mjs +12 -5
- package/dist/index-frontend.mjs.map +1 -1
- package/dist/index.cjs +112 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +112 -14
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +3 -3
- package/dist/test.js +302 -3962
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +35 -0
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/market-time.d.ts +6 -6
- package/dist/types/market-time.d.ts.map +1 -1
- package/dist/types/types/alpaca-types.d.ts +12 -0
- package/dist/types/types/alpaca-types.d.ts.map +1 -1
- package/dist/types/types/market-time-types.d.ts +5 -0
- package/dist/types/types/market-time-types.d.ts.map +1 -1
- package/dist/types-frontend/alpaca-trading-api.d.ts +35 -0
- package/dist/types-frontend/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types-frontend/market-time.d.ts +6 -6
- package/dist/types-frontend/market-time.d.ts.map +1 -1
- package/dist/types-frontend/types/alpaca-types.d.ts +12 -0
- package/dist/types-frontend/types/alpaca-types.d.ts.map +1 -1
- package/dist/types-frontend/types/market-time-types.d.ts +5 -0
- package/dist/types-frontend/types/market-time-types.d.ts.map +1 -1
- package/package.json +3 -3
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,656 +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
|
-
*
|
|
971
|
-
* @param options - Object with:
|
|
972
|
-
* - referenceDate: Date to count back from (default: now)
|
|
973
|
-
* - days: Number of trading days to go back (must be >= 1)
|
|
974
|
-
* @returns Object containing:
|
|
975
|
-
* - date: Trading date in YYYY-MM-DD format
|
|
976
|
-
* - marketOpenISO: Market open time as ISO string (e.g., "2025-11-15T13:30:00.000Z")
|
|
977
|
-
* - unixTimestamp: Market open time as Unix timestamp in seconds
|
|
978
|
-
* @example
|
|
979
|
-
* ```typescript
|
|
980
|
-
* // Get the trading day 1 day back (most recent full trading day)
|
|
981
|
-
* const result = getTradingDaysBack({ days: 1 });
|
|
982
|
-
* console.log(result.date); // "2025-11-01"
|
|
983
|
-
* console.log(result.marketOpenISO); // "2025-11-01T13:30:00.000Z"
|
|
984
|
-
* console.log(result.unixTimestamp); // 1730466600
|
|
985
|
-
*
|
|
986
|
-
* // Get the trading day 5 days back from a specific date
|
|
987
|
-
* const result2 = getTradingDaysBack({
|
|
988
|
-
* referenceDate: new Date('2025-11-15T12:00:00-05:00'),
|
|
989
|
-
* days: 5
|
|
990
|
-
* });
|
|
991
|
-
* ```
|
|
992
|
-
*/
|
|
993
|
-
function getTradingDaysBack(options) {
|
|
994
|
-
const calendar = new MarketCalendar();
|
|
995
|
-
const refDate = options.referenceDate || new Date();
|
|
996
|
-
const daysBack = options.days;
|
|
997
|
-
if (daysBack < 1) {
|
|
998
|
-
throw new Error('days must be at least 1');
|
|
999
|
-
}
|
|
1000
|
-
// Start from the last full trading date relative to reference
|
|
1001
|
-
let targetDate = getLastFullTradingDateImpl(refDate);
|
|
1002
|
-
// Go back the specified number of days (we're already at day 1, so go back days-1 more)
|
|
1003
|
-
for (let i = 1; i < daysBack; i++) {
|
|
1004
|
-
targetDate = calendar.getPreviousMarketDay(targetDate);
|
|
1005
|
-
}
|
|
1006
|
-
// Get market open time for this date
|
|
1007
|
-
const marketTimes = getMarketTimes(targetDate);
|
|
1008
|
-
if (!marketTimes.open) {
|
|
1009
|
-
throw new Error(`No market open time for target date`);
|
|
1010
|
-
}
|
|
1011
|
-
// Format the date string (YYYY-MM-DD) in NY time
|
|
1012
|
-
const nyDate = toNYTime(marketTimes.open);
|
|
1013
|
-
const dateStr = `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
|
|
1014
|
-
const marketOpenISO = marketTimes.open.toISOString();
|
|
1015
|
-
const unixTimestamp = Math.floor(marketTimes.open.getTime() / 1000);
|
|
1016
|
-
return {
|
|
1017
|
-
date: dateStr,
|
|
1018
|
-
marketOpenISO,
|
|
1019
|
-
unixTimestamp,
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
// Export MARKET_TIMES for compatibility
|
|
1023
|
-
const MARKET_TIMES = {
|
|
1024
|
-
TIMEZONE: MARKET_CONFIG.TIMEZONE,
|
|
1025
|
-
PRE: {
|
|
1026
|
-
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
1027
|
-
END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1028
|
-
},
|
|
1029
|
-
EARLY_MORNING: {
|
|
1030
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1031
|
-
END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
|
|
1032
|
-
},
|
|
1033
|
-
EARLY_CLOSE_BEFORE_HOLIDAY: {
|
|
1034
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1035
|
-
END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
|
|
1036
|
-
},
|
|
1037
|
-
EARLY_EXTENDED_BEFORE_HOLIDAY: {
|
|
1038
|
-
START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
|
|
1039
|
-
END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
|
|
1040
|
-
},
|
|
1041
|
-
REGULAR: {
|
|
1042
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1043
|
-
END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
|
|
1044
|
-
},
|
|
1045
|
-
EXTENDED: {
|
|
1046
|
-
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
1047
|
-
END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
|
|
1048
|
-
},
|
|
1049
|
-
};
|
|
1050
383
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
* @returns {string} The capitalized string, or original value if not a string
|
|
1056
|
-
* @example
|
|
1057
|
-
* capitalize('hello') // 'Hello'
|
|
1058
|
-
* capitalize(123) // 123
|
|
1059
|
-
*/
|
|
1060
|
-
function capFirstLetter(str) {
|
|
1061
|
-
if (!str || typeof str !== 'string')
|
|
1062
|
-
return str;
|
|
1063
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Formats a number as US currency
|
|
1067
|
-
* @param {number} value - The number to format
|
|
1068
|
-
* @returns {string} The formatted currency string (e.g. '$1,234.56')
|
|
1069
|
-
* @example
|
|
1070
|
-
* formatCurrency(1234.56) // '$1,234.56'
|
|
1071
|
-
* formatCurrency(NaN) // '$0.00'
|
|
1072
|
-
*/
|
|
1073
|
-
function formatCurrency(value) {
|
|
1074
|
-
if (isNaN(value)) {
|
|
1075
|
-
return '$0.00';
|
|
1076
|
-
}
|
|
1077
|
-
return new Intl.NumberFormat('en-US', {
|
|
1078
|
-
style: 'currency',
|
|
1079
|
-
currency: 'USD',
|
|
1080
|
-
}).format(value);
|
|
1081
|
-
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Formats a number with commas
|
|
1084
|
-
* @param {number} value - The number to format
|
|
1085
|
-
* @returns {string} The formatted number string (e.g. '1,234.56')
|
|
1086
|
-
* @example
|
|
1087
|
-
* formatNumber(1234.56) // '1,234.56'
|
|
1088
|
-
* formatNumber(NaN) // '0'
|
|
1089
|
-
*/
|
|
1090
|
-
function formatNumber(value) {
|
|
1091
|
-
if (isNaN(value)) {
|
|
1092
|
-
return '0';
|
|
1093
|
-
}
|
|
1094
|
-
return new Intl.NumberFormat('en-US').format(value);
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Formats a number as a percentage
|
|
1098
|
-
* @param {number} value - The number to format (e.g. 0.75 for 75%)
|
|
1099
|
-
* @param {number} [decimalPlaces=2] - Number of decimal places to show
|
|
1100
|
-
* @returns {string} The formatted percentage string (e.g. '75.00%')
|
|
1101
|
-
* @example
|
|
1102
|
-
* formatPercentage(0.75) // '75.00%'
|
|
1103
|
-
* formatPercentage(0.753, 1) // '75.3%'
|
|
1104
|
-
*/
|
|
1105
|
-
function formatPercentage(value, decimalPlaces = 2) {
|
|
1106
|
-
if (isNaN(value)) {
|
|
1107
|
-
return '0%';
|
|
1108
|
-
}
|
|
1109
|
-
return new Intl.NumberFormat('en-US', {
|
|
1110
|
-
style: 'percent',
|
|
1111
|
-
minimumFractionDigits: decimalPlaces,
|
|
1112
|
-
}).format(value);
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Formats a Date object to Australian datetime format for Google Sheets
|
|
1116
|
-
* @param {Date} date - The date to format
|
|
1117
|
-
* @returns {string} The formatted datetime string in 'DD/MM/YYYY HH:MM:SS' format
|
|
1118
|
-
* @example
|
|
1119
|
-
* dateTimeForGS(new Date('2025-01-01T12:34:56')) // '01/01/2025 12:34:56'
|
|
1120
|
-
*/
|
|
1121
|
-
function dateTimeForGS(date) {
|
|
1122
|
-
return date
|
|
1123
|
-
.toLocaleString('en-AU', {
|
|
1124
|
-
day: '2-digit',
|
|
1125
|
-
month: '2-digit',
|
|
1126
|
-
year: 'numeric',
|
|
1127
|
-
hour: '2-digit',
|
|
1128
|
-
minute: '2-digit',
|
|
1129
|
-
second: '2-digit',
|
|
1130
|
-
hour12: false,
|
|
1131
|
-
})
|
|
1132
|
-
.replace(/\./g, '/');
|
|
1133
|
-
}
|
|
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
|
+
*/
|
|
1134
388
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
'openai/gpt-5-nano',
|
|
1143
|
-
'openai/gpt-5.1',
|
|
1144
|
-
'openai/gpt-5.2',
|
|
1145
|
-
'openai/gpt-5.2-pro',
|
|
1146
|
-
'openai/gpt-5.1-codex',
|
|
1147
|
-
'openai/gpt-5.1-codex-max',
|
|
1148
|
-
'openai/gpt-oss-120b',
|
|
1149
|
-
'z.ai/glm-4.5',
|
|
1150
|
-
'z.ai/glm-4.5-air',
|
|
1151
|
-
'google/gemini-2.5-flash',
|
|
1152
|
-
'google/gemini-2.5-flash-lite',
|
|
1153
|
-
'deepseek/deepseek-r1-0528',
|
|
1154
|
-
'deepseek/deepseek-chat-v3-0324',
|
|
1155
|
-
];
|
|
1156
|
-
return openRouterModels.includes(model);
|
|
389
|
+
class Node {
|
|
390
|
+
value;
|
|
391
|
+
next;
|
|
392
|
+
|
|
393
|
+
constructor(value) {
|
|
394
|
+
this.value = value;
|
|
395
|
+
}
|
|
1157
396
|
}
|
|
1158
397
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
// Utility function for debug logging
|
|
1165
|
-
// Define the possible log types as a const array for better type inference
|
|
1166
|
-
/**
|
|
1167
|
-
* Debug logging utility that respects environment debug flags.
|
|
1168
|
-
* Logs messages to the console based on the specified log level.
|
|
1169
|
-
*
|
|
1170
|
-
* @param message - The message to log.
|
|
1171
|
-
* @param data - Optional data to log alongside the message. This can be any type of data.
|
|
1172
|
-
* @param type - Log level. One of: 'info' | 'warn' | 'error' | 'debug' | 'trace'. Defaults to 'info'.
|
|
1173
|
-
*
|
|
1174
|
-
* @example
|
|
1175
|
-
* logIfDebug("User login failed", { userId: 123 }, "error");
|
|
1176
|
-
* logIfDebug("Cache miss", undefined, "warn");
|
|
1177
|
-
* logIfDebug("Processing request", { requestId: "abc" }, "debug");
|
|
1178
|
-
*/
|
|
1179
|
-
const logIfDebug = (message, data, type = 'info') => {
|
|
1180
|
-
const prefix = `[DEBUG][${type.toUpperCase()}]`;
|
|
1181
|
-
const formattedData = data !== undefined ? JSON.stringify(data, null, 2) : '';
|
|
1182
|
-
switch (type) {
|
|
1183
|
-
case 'error':
|
|
1184
|
-
console.error(prefix, message, formattedData);
|
|
1185
|
-
break;
|
|
1186
|
-
case 'warn':
|
|
1187
|
-
console.warn(prefix, message, formattedData);
|
|
1188
|
-
break;
|
|
1189
|
-
case 'debug':
|
|
1190
|
-
console.debug(prefix, message, formattedData);
|
|
1191
|
-
break;
|
|
1192
|
-
case 'trace':
|
|
1193
|
-
console.trace(prefix, message, formattedData);
|
|
1194
|
-
break;
|
|
1195
|
-
case 'info':
|
|
1196
|
-
default:
|
|
1197
|
-
console.info(prefix, message, formattedData);
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
/**
|
|
1201
|
-
* Masks the middle part of an API key, returning only the first 2 and last 2 characters.
|
|
1202
|
-
* If the API key is very short (<= 4 characters), it will be returned as is.
|
|
1203
|
-
*
|
|
1204
|
-
* @param keyValue - The API key to mask.
|
|
1205
|
-
* @returns The masked API key.
|
|
1206
|
-
*
|
|
1207
|
-
* @example
|
|
1208
|
-
* maskApiKey("12341239856677"); // Returns "12****77"
|
|
1209
|
-
*/
|
|
1210
|
-
function maskApiKey(keyValue) {
|
|
1211
|
-
if (keyValue.length <= 4) {
|
|
1212
|
-
return keyValue;
|
|
1213
|
-
}
|
|
1214
|
-
const firstTwo = keyValue.slice(0, 2);
|
|
1215
|
-
const lastTwo = keyValue.slice(-2);
|
|
1216
|
-
return `${firstTwo}****${lastTwo}`;
|
|
1217
|
-
}
|
|
1218
|
-
/**
|
|
1219
|
-
* Hides (masks) the value of any query parameter that is "apiKey" (case-insensitive),
|
|
1220
|
-
* replacing the middle part with **** and keeping only the first 2 and last 2 characters.
|
|
1221
|
-
*
|
|
1222
|
-
* @param url - The URL containing the query parameters.
|
|
1223
|
-
* @returns The URL with the masked API key.
|
|
1224
|
-
*
|
|
1225
|
-
* @example
|
|
1226
|
-
* hideApiKeyFromurl("https://xxx.com/s/23/fdsa/?apiKey=12341239856677");
|
|
1227
|
-
* // Returns "https://xxx.com/s/23/fdsa/?apiKey=12****77"
|
|
1228
|
-
*/
|
|
1229
|
-
function hideApiKeyFromurl(url) {
|
|
1230
|
-
try {
|
|
1231
|
-
const parsedUrl = new URL(url);
|
|
1232
|
-
// We iterate over all search params and look for one named 'apikey' (case-insensitive)
|
|
1233
|
-
for (const [key, value] of parsedUrl.searchParams.entries()) {
|
|
1234
|
-
if (key.toLowerCase() === 'apikey') {
|
|
1235
|
-
const masked = maskApiKey(value);
|
|
1236
|
-
parsedUrl.searchParams.set(key, masked);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
return parsedUrl.toString();
|
|
1240
|
-
}
|
|
1241
|
-
catch {
|
|
1242
|
-
// If we can't parse it as a valid URL, just return the original string
|
|
1243
|
-
return url;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Extracts meaningful error information from various error types.
|
|
1248
|
-
* @param error - The error to analyze.
|
|
1249
|
-
* @param response - Optional response object for HTTP errors.
|
|
1250
|
-
* @returns Structured error details.
|
|
1251
|
-
*/
|
|
1252
|
-
function extractErrorDetails(error, response) {
|
|
1253
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1254
|
-
const errName = error instanceof Error ? error.name : 'Error';
|
|
1255
|
-
if (errName === 'TypeError' && errMsg.includes('fetch')) {
|
|
1256
|
-
return { type: 'NETWORK_ERROR', reason: 'Network connectivity issue', status: null };
|
|
1257
|
-
}
|
|
1258
|
-
if (errMsg.includes('HTTP error: 429')) {
|
|
1259
|
-
const match = errMsg.match(/RATE_LIMIT: 429:(\d+)/);
|
|
1260
|
-
const retryAfter = match ? parseInt(match[1]) : undefined;
|
|
1261
|
-
return { type: 'RATE_LIMIT', reason: 'Rate limit exceeded', status: 429, retryAfter };
|
|
1262
|
-
}
|
|
1263
|
-
if (errMsg.includes('HTTP error: 401') || errMsg.includes('AUTH_ERROR: 401')) {
|
|
1264
|
-
return { type: 'AUTH_ERROR', reason: 'Authentication failed - invalid API key', status: 401 };
|
|
1265
|
-
}
|
|
1266
|
-
if (errMsg.includes('HTTP error: 403') || errMsg.includes('AUTH_ERROR: 403')) {
|
|
1267
|
-
return { type: 'AUTH_ERROR', reason: 'Access forbidden - insufficient permissions', status: 403 };
|
|
1268
|
-
}
|
|
1269
|
-
if (errMsg.includes('SERVER_ERROR:')) {
|
|
1270
|
-
const status = parseInt(errMsg.split('SERVER_ERROR: ')[1]) || 500;
|
|
1271
|
-
return { type: 'SERVER_ERROR', reason: `Server error (${status})`, status };
|
|
1272
|
-
}
|
|
1273
|
-
if (errMsg.includes('CLIENT_ERROR:')) {
|
|
1274
|
-
const status = parseInt(errMsg.split('CLIENT_ERROR: ')[1]) || 400;
|
|
1275
|
-
return { type: 'CLIENT_ERROR', reason: `Client error (${status})`, status };
|
|
1276
|
-
}
|
|
1277
|
-
return { type: 'UNKNOWN', reason: errMsg || 'Unknown error', status: null };
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Fetches a resource with intelligent retry logic for handling transient errors.
|
|
1281
|
-
* Features enhanced error logging, rate limit detection, and adaptive backoff.
|
|
1282
|
-
*
|
|
1283
|
-
* @param url - The URL to fetch.
|
|
1284
|
-
* @param options - Optional fetch options.
|
|
1285
|
-
* @param retries - The number of retry attempts. Defaults to 3.
|
|
1286
|
-
* @param initialBackoff - The initial backoff time in milliseconds. Defaults to 1000.
|
|
1287
|
-
* @returns A promise that resolves to the response.
|
|
1288
|
-
*
|
|
1289
|
-
* @throws Will throw an error if the fetch fails after the specified number of retries.
|
|
1290
|
-
*/
|
|
1291
|
-
async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1000) {
|
|
1292
|
-
let backoff = initialBackoff;
|
|
1293
|
-
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1294
|
-
try {
|
|
1295
|
-
const response = await fetch(url, options);
|
|
1296
|
-
if (!response.ok) {
|
|
1297
|
-
// Enhanced HTTP error handling with specific error types
|
|
1298
|
-
if (response.status === 429) {
|
|
1299
|
-
// Check for Retry-After header
|
|
1300
|
-
const retryAfter = response.headers.get('Retry-After');
|
|
1301
|
-
const retryDelay = retryAfter ? parseInt(retryAfter) * 1000 : null;
|
|
1302
|
-
throw new Error(`RATE_LIMIT: ${response.status}${retryDelay ? `:${retryDelay}` : ''}`);
|
|
1303
|
-
}
|
|
1304
|
-
if ([500, 502, 503, 504].includes(response.status)) {
|
|
1305
|
-
throw new Error(`SERVER_ERROR: ${response.status}`);
|
|
1306
|
-
}
|
|
1307
|
-
if ([401, 403].includes(response.status)) {
|
|
1308
|
-
throw new Error(`AUTH_ERROR: ${response.status}`);
|
|
1309
|
-
}
|
|
1310
|
-
if (response.status >= 400 && response.status < 500) {
|
|
1311
|
-
// Don't retry most 4xx client errors
|
|
1312
|
-
throw new Error(`CLIENT_ERROR: ${response.status}`);
|
|
1313
|
-
}
|
|
1314
|
-
throw new Error(`HTTP_ERROR: ${response.status}`);
|
|
1315
|
-
}
|
|
1316
|
-
return response;
|
|
1317
|
-
}
|
|
1318
|
-
catch (error) {
|
|
1319
|
-
if (attempt === retries) {
|
|
1320
|
-
throw error;
|
|
1321
|
-
}
|
|
1322
|
-
// Extract meaningful error information
|
|
1323
|
-
const errorDetails = extractErrorDetails(error);
|
|
1324
|
-
let adaptiveBackoff = backoff;
|
|
1325
|
-
// Adaptive backoff based on error type
|
|
1326
|
-
if (errorDetails.type === 'RATE_LIMIT') {
|
|
1327
|
-
// Use Retry-After header if available, otherwise use minimum 5s for rate limits
|
|
1328
|
-
if (errorDetails.retryAfter) {
|
|
1329
|
-
adaptiveBackoff = errorDetails.retryAfter;
|
|
1330
|
-
}
|
|
1331
|
-
else {
|
|
1332
|
-
adaptiveBackoff = Math.max(backoff, 5000);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
else if (errorDetails.type === 'AUTH_ERROR') {
|
|
1336
|
-
// Don't retry auth errors - fail fast
|
|
1337
|
-
console.error(`Authentication error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
|
|
1338
|
-
attemptNumber: attempt,
|
|
1339
|
-
errorType: errorDetails.type,
|
|
1340
|
-
httpStatus: errorDetails.status,
|
|
1341
|
-
url: hideApiKeyFromurl(url),
|
|
1342
|
-
source: 'fetchWithRetry',
|
|
1343
|
-
timestamp: new Date().toISOString(),
|
|
1344
|
-
});
|
|
1345
|
-
throw error;
|
|
1346
|
-
}
|
|
1347
|
-
else if (errorDetails.type === 'CLIENT_ERROR') {
|
|
1348
|
-
// Don't retry client errors (except 429 which is handled above)
|
|
1349
|
-
console.error(`Client error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
|
|
1350
|
-
attemptNumber: attempt,
|
|
1351
|
-
errorType: errorDetails.type,
|
|
1352
|
-
httpStatus: errorDetails.status,
|
|
1353
|
-
url: hideApiKeyFromurl(url),
|
|
1354
|
-
source: 'fetchWithRetry',
|
|
1355
|
-
timestamp: new Date().toISOString(),
|
|
1356
|
-
});
|
|
1357
|
-
throw error;
|
|
1358
|
-
}
|
|
1359
|
-
// Enhanced error logging with structured data
|
|
1360
|
-
console.warn(`Fetch attempt ${attempt} of ${retries} for ${hideApiKeyFromurl(url)} failed: ${errorDetails.reason}. Retrying in ${adaptiveBackoff}ms...`, {
|
|
1361
|
-
attemptNumber: attempt,
|
|
1362
|
-
totalRetries: retries,
|
|
1363
|
-
errorType: errorDetails.type,
|
|
1364
|
-
httpStatus: errorDetails.status,
|
|
1365
|
-
retryDelay: adaptiveBackoff,
|
|
1366
|
-
url: hideApiKeyFromurl(url),
|
|
1367
|
-
source: 'fetchWithRetry',
|
|
1368
|
-
timestamp: new Date().toISOString(),
|
|
1369
|
-
});
|
|
1370
|
-
await new Promise((resolve) => setTimeout(resolve, adaptiveBackoff));
|
|
1371
|
-
backoff = Math.min(backoff * 2, 30000); // Cap at 30 seconds
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
throw new Error('Failed to fetch after multiple attempts');
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Validates a Polygon.io API key by making a test request.
|
|
1378
|
-
* @param apiKey - The API key to validate.
|
|
1379
|
-
* @returns Promise that resolves to true if valid, false otherwise.
|
|
1380
|
-
*/
|
|
1381
|
-
async function validatePolygonApiKey(apiKey) {
|
|
1382
|
-
try {
|
|
1383
|
-
const response = await fetch(`https://api.polygon.io/v1/meta/symbols?apikey=${apiKey}&limit=1`);
|
|
1384
|
-
if (response.status === 401) {
|
|
1385
|
-
throw new Error('Invalid or expired Polygon.io API key');
|
|
1386
|
-
}
|
|
1387
|
-
if (response.status === 403) {
|
|
1388
|
-
throw new Error('Polygon.io API key lacks required permissions');
|
|
1389
|
-
}
|
|
1390
|
-
return response.ok;
|
|
1391
|
-
}
|
|
1392
|
-
catch (error) {
|
|
1393
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1394
|
-
console.error('Polygon.io API key validation failed:', errorMessage);
|
|
1395
|
-
return false;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/*
|
|
1400
|
-
How it works:
|
|
1401
|
-
`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.
|
|
1402
|
-
*/
|
|
1403
|
-
|
|
1404
|
-
class Node {
|
|
1405
|
-
value;
|
|
1406
|
-
next;
|
|
1407
|
-
|
|
1408
|
-
constructor(value) {
|
|
1409
|
-
this.value = value;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
class Queue {
|
|
1414
|
-
#head;
|
|
1415
|
-
#tail;
|
|
1416
|
-
#size;
|
|
398
|
+
class Queue {
|
|
399
|
+
#head;
|
|
400
|
+
#tail;
|
|
401
|
+
#size;
|
|
1417
402
|
|
|
1418
403
|
constructor() {
|
|
1419
404
|
this.clear();
|
|
@@ -1583,473 +568,10 @@ function validateConcurrency(concurrency) {
|
|
|
1583
568
|
* Polygon.io calls
|
|
1584
569
|
**********************************************************************************/
|
|
1585
570
|
// Constants from environment variables
|
|
1586
|
-
|
|
571
|
+
process.env.POLYGON_API_KEY;
|
|
1587
572
|
// Define concurrency limits per API
|
|
1588
573
|
const POLYGON_CONCURRENCY_LIMIT = 100;
|
|
1589
|
-
|
|
1590
|
-
// Use to update general information about stocks
|
|
1591
|
-
/**
|
|
1592
|
-
* Fetches general information about a stock ticker.
|
|
1593
|
-
* @param {string} symbol - The stock ticker symbol to fetch information for.
|
|
1594
|
-
* @param {Object} [options] - Optional parameters.
|
|
1595
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1596
|
-
* @returns {Promise<PolygonTickerInfo | null>} The ticker information or null if not found.
|
|
1597
|
-
*/
|
|
1598
|
-
const fetchTickerInfo = async (symbol, options) => {
|
|
1599
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1600
|
-
throw new Error('Polygon API key is missing');
|
|
1601
|
-
}
|
|
1602
|
-
const baseUrl = `https://api.polygon.io/v3/reference/tickers/${encodeURIComponent(symbol)}`;
|
|
1603
|
-
const params = new URLSearchParams({
|
|
1604
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1605
|
-
});
|
|
1606
|
-
return polygonLimit(async () => {
|
|
1607
|
-
try {
|
|
1608
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1609
|
-
const data = await response.json();
|
|
1610
|
-
// Check for "NOT_FOUND" status and return null
|
|
1611
|
-
if (data.status === 'NOT_FOUND') {
|
|
1612
|
-
console.warn(`Ticker not found: ${symbol}`);
|
|
1613
|
-
return null;
|
|
1614
|
-
}
|
|
1615
|
-
// Map the results to the required structure
|
|
1616
|
-
const results = data.results;
|
|
1617
|
-
if (!results) {
|
|
1618
|
-
throw new Error('No results in Polygon API response');
|
|
1619
|
-
}
|
|
1620
|
-
// Validate required fields
|
|
1621
|
-
const requiredFields = [
|
|
1622
|
-
'active',
|
|
1623
|
-
'currency_name',
|
|
1624
|
-
'locale',
|
|
1625
|
-
'market',
|
|
1626
|
-
'name',
|
|
1627
|
-
'primary_exchange',
|
|
1628
|
-
'ticker',
|
|
1629
|
-
'type',
|
|
1630
|
-
];
|
|
1631
|
-
for (const field of requiredFields) {
|
|
1632
|
-
if (results[field] === undefined) {
|
|
1633
|
-
throw new Error(`Missing required field in Polygon API response: ${field}`);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
// Handle optional share_class_shares_outstanding field
|
|
1637
|
-
if (results.share_class_shares_outstanding === undefined) {
|
|
1638
|
-
results.share_class_shares_outstanding = null;
|
|
1639
|
-
}
|
|
1640
|
-
return {
|
|
1641
|
-
ticker: results.ticker,
|
|
1642
|
-
type: results.type,
|
|
1643
|
-
active: results.active,
|
|
1644
|
-
currency_name: results.currency_name,
|
|
1645
|
-
description: results.description ?? 'No description available',
|
|
1646
|
-
locale: results.locale,
|
|
1647
|
-
market: results.market,
|
|
1648
|
-
market_cap: results.market_cap ?? 0,
|
|
1649
|
-
name: results.name,
|
|
1650
|
-
primary_exchange: results.primary_exchange,
|
|
1651
|
-
share_class_shares_outstanding: results.share_class_shares_outstanding,
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
catch (error) {
|
|
1655
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1656
|
-
const contextualMessage = `Error fetching ticker info for ${symbol}`;
|
|
1657
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1658
|
-
symbol,
|
|
1659
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1660
|
-
? 'AUTH_ERROR'
|
|
1661
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1662
|
-
? 'RATE_LIMIT'
|
|
1663
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1664
|
-
? 'NETWORK_ERROR'
|
|
1665
|
-
: 'UNKNOWN',
|
|
1666
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1667
|
-
source: 'PolygonAPI.fetchTickerInfo',
|
|
1668
|
-
timestamp: new Date().toISOString(),
|
|
1669
|
-
});
|
|
1670
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1673
|
-
};
|
|
1674
|
-
// Fetch last trade using Polygon.io
|
|
1675
|
-
/**
|
|
1676
|
-
* Fetches the last trade for a given stock ticker.
|
|
1677
|
-
* @param {string} symbol - The stock ticker symbol to fetch the last trade for.
|
|
1678
|
-
* @param {Object} [options] - Optional parameters.
|
|
1679
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1680
|
-
* @returns {Promise<PolygonQuote>} The last trade information.
|
|
1681
|
-
*/
|
|
1682
|
-
const fetchLastTrade = async (symbol, options) => {
|
|
1683
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1684
|
-
throw new Error('Polygon API key is missing');
|
|
1685
|
-
}
|
|
1686
|
-
const baseUrl = `https://api.polygon.io/v2/last/trade/${encodeURIComponent(symbol)}`;
|
|
1687
|
-
const params = new URLSearchParams({
|
|
1688
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1689
|
-
});
|
|
1690
|
-
return polygonLimit(async () => {
|
|
1691
|
-
try {
|
|
1692
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1693
|
-
const data = await response.json();
|
|
1694
|
-
if (data.status !== 'OK' || !data.results) {
|
|
1695
|
-
throw new Error(`Polygon.io API error: ${data.status || 'No results'} ${data.error || ''}`);
|
|
1696
|
-
}
|
|
1697
|
-
const { p: price, s: vol, t: timestamp } = data.results;
|
|
1698
|
-
if (typeof price !== 'number' || typeof vol !== 'number' || typeof timestamp !== 'number') {
|
|
1699
|
-
throw new Error('Invalid trade data received from Polygon.io API');
|
|
1700
|
-
}
|
|
1701
|
-
return {
|
|
1702
|
-
price,
|
|
1703
|
-
vol,
|
|
1704
|
-
time: new Date(Math.floor(timestamp / 1000000)), // Convert nanoseconds to milliseconds
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
catch (error) {
|
|
1708
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1709
|
-
const contextualMessage = `Error fetching last trade for ${symbol}`;
|
|
1710
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1711
|
-
symbol,
|
|
1712
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1713
|
-
? 'AUTH_ERROR'
|
|
1714
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1715
|
-
? 'RATE_LIMIT'
|
|
1716
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1717
|
-
? 'NETWORK_ERROR'
|
|
1718
|
-
: 'UNKNOWN',
|
|
1719
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1720
|
-
source: 'PolygonAPI.fetchLastTrade',
|
|
1721
|
-
timestamp: new Date().toISOString(),
|
|
1722
|
-
});
|
|
1723
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
};
|
|
1727
|
-
// use Polygon for all price data fetching
|
|
1728
|
-
/**
|
|
1729
|
-
* Fetches price data for a given stock ticker.
|
|
1730
|
-
* @param {Object} params - The parameters for fetching price data.
|
|
1731
|
-
* @param {string} params.ticker - The stock ticker symbol.
|
|
1732
|
-
* @param {number} params.start - The start timestamp for fetching price data.
|
|
1733
|
-
* @param {number} [params.end] - The end timestamp for fetching price data.
|
|
1734
|
-
* @param {number} params.multiplier - The multiplier for the price data.
|
|
1735
|
-
* @param {string} params.timespan - The timespan for the price data.
|
|
1736
|
-
* @param {number} [params.limit] - The maximum number of price data points to fetch.
|
|
1737
|
-
* @param {Object} [options] - Optional parameters.
|
|
1738
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1739
|
-
* @returns {Promise<PolygonPriceData[]>} The fetched price data.
|
|
1740
|
-
*/
|
|
1741
|
-
const fetchPrices = async (params, options) => {
|
|
1742
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1743
|
-
throw new Error('Polygon API key is missing');
|
|
1744
|
-
}
|
|
1745
|
-
const { ticker, start, end = Date.now().valueOf(), multiplier, timespan, limit = 1000 } = params;
|
|
1746
|
-
const baseUrl = `https://api.polygon.io/v2/aggs/ticker/${encodeURIComponent(ticker)}/range/${multiplier}/${timespan}/${start}/${end}`;
|
|
1747
|
-
const urlParams = new URLSearchParams({
|
|
1748
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1749
|
-
adjusted: 'true',
|
|
1750
|
-
sort: 'asc',
|
|
1751
|
-
limit: limit.toString(),
|
|
1752
|
-
});
|
|
1753
|
-
return polygonLimit(async () => {
|
|
1754
|
-
try {
|
|
1755
|
-
let allResults = [];
|
|
1756
|
-
let nextUrl = `${baseUrl}?${urlParams.toString()}`;
|
|
1757
|
-
while (nextUrl) {
|
|
1758
|
-
//console.log(`Debug: Fetching ${nextUrl}`);
|
|
1759
|
-
const response = await fetchWithRetry(nextUrl, {}, 3, 1000);
|
|
1760
|
-
const data = await response.json();
|
|
1761
|
-
if (data.status !== 'OK') {
|
|
1762
|
-
throw new Error(`Polygon.io API responded with status: ${data.status}`);
|
|
1763
|
-
}
|
|
1764
|
-
if (data.results) {
|
|
1765
|
-
allResults = [...allResults, ...data.results];
|
|
1766
|
-
}
|
|
1767
|
-
// Check if there's a next page and append API key
|
|
1768
|
-
nextUrl = data.next_url ? `${data.next_url}&apiKey=${options?.apiKey || POLYGON_API_KEY}` : '';
|
|
1769
|
-
}
|
|
1770
|
-
return allResults.map((entry) => ({
|
|
1771
|
-
date: new Date(entry.t).toLocaleString('en-US', {
|
|
1772
|
-
year: 'numeric',
|
|
1773
|
-
month: 'short',
|
|
1774
|
-
day: '2-digit',
|
|
1775
|
-
hour: '2-digit',
|
|
1776
|
-
minute: '2-digit',
|
|
1777
|
-
second: '2-digit',
|
|
1778
|
-
timeZone: 'America/New_York',
|
|
1779
|
-
timeZoneName: 'short',
|
|
1780
|
-
hourCycle: 'h23',
|
|
1781
|
-
}),
|
|
1782
|
-
timeStamp: entry.t,
|
|
1783
|
-
open: entry.o,
|
|
1784
|
-
high: entry.h,
|
|
1785
|
-
low: entry.l,
|
|
1786
|
-
close: entry.c,
|
|
1787
|
-
vol: entry.v,
|
|
1788
|
-
vwap: entry.vw,
|
|
1789
|
-
trades: entry.n,
|
|
1790
|
-
}));
|
|
1791
|
-
}
|
|
1792
|
-
catch (error) {
|
|
1793
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1794
|
-
const contextualMessage = `Error fetching price data for ${ticker}`;
|
|
1795
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1796
|
-
ticker,
|
|
1797
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1798
|
-
? 'AUTH_ERROR'
|
|
1799
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1800
|
-
? 'RATE_LIMIT'
|
|
1801
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1802
|
-
? 'NETWORK_ERROR'
|
|
1803
|
-
: 'UNKNOWN',
|
|
1804
|
-
source: 'PolygonAPI.fetchPrices',
|
|
1805
|
-
timestamp: new Date().toISOString(),
|
|
1806
|
-
});
|
|
1807
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1808
|
-
}
|
|
1809
|
-
});
|
|
1810
|
-
};
|
|
1811
|
-
/**
|
|
1812
|
-
* Analyzes the price data for a given stock.
|
|
1813
|
-
* @param {PolygonPriceData[]} priceData - The price data to analyze.
|
|
1814
|
-
* @returns {string} The analysis report.
|
|
1815
|
-
*/
|
|
1816
|
-
function analysePolygonPriceData(priceData) {
|
|
1817
|
-
if (!priceData || priceData.length === 0) {
|
|
1818
|
-
return 'No price data available for analysis.';
|
|
1819
|
-
}
|
|
1820
|
-
// Parse the dates into Date objects
|
|
1821
|
-
const parsedData = priceData.map((entry) => ({
|
|
1822
|
-
...entry,
|
|
1823
|
-
date: new Date(entry.date),
|
|
1824
|
-
}));
|
|
1825
|
-
// Sort the data by date
|
|
1826
|
-
parsedData.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1827
|
-
// Extract start and end times
|
|
1828
|
-
const startTime = parsedData[0].date;
|
|
1829
|
-
const endTime = parsedData[parsedData.length - 1].date;
|
|
1830
|
-
// Calculate the total time in hours
|
|
1831
|
-
(endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
|
|
1832
|
-
// Calculate the interval between data points
|
|
1833
|
-
const intervals = parsedData
|
|
1834
|
-
.slice(1)
|
|
1835
|
-
.map((_, i) => (parsedData[i + 1].date.getTime() - parsedData[i].date.getTime()) / 1000); // in seconds
|
|
1836
|
-
const avgInterval = intervals.length > 0 ? intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length : 0;
|
|
1837
|
-
// Format the report
|
|
1838
|
-
const report = `
|
|
1839
|
-
Report:
|
|
1840
|
-
* Start time of data (US Eastern): ${startTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
|
|
1841
|
-
* End time of data (US Eastern): ${endTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}
|
|
1842
|
-
* Number of data points: ${priceData.length}
|
|
1843
|
-
* Average interval between data points (seconds): ${avgInterval.toFixed(2)}
|
|
1844
|
-
`;
|
|
1845
|
-
return report.trim();
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* Fetches grouped daily price data for a specific date.
|
|
1849
|
-
* @param {string} date - The date to fetch grouped daily data for.
|
|
1850
|
-
* @param {Object} [options] - Optional parameters.
|
|
1851
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1852
|
-
* @param {boolean} [options.adjusted] - Whether to adjust the data.
|
|
1853
|
-
* @param {boolean} [options.includeOTC] - Whether to include OTC data.
|
|
1854
|
-
* @returns {Promise<PolygonGroupedDailyResponse>} The grouped daily response.
|
|
1855
|
-
*/
|
|
1856
|
-
const fetchGroupedDaily = async (date, options) => {
|
|
1857
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1858
|
-
throw new Error('Polygon API key is missing');
|
|
1859
|
-
}
|
|
1860
|
-
const baseUrl = `https://api.polygon.io/v2/aggs/grouped/locale/us/market/stocks/${date}`;
|
|
1861
|
-
const params = new URLSearchParams({
|
|
1862
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1863
|
-
adjusted: options?.adjusted !== false ? 'true' : 'false',
|
|
1864
|
-
include_otc: options?.includeOTC ? 'true' : 'false',
|
|
1865
|
-
});
|
|
1866
|
-
return polygonLimit(async () => {
|
|
1867
|
-
try {
|
|
1868
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1869
|
-
const data = await response.json();
|
|
1870
|
-
if (data.status !== 'OK') {
|
|
1871
|
-
throw new Error(`Polygon.io API responded with status: ${data.status}`);
|
|
1872
|
-
}
|
|
1873
|
-
return {
|
|
1874
|
-
adjusted: data.adjusted,
|
|
1875
|
-
queryCount: data.queryCount,
|
|
1876
|
-
request_id: data.request_id,
|
|
1877
|
-
resultsCount: data.resultsCount,
|
|
1878
|
-
status: data.status,
|
|
1879
|
-
results: data.results.map((result) => ({
|
|
1880
|
-
symbol: result.T,
|
|
1881
|
-
timeStamp: result.t,
|
|
1882
|
-
open: result.o,
|
|
1883
|
-
high: result.h,
|
|
1884
|
-
low: result.l,
|
|
1885
|
-
close: result.c,
|
|
1886
|
-
vol: result.v,
|
|
1887
|
-
vwap: result.vw,
|
|
1888
|
-
trades: result.n,
|
|
1889
|
-
})),
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
catch (error) {
|
|
1893
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1894
|
-
const contextualMessage = `Error fetching grouped daily data for ${date}`;
|
|
1895
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
1896
|
-
date,
|
|
1897
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
1898
|
-
? 'AUTH_ERROR'
|
|
1899
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
1900
|
-
? 'RATE_LIMIT'
|
|
1901
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
1902
|
-
? 'NETWORK_ERROR'
|
|
1903
|
-
: 'UNKNOWN',
|
|
1904
|
-
url: hideApiKeyFromurl(`${baseUrl}?${params.toString()}`),
|
|
1905
|
-
source: 'PolygonAPI.fetchGroupedDaily',
|
|
1906
|
-
timestamp: new Date().toISOString(),
|
|
1907
|
-
});
|
|
1908
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
1909
|
-
}
|
|
1910
|
-
});
|
|
1911
|
-
};
|
|
1912
|
-
/**
|
|
1913
|
-
* Formats the price data into a readable string.
|
|
1914
|
-
* @param {PolygonPriceData[]} priceData - The price data to format.
|
|
1915
|
-
* @returns {string} The formatted price data.
|
|
1916
|
-
*/
|
|
1917
|
-
function formatPriceData(priceData) {
|
|
1918
|
-
if (!priceData || priceData.length === 0)
|
|
1919
|
-
return 'No price data available';
|
|
1920
|
-
return priceData
|
|
1921
|
-
.map((d) => {
|
|
1922
|
-
// For daily data, remove the time portion if it's all zeros
|
|
1923
|
-
const dateStr = d.date.includes(', 00:00:00') ? d.date.split(', 00:00:00')[0] : d.date;
|
|
1924
|
-
return [
|
|
1925
|
-
dateStr,
|
|
1926
|
-
`O: ${formatCurrency(d.open)}`,
|
|
1927
|
-
`H: ${formatCurrency(d.high)}`,
|
|
1928
|
-
`L: ${formatCurrency(d.low)}`,
|
|
1929
|
-
`C: ${formatCurrency(d.close)}`,
|
|
1930
|
-
`Vol: ${d.vol}`,
|
|
1931
|
-
].join(' | ');
|
|
1932
|
-
})
|
|
1933
|
-
.join('\n');
|
|
1934
|
-
}
|
|
1935
|
-
const fetchDailyOpenClose = async (
|
|
1936
|
-
/**
|
|
1937
|
-
* Fetches the daily open and close data for a given stock ticker.
|
|
1938
|
-
* @param {string} symbol - The stock ticker symbol to fetch data for.
|
|
1939
|
-
* @param {Date} [date=new Date()] - The date to fetch data for.
|
|
1940
|
-
* @param {Object} [options] - Optional parameters.
|
|
1941
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1942
|
-
* @param {boolean} [options.adjusted] - Whether to adjust the data.
|
|
1943
|
-
* @returns {Promise<PolygonDailyOpenClose>} The daily open and close data.
|
|
1944
|
-
*/
|
|
1945
|
-
symbol, date = new Date(), options) => {
|
|
1946
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1947
|
-
throw new Error('Polygon API key is missing');
|
|
1948
|
-
}
|
|
1949
|
-
const formattedDate = date.toISOString().split('T')[0]; // Format as YYYY-MM-DD
|
|
1950
|
-
const baseUrl = `https://api.polygon.io/v1/open-close/${encodeURIComponent(symbol)}/${formattedDate}`;
|
|
1951
|
-
const params = new URLSearchParams({
|
|
1952
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
1953
|
-
adjusted: (options?.adjusted ?? true).toString(),
|
|
1954
|
-
});
|
|
1955
|
-
return polygonLimit(async () => {
|
|
1956
|
-
const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, {}, 3, 1000);
|
|
1957
|
-
const data = await response.json();
|
|
1958
|
-
if (data.status !== 'OK') {
|
|
1959
|
-
throw new Error(`Failed to fetch daily open/close data for ${symbol}: ${data.status}`);
|
|
1960
|
-
}
|
|
1961
|
-
return data;
|
|
1962
|
-
});
|
|
1963
|
-
};
|
|
1964
|
-
/**
|
|
1965
|
-
* Gets the previous close price for a given stock ticker.
|
|
1966
|
-
* @param {string} symbol - The stock ticker symbol to fetch the previous close for.
|
|
1967
|
-
* @param {Date} [referenceDate] - The reference date to use for fetching the previous close.
|
|
1968
|
-
* @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
|
|
1969
|
-
*/
|
|
1970
|
-
async function getPreviousClose(symbol, referenceDate, options) {
|
|
1971
|
-
const previousDate = getLastFullTradingDate(referenceDate);
|
|
1972
|
-
const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
|
|
1973
|
-
if (!lastOpenClose) {
|
|
1974
|
-
throw new Error(`Could not fetch last trade price for ${symbol}`);
|
|
1975
|
-
}
|
|
1976
|
-
return {
|
|
1977
|
-
close: lastOpenClose.close,
|
|
1978
|
-
date: previousDate,
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Fetches trade data for a given stock ticker.
|
|
1983
|
-
* @param {string} symbol - The stock ticker symbol to fetch trades for.
|
|
1984
|
-
* @param {Object} [options] - Optional parameters.
|
|
1985
|
-
* @param {string} [options.apiKey] - The API key to use for the request.
|
|
1986
|
-
* @param {string | number} [options.timestamp] - The timestamp for fetching trades.
|
|
1987
|
-
* @param {string | number} [options.timestampgt] - Greater than timestamp for fetching trades.
|
|
1988
|
-
* @param {string | number} [options.timestampgte] - Greater than or equal to timestamp for fetching trades.
|
|
1989
|
-
* @param {string | number} [options.timestamplt] - Less than timestamp for fetching trades.
|
|
1990
|
-
* @param {string | number} [options.timestamplte] - Less than or equal to timestamp for fetching trades.
|
|
1991
|
-
* @param {'asc' | 'desc'} [options.order] - The order of the trades.
|
|
1992
|
-
* @param {number} [options.limit] - The maximum number of trades to fetch.
|
|
1993
|
-
* @param {string} [options.sort] - The sort order for the trades.
|
|
1994
|
-
* @returns {Promise<PolygonTradesResponse>} The fetched trades response.
|
|
1995
|
-
*/
|
|
1996
|
-
const fetchTrades = async (symbol, options) => {
|
|
1997
|
-
if (!options?.apiKey && !POLYGON_API_KEY) {
|
|
1998
|
-
throw new Error('Polygon API key is missing');
|
|
1999
|
-
}
|
|
2000
|
-
const baseUrl = `https://api.polygon.io/v3/trades/${encodeURIComponent(symbol)}`;
|
|
2001
|
-
const params = new URLSearchParams({
|
|
2002
|
-
apiKey: options?.apiKey || POLYGON_API_KEY,
|
|
2003
|
-
});
|
|
2004
|
-
// Add optional parameters if they exist
|
|
2005
|
-
if (options?.timestamp)
|
|
2006
|
-
params.append('timestamp', options.timestamp.toString());
|
|
2007
|
-
if (options?.timestampgt)
|
|
2008
|
-
params.append('timestamp.gt', options.timestampgt.toString());
|
|
2009
|
-
if (options?.timestampgte)
|
|
2010
|
-
params.append('timestamp.gte', options.timestampgte.toString());
|
|
2011
|
-
if (options?.timestamplt)
|
|
2012
|
-
params.append('timestamp.lt', options.timestamplt.toString());
|
|
2013
|
-
if (options?.timestamplte)
|
|
2014
|
-
params.append('timestamp.lte', options.timestamplte.toString());
|
|
2015
|
-
if (options?.order)
|
|
2016
|
-
params.append('order', options.order);
|
|
2017
|
-
if (options?.limit)
|
|
2018
|
-
params.append('limit', options.limit.toString());
|
|
2019
|
-
if (options?.sort)
|
|
2020
|
-
params.append('sort', options.sort);
|
|
2021
|
-
return polygonLimit(async () => {
|
|
2022
|
-
const url = `${baseUrl}?${params.toString()}`;
|
|
2023
|
-
try {
|
|
2024
|
-
console.log(`[DEBUG] Fetching trades for ${symbol} from ${url}`);
|
|
2025
|
-
const response = await fetchWithRetry(url, {}, 3, 1000);
|
|
2026
|
-
const data = (await response.json());
|
|
2027
|
-
if ('message' in data) {
|
|
2028
|
-
// This is an error response
|
|
2029
|
-
throw new Error(`Polygon API Error: ${data.message}`);
|
|
2030
|
-
}
|
|
2031
|
-
return data;
|
|
2032
|
-
}
|
|
2033
|
-
catch (error) {
|
|
2034
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2035
|
-
const contextualMessage = `Error fetching trades for ${symbol}`;
|
|
2036
|
-
console.error(`${contextualMessage}: ${errorMessage}`, {
|
|
2037
|
-
symbol,
|
|
2038
|
-
errorType: error instanceof Error && error.message.includes('AUTH_ERROR')
|
|
2039
|
-
? 'AUTH_ERROR'
|
|
2040
|
-
: error instanceof Error && error.message.includes('RATE_LIMIT')
|
|
2041
|
-
? 'RATE_LIMIT'
|
|
2042
|
-
: error instanceof Error && error.message.includes('NETWORK_ERROR')
|
|
2043
|
-
? 'NETWORK_ERROR'
|
|
2044
|
-
: 'UNKNOWN',
|
|
2045
|
-
url: hideApiKeyFromurl(url),
|
|
2046
|
-
source: 'PolygonAPI.fetchTrades',
|
|
2047
|
-
timestamp: new Date().toISOString(),
|
|
2048
|
-
});
|
|
2049
|
-
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
2050
|
-
}
|
|
2051
|
-
});
|
|
2052
|
-
};
|
|
574
|
+
pLimit(POLYGON_CONCURRENCY_LIMIT);
|
|
2053
575
|
|
|
2054
576
|
/**
|
|
2055
577
|
* Polygon Indices API Implementation
|
|
@@ -2060,224 +582,7 @@ const fetchTrades = async (symbol, options) => {
|
|
|
2060
582
|
const { ALPACA_INDICES_API_KEY } = process.env;
|
|
2061
583
|
// Define concurrency limits for API
|
|
2062
584
|
const POLYGON_INDICES_CONCURRENCY_LIMIT = 5;
|
|
2063
|
-
|
|
2064
|
-
// Base URL for Polygon API
|
|
2065
|
-
const POLYGON_API_BASE_URL = 'https://api.polygon.io';
|
|
2066
|
-
/**
|
|
2067
|
-
* Validates that an API key is available
|
|
2068
|
-
* @param {string | undefined} apiKey - Optional API key to use
|
|
2069
|
-
* @throws {Error} If no API key is available
|
|
2070
|
-
*/
|
|
2071
|
-
const validateApiKey = (apiKey) => {
|
|
2072
|
-
const key = apiKey || ALPACA_INDICES_API_KEY;
|
|
2073
|
-
if (!key) {
|
|
2074
|
-
throw new Error('Polygon Indices API key is missing');
|
|
2075
|
-
}
|
|
2076
|
-
return key;
|
|
2077
|
-
};
|
|
2078
|
-
/**
|
|
2079
|
-
* Fetches aggregate bars for an index over a given date range in custom time window sizes.
|
|
2080
|
-
*
|
|
2081
|
-
* @param {PolygonIndicesAggregatesParams} params - Parameters for the aggregates request
|
|
2082
|
-
* @param {Object} [options] - Optional parameters
|
|
2083
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2084
|
-
* @returns {Promise<PolygonIndicesAggregatesResponse>} The aggregates response
|
|
2085
|
-
*/
|
|
2086
|
-
const fetchIndicesAggregates = async (params, options) => {
|
|
2087
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2088
|
-
const { indicesTicker, multiplier, timespan, from, to, sort = 'asc', limit } = params;
|
|
2089
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/range/${multiplier}/${timespan}/${from}/${to}`);
|
|
2090
|
-
const queryParams = new URLSearchParams();
|
|
2091
|
-
queryParams.append('apiKey', apiKey);
|
|
2092
|
-
if (sort) {
|
|
2093
|
-
queryParams.append('sort', sort);
|
|
2094
|
-
}
|
|
2095
|
-
if (limit) {
|
|
2096
|
-
queryParams.append('limit', limit.toString());
|
|
2097
|
-
}
|
|
2098
|
-
url.search = queryParams.toString();
|
|
2099
|
-
return polygonIndicesLimit(async () => {
|
|
2100
|
-
try {
|
|
2101
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2102
|
-
const data = await response.json();
|
|
2103
|
-
if (data.status === 'ERROR') {
|
|
2104
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2105
|
-
}
|
|
2106
|
-
return data;
|
|
2107
|
-
}
|
|
2108
|
-
catch (error) {
|
|
2109
|
-
console.error('Error fetching indices aggregates:', error);
|
|
2110
|
-
throw error;
|
|
2111
|
-
}
|
|
2112
|
-
});
|
|
2113
|
-
};
|
|
2114
|
-
/**
|
|
2115
|
-
* Gets the previous day's open, high, low, and close (OHLC) for the specified index.
|
|
2116
|
-
*
|
|
2117
|
-
* @param {string} indicesTicker - The ticker symbol of the index
|
|
2118
|
-
* @param {Object} [options] - Optional parameters
|
|
2119
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2120
|
-
* @returns {Promise<PolygonIndicesPrevCloseResponse>} The previous close response
|
|
2121
|
-
*/
|
|
2122
|
-
const fetchIndicesPreviousClose = async (indicesTicker, options) => {
|
|
2123
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2124
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v2/aggs/ticker/${encodeURIComponent(indicesTicker)}/prev`);
|
|
2125
|
-
const queryParams = new URLSearchParams();
|
|
2126
|
-
queryParams.append('apiKey', apiKey);
|
|
2127
|
-
url.search = queryParams.toString();
|
|
2128
|
-
return polygonIndicesLimit(async () => {
|
|
2129
|
-
try {
|
|
2130
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2131
|
-
const data = await response.json();
|
|
2132
|
-
if (data.status === 'ERROR') {
|
|
2133
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2134
|
-
}
|
|
2135
|
-
return data;
|
|
2136
|
-
}
|
|
2137
|
-
catch (error) {
|
|
2138
|
-
console.error('Error fetching indices previous close:', error);
|
|
2139
|
-
throw error;
|
|
2140
|
-
}
|
|
2141
|
-
});
|
|
2142
|
-
};
|
|
2143
|
-
/**
|
|
2144
|
-
* Gets the open, close and afterhours values of an index symbol on a certain date.
|
|
2145
|
-
*
|
|
2146
|
-
* @param {string} indicesTicker - The ticker symbol of the index
|
|
2147
|
-
* @param {string} date - The date in YYYY-MM-DD format
|
|
2148
|
-
* @param {Object} [options] - Optional parameters
|
|
2149
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2150
|
-
* @returns {Promise<PolygonIndicesDailyOpenCloseResponse>} The daily open/close response
|
|
2151
|
-
*/
|
|
2152
|
-
const fetchIndicesDailyOpenClose = async (indicesTicker, date, options) => {
|
|
2153
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2154
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v1/open-close/${encodeURIComponent(indicesTicker)}/${date}`);
|
|
2155
|
-
const queryParams = new URLSearchParams();
|
|
2156
|
-
queryParams.append('apiKey', apiKey);
|
|
2157
|
-
url.search = queryParams.toString();
|
|
2158
|
-
return polygonIndicesLimit(async () => {
|
|
2159
|
-
try {
|
|
2160
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2161
|
-
const data = await response.json();
|
|
2162
|
-
if (data.status === 'ERROR') {
|
|
2163
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2164
|
-
}
|
|
2165
|
-
return data;
|
|
2166
|
-
}
|
|
2167
|
-
catch (error) {
|
|
2168
|
-
console.error('Error fetching indices daily open/close:', error);
|
|
2169
|
-
throw error;
|
|
2170
|
-
}
|
|
2171
|
-
});
|
|
2172
|
-
};
|
|
2173
|
-
/**
|
|
2174
|
-
* Gets a snapshot of indices data for specified tickers.
|
|
2175
|
-
*
|
|
2176
|
-
* @param {PolygonIndicesSnapshotParams} [params] - Parameters for the snapshot request
|
|
2177
|
-
* @param {Object} [options] - Optional parameters
|
|
2178
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2179
|
-
* @returns {Promise<PolygonIndicesSnapshotResponse>} The indices snapshot response
|
|
2180
|
-
*/
|
|
2181
|
-
const fetchIndicesSnapshot = async (params, options) => {
|
|
2182
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2183
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot/indices`);
|
|
2184
|
-
const queryParams = new URLSearchParams();
|
|
2185
|
-
queryParams.append('apiKey', apiKey);
|
|
2186
|
-
if (params?.tickers?.length) {
|
|
2187
|
-
queryParams.append('ticker.any_of', params.tickers.join(','));
|
|
2188
|
-
}
|
|
2189
|
-
if (params?.order) {
|
|
2190
|
-
queryParams.append('order', params.order);
|
|
2191
|
-
}
|
|
2192
|
-
if (params?.limit) {
|
|
2193
|
-
queryParams.append('limit', params.limit.toString());
|
|
2194
|
-
}
|
|
2195
|
-
if (params?.sort) {
|
|
2196
|
-
queryParams.append('sort', params.sort);
|
|
2197
|
-
}
|
|
2198
|
-
url.search = queryParams.toString();
|
|
2199
|
-
return polygonIndicesLimit(async () => {
|
|
2200
|
-
try {
|
|
2201
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2202
|
-
const data = await response.json();
|
|
2203
|
-
if (data.status === 'ERROR') {
|
|
2204
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2205
|
-
}
|
|
2206
|
-
return data;
|
|
2207
|
-
}
|
|
2208
|
-
catch (error) {
|
|
2209
|
-
console.error('Error fetching indices snapshot:', error);
|
|
2210
|
-
throw error;
|
|
2211
|
-
}
|
|
2212
|
-
});
|
|
2213
|
-
};
|
|
2214
|
-
/**
|
|
2215
|
-
* Gets snapshots for assets of all types, including indices.
|
|
2216
|
-
*
|
|
2217
|
-
* @param {string[]} tickers - Array of tickers to fetch snapshots for
|
|
2218
|
-
* @param {Object} [options] - Optional parameters
|
|
2219
|
-
* @param {string} [options.apiKey] - API key to use for the request
|
|
2220
|
-
* @param {string} [options.type] - Filter by asset type
|
|
2221
|
-
* @param {string} [options.order] - Order results
|
|
2222
|
-
* @param {number} [options.limit] - Limit the number of results
|
|
2223
|
-
* @param {string} [options.sort] - Sort field
|
|
2224
|
-
* @returns {Promise<any>} The universal snapshot response
|
|
2225
|
-
*/
|
|
2226
|
-
const fetchUniversalSnapshot = async (tickers, options) => {
|
|
2227
|
-
const apiKey = validateApiKey(options?.apiKey);
|
|
2228
|
-
const url = new URL(`${POLYGON_API_BASE_URL}/v3/snapshot`);
|
|
2229
|
-
const queryParams = new URLSearchParams();
|
|
2230
|
-
queryParams.append('apiKey', apiKey);
|
|
2231
|
-
if (tickers.length) {
|
|
2232
|
-
queryParams.append('ticker.any_of', tickers.join(','));
|
|
2233
|
-
}
|
|
2234
|
-
if (options?.type) {
|
|
2235
|
-
queryParams.append('type', options.type);
|
|
2236
|
-
}
|
|
2237
|
-
if (options?.order) {
|
|
2238
|
-
queryParams.append('order', options.order);
|
|
2239
|
-
}
|
|
2240
|
-
if (options?.limit) {
|
|
2241
|
-
queryParams.append('limit', options.limit.toString());
|
|
2242
|
-
}
|
|
2243
|
-
if (options?.sort) {
|
|
2244
|
-
queryParams.append('sort', options.sort);
|
|
2245
|
-
}
|
|
2246
|
-
url.search = queryParams.toString();
|
|
2247
|
-
return polygonIndicesLimit(async () => {
|
|
2248
|
-
try {
|
|
2249
|
-
const response = await fetchWithRetry(url.toString(), {}, 3, 300);
|
|
2250
|
-
const data = await response.json();
|
|
2251
|
-
if (data.status === 'ERROR') {
|
|
2252
|
-
throw new Error(`Polygon API Error: ${data.error}`);
|
|
2253
|
-
}
|
|
2254
|
-
return data;
|
|
2255
|
-
}
|
|
2256
|
-
catch (error) {
|
|
2257
|
-
console.error('Error fetching universal snapshot:', error);
|
|
2258
|
-
throw error;
|
|
2259
|
-
}
|
|
2260
|
-
});
|
|
2261
|
-
};
|
|
2262
|
-
/**
|
|
2263
|
-
* Converts Polygon Indices bar data to a more standardized format
|
|
2264
|
-
*
|
|
2265
|
-
* @param {PolygonIndicesAggregatesResponse} data - The raw aggregates response
|
|
2266
|
-
* @returns {Array<{date: string, open: number, high: number, low: number, close: number, timestamp: number}>} Formatted bar data
|
|
2267
|
-
*/
|
|
2268
|
-
const formatIndicesBarData = (data) => {
|
|
2269
|
-
return data.results.map((bar) => {
|
|
2270
|
-
const date = new Date(bar.t);
|
|
2271
|
-
return {
|
|
2272
|
-
date: date.toISOString().split('T')[0],
|
|
2273
|
-
open: bar.o,
|
|
2274
|
-
high: bar.h,
|
|
2275
|
-
low: bar.l,
|
|
2276
|
-
close: bar.c,
|
|
2277
|
-
timestamp: bar.t,
|
|
2278
|
-
};
|
|
2279
|
-
});
|
|
2280
|
-
};
|
|
585
|
+
pLimit(POLYGON_INDICES_CONCURRENCY_LIMIT);
|
|
2281
586
|
|
|
2282
587
|
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
2283
588
|
if (typeof state === "function" ? receiver !== state || true : !state.has(receiver))
|
|
@@ -7894,9 +6199,9 @@ function maybeParseResponse(response, params) {
|
|
|
7894
6199
|
}),
|
|
7895
6200
|
};
|
|
7896
6201
|
}
|
|
7897
|
-
return parseResponse
|
|
6202
|
+
return parseResponse(response, params);
|
|
7898
6203
|
}
|
|
7899
|
-
function parseResponse
|
|
6204
|
+
function parseResponse(response, params) {
|
|
7900
6205
|
const output = response.output.map((item) => {
|
|
7901
6206
|
if (item.type === 'function_call') {
|
|
7902
6207
|
return {
|
|
@@ -8328,7 +6633,7 @@ class Responses extends APIResource {
|
|
|
8328
6633
|
parse(body, options) {
|
|
8329
6634
|
return this._client.responses
|
|
8330
6635
|
.create(body, options)
|
|
8331
|
-
._thenUnwrap((response) => parseResponse
|
|
6636
|
+
._thenUnwrap((response) => parseResponse(response, body));
|
|
8332
6637
|
}
|
|
8333
6638
|
/**
|
|
8334
6639
|
* Creates a model response stream
|
|
@@ -9326,2033 +7631,129 @@ class OpenAI {
|
|
|
9326
7631
|
const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
|
|
9327
7632
|
// Apply some jitter, take up to at most 25 percent of the retry time.
|
|
9328
7633
|
const jitter = 1 - Math.random() * 0.25;
|
|
9329
|
-
return sleepSeconds * jitter * 1000;
|
|
9330
|
-
}
|
|
9331
|
-
async buildRequest(inputOptions, { retryCount = 0 } = {}) {
|
|
9332
|
-
const options = { ...inputOptions };
|
|
9333
|
-
const { method, path, query, defaultBaseURL } = options;
|
|
9334
|
-
const url = this.buildURL(path, query, defaultBaseURL);
|
|
9335
|
-
if ('timeout' in options)
|
|
9336
|
-
validatePositiveInteger('timeout', options.timeout);
|
|
9337
|
-
options.timeout = options.timeout ?? this.timeout;
|
|
9338
|
-
const { bodyHeaders, body } = this.buildBody({ options });
|
|
9339
|
-
const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
|
|
9340
|
-
const req = {
|
|
9341
|
-
method,
|
|
9342
|
-
headers: reqHeaders,
|
|
9343
|
-
...(options.signal && { signal: options.signal }),
|
|
9344
|
-
...(globalThis.ReadableStream &&
|
|
9345
|
-
body instanceof globalThis.ReadableStream && { duplex: 'half' }),
|
|
9346
|
-
...(body && { body }),
|
|
9347
|
-
...(this.fetchOptions ?? {}),
|
|
9348
|
-
...(options.fetchOptions ?? {}),
|
|
9349
|
-
};
|
|
9350
|
-
return { req, url, timeout: options.timeout };
|
|
9351
|
-
}
|
|
9352
|
-
async buildHeaders({ options, method, bodyHeaders, retryCount, }) {
|
|
9353
|
-
let idempotencyHeaders = {};
|
|
9354
|
-
if (this.idempotencyHeader && method !== 'get') {
|
|
9355
|
-
if (!options.idempotencyKey)
|
|
9356
|
-
options.idempotencyKey = this.defaultIdempotencyKey();
|
|
9357
|
-
idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
|
|
9358
|
-
}
|
|
9359
|
-
const headers = buildHeaders([
|
|
9360
|
-
idempotencyHeaders,
|
|
9361
|
-
{
|
|
9362
|
-
Accept: 'application/json',
|
|
9363
|
-
'User-Agent': this.getUserAgent(),
|
|
9364
|
-
'X-Stainless-Retry-Count': String(retryCount),
|
|
9365
|
-
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
|
|
9366
|
-
...getPlatformHeaders(),
|
|
9367
|
-
'OpenAI-Organization': this.organization,
|
|
9368
|
-
'OpenAI-Project': this.project,
|
|
9369
|
-
},
|
|
9370
|
-
await this.authHeaders(options),
|
|
9371
|
-
this._options.defaultHeaders,
|
|
9372
|
-
bodyHeaders,
|
|
9373
|
-
options.headers,
|
|
9374
|
-
]);
|
|
9375
|
-
this.validateHeaders(headers);
|
|
9376
|
-
return headers.values;
|
|
9377
|
-
}
|
|
9378
|
-
buildBody({ options: { body, headers: rawHeaders } }) {
|
|
9379
|
-
if (!body) {
|
|
9380
|
-
return { bodyHeaders: undefined, body: undefined };
|
|
9381
|
-
}
|
|
9382
|
-
const headers = buildHeaders([rawHeaders]);
|
|
9383
|
-
if (
|
|
9384
|
-
// Pass raw type verbatim
|
|
9385
|
-
ArrayBuffer.isView(body) ||
|
|
9386
|
-
body instanceof ArrayBuffer ||
|
|
9387
|
-
body instanceof DataView ||
|
|
9388
|
-
(typeof body === 'string' &&
|
|
9389
|
-
// Preserve legacy string encoding behavior for now
|
|
9390
|
-
headers.values.has('content-type')) ||
|
|
9391
|
-
// `Blob` is superset of `File`
|
|
9392
|
-
(globalThis.Blob && body instanceof globalThis.Blob) ||
|
|
9393
|
-
// `FormData` -> `multipart/form-data`
|
|
9394
|
-
body instanceof FormData ||
|
|
9395
|
-
// `URLSearchParams` -> `application/x-www-form-urlencoded`
|
|
9396
|
-
body instanceof URLSearchParams ||
|
|
9397
|
-
// Send chunked stream (each chunk has own `length`)
|
|
9398
|
-
(globalThis.ReadableStream && body instanceof globalThis.ReadableStream)) {
|
|
9399
|
-
return { bodyHeaders: undefined, body: body };
|
|
9400
|
-
}
|
|
9401
|
-
else if (typeof body === 'object' &&
|
|
9402
|
-
(Symbol.asyncIterator in body ||
|
|
9403
|
-
(Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))) {
|
|
9404
|
-
return { bodyHeaders: undefined, body: ReadableStreamFrom(body) };
|
|
9405
|
-
}
|
|
9406
|
-
else {
|
|
9407
|
-
return __classPrivateFieldGet(this, _OpenAI_encoder, "f").call(this, { body, headers });
|
|
9408
|
-
}
|
|
9409
|
-
}
|
|
9410
|
-
}
|
|
9411
|
-
_a = OpenAI, _OpenAI_encoder = new WeakMap(), _OpenAI_instances = new WeakSet(), _OpenAI_baseURLOverridden = function _OpenAI_baseURLOverridden() {
|
|
9412
|
-
return this.baseURL !== 'https://api.openai.com/v1';
|
|
9413
|
-
};
|
|
9414
|
-
OpenAI.OpenAI = _a;
|
|
9415
|
-
OpenAI.DEFAULT_TIMEOUT = 600000; // 10 minutes
|
|
9416
|
-
OpenAI.OpenAIError = OpenAIError;
|
|
9417
|
-
OpenAI.APIError = APIError;
|
|
9418
|
-
OpenAI.APIConnectionError = APIConnectionError;
|
|
9419
|
-
OpenAI.APIConnectionTimeoutError = APIConnectionTimeoutError;
|
|
9420
|
-
OpenAI.APIUserAbortError = APIUserAbortError;
|
|
9421
|
-
OpenAI.NotFoundError = NotFoundError;
|
|
9422
|
-
OpenAI.ConflictError = ConflictError;
|
|
9423
|
-
OpenAI.RateLimitError = RateLimitError;
|
|
9424
|
-
OpenAI.BadRequestError = BadRequestError;
|
|
9425
|
-
OpenAI.AuthenticationError = AuthenticationError;
|
|
9426
|
-
OpenAI.InternalServerError = InternalServerError;
|
|
9427
|
-
OpenAI.PermissionDeniedError = PermissionDeniedError;
|
|
9428
|
-
OpenAI.UnprocessableEntityError = UnprocessableEntityError;
|
|
9429
|
-
OpenAI.InvalidWebhookSignatureError = InvalidWebhookSignatureError;
|
|
9430
|
-
OpenAI.toFile = toFile;
|
|
9431
|
-
OpenAI.Completions = Completions;
|
|
9432
|
-
OpenAI.Chat = Chat;
|
|
9433
|
-
OpenAI.Embeddings = Embeddings;
|
|
9434
|
-
OpenAI.Files = Files$1;
|
|
9435
|
-
OpenAI.Images = Images;
|
|
9436
|
-
OpenAI.Audio = Audio;
|
|
9437
|
-
OpenAI.Moderations = Moderations;
|
|
9438
|
-
OpenAI.Models = Models;
|
|
9439
|
-
OpenAI.FineTuning = FineTuning;
|
|
9440
|
-
OpenAI.Graders = Graders;
|
|
9441
|
-
OpenAI.VectorStores = VectorStores;
|
|
9442
|
-
OpenAI.Webhooks = Webhooks;
|
|
9443
|
-
OpenAI.Beta = Beta;
|
|
9444
|
-
OpenAI.Batches = Batches;
|
|
9445
|
-
OpenAI.Uploads = Uploads;
|
|
9446
|
-
OpenAI.Responses = Responses;
|
|
9447
|
-
OpenAI.Realtime = Realtime;
|
|
9448
|
-
OpenAI.Conversations = Conversations;
|
|
9449
|
-
OpenAI.Evals = Evals;
|
|
9450
|
-
OpenAI.Containers = Containers;
|
|
9451
|
-
OpenAI.Videos = Videos;
|
|
9452
|
-
|
|
9453
|
-
// llm-openai-config.ts
|
|
9454
|
-
const DEFAULT_MODEL = 'gpt-4.1-mini';
|
|
9455
|
-
/** Token costs in USD per 1M tokens. Last updated Feb 2025. */
|
|
9456
|
-
const openAiModelCosts = {
|
|
9457
|
-
'gpt-4o': {
|
|
9458
|
-
inputCost: 2.5 / 1_000_000,
|
|
9459
|
-
outputCost: 10 / 1_000_000,
|
|
9460
|
-
},
|
|
9461
|
-
'gpt-4o-mini': {
|
|
9462
|
-
inputCost: 0.15 / 1_000_000,
|
|
9463
|
-
outputCost: 0.6 / 1_000_000,
|
|
9464
|
-
},
|
|
9465
|
-
'o1-mini': {
|
|
9466
|
-
inputCost: 1.1 / 1_000_000,
|
|
9467
|
-
outputCost: 4.4 / 1_000_000,
|
|
9468
|
-
},
|
|
9469
|
-
'o1': {
|
|
9470
|
-
inputCost: 15 / 1_000_000,
|
|
9471
|
-
outputCost: 60 / 1_000_000,
|
|
9472
|
-
},
|
|
9473
|
-
'o3-mini': {
|
|
9474
|
-
inputCost: 1.1 / 1_000_000,
|
|
9475
|
-
outputCost: 4.4 / 1_000_000,
|
|
9476
|
-
},
|
|
9477
|
-
'o3': {
|
|
9478
|
-
inputCost: 2 / 1_000_000,
|
|
9479
|
-
outputCost: 8 / 1_000_000,
|
|
9480
|
-
},
|
|
9481
|
-
'gpt-4.1': {
|
|
9482
|
-
inputCost: 2 / 1_000_000,
|
|
9483
|
-
outputCost: 8 / 1_000_000,
|
|
9484
|
-
},
|
|
9485
|
-
'gpt-4.1-mini': {
|
|
9486
|
-
inputCost: 0.4 / 1_000_000,
|
|
9487
|
-
outputCost: 1.6 / 1_000_000,
|
|
9488
|
-
},
|
|
9489
|
-
'gpt-4.1-nano': {
|
|
9490
|
-
inputCost: 0.1 / 1_000_000,
|
|
9491
|
-
outputCost: 0.4 / 1_000_000,
|
|
9492
|
-
},
|
|
9493
|
-
'gpt-5': {
|
|
9494
|
-
inputCost: 1.25 / 1_000_000,
|
|
9495
|
-
outputCost: 10 / 1_000_000,
|
|
9496
|
-
},
|
|
9497
|
-
'gpt-5-mini': {
|
|
9498
|
-
inputCost: 0.25 / 1_000_000,
|
|
9499
|
-
outputCost: 2 / 1_000_000,
|
|
9500
|
-
},
|
|
9501
|
-
'gpt-5-nano': {
|
|
9502
|
-
inputCost: 0.05 / 1_000_000,
|
|
9503
|
-
outputCost: 0.4 / 1_000_000,
|
|
9504
|
-
},
|
|
9505
|
-
'gpt-5.1': {
|
|
9506
|
-
inputCost: 1.25 / 1_000_000,
|
|
9507
|
-
outputCost: 10 / 1_000_000,
|
|
9508
|
-
},
|
|
9509
|
-
'gpt-5.2': {
|
|
9510
|
-
inputCost: 1.5 / 1_000_000,
|
|
9511
|
-
outputCost: 12 / 1_000_000,
|
|
9512
|
-
},
|
|
9513
|
-
'gpt-5.2-pro': {
|
|
9514
|
-
inputCost: 3 / 1_000_000,
|
|
9515
|
-
outputCost: 24 / 1_000_000,
|
|
9516
|
-
},
|
|
9517
|
-
'gpt-5.1-codex': {
|
|
9518
|
-
inputCost: 1.1 / 1_000_000,
|
|
9519
|
-
outputCost: 8.8 / 1_000_000,
|
|
9520
|
-
},
|
|
9521
|
-
'gpt-5.1-codex-max': {
|
|
9522
|
-
inputCost: 1.8 / 1_000_000,
|
|
9523
|
-
outputCost: 14.4 / 1_000_000,
|
|
9524
|
-
},
|
|
9525
|
-
'o4-mini': {
|
|
9526
|
-
inputCost: 1.1 / 1_000_000,
|
|
9527
|
-
outputCost: 4.4 / 1_000_000,
|
|
9528
|
-
},
|
|
9529
|
-
};
|
|
9530
|
-
const deepseekModelCosts = {
|
|
9531
|
-
'deepseek-chat': {
|
|
9532
|
-
inputCost: 0.27 / 1_000_000, // $0.27 per 1M tokens (Cache miss price)
|
|
9533
|
-
cacheHitCost: 0.07 / 1_000_000, // $0.07 per 1M tokens (Cache hit price)
|
|
9534
|
-
outputCost: 1.1 / 1_000_000, // $1.10 per 1M tokens
|
|
9535
|
-
},
|
|
9536
|
-
'deepseek-reasoner': {
|
|
9537
|
-
inputCost: 0.55 / 1_000_000, // $0.55 per 1M tokens (Cache miss price)
|
|
9538
|
-
cacheHitCost: 0.14 / 1_000_000, // $0.14 per 1M tokens (Cache hit price)
|
|
9539
|
-
outputCost: 2.19 / 1_000_000, // $2.19 per 1M tokens
|
|
9540
|
-
},
|
|
9541
|
-
};
|
|
9542
|
-
/** Image generation costs in USD per image. Based on OpenAI pricing as of Feb 2025. */
|
|
9543
|
-
const openAiImageCosts = {
|
|
9544
|
-
'gpt-image-1': 0.0075, // $0.0075 per image for gpt-image-1
|
|
9545
|
-
'gpt-image-1.5': 0.0075, // Assumes parity pricing with gpt-image-1 until OpenAI publishes updated rates
|
|
9546
|
-
};
|
|
9547
|
-
/**
|
|
9548
|
-
* Calculates the cost of generating images using OpenAI's Images API.
|
|
9549
|
-
*
|
|
9550
|
-
* @param model The image generation model name.
|
|
9551
|
-
* @param imageCount The number of images generated.
|
|
9552
|
-
* @returns The cost of generating the images in USD.
|
|
9553
|
-
*/
|
|
9554
|
-
function calculateImageCost(model, imageCount) {
|
|
9555
|
-
if (typeof model !== 'string' || typeof imageCount !== 'number' || imageCount <= 0) {
|
|
9556
|
-
return 0;
|
|
9557
|
-
}
|
|
9558
|
-
const costPerImage = openAiImageCosts[model];
|
|
9559
|
-
if (!costPerImage)
|
|
9560
|
-
return 0;
|
|
9561
|
-
return imageCount * costPerImage;
|
|
9562
|
-
}
|
|
9563
|
-
/**
|
|
9564
|
-
* Calculates the cost of calling a language model in USD based on the provider and model, tokens, and given costs per 1M tokens.
|
|
9565
|
-
*
|
|
9566
|
-
* @param provider The provider of the language model. Supported providers are 'openai' and 'deepseek'.
|
|
9567
|
-
* @param model The name of the language model. Supported models are listed in the `openAiModelCosts` and `deepseekModelCosts` objects.
|
|
9568
|
-
* @param inputTokens The number of input tokens passed to the language model.
|
|
9569
|
-
* @param outputTokens The number of output tokens generated by the language model.
|
|
9570
|
-
* @param reasoningTokens The number of output tokens generated by the language model for reasoning. This is only used for Deepseek models.
|
|
9571
|
-
* @param cacheHitTokens The number of input tokens that were cache hits for Deepseek models.
|
|
9572
|
-
* @returns The cost of calling the language model in USD.
|
|
9573
|
-
*/
|
|
9574
|
-
function calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens) {
|
|
9575
|
-
if (typeof provider !== 'string' ||
|
|
9576
|
-
typeof model !== 'string' ||
|
|
9577
|
-
typeof inputTokens !== 'number' ||
|
|
9578
|
-
typeof outputTokens !== 'number' ||
|
|
9579
|
-
(reasoningTokens !== undefined && typeof reasoningTokens !== 'number') ||
|
|
9580
|
-
(cacheHitTokens !== undefined && typeof cacheHitTokens !== 'number')) {
|
|
9581
|
-
return 0;
|
|
9582
|
-
}
|
|
9583
|
-
const modelCosts = provider === 'deepseek' ? deepseekModelCosts[model] : openAiModelCosts[model];
|
|
9584
|
-
if (!modelCosts)
|
|
9585
|
-
return 0;
|
|
9586
|
-
// Calculate input cost based on cache hit/miss for Deepseek
|
|
9587
|
-
const inputCost = provider === 'deepseek' && modelCosts.cacheHitCost
|
|
9588
|
-
? (cacheHitTokens || 0) * modelCosts.cacheHitCost + (inputTokens - (cacheHitTokens || 0)) * modelCosts.inputCost
|
|
9589
|
-
: inputTokens * modelCosts.inputCost;
|
|
9590
|
-
const outputCost = outputTokens * modelCosts.outputCost;
|
|
9591
|
-
const reasoningCost = (reasoningTokens || 0) * modelCosts.outputCost;
|
|
9592
|
-
return inputCost + outputCost + reasoningCost;
|
|
9593
|
-
}
|
|
9594
|
-
|
|
9595
|
-
/**
|
|
9596
|
-
* 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.
|
|
9597
|
-
* @param {string} jsonStr - The broken JSON string to fix
|
|
9598
|
-
* @returns {JsonValue} - The parsed JSON value
|
|
9599
|
-
*/
|
|
9600
|
-
function fixBrokenJson(jsonStr) {
|
|
9601
|
-
// Pre-process: Fix improperly escaped $ signs
|
|
9602
|
-
jsonStr = jsonStr.replace(/\\\$/g, '$');
|
|
9603
|
-
let index = 0;
|
|
9604
|
-
function parse() {
|
|
9605
|
-
const results = [];
|
|
9606
|
-
while (index < jsonStr.length) {
|
|
9607
|
-
skipWhitespace();
|
|
9608
|
-
const value = parseValue();
|
|
9609
|
-
if (value !== undefined) {
|
|
9610
|
-
results.push(value);
|
|
9611
|
-
}
|
|
9612
|
-
else {
|
|
9613
|
-
index++; // Skip invalid character
|
|
9614
|
-
}
|
|
9615
|
-
}
|
|
9616
|
-
return results.length === 1 ? results[0] : results;
|
|
9617
|
-
}
|
|
9618
|
-
function parseValue() {
|
|
9619
|
-
skipWhitespace();
|
|
9620
|
-
const char = getChar();
|
|
9621
|
-
if (!char)
|
|
9622
|
-
return undefined;
|
|
9623
|
-
if (char === '{')
|
|
9624
|
-
return parseObject();
|
|
9625
|
-
if (char === '[')
|
|
9626
|
-
return parseArray();
|
|
9627
|
-
if (char === '"' || char === "'")
|
|
9628
|
-
return parseString();
|
|
9629
|
-
if (char === 't' && jsonStr.slice(index, index + 4).toLowerCase() === 'true') {
|
|
9630
|
-
index += 4;
|
|
9631
|
-
return true;
|
|
9632
|
-
}
|
|
9633
|
-
if (char === 'f' && jsonStr.slice(index, index + 5).toLowerCase() === 'false') {
|
|
9634
|
-
index += 5;
|
|
9635
|
-
return false;
|
|
9636
|
-
}
|
|
9637
|
-
if (char === 'n' && jsonStr.slice(index, index + 4).toLowerCase() === 'null') {
|
|
9638
|
-
index += 4;
|
|
9639
|
-
return null;
|
|
9640
|
-
}
|
|
9641
|
-
if (/[a-zA-Z]/.test(char))
|
|
9642
|
-
return parseString(); // Unquoted string
|
|
9643
|
-
if (char === '-' || char === '.' || /\d/.test(char))
|
|
9644
|
-
return parseNumber();
|
|
9645
|
-
return undefined; // Unknown character
|
|
9646
|
-
}
|
|
9647
|
-
function parseObject() {
|
|
9648
|
-
const obj = {};
|
|
9649
|
-
index++; // Skip opening brace
|
|
9650
|
-
skipWhitespace();
|
|
9651
|
-
while (index < jsonStr.length && getChar() !== '}') {
|
|
9652
|
-
skipWhitespace();
|
|
9653
|
-
const key = parseString();
|
|
9654
|
-
if (key === undefined) {
|
|
9655
|
-
console.warn(`Expected key at position ${index}`);
|
|
9656
|
-
index++;
|
|
9657
|
-
continue;
|
|
9658
|
-
}
|
|
9659
|
-
skipWhitespace();
|
|
9660
|
-
if (getChar() === ':') {
|
|
9661
|
-
index++; // Skip colon
|
|
9662
|
-
}
|
|
9663
|
-
else {
|
|
9664
|
-
console.warn(`Missing colon after key "${key}" at position ${index}`);
|
|
9665
|
-
}
|
|
9666
|
-
skipWhitespace();
|
|
9667
|
-
const value = parseValue();
|
|
9668
|
-
if (value === undefined) {
|
|
9669
|
-
console.warn(`Expected value for key "${key}" at position ${index}`);
|
|
9670
|
-
index++;
|
|
9671
|
-
continue;
|
|
9672
|
-
}
|
|
9673
|
-
obj[key] = value;
|
|
9674
|
-
skipWhitespace();
|
|
9675
|
-
if (getChar() === ',') {
|
|
9676
|
-
index++; // Skip comma
|
|
9677
|
-
}
|
|
9678
|
-
else {
|
|
9679
|
-
break;
|
|
9680
|
-
}
|
|
9681
|
-
skipWhitespace();
|
|
9682
|
-
}
|
|
9683
|
-
if (getChar() === '}') {
|
|
9684
|
-
index++; // Skip closing brace
|
|
9685
|
-
}
|
|
9686
|
-
else {
|
|
9687
|
-
// Add a closing brace if it's missing
|
|
9688
|
-
jsonStr += '}';
|
|
9689
|
-
}
|
|
9690
|
-
return obj;
|
|
9691
|
-
}
|
|
9692
|
-
function parseArray() {
|
|
9693
|
-
const arr = [];
|
|
9694
|
-
index++; // Skip opening bracket
|
|
9695
|
-
skipWhitespace();
|
|
9696
|
-
while (index < jsonStr.length && getChar() !== ']') {
|
|
9697
|
-
const value = parseValue();
|
|
9698
|
-
if (value === undefined) {
|
|
9699
|
-
console.warn(`Expected value at position ${index}`);
|
|
9700
|
-
index++;
|
|
9701
|
-
}
|
|
9702
|
-
else {
|
|
9703
|
-
arr.push(value);
|
|
9704
|
-
}
|
|
9705
|
-
skipWhitespace();
|
|
9706
|
-
if (getChar() === ',') {
|
|
9707
|
-
index++; // Skip comma
|
|
9708
|
-
}
|
|
9709
|
-
else {
|
|
9710
|
-
break;
|
|
9711
|
-
}
|
|
9712
|
-
skipWhitespace();
|
|
9713
|
-
}
|
|
9714
|
-
if (getChar() === ']') {
|
|
9715
|
-
index++; // Skip closing bracket
|
|
9716
|
-
}
|
|
9717
|
-
else {
|
|
9718
|
-
console.warn(`Missing closing bracket for array at position ${index}`);
|
|
9719
|
-
}
|
|
9720
|
-
return arr;
|
|
9721
|
-
}
|
|
9722
|
-
function parseString() {
|
|
9723
|
-
const startChar = getChar();
|
|
9724
|
-
let result = '';
|
|
9725
|
-
let isQuoted = false;
|
|
9726
|
-
let delimiter = '';
|
|
9727
|
-
if (startChar === '"' || startChar === "'") {
|
|
9728
|
-
isQuoted = true;
|
|
9729
|
-
delimiter = startChar;
|
|
9730
|
-
index++; // Skip opening quote
|
|
9731
|
-
}
|
|
9732
|
-
while (index < jsonStr.length) {
|
|
9733
|
-
const char = getChar();
|
|
9734
|
-
if (isQuoted) {
|
|
9735
|
-
if (char === delimiter) {
|
|
9736
|
-
index++; // Skip closing quote
|
|
9737
|
-
return result;
|
|
9738
|
-
}
|
|
9739
|
-
else if (char === '\\') {
|
|
9740
|
-
index++;
|
|
9741
|
-
const escapeChar = getChar();
|
|
9742
|
-
const escapeSequences = {
|
|
9743
|
-
n: '\n',
|
|
9744
|
-
r: '\r',
|
|
9745
|
-
t: '\t',
|
|
9746
|
-
b: '\b',
|
|
9747
|
-
f: '\f',
|
|
9748
|
-
'\\': '\\',
|
|
9749
|
-
'/': '/',
|
|
9750
|
-
'"': '"',
|
|
9751
|
-
"'": "'",
|
|
9752
|
-
$: '$',
|
|
9753
|
-
};
|
|
9754
|
-
result += escapeSequences[escapeChar] || escapeChar;
|
|
9755
|
-
}
|
|
9756
|
-
else {
|
|
9757
|
-
result += char;
|
|
9758
|
-
}
|
|
9759
|
-
index++;
|
|
9760
|
-
}
|
|
9761
|
-
else {
|
|
9762
|
-
if (/\s|,|}|]|\:/.test(char)) {
|
|
9763
|
-
return result;
|
|
9764
|
-
}
|
|
9765
|
-
else {
|
|
9766
|
-
result += char;
|
|
9767
|
-
index++;
|
|
9768
|
-
}
|
|
9769
|
-
}
|
|
9770
|
-
}
|
|
9771
|
-
if (isQuoted) {
|
|
9772
|
-
console.warn(`Missing closing quote for string starting at position ${index}`);
|
|
9773
|
-
}
|
|
9774
|
-
return result;
|
|
9775
|
-
}
|
|
9776
|
-
function parseNumber() {
|
|
9777
|
-
const numberRegex = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/;
|
|
9778
|
-
const match = numberRegex.exec(jsonStr.slice(index));
|
|
9779
|
-
if (match) {
|
|
9780
|
-
index += match[0].length;
|
|
9781
|
-
return parseFloat(match[0]);
|
|
9782
|
-
}
|
|
9783
|
-
else {
|
|
9784
|
-
console.warn(`Invalid number at position ${index}`);
|
|
9785
|
-
index++;
|
|
9786
|
-
return undefined;
|
|
9787
|
-
}
|
|
9788
|
-
}
|
|
9789
|
-
function getChar() {
|
|
9790
|
-
return jsonStr[index];
|
|
9791
|
-
}
|
|
9792
|
-
function skipWhitespace() {
|
|
9793
|
-
while (/\s/.test(getChar()))
|
|
9794
|
-
index++;
|
|
9795
|
-
}
|
|
9796
|
-
try {
|
|
9797
|
-
return parse();
|
|
9798
|
-
}
|
|
9799
|
-
catch (error) {
|
|
9800
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
9801
|
-
console.error(`Error parsing JSON at position ${index}: ${msg}`);
|
|
9802
|
-
return null;
|
|
9803
|
-
}
|
|
9804
|
-
}
|
|
9805
|
-
/**
|
|
9806
|
-
* Returns true if the given JSON string is valid, false otherwise.
|
|
9807
|
-
*
|
|
9808
|
-
* This function simply attempts to parse the given string as JSON and returns true if successful, or false if an error is thrown.
|
|
9809
|
-
*
|
|
9810
|
-
* @param {string} jsonString The JSON string to validate.
|
|
9811
|
-
* @returns {boolean} True if the JSON string is valid, false otherwise.
|
|
9812
|
-
*/
|
|
9813
|
-
function isValidJson(jsonString) {
|
|
9814
|
-
try {
|
|
9815
|
-
JSON.parse(jsonString);
|
|
9816
|
-
return true;
|
|
9817
|
-
}
|
|
9818
|
-
catch {
|
|
9819
|
-
return false;
|
|
9820
|
-
}
|
|
9821
|
-
}
|
|
9822
|
-
let openai;
|
|
9823
|
-
/**
|
|
9824
|
-
* 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.
|
|
9825
|
-
* @param apiKey - the API key to use, or undefined to use the value of the OPENAI_API_KEY environment variable
|
|
9826
|
-
* @returns an instance of the OpenAI client
|
|
9827
|
-
* @throws an error if the API key is not provided and the OPENAI_API_KEY environment variable is not set
|
|
9828
|
-
*/
|
|
9829
|
-
function initializeOpenAI(apiKey) {
|
|
9830
|
-
const key = process.env.OPENAI_API_KEY;
|
|
9831
|
-
if (!key) {
|
|
9832
|
-
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
9833
|
-
}
|
|
9834
|
-
return new OpenAI({
|
|
9835
|
-
apiKey: key,
|
|
9836
|
-
defaultHeaders: { 'User-Agent': 'My Server-side Application (compatible; OpenAI API Client)' },
|
|
9837
|
-
});
|
|
9838
|
-
}
|
|
9839
|
-
/**
|
|
9840
|
-
* Fixes broken JSON by sending it to OpenAI to fix it.
|
|
9841
|
-
* If the model fails to return valid JSON, an error is thrown.
|
|
9842
|
-
* @param jsonStr - the broken JSON to fix
|
|
9843
|
-
* @param apiKey - the OpenAI API key to use, or undefined to use the value of the OPENAI_API_KEY environment variable
|
|
9844
|
-
* @returns the fixed JSON
|
|
9845
|
-
* @throws an error if the model fails to return valid JSON
|
|
9846
|
-
*/
|
|
9847
|
-
async function fixJsonWithAI(jsonStr, apiKey) {
|
|
9848
|
-
try {
|
|
9849
|
-
if (!openai) {
|
|
9850
|
-
openai = initializeOpenAI();
|
|
9851
|
-
}
|
|
9852
|
-
const completion = await openai.chat.completions.create({
|
|
9853
|
-
model: DEFAULT_MODEL,
|
|
9854
|
-
messages: [
|
|
9855
|
-
{
|
|
9856
|
-
role: 'system',
|
|
9857
|
-
content: 'You are a JSON fixer. Return only valid JSON without any additional text or explanation.',
|
|
9858
|
-
},
|
|
9859
|
-
{
|
|
9860
|
-
role: 'user',
|
|
9861
|
-
content: `Fix this broken JSON:\n${jsonStr}`,
|
|
9862
|
-
},
|
|
9863
|
-
],
|
|
9864
|
-
response_format: { type: 'json_object' },
|
|
9865
|
-
});
|
|
9866
|
-
const fixedJson = completion.choices[0]?.message?.content;
|
|
9867
|
-
if (fixedJson && isValidJson(fixedJson)) {
|
|
9868
|
-
return JSON.parse(fixedJson);
|
|
9869
|
-
}
|
|
9870
|
-
throw new Error('Failed to fix JSON with AI');
|
|
9871
|
-
}
|
|
9872
|
-
catch (err) {
|
|
9873
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
9874
|
-
throw new Error(`Error fixing JSON with AI: ${errorMessage}`);
|
|
9875
|
-
}
|
|
9876
|
-
}
|
|
9877
|
-
|
|
9878
|
-
// llm-utils.ts
|
|
9879
|
-
function normalizeModelName(model) {
|
|
9880
|
-
return model.replace(/_/g, '-').toLowerCase();
|
|
9881
|
-
}
|
|
9882
|
-
const CODE_BLOCK_TYPES = ['javascript', 'js', 'graphql', 'json', 'typescript', 'python', 'markdown', 'yaml'];
|
|
9883
|
-
/**
|
|
9884
|
-
* Tries to parse JSON from the given content string according to the specified
|
|
9885
|
-
* response format. If the response format is 'json' or a JSON schema object, it
|
|
9886
|
-
* will attempt to parse the content using multiple strategies. If any of the
|
|
9887
|
-
* strategies succeed, it will return the parsed JSON object. If all of them fail,
|
|
9888
|
-
* it will throw an error.
|
|
9889
|
-
*
|
|
9890
|
-
* @param content The content string to parse
|
|
9891
|
-
* @param responseFormat The desired format of the response
|
|
9892
|
-
* @returns The parsed JSON object or null if it fails
|
|
9893
|
-
*/
|
|
9894
|
-
async function parseResponse(content, responseFormat) {
|
|
9895
|
-
if (!content)
|
|
9896
|
-
return null;
|
|
9897
|
-
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat?.type === 'json_schema')) {
|
|
9898
|
-
let cleanedContent = content.trim();
|
|
9899
|
-
let detectedType = null;
|
|
9900
|
-
// Remove code block markers if present
|
|
9901
|
-
const startsWithCodeBlock = CODE_BLOCK_TYPES.some((type) => {
|
|
9902
|
-
if (cleanedContent.startsWith(`\`\`\`${type}`)) {
|
|
9903
|
-
detectedType = type;
|
|
9904
|
-
return true;
|
|
9905
|
-
}
|
|
9906
|
-
return false;
|
|
9907
|
-
});
|
|
9908
|
-
if (startsWithCodeBlock && cleanedContent.endsWith('```')) {
|
|
9909
|
-
const firstLineEndIndex = cleanedContent.indexOf('\n');
|
|
9910
|
-
if (firstLineEndIndex !== -1) {
|
|
9911
|
-
cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
|
|
9912
|
-
console.log(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
|
|
9913
|
-
}
|
|
9914
|
-
}
|
|
9915
|
-
// Multiple JSON parsing strategies
|
|
9916
|
-
const jsonParsingStrategies = [
|
|
9917
|
-
// Strategy 1: Direct parsing
|
|
9918
|
-
() => {
|
|
9919
|
-
try {
|
|
9920
|
-
return JSON.parse(cleanedContent);
|
|
9921
|
-
}
|
|
9922
|
-
catch {
|
|
9923
|
-
return null;
|
|
9924
|
-
}
|
|
9925
|
-
},
|
|
9926
|
-
// Strategy 2: Extract JSON from between first {} or []
|
|
9927
|
-
() => {
|
|
9928
|
-
const jsonMatch = cleanedContent.match(/[\{\[].*[\}\]]/s);
|
|
9929
|
-
if (jsonMatch) {
|
|
9930
|
-
try {
|
|
9931
|
-
return JSON.parse(jsonMatch[0]);
|
|
9932
|
-
}
|
|
9933
|
-
catch {
|
|
9934
|
-
return null;
|
|
9935
|
-
}
|
|
9936
|
-
}
|
|
9937
|
-
return null;
|
|
9938
|
-
},
|
|
9939
|
-
// Strategy 3: Remove leading/trailing text and try parsing
|
|
9940
|
-
() => {
|
|
9941
|
-
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, '');
|
|
9942
|
-
try {
|
|
9943
|
-
return JSON.parse(jsonMatch);
|
|
9944
|
-
}
|
|
9945
|
-
catch {
|
|
9946
|
-
return null;
|
|
9947
|
-
}
|
|
9948
|
-
},
|
|
9949
|
-
// Strategy 4: Use fixJsonWithAI
|
|
9950
|
-
() => {
|
|
9951
|
-
return fixJsonWithAI(cleanedContent);
|
|
9952
|
-
},
|
|
9953
|
-
// Strategy 5: Use fixBrokenJson
|
|
9954
|
-
() => {
|
|
9955
|
-
return fixBrokenJson(cleanedContent);
|
|
9956
|
-
},
|
|
9957
|
-
];
|
|
9958
|
-
// Try each parsing strategy
|
|
9959
|
-
for (const strategy of jsonParsingStrategies) {
|
|
9960
|
-
const result = await strategy();
|
|
9961
|
-
if (result !== null) {
|
|
9962
|
-
return result;
|
|
9963
|
-
}
|
|
9964
|
-
}
|
|
9965
|
-
// If all strategies fail, throw an error
|
|
9966
|
-
console.error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
|
|
9967
|
-
throw new Error('Unable to parse JSON response');
|
|
9968
|
-
}
|
|
9969
|
-
else {
|
|
9970
|
-
return content;
|
|
9971
|
-
}
|
|
9972
|
-
}
|
|
9973
|
-
|
|
9974
|
-
// llm-openai.ts
|
|
9975
|
-
const isSupportedModel = (model) => {
|
|
9976
|
-
return [
|
|
9977
|
-
'gpt-4o-mini',
|
|
9978
|
-
'gpt-4o',
|
|
9979
|
-
'o1-mini',
|
|
9980
|
-
'o1',
|
|
9981
|
-
'o3-mini',
|
|
9982
|
-
'gpt-4.1',
|
|
9983
|
-
'gpt-4.1-mini',
|
|
9984
|
-
'gpt-4.1-nano',
|
|
9985
|
-
'gpt-5',
|
|
9986
|
-
'gpt-5-mini',
|
|
9987
|
-
'gpt-5-nano',
|
|
9988
|
-
'gpt-5.1',
|
|
9989
|
-
'gpt-5.2',
|
|
9990
|
-
'gpt-5.2-pro',
|
|
9991
|
-
'gpt-5.1-codex',
|
|
9992
|
-
'gpt-5.1-codex-max',
|
|
9993
|
-
'o4-mini',
|
|
9994
|
-
'o3',
|
|
9995
|
-
].includes(model);
|
|
9996
|
-
};
|
|
9997
|
-
/**
|
|
9998
|
-
* Checks if the given model supports the temperature parameter. Reasoning models (o1*, o3*, o4*) do not support temperature.
|
|
9999
|
-
* @param model The model to check.
|
|
10000
|
-
* @returns True if the model supports the temperature parameter, false otherwise.
|
|
10001
|
-
*/
|
|
10002
|
-
function supportsTemperature(model) {
|
|
10003
|
-
// Reasoning models don't support temperature
|
|
10004
|
-
// GPT-5 models also do not support temperature
|
|
10005
|
-
const reasoningAndGPT5Models = [
|
|
10006
|
-
'o1',
|
|
10007
|
-
'o1-mini',
|
|
10008
|
-
'o3-mini',
|
|
10009
|
-
'o4-mini',
|
|
10010
|
-
'o3',
|
|
10011
|
-
'gpt-5',
|
|
10012
|
-
'gpt-5-mini',
|
|
10013
|
-
'gpt-5-nano',
|
|
10014
|
-
'gpt-5.1',
|
|
10015
|
-
'gpt-5.2',
|
|
10016
|
-
'gpt-5.2-pro',
|
|
10017
|
-
'gpt-5.1-codex',
|
|
10018
|
-
'gpt-5.1-codex-max',
|
|
10019
|
-
];
|
|
10020
|
-
return !reasoningAndGPT5Models.includes(model);
|
|
10021
|
-
}
|
|
10022
|
-
/**
|
|
10023
|
-
* Checks if the given model is a reasoning model. Reasoning models have different tool choice constraints.
|
|
10024
|
-
* @param model The model to check.
|
|
10025
|
-
* @returns True if the model is a reasoning model, false otherwise.
|
|
10026
|
-
*/
|
|
10027
|
-
function isReasoningModel(model) {
|
|
10028
|
-
const reasoningModels = ['o1', 'o1-mini', 'o3-mini', 'o4-mini', 'o3'];
|
|
10029
|
-
return reasoningModels.includes(model);
|
|
10030
|
-
}
|
|
10031
|
-
/**
|
|
10032
|
-
* Checks if the given model is a GPT-5 model. GPT-5 models don't support tool_choice other than 'auto'.
|
|
10033
|
-
* @param model The model to check.
|
|
10034
|
-
* @returns True if the model is a GPT-5 model, false otherwise.
|
|
10035
|
-
*/
|
|
10036
|
-
function isGPT5Model(model) {
|
|
10037
|
-
const gpt5Models = [
|
|
10038
|
-
'gpt-5',
|
|
10039
|
-
'gpt-5-mini',
|
|
10040
|
-
'gpt-5-nano',
|
|
10041
|
-
'gpt-5.1',
|
|
10042
|
-
'gpt-5.2',
|
|
10043
|
-
'gpt-5.2-pro',
|
|
10044
|
-
'gpt-5.1-codex',
|
|
10045
|
-
'gpt-5.1-codex-max',
|
|
10046
|
-
];
|
|
10047
|
-
return gpt5Models.includes(model);
|
|
10048
|
-
}
|
|
10049
|
-
/**
|
|
10050
|
-
* Makes a call to OpenAI's Responses API for more advanced use cases with built-in tools.
|
|
10051
|
-
*
|
|
10052
|
-
* This function provides access to the Responses API which supports:
|
|
10053
|
-
* - Built-in tools (web search, file search, computer use, code interpreter, image generation)
|
|
10054
|
-
* - Background processing
|
|
10055
|
-
* - Conversation state management
|
|
10056
|
-
* - Reasoning support for o-series models
|
|
10057
|
-
*
|
|
10058
|
-
* @example
|
|
10059
|
-
* // Basic text response
|
|
10060
|
-
* const response = await makeResponsesAPICall('What is the weather like?');
|
|
10061
|
-
*
|
|
10062
|
-
* @example
|
|
10063
|
-
* // With web search tool
|
|
10064
|
-
* const response = await makeResponsesAPICall('Latest news about AI', {
|
|
10065
|
-
* tools: [{ type: 'web_search_preview' }]
|
|
10066
|
-
* });
|
|
10067
|
-
*
|
|
10068
|
-
* @param input - The input content. Can be:
|
|
10069
|
-
* - A string for simple text prompts
|
|
10070
|
-
* - An array of input items for complex/multi-modal content
|
|
10071
|
-
* @param options - Configuration options for the Responses API
|
|
10072
|
-
* @returns Promise<LLMResponse<T>> - The response in the same format as makeLLMCall
|
|
10073
|
-
* @throws Error if the API call fails
|
|
10074
|
-
*/
|
|
10075
|
-
const makeResponsesAPICall = async (input, options = {}) => {
|
|
10076
|
-
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
10077
|
-
const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
|
|
10078
|
-
if (!apiKey) {
|
|
10079
|
-
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
10080
|
-
}
|
|
10081
|
-
const openai = new OpenAI({
|
|
10082
|
-
apiKey: apiKey,
|
|
10083
|
-
});
|
|
10084
|
-
// Remove apiKey from options before creating request body
|
|
10085
|
-
const { apiKey: _, model: __, ...cleanOptions } = options;
|
|
10086
|
-
const requestBody = {
|
|
10087
|
-
model: normalizedModel,
|
|
10088
|
-
input,
|
|
10089
|
-
...cleanOptions,
|
|
10090
|
-
};
|
|
10091
|
-
// Make the API call to the Responses endpoint
|
|
10092
|
-
const response = await openai.responses.create(requestBody);
|
|
10093
|
-
// Extract tool calls from the output
|
|
10094
|
-
const toolCalls = response.output
|
|
10095
|
-
?.filter((item) => item.type === 'function_call')
|
|
10096
|
-
.map((toolCall) => ({
|
|
10097
|
-
id: toolCall.call_id,
|
|
10098
|
-
type: 'function',
|
|
10099
|
-
function: {
|
|
10100
|
-
name: toolCall.name,
|
|
10101
|
-
arguments: toolCall.arguments,
|
|
10102
|
-
},
|
|
10103
|
-
}));
|
|
10104
|
-
// Extract code interpreter outputs if present
|
|
10105
|
-
const codeInterpreterCalls = response.output?.filter((item) => item.type === 'code_interpreter_call');
|
|
10106
|
-
let codeInterpreterOutputs = undefined;
|
|
10107
|
-
if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
|
|
10108
|
-
// Each code_interpreter_call has an 'outputs' array property
|
|
10109
|
-
codeInterpreterOutputs = codeInterpreterCalls.flatMap((ci) => {
|
|
10110
|
-
// Return outputs if present, otherwise empty array
|
|
10111
|
-
if (ci.outputs)
|
|
10112
|
-
return ci.outputs;
|
|
10113
|
-
return [];
|
|
10114
|
-
});
|
|
10115
|
-
}
|
|
10116
|
-
// Handle tool calls differently
|
|
10117
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
10118
|
-
return {
|
|
10119
|
-
response: {
|
|
10120
|
-
tool_calls: toolCalls.map((tc) => ({
|
|
10121
|
-
id: tc.id,
|
|
10122
|
-
name: tc.function.name,
|
|
10123
|
-
arguments: JSON.parse(tc.function.arguments),
|
|
10124
|
-
})),
|
|
10125
|
-
},
|
|
10126
|
-
usage: {
|
|
10127
|
-
prompt_tokens: response.usage?.input_tokens || 0,
|
|
10128
|
-
completion_tokens: response.usage?.output_tokens || 0,
|
|
10129
|
-
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
10130
|
-
provider: 'openai',
|
|
10131
|
-
model: normalizedModel,
|
|
10132
|
-
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0),
|
|
10133
|
-
},
|
|
10134
|
-
tool_calls: toolCalls,
|
|
10135
|
-
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
10136
|
-
};
|
|
10137
|
-
}
|
|
10138
|
-
// Extract text content from the response output
|
|
10139
|
-
const textContent = response.output
|
|
10140
|
-
?.filter((item) => item.type === 'message')
|
|
10141
|
-
.map((item) => item.content
|
|
10142
|
-
.filter((content) => content.type === 'output_text')
|
|
10143
|
-
.map((content) => content.text)
|
|
10144
|
-
.join(''))
|
|
10145
|
-
.join('') || '';
|
|
10146
|
-
// Determine the format for parsing the response
|
|
10147
|
-
let parsingFormat = 'text';
|
|
10148
|
-
if (requestBody.text?.format?.type === 'json_object') {
|
|
10149
|
-
parsingFormat = 'json';
|
|
10150
|
-
}
|
|
10151
|
-
// Handle regular responses
|
|
10152
|
-
const parsedResponse = await parseResponse(textContent, parsingFormat);
|
|
10153
|
-
if (parsedResponse === null) {
|
|
10154
|
-
throw new Error('Failed to parse response from Responses API');
|
|
10155
|
-
}
|
|
10156
|
-
return {
|
|
10157
|
-
response: parsedResponse,
|
|
10158
|
-
usage: {
|
|
10159
|
-
prompt_tokens: response.usage?.input_tokens || 0,
|
|
10160
|
-
completion_tokens: response.usage?.output_tokens || 0,
|
|
10161
|
-
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
10162
|
-
provider: 'openai',
|
|
10163
|
-
model: normalizedModel,
|
|
10164
|
-
cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0),
|
|
10165
|
-
},
|
|
10166
|
-
tool_calls: toolCalls,
|
|
10167
|
-
...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
|
|
10168
|
-
};
|
|
10169
|
-
};
|
|
10170
|
-
/**
|
|
10171
|
-
* Makes a call to the OpenAI Responses API for advanced use cases with built-in tools.
|
|
10172
|
-
*
|
|
10173
|
-
* @param input The text prompt to send to the model (e.g., "What's in this image?")
|
|
10174
|
-
* @param options The options for the Responses API call, including optional image data and context.
|
|
10175
|
-
* @return A promise that resolves to the response from the Responses API.
|
|
10176
|
-
*
|
|
10177
|
-
* @example
|
|
10178
|
-
* // With conversation context
|
|
10179
|
-
* const response = await makeLLMCall("What did I ask about earlier?", {
|
|
10180
|
-
* context: [
|
|
10181
|
-
* { role: 'user', content: 'What is the capital of France?' },
|
|
10182
|
-
* { role: 'assistant', content: 'The capital of France is Paris.' }
|
|
10183
|
-
* ]
|
|
10184
|
-
* });
|
|
10185
|
-
*/
|
|
10186
|
-
async function makeLLMCall(input, options = {}) {
|
|
10187
|
-
const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', tools, useCodeInterpreter = false, useWebSearch = false, imageBase64, imageDetail = 'high', context, } = options;
|
|
10188
|
-
// Validate model
|
|
10189
|
-
const normalizedModel = normalizeModelName(model);
|
|
10190
|
-
if (!isSupportedModel(normalizedModel)) {
|
|
10191
|
-
throw new Error(`Unsupported model: ${normalizedModel}. Please use one of the supported models.`);
|
|
10192
|
-
}
|
|
10193
|
-
// Process input for conversation context and image analysis
|
|
10194
|
-
let processedInput;
|
|
10195
|
-
if (context && context.length > 0) {
|
|
10196
|
-
// Build conversation array with context
|
|
10197
|
-
const conversationMessages = [];
|
|
10198
|
-
// Add context messages
|
|
10199
|
-
for (const contextMsg of context) {
|
|
10200
|
-
conversationMessages.push({
|
|
10201
|
-
role: contextMsg.role,
|
|
10202
|
-
content: contextMsg.content,
|
|
10203
|
-
type: 'message',
|
|
10204
|
-
});
|
|
10205
|
-
}
|
|
10206
|
-
// Add current input message
|
|
10207
|
-
if (imageBase64) {
|
|
10208
|
-
// Current message includes both text and image
|
|
10209
|
-
conversationMessages.push({
|
|
10210
|
-
role: 'user',
|
|
10211
|
-
content: [
|
|
10212
|
-
{ type: 'input_text', text: input },
|
|
10213
|
-
{
|
|
10214
|
-
type: 'input_image',
|
|
10215
|
-
detail: imageDetail,
|
|
10216
|
-
image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
|
|
10217
|
-
},
|
|
10218
|
-
],
|
|
10219
|
-
type: 'message',
|
|
10220
|
-
});
|
|
10221
|
-
}
|
|
10222
|
-
else {
|
|
10223
|
-
// Current message is just text
|
|
10224
|
-
conversationMessages.push({
|
|
10225
|
-
role: 'user',
|
|
10226
|
-
content: input,
|
|
10227
|
-
type: 'message',
|
|
10228
|
-
});
|
|
10229
|
-
}
|
|
10230
|
-
processedInput = conversationMessages;
|
|
10231
|
-
}
|
|
10232
|
-
else if (imageBase64) {
|
|
10233
|
-
// No context, but has image - use the original image logic
|
|
10234
|
-
processedInput = [
|
|
10235
|
-
{
|
|
10236
|
-
role: 'user',
|
|
10237
|
-
content: [
|
|
10238
|
-
{ type: 'input_text', text: input },
|
|
10239
|
-
{
|
|
10240
|
-
type: 'input_image',
|
|
10241
|
-
detail: imageDetail,
|
|
10242
|
-
image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
|
|
10243
|
-
},
|
|
10244
|
-
],
|
|
10245
|
-
type: 'message',
|
|
10246
|
-
},
|
|
10247
|
-
];
|
|
10248
|
-
}
|
|
10249
|
-
else {
|
|
10250
|
-
// No context, no image - simple string input
|
|
10251
|
-
processedInput = input;
|
|
10252
|
-
}
|
|
10253
|
-
// Build the options object for makeResponsesAPICall
|
|
10254
|
-
let responsesOptions = {
|
|
10255
|
-
apiKey,
|
|
10256
|
-
model: normalizedModel,
|
|
10257
|
-
parallel_tool_calls: false,
|
|
10258
|
-
tools,
|
|
10259
|
-
};
|
|
10260
|
-
// Only include temperature if the model supports it
|
|
10261
|
-
if (supportsTemperature(normalizedModel)) {
|
|
10262
|
-
responsesOptions.temperature = 0.2;
|
|
10263
|
-
}
|
|
10264
|
-
// Configure response format
|
|
10265
|
-
if (responseFormat === 'json') {
|
|
10266
|
-
responsesOptions.text = { format: { type: 'json_object' } };
|
|
10267
|
-
}
|
|
10268
|
-
// Configure built-in tools
|
|
10269
|
-
if (useCodeInterpreter) {
|
|
10270
|
-
responsesOptions.tools = [{ type: 'code_interpreter', container: { type: 'auto' } }];
|
|
10271
|
-
// For reasoning models, we can't force tool choice - they only support 'auto'
|
|
10272
|
-
if (!isReasoningModel(normalizedModel)) {
|
|
10273
|
-
responsesOptions.tool_choice = { type: 'code_interpreter' };
|
|
10274
|
-
}
|
|
10275
|
-
responsesOptions.include = ['code_interpreter_call.outputs'];
|
|
10276
|
-
}
|
|
10277
|
-
if (useWebSearch) {
|
|
10278
|
-
responsesOptions.tools = [{ type: 'web_search_preview' }];
|
|
10279
|
-
// For reasoning models and GPT-5 models, we can't force tool choice - they only support 'auto'
|
|
10280
|
-
if (!isReasoningModel(normalizedModel) && !isGPT5Model(normalizedModel)) {
|
|
10281
|
-
responsesOptions.tool_choice = { type: 'web_search_preview' };
|
|
10282
|
-
}
|
|
10283
|
-
}
|
|
10284
|
-
return await makeResponsesAPICall(processedInput, responsesOptions);
|
|
10285
|
-
}
|
|
10286
|
-
|
|
10287
|
-
const DEFAULT_IMAGE_MODEL = 'gpt-image-1.5';
|
|
10288
|
-
const resolveImageModel = (model) => model ?? DEFAULT_IMAGE_MODEL;
|
|
10289
|
-
const MULTIMODAL_VISION_MODELS = new Set([
|
|
10290
|
-
'gpt-4o-mini',
|
|
10291
|
-
'gpt-4o',
|
|
10292
|
-
'gpt-5',
|
|
10293
|
-
'gpt-5-mini',
|
|
10294
|
-
'gpt-5-nano',
|
|
10295
|
-
'gpt-5.1',
|
|
10296
|
-
'gpt-5.2',
|
|
10297
|
-
'gpt-5.2-pro',
|
|
10298
|
-
'gpt-5.1-codex',
|
|
10299
|
-
'gpt-5.1-codex-max',
|
|
10300
|
-
]);
|
|
10301
|
-
/**
|
|
10302
|
-
* Makes a call to the OpenAI Images API to generate images based on a text prompt.
|
|
10303
|
-
*
|
|
10304
|
-
* This function provides access to OpenAI's image generation capabilities with support for:
|
|
10305
|
-
* - Different output formats (JPEG, PNG, WebP)
|
|
10306
|
-
* - Various image sizes and quality settings
|
|
10307
|
-
* - Multiple image generation in a single call
|
|
10308
|
-
* - Base64 encoded image data return
|
|
10309
|
-
*
|
|
10310
|
-
* @example
|
|
10311
|
-
* // Basic image generation
|
|
10312
|
-
* const response = await makeImagesCall('A beautiful sunset over mountains');
|
|
10313
|
-
*
|
|
10314
|
-
* @example
|
|
10315
|
-
* // Custom options
|
|
10316
|
-
* const response = await makeImagesCall('A birthday cake', {
|
|
10317
|
-
* size: '1024x1024',
|
|
10318
|
-
* outputFormat: 'png',
|
|
10319
|
-
* quality: 'high',
|
|
10320
|
-
* count: 2
|
|
10321
|
-
* });
|
|
10322
|
-
*
|
|
10323
|
-
* @param prompt - The text prompt describing the image to generate
|
|
10324
|
-
* @param options - Configuration options for image generation. Includes:
|
|
10325
|
-
* - size: Image dimensions (uses OpenAI's size options)
|
|
10326
|
-
* - outputFormat: Image format ('jpeg', 'png', 'webp') - defaults to 'webp'
|
|
10327
|
-
* - compression: Compression level (number) - defaults to 50
|
|
10328
|
-
* - quality: Quality setting (uses OpenAI's quality options) - defaults to 'high'
|
|
10329
|
-
* - count: Number of images to generate - defaults to 1
|
|
10330
|
-
* - background: Background setting for transparency support
|
|
10331
|
-
* - moderation: Content moderation level
|
|
10332
|
-
* - apiKey: OpenAI API key (optional, falls back to environment variable)
|
|
10333
|
-
* @returns Promise<ImageResponseWithUsage> - The image generation response with cost information
|
|
10334
|
-
* @throws Error if the API call fails or invalid parameters are provided
|
|
10335
|
-
*/
|
|
10336
|
-
async function makeImagesCall(prompt, options = {}) {
|
|
10337
|
-
const { model, size = 'auto', outputFormat = 'webp', compression = 50, quality = 'high', count = 1, background = 'auto', moderation = 'auto', apiKey, visionModel, } = options;
|
|
10338
|
-
const imageModel = resolveImageModel(model);
|
|
10339
|
-
const supportedVisionModel = visionModel && MULTIMODAL_VISION_MODELS.has(visionModel) ? visionModel : undefined;
|
|
10340
|
-
if (visionModel && !supportedVisionModel) {
|
|
10341
|
-
console.warn(`Vision model ${visionModel} is not recognized as a multimodal OpenAI model. Ignoring for image usage metadata.`);
|
|
10342
|
-
}
|
|
10343
|
-
// Get API key
|
|
10344
|
-
const effectiveApiKey = apiKey || process.env.OPENAI_API_KEY;
|
|
10345
|
-
if (!effectiveApiKey) {
|
|
10346
|
-
throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
|
|
10347
|
-
}
|
|
10348
|
-
// Validate inputs
|
|
10349
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
10350
|
-
throw new Error('Prompt must be a non-empty string');
|
|
10351
|
-
}
|
|
10352
|
-
if (count && (count < 1 || count > 10)) {
|
|
10353
|
-
throw new Error('Count must be between 1 and 10');
|
|
10354
|
-
}
|
|
10355
|
-
if (compression && (compression < 0 || compression > 100)) {
|
|
10356
|
-
throw new Error('Compression must be between 0 and 100');
|
|
10357
|
-
}
|
|
10358
|
-
const openai = new OpenAI({
|
|
10359
|
-
apiKey: effectiveApiKey,
|
|
10360
|
-
});
|
|
10361
|
-
// Build the request parameters using OpenAI's type
|
|
10362
|
-
const requestParams = {
|
|
10363
|
-
model: imageModel,
|
|
10364
|
-
prompt,
|
|
10365
|
-
n: count || 1,
|
|
10366
|
-
size: size || 'auto',
|
|
10367
|
-
quality: quality || 'auto',
|
|
10368
|
-
background: background || 'auto',
|
|
10369
|
-
moderation: moderation || 'auto',
|
|
10370
|
-
};
|
|
10371
|
-
// Add output format if specified
|
|
10372
|
-
if (outputFormat) {
|
|
10373
|
-
requestParams.output_format = outputFormat;
|
|
10374
|
-
}
|
|
10375
|
-
// Add compression if specified
|
|
10376
|
-
if (compression !== undefined) {
|
|
10377
|
-
requestParams.output_compression = compression;
|
|
10378
|
-
}
|
|
10379
|
-
try {
|
|
10380
|
-
// Make the API call
|
|
10381
|
-
const response = await openai.images.generate(requestParams);
|
|
10382
|
-
// Validate response
|
|
10383
|
-
if (!response.data || response.data.length === 0) {
|
|
10384
|
-
throw new Error('No images returned from OpenAI Images API');
|
|
10385
|
-
}
|
|
10386
|
-
// Calculate cost
|
|
10387
|
-
const cost = calculateImageCost(imageModel, count || 1);
|
|
10388
|
-
// Return the response with enhanced usage information
|
|
10389
|
-
const enhancedResponse = {
|
|
10390
|
-
...response,
|
|
10391
|
-
usage: {
|
|
10392
|
-
// OpenAI Images response may not include usage details per image; preserve if present
|
|
10393
|
-
...(response.usage ?? {
|
|
10394
|
-
input_tokens: 0,
|
|
10395
|
-
input_tokens_details: { image_tokens: 0, text_tokens: 0 },
|
|
10396
|
-
output_tokens: 0,
|
|
10397
|
-
total_tokens: 0,
|
|
10398
|
-
}),
|
|
10399
|
-
provider: 'openai',
|
|
10400
|
-
model: imageModel,
|
|
10401
|
-
cost,
|
|
10402
|
-
...(supportedVisionModel ? { visionModel: supportedVisionModel } : {}),
|
|
10403
|
-
},
|
|
10404
|
-
};
|
|
10405
|
-
return enhancedResponse;
|
|
10406
|
-
}
|
|
10407
|
-
catch (error) {
|
|
10408
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
10409
|
-
throw new Error(`OpenAI Images API call failed: ${message}`);
|
|
10410
|
-
}
|
|
10411
|
-
}
|
|
10412
|
-
|
|
10413
|
-
/**
|
|
10414
|
-
* Default options for Deepseek API calls
|
|
10415
|
-
*/
|
|
10416
|
-
const DEFAULT_DEEPSEEK_OPTIONS = {
|
|
10417
|
-
defaultModel: 'deepseek-chat',
|
|
10418
|
-
};
|
|
10419
|
-
/**
|
|
10420
|
-
* Checks if the given model is a supported Deepseek model
|
|
10421
|
-
* @param model The model to check
|
|
10422
|
-
* @returns True if the model is supported, false otherwise
|
|
10423
|
-
*/
|
|
10424
|
-
const isSupportedDeepseekModel = (model) => {
|
|
10425
|
-
return ['deepseek-chat', 'deepseek-reasoner'].includes(model);
|
|
10426
|
-
};
|
|
10427
|
-
/**
|
|
10428
|
-
* Checks if the given Deepseek model supports JSON output format
|
|
10429
|
-
* @param model The model to check
|
|
10430
|
-
* @returns True if JSON output is supported, false otherwise
|
|
10431
|
-
*/
|
|
10432
|
-
const supportsJsonOutput = (model) => {
|
|
10433
|
-
return model === 'deepseek-chat';
|
|
10434
|
-
};
|
|
10435
|
-
/**
|
|
10436
|
-
* Checks if the given Deepseek model supports tool calling
|
|
10437
|
-
* @param model The model to check
|
|
10438
|
-
* @returns True if tool calling is supported, false otherwise
|
|
10439
|
-
*/
|
|
10440
|
-
const supportsToolCalling = (model) => {
|
|
10441
|
-
return model === 'deepseek-chat';
|
|
10442
|
-
};
|
|
10443
|
-
/**
|
|
10444
|
-
* Gets the appropriate response format option for Deepseek API
|
|
10445
|
-
*
|
|
10446
|
-
* @param responseFormat The desired response format
|
|
10447
|
-
* @param model The model being used
|
|
10448
|
-
* @returns Object representing the Deepseek response format option
|
|
10449
|
-
*/
|
|
10450
|
-
function getDeepseekResponseFormatOption(responseFormat, model) {
|
|
10451
|
-
// Check if the model supports JSON output
|
|
10452
|
-
if (responseFormat !== 'text' && !supportsJsonOutput(model)) {
|
|
10453
|
-
console.warn(`Model ${model} does not support JSON output. Using text format instead.`);
|
|
10454
|
-
return { type: 'text' };
|
|
10455
|
-
}
|
|
10456
|
-
if (responseFormat === 'text') {
|
|
10457
|
-
return { type: 'text' };
|
|
10458
|
-
}
|
|
10459
|
-
if (responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema')) {
|
|
10460
|
-
return { type: 'json_object' };
|
|
10461
|
-
}
|
|
10462
|
-
return { type: 'text' };
|
|
10463
|
-
}
|
|
10464
|
-
/**
|
|
10465
|
-
* Creates a completion using the Deepseek API
|
|
10466
|
-
*
|
|
10467
|
-
* @param content The content to pass to the API
|
|
10468
|
-
* @param responseFormat The desired format of the response
|
|
10469
|
-
* @param options Additional options for the API call
|
|
10470
|
-
* @returns Promise resolving to the API response
|
|
10471
|
-
*/
|
|
10472
|
-
async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
10473
|
-
// Extract the model or use default
|
|
10474
|
-
const modelOption = options.model || DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
10475
|
-
const normalizedModel = normalizeModelName(modelOption);
|
|
10476
|
-
// Validate model
|
|
10477
|
-
if (!isSupportedDeepseekModel(normalizedModel)) {
|
|
10478
|
-
throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
|
|
10479
|
-
}
|
|
10480
|
-
// Check if tools are requested with a model that doesn't support them
|
|
10481
|
-
if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
|
|
10482
|
-
throw new Error(`Model ${normalizedModel} does not support tool calling.`);
|
|
10483
|
-
}
|
|
10484
|
-
const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY;
|
|
10485
|
-
if (!apiKey) {
|
|
10486
|
-
throw new Error('Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set');
|
|
10487
|
-
}
|
|
10488
|
-
// Initialize OpenAI client with Deepseek API URL
|
|
10489
|
-
const openai = new OpenAI({
|
|
10490
|
-
apiKey,
|
|
10491
|
-
baseURL: 'https://api.deepseek.com',
|
|
10492
|
-
});
|
|
10493
|
-
const messages = [];
|
|
10494
|
-
// Add system message with developer prompt if present
|
|
10495
|
-
if (options.developerPrompt) {
|
|
10496
|
-
messages.push({
|
|
10497
|
-
role: 'system',
|
|
10498
|
-
content: options.developerPrompt,
|
|
10499
|
-
});
|
|
10500
|
-
}
|
|
10501
|
-
// Add context if present
|
|
10502
|
-
if (options.context) {
|
|
10503
|
-
messages.push(...options.context);
|
|
10504
|
-
}
|
|
10505
|
-
// Add user content
|
|
10506
|
-
if (typeof content === 'string') {
|
|
10507
|
-
messages.push({
|
|
10508
|
-
role: 'user',
|
|
10509
|
-
content,
|
|
10510
|
-
});
|
|
10511
|
-
}
|
|
10512
|
-
else {
|
|
10513
|
-
messages.push({
|
|
10514
|
-
role: 'user',
|
|
10515
|
-
content,
|
|
10516
|
-
});
|
|
10517
|
-
}
|
|
10518
|
-
// If JSON response format, include a hint in the system prompt
|
|
10519
|
-
if ((responseFormat === 'json' || (typeof responseFormat === 'object' && responseFormat.type === 'json_schema'))
|
|
10520
|
-
&& supportsJsonOutput(normalizedModel)) {
|
|
10521
|
-
// If there's no system message yet, add one
|
|
10522
|
-
if (!messages.some(m => m.role === 'system')) {
|
|
10523
|
-
messages.unshift({
|
|
10524
|
-
role: 'system',
|
|
10525
|
-
content: 'Please respond in valid JSON format.',
|
|
10526
|
-
});
|
|
10527
|
-
}
|
|
10528
|
-
else {
|
|
10529
|
-
// Append to existing system message
|
|
10530
|
-
const systemMsgIndex = messages.findIndex(m => m.role === 'system');
|
|
10531
|
-
const systemMsg = messages[systemMsgIndex];
|
|
10532
|
-
if (typeof systemMsg.content === 'string') {
|
|
10533
|
-
messages[systemMsgIndex] = {
|
|
10534
|
-
...systemMsg,
|
|
10535
|
-
content: `${systemMsg.content} Please respond in valid JSON format.`,
|
|
10536
|
-
};
|
|
10537
|
-
}
|
|
10538
|
-
}
|
|
10539
|
-
}
|
|
10540
|
-
const queryOptions = {
|
|
10541
|
-
model: normalizedModel,
|
|
10542
|
-
messages,
|
|
10543
|
-
response_format: getDeepseekResponseFormatOption(responseFormat, normalizedModel),
|
|
10544
|
-
temperature: options.temperature,
|
|
10545
|
-
top_p: options.top_p,
|
|
10546
|
-
frequency_penalty: options.frequency_penalty,
|
|
10547
|
-
presence_penalty: options.presence_penalty,
|
|
10548
|
-
max_tokens: options.max_completion_tokens,
|
|
10549
|
-
};
|
|
10550
|
-
// Only add tools if the model supports them
|
|
10551
|
-
if (options.tools && supportsToolCalling(normalizedModel)) {
|
|
10552
|
-
queryOptions.tools = options.tools;
|
|
10553
|
-
}
|
|
10554
|
-
try {
|
|
10555
|
-
const completion = await openai.chat.completions.create(queryOptions);
|
|
10556
|
-
return {
|
|
10557
|
-
id: completion.id,
|
|
10558
|
-
content: completion.choices[0]?.message?.content || '',
|
|
10559
|
-
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
10560
|
-
usage: completion.usage || {
|
|
10561
|
-
prompt_tokens: 0,
|
|
10562
|
-
completion_tokens: 0,
|
|
10563
|
-
total_tokens: 0,
|
|
10564
|
-
},
|
|
10565
|
-
system_fingerprint: completion.system_fingerprint,
|
|
10566
|
-
provider: 'deepseek',
|
|
10567
|
-
model: normalizedModel,
|
|
10568
|
-
};
|
|
10569
|
-
}
|
|
10570
|
-
catch (error) {
|
|
10571
|
-
console.error('Error calling Deepseek API:', error);
|
|
10572
|
-
throw error;
|
|
10573
|
-
}
|
|
10574
|
-
}
|
|
10575
|
-
/**
|
|
10576
|
-
* Makes a call to the Deepseek AI API.
|
|
10577
|
-
*
|
|
10578
|
-
* @param content The content to pass to the LLM. Can be a string or an array of content parts.
|
|
10579
|
-
* @param responseFormat The format of the response. Defaults to 'json'.
|
|
10580
|
-
* @param options Configuration options including model ('deepseek-chat' or 'deepseek-reasoner'), tools, and apiKey.
|
|
10581
|
-
* @return A promise that resolves to the response from the Deepseek API.
|
|
10582
|
-
*/
|
|
10583
|
-
const makeDeepseekCall = async (content, responseFormat = 'json', options = {}) => {
|
|
10584
|
-
// Set default model if not provided
|
|
10585
|
-
const mergedOptions = {
|
|
10586
|
-
...options,
|
|
10587
|
-
};
|
|
10588
|
-
if (!mergedOptions.model) {
|
|
10589
|
-
mergedOptions.model = DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
10590
|
-
}
|
|
10591
|
-
const modelName = normalizeModelName(mergedOptions.model);
|
|
10592
|
-
// Check if the requested response format is compatible with the model
|
|
10593
|
-
if (responseFormat !== 'text' && !supportsJsonOutput(modelName)) {
|
|
10594
|
-
console.warn(`Model ${modelName} does not support JSON output. Will return error in the response.`);
|
|
10595
|
-
return {
|
|
10596
|
-
response: {
|
|
10597
|
-
error: `Model ${modelName} does not support JSON output format.`
|
|
10598
|
-
},
|
|
10599
|
-
usage: {
|
|
10600
|
-
prompt_tokens: 0,
|
|
10601
|
-
completion_tokens: 0,
|
|
10602
|
-
reasoning_tokens: 0,
|
|
10603
|
-
provider: 'deepseek',
|
|
10604
|
-
model: modelName,
|
|
10605
|
-
cache_hit_tokens: 0,
|
|
10606
|
-
cost: 0,
|
|
10607
|
-
},
|
|
10608
|
-
tool_calls: undefined,
|
|
10609
|
-
};
|
|
10610
|
-
}
|
|
10611
|
-
// Check if tools are requested with a model that doesn't support them
|
|
10612
|
-
if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
|
|
10613
|
-
console.warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
|
|
10614
|
-
return {
|
|
10615
|
-
response: {
|
|
10616
|
-
error: `Model ${modelName} does not support tool calling.`
|
|
10617
|
-
},
|
|
10618
|
-
usage: {
|
|
10619
|
-
prompt_tokens: 0,
|
|
10620
|
-
completion_tokens: 0,
|
|
10621
|
-
reasoning_tokens: 0,
|
|
10622
|
-
provider: 'deepseek',
|
|
10623
|
-
model: modelName,
|
|
10624
|
-
cache_hit_tokens: 0,
|
|
10625
|
-
cost: 0,
|
|
10626
|
-
},
|
|
10627
|
-
tool_calls: undefined,
|
|
10628
|
-
};
|
|
10629
|
-
}
|
|
10630
|
-
try {
|
|
10631
|
-
const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
|
|
10632
|
-
// Handle tool calls similarly to OpenAI
|
|
10633
|
-
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
10634
|
-
const fnCalls = completion.tool_calls
|
|
10635
|
-
.filter((tc) => tc.type === 'function')
|
|
10636
|
-
.map((tc) => ({
|
|
10637
|
-
id: tc.id,
|
|
10638
|
-
name: tc.function.name,
|
|
10639
|
-
arguments: JSON.parse(tc.function.arguments),
|
|
10640
|
-
}));
|
|
10641
|
-
return {
|
|
10642
|
-
response: { tool_calls: fnCalls },
|
|
10643
|
-
usage: {
|
|
10644
|
-
prompt_tokens: completion.usage.prompt_tokens,
|
|
10645
|
-
completion_tokens: completion.usage.completion_tokens,
|
|
10646
|
-
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
10647
|
-
provider: 'deepseek',
|
|
10648
|
-
model: completion.model,
|
|
10649
|
-
cache_hit_tokens: 0, // Not provided directly in API response
|
|
10650
|
-
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, 0 // Cache hit tokens (not provided in the response)
|
|
10651
|
-
),
|
|
10652
|
-
},
|
|
10653
|
-
tool_calls: completion.tool_calls,
|
|
10654
|
-
};
|
|
10655
|
-
}
|
|
10656
|
-
// Handle regular responses
|
|
10657
|
-
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
10658
|
-
if (parsedResponse === null) {
|
|
10659
|
-
throw new Error('Failed to parse Deepseek response');
|
|
10660
|
-
}
|
|
10661
|
-
return {
|
|
10662
|
-
response: parsedResponse,
|
|
10663
|
-
usage: {
|
|
10664
|
-
prompt_tokens: completion.usage.prompt_tokens,
|
|
10665
|
-
completion_tokens: completion.usage.completion_tokens,
|
|
10666
|
-
reasoning_tokens: 0, // Deepseek doesn't provide reasoning tokens separately
|
|
10667
|
-
provider: 'deepseek',
|
|
10668
|
-
model: completion.model,
|
|
10669
|
-
cache_hit_tokens: 0, // Not provided directly in API response
|
|
10670
|
-
cost: calculateCost('deepseek', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, 0 // Cache hit tokens (not provided in the response)
|
|
10671
|
-
),
|
|
10672
|
-
},
|
|
10673
|
-
tool_calls: completion.tool_calls,
|
|
10674
|
-
};
|
|
10675
|
-
}
|
|
10676
|
-
catch (error) {
|
|
10677
|
-
// If there's an error due to incompatible features, return a structured error
|
|
10678
|
-
console.error('Error in Deepseek API call:', error);
|
|
10679
|
-
return {
|
|
10680
|
-
response: {
|
|
10681
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
10682
|
-
},
|
|
10683
|
-
usage: {
|
|
10684
|
-
prompt_tokens: 0,
|
|
10685
|
-
completion_tokens: 0,
|
|
10686
|
-
reasoning_tokens: 0,
|
|
10687
|
-
provider: 'deepseek',
|
|
10688
|
-
model: modelName,
|
|
10689
|
-
cache_hit_tokens: 0,
|
|
10690
|
-
cost: 0,
|
|
10691
|
-
},
|
|
10692
|
-
tool_calls: undefined,
|
|
10693
|
-
};
|
|
10694
|
-
}
|
|
10695
|
-
};
|
|
10696
|
-
|
|
10697
|
-
// llm-openrouter.ts
|
|
10698
|
-
// Map our ContextMessage to OpenAI chat message
|
|
10699
|
-
function mapContextToMessages(context) {
|
|
10700
|
-
return context.map((msg) => {
|
|
10701
|
-
const role = msg.role === 'developer' ? 'system' : msg.role;
|
|
10702
|
-
return { role, content: msg.content };
|
|
10703
|
-
});
|
|
10704
|
-
}
|
|
10705
|
-
function toOpenRouterModel(model) {
|
|
10706
|
-
if (model && model.includes('/'))
|
|
10707
|
-
return model;
|
|
10708
|
-
const base = normalizeModelName(model || DEFAULT_MODEL);
|
|
10709
|
-
return `openai/${base}`;
|
|
10710
|
-
}
|
|
10711
|
-
// Normalize model name for pricing
|
|
10712
|
-
function normalizeModelForPricing(model) {
|
|
10713
|
-
if (!model)
|
|
10714
|
-
return { provider: 'openai', coreModel: normalizeModelName(DEFAULT_MODEL) };
|
|
10715
|
-
const [maybeProvider, maybeModel] = model.includes('/') ? model.split('/') : ['openai', model];
|
|
10716
|
-
const provider = (maybeProvider === 'deepseek' ? 'deepseek' : 'openai');
|
|
10717
|
-
const coreModel = normalizeModelName(maybeModel || model);
|
|
10718
|
-
return { provider, coreModel };
|
|
10719
|
-
}
|
|
10720
|
-
/**
|
|
10721
|
-
* Make a call through OpenRouter using the OpenAI Chat Completions-compatible API.
|
|
10722
|
-
* Supports: JSON mode, model selection, message history, and tools.
|
|
10723
|
-
*/
|
|
10724
|
-
async function makeOpenRouterCall(input, options = {}) {
|
|
10725
|
-
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;
|
|
10726
|
-
if (!apiKey) {
|
|
10727
|
-
throw new Error('OpenRouter API key is not provided and OPENROUTER_API_KEY is not set');
|
|
10728
|
-
}
|
|
10729
|
-
const client = new OpenAI({
|
|
10730
|
-
apiKey,
|
|
10731
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
10732
|
-
defaultHeaders: {
|
|
10733
|
-
...(referer ? { 'HTTP-Referer': referer } : {}),
|
|
10734
|
-
...(title ? { 'X-Title': title } : {}),
|
|
10735
|
-
},
|
|
10736
|
-
});
|
|
10737
|
-
const messages = [];
|
|
10738
|
-
if (developerPrompt && developerPrompt.trim()) {
|
|
10739
|
-
messages.push({ role: 'system', content: developerPrompt });
|
|
10740
|
-
}
|
|
10741
|
-
if (context && context.length > 0) {
|
|
10742
|
-
messages.push(...mapContextToMessages(context));
|
|
10743
|
-
}
|
|
10744
|
-
messages.push({ role: 'user', content: input });
|
|
10745
|
-
// Configure response_format
|
|
10746
|
-
let response_format;
|
|
10747
|
-
let parsingFormat = 'text';
|
|
10748
|
-
if (responseFormat === 'json') {
|
|
10749
|
-
response_format = { type: 'json_object' };
|
|
10750
|
-
parsingFormat = 'json';
|
|
10751
|
-
}
|
|
10752
|
-
else if (typeof responseFormat === 'object') {
|
|
10753
|
-
response_format = { type: 'json_object' };
|
|
10754
|
-
parsingFormat = responseFormat;
|
|
10755
|
-
}
|
|
10756
|
-
const modelId = toOpenRouterModel(model);
|
|
10757
|
-
const completion = await client.chat.completions.create({
|
|
10758
|
-
model: modelId,
|
|
10759
|
-
messages,
|
|
10760
|
-
response_format,
|
|
10761
|
-
tools,
|
|
10762
|
-
tool_choice: toolChoice,
|
|
10763
|
-
temperature,
|
|
10764
|
-
max_tokens,
|
|
10765
|
-
top_p,
|
|
10766
|
-
frequency_penalty,
|
|
10767
|
-
presence_penalty,
|
|
10768
|
-
stop,
|
|
10769
|
-
seed,
|
|
10770
|
-
});
|
|
10771
|
-
const choice = completion.choices && completion.choices.length > 0 ? completion.choices[0] : undefined;
|
|
10772
|
-
const message = (choice && 'message' in choice ? choice.message : undefined);
|
|
10773
|
-
const { provider: pricingProvider, coreModel } = normalizeModelForPricing(modelId);
|
|
10774
|
-
const promptTokens = completion.usage?.prompt_tokens ?? 0;
|
|
10775
|
-
const completionTokens = completion.usage?.completion_tokens ?? 0;
|
|
10776
|
-
const cost = calculateCost(pricingProvider, coreModel, promptTokens, completionTokens);
|
|
10777
|
-
// Tool calls branch: return empty string response and expose tool_calls on LLMResponse
|
|
10778
|
-
const hasToolCalls = Array.isArray(message?.tool_calls) && message.tool_calls.length > 0;
|
|
10779
|
-
if (hasToolCalls) {
|
|
10780
|
-
const usageModel = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
|
|
10781
|
-
return {
|
|
10782
|
-
response: '',
|
|
10783
|
-
usage: {
|
|
10784
|
-
prompt_tokens: promptTokens,
|
|
10785
|
-
completion_tokens: completionTokens,
|
|
10786
|
-
provider: 'openrouter',
|
|
10787
|
-
model: usageModel,
|
|
10788
|
-
cost,
|
|
10789
|
-
},
|
|
10790
|
-
tool_calls: message.tool_calls,
|
|
10791
|
-
};
|
|
10792
|
-
}
|
|
10793
|
-
const rawText = typeof message?.content === 'string' ? message.content : '';
|
|
10794
|
-
const parsed = await parseResponse(rawText, parsingFormat);
|
|
10795
|
-
if (parsed === null) {
|
|
10796
|
-
throw new Error('Failed to parse OpenRouter response');
|
|
10797
|
-
}
|
|
10798
|
-
// Ensure the model value conforms to LLMModel; otherwise fall back to DEFAULT_MODEL
|
|
10799
|
-
const usageModel = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
|
|
10800
|
-
return {
|
|
10801
|
-
response: parsed,
|
|
10802
|
-
usage: {
|
|
10803
|
-
prompt_tokens: promptTokens,
|
|
10804
|
-
completion_tokens: completionTokens,
|
|
10805
|
-
provider: 'openrouter',
|
|
10806
|
-
model: usageModel,
|
|
10807
|
-
cost,
|
|
10808
|
-
},
|
|
10809
|
-
...(hasToolCalls ? { tool_calls: message.tool_calls } : {}),
|
|
10810
|
-
};
|
|
10811
|
-
}
|
|
10812
|
-
|
|
10813
|
-
/**
|
|
10814
|
-
* A class to measure performance of code execution.
|
|
10815
|
-
*
|
|
10816
|
-
* This utility records elapsed time during execution and provides a detailed
|
|
10817
|
-
* breakdown of elapsed time between checkpoints.
|
|
10818
|
-
*
|
|
10819
|
-
* Usage example:
|
|
10820
|
-
* const timer = new PerformanceTimer();
|
|
10821
|
-
* timer.checkpoint('Start Processing');
|
|
10822
|
-
* // ... code for processing
|
|
10823
|
-
* timer.checkpoint('After Task 1');
|
|
10824
|
-
* // ... additional processing code
|
|
10825
|
-
* timer.stop();
|
|
10826
|
-
* console.log(timer.generateReport());
|
|
10827
|
-
*
|
|
10828
|
-
* Methods:
|
|
10829
|
-
* - checkpoint(label: string): Record a timestamped checkpoint.
|
|
10830
|
-
* - stop(): Stop the timer and compute the total elapsed time.
|
|
10831
|
-
* - generateReport(): Return a performance report with total time and phases.
|
|
10832
|
-
* - formatPerformanceReport(): Return a formatted string of the performance report.
|
|
10833
|
-
* - getElapsedSeconds(): Get the current elapsed time in seconds.
|
|
10834
|
-
*/
|
|
10835
|
-
class PerformanceTimer {
|
|
10836
|
-
startTime;
|
|
10837
|
-
checkpoints;
|
|
10838
|
-
totalTime;
|
|
10839
|
-
constructor() {
|
|
10840
|
-
this.startTime = performance.now();
|
|
10841
|
-
this.checkpoints = new Map();
|
|
10842
|
-
this.totalTime = null;
|
|
10843
|
-
}
|
|
10844
|
-
/**
|
|
10845
|
-
* Gets the current elapsed time in seconds.
|
|
10846
|
-
*
|
|
10847
|
-
* @returns The elapsed time in seconds with 3 decimal places of precision.
|
|
10848
|
-
*/
|
|
10849
|
-
getElapsedSeconds() {
|
|
10850
|
-
const elapsedMs = this.totalTime ?? (performance.now() - this.startTime);
|
|
10851
|
-
return Number((elapsedMs / 1000).toFixed(3));
|
|
10852
|
-
}
|
|
10853
|
-
/**
|
|
10854
|
-
* Records a checkpoint with the given label.
|
|
10855
|
-
*
|
|
10856
|
-
* @param label - A descriptive label for the checkpoint.
|
|
10857
|
-
*/
|
|
10858
|
-
checkpoint(label) {
|
|
10859
|
-
this.checkpoints.set(label, performance.now());
|
|
10860
|
-
}
|
|
10861
|
-
/**
|
|
10862
|
-
* Stops the timer and sets the total elapsed time.
|
|
10863
|
-
*/
|
|
10864
|
-
stop() {
|
|
10865
|
-
this.totalTime = performance.now() - this.startTime;
|
|
10866
|
-
}
|
|
10867
|
-
/**
|
|
10868
|
-
* Generates a performance report containing the total elapsed time and
|
|
10869
|
-
* breakdown of phases between checkpoints.
|
|
10870
|
-
*
|
|
10871
|
-
* @returns A performance report object.
|
|
10872
|
-
* @throws Error if the timer is not stopped before generating the report.
|
|
10873
|
-
*/
|
|
10874
|
-
analyseReportData() {
|
|
10875
|
-
if (this.totalTime === null) {
|
|
10876
|
-
throw new Error('Timer must be stopped before generating report');
|
|
10877
|
-
}
|
|
10878
|
-
const report = {
|
|
10879
|
-
totalTime: this.totalTime,
|
|
10880
|
-
phases: []
|
|
10881
|
-
};
|
|
10882
|
-
let lastTime = this.startTime;
|
|
10883
|
-
// Convert checkpoints to a sorted array by time
|
|
10884
|
-
const sortedCheckpoints = Array.from(this.checkpoints.entries())
|
|
10885
|
-
.sort((a, b) => a[1] - b[1]);
|
|
10886
|
-
for (const [label, time] of sortedCheckpoints) {
|
|
10887
|
-
const duration = time - lastTime;
|
|
10888
|
-
if (duration < 0) {
|
|
10889
|
-
throw new Error(`Negative duration detected for checkpoint: ${label}`);
|
|
10890
|
-
}
|
|
10891
|
-
report.phases.push({
|
|
10892
|
-
label,
|
|
10893
|
-
duration
|
|
10894
|
-
});
|
|
10895
|
-
lastTime = time;
|
|
10896
|
-
}
|
|
10897
|
-
// Add final phase from last checkpoint to stop
|
|
10898
|
-
if (sortedCheckpoints.length > 0) {
|
|
10899
|
-
const finalDuration = this.totalTime - lastTime;
|
|
10900
|
-
report.phases.push({
|
|
10901
|
-
label: 'Final Processing',
|
|
10902
|
-
duration: Math.max(finalDuration, 0) // Prevent negative duration
|
|
10903
|
-
});
|
|
10904
|
-
}
|
|
10905
|
-
return report;
|
|
10906
|
-
}
|
|
10907
|
-
/**
|
|
10908
|
-
* Returns a formatted string of the performance report.
|
|
10909
|
-
*
|
|
10910
|
-
* @returns A string detailing the total execution time and phase breakdown.
|
|
10911
|
-
*/
|
|
10912
|
-
generateReport() {
|
|
10913
|
-
const report = this.analyseReportData();
|
|
10914
|
-
// Format numbers with thousands separators and 2 decimal places
|
|
10915
|
-
const formatNumber = (num) => num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
10916
|
-
// Calculate column widths
|
|
10917
|
-
const maxLabelLength = Math.max(...report.phases.map(p => p.label.length), 30);
|
|
10918
|
-
const maxDurationLength = Math.max(...report.phases.map(p => formatNumber(p.duration).length), 10);
|
|
10919
|
-
// Create table header
|
|
10920
|
-
let output = `╔${'═'.repeat(maxLabelLength + 2)}╦${'═'.repeat(maxDurationLength + 2)}╗\n`;
|
|
10921
|
-
output += `║ ${'Phase'.padEnd(maxLabelLength)} ║ ${'Time (ms)'.padEnd(maxDurationLength)} ║\n`;
|
|
10922
|
-
output += `╠${'═'.repeat(maxLabelLength + 2)}╬${'═'.repeat(maxDurationLength + 2)}╣\n`;
|
|
10923
|
-
// Add table rows
|
|
10924
|
-
report.phases.forEach(phase => {
|
|
10925
|
-
output += `║ ${phase.label.padEnd(maxLabelLength)} ║ ${formatNumber(phase.duration).padStart(maxDurationLength)} ║\n`;
|
|
10926
|
-
});
|
|
10927
|
-
// Add table footer with total
|
|
10928
|
-
output += `╠${'═'.repeat(maxLabelLength + 2)}╬${'═'.repeat(maxDurationLength + 2)}╣\n`;
|
|
10929
|
-
output += `║ ${'Total'.padEnd(maxLabelLength)} ║ ${formatNumber(report.totalTime).padStart(maxDurationLength)} ║\n`;
|
|
10930
|
-
output += `╚${'═'.repeat(maxLabelLength + 2)}╩${'═'.repeat(maxDurationLength + 2)}╝\n`;
|
|
10931
|
-
return output;
|
|
10932
|
-
}
|
|
10933
|
-
}
|
|
10934
|
-
|
|
10935
|
-
/**
|
|
10936
|
-
* Calculates Bollinger Bands for a given set of price data.
|
|
10937
|
-
* Bollinger Bands consist of a middle band (SMA) and two outer bands
|
|
10938
|
-
* that are standard deviations away from the middle band.
|
|
10939
|
-
*
|
|
10940
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
10941
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
10942
|
-
* @param params.period - The number of periods to use for the SMA (default is 20).
|
|
10943
|
-
* @param params.standardDeviations - The number of standard deviations for the outer bands (default is 2).
|
|
10944
|
-
* @returns An array of BollingerBandsData objects containing the calculated bands.
|
|
10945
|
-
*/
|
|
10946
|
-
function calculateBollingerBands(priceData, { period = 20, standardDeviations = 2 } = {}) {
|
|
10947
|
-
if (priceData.length < period) {
|
|
10948
|
-
logIfDebug(`Insufficient data for Bollinger Bands calculation: required periods: ${period}, but only received ${priceData.length} periods of data`);
|
|
10949
|
-
return [];
|
|
10950
|
-
}
|
|
10951
|
-
const result = [];
|
|
10952
|
-
for (let i = period - 1; i < priceData.length; i++) {
|
|
10953
|
-
const periodSlice = priceData.slice(i - period + 1, i + 1);
|
|
10954
|
-
const prices = periodSlice.map((d) => d.close);
|
|
10955
|
-
// Calculate middle band (SMA)
|
|
10956
|
-
const sum = prices.reduce((acc, price) => acc + price, 0);
|
|
10957
|
-
const sma = sum / period;
|
|
10958
|
-
// Calculate standard deviation
|
|
10959
|
-
const squaredDifferences = prices.map((price) => Math.pow(price - sma, 2));
|
|
10960
|
-
const variance = squaredDifferences.reduce((acc, val) => acc + val, 0) / period;
|
|
10961
|
-
const standardDeviation = Math.sqrt(variance);
|
|
10962
|
-
// Calculate bands
|
|
10963
|
-
const upperBand = sma + standardDeviation * standardDeviations;
|
|
10964
|
-
const lowerBand = sma - standardDeviation * standardDeviations;
|
|
10965
|
-
result.push({
|
|
10966
|
-
date: priceData[i].date,
|
|
10967
|
-
middle: parseFloat(sma.toFixed(2)),
|
|
10968
|
-
upper: parseFloat(upperBand.toFixed(2)),
|
|
10969
|
-
lower: parseFloat(lowerBand.toFixed(2)),
|
|
10970
|
-
close: priceData[i].close,
|
|
10971
|
-
});
|
|
10972
|
-
}
|
|
10973
|
-
// logIfDebug(`Calculated Bollinger Bands for ${result.length} periods`);
|
|
10974
|
-
return result;
|
|
10975
|
-
}
|
|
10976
|
-
/**
|
|
10977
|
-
* Calculates the Exponential Moving Average (EMA) for a given set of price data.
|
|
10978
|
-
* The EMA gives more weight to recent prices, making it more responsive to new information.
|
|
10979
|
-
*
|
|
10980
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
10981
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
10982
|
-
* @param params.period - The number of periods to use for the EMA (default is 20).
|
|
10983
|
-
* @param params.period2 - An optional second period for a second EMA (default is 9).
|
|
10984
|
-
* @returns An array of EMAData objects containing the calculated EMA values.
|
|
10985
|
-
*/
|
|
10986
|
-
function calculateEMA(priceData, { period = 20, period2 = 9 } = {}) {
|
|
10987
|
-
if (priceData.length < period || (period2 && priceData.length < period2)) {
|
|
10988
|
-
logIfDebug(`Insufficient data for EMA calculation: required periods: ${period}, ${period2}, but only received ${priceData.length} periods of data`);
|
|
10989
|
-
return [];
|
|
10990
|
-
}
|
|
10991
|
-
const result = [];
|
|
10992
|
-
const multiplier = 2 / (period + 1);
|
|
10993
|
-
const multiplier2 = period2 ? 2 / (period2 + 1) : 0;
|
|
10994
|
-
// Calculate initial SMA for first period
|
|
10995
|
-
let sum = 0;
|
|
10996
|
-
for (let i = 0; i < period; i++) {
|
|
10997
|
-
sum += priceData[i].close;
|
|
10998
|
-
}
|
|
10999
|
-
let prevEMA = sum / period;
|
|
11000
|
-
// Calculate initial SMA for second period if needed
|
|
11001
|
-
let prevEMA2;
|
|
11002
|
-
if (period2) {
|
|
11003
|
-
sum = 0;
|
|
11004
|
-
for (let i = 0; i < period2; i++) {
|
|
11005
|
-
sum += priceData[i].close;
|
|
11006
|
-
}
|
|
11007
|
-
prevEMA2 = sum / period2;
|
|
11008
|
-
}
|
|
11009
|
-
// Add first EMA(s)
|
|
11010
|
-
const firstEntry = {
|
|
11011
|
-
date: priceData[Math.max(period, period2 || 0) - 1].date,
|
|
11012
|
-
ema: parseFloat(prevEMA.toFixed(2)),
|
|
11013
|
-
close: priceData[Math.max(period, period2 || 0) - 1].close,
|
|
11014
|
-
};
|
|
11015
|
-
if (period2) {
|
|
11016
|
-
firstEntry.ema2 = parseFloat(prevEMA2.toFixed(2));
|
|
11017
|
-
}
|
|
11018
|
-
result.push(firstEntry);
|
|
11019
|
-
// Calculate EMA for remaining periods
|
|
11020
|
-
for (let i = Math.max(period, period2 || 0); i < priceData.length; i++) {
|
|
11021
|
-
const currentClose = priceData[i].close;
|
|
11022
|
-
const currentEMA = (currentClose - prevEMA) * multiplier + prevEMA;
|
|
11023
|
-
prevEMA = currentEMA;
|
|
11024
|
-
const entry = {
|
|
11025
|
-
date: priceData[i].date,
|
|
11026
|
-
ema: parseFloat(currentEMA.toFixed(2)),
|
|
11027
|
-
close: currentClose,
|
|
11028
|
-
};
|
|
11029
|
-
if (period2) {
|
|
11030
|
-
const currentEMA2 = (currentClose - prevEMA2) * multiplier2 + prevEMA2;
|
|
11031
|
-
prevEMA2 = currentEMA2;
|
|
11032
|
-
entry.ema2 = parseFloat(currentEMA2.toFixed(2));
|
|
11033
|
-
}
|
|
11034
|
-
result.push(entry);
|
|
11035
|
-
}
|
|
11036
|
-
// logIfDebug(`Calculated EMA for ${result.length} periods`);
|
|
11037
|
-
return result;
|
|
11038
|
-
}
|
|
11039
|
-
/**
|
|
11040
|
-
* Calculates Fibonacci retracement and extension levels based on price data.
|
|
11041
|
-
* Fibonacci levels are used to identify potential support and resistance levels.
|
|
11042
|
-
*
|
|
11043
|
-
* @param priceData - An array of price data objects containing high and low prices.
|
|
11044
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11045
|
-
* @param params.lookbackPeriod - The number of periods to look back for swing high/low (default is 20).
|
|
11046
|
-
* @param params.retracementLevels - An array of retracement levels to calculate (default is [0.236, 0.382, 0.5, 0.618, 0.786]).
|
|
11047
|
-
* @param params.extensionLevels - An array of extension levels to calculate (default is [1.272, 1.618, 2.618]).
|
|
11048
|
-
* @param params.reverseDirection - A boolean indicating if the trend is reversed (default is false).
|
|
11049
|
-
* @returns An array of FibonacciData objects containing the calculated levels.
|
|
11050
|
-
*/
|
|
11051
|
-
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, } = {}) {
|
|
11052
|
-
const result = [];
|
|
11053
|
-
for (let i = 0; i < priceData.length; i++) {
|
|
11054
|
-
const periodSlice = priceData.slice(Math.max(0, i - lookbackPeriod + 1), i + 1);
|
|
11055
|
-
const swingHigh = Math.max(...periodSlice.map((d) => d.high));
|
|
11056
|
-
const swingLow = Math.min(...periodSlice.map((d) => d.low));
|
|
11057
|
-
const priceRange = swingHigh - swingLow;
|
|
11058
|
-
const trend = reverseDirection ? 'downtrend' : 'uptrend';
|
|
11059
|
-
let levels = [];
|
|
11060
|
-
if (priceRange > 0) {
|
|
11061
|
-
// Calculate retracement levels
|
|
11062
|
-
retracementLevels.forEach((level) => {
|
|
11063
|
-
const price = reverseDirection ? swingLow + priceRange * level : swingHigh - priceRange * level;
|
|
11064
|
-
levels.push({
|
|
11065
|
-
level,
|
|
11066
|
-
price: parseFloat(price.toFixed(2)),
|
|
11067
|
-
type: 'retracement',
|
|
11068
|
-
});
|
|
11069
|
-
});
|
|
11070
|
-
// Calculate extension levels
|
|
11071
|
-
extensionLevels.forEach((level) => {
|
|
11072
|
-
const price = reverseDirection
|
|
11073
|
-
? swingHigh - priceRange * (level - 1) // For downtrend
|
|
11074
|
-
: swingHigh + priceRange * (level - 1); // For uptrend
|
|
11075
|
-
levels.push({
|
|
11076
|
-
level,
|
|
11077
|
-
price: parseFloat(price.toFixed(2)),
|
|
11078
|
-
type: 'extension',
|
|
11079
|
-
});
|
|
11080
|
-
});
|
|
11081
|
-
// Sort levels by price
|
|
11082
|
-
levels.sort((a, b) => (reverseDirection ? b.price - a.price : a.price - b.price));
|
|
11083
|
-
}
|
|
11084
|
-
else {
|
|
11085
|
-
logIfDebug(`Price range is zero on date ${priceData[i].date}; no levels calculated.`);
|
|
11086
|
-
}
|
|
11087
|
-
result.push({
|
|
11088
|
-
date: priceData[i].date,
|
|
11089
|
-
levels,
|
|
11090
|
-
swingHigh,
|
|
11091
|
-
swingLow,
|
|
11092
|
-
trend,
|
|
11093
|
-
close: priceData[i].close,
|
|
11094
|
-
});
|
|
7634
|
+
return sleepSeconds * jitter * 1000;
|
|
11095
7635
|
}
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
}
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
const emaLong = calculateEMA(priceData, { period: longPeriod });
|
|
11117
|
-
// Align EMAs by trimming the beginning of emaShort to match emaLong length
|
|
11118
|
-
if (emaShort.length < emaLong.length) {
|
|
11119
|
-
logIfDebug('Short EMA length is less than Long EMA length for MACD calculation');
|
|
11120
|
-
return [];
|
|
11121
|
-
}
|
|
11122
|
-
const emaShortAligned = emaShort.slice(emaShort.length - emaLong.length);
|
|
11123
|
-
const macdLine = emaShortAligned.map((short, i) => short.ema - emaLong[i].ema);
|
|
11124
|
-
const result = [];
|
|
11125
|
-
if (macdLine.length < signalPeriod) {
|
|
11126
|
-
logIfDebug(`Insufficient MACD data for Signal Line calculation: required periods: ${signalPeriod}, but only received ${macdLine.length} periods of data`);
|
|
11127
|
-
return [];
|
|
11128
|
-
}
|
|
11129
|
-
const signalMultiplier = 2 / (signalPeriod + 1);
|
|
11130
|
-
let signalEMA = macdLine.slice(0, signalPeriod).reduce((sum, val) => sum + val, 0) / signalPeriod;
|
|
11131
|
-
for (let i = signalPeriod; i < macdLine.length; i++) {
|
|
11132
|
-
const macdValue = macdLine[i];
|
|
11133
|
-
signalEMA = (macdValue - signalEMA) * signalMultiplier + signalEMA;
|
|
11134
|
-
const hist = macdValue - signalEMA;
|
|
11135
|
-
result.push({
|
|
11136
|
-
date: emaLong[i].date, // Use emaLong's date for alignment
|
|
11137
|
-
macd: parseFloat(macdValue.toFixed(2)),
|
|
11138
|
-
signal: parseFloat(signalEMA.toFixed(2)),
|
|
11139
|
-
histogram: parseFloat(hist.toFixed(2)),
|
|
11140
|
-
close: emaLong[i].close,
|
|
11141
|
-
});
|
|
7636
|
+
async buildRequest(inputOptions, { retryCount = 0 } = {}) {
|
|
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 };
|
|
11142
7656
|
}
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
*
|
|
11150
|
-
* @param priceData - An array of price data objects containing closing prices.
|
|
11151
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11152
|
-
* @param params.period - The number of periods to use for the RSI (default is 14).
|
|
11153
|
-
* @returns An array of RSIData objects containing the calculated RSI values.
|
|
11154
|
-
*/
|
|
11155
|
-
function calculateRSI(priceData, { period = 14 } = {}) {
|
|
11156
|
-
if (priceData.length < period + 1) {
|
|
11157
|
-
logIfDebug(`Insufficient data for RSI calculation: required periods: ${period + 1}, but only received ${priceData.length} periods of data`);
|
|
11158
|
-
return [];
|
|
11159
|
-
}
|
|
11160
|
-
const result = [];
|
|
11161
|
-
let avgGain = 0;
|
|
11162
|
-
let avgLoss = 0;
|
|
11163
|
-
// Calculate first average gain and loss
|
|
11164
|
-
for (let i = 1; i <= period; i++) {
|
|
11165
|
-
const change = priceData[i].close - priceData[i - 1].close;
|
|
11166
|
-
if (change >= 0) {
|
|
11167
|
-
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;
|
|
11168
7663
|
}
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
const loss = change < 0 ? Math.abs(change) : 0;
|
|
11188
|
-
// Use smoothed averages
|
|
11189
|
-
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
11190
|
-
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
11191
|
-
rs = avgGain / avgLoss;
|
|
11192
|
-
rsi = 100 - 100 / (1 + rs);
|
|
11193
|
-
result.push({
|
|
11194
|
-
date: priceData[i].date,
|
|
11195
|
-
rsi: parseFloat(rsi.toFixed(2)),
|
|
11196
|
-
close: priceData[i].close,
|
|
11197
|
-
});
|
|
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;
|
|
11198
7682
|
}
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
}
|
|
11202
|
-
/**
|
|
11203
|
-
* Calculates the Stochastic Oscillator for a given set of price data.
|
|
11204
|
-
* The Stochastic Oscillator compares a particular closing price of a security to a range of its prices over a certain period of time.
|
|
11205
|
-
*
|
|
11206
|
-
* @param priceData - An array of price data objects containing high, low, and closing prices.
|
|
11207
|
-
* @param params - An object containing optional parameters for the calculation.
|
|
11208
|
-
* @param params.lookbackPeriod - The number of periods to look back for the calculation of %K (default is 5).
|
|
11209
|
-
* @param params.signalPeriod - The number of periods for the %D signal line (default is 3).
|
|
11210
|
-
* @param params.smoothingFactor - The smoothing factor for %K (default is 3).
|
|
11211
|
-
* @returns An array of StochData objects containing the calculated %K and %D values.
|
|
11212
|
-
*/
|
|
11213
|
-
function calculateStochasticOscillator(priceData, { lookbackPeriod = 5, signalPeriod = 3, smoothingFactor = 3 } = {}) {
|
|
11214
|
-
if (priceData.length < lookbackPeriod) {
|
|
11215
|
-
logIfDebug(`Insufficient data for Stochastic Oscillator calculation: required periods: ${lookbackPeriod}, but only received ${priceData.length} periods of data`);
|
|
11216
|
-
return [];
|
|
11217
|
-
}
|
|
11218
|
-
const kValues = [];
|
|
11219
|
-
const result = [];
|
|
11220
|
-
let kSum = 0;
|
|
11221
|
-
let dSum = 0;
|
|
11222
|
-
for (let i = lookbackPeriod - 1; i < priceData.length; i++) {
|
|
11223
|
-
const periodSlice = priceData.slice(i - lookbackPeriod + 1, i + 1);
|
|
11224
|
-
const currentClose = periodSlice[periodSlice.length - 1].close;
|
|
11225
|
-
const highPrices = periodSlice.map((d) => d.high);
|
|
11226
|
-
const lowPrices = periodSlice.map((d) => d.low);
|
|
11227
|
-
const highestHigh = Math.max(...highPrices);
|
|
11228
|
-
const lowestLow = Math.min(...lowPrices);
|
|
11229
|
-
const k = highestHigh === lowestLow ? 0 : ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100;
|
|
11230
|
-
kValues.push(k);
|
|
11231
|
-
kSum += k;
|
|
11232
|
-
if (kValues.length > smoothingFactor)
|
|
11233
|
-
kSum -= kValues[kValues.length - smoothingFactor - 1];
|
|
11234
|
-
const smoothedK = kSum / Math.min(kValues.length, smoothingFactor);
|
|
11235
|
-
dSum += smoothedK;
|
|
11236
|
-
if (kValues.length > smoothingFactor + signalPeriod - 1)
|
|
11237
|
-
dSum -= kValues[kValues.length - smoothingFactor - signalPeriod];
|
|
11238
|
-
const smoothedD = dSum / Math.min(kValues.length - smoothingFactor + 1, signalPeriod);
|
|
11239
|
-
if (kValues.length >= smoothingFactor + signalPeriod - 1) {
|
|
11240
|
-
result.push({
|
|
11241
|
-
date: priceData[i].date,
|
|
11242
|
-
slowK: parseFloat(smoothedK.toFixed(2)),
|
|
11243
|
-
slowD: parseFloat(smoothedD.toFixed(2)),
|
|
11244
|
-
close: currentClose,
|
|
11245
|
-
});
|
|
7683
|
+
buildBody({ options: { body, headers: rawHeaders } }) {
|
|
7684
|
+
if (!body) {
|
|
7685
|
+
return { bodyHeaders: undefined, body: undefined };
|
|
11246
7686
|
}
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
const analysisWindow = priceData.slice(startIdx, i + 1);
|
|
11266
|
-
const pivotPoints = [];
|
|
11267
|
-
// **Compute Volatility Metrics**
|
|
11268
|
-
const priceChanges = analysisWindow.slice(1).map((bar, idx) => Math.abs(bar.close - analysisWindow[idx].close));
|
|
11269
|
-
const avgPriceChange = priceChanges.reduce((sum, change) => sum + change, 0) / priceChanges.length;
|
|
11270
|
-
const volatility = avgPriceChange / analysisWindow[0].close; // Relative volatility
|
|
11271
|
-
// **Adjust Sensitivity and minGapBetweenLevels Dynamically**
|
|
11272
|
-
const sensitivity = volatility * 2; // Adjust the multiplier as needed
|
|
11273
|
-
const minGapBetweenLevels = volatility * 100; // Convert to percentage
|
|
11274
|
-
// Analyze each point in window for pivot status
|
|
11275
|
-
for (let j = 1; j < analysisWindow.length - 1; j++) {
|
|
11276
|
-
const curr = analysisWindow[j];
|
|
11277
|
-
const prevBar = analysisWindow[j - 1];
|
|
11278
|
-
const nextBar = analysisWindow[j + 1];
|
|
11279
|
-
// Check for high pivot
|
|
11280
|
-
if (curr.high > prevBar.high && curr.high > nextBar.high) {
|
|
11281
|
-
const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.high) / curr.high < sensitivity);
|
|
11282
|
-
if (existingPivot) {
|
|
11283
|
-
existingPivot.count++;
|
|
11284
|
-
existingPivot.volume += curr.vol; // **Include Volume**
|
|
11285
|
-
}
|
|
11286
|
-
else {
|
|
11287
|
-
pivotPoints.push({ price: curr.high, count: 1, volume: curr.vol });
|
|
11288
|
-
}
|
|
11289
|
-
}
|
|
11290
|
-
// Check for low pivot
|
|
11291
|
-
if (curr.low < prevBar.low && curr.low < nextBar.low) {
|
|
11292
|
-
const existingPivot = pivotPoints.find((p) => Math.abs(p.price - curr.low) / curr.low < sensitivity);
|
|
11293
|
-
if (existingPivot) {
|
|
11294
|
-
existingPivot.count++;
|
|
11295
|
-
existingPivot.volume += curr.vol; // **Include Volume**
|
|
11296
|
-
}
|
|
11297
|
-
else {
|
|
11298
|
-
pivotPoints.push({ price: curr.low, count: 1, volume: curr.vol });
|
|
11299
|
-
}
|
|
11300
|
-
}
|
|
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 };
|
|
11301
7705
|
}
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
pivotPoints.sort((a, b) => a.price - b.price);
|
|
11307
|
-
// Group close pivots
|
|
11308
|
-
let currentGroup = [];
|
|
11309
|
-
for (let j = 0; j < pivotPoints.length; j++) {
|
|
11310
|
-
if (currentGroup.length === 0) {
|
|
11311
|
-
currentGroup.push(pivotPoints[j]);
|
|
11312
|
-
}
|
|
11313
|
-
else {
|
|
11314
|
-
const lastPrice = currentGroup[currentGroup.length - 1].price;
|
|
11315
|
-
if ((Math.abs(pivotPoints[j].price - lastPrice) / lastPrice) * 100 <= minGapBetweenLevels) {
|
|
11316
|
-
currentGroup.push(pivotPoints[j]);
|
|
11317
|
-
}
|
|
11318
|
-
else {
|
|
11319
|
-
// Process current group
|
|
11320
|
-
if (currentGroup.length > 0) {
|
|
11321
|
-
const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
|
|
11322
|
-
const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
|
|
11323
|
-
const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
|
|
11324
|
-
levels.push({
|
|
11325
|
-
price: parseFloat(avgPrice.toFixed(2)),
|
|
11326
|
-
strength: parseFloat(totalStrength.toFixed(2)),
|
|
11327
|
-
type: avgPrice > currentPrice ? 'resistance' : 'support',
|
|
11328
|
-
});
|
|
11329
|
-
}
|
|
11330
|
-
currentGroup = [pivotPoints[j]];
|
|
11331
|
-
}
|
|
11332
|
-
}
|
|
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) };
|
|
11333
7710
|
}
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
const totalVolume = currentGroup.reduce((sum, p) => sum + p.volume, 0);
|
|
11337
|
-
const avgPrice = currentGroup.reduce((sum, p) => sum + p.price * p.volume, 0) / totalVolume;
|
|
11338
|
-
const totalStrength = currentGroup.reduce((sum, p) => sum + p.count * (p.volume / totalVolume), 0);
|
|
11339
|
-
levels.push({
|
|
11340
|
-
price: parseFloat(avgPrice.toFixed(2)),
|
|
11341
|
-
strength: parseFloat(totalStrength.toFixed(2)),
|
|
11342
|
-
type: avgPrice > currentPrice ? 'resistance' : 'support',
|
|
11343
|
-
});
|
|
7711
|
+
else {
|
|
7712
|
+
return __classPrivateFieldGet(this, _OpenAI_encoder, "f").call(this, { body, headers });
|
|
11344
7713
|
}
|
|
11345
|
-
// Sort by strength and limit
|
|
11346
|
-
const finalLevels = levels.sort((a, b) => b.strength - a.strength).slice(0, maxLevels);
|
|
11347
|
-
result.push({
|
|
11348
|
-
date: priceData[i].date,
|
|
11349
|
-
levels: finalLevels,
|
|
11350
|
-
close: currentPrice,
|
|
11351
|
-
});
|
|
11352
7714
|
}
|
|
11353
|
-
logIfDebug(`Found ${result.reduce((sum, r) => sum + r.levels.length, 0)} support/resistance levels across ${result.length} periods`);
|
|
11354
|
-
return result;
|
|
11355
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;
|
|
11356
7757
|
|
|
11357
7758
|
function getDefaultExportFromCjs (x) {
|
|
11358
7759
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -11374,6 +7775,7 @@ function requireConstants () {
|
|
|
11374
7775
|
|
|
11375
7776
|
constants = {
|
|
11376
7777
|
BINARY_TYPES,
|
|
7778
|
+
CLOSE_TIMEOUT: 30000,
|
|
11377
7779
|
EMPTY_BUFFER: Buffer.alloc(0),
|
|
11378
7780
|
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
|
|
11379
7781
|
hasBlob,
|
|
@@ -14144,6 +10546,7 @@ function requireWebsocket () {
|
|
|
14144
10546
|
|
|
14145
10547
|
const {
|
|
14146
10548
|
BINARY_TYPES,
|
|
10549
|
+
CLOSE_TIMEOUT,
|
|
14147
10550
|
EMPTY_BUFFER,
|
|
14148
10551
|
GUID,
|
|
14149
10552
|
kForOnEventAttribute,
|
|
@@ -14158,7 +10561,6 @@ function requireWebsocket () {
|
|
|
14158
10561
|
const { format, parse } = requireExtension();
|
|
14159
10562
|
const { toBuffer } = requireBufferUtil();
|
|
14160
10563
|
|
|
14161
|
-
const closeTimeout = 30 * 1000;
|
|
14162
10564
|
const kAborted = Symbol('kAborted');
|
|
14163
10565
|
const protocolVersions = [8, 13];
|
|
14164
10566
|
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
|
@@ -14214,6 +10616,7 @@ function requireWebsocket () {
|
|
|
14214
10616
|
initAsClient(this, address, protocols, options);
|
|
14215
10617
|
} else {
|
|
14216
10618
|
this._autoPong = options.autoPong;
|
|
10619
|
+
this._closeTimeout = options.closeTimeout;
|
|
14217
10620
|
this._isServer = true;
|
|
14218
10621
|
}
|
|
14219
10622
|
}
|
|
@@ -14755,6 +11158,8 @@ function requireWebsocket () {
|
|
|
14755
11158
|
* times in the same tick
|
|
14756
11159
|
* @param {Boolean} [options.autoPong=true] Specifies whether or not to
|
|
14757
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
|
|
14758
11163
|
* @param {Function} [options.finishRequest] A function which can be used to
|
|
14759
11164
|
* customize the headers of each http request before it is sent
|
|
14760
11165
|
* @param {Boolean} [options.followRedirects=false] Whether or not to follow
|
|
@@ -14781,6 +11186,7 @@ function requireWebsocket () {
|
|
|
14781
11186
|
const opts = {
|
|
14782
11187
|
allowSynchronousEvents: true,
|
|
14783
11188
|
autoPong: true,
|
|
11189
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
14784
11190
|
protocolVersion: protocolVersions[1],
|
|
14785
11191
|
maxPayload: 100 * 1024 * 1024,
|
|
14786
11192
|
skipUTF8Validation: false,
|
|
@@ -14799,6 +11205,7 @@ function requireWebsocket () {
|
|
|
14799
11205
|
};
|
|
14800
11206
|
|
|
14801
11207
|
websocket._autoPong = opts.autoPong;
|
|
11208
|
+
websocket._closeTimeout = opts.closeTimeout;
|
|
14802
11209
|
|
|
14803
11210
|
if (!protocolVersions.includes(opts.protocolVersion)) {
|
|
14804
11211
|
throw new RangeError(
|
|
@@ -15416,7 +11823,7 @@ function requireWebsocket () {
|
|
|
15416
11823
|
function setCloseTimer(websocket) {
|
|
15417
11824
|
websocket._closeTimer = setTimeout(
|
|
15418
11825
|
websocket._socket.destroy.bind(websocket._socket),
|
|
15419
|
-
|
|
11826
|
+
websocket._closeTimeout
|
|
15420
11827
|
);
|
|
15421
11828
|
}
|
|
15422
11829
|
|
|
@@ -15434,23 +11841,23 @@ function requireWebsocket () {
|
|
|
15434
11841
|
|
|
15435
11842
|
websocket._readyState = WebSocket.CLOSING;
|
|
15436
11843
|
|
|
15437
|
-
let chunk;
|
|
15438
|
-
|
|
15439
11844
|
//
|
|
15440
11845
|
// The close frame might not have been received or the `'end'` event emitted,
|
|
15441
11846
|
// for example, if the socket was destroyed due to an error. Ensure that the
|
|
15442
11847
|
// `receiver` stream is closed after writing any remaining buffered data to
|
|
15443
11848
|
// it. If the readable side of the socket is in flowing mode then there is no
|
|
15444
|
-
// buffered data as everything has been already written
|
|
15445
|
-
//
|
|
15446
|
-
//
|
|
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.
|
|
15447
11852
|
//
|
|
15448
11853
|
if (
|
|
15449
11854
|
!this._readableState.endEmitted &&
|
|
15450
11855
|
!websocket._closeFrameReceived &&
|
|
15451
11856
|
!websocket._receiver._writableState.errorEmitted &&
|
|
15452
|
-
|
|
11857
|
+
this._readableState.length !== 0
|
|
15453
11858
|
) {
|
|
11859
|
+
const chunk = this.read(this._readableState.length);
|
|
11860
|
+
|
|
15454
11861
|
websocket._receiver.write(chunk);
|
|
15455
11862
|
}
|
|
15456
11863
|
|
|
@@ -15782,7 +12189,7 @@ function requireWebsocketServer () {
|
|
|
15782
12189
|
const PerMessageDeflate = requirePermessageDeflate();
|
|
15783
12190
|
const subprotocol = requireSubprotocol();
|
|
15784
12191
|
const WebSocket = requireWebsocket();
|
|
15785
|
-
const { GUID, kWebSocket } = requireConstants();
|
|
12192
|
+
const { CLOSE_TIMEOUT, GUID, kWebSocket } = requireConstants();
|
|
15786
12193
|
|
|
15787
12194
|
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
|
|
15788
12195
|
|
|
@@ -15809,6 +12216,9 @@ function requireWebsocketServer () {
|
|
|
15809
12216
|
* pending connections
|
|
15810
12217
|
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
|
|
15811
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
|
|
15812
12222
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
15813
12223
|
* @param {String} [options.host] The hostname where to bind the server
|
|
15814
12224
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
@@ -15838,6 +12248,7 @@ function requireWebsocketServer () {
|
|
|
15838
12248
|
perMessageDeflate: false,
|
|
15839
12249
|
handleProtocols: null,
|
|
15840
12250
|
clientTracking: true,
|
|
12251
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
15841
12252
|
verifyClient: null,
|
|
15842
12253
|
noServer: false,
|
|
15843
12254
|
backlog: null, // use default (511 as implemented in net.js)
|
|
@@ -18457,6 +14868,7 @@ class AlpacaTradingAPI {
|
|
|
18457
14868
|
* @param qty (number) - the quantity of the order
|
|
18458
14869
|
* @param side (string) - the side of the order
|
|
18459
14870
|
* @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
|
|
14871
|
+
* @param client_order_id (string) - optional client order id
|
|
18460
14872
|
*/
|
|
18461
14873
|
async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
18462
14874
|
this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
@@ -18482,6 +14894,86 @@ class AlpacaTradingAPI {
|
|
|
18482
14894
|
throw error;
|
|
18483
14895
|
}
|
|
18484
14896
|
}
|
|
14897
|
+
/**
|
|
14898
|
+
* Create a Market on Open (MOO) order - executes in the opening auction
|
|
14899
|
+
*
|
|
14900
|
+
* IMPORTANT TIMING CONSTRAINTS:
|
|
14901
|
+
* - Valid submission window: After 7:00pm ET and before 9:28am ET
|
|
14902
|
+
* - Orders submitted between 9:28am and 7:00pm ET will be REJECTED
|
|
14903
|
+
* - Orders submitted after 7:00pm ET are queued for the next trading day's opening auction
|
|
14904
|
+
* - Example: An order at 8:00pm Monday will execute at Tuesday's market open (9:30am)
|
|
14905
|
+
*
|
|
14906
|
+
* @param symbol - The symbol of the order
|
|
14907
|
+
* @param qty - The quantity of shares
|
|
14908
|
+
* @param side - Buy or sell
|
|
14909
|
+
* @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
|
|
14910
|
+
* @param client_order_id - Optional client order id
|
|
14911
|
+
* @returns The created order
|
|
14912
|
+
*/
|
|
14913
|
+
async createMOOOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
14914
|
+
this.log(`Creating Market on Open order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
14915
|
+
symbol,
|
|
14916
|
+
});
|
|
14917
|
+
const body = {
|
|
14918
|
+
symbol,
|
|
14919
|
+
qty: Math.abs(qty).toString(),
|
|
14920
|
+
side,
|
|
14921
|
+
position_intent,
|
|
14922
|
+
type: 'market',
|
|
14923
|
+
time_in_force: 'opg',
|
|
14924
|
+
order_class: 'simple',
|
|
14925
|
+
};
|
|
14926
|
+
if (client_order_id !== undefined) {
|
|
14927
|
+
body.client_order_id = client_order_id;
|
|
14928
|
+
}
|
|
14929
|
+
try {
|
|
14930
|
+
return await this.makeRequest('/orders', 'POST', body);
|
|
14931
|
+
}
|
|
14932
|
+
catch (error) {
|
|
14933
|
+
this.log(`Error creating MOO order: ${error}`, { type: 'error' });
|
|
14934
|
+
throw error;
|
|
14935
|
+
}
|
|
14936
|
+
}
|
|
14937
|
+
/**
|
|
14938
|
+
* Create a Market on Close (MOC) order - executes in the closing auction
|
|
14939
|
+
*
|
|
14940
|
+
* IMPORTANT TIMING CONSTRAINTS:
|
|
14941
|
+
* - Valid submission window: After 7:00pm ET (previous day) and before 3:50pm ET (same day)
|
|
14942
|
+
* - Orders submitted between 3:50pm and 7:00pm ET will be REJECTED
|
|
14943
|
+
* - Orders submitted after 7:00pm ET are queued for the next trading day's closing auction
|
|
14944
|
+
* - Example: An order at 8:00pm Monday will execute at Tuesday's market close (4:00pm)
|
|
14945
|
+
*
|
|
14946
|
+
* @param symbol - The symbol of the order
|
|
14947
|
+
* @param qty - The quantity of shares
|
|
14948
|
+
* @param side - Buy or sell
|
|
14949
|
+
* @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
|
|
14950
|
+
* @param client_order_id - Optional client order id
|
|
14951
|
+
* @returns The created order
|
|
14952
|
+
*/
|
|
14953
|
+
async createMOCOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
14954
|
+
this.log(`Creating Market on Close order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
14955
|
+
symbol,
|
|
14956
|
+
});
|
|
14957
|
+
const body = {
|
|
14958
|
+
symbol,
|
|
14959
|
+
qty: Math.abs(qty).toString(),
|
|
14960
|
+
side,
|
|
14961
|
+
position_intent,
|
|
14962
|
+
type: 'market',
|
|
14963
|
+
time_in_force: 'cls',
|
|
14964
|
+
order_class: 'simple',
|
|
14965
|
+
};
|
|
14966
|
+
if (client_order_id !== undefined) {
|
|
14967
|
+
body.client_order_id = client_order_id;
|
|
14968
|
+
}
|
|
14969
|
+
try {
|
|
14970
|
+
return await this.makeRequest('/orders', 'POST', body);
|
|
14971
|
+
}
|
|
14972
|
+
catch (error) {
|
|
14973
|
+
this.log(`Error creating MOC order: ${error}`, { type: 'error' });
|
|
14974
|
+
throw error;
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
18485
14977
|
/**
|
|
18486
14978
|
* 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.
|
|
18487
14979
|
* @param symbol (string) - the symbol of the order
|
|
@@ -19466,226 +15958,73 @@ class AlpacaTradingAPI {
|
|
|
19466
15958
|
}
|
|
19467
15959
|
}
|
|
19468
15960
|
|
|
19469
|
-
const disco = {
|
|
19470
|
-
types: Types,
|
|
19471
|
-
alpaca: {
|
|
19472
|
-
TradingAPI: AlpacaTradingAPI,
|
|
19473
|
-
MarketDataAPI: AlpacaMarketDataAPI,
|
|
19474
|
-
},
|
|
19475
|
-
format: {
|
|
19476
|
-
capFirstLetter: capFirstLetter,
|
|
19477
|
-
currency: formatCurrency,
|
|
19478
|
-
number: formatNumber,
|
|
19479
|
-
pct: formatPercentage,
|
|
19480
|
-
dateTimeForGS: dateTimeForGS,
|
|
19481
|
-
},
|
|
19482
|
-
indices: {
|
|
19483
|
-
fetchAggregates: fetchIndicesAggregates,
|
|
19484
|
-
fetchPreviousClose: fetchIndicesPreviousClose,
|
|
19485
|
-
fetchDailyOpenClose: fetchIndicesDailyOpenClose,
|
|
19486
|
-
fetchSnapshot: fetchIndicesSnapshot,
|
|
19487
|
-
fetchUniversalSnapshot: fetchUniversalSnapshot,
|
|
19488
|
-
formatBarData: formatIndicesBarData,
|
|
19489
|
-
},
|
|
19490
|
-
llm: {
|
|
19491
|
-
call: makeLLMCall,
|
|
19492
|
-
seek: makeDeepseekCall,
|
|
19493
|
-
images: makeImagesCall,
|
|
19494
|
-
open: makeOpenRouterCall,
|
|
19495
|
-
},
|
|
19496
|
-
polygon: {
|
|
19497
|
-
fetchTickerInfo: fetchTickerInfo,
|
|
19498
|
-
fetchGroupedDaily: fetchGroupedDaily,
|
|
19499
|
-
fetchLastTrade: fetchLastTrade,
|
|
19500
|
-
fetchTrades: fetchTrades,
|
|
19501
|
-
fetchPrices: fetchPrices,
|
|
19502
|
-
analysePolygonPriceData: analysePolygonPriceData,
|
|
19503
|
-
formatPriceData: formatPriceData,
|
|
19504
|
-
fetchDailyOpenClose: fetchDailyOpenClose,
|
|
19505
|
-
getPreviousClose: getPreviousClose,
|
|
19506
|
-
},
|
|
19507
|
-
ta: {
|
|
19508
|
-
calculateEMA: calculateEMA,
|
|
19509
|
-
calculateMACD: calculateMACD,
|
|
19510
|
-
calculateRSI: calculateRSI,
|
|
19511
|
-
calculateStochasticOscillator: calculateStochasticOscillator,
|
|
19512
|
-
calculateBollingerBands: calculateBollingerBands,
|
|
19513
|
-
calculateSupportAndResistance: calculateSupportAndResistance,
|
|
19514
|
-
calculateFibonacciLevels: calculateFibonacciLevels,
|
|
19515
|
-
},
|
|
19516
|
-
time: {
|
|
19517
|
-
convertDateToMarketTimeZone: convertDateToMarketTimeZone,
|
|
19518
|
-
getStartAndEndDates: getStartAndEndDates,
|
|
19519
|
-
getMarketOpenClose: getMarketOpenClose,
|
|
19520
|
-
getOpenCloseForTradingDay: getOpenCloseForTradingDay,
|
|
19521
|
-
getLastFullTradingDate: getLastFullTradingDate,
|
|
19522
|
-
getNextMarketDay: getNextMarketDay,
|
|
19523
|
-
getPreviousMarketDay: getPreviousMarketDay,
|
|
19524
|
-
getMarketTimePeriod: getMarketTimePeriod,
|
|
19525
|
-
getMarketStatus: getMarketStatus,
|
|
19526
|
-
getNYTimeZone: getNYTimeZone,
|
|
19527
|
-
getTradingDate: getTradingDate,
|
|
19528
|
-
getTradingStartAndEndDates: getTradingStartAndEndDates,
|
|
19529
|
-
getTradingDaysBack: getTradingDaysBack,
|
|
19530
|
-
isMarketDay: isMarketDay,
|
|
19531
|
-
isWithinMarketHours: isWithinMarketHours,
|
|
19532
|
-
countTradingDays: countTradingDays,
|
|
19533
|
-
MARKET_TIMES: MARKET_TIMES,
|
|
19534
|
-
},
|
|
19535
|
-
utils: {
|
|
19536
|
-
logIfDebug: logIfDebug,
|
|
19537
|
-
fetchWithRetry: fetchWithRetry,
|
|
19538
|
-
validatePolygonApiKey: validatePolygonApiKey,
|
|
19539
|
-
Timer: PerformanceTimer,
|
|
19540
|
-
},
|
|
19541
|
-
};
|
|
19542
|
-
|
|
19543
15961
|
// Test file for context functionality
|
|
19544
|
-
function
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
{
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
}
|
|
19573
|
-
{
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
}
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19588
|
-
|
|
19589
|
-
|
|
19590
|
-
}
|
|
19591
|
-
{
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
}
|
|
19600
|
-
|
|
19601
|
-
|
|
19602
|
-
|
|
19603
|
-
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19608
|
-
|
|
19609
|
-
{
|
|
19610
|
-
label: '3 days back from Monday (skips weekend)',
|
|
19611
|
-
referenceDate: '2025-07-14T18:00:00-04:00',
|
|
19612
|
-
days: 3,
|
|
19613
|
-
expected: {
|
|
19614
|
-
date: '2025-07-10',
|
|
19615
|
-
marketOpenISO: '2025-07-10T13:30:00.000Z',
|
|
19616
|
-
},
|
|
19617
|
-
},
|
|
19618
|
-
{
|
|
19619
|
-
label: '1 day back from day after holiday (July 7, after July 4)',
|
|
19620
|
-
referenceDate: '2025-07-07T18:00:00-04:00',
|
|
19621
|
-
days: 1,
|
|
19622
|
-
expected: {
|
|
19623
|
-
date: '2025-07-07',
|
|
19624
|
-
marketOpenISO: '2025-07-07T13:30:00.000Z',
|
|
19625
|
-
},
|
|
19626
|
-
},
|
|
19627
|
-
{
|
|
19628
|
-
label: '2 days back from day after holiday (skips July 4)',
|
|
19629
|
-
referenceDate: '2025-07-07T18:00:00-04:00',
|
|
19630
|
-
days: 2,
|
|
19631
|
-
expected: {
|
|
19632
|
-
date: '2025-07-03',
|
|
19633
|
-
marketOpenISO: '2025-07-03T13:30:00.000Z',
|
|
19634
|
-
},
|
|
19635
|
-
},
|
|
19636
|
-
{
|
|
19637
|
-
label: 'Early close day (July 3) - 1 day back from July 7',
|
|
19638
|
-
referenceDate: '2025-07-07T18:00:00-04:00',
|
|
19639
|
-
days: 2,
|
|
19640
|
-
expected: {
|
|
19641
|
-
date: '2025-07-03',
|
|
19642
|
-
marketOpenISO: '2025-07-03T13:30:00.000Z', // Still opens at normal time
|
|
19643
|
-
},
|
|
19644
|
-
},
|
|
19645
|
-
];
|
|
19646
|
-
console.log('\n=== Testing getTradingDaysBack ===\n');
|
|
19647
|
-
for (const { label, referenceDate, days, expected } of testCases) {
|
|
19648
|
-
try {
|
|
19649
|
-
const refDate = new Date(referenceDate);
|
|
19650
|
-
const result = disco.time.getTradingDaysBack({ referenceDate: refDate, days });
|
|
19651
|
-
const dateMatches = result.date === expected.date;
|
|
19652
|
-
const isoMatches = result.marketOpenISO === expected.marketOpenISO;
|
|
19653
|
-
const unixValid = typeof result.unixTimestamp === 'number' && result.unixTimestamp > 0;
|
|
19654
|
-
// Verify unix timestamp matches ISO
|
|
19655
|
-
const dateFromUnix = new Date(result.unixTimestamp * 1000).toISOString();
|
|
19656
|
-
const unixMatches = dateFromUnix === result.marketOpenISO;
|
|
19657
|
-
const allPass = dateMatches && isoMatches && unixValid && unixMatches;
|
|
19658
|
-
console.log(`${allPass ? '✅' : '❌'} ${label}`);
|
|
19659
|
-
if (!allPass) {
|
|
19660
|
-
console.log(` Expected: ${JSON.stringify(expected)}`);
|
|
19661
|
-
console.log(` Got: ${JSON.stringify(result)}`);
|
|
19662
|
-
if (!dateMatches)
|
|
19663
|
-
console.log(` ❌ Date mismatch`);
|
|
19664
|
-
if (!isoMatches)
|
|
19665
|
-
console.log(` ❌ ISO mismatch`);
|
|
19666
|
-
if (!unixValid)
|
|
19667
|
-
console.log(` ❌ Unix timestamp invalid`);
|
|
19668
|
-
if (!unixMatches)
|
|
19669
|
-
console.log(` ❌ Unix timestamp doesn't match ISO`);
|
|
19670
|
-
}
|
|
19671
|
-
}
|
|
19672
|
-
catch (error) {
|
|
19673
|
-
console.log(`❌ ${label}`);
|
|
19674
|
-
console.log(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
19675
|
-
}
|
|
15962
|
+
async function testMOOAndMOCOrders() {
|
|
15963
|
+
console.log('\n--- Testing Market on Open and Market on Close Orders ---');
|
|
15964
|
+
console.log('NOTE: MOO orders must be submitted after 7:00pm ET and before 9:28am ET');
|
|
15965
|
+
console.log('NOTE: MOC orders must be submitted after 7:00pm ET and before 3:50pm ET');
|
|
15966
|
+
console.log('Orders submitted outside these windows will be rejected.\n');
|
|
15967
|
+
const log = (message, options = { type: 'info' }) => {
|
|
15968
|
+
log$1(message, { ...options, source: 'Test' });
|
|
15969
|
+
};
|
|
15970
|
+
if (!process.env.ALPACA_TRADING_API_KEY ||
|
|
15971
|
+
!process.env.ALPACA_TRADING_SECRET_KEY ||
|
|
15972
|
+
!process.env.ALPACA_TRADING_ACCOUNT_TYPE) {
|
|
15973
|
+
log('Missing required ALPACA_TRADING_* environment variables', { type: 'error' });
|
|
15974
|
+
return;
|
|
15975
|
+
}
|
|
15976
|
+
const credentials = {
|
|
15977
|
+
accountName: 'Test Account',
|
|
15978
|
+
apiKey: process.env.ALPACA_TRADING_API_KEY,
|
|
15979
|
+
apiSecret: process.env.ALPACA_TRADING_SECRET_KEY,
|
|
15980
|
+
type: process.env.ALPACA_TRADING_ACCOUNT_TYPE,
|
|
15981
|
+
orderType: 'limit',
|
|
15982
|
+
engine: 'quant',
|
|
15983
|
+
};
|
|
15984
|
+
const tradingAPI = AlpacaTradingAPI.getInstance(credentials);
|
|
15985
|
+
try {
|
|
15986
|
+
// Test creating a Market on Open order
|
|
15987
|
+
log('Creating Market on Open (MOO) order for SPY...');
|
|
15988
|
+
const mooOrder = await tradingAPI.createMOOOrder('SPY', 1, 'buy', 'buy_to_open', 'test-moo-order');
|
|
15989
|
+
log(`MOO order created successfully: ${mooOrder.id}`);
|
|
15990
|
+
log(` Symbol: ${mooOrder.symbol}`);
|
|
15991
|
+
log(` Qty: ${mooOrder.qty}`);
|
|
15992
|
+
log(` Side: ${mooOrder.side}`);
|
|
15993
|
+
log(` Type: ${mooOrder.type}`);
|
|
15994
|
+
log(` Time in Force: ${mooOrder.time_in_force}`);
|
|
15995
|
+
log(` Status: ${mooOrder.status}`);
|
|
15996
|
+
// Wait a moment before canceling
|
|
15997
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
15998
|
+
// Cancel the MOO order
|
|
15999
|
+
log(`Canceling MOO order ${mooOrder.id}...`);
|
|
16000
|
+
await tradingAPI.cancelOrder(mooOrder.id);
|
|
16001
|
+
log(`MOO order canceled successfully`);
|
|
16002
|
+
// Wait a moment before next order
|
|
16003
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
16004
|
+
// Test creating a Market on Close order
|
|
16005
|
+
log('Creating Market on Close (MOC) order for SPY...');
|
|
16006
|
+
const mocOrder = await tradingAPI.createMOCOrder('SPY', 1, 'sell', 'sell_to_open', 'test-moc-order');
|
|
16007
|
+
log(`MOC order created successfully: ${mocOrder.id}`);
|
|
16008
|
+
log(` Symbol: ${mocOrder.symbol}`);
|
|
16009
|
+
log(` Qty: ${mocOrder.qty}`);
|
|
16010
|
+
log(` Side: ${mocOrder.side}`);
|
|
16011
|
+
log(` Type: ${mocOrder.type}`);
|
|
16012
|
+
log(` Time in Force: ${mocOrder.time_in_force}`);
|
|
16013
|
+
log(` Status: ${mocOrder.status}`);
|
|
16014
|
+
// Wait a moment before canceling
|
|
16015
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
16016
|
+
// Cancel the MOC order
|
|
16017
|
+
log(`Canceling MOC order ${mocOrder.id}...`);
|
|
16018
|
+
await tradingAPI.cancelOrder(mocOrder.id);
|
|
16019
|
+
log(`MOC order canceled successfully`);
|
|
16020
|
+
log('\nMOO/MOC order test completed successfully');
|
|
16021
|
+
}
|
|
16022
|
+
catch (error) {
|
|
16023
|
+
log(`Error during MOO/MOC order test: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
|
16024
|
+
type: 'error',
|
|
16025
|
+
});
|
|
16026
|
+
throw error;
|
|
19676
16027
|
}
|
|
19677
|
-
const totalTests = testCases.length;
|
|
19678
|
-
const passedTests = testCases.filter((tc) => {
|
|
19679
|
-
try {
|
|
19680
|
-
const refDate = new Date(tc.referenceDate);
|
|
19681
|
-
const result = disco.time.getTradingDaysBack({ referenceDate: refDate, days: tc.days });
|
|
19682
|
-
return result.date === tc.expected.date && result.marketOpenISO === tc.expected.marketOpenISO;
|
|
19683
|
-
}
|
|
19684
|
-
catch {
|
|
19685
|
-
return false;
|
|
19686
|
-
}
|
|
19687
|
-
}).length;
|
|
19688
|
-
console.log(`\n=== Summary: ${passedTests}/${totalTests} tests passed ===\n`);
|
|
19689
16028
|
}
|
|
19690
16029
|
// testGetTradingDate();
|
|
19691
16030
|
// testGetTradingStartAndEndDates();
|
|
@@ -19704,5 +16043,6 @@ function testGetTradingDaysBack() {
|
|
|
19704
16043
|
// testMarketDataAPI();
|
|
19705
16044
|
// testLLM();
|
|
19706
16045
|
// testImageModelDefaults();
|
|
19707
|
-
testGetTradingDaysBack();
|
|
16046
|
+
// testGetTradingDaysBack();
|
|
16047
|
+
testMOOAndMOCOrders();
|
|
19708
16048
|
//# sourceMappingURL=test.js.map
|