@discomedia/utils 1.0.5 → 1.0.6

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.cjs CHANGED
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var ms = require('ms');
4
- var dateFnsTz = require('date-fns-tz');
5
3
  var dateFns = require('date-fns');
4
+ var dateFnsTz = require('date-fns-tz');
6
5
  var require$$0$3 = require('events');
7
6
  var require$$1$1 = require('https');
8
7
  var require$$2 = require('http');
@@ -14,170 +13,34 @@ var require$$7 = require('url');
14
13
  var require$$0 = require('zlib');
15
14
  var require$$0$1 = require('buffer');
16
15
 
17
- // time-utils.ts
18
- // Helper function to convert timestamp to Unix timestamp in seconds
19
- const toUnixTimestamp = (ts) => {
20
- return Math.floor(new Date(ts).getTime() / 1000);
21
- };
22
- function getTimeAgo(dateString) {
23
- // if format is like this: '20240919T102005', then first convert to '2024-09-19T10:20:05' format
24
- let dateValue = dateString;
25
- if (dateString && dateString.length === 15) {
26
- dateValue = dateString.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6');
27
- }
28
- const date = new Date(dateValue);
29
- const now = new Date();
30
- const diff = now.getTime() - date.getTime();
31
- const seconds = Math.floor(diff / 1000);
32
- const minutes = Math.floor(seconds / 60);
33
- const hours = Math.floor(minutes / 60);
34
- const days = Math.floor(hours / 24);
35
- const months = Math.floor(days / 30);
36
- const years = Math.floor(months / 12);
37
- if (years > 0) {
38
- return years === 1 ? '1 year ago' : `${years} years ago`;
39
- }
40
- else if (months > 0) {
41
- return months === 1 ? '1 month ago' : `${months} months ago`;
42
- }
43
- else if (days > 0) {
44
- return days === 1 ? '1 day ago' : `${days} days ago`;
45
- }
46
- else if (hours > 0) {
47
- return hours === 1 ? '1 hr ago' : `${hours} hrs ago`;
48
- }
49
- else if (minutes > 0) {
50
- return minutes === 1 ? '1 min ago' : `${minutes} mins ago`;
51
- }
52
- else {
53
- return 'A few seconds ago';
54
- }
55
- }
56
- function normalizeDate(timestamp) {
57
- const date = new Date(timestamp);
58
- return date.toISOString().split('T')[0]; // Returns 'YYYY-MM-DD'
59
- }
60
- // the function formerly known as CalculateRange, like a camel with two humps. Gross
61
- function calculateTimeRange(range) {
62
- const currentDate = new Date();
63
- switch (range) {
64
- case '1d':
65
- currentDate.setDate(currentDate.getDate() - 1);
66
- break;
67
- case '3d':
68
- currentDate.setDate(currentDate.getDate() - 3);
69
- break;
70
- case '1w':
71
- currentDate.setDate(currentDate.getDate() - 7);
72
- break;
73
- case '1m':
74
- currentDate.setMonth(currentDate.getMonth() - 1);
75
- break;
76
- case '3m':
77
- currentDate.setMonth(currentDate.getMonth() - 3);
78
- break;
79
- case '1y':
80
- currentDate.setFullYear(currentDate.getFullYear() - 1);
81
- break;
82
- default:
83
- throw new Error(`Invalid range: ${range}`);
16
+ /**
17
+ * Logs a message to the console.
18
+ * @param message The message to log.
19
+ * @param options Optional options.
20
+ * @param options.source The source of the message.
21
+ * @param options.type The type of message to log.
22
+ * @param options.symbol The trading symbol associated with this log.
23
+ * @param options.account The account associated with this log.
24
+ */
25
+ function log$1(message, options = { source: 'App', type: 'info' }) {
26
+ // Format the timestamp
27
+ const date = new Date();
28
+ const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
29
+ const account = options?.account;
30
+ const symbol = options?.symbol;
31
+ // Build the log message
32
+ const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
33
+ // Use appropriate console method based on type
34
+ if (options?.type === 'error') {
35
+ console.error(logMessage);
84
36
  }
85
- return currentDate.toISOString().split('T')[0]; // format date to 'YYYY-MM-DD'
86
- }
87
- const daysLeft = (accountCreationDate, maxDays) => {
88
- const now = new Date();
89
- const endPeriodDate = new Date(accountCreationDate);
90
- endPeriodDate.setDate(accountCreationDate.getDate() + maxDays);
91
- const diffInMilliseconds = endPeriodDate.getTime() - now.getTime();
92
- // Convert milliseconds to days and return
93
- return Math.ceil(diffInMilliseconds / (1000 * 60 * 60 * 24));
94
- };
95
- const cutoffDate = new Date('2023-10-17T00:00:00.000Z');
96
- const calculateDaysLeft = (accountCreationDate) => {
97
- let maxDays;
98
- if (accountCreationDate < cutoffDate) {
99
- maxDays = 30;
100
- accountCreationDate = new Date('2023-10-01T00:00:00.000Z');
37
+ else if (options?.type === 'warn') {
38
+ console.warn(logMessage);
101
39
  }
102
40
  else {
103
- maxDays = 14;
104
- }
105
- return daysLeft(accountCreationDate, maxDays);
106
- };
107
- const timeAgo = (timestamp) => {
108
- if (!timestamp)
109
- return 'Just now';
110
- const diff = Date.now() - new Date(timestamp).getTime();
111
- if (diff < 60000) {
112
- // less than 1 second
113
- return 'Just now';
114
- }
115
- else if (diff > 82800000) {
116
- // more than 23 hours – similar to how Twitter displays timestamps
117
- return new Date(timestamp).toLocaleDateString('en-US', {
118
- month: 'short',
119
- day: 'numeric',
120
- year: new Date(timestamp).getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
121
- });
122
- }
123
- return `${ms(diff)} ago`;
124
- };
125
- // returns date utc
126
- const formatDate = (dateString, updateDate) => {
127
- return new Date(dateString).toLocaleDateString('en-US', {
128
- day: 'numeric',
129
- month: 'long',
130
- year: updateDate && new Date(dateString).getFullYear() === new Date().getFullYear() ? undefined : 'numeric',
131
- timeZone: 'UTC',
132
- });
133
- };
134
- const parseETDateFromAV = (dateString) => {
135
- // Time zone identifier for Eastern Time
136
- const timeZone = 'America/New_York';
137
- // Split the input string into date and time components
138
- const [datePart, timePart] = dateString.split(' ');
139
- // Construct a full date-time string in ISO format
140
- const fullString = `${datePart}T${timePart}`;
141
- // Convert the string to a UTC Date object using date-fns-tz
142
- const utcDate = dateFnsTz.fromZonedTime(fullString, timeZone); // Convert to UTC
143
- return utcDate;
144
- };
145
- const formatToUSEastern = (date, justDate) => {
146
- const options = {
147
- timeZone: 'America/New_York',
148
- month: 'short',
149
- day: 'numeric',
150
- year: 'numeric',
151
- };
152
- if (!justDate) {
153
- options.hour = 'numeric';
154
- options.minute = '2-digit';
155
- options.hour12 = true;
41
+ console.log(logMessage);
156
42
  }
157
- return date.toLocaleString('en-US', options);
158
- };
159
- const unixTimetoUSEastern = (timestamp) => {
160
- const date = new Date(timestamp);
161
- const timeString = formatToUSEastern(date);
162
- const dateString = formatToUSEastern(date, true);
163
- return { date, timeString, dateString };
164
- };
165
- const timeDiffString = (milliseconds) => {
166
- const seconds = Math.floor(milliseconds / 1000);
167
- const minutes = Math.floor(seconds / 60);
168
- const hours = Math.floor(minutes / 60);
169
- const days = Math.floor(hours / 24);
170
- const remainingHours = hours % 24;
171
- const remainingMinutes = minutes % 60;
172
- const parts = [];
173
- if (days > 0)
174
- parts.push(`${days} day${days > 1 ? 's' : ''}`);
175
- if (remainingHours > 0)
176
- parts.push(`${remainingHours} hour${remainingHours > 1 ? 's' : ''}`);
177
- if (remainingMinutes > 0)
178
- parts.push(`${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}`);
179
- return parts.join(', ');
180
- };
43
+ }
181
44
 
182
45
  // market-hours.ts
183
46
  const marketHolidays = {
@@ -282,99 +145,66 @@ const marketEarlyCloses = {
282
145
  },
283
146
  };
284
147
 
285
- // market-time.ts
148
+ // market-time.ts - Refactored for better organization and usability
149
+ // ===== CONFIGURATION =====
286
150
  /**
287
- * Market times for NYSE
288
- * Regular market hours are 9:30am-4:00pm
289
- * Early market hours are 9:30am-10:00am (first 30 minutes)
290
- * Extended market hours are 4:00am to 9:30am and 4:00pm-8:00pm
291
- * On days before some holidays, the market closes early at 1:00pm
292
- * Early extended market hours are 1:00pm-5:00pm on early close days
151
+ * Market configuration constants
293
152
  */
294
- const MARKET_TIMES = {
153
+ const MARKET_CONFIG = {
295
154
  TIMEZONE: 'America/New_York',
296
- PRE: { START: { HOUR: 4, MINUTE: 0, MINUTES: 240 }, END: { HOUR: 9, MINUTE: 30, MINUTES: 570 } },
297
- EARLY_MORNING: { START: { HOUR: 9, MINUTE: 30, MINUTES: 570 }, END: { HOUR: 10, MINUTE: 0, MINUTES: 600 } }, // early market trading
298
- EARLY_CLOSE_BEFORE_HOLIDAY: {
299
- START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
300
- END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
301
- }, // early market trading end
302
- EARLY_EXTENDED_BEFORE_HOLIDAY: {
303
- START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
304
- END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
305
- }, // extended hours trading on early close days
306
- REGULAR: { START: { HOUR: 9, MINUTE: 30, MINUTES: 570 }, END: { HOUR: 16, MINUTE: 0, MINUTES: 960 } },
307
- EXTENDED: { START: { HOUR: 4, MINUTE: 0, MINUTES: 240 }, END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 } },
155
+ TIMES: {
156
+ EXTENDED_START: { hour: 4, minute: 0 },
157
+ MARKET_OPEN: { hour: 9, minute: 30 },
158
+ EARLY_MARKET_END: { hour: 10, minute: 0 },
159
+ MARKET_CLOSE: { hour: 16, minute: 0 },
160
+ EARLY_CLOSE: { hour: 13, minute: 0 },
161
+ EXTENDED_END: { hour: 20, minute: 0 },
162
+ EARLY_EXTENDED_END: { hour: 17, minute: 0 },
163
+ },
308
164
  };
165
+ // ===== MARKET CALENDAR SERVICE =====
309
166
  /**
310
- * Utility class for handling market time-related operations
167
+ * Service for handling market calendar operations (holidays, early closes, market days)
311
168
  */
312
- class MarketTimeUtil {
169
+ class MarketCalendar {
313
170
  timezone;
314
- intradayReporting;
315
- /**
316
- * Creates a new MarketTimeUtil instance
317
- * @param {string} [timezone='America/New_York'] - The timezone to use for market time calculations
318
- * @param {IntradayReporting} [intradayReporting='market_hours'] - The intraday reporting mode
319
- */
320
- constructor(timezone = MARKET_TIMES.TIMEZONE, intradayReporting = 'market_hours') {
321
- this.validateTimezone(timezone);
171
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
322
172
  this.timezone = timezone;
323
- this.intradayReporting = intradayReporting;
324
173
  }
325
174
  /**
326
- * Validates the provided timezone
327
- * @private
328
- * @param {string} timezone - The timezone to validate
329
- * @throws {Error} If the timezone is invalid
175
+ * Check if a date is a weekend
330
176
  */
331
- validateTimezone(timezone) {
332
- try {
333
- Intl.DateTimeFormat(undefined, { timeZone: timezone });
334
- }
335
- catch (error) {
336
- throw new Error(`Invalid timezone: ${timezone}`);
337
- }
338
- }
339
- formatDate(date, outputFormat = 'iso') {
340
- switch (outputFormat) {
341
- case 'unix-seconds':
342
- return Math.floor(date.getTime() / 1000);
343
- case 'unix-ms':
344
- return date.getTime();
345
- case 'iso':
346
- default:
347
- // return with timezone offset
348
- return dateFnsTz.formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
349
- }
350
- }
351
177
  isWeekend(date) {
352
178
  const day = date.getDay();
353
- return day === 0 || day === 6;
179
+ return day === 0 || day === 6; // Sunday or Saturday
354
180
  }
181
+ /**
182
+ * Check if a date is a market holiday
183
+ */
355
184
  isHoliday(date) {
356
- const formattedDate = dateFns.format(date, 'yyyy-MM-dd');
357
- const yearHolidays = marketHolidays[date.getFullYear()];
358
- for (const holiday in yearHolidays) {
359
- if (yearHolidays[holiday].date === formattedDate) {
360
- return true;
361
- }
362
- }
363
- return false;
185
+ const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
186
+ const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
187
+ const yearHolidays = marketHolidays[year];
188
+ if (!yearHolidays)
189
+ return false;
190
+ return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
364
191
  }
192
+ /**
193
+ * Check if a date is an early close day
194
+ */
365
195
  isEarlyCloseDay(date) {
366
- const formattedDate = dateFns.format(date, 'yyyy-MM-dd');
367
- const yearEarlyCloses = marketEarlyCloses[date.getFullYear()];
196
+ const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
197
+ const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
198
+ const yearEarlyCloses = marketEarlyCloses[year];
368
199
  return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
369
200
  }
370
201
  /**
371
- * Get the early close time for a given date
372
- * @param date - The date to get the early close time for
373
- * @returns The early close time in minutes from midnight, or null if there is no early close
202
+ * Get the early close time for a date (in minutes from midnight)
374
203
  */
375
204
  getEarlyCloseTime(date) {
376
- const formattedDate = dateFns.format(date, 'yyyy-MM-dd');
377
- const yearEarlyCloses = marketEarlyCloses[date.getFullYear()];
205
+ const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
206
+ const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
207
+ const yearEarlyCloses = marketEarlyCloses[year];
378
208
  if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
379
209
  const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
380
210
  return hours * 60 + minutes;
@@ -382,194 +212,283 @@ class MarketTimeUtil {
382
212
  return null;
383
213
  }
384
214
  /**
385
- * Check if a given date is a market day
386
- * @param date - The date to check
387
- * @returns true if the date is a market day, false otherwise
215
+ * Check if a date is a market day (not weekend or holiday)
388
216
  */
389
217
  isMarketDay(date) {
390
- const isWeekendDay = this.isWeekend(date);
391
- const isHolidayDay = this.isHoliday(date);
392
- const returner = !isWeekendDay && !isHolidayDay;
393
- return returner;
218
+ return !this.isWeekend(date) && !this.isHoliday(date);
394
219
  }
395
220
  /**
396
- * Check if a given date is within market hours
397
- * @param date - The date to check
398
- * @returns true if the date is within market hours, false otherwise
221
+ * Get the next market day from a given date
399
222
  */
400
- isWithinMarketHours(date) {
401
- // Check for holidays first
402
- if (this.isHoliday(date)) {
403
- return false;
223
+ getNextMarketDay(date) {
224
+ let nextDay = dateFns.add(date, { days: 1 });
225
+ while (!this.isMarketDay(nextDay)) {
226
+ nextDay = dateFns.add(nextDay, { days: 1 });
404
227
  }
405
- const timeInMinutes = date.getHours() * 60 + date.getMinutes();
406
- // Check for early closure
407
- if (this.isEarlyCloseDay(date)) {
408
- const earlyCloseMinutes = this.getEarlyCloseTime(date);
409
- if (earlyCloseMinutes !== null && timeInMinutes > earlyCloseMinutes) {
410
- return false;
411
- }
228
+ return nextDay;
229
+ }
230
+ /**
231
+ * Get the previous market day from a given date
232
+ */
233
+ getPreviousMarketDay(date) {
234
+ let prevDay = dateFns.sub(date, { days: 1 });
235
+ while (!this.isMarketDay(prevDay)) {
236
+ prevDay = dateFns.sub(prevDay, { days: 1 });
412
237
  }
413
- // Regular market hours logic
414
- let returner;
415
- switch (this.intradayReporting) {
416
- case 'extended_hours': {
417
- const extendedStartMinutes = MARKET_TIMES.EXTENDED.START.HOUR * 60 + MARKET_TIMES.EXTENDED.START.MINUTE;
418
- const extendedEndMinutes = MARKET_TIMES.EXTENDED.END.HOUR * 60 + MARKET_TIMES.EXTENDED.END.MINUTE;
419
- // Comprehensive handling of times crossing midnight
420
- const adjustedDate = timeInMinutes < extendedStartMinutes ? dateFns.sub(date, { days: 1 }) : date;
421
- const adjustedTimeInMinutes = adjustedDate.getHours() * 60 + adjustedDate.getMinutes();
422
- returner = adjustedTimeInMinutes >= extendedStartMinutes && adjustedTimeInMinutes <= extendedEndMinutes;
423
- break;
424
- }
425
- case 'continuous':
426
- returner = true;
427
- break;
428
- default: {
429
- // market_hours
430
- const regularStartMinutes = MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
431
- const regularEndMinutes = MARKET_TIMES.REGULAR.END.HOUR * 60 + MARKET_TIMES.REGULAR.END.MINUTE;
432
- returner = timeInMinutes >= regularStartMinutes && timeInMinutes <= regularEndMinutes;
433
- break;
434
- }
238
+ return prevDay;
239
+ }
240
+ }
241
+ // ===== TIME FORMATTER SERVICE =====
242
+ /**
243
+ * Service for formatting time outputs
244
+ */
245
+ class TimeFormatter {
246
+ timezone;
247
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
248
+ this.timezone = timezone;
249
+ }
250
+ /**
251
+ * Format a date based on the output format
252
+ */
253
+ formatDate(date, outputFormat = 'iso') {
254
+ switch (outputFormat) {
255
+ case 'unix-seconds':
256
+ return Math.floor(date.getTime() / 1000);
257
+ case 'unix-ms':
258
+ return date.getTime();
259
+ case 'iso':
260
+ default:
261
+ return dateFnsTz.formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
435
262
  }
436
- return returner;
437
263
  }
438
264
  /**
439
- * Check if a given date is before market hours
440
- * @param date - The date to check
441
- * @returns true if the date is before market hours, false otherwise
265
+ * Get New York timezone offset
442
266
  */
443
- isBeforeMarketHours(date) {
444
- const timeInMinutes = date.getHours() * 60 + date.getMinutes();
445
- const startMinutes = this.intradayReporting === 'extended_hours'
446
- ? MARKET_TIMES.EXTENDED.START.HOUR * 60 + MARKET_TIMES.EXTENDED.START.MINUTE
447
- : MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
448
- return timeInMinutes < startMinutes;
267
+ getNYTimeZone(date = new Date()) {
268
+ const dtf = new Intl.DateTimeFormat('en-US', {
269
+ timeZone: this.timezone,
270
+ timeZoneName: 'shortOffset',
271
+ });
272
+ const parts = dtf.formatToParts(date);
273
+ const tz = parts.find(p => p.type === 'timeZoneName')?.value;
274
+ if (!tz) {
275
+ throw new Error('Could not determine New York offset');
276
+ }
277
+ const shortOffset = tz.replace('GMT', '');
278
+ if (shortOffset === '-4') {
279
+ return '-04:00';
280
+ }
281
+ else if (shortOffset === '-5') {
282
+ return '-05:00';
283
+ }
284
+ else {
285
+ throw new Error(`Unexpected timezone offset: ${shortOffset}`);
286
+ }
449
287
  }
450
288
  /**
451
- * Get the last trading date, i.e. the last date that was a market day
452
- * @param currentDate - The current date
453
- * @returns The last trading date
289
+ * Get trading date in YYYY-MM-DD format
454
290
  */
455
- getLastTradingDate(currentDate = new Date()) {
456
- const nowET = dateFnsTz.toZonedTime(currentDate, this.timezone);
457
- const isMarketDayToday = this.isMarketDay(nowET);
458
- const currentMinutes = nowET.getHours() * 60 + nowET.getMinutes();
459
- const marketOpenMinutes = MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
460
- if (isMarketDayToday && currentMinutes >= marketOpenMinutes) {
461
- // After market open on a market day, return today
462
- return nowET;
291
+ getTradingDate(time) {
292
+ let date;
293
+ if (typeof time === 'number') {
294
+ date = new Date(time);
295
+ }
296
+ else if (typeof time === 'string') {
297
+ date = new Date(time);
463
298
  }
464
299
  else {
465
- // Before market open, or not a market day, return previous trading day
466
- let lastTradingDate = dateFns.sub(nowET, { days: 1 });
467
- while (!this.isMarketDay(lastTradingDate)) {
468
- lastTradingDate = dateFns.sub(lastTradingDate, { days: 1 });
469
- }
470
- return lastTradingDate;
300
+ date = time;
471
301
  }
302
+ return dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
472
303
  }
473
- getLastMarketDay(date) {
474
- let currentDate = dateFns.sub(date, { days: 1 });
475
- while (!this.isMarketDay(currentDate)) {
476
- currentDate = dateFns.sub(currentDate, { days: 1 });
304
+ }
305
+ // ===== MARKET TIME CALCULATOR =====
306
+ /**
307
+ * Service for core market time calculations
308
+ */
309
+ class MarketTimeCalculator {
310
+ calendar;
311
+ formatter;
312
+ timezone;
313
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
314
+ this.timezone = timezone;
315
+ this.calendar = new MarketCalendar(timezone);
316
+ this.formatter = new TimeFormatter(timezone);
317
+ }
318
+ /**
319
+ * Get market open/close times for a date
320
+ */
321
+ getMarketTimes(date) {
322
+ const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
323
+ // Market closed on weekends and holidays
324
+ if (!this.calendar.isMarketDay(zonedDate)) {
325
+ return {
326
+ marketOpen: false,
327
+ open: null,
328
+ close: null,
329
+ openExt: null,
330
+ closeExt: null,
331
+ };
332
+ }
333
+ const dayStart = dateFns.startOfDay(zonedDate);
334
+ // Regular market times
335
+ const open = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour, minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute }), this.timezone);
336
+ let close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute }), this.timezone);
337
+ // Extended hours
338
+ const openExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute }), this.timezone);
339
+ let closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute }), this.timezone);
340
+ // Handle early close days
341
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
342
+ close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute }), this.timezone);
343
+ closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute }), this.timezone);
344
+ }
345
+ return {
346
+ marketOpen: true,
347
+ open,
348
+ close,
349
+ openExt,
350
+ closeExt,
351
+ };
352
+ }
353
+ /**
354
+ * Check if a time is within market hours based on intraday reporting mode
355
+ */
356
+ isWithinMarketHours(date, intradayReporting = 'market_hours') {
357
+ const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
358
+ // Not a market day
359
+ if (!this.calendar.isMarketDay(zonedDate)) {
360
+ return false;
361
+ }
362
+ const timeInMinutes = zonedDate.getHours() * 60 + zonedDate.getMinutes();
363
+ switch (intradayReporting) {
364
+ case 'extended_hours': {
365
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
366
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
367
+ // Handle early close
368
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
369
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
370
+ }
371
+ return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
372
+ }
373
+ case 'continuous':
374
+ return true;
375
+ default: {
376
+ // 'market_hours'
377
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
378
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
379
+ // Handle early close
380
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
381
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
382
+ }
383
+ return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
384
+ }
477
385
  }
478
- return currentDate;
479
386
  }
387
+ /**
388
+ * Get the last full trading date
389
+ */
480
390
  getLastFullTradingDate(currentDate = new Date()) {
481
391
  const nowET = dateFnsTz.toZonedTime(currentDate, this.timezone);
482
- // If today is a market day and we're after extended hours close
483
- // then return today since it's a completed trading day
484
- if (this.isMarketDay(nowET)) {
392
+ // If today is a market day and we're after extended hours close, return today
393
+ if (this.calendar.isMarketDay(nowET)) {
485
394
  const timeInMinutes = nowET.getHours() * 60 + nowET.getMinutes();
486
- const extendedEndMinutes = MARKET_TIMES.EXTENDED.END.HOUR * 60 + MARKET_TIMES.EXTENDED.END.MINUTE;
487
- // Check if we're after market close (including extended hours)
395
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
396
+ if (this.calendar.isEarlyCloseDay(nowET)) {
397
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
398
+ }
488
399
  if (timeInMinutes >= extendedEndMinutes) {
489
- // Set to midnight ET while preserving the date
490
400
  return dateFnsTz.fromZonedTime(dateFns.set(nowET, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
491
401
  }
492
402
  }
493
- // In all other cases (during trading hours, before market open, holidays, weekends),
494
- // we want the last completed trading day
495
- let lastFullDate = this.getLastMarketDay(nowET);
496
- // Set to midnight ET while preserving the date
497
- return dateFnsTz.fromZonedTime(dateFns.set(lastFullDate, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
403
+ // Return the last completed trading day
404
+ const lastMarketDay = this.calendar.getPreviousMarketDay(nowET);
405
+ return dateFnsTz.fromZonedTime(dateFns.set(lastMarketDay, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
498
406
  }
499
407
  /**
500
- * Gets the next market day from a reference date
501
- * @param {Object} [options] - Options object
502
- * @param {Date} [options.referenceDate] - The reference date (defaults to current date)
503
- * @returns {Object} The next market day information
504
- * @property {Date} date - The date object (start of day in NY time)
505
- * @property {string} yyyymmdd - The date in YYYY-MM-DD format
506
- * @property {string} dateISOString - Full ISO date string
408
+ * Get day boundaries based on intraday reporting mode
507
409
  */
508
- getNextMarketDay(date) {
509
- let currentDate = dateFns.add(date, { days: 1 });
510
- while (!this.isMarketDay(currentDate)) {
511
- currentDate = dateFns.add(currentDate, { days: 1 });
512
- }
513
- return currentDate;
514
- }
515
- getDayBoundaries(date) {
410
+ getDayBoundaries(date, intradayReporting = 'market_hours') {
411
+ const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
516
412
  let start;
517
413
  let end;
518
- switch (this.intradayReporting) {
414
+ switch (intradayReporting) {
519
415
  case 'extended_hours': {
520
- start = dateFns.set(date, {
521
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
522
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
416
+ start = dateFns.set(zonedDate, {
417
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
418
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
523
419
  seconds: 0,
524
420
  milliseconds: 0,
525
421
  });
526
- end = dateFns.set(date, {
527
- hours: MARKET_TIMES.EXTENDED.END.HOUR,
528
- minutes: MARKET_TIMES.EXTENDED.END.MINUTE,
422
+ end = dateFns.set(zonedDate, {
423
+ hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour,
424
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute,
529
425
  seconds: 59,
530
426
  milliseconds: 999,
531
427
  });
428
+ // Handle early close
429
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
430
+ end = dateFns.set(zonedDate, {
431
+ hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour,
432
+ minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute,
433
+ seconds: 59,
434
+ milliseconds: 999,
435
+ });
436
+ }
532
437
  break;
533
438
  }
534
439
  case 'continuous': {
535
- start = dateFns.startOfDay(date);
536
- end = dateFns.endOfDay(date);
440
+ start = dateFns.startOfDay(zonedDate);
441
+ end = dateFns.endOfDay(zonedDate);
537
442
  break;
538
443
  }
539
444
  default: {
540
- // market_hours
541
- start = dateFns.set(date, {
542
- hours: MARKET_TIMES.REGULAR.START.HOUR,
543
- minutes: MARKET_TIMES.REGULAR.START.MINUTE,
445
+ // 'market_hours'
446
+ start = dateFns.set(zonedDate, {
447
+ hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
448
+ minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
544
449
  seconds: 0,
545
450
  milliseconds: 0,
546
451
  });
547
- // Check for early close
548
- if (this.isEarlyCloseDay(date)) {
549
- const earlyCloseMinutes = this.getEarlyCloseTime(date);
550
- if (earlyCloseMinutes !== null) {
551
- const earlyCloseHours = Math.floor(earlyCloseMinutes / 60);
552
- const earlyCloseMinutesRemainder = earlyCloseMinutes % 60;
553
- end = dateFns.set(date, {
554
- hours: earlyCloseHours,
555
- minutes: earlyCloseMinutesRemainder,
556
- seconds: 59,
557
- milliseconds: 999,
558
- });
559
- break;
560
- }
561
- }
562
- end = dateFns.set(date, {
563
- hours: MARKET_TIMES.REGULAR.END.HOUR,
564
- minutes: MARKET_TIMES.REGULAR.END.MINUTE,
452
+ end = dateFns.set(zonedDate, {
453
+ hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
454
+ minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
565
455
  seconds: 59,
566
456
  milliseconds: 999,
567
457
  });
458
+ // Handle early close
459
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
460
+ end = dateFns.set(zonedDate, {
461
+ hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour,
462
+ minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute,
463
+ seconds: 59,
464
+ milliseconds: 999,
465
+ });
466
+ }
568
467
  break;
569
468
  }
570
469
  }
571
- return { start, end };
470
+ return {
471
+ start: dateFnsTz.fromZonedTime(start, this.timezone),
472
+ end: dateFnsTz.fromZonedTime(end, this.timezone),
473
+ };
572
474
  }
475
+ }
476
+ // ===== PERIOD CALCULATOR =====
477
+ /**
478
+ * Service for calculating time periods
479
+ */
480
+ class PeriodCalculator {
481
+ calendar;
482
+ timeCalculator;
483
+ formatter;
484
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
485
+ this.calendar = new MarketCalendar(timezone);
486
+ this.timeCalculator = new MarketTimeCalculator(timezone);
487
+ this.formatter = new TimeFormatter(timezone);
488
+ }
489
+ /**
490
+ * Calculate the start date for a given period
491
+ */
573
492
  calculatePeriodStartDate(endDate, period) {
574
493
  let startDate;
575
494
  switch (period) {
@@ -577,7 +496,7 @@ class MarketTimeUtil {
577
496
  startDate = dateFns.set(endDate, { month: 0, date: 1 });
578
497
  break;
579
498
  case '1D':
580
- startDate = this.getLastMarketDay(endDate);
499
+ startDate = this.calendar.getPreviousMarketDay(endDate);
581
500
  break;
582
501
  case '3D':
583
502
  startDate = dateFns.sub(endDate, { days: 3 });
@@ -603,405 +522,286 @@ class MarketTimeUtil {
603
522
  default:
604
523
  throw new Error(`Invalid period: ${period}`);
605
524
  }
606
- while (!this.isMarketDay(startDate)) {
607
- startDate = this.getNextMarketDay(startDate);
525
+ // Ensure start date is a market day
526
+ while (!this.calendar.isMarketDay(startDate)) {
527
+ startDate = this.calendar.getNextMarketDay(startDate);
608
528
  }
609
529
  return startDate;
610
530
  }
611
- getMarketTimePeriod({ period, end = new Date(), intraday_reporting, outputFormat = 'iso', }) {
531
+ /**
532
+ * Get period dates for market time calculations
533
+ */
534
+ getMarketTimePeriod(params) {
535
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
612
536
  if (!period) {
613
537
  throw new Error('Period is required');
614
538
  }
615
- if (intraday_reporting) {
616
- this.intradayReporting = intraday_reporting;
617
- }
618
- // Convert end date to specified timezone
619
- const zonedEndDate = dateFnsTz.toZonedTime(end, this.timezone);
620
- let startDate;
539
+ const zonedEndDate = dateFnsTz.toZonedTime(end, MARKET_CONFIG.TIMEZONE);
540
+ // Determine effective end date based on current market conditions
621
541
  let endDate;
622
- const isCurrentMarketDay = this.isMarketDay(zonedEndDate);
623
- const isWithinHours = this.isWithinMarketHours(zonedEndDate);
624
- const isBeforeHours = this.isBeforeMarketHours(zonedEndDate);
625
- // First determine the end date based on current market conditions
542
+ const isCurrentMarketDay = this.calendar.isMarketDay(zonedEndDate);
543
+ const isWithinHours = this.timeCalculator.isWithinMarketHours(end, intraday_reporting);
544
+ const timeInMinutes = zonedEndDate.getHours() * 60 + zonedEndDate.getMinutes();
545
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
626
546
  if (isCurrentMarketDay) {
627
- if (isBeforeHours) {
628
- // Case 1: Market day before open hours - use previous full trading day
629
- const lastMarketDay = this.getLastMarketDay(zonedEndDate);
630
- const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
547
+ if (timeInMinutes < marketStartMinutes) {
548
+ // Before market open - use previous day's close
549
+ const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
550
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
631
551
  endDate = dayEnd;
632
552
  }
633
553
  else if (isWithinHours) {
634
- // Case 2: Market day during hours - use current time
635
- endDate = zonedEndDate;
554
+ // During market hours - use current time
555
+ endDate = end;
636
556
  }
637
557
  else {
638
- // Case 3: Market day after close - use today's close
639
- const { end: dayEnd } = this.getDayBoundaries(zonedEndDate);
558
+ // After market close - use today's close
559
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(zonedEndDate, intraday_reporting);
640
560
  endDate = dayEnd;
641
561
  }
642
562
  }
643
563
  else {
644
- // Case 4: Not a market day - use previous market day's close
645
- const lastMarketDay = this.getLastMarketDay(zonedEndDate);
646
- const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
564
+ // Not a market day - use previous market day's close
565
+ const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
566
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
647
567
  endDate = dayEnd;
648
568
  }
649
- // Now calculate the start date based on the period
569
+ // Calculate start date
650
570
  const periodStartDate = this.calculatePeriodStartDate(endDate, period);
651
- const { start: dayStart } = this.getDayBoundaries(periodStartDate);
652
- startDate = dayStart;
653
- // Convert boundaries back to UTC for final output
654
- const utcStart = dateFnsTz.fromZonedTime(startDate, this.timezone);
655
- const utcEnd = dateFnsTz.fromZonedTime(endDate, this.timezone);
571
+ const { start: dayStart } = this.timeCalculator.getDayBoundaries(periodStartDate, intraday_reporting);
656
572
  // Ensure start is not after end
657
- if (dateFns.isBefore(utcEnd, utcStart)) {
573
+ if (dateFns.isBefore(endDate, dayStart)) {
658
574
  throw new Error('Start date cannot be after end date');
659
575
  }
660
576
  return {
661
- start: this.formatDate(utcStart, outputFormat),
662
- end: this.formatDate(utcEnd, outputFormat),
577
+ start: this.formatter.formatDate(dayStart, outputFormat),
578
+ end: this.formatter.formatDate(endDate, outputFormat),
663
579
  };
664
580
  }
665
- getMarketOpenClose(options = {}) {
666
- const { date = new Date() } = options;
667
- const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
668
- // Check if market is closed for the day
669
- if (this.isWeekend(zonedDate) || this.isHoliday(zonedDate)) {
670
- return {
671
- marketOpen: false,
672
- open: null,
673
- close: null,
674
- openExt: null,
675
- closeExt: null,
676
- };
581
+ }
582
+ // ===== MARKET STATUS SERVICE =====
583
+ /**
584
+ * Service for determining market status
585
+ */
586
+ class MarketStatusService {
587
+ calendar;
588
+ timeCalculator;
589
+ timezone;
590
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
591
+ this.timezone = timezone;
592
+ this.calendar = new MarketCalendar(timezone);
593
+ this.timeCalculator = new MarketTimeCalculator(timezone);
594
+ }
595
+ /**
596
+ * Get current market status
597
+ */
598
+ getMarketStatus(date = new Date()) {
599
+ const nyTime = dateFnsTz.toZonedTime(date, this.timezone);
600
+ const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
601
+ const isMarketDay = this.calendar.isMarketDay(nyTime);
602
+ const isEarlyCloseDay = this.calendar.isEarlyCloseDay(nyTime);
603
+ // Time boundaries
604
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
605
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
606
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
607
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
608
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
609
+ if (isEarlyCloseDay) {
610
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
611
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
612
+ }
613
+ let status;
614
+ let nextStatus;
615
+ let nextStatusTime;
616
+ let marketPeriod;
617
+ if (!isMarketDay) {
618
+ // Market is closed (holiday/weekend)
619
+ status = 'closed';
620
+ nextStatus = 'extended hours';
621
+ marketPeriod = 'closed';
622
+ const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
623
+ nextStatusTime = dateFns.set(nextMarketDay, {
624
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
625
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
626
+ });
677
627
  }
678
- const dayStart = dateFns.startOfDay(zonedDate);
679
- const regularOpenTime = MARKET_TIMES.REGULAR.START;
680
- let regularCloseTime = MARKET_TIMES.REGULAR.END;
681
- const extendedOpenTime = MARKET_TIMES.EXTENDED.START;
682
- let extendedCloseTime = MARKET_TIMES.EXTENDED.END;
683
- // Check for early close
684
- const isEarlyClose = this.isEarlyCloseDay(zonedDate);
685
- if (isEarlyClose) {
686
- const earlyCloseMinutes = this.getEarlyCloseTime(zonedDate);
687
- if (earlyCloseMinutes !== null) {
688
- // For regular hours, use the early close time
689
- regularCloseTime = {
690
- HOUR: Math.floor(earlyCloseMinutes / 60),
691
- MINUTE: earlyCloseMinutes % 60,
692
- MINUTES: earlyCloseMinutes,
693
- };
694
- // For extended hours on early close days, close at 5:00 PM
695
- extendedCloseTime = {
696
- HOUR: 17,
697
- MINUTE: 0,
698
- MINUTES: 1020,
699
- };
700
- }
628
+ else if (timeInMinutes < extendedStartMinutes) {
629
+ // Before extended hours
630
+ status = 'closed';
631
+ nextStatus = 'extended hours';
632
+ marketPeriod = 'closed';
633
+ nextStatusTime = dateFns.set(nyTime, {
634
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
635
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
636
+ });
637
+ }
638
+ else if (timeInMinutes < marketStartMinutes) {
639
+ // Pre-market extended hours
640
+ status = 'extended hours';
641
+ nextStatus = 'open';
642
+ marketPeriod = 'preMarket';
643
+ nextStatusTime = dateFns.set(nyTime, {
644
+ hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
645
+ minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
646
+ });
647
+ }
648
+ else if (timeInMinutes < marketCloseMinutes) {
649
+ // Market is open
650
+ status = 'open';
651
+ nextStatus = 'extended hours';
652
+ marketPeriod = timeInMinutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
653
+ nextStatusTime = dateFns.set(nyTime, {
654
+ hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.hour : MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
655
+ minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.minute : MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
656
+ });
701
657
  }
702
- const open = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: regularOpenTime.HOUR, minutes: regularOpenTime.MINUTE }), this.timezone);
703
- const close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: regularCloseTime.HOUR, minutes: regularCloseTime.MINUTE }), this.timezone);
704
- const openExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: extendedOpenTime.HOUR, minutes: extendedOpenTime.MINUTE }), this.timezone);
705
- const closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: extendedCloseTime.HOUR, minutes: extendedCloseTime.MINUTE }), this.timezone);
658
+ else if (timeInMinutes < extendedEndMinutes) {
659
+ // After-market extended hours
660
+ status = 'extended hours';
661
+ nextStatus = 'closed';
662
+ marketPeriod = 'afterMarket';
663
+ nextStatusTime = dateFns.set(nyTime, {
664
+ hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour : MARKET_CONFIG.TIMES.EXTENDED_END.hour,
665
+ minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute : MARKET_CONFIG.TIMES.EXTENDED_END.minute,
666
+ });
667
+ }
668
+ else {
669
+ // After extended hours
670
+ status = 'closed';
671
+ nextStatus = 'extended hours';
672
+ marketPeriod = 'closed';
673
+ const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
674
+ nextStatusTime = dateFns.set(nextMarketDay, {
675
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
676
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
677
+ });
678
+ }
679
+ const nextStatusTimeUTC = dateFnsTz.fromZonedTime(nextStatusTime, this.timezone);
680
+ const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
706
681
  return {
707
- marketOpen: true,
708
- open,
709
- close,
710
- openExt,
711
- closeExt,
682
+ time: date,
683
+ timeString: dateFns.format(nyTime, dateFormat),
684
+ status,
685
+ nextStatus,
686
+ marketPeriod,
687
+ nextStatusTime: nextStatusTimeUTC,
688
+ nextStatusTimeDifference: dateFns.differenceInMilliseconds(nextStatusTime, nyTime),
689
+ nextStatusTimeString: dateFns.format(nextStatusTime, dateFormat),
712
690
  };
713
691
  }
714
692
  }
693
+ // ===== FUNCTIONAL API =====
715
694
  /**
716
- * Creates a new MarketTimeUtil instance
717
- * @param {string} [timezone] - The timezone to use for market time calculations
718
- * @param {IntradayReporting} [intraday_reporting] - The intraday reporting mode
719
- * @returns {MarketTimeUtil} A new MarketTimeUtil instance
695
+ * Simple functional API that uses the services above
720
696
  */
721
- function createMarketTimeUtil(timezone, intraday_reporting) {
722
- return new MarketTimeUtil(timezone, intraday_reporting);
723
- }
697
+ // Create service instances
698
+ const marketCalendar = new MarketCalendar();
699
+ const marketTimeCalculator = new MarketTimeCalculator();
700
+ const periodCalculator = new PeriodCalculator();
701
+ const marketStatusService = new MarketStatusService();
702
+ const timeFormatter = new TimeFormatter();
724
703
  /**
725
- * Gets start and end timestamps for a given market time period
726
- * @param {MarketTimeParams} [params] - The market time parameters
727
- * @returns {PeriodDates} The start and end timestamps
728
- */
729
- function getStartAndEndTimestamps(params = {}) {
730
- const util = createMarketTimeUtil(params.timezone, params.intraday_reporting);
731
- const effectiveParams = {
732
- ...params,
733
- end: params.referenceDate || params.end || new Date(),
734
- };
735
- return util.getMarketTimePeriod(effectiveParams);
736
- }
737
- /**
738
- * Gets the market open/close times for a given date
739
- * @param {Object} [options] - Options object
740
- * @param {Date} [options.date] - The date to check (defaults to current date)
741
- * @returns {MarketOpenCloseResult} The market open/close times
704
+ * Get market open/close times for a given date
742
705
  */
743
706
  function getMarketOpenClose(options = {}) {
744
- const marketTimeUtil = new MarketTimeUtil();
745
- return marketTimeUtil.getMarketOpenClose(options);
707
+ const { date = new Date() } = options;
708
+ return marketTimeCalculator.getMarketTimes(date);
746
709
  }
747
710
  /**
748
- * Gets the start and end dates for a given market time period
749
- * @param {MarketTimeParams} [params] - The market time parameters
750
- * @returns {Object} The start and end dates
751
- * @property {Date} start - The start date
752
- * @property {Date} end - The end date
711
+ * Get start and end dates for a given market time period
753
712
  */
754
713
  function getStartAndEndDates(params = {}) {
755
- const util = createMarketTimeUtil(params.timezone, params.intraday_reporting);
756
- const effectiveParams = {
757
- ...params,
758
- end: params.referenceDate || params.end || new Date(),
759
- };
760
- const { start, end } = util.getMarketTimePeriod(effectiveParams);
761
- // Ensure the returned values are Dates
714
+ const { start, end } = periodCalculator.getMarketTimePeriod(params);
762
715
  return {
763
716
  start: new Date(start),
764
717
  end: new Date(end),
765
718
  };
766
719
  }
767
720
  /**
768
- * Gets the last trading date in YYYY-MM-DD format
769
- * @returns {string} The last trading date in YYYY-MM-DD format
770
- */
771
- function getLastTradingDateYYYYMMDD() {
772
- const util = new MarketTimeUtil();
773
- const lastTradingDate = util.getLastTradingDate();
774
- return dateFns.format(lastTradingDate, 'yyyy-MM-dd');
775
- }
776
- /**
777
- * Gets the last full trading date
778
- * @param {Date} [currentDate] - The current date (defaults to now)
779
- * @returns {Object} The last full trading date
780
- * @property {Date} date - The date object
781
- * @property {string} YYYYMMDD - The date in YYYY-MM-DD format
721
+ * Get the last full trading date
782
722
  */
783
723
  function getLastFullTradingDate(currentDate = new Date()) {
784
- const util = new MarketTimeUtil();
785
- const date = util.getLastFullTradingDate(currentDate);
786
- // Format the date in NY timezone to ensure consistency
724
+ const date = marketTimeCalculator.getLastFullTradingDate(currentDate);
787
725
  return {
788
726
  date,
789
- YYYYMMDD: dateFnsTz.formatInTimeZone(date, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd'),
727
+ YYYYMMDD: timeFormatter.getTradingDate(date),
790
728
  };
791
729
  }
792
730
  /**
793
- * Gets the next market day from a reference date
794
- * @param {Object} [options] - Options object
795
- * @param {Date} [options.referenceDate] - The reference date (defaults to current date)
796
- * @returns {Object} The next market day information
797
- * @property {Date} date - The date object (start of day in NY time)
798
- * @property {string} yyyymmdd - The date in YYYY-MM-DD format
799
- * @property {string} dateISOString - Full ISO date string
731
+ * Get the next market day
800
732
  */
801
733
  function getNextMarketDay({ referenceDate } = {}) {
802
- const util = new MarketTimeUtil();
803
734
  const startDate = referenceDate || new Date();
804
- const nextDate = util.getNextMarketDay(startDate);
735
+ const nextDate = marketCalendar.getNextMarketDay(startDate);
805
736
  // Convert to start of day in NY time
806
- const startOfDayNY = dateFns.startOfDay(dateFnsTz.toZonedTime(nextDate, MARKET_TIMES.TIMEZONE));
807
- const dateInET = dateFnsTz.fromZonedTime(startOfDayNY, MARKET_TIMES.TIMEZONE);
737
+ const startOfDayNY = dateFns.startOfDay(dateFnsTz.toZonedTime(nextDate, MARKET_CONFIG.TIMEZONE));
738
+ const dateInET = dateFnsTz.fromZonedTime(startOfDayNY, MARKET_CONFIG.TIMEZONE);
808
739
  return {
809
740
  date: dateInET,
810
- yyyymmdd: dateFnsTz.formatInTimeZone(dateInET, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd'),
741
+ yyyymmdd: timeFormatter.getTradingDate(dateInET),
811
742
  dateISOString: dateInET.toISOString(),
812
743
  };
813
744
  }
814
745
  /**
815
- * Gets the current time in Eastern Time
816
- * @returns {Date} The current time in Eastern Time
746
+ * Get trading date in YYYY-MM-DD format
817
747
  */
818
- const currentTimeET = () => {
819
- return dateFnsTz.toZonedTime(new Date(), MARKET_TIMES.TIMEZONE);
820
- };
748
+ function getTradingDate(time) {
749
+ return timeFormatter.getTradingDate(time);
750
+ }
821
751
  /**
822
- * Gets a date in New York timezone, rezoned using date-fns-tz
823
- * @param {number|string|Date} time - The time to convert
824
- * @returns {Date} The date in New York timezone
752
+ * Get New York timezone offset
825
753
  */
826
- function getDateInNY(time) {
827
- let date;
828
- if (typeof time === 'number' || typeof time === 'string' || time instanceof Date) {
829
- // Assuming Unix timestamp in epoch milliseconds, string date, or Date object
830
- date = new Date(time);
831
- }
832
- else {
833
- // Assuming object with year, month, and day
834
- date = new Date(time.year, time.month - 1, time.day);
835
- }
836
- return dateFnsTz.toZonedTime(date, 'America/New_York');
754
+ function getNYTimeZone(date) {
755
+ return timeFormatter.getNYTimeZone(date);
837
756
  }
838
757
  /**
839
- * Gets the trading date in YYYY-MM-DD format for New York timezone, for grouping of data
840
- * @param {string|number|Date} time - The time to convert (string, unix timestamp in ms, or Date object)
841
- * @returns {string} The trading date in YYYY-MM-DD format
758
+ * Get current market status
842
759
  */
843
- function getTradingDate(time) {
844
- let date;
845
- if (typeof time === 'number') {
846
- // Assuming Unix timestamp in milliseconds
847
- date = new Date(time);
848
- }
849
- else if (typeof time === 'string') {
850
- date = new Date(time);
851
- }
852
- else {
853
- date = time;
854
- }
855
- // Convert to NY timezone and format as YYYY-MM-DD
856
- return dateFnsTz.formatInTimeZone(date, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd');
760
+ function getMarketStatus(options = {}) {
761
+ const { date = new Date() } = options;
762
+ return marketStatusService.getMarketStatus(date);
857
763
  }
858
764
  /**
859
- * Returns the New York timezone offset based on whether daylight savings is active
860
- * @param dateString - The date string to check
861
- * @returns "-04:00" during daylight savings (EDT) or "-05:00" during standard time (EST)
765
+ * Check if a date is a market day
862
766
  */
863
- const getNYTimeZone = (date) => {
864
- if (!date) {
865
- date = new Date();
866
- }
867
- const dtf = new Intl.DateTimeFormat('en-US', {
868
- timeZone: 'America/New_York',
869
- timeZoneName: 'shortOffset',
870
- });
871
- const parts = dtf.formatToParts(date);
872
- const tz = parts.find((p) => p.type === 'timeZoneName')?.value;
873
- // tz will be "GMT-5" or "GMT-4"
874
- if (!tz) {
875
- throw new Error('Could not determine New York offset');
876
- }
877
- // extract the -4 or -5 from the string
878
- const shortOffset = tz.replace('GMT', '');
879
- // return the correct offset
880
- if (shortOffset === '-4') {
881
- console.log(`New York is on EDT; using -04:00. Full date: ${date.toLocaleString('en-US', {
882
- timeZone: 'America/New_York',
883
- })}, time zone part: ${tz}`);
884
- return '-04:00';
885
- }
886
- else if (shortOffset === '-5') {
887
- console.log(`New York is on EST; using -05:00. Full date: ${date.toLocaleString('en-US', {
888
- timeZone: 'America/New_York',
889
- })}, time zone part: ${tz}`);
890
- return '-05:00';
891
- }
892
- else {
893
- throw new Error('Could not determine New York offset');
894
- }
895
- };
767
+ function isMarketDay(date) {
768
+ return marketCalendar.isMarketDay(date);
769
+ }
896
770
  /**
897
- * Gets the current market status
898
- * @param {Object} [options] - Options object
899
- * @param {Date} [options.date] - The date to check (defaults to current date)
900
- * @returns {MarketStatus} The current market status
771
+ * Function to find complete trading date periods, starting at the beginning of one trading date and ending at the last.
772
+ * By default, it gets the last trading date, returning the beginning and end. But we can also a) define the end date
773
+ * on which to look (which is the end market date, e.g. for 8 july 2025 we'd return an end of 16:00 NY time on that date),
774
+ * and to go back x days before, e.g. 7 would just subtract raw 7 days, giving us 4-5 trading days (depending on holidays).
775
+ * @param options.endDate - The end date to use, defaults to today
776
+ * @param options.days - The number of days to go back, defaults to 1
777
+ * @returns The start and end dates with proper market open/close times
901
778
  */
902
- function getMarketStatus(options = {}) {
903
- const util = new MarketTimeUtil();
904
- const now = options.date || new Date();
905
- const nyTime = dateFnsTz.toZonedTime(now, MARKET_TIMES.TIMEZONE);
906
- const isEarlyCloseDay = util.isEarlyCloseDay(nyTime);
907
- const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
908
- const extendedStartMinutes = MARKET_TIMES.EXTENDED.START.MINUTES;
909
- const marketStartMinutes = MARKET_TIMES.REGULAR.START.MINUTES;
910
- MARKET_TIMES.EARLY_MORNING.END.MINUTES;
911
- const marketRegularCloseMinutes = isEarlyCloseDay
912
- ? MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTES
913
- : MARKET_TIMES.REGULAR.END.MINUTES;
914
- const extendedEndMinutes = isEarlyCloseDay
915
- ? MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTES
916
- : MARKET_TIMES.EXTENDED.END.MINUTES;
917
- let status;
918
- let nextStatus;
919
- let nextStatusTime;
920
- let marketPeriod;
921
- const nextMarketDay = util.getNextMarketDay(nyTime);
922
- // Determine current status and market period
923
- if (!util.isMarketDay(nyTime)) {
924
- // Not a market day! market is closed
925
- marketPeriod = 'closed';
926
- status = 'closed';
927
- nextStatus = 'extended hours';
928
- // Find next market day and set to extended hours start time
929
- nextStatusTime = dateFns.set(nextMarketDay, {
930
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
931
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
932
- });
933
- } // check if the market isn't in extended hours yet
934
- else if (timeInMinutes >= 0 && timeInMinutes < extendedStartMinutes) {
935
- marketPeriod = 'closed';
936
- status = 'closed';
937
- nextStatus = 'extended hours';
938
- nextStatusTime = dateFns.set(nyTime, {
939
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
940
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
941
- });
942
- // check if we're in pre-market hours
943
- }
944
- else if (timeInMinutes >= extendedStartMinutes && timeInMinutes < marketStartMinutes) {
945
- marketPeriod = 'preMarket';
946
- status = 'extended hours';
947
- nextStatus = 'open';
948
- nextStatusTime = dateFns.set(nyTime, {
949
- hours: MARKET_TIMES.REGULAR.START.HOUR,
950
- minutes: MARKET_TIMES.REGULAR.START.MINUTE,
951
- });
952
- // check if market is open
953
- }
954
- else if (timeInMinutes >= marketStartMinutes && timeInMinutes < marketRegularCloseMinutes) {
955
- status = 'open';
956
- nextStatus = 'extended hours';
957
- // market is open, but just check the marketPeriod - could be earlyMarket or regularMarket
958
- marketPeriod = timeInMinutes < MARKET_TIMES.EARLY_MORNING.END.MINUTES ? 'earlyMarket' : 'regularMarket';
959
- nextStatusTime = isEarlyCloseDay
960
- ? dateFns.set(nyTime, {
961
- hours: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.HOUR,
962
- minutes: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTE,
963
- })
964
- : dateFns.set(nyTime, {
965
- hours: MARKET_TIMES.REGULAR.END.HOUR,
966
- minutes: MARKET_TIMES.REGULAR.END.MINUTE,
967
- });
968
- // check if it's after-market extended hours
969
- }
970
- else if (timeInMinutes >= marketRegularCloseMinutes && timeInMinutes < extendedEndMinutes) {
971
- status = 'extended hours';
972
- nextStatus = 'closed';
973
- marketPeriod = 'afterMarket';
974
- nextStatusTime = isEarlyCloseDay
975
- ? dateFns.set(nyTime, {
976
- hours: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.HOUR,
977
- minutes: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTE,
978
- })
979
- : dateFns.set(nyTime, {
980
- hours: MARKET_TIMES.EXTENDED.END.HOUR,
981
- minutes: MARKET_TIMES.EXTENDED.END.MINUTE,
982
- });
983
- // otherwise, the market is closed
779
+ function getTradingStartAndEndDates(options = {}) {
780
+ const { endDate = new Date(), days = 1 } = options;
781
+ // Get the last full trading date for the end date
782
+ const endTradingDate = getLastFullTradingDate(endDate).date;
783
+ // Get the market close time for the end date (4:00 PM ET or 1:00 PM on short days)
784
+ const endMarketClose = getMarketOpenClose({ date: endTradingDate }).close;
785
+ let startDate;
786
+ if (days <= 1) {
787
+ // For 1 day, start is market open of the same trading day as end
788
+ startDate = getMarketOpenClose({ date: endTradingDate }).open;
984
789
  }
985
790
  else {
986
- status = 'closed';
987
- nextStatus = 'extended hours';
988
- marketPeriod = 'closed';
989
- nextStatusTime = dateFns.set(nextMarketDay, {
990
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
991
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
992
- });
791
+ // For multiple days, go back the specified number of trading days
792
+ let currentDate = new Date(endTradingDate);
793
+ let tradingDaysBack = 0;
794
+ // Count back trading days
795
+ while (tradingDaysBack < days - 1) {
796
+ currentDate = dateFns.sub(currentDate, { days: 1 });
797
+ if (isMarketDay(currentDate)) {
798
+ tradingDaysBack++;
799
+ }
800
+ }
801
+ // Get the market open time for the start date
802
+ startDate = getMarketOpenClose({ date: currentDate }).open;
993
803
  }
994
- const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
995
- return {
996
- time: now,
997
- timeString: dateFns.format(nyTime, dateFormat),
998
- status,
999
- nextStatus,
1000
- marketPeriod,
1001
- nextStatusTime: dateFnsTz.fromZonedTime(nextStatusTime, MARKET_TIMES.TIMEZONE),
1002
- nextStatusTimeDifference: dateFns.differenceInMilliseconds(nextStatusTime, nyTime),
1003
- nextStatusTimeString: dateFns.format(nextStatusTime, dateFormat),
1004
- };
804
+ return { startDate, endDate: endMarketClose };
1005
805
  }
1006
806
 
1007
807
  // format-tools.ts
@@ -2431,7 +2231,7 @@ const safeJSON = (text) => {
2431
2231
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2432
2232
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2433
2233
 
2434
- const VERSION = '5.8.2'; // x-release-please-version
2234
+ const VERSION = '5.8.3'; // x-release-please-version
2435
2235
 
2436
2236
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2437
2237
  const isRunningInBrowser = () => {
@@ -3183,13 +2983,95 @@ function findDoubleNewlineIndex(buffer) {
3183
2983
  return -1;
3184
2984
  }
3185
2985
 
2986
+ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2987
+ const levelNumbers = {
2988
+ off: 0,
2989
+ error: 200,
2990
+ warn: 300,
2991
+ info: 400,
2992
+ debug: 500,
2993
+ };
2994
+ const parseLogLevel = (maybeLevel, sourceName, client) => {
2995
+ if (!maybeLevel) {
2996
+ return undefined;
2997
+ }
2998
+ if (hasOwn(levelNumbers, maybeLevel)) {
2999
+ return maybeLevel;
3000
+ }
3001
+ loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
3002
+ return undefined;
3003
+ };
3004
+ function noop() { }
3005
+ function makeLogFn(fnLevel, logger, logLevel) {
3006
+ if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
3007
+ return noop;
3008
+ }
3009
+ else {
3010
+ // Don't wrap logger functions, we want the stacktrace intact!
3011
+ return logger[fnLevel].bind(logger);
3012
+ }
3013
+ }
3014
+ const noopLogger = {
3015
+ error: noop,
3016
+ warn: noop,
3017
+ info: noop,
3018
+ debug: noop,
3019
+ };
3020
+ let cachedLoggers = /* @__PURE__ */ new WeakMap();
3021
+ function loggerFor(client) {
3022
+ const logger = client.logger;
3023
+ const logLevel = client.logLevel ?? 'off';
3024
+ if (!logger) {
3025
+ return noopLogger;
3026
+ }
3027
+ const cachedLogger = cachedLoggers.get(logger);
3028
+ if (cachedLogger && cachedLogger[0] === logLevel) {
3029
+ return cachedLogger[1];
3030
+ }
3031
+ const levelLogger = {
3032
+ error: makeLogFn('error', logger, logLevel),
3033
+ warn: makeLogFn('warn', logger, logLevel),
3034
+ info: makeLogFn('info', logger, logLevel),
3035
+ debug: makeLogFn('debug', logger, logLevel),
3036
+ };
3037
+ cachedLoggers.set(logger, [logLevel, levelLogger]);
3038
+ return levelLogger;
3039
+ }
3040
+ const formatRequestDetails = (details) => {
3041
+ if (details.options) {
3042
+ details.options = { ...details.options };
3043
+ delete details.options['headers']; // redundant + leaks internals
3044
+ }
3045
+ if (details.headers) {
3046
+ details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
3047
+ name,
3048
+ (name.toLowerCase() === 'authorization' ||
3049
+ name.toLowerCase() === 'cookie' ||
3050
+ name.toLowerCase() === 'set-cookie') ?
3051
+ '***'
3052
+ : value,
3053
+ ]));
3054
+ }
3055
+ if ('retryOfRequestLogID' in details) {
3056
+ if (details.retryOfRequestLogID) {
3057
+ details.retryOf = details.retryOfRequestLogID;
3058
+ }
3059
+ delete details.retryOfRequestLogID;
3060
+ }
3061
+ return details;
3062
+ };
3063
+
3064
+ var _Stream_client;
3186
3065
  class Stream {
3187
- constructor(iterator, controller) {
3066
+ constructor(iterator, controller, client) {
3188
3067
  this.iterator = iterator;
3068
+ _Stream_client.set(this, void 0);
3189
3069
  this.controller = controller;
3070
+ __classPrivateFieldSet(this, _Stream_client, client);
3190
3071
  }
3191
- static fromSSEResponse(response, controller) {
3072
+ static fromSSEResponse(response, controller, client) {
3192
3073
  let consumed = false;
3074
+ const logger = client ? loggerFor(client) : console;
3193
3075
  async function* iterator() {
3194
3076
  if (consumed) {
3195
3077
  throw new OpenAIError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
@@ -3212,8 +3094,8 @@ class Stream {
3212
3094
  data = JSON.parse(sse.data);
3213
3095
  }
3214
3096
  catch (e) {
3215
- console.error(`Could not parse message into JSON:`, sse.data);
3216
- console.error(`From chunk:`, sse.raw);
3097
+ logger.error(`Could not parse message into JSON:`, sse.data);
3098
+ logger.error(`From chunk:`, sse.raw);
3217
3099
  throw e;
3218
3100
  }
3219
3101
  if (data && data.error) {
@@ -3252,13 +3134,13 @@ class Stream {
3252
3134
  controller.abort();
3253
3135
  }
3254
3136
  }
3255
- return new Stream(iterator, controller);
3137
+ return new Stream(iterator, controller, client);
3256
3138
  }
3257
3139
  /**
3258
3140
  * Generates a Stream from a newline-separated ReadableStream
3259
3141
  * where each item is a JSON value.
3260
3142
  */
3261
- static fromReadableStream(readableStream, controller) {
3143
+ static fromReadableStream(readableStream, controller, client) {
3262
3144
  let consumed = false;
3263
3145
  async function* iterLines() {
3264
3146
  const lineDecoder = new LineDecoder();
@@ -3299,9 +3181,9 @@ class Stream {
3299
3181
  controller.abort();
3300
3182
  }
3301
3183
  }
3302
- return new Stream(iterator, controller);
3184
+ return new Stream(iterator, controller, client);
3303
3185
  }
3304
- [Symbol.asyncIterator]() {
3186
+ [(_Stream_client = new WeakMap(), Symbol.asyncIterator)]() {
3305
3187
  return this.iterator();
3306
3188
  }
3307
3189
  /**
@@ -3325,8 +3207,8 @@ class Stream {
3325
3207
  };
3326
3208
  };
3327
3209
  return [
3328
- new Stream(() => teeIterator(left), this.controller),
3329
- new Stream(() => teeIterator(right), this.controller),
3210
+ new Stream(() => teeIterator(left), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
3211
+ new Stream(() => teeIterator(right), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
3330
3212
  ];
3331
3213
  }
3332
3214
  /**
@@ -3460,84 +3342,6 @@ function partition(str, delimiter) {
3460
3342
  return [str, '', ''];
3461
3343
  }
3462
3344
 
3463
- // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
3464
- const levelNumbers = {
3465
- off: 0,
3466
- error: 200,
3467
- warn: 300,
3468
- info: 400,
3469
- debug: 500,
3470
- };
3471
- const parseLogLevel = (maybeLevel, sourceName, client) => {
3472
- if (!maybeLevel) {
3473
- return undefined;
3474
- }
3475
- if (hasOwn(levelNumbers, maybeLevel)) {
3476
- return maybeLevel;
3477
- }
3478
- loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
3479
- return undefined;
3480
- };
3481
- function noop() { }
3482
- function makeLogFn(fnLevel, logger, logLevel) {
3483
- if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
3484
- return noop;
3485
- }
3486
- else {
3487
- // Don't wrap logger functions, we want the stacktrace intact!
3488
- return logger[fnLevel].bind(logger);
3489
- }
3490
- }
3491
- const noopLogger = {
3492
- error: noop,
3493
- warn: noop,
3494
- info: noop,
3495
- debug: noop,
3496
- };
3497
- let cachedLoggers = /** @__PURE__ */ new WeakMap();
3498
- function loggerFor(client) {
3499
- const logger = client.logger;
3500
- const logLevel = client.logLevel ?? 'off';
3501
- if (!logger) {
3502
- return noopLogger;
3503
- }
3504
- const cachedLogger = cachedLoggers.get(logger);
3505
- if (cachedLogger && cachedLogger[0] === logLevel) {
3506
- return cachedLogger[1];
3507
- }
3508
- const levelLogger = {
3509
- error: makeLogFn('error', logger, logLevel),
3510
- warn: makeLogFn('warn', logger, logLevel),
3511
- info: makeLogFn('info', logger, logLevel),
3512
- debug: makeLogFn('debug', logger, logLevel),
3513
- };
3514
- cachedLoggers.set(logger, [logLevel, levelLogger]);
3515
- return levelLogger;
3516
- }
3517
- const formatRequestDetails = (details) => {
3518
- if (details.options) {
3519
- details.options = { ...details.options };
3520
- delete details.options['headers']; // redundant + leaks internals
3521
- }
3522
- if (details.headers) {
3523
- details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
3524
- name,
3525
- (name.toLowerCase() === 'authorization' ||
3526
- name.toLowerCase() === 'cookie' ||
3527
- name.toLowerCase() === 'set-cookie') ?
3528
- '***'
3529
- : value,
3530
- ]));
3531
- }
3532
- if ('retryOfRequestLogID' in details) {
3533
- if (details.retryOfRequestLogID) {
3534
- details.retryOf = details.retryOfRequestLogID;
3535
- }
3536
- delete details.retryOfRequestLogID;
3537
- }
3538
- return details;
3539
- };
3540
-
3541
3345
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
3542
3346
  async function defaultParseResponse(client, props) {
3543
3347
  const { response, requestLogID, retryOfRequestLogID, startTime } = props;
@@ -3547,9 +3351,9 @@ async function defaultParseResponse(client, props) {
3547
3351
  // Note: there is an invariant here that isn't represented in the type system
3548
3352
  // that if you set `stream: true` the response type must also be `Stream<T>`
3549
3353
  if (props.options.__streamClass) {
3550
- return props.options.__streamClass.fromSSEResponse(response, props.controller);
3354
+ return props.options.__streamClass.fromSSEResponse(response, props.controller, client);
3551
3355
  }
3552
- return Stream.fromSSEResponse(response, props.controller);
3356
+ return Stream.fromSSEResponse(response, props.controller, client);
3553
3357
  }
3554
3358
  // fetch refuses to read the body when the status code is 204.
3555
3359
  if (response.status === 204) {
@@ -3803,7 +3607,7 @@ const isAsyncIterable = (value) => value != null && typeof value === 'object' &&
3803
3607
  const multipartFormRequestOptions = async (opts, fetch) => {
3804
3608
  return { ...opts, body: await createForm(opts.body, fetch) };
3805
3609
  };
3806
- const supportsFormDataMap = /** @__PURE__ */ new WeakMap();
3610
+ const supportsFormDataMap = /* @__PURE__ */ new WeakMap();
3807
3611
  /**
3808
3612
  * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
3809
3613
  * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
@@ -3979,21 +3783,38 @@ class APIResource {
3979
3783
  function encodeURIPath(str) {
3980
3784
  return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
3981
3785
  }
3786
+ const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
3982
3787
  const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(statics, ...params) {
3983
3788
  // If there are no params, no processing is needed.
3984
3789
  if (statics.length === 1)
3985
3790
  return statics[0];
3986
3791
  let postPath = false;
3792
+ const invalidSegments = [];
3987
3793
  const path = statics.reduce((previousValue, currentValue, index) => {
3988
3794
  if (/[?#]/.test(currentValue)) {
3989
3795
  postPath = true;
3990
3796
  }
3991
- return (previousValue +
3992
- currentValue +
3993
- (index === params.length ? '' : (postPath ? encodeURIComponent : pathEncoder)(String(params[index]))));
3797
+ const value = params[index];
3798
+ let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
3799
+ if (index !== params.length &&
3800
+ (value == null ||
3801
+ (typeof value === 'object' &&
3802
+ // handle values from other realms
3803
+ value.toString ===
3804
+ Object.getPrototypeOf(Object.getPrototypeOf(value.hasOwnProperty ?? EMPTY) ?? EMPTY)
3805
+ ?.toString))) {
3806
+ encoded = value + '';
3807
+ invalidSegments.push({
3808
+ start: previousValue.length + currentValue.length,
3809
+ length: encoded.length,
3810
+ error: `Value of type ${Object.prototype.toString
3811
+ .call(value)
3812
+ .slice(8, -1)} is not a valid path parameter`,
3813
+ });
3814
+ }
3815
+ return previousValue + currentValue + (index === params.length ? '' : encoded);
3994
3816
  }, '');
3995
3817
  const pathOnly = path.split(/[?#]/, 1)[0];
3996
- const invalidSegments = [];
3997
3818
  const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
3998
3819
  let match;
3999
3820
  // Find all invalid segments
@@ -4001,8 +3822,10 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
4001
3822
  invalidSegments.push({
4002
3823
  start: match.index,
4003
3824
  length: match[0].length,
3825
+ error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
4004
3826
  });
4005
3827
  }
3828
+ invalidSegments.sort((a, b) => a.start - b.start);
4006
3829
  if (invalidSegments.length > 0) {
4007
3830
  let lastEnd = 0;
4008
3831
  const underline = invalidSegments.reduce((acc, segment) => {
@@ -4011,7 +3834,9 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
4011
3834
  lastEnd = segment.start + segment.length;
4012
3835
  return acc + spaces + arrows;
4013
3836
  }, '');
4014
- throw new OpenAIError(`Path parameters result in path with invalid segments:\n${path}\n${underline}`);
3837
+ throw new OpenAIError(`Path parameters result in path with invalid segments:\n${invalidSegments
3838
+ .map((e) => e.error)
3839
+ .join('\n')}\n${path}\n${underline}`);
4015
3840
  }
4016
3841
  return path;
4017
3842
  };
@@ -15371,35 +15196,6 @@ function requireWebsocketServer () {
15371
15196
 
15372
15197
  requireWebsocketServer();
15373
15198
 
15374
- /**
15375
- * Logs a message to the console.
15376
- * @param message The message to log.
15377
- * @param options Optional options.
15378
- * @param options.source The source of the message.
15379
- * @param options.type The type of message to log.
15380
- * @param options.symbol The trading symbol associated with this log.
15381
- * @param options.account The account associated with this log.
15382
- */
15383
- function log$1(message, options = { source: 'App', type: 'info' }) {
15384
- // Format the timestamp
15385
- const date = new Date();
15386
- const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
15387
- const account = options?.account;
15388
- const symbol = options?.symbol;
15389
- // Build the log message
15390
- const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
15391
- // Use appropriate console method based on type
15392
- if (options?.type === 'error') {
15393
- console.error(logMessage);
15394
- }
15395
- else if (options?.type === 'warn') {
15396
- console.warn(logMessage);
15397
- }
15398
- else {
15399
- console.log(logMessage);
15400
- }
15401
- }
15402
-
15403
15199
  const log = (message, options = { type: 'info' }) => {
15404
15200
  log$1(message, { ...options, source: 'AlpacaMarketDataAPI' });
15405
15201
  };
@@ -15423,22 +15219,16 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15423
15219
  optionWs = null;
15424
15220
  stockSubscriptions = { trades: [], quotes: [], bars: [] };
15425
15221
  optionSubscriptions = { trades: [], quotes: [], bars: [] };
15426
- apiKey;
15427
- secretKey;
15428
- accountType;
15429
15222
  setMode(mode = 'production') {
15430
- if (mode === 'sandbox') {
15431
- // sandbox mode
15223
+ if (mode === 'sandbox') { // sandbox mode
15432
15224
  this.stockStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v2/sip';
15433
15225
  this.optionStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v1beta3/options';
15434
15226
  }
15435
- else if (mode === 'test') {
15436
- // test mode, can only use ticker FAKEPACA
15227
+ else if (mode === 'test') { // test mode, can only use ticker FAKEPACA
15437
15228
  this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/test';
15438
15229
  this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options'; // there's no test mode for options
15439
15230
  }
15440
- else {
15441
- // production
15231
+ else { // production
15442
15232
  this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/sip';
15443
15233
  this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options';
15444
15234
  }
@@ -15454,30 +15244,24 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15454
15244
  return 'production';
15455
15245
  }
15456
15246
  }
15457
- constructor(config) {
15247
+ constructor() {
15458
15248
  super();
15459
- this.apiKey = config.apiKey;
15460
- this.secretKey = config.secretKey;
15461
- this.accountType = config.accountType || 'LIVE';
15462
15249
  this.dataURL = 'https://data.alpaca.markets/v2';
15463
15250
  this.apiURL =
15464
- this.accountType === 'PAPER'
15251
+ process.env.ALPACA_ACCOUNT_TYPE === 'PAPER'
15465
15252
  ? 'https://paper-api.alpaca.markets/v2'
15466
15253
  : 'https://api.alpaca.markets/v2'; // used by some, e.g. getAssets
15467
15254
  this.v1beta1url = 'https://data.alpaca.markets/v1beta1'; // used for options endpoints
15468
15255
  this.setMode('production'); // sets stockStreamUrl and optionStreamUrl
15469
15256
  this.headers = {
15470
- 'APCA-API-KEY-ID': this.apiKey,
15471
- 'APCA-API-SECRET-KEY': this.secretKey,
15257
+ 'APCA-API-KEY-ID': process.env.ALPACA_API_KEY,
15258
+ 'APCA-API-SECRET-KEY': process.env.ALPACA_SECRET_KEY,
15472
15259
  'Content-Type': 'application/json',
15473
15260
  };
15474
15261
  }
15475
- static getInstance(config) {
15262
+ static getInstance() {
15476
15263
  if (!AlpacaMarketDataAPI.instance) {
15477
- if (!config) {
15478
- throw new Error('AlpacaMarketDataAPI config is required for first initialization');
15479
- }
15480
- AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI(config);
15264
+ AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI();
15481
15265
  }
15482
15266
  return AlpacaMarketDataAPI.instance;
15483
15267
  }
@@ -15500,8 +15284,8 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15500
15284
  log(`${streamType} stream connected`, { type: 'info' });
15501
15285
  const authMessage = {
15502
15286
  action: 'auth',
15503
- key: this.apiKey,
15504
- secret: this.secretKey,
15287
+ key: process.env.ALPACA_API_KEY,
15288
+ secret: process.env.ALPACA_SECRET_KEY,
15505
15289
  };
15506
15290
  ws.send(JSON.stringify(authMessage));
15507
15291
  });
@@ -15592,7 +15376,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15592
15376
  const currentSubscriptions = streamType === 'stock' ? this.stockSubscriptions : this.optionSubscriptions;
15593
15377
  Object.entries(subscriptions).forEach(([key, value]) => {
15594
15378
  if (value) {
15595
- currentSubscriptions[key] = (currentSubscriptions[key] || []).filter((s) => !value.includes(s));
15379
+ currentSubscriptions[key] = (currentSubscriptions[key] || []).filter(s => !value.includes(s));
15596
15380
  }
15597
15381
  });
15598
15382
  const unsubMessage = {
@@ -15655,11 +15439,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15655
15439
  let pageCount = 0;
15656
15440
  let currency = '';
15657
15441
  // Initialize bar arrays for each symbol
15658
- symbols.forEach((symbol) => {
15442
+ symbols.forEach(symbol => {
15659
15443
  allBars[symbol] = [];
15660
15444
  });
15661
15445
  log(`Starting historical bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
15662
- type: 'info',
15446
+ type: 'info'
15663
15447
  });
15664
15448
  while (hasMorePages) {
15665
15449
  pageCount++;
@@ -15687,7 +15471,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15687
15471
  allBars[symbol] = [...allBars[symbol], ...bars];
15688
15472
  pageBarsCount += bars.length;
15689
15473
  // Track date range for this page
15690
- bars.forEach((bar) => {
15474
+ bars.forEach(bar => {
15691
15475
  const barDate = new Date(bar.t);
15692
15476
  if (!earliestTimestamp || barDate < earliestTimestamp) {
15693
15477
  earliestTimestamp = barDate;
@@ -15706,7 +15490,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15706
15490
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
15707
15491
  : 'unknown range';
15708
15492
  log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
15709
- type: 'info',
15493
+ type: 'info'
15710
15494
  });
15711
15495
  // Prevent infinite loops
15712
15496
  if (pageCount > 1000) {
@@ -15715,11 +15499,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15715
15499
  }
15716
15500
  }
15717
15501
  // Final summary
15718
- const symbolCounts = Object.entries(allBars)
15719
- .map(([symbol, bars]) => `${symbol}: ${bars.length}`)
15720
- .join(', ');
15502
+ const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
15721
15503
  log(`Historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
15722
- type: 'info',
15504
+ type: 'info'
15723
15505
  });
15724
15506
  return {
15725
15507
  bars: allBars,
@@ -15987,11 +15769,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
15987
15769
  let totalBarsCount = 0;
15988
15770
  let pageCount = 0;
15989
15771
  // Initialize bar arrays for each symbol
15990
- symbols.forEach((symbol) => {
15772
+ symbols.forEach(symbol => {
15991
15773
  allBars[symbol] = [];
15992
15774
  });
15993
15775
  log(`Starting historical options bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
15994
- type: 'info',
15776
+ type: 'info'
15995
15777
  });
15996
15778
  while (hasMorePages) {
15997
15779
  pageCount++;
@@ -16013,7 +15795,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16013
15795
  allBars[symbol] = [...allBars[symbol], ...bars];
16014
15796
  pageBarsCount += bars.length;
16015
15797
  // Track date range for this page
16016
- bars.forEach((bar) => {
15798
+ bars.forEach(bar => {
16017
15799
  const barDate = new Date(bar.t);
16018
15800
  if (!earliestTimestamp || barDate < earliestTimestamp) {
16019
15801
  earliestTimestamp = barDate;
@@ -16032,7 +15814,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16032
15814
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
16033
15815
  : 'unknown range';
16034
15816
  log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} option bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
16035
- type: 'info',
15817
+ type: 'info'
16036
15818
  });
16037
15819
  // Prevent infinite loops
16038
15820
  if (pageCount > 1000) {
@@ -16041,11 +15823,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16041
15823
  }
16042
15824
  }
16043
15825
  // Final summary
16044
- const symbolCounts = Object.entries(allBars)
16045
- .map(([symbol, bars]) => `${symbol}: ${bars.length}`)
16046
- .join(', ');
15826
+ const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
16047
15827
  log(`Historical options bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
16048
- type: 'info',
15828
+ type: 'info'
16049
15829
  });
16050
15830
  return {
16051
15831
  bars: allBars,
@@ -16069,11 +15849,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16069
15849
  let totalTradesCount = 0;
16070
15850
  let pageCount = 0;
16071
15851
  // Initialize trades arrays for each symbol
16072
- symbols.forEach((symbol) => {
15852
+ symbols.forEach(symbol => {
16073
15853
  allTrades[symbol] = [];
16074
15854
  });
16075
15855
  log(`Starting historical options trades fetch for ${symbolsStr} (${params.start || 'no start'} to ${params.end || 'no end'})`, {
16076
- type: 'info',
15856
+ type: 'info'
16077
15857
  });
16078
15858
  while (hasMorePages) {
16079
15859
  pageCount++;
@@ -16095,7 +15875,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16095
15875
  allTrades[symbol] = [...allTrades[symbol], ...trades];
16096
15876
  pageTradesCount += trades.length;
16097
15877
  // Track date range for this page
16098
- trades.forEach((trade) => {
15878
+ trades.forEach(trade => {
16099
15879
  const tradeDate = new Date(trade.t);
16100
15880
  if (!earliestTimestamp || tradeDate < earliestTimestamp) {
16101
15881
  earliestTimestamp = tradeDate;
@@ -16114,7 +15894,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16114
15894
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
16115
15895
  : 'unknown range';
16116
15896
  log(`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} option trades (total: ${totalTradesCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
16117
- type: 'info',
15897
+ type: 'info'
16118
15898
  });
16119
15899
  // Prevent infinite loops
16120
15900
  if (pageCount > 1000) {
@@ -16123,11 +15903,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16123
15903
  }
16124
15904
  }
16125
15905
  // Final summary
16126
- const symbolCounts = Object.entries(allTrades)
16127
- .map(([symbol, trades]) => `${symbol}: ${trades.length}`)
16128
- .join(', ');
15906
+ const symbolCounts = Object.entries(allTrades).map(([symbol, trades]) => `${symbol}: ${trades.length}`).join(', ');
16129
15907
  log(`Historical options trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages (${symbolCounts})`, {
16130
- type: 'info',
15908
+ type: 'info'
16131
15909
  });
16132
15910
  return {
16133
15911
  trades: allTrades,
@@ -16272,9 +16050,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16272
16050
  ...(symbol && { symbols: symbol }),
16273
16051
  ...(mergedParams.limit && { limit: Math.min(50, maxLimit - fetchedCount).toString() }),
16274
16052
  ...(mergedParams.sort && { sort: mergedParams.sort }),
16275
- ...(mergedParams.include_content !== undefined
16276
- ? { include_content: mergedParams.include_content.toString() }
16277
- : {}),
16053
+ ...(mergedParams.include_content !== undefined ? { include_content: mergedParams.include_content.toString() } : {}),
16278
16054
  ...(pageToken && { page_token: pageToken }),
16279
16055
  });
16280
16056
  const url = `${this.v1beta1url}/news?${queryParams}`;
@@ -16318,6 +16094,8 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16318
16094
  return newsArticles;
16319
16095
  }
16320
16096
  }
16097
+ // Export the singleton instance
16098
+ const marketDataAPI = AlpacaMarketDataAPI.getInstance();
16321
16099
 
16322
16100
  const limitPriceSlippagePercent100 = 0.1; // 0.1%
16323
16101
  /**
@@ -16329,11 +16107,11 @@ Websocket example
16329
16107
  alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
16330
16108
  */
16331
16109
  class AlpacaTradingAPI {
16332
- static new(credentials, marketDataConfig) {
16333
- return new AlpacaTradingAPI(credentials, marketDataConfig);
16110
+ static new(credentials) {
16111
+ return new AlpacaTradingAPI(credentials);
16334
16112
  }
16335
- static getInstance(credentials, marketDataConfig) {
16336
- return new AlpacaTradingAPI(credentials, marketDataConfig);
16113
+ static getInstance(credentials) {
16114
+ return new AlpacaTradingAPI(credentials);
16337
16115
  }
16338
16116
  ws = null;
16339
16117
  headers;
@@ -16347,7 +16125,6 @@ class AlpacaTradingAPI {
16347
16125
  reconnectTimeout = null;
16348
16126
  messageHandlers = new Map();
16349
16127
  debugLogging = false;
16350
- marketDataAPI;
16351
16128
  /**
16352
16129
  * Constructor for AlpacaTradingAPI
16353
16130
  * @param credentials - Alpaca credentials,
@@ -16356,14 +16133,11 @@ class AlpacaTradingAPI {
16356
16133
  * apiSecret: string; // Alpaca API secret
16357
16134
  * type: AlpacaAccountType;
16358
16135
  * orderType: AlpacaOrderType;
16359
- * @param marketDataConfig - Market data API configuration
16360
16136
  * @param options - Optional options
16361
16137
  * debugLogging: boolean; // Whether to log messages of type 'debug'
16362
16138
  */
16363
- constructor(credentials, marketDataConfig, options) {
16139
+ constructor(credentials, options) {
16364
16140
  this.credentials = credentials;
16365
- // Initialize market data API instance
16366
- this.marketDataAPI = AlpacaMarketDataAPI.getInstance(marketDataConfig);
16367
16141
  // Set URLs based on account type
16368
16142
  this.apiBaseUrl =
16369
16143
  credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
@@ -16902,7 +16676,7 @@ class AlpacaTradingAPI {
16902
16676
  this.log(`Found ${positions.length} positions to close`);
16903
16677
  // Get latest quotes for all positions
16904
16678
  const symbols = positions.map((position) => position.symbol);
16905
- const quotesResponse = await this.marketDataAPI.getLatestQuotes(symbols);
16679
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
16906
16680
  const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
16907
16681
  if (lengthOfQuotes === 0) {
16908
16682
  this.log('No quotes available for positions, received 0 quotes', {
@@ -16969,7 +16743,7 @@ class AlpacaTradingAPI {
16969
16743
  this.log(`Cancelled all open orders`);
16970
16744
  // Get latest quotes for all positions
16971
16745
  const symbols = positions.map((position) => position.symbol);
16972
- const quotesResponse = await this.marketDataAPI.getLatestQuotes(symbols);
16746
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
16973
16747
  // Create limit orders to close each position
16974
16748
  for (const position of positions) {
16975
16749
  const quote = quotesResponse.quotes[position.symbol];
@@ -17652,13 +17426,6 @@ class AlpacaTradingAPI {
17652
17426
  }
17653
17427
  }
17654
17428
 
17655
- // Export factory functions for easier instantiation
17656
- const createAlpacaTradingAPI = (credentials, marketDataConfig) => {
17657
- return new AlpacaTradingAPI(credentials, marketDataConfig);
17658
- };
17659
- const createAlpacaMarketDataAPI = (config) => {
17660
- return AlpacaMarketDataAPI.getInstance(config);
17661
- };
17662
17429
  const disco = {
17663
17430
  types: Types,
17664
17431
  alpaca: {
@@ -17706,31 +17473,14 @@ const disco = {
17706
17473
  calculateFibonacciLevels: calculateFibonacciLevels,
17707
17474
  },
17708
17475
  time: {
17709
- toUnixTimestamp: toUnixTimestamp,
17710
- getTimeAgo: getTimeAgo,
17711
- timeAgo: timeAgo,
17712
- normalizeDate: normalizeDate,
17713
- getDateInNY: getDateInNY,
17714
- createMarketTimeUtil: createMarketTimeUtil,
17715
- getStartAndEndTimestamps: getStartAndEndTimestamps,
17716
17476
  getStartAndEndDates: getStartAndEndDates,
17717
17477
  getMarketOpenClose: getMarketOpenClose,
17718
- calculateTimeRange: calculateTimeRange,
17719
- calculateDaysLeft: calculateDaysLeft,
17720
- formatDate: formatDate /* move to format, keeping here for compatibility */,
17721
- currentTimeET: currentTimeET,
17722
- MarketTimeUtil: MarketTimeUtil,
17723
- MARKET_TIMES: MARKET_TIMES,
17724
- getLastTradingDateYYYYMMDD: getLastTradingDateYYYYMMDD,
17725
17478
  getLastFullTradingDate: getLastFullTradingDate,
17726
17479
  getNextMarketDay: getNextMarketDay,
17727
- parseETDateFromAV: parseETDateFromAV,
17728
- formatToUSEastern: formatToUSEastern,
17729
- unixTimetoUSEastern: unixTimetoUSEastern,
17730
17480
  getMarketStatus: getMarketStatus,
17731
- timeDiffString: timeDiffString,
17732
17481
  getNYTimeZone: getNYTimeZone,
17733
17482
  getTradingDate: getTradingDate,
17483
+ getTradingStartAndEndDates: getTradingStartAndEndDates,
17734
17484
  },
17735
17485
  utils: {
17736
17486
  logIfDebug: logIfDebug,
@@ -17741,7 +17491,5 @@ const disco = {
17741
17491
 
17742
17492
  exports.AlpacaMarketDataAPI = AlpacaMarketDataAPI;
17743
17493
  exports.AlpacaTradingAPI = AlpacaTradingAPI;
17744
- exports.createAlpacaMarketDataAPI = createAlpacaMarketDataAPI;
17745
- exports.createAlpacaTradingAPI = createAlpacaTradingAPI;
17746
17494
  exports.disco = disco;
17747
17495
  //# sourceMappingURL=index.cjs.map