@discomedia/utils 1.0.5 → 1.0.7

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.
Files changed (87) hide show
  1. package/README.md +95 -3
  2. package/dist/index-frontend.cjs +16027 -0
  3. package/dist/index-frontend.cjs.map +1 -0
  4. package/dist/index-frontend.mjs +16023 -0
  5. package/dist/index-frontend.mjs.map +1 -0
  6. package/dist/index.cjs +1188 -921
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +1190 -921
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/package.json +8 -2
  11. package/dist/test.js +835 -731
  12. package/dist/test.js.map +1 -1
  13. package/dist/types/alpaca-market-data-api.d.ts +3 -15
  14. package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
  15. package/dist/types/alpaca-trading-api.d.ts +3 -6
  16. package/dist/types/alpaca-trading-api.d.ts.map +1 -1
  17. package/dist/types/index-frontend.d.ts +15 -0
  18. package/dist/types/index-frontend.d.ts.map +1 -0
  19. package/dist/types/index.d.ts +3 -28
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/market-time.d.ts +187 -117
  22. package/dist/types/market-time.d.ts.map +1 -1
  23. package/dist/types/old-test.d.ts +2 -0
  24. package/dist/types/old-test.d.ts.map +1 -0
  25. package/dist/types/testing/market-time-refactor-test.d.ts +1 -0
  26. package/dist/types/testing/market-time-refactor-test.d.ts.map +1 -0
  27. package/dist/types-frontend/alpaca-market-data-api.d.ts +372 -0
  28. package/dist/types-frontend/alpaca-market-data-api.d.ts.map +1 -0
  29. package/dist/types-frontend/alpaca-trading-api.d.ts +315 -0
  30. package/dist/types-frontend/alpaca-trading-api.d.ts.map +1 -0
  31. package/dist/types-frontend/format-tools.d.ts +46 -0
  32. package/dist/types-frontend/format-tools.d.ts.map +1 -0
  33. package/dist/types-frontend/index-frontend.d.ts +15 -0
  34. package/dist/types-frontend/index-frontend.d.ts.map +1 -0
  35. package/dist/types-frontend/index.d.ts +125 -0
  36. package/dist/types-frontend/index.d.ts.map +1 -0
  37. package/dist/types-frontend/json-tools.d.ts +33 -0
  38. package/dist/types-frontend/json-tools.d.ts.map +1 -0
  39. package/dist/types-frontend/llm-config.d.ts +36 -0
  40. package/dist/types-frontend/llm-config.d.ts.map +1 -0
  41. package/dist/types-frontend/llm-deepseek.d.ts +12 -0
  42. package/dist/types-frontend/llm-deepseek.d.ts.map +1 -0
  43. package/dist/types-frontend/llm-images.d.ts +49 -0
  44. package/dist/types-frontend/llm-images.d.ts.map +1 -0
  45. package/dist/types-frontend/llm-openai.d.ts +64 -0
  46. package/dist/types-frontend/llm-openai.d.ts.map +1 -0
  47. package/dist/types-frontend/llm-utils.d.ts +16 -0
  48. package/dist/types-frontend/llm-utils.d.ts.map +1 -0
  49. package/dist/types-frontend/logging.d.ts +12 -0
  50. package/dist/types-frontend/logging.d.ts.map +1 -0
  51. package/dist/types-frontend/market-hours.d.ts +24 -0
  52. package/dist/types-frontend/market-hours.d.ts.map +1 -0
  53. package/dist/types-frontend/market-time.d.ts +254 -0
  54. package/dist/types-frontend/market-time.d.ts.map +1 -0
  55. package/dist/types-frontend/misc-utils.d.ts +49 -0
  56. package/dist/types-frontend/misc-utils.d.ts.map +1 -0
  57. package/dist/types-frontend/old-test.d.ts +2 -0
  58. package/dist/types-frontend/old-test.d.ts.map +1 -0
  59. package/dist/types-frontend/polygon-indices.d.ts +85 -0
  60. package/dist/types-frontend/polygon-indices.d.ts.map +1 -0
  61. package/dist/types-frontend/polygon.d.ts +126 -0
  62. package/dist/types-frontend/polygon.d.ts.map +1 -0
  63. package/dist/types-frontend/technical-analysis.d.ts +90 -0
  64. package/dist/types-frontend/technical-analysis.d.ts.map +1 -0
  65. package/dist/types-frontend/test.d.ts +2 -0
  66. package/dist/types-frontend/test.d.ts.map +1 -0
  67. package/dist/types-frontend/testing/market-time-refactor-test.d.ts +1 -0
  68. package/dist/types-frontend/testing/market-time-refactor-test.d.ts.map +1 -0
  69. package/dist/types-frontend/types/alpaca-types.d.ts +962 -0
  70. package/dist/types-frontend/types/alpaca-types.d.ts.map +1 -0
  71. package/dist/types-frontend/types/index.d.ts +7 -0
  72. package/dist/types-frontend/types/index.d.ts.map +1 -0
  73. package/dist/types-frontend/types/llm-types.d.ts +82 -0
  74. package/dist/types-frontend/types/llm-types.d.ts.map +1 -0
  75. package/dist/types-frontend/types/logging-types.d.ts +10 -0
  76. package/dist/types-frontend/types/logging-types.d.ts.map +1 -0
  77. package/dist/types-frontend/types/market-time-types.d.ts +59 -0
  78. package/dist/types-frontend/types/market-time-types.d.ts.map +1 -0
  79. package/dist/types-frontend/types/polygon-indices-types.d.ts +190 -0
  80. package/dist/types-frontend/types/polygon-indices-types.d.ts.map +1 -0
  81. package/dist/types-frontend/types/polygon-types.d.ts +204 -0
  82. package/dist/types-frontend/types/polygon-types.d.ts.map +1 -0
  83. package/dist/types-frontend/types/ta-types.d.ts +89 -0
  84. package/dist/types-frontend/types/ta-types.d.ts.map +1 -0
  85. package/package.json +8 -2
  86. package/dist/types/time-utils.d.ts +0 -17
  87. package/dist/types/time-utils.d.ts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,181 +1,47 @@
1
- import ms from 'ms';
2
- import { fromZonedTime, formatInTimeZone, toZonedTime } from 'date-fns-tz';
3
- import { set, format, differenceInMilliseconds, startOfDay, sub, add, endOfDay, isBefore } from 'date-fns';
1
+ import { startOfDay, set, endOfDay, add, sub, format, differenceInMilliseconds, isBefore } from 'date-fns';
2
+ import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz';
4
3
  import require$$0$3, { EventEmitter } from 'events';
5
- import require$$1$1 from 'https';
4
+ import require$$1 from 'https';
6
5
  import require$$2 from 'http';
7
- import require$$3 from 'net';
8
- import require$$4 from 'tls';
9
- import require$$1 from 'crypto';
6
+ import require$$3$1 from 'net';
7
+ import require$$4$1 from 'tls';
8
+ import require$$3 from 'crypto';
10
9
  import require$$0$2 from 'stream';
11
10
  import require$$7 from 'url';
12
11
  import require$$0 from 'zlib';
13
12
  import require$$0$1 from 'buffer';
13
+ import require$$0$4 from 'fs';
14
+ import require$$1$1 from 'path';
15
+ import require$$2$1 from 'os';
14
16
 
15
- // time-utils.ts
16
- // Helper function to convert timestamp to Unix timestamp in seconds
17
- const toUnixTimestamp = (ts) => {
18
- return Math.floor(new Date(ts).getTime() / 1000);
19
- };
20
- function getTimeAgo(dateString) {
21
- // if format is like this: '20240919T102005', then first convert to '2024-09-19T10:20:05' format
22
- let dateValue = dateString;
23
- if (dateString && dateString.length === 15) {
24
- dateValue = dateString.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6');
25
- }
26
- const date = new Date(dateValue);
27
- const now = new Date();
28
- const diff = now.getTime() - date.getTime();
29
- const seconds = Math.floor(diff / 1000);
30
- const minutes = Math.floor(seconds / 60);
31
- const hours = Math.floor(minutes / 60);
32
- const days = Math.floor(hours / 24);
33
- const months = Math.floor(days / 30);
34
- const years = Math.floor(months / 12);
35
- if (years > 0) {
36
- return years === 1 ? '1 year ago' : `${years} years ago`;
37
- }
38
- else if (months > 0) {
39
- return months === 1 ? '1 month ago' : `${months} months ago`;
40
- }
41
- else if (days > 0) {
42
- return days === 1 ? '1 day ago' : `${days} days ago`;
43
- }
44
- else if (hours > 0) {
45
- return hours === 1 ? '1 hr ago' : `${hours} hrs ago`;
46
- }
47
- else if (minutes > 0) {
48
- return minutes === 1 ? '1 min ago' : `${minutes} mins ago`;
49
- }
50
- else {
51
- return 'A few seconds ago';
52
- }
53
- }
54
- function normalizeDate(timestamp) {
55
- const date = new Date(timestamp);
56
- return date.toISOString().split('T')[0]; // Returns 'YYYY-MM-DD'
57
- }
58
- // the function formerly known as CalculateRange, like a camel with two humps. Gross
59
- function calculateTimeRange(range) {
60
- const currentDate = new Date();
61
- switch (range) {
62
- case '1d':
63
- currentDate.setDate(currentDate.getDate() - 1);
64
- break;
65
- case '3d':
66
- currentDate.setDate(currentDate.getDate() - 3);
67
- break;
68
- case '1w':
69
- currentDate.setDate(currentDate.getDate() - 7);
70
- break;
71
- case '1m':
72
- currentDate.setMonth(currentDate.getMonth() - 1);
73
- break;
74
- case '3m':
75
- currentDate.setMonth(currentDate.getMonth() - 3);
76
- break;
77
- case '1y':
78
- currentDate.setFullYear(currentDate.getFullYear() - 1);
79
- break;
80
- default:
81
- throw new Error(`Invalid range: ${range}`);
17
+ /**
18
+ * Logs a message to the console.
19
+ * @param message The message to log.
20
+ * @param options Optional options.
21
+ * @param options.source The source of the message.
22
+ * @param options.type The type of message to log.
23
+ * @param options.symbol The trading symbol associated with this log.
24
+ * @param options.account The account associated with this log.
25
+ */
26
+ function log$1(message, options = { source: 'App', type: 'info' }) {
27
+ // Format the timestamp
28
+ const date = new Date();
29
+ const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
30
+ const account = options?.account;
31
+ const symbol = options?.symbol;
32
+ // Build the log message
33
+ const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
34
+ // Use appropriate console method based on type
35
+ if (options?.type === 'error') {
36
+ console.error(logMessage);
82
37
  }
83
- return currentDate.toISOString().split('T')[0]; // format date to 'YYYY-MM-DD'
84
- }
85
- const daysLeft = (accountCreationDate, maxDays) => {
86
- const now = new Date();
87
- const endPeriodDate = new Date(accountCreationDate);
88
- endPeriodDate.setDate(accountCreationDate.getDate() + maxDays);
89
- const diffInMilliseconds = endPeriodDate.getTime() - now.getTime();
90
- // Convert milliseconds to days and return
91
- return Math.ceil(diffInMilliseconds / (1000 * 60 * 60 * 24));
92
- };
93
- const cutoffDate = new Date('2023-10-17T00:00:00.000Z');
94
- const calculateDaysLeft = (accountCreationDate) => {
95
- let maxDays;
96
- if (accountCreationDate < cutoffDate) {
97
- maxDays = 30;
98
- accountCreationDate = new Date('2023-10-01T00:00:00.000Z');
38
+ else if (options?.type === 'warn') {
39
+ console.warn(logMessage);
99
40
  }
100
41
  else {
101
- maxDays = 14;
102
- }
103
- return daysLeft(accountCreationDate, maxDays);
104
- };
105
- const timeAgo = (timestamp) => {
106
- if (!timestamp)
107
- return 'Just now';
108
- const diff = Date.now() - new Date(timestamp).getTime();
109
- if (diff < 60000) {
110
- // less than 1 second
111
- return 'Just now';
112
- }
113
- else if (diff > 82800000) {
114
- // more than 23 hours – similar to how Twitter displays timestamps
115
- return new Date(timestamp).toLocaleDateString('en-US', {
116
- month: 'short',
117
- day: 'numeric',
118
- year: new Date(timestamp).getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
119
- });
120
- }
121
- return `${ms(diff)} ago`;
122
- };
123
- // returns date utc
124
- const formatDate = (dateString, updateDate) => {
125
- return new Date(dateString).toLocaleDateString('en-US', {
126
- day: 'numeric',
127
- month: 'long',
128
- year: updateDate && new Date(dateString).getFullYear() === new Date().getFullYear() ? undefined : 'numeric',
129
- timeZone: 'UTC',
130
- });
131
- };
132
- const parseETDateFromAV = (dateString) => {
133
- // Time zone identifier for Eastern Time
134
- const timeZone = 'America/New_York';
135
- // Split the input string into date and time components
136
- const [datePart, timePart] = dateString.split(' ');
137
- // Construct a full date-time string in ISO format
138
- const fullString = `${datePart}T${timePart}`;
139
- // Convert the string to a UTC Date object using date-fns-tz
140
- const utcDate = fromZonedTime(fullString, timeZone); // Convert to UTC
141
- return utcDate;
142
- };
143
- const formatToUSEastern = (date, justDate) => {
144
- const options = {
145
- timeZone: 'America/New_York',
146
- month: 'short',
147
- day: 'numeric',
148
- year: 'numeric',
149
- };
150
- if (!justDate) {
151
- options.hour = 'numeric';
152
- options.minute = '2-digit';
153
- options.hour12 = true;
42
+ console.log(logMessage);
154
43
  }
155
- return date.toLocaleString('en-US', options);
156
- };
157
- const unixTimetoUSEastern = (timestamp) => {
158
- const date = new Date(timestamp);
159
- const timeString = formatToUSEastern(date);
160
- const dateString = formatToUSEastern(date, true);
161
- return { date, timeString, dateString };
162
- };
163
- const timeDiffString = (milliseconds) => {
164
- const seconds = Math.floor(milliseconds / 1000);
165
- const minutes = Math.floor(seconds / 60);
166
- const hours = Math.floor(minutes / 60);
167
- const days = Math.floor(hours / 24);
168
- const remainingHours = hours % 24;
169
- const remainingMinutes = minutes % 60;
170
- const parts = [];
171
- if (days > 0)
172
- parts.push(`${days} day${days > 1 ? 's' : ''}`);
173
- if (remainingHours > 0)
174
- parts.push(`${remainingHours} hour${remainingHours > 1 ? 's' : ''}`);
175
- if (remainingMinutes > 0)
176
- parts.push(`${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}`);
177
- return parts.join(', ');
178
- };
44
+ }
179
45
 
180
46
  // market-hours.ts
181
47
  const marketHolidays = {
@@ -280,99 +146,66 @@ const marketEarlyCloses = {
280
146
  },
281
147
  };
282
148
 
283
- // market-time.ts
149
+ // market-time.ts - Refactored for better organization and usability
150
+ // ===== CONFIGURATION =====
284
151
  /**
285
- * Market times for NYSE
286
- * Regular market hours are 9:30am-4:00pm
287
- * Early market hours are 9:30am-10:00am (first 30 minutes)
288
- * Extended market hours are 4:00am to 9:30am and 4:00pm-8:00pm
289
- * On days before some holidays, the market closes early at 1:00pm
290
- * Early extended market hours are 1:00pm-5:00pm on early close days
152
+ * Market configuration constants
291
153
  */
292
- const MARKET_TIMES = {
154
+ const MARKET_CONFIG = {
293
155
  TIMEZONE: 'America/New_York',
294
- PRE: { START: { HOUR: 4, MINUTE: 0, MINUTES: 240 }, END: { HOUR: 9, MINUTE: 30, MINUTES: 570 } },
295
- EARLY_MORNING: { START: { HOUR: 9, MINUTE: 30, MINUTES: 570 }, END: { HOUR: 10, MINUTE: 0, MINUTES: 600 } }, // early market trading
296
- EARLY_CLOSE_BEFORE_HOLIDAY: {
297
- START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
298
- END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
299
- }, // early market trading end
300
- EARLY_EXTENDED_BEFORE_HOLIDAY: {
301
- START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
302
- END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
303
- }, // extended hours trading on early close days
304
- REGULAR: { START: { HOUR: 9, MINUTE: 30, MINUTES: 570 }, END: { HOUR: 16, MINUTE: 0, MINUTES: 960 } },
305
- EXTENDED: { START: { HOUR: 4, MINUTE: 0, MINUTES: 240 }, END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 } },
156
+ TIMES: {
157
+ EXTENDED_START: { hour: 4, minute: 0 },
158
+ MARKET_OPEN: { hour: 9, minute: 30 },
159
+ EARLY_MARKET_END: { hour: 10, minute: 0 },
160
+ MARKET_CLOSE: { hour: 16, minute: 0 },
161
+ EARLY_CLOSE: { hour: 13, minute: 0 },
162
+ EXTENDED_END: { hour: 20, minute: 0 },
163
+ EARLY_EXTENDED_END: { hour: 17, minute: 0 },
164
+ },
306
165
  };
166
+ // ===== MARKET CALENDAR SERVICE =====
307
167
  /**
308
- * Utility class for handling market time-related operations
168
+ * Service for handling market calendar operations (holidays, early closes, market days)
309
169
  */
310
- class MarketTimeUtil {
170
+ class MarketCalendar {
311
171
  timezone;
312
- intradayReporting;
313
- /**
314
- * Creates a new MarketTimeUtil instance
315
- * @param {string} [timezone='America/New_York'] - The timezone to use for market time calculations
316
- * @param {IntradayReporting} [intradayReporting='market_hours'] - The intraday reporting mode
317
- */
318
- constructor(timezone = MARKET_TIMES.TIMEZONE, intradayReporting = 'market_hours') {
319
- this.validateTimezone(timezone);
172
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
320
173
  this.timezone = timezone;
321
- this.intradayReporting = intradayReporting;
322
174
  }
323
175
  /**
324
- * Validates the provided timezone
325
- * @private
326
- * @param {string} timezone - The timezone to validate
327
- * @throws {Error} If the timezone is invalid
176
+ * Check if a date is a weekend
328
177
  */
329
- validateTimezone(timezone) {
330
- try {
331
- Intl.DateTimeFormat(undefined, { timeZone: timezone });
332
- }
333
- catch (error) {
334
- throw new Error(`Invalid timezone: ${timezone}`);
335
- }
336
- }
337
- formatDate(date, outputFormat = 'iso') {
338
- switch (outputFormat) {
339
- case 'unix-seconds':
340
- return Math.floor(date.getTime() / 1000);
341
- case 'unix-ms':
342
- return date.getTime();
343
- case 'iso':
344
- default:
345
- // return with timezone offset
346
- return formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
347
- }
348
- }
349
178
  isWeekend(date) {
350
179
  const day = date.getDay();
351
- return day === 0 || day === 6;
180
+ return day === 0 || day === 6; // Sunday or Saturday
352
181
  }
182
+ /**
183
+ * Check if a date is a market holiday
184
+ */
353
185
  isHoliday(date) {
354
- const formattedDate = format(date, 'yyyy-MM-dd');
355
- const yearHolidays = marketHolidays[date.getFullYear()];
356
- for (const holiday in yearHolidays) {
357
- if (yearHolidays[holiday].date === formattedDate) {
358
- return true;
359
- }
360
- }
361
- return false;
186
+ const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
187
+ const year = toZonedTime(date, this.timezone).getFullYear();
188
+ const yearHolidays = marketHolidays[year];
189
+ if (!yearHolidays)
190
+ return false;
191
+ return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
362
192
  }
193
+ /**
194
+ * Check if a date is an early close day
195
+ */
363
196
  isEarlyCloseDay(date) {
364
- const formattedDate = format(date, 'yyyy-MM-dd');
365
- const yearEarlyCloses = marketEarlyCloses[date.getFullYear()];
197
+ const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
198
+ const year = toZonedTime(date, this.timezone).getFullYear();
199
+ const yearEarlyCloses = marketEarlyCloses[year];
366
200
  return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
367
201
  }
368
202
  /**
369
- * Get the early close time for a given date
370
- * @param date - The date to get the early close time for
371
- * @returns The early close time in minutes from midnight, or null if there is no early close
203
+ * Get the early close time for a date (in minutes from midnight)
372
204
  */
373
205
  getEarlyCloseTime(date) {
374
- const formattedDate = format(date, 'yyyy-MM-dd');
375
- const yearEarlyCloses = marketEarlyCloses[date.getFullYear()];
206
+ const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
207
+ const year = toZonedTime(date, this.timezone).getFullYear();
208
+ const yearEarlyCloses = marketEarlyCloses[year];
376
209
  if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
377
210
  const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
378
211
  return hours * 60 + minutes;
@@ -380,194 +213,283 @@ class MarketTimeUtil {
380
213
  return null;
381
214
  }
382
215
  /**
383
- * Check if a given date is a market day
384
- * @param date - The date to check
385
- * @returns true if the date is a market day, false otherwise
216
+ * Check if a date is a market day (not weekend or holiday)
386
217
  */
387
218
  isMarketDay(date) {
388
- const isWeekendDay = this.isWeekend(date);
389
- const isHolidayDay = this.isHoliday(date);
390
- const returner = !isWeekendDay && !isHolidayDay;
391
- return returner;
219
+ return !this.isWeekend(date) && !this.isHoliday(date);
392
220
  }
393
221
  /**
394
- * Check if a given date is within market hours
395
- * @param date - The date to check
396
- * @returns true if the date is within market hours, false otherwise
222
+ * Get the next market day from a given date
397
223
  */
398
- isWithinMarketHours(date) {
399
- // Check for holidays first
400
- if (this.isHoliday(date)) {
401
- return false;
224
+ getNextMarketDay(date) {
225
+ let nextDay = add(date, { days: 1 });
226
+ while (!this.isMarketDay(nextDay)) {
227
+ nextDay = add(nextDay, { days: 1 });
402
228
  }
403
- const timeInMinutes = date.getHours() * 60 + date.getMinutes();
404
- // Check for early closure
405
- if (this.isEarlyCloseDay(date)) {
406
- const earlyCloseMinutes = this.getEarlyCloseTime(date);
407
- if (earlyCloseMinutes !== null && timeInMinutes > earlyCloseMinutes) {
408
- return false;
409
- }
229
+ return nextDay;
230
+ }
231
+ /**
232
+ * Get the previous market day from a given date
233
+ */
234
+ getPreviousMarketDay(date) {
235
+ let prevDay = sub(date, { days: 1 });
236
+ while (!this.isMarketDay(prevDay)) {
237
+ prevDay = sub(prevDay, { days: 1 });
410
238
  }
411
- // Regular market hours logic
412
- let returner;
413
- switch (this.intradayReporting) {
414
- case 'extended_hours': {
415
- const extendedStartMinutes = MARKET_TIMES.EXTENDED.START.HOUR * 60 + MARKET_TIMES.EXTENDED.START.MINUTE;
416
- const extendedEndMinutes = MARKET_TIMES.EXTENDED.END.HOUR * 60 + MARKET_TIMES.EXTENDED.END.MINUTE;
417
- // Comprehensive handling of times crossing midnight
418
- const adjustedDate = timeInMinutes < extendedStartMinutes ? sub(date, { days: 1 }) : date;
419
- const adjustedTimeInMinutes = adjustedDate.getHours() * 60 + adjustedDate.getMinutes();
420
- returner = adjustedTimeInMinutes >= extendedStartMinutes && adjustedTimeInMinutes <= extendedEndMinutes;
421
- break;
422
- }
423
- case 'continuous':
424
- returner = true;
425
- break;
426
- default: {
427
- // market_hours
428
- const regularStartMinutes = MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
429
- const regularEndMinutes = MARKET_TIMES.REGULAR.END.HOUR * 60 + MARKET_TIMES.REGULAR.END.MINUTE;
430
- returner = timeInMinutes >= regularStartMinutes && timeInMinutes <= regularEndMinutes;
431
- break;
432
- }
239
+ return prevDay;
240
+ }
241
+ }
242
+ // ===== TIME FORMATTER SERVICE =====
243
+ /**
244
+ * Service for formatting time outputs
245
+ */
246
+ class TimeFormatter {
247
+ timezone;
248
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
249
+ this.timezone = timezone;
250
+ }
251
+ /**
252
+ * Format a date based on the output format
253
+ */
254
+ formatDate(date, outputFormat = 'iso') {
255
+ switch (outputFormat) {
256
+ case 'unix-seconds':
257
+ return Math.floor(date.getTime() / 1000);
258
+ case 'unix-ms':
259
+ return date.getTime();
260
+ case 'iso':
261
+ default:
262
+ return formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
433
263
  }
434
- return returner;
435
264
  }
436
265
  /**
437
- * Check if a given date is before market hours
438
- * @param date - The date to check
439
- * @returns true if the date is before market hours, false otherwise
266
+ * Get New York timezone offset
440
267
  */
441
- isBeforeMarketHours(date) {
442
- const timeInMinutes = date.getHours() * 60 + date.getMinutes();
443
- const startMinutes = this.intradayReporting === 'extended_hours'
444
- ? MARKET_TIMES.EXTENDED.START.HOUR * 60 + MARKET_TIMES.EXTENDED.START.MINUTE
445
- : MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
446
- return timeInMinutes < startMinutes;
268
+ getNYTimeZone(date = new Date()) {
269
+ const dtf = new Intl.DateTimeFormat('en-US', {
270
+ timeZone: this.timezone,
271
+ timeZoneName: 'shortOffset',
272
+ });
273
+ const parts = dtf.formatToParts(date);
274
+ const tz = parts.find(p => p.type === 'timeZoneName')?.value;
275
+ if (!tz) {
276
+ throw new Error('Could not determine New York offset');
277
+ }
278
+ const shortOffset = tz.replace('GMT', '');
279
+ if (shortOffset === '-4') {
280
+ return '-04:00';
281
+ }
282
+ else if (shortOffset === '-5') {
283
+ return '-05:00';
284
+ }
285
+ else {
286
+ throw new Error(`Unexpected timezone offset: ${shortOffset}`);
287
+ }
447
288
  }
448
289
  /**
449
- * Get the last trading date, i.e. the last date that was a market day
450
- * @param currentDate - The current date
451
- * @returns The last trading date
290
+ * Get trading date in YYYY-MM-DD format
452
291
  */
453
- getLastTradingDate(currentDate = new Date()) {
454
- const nowET = toZonedTime(currentDate, this.timezone);
455
- const isMarketDayToday = this.isMarketDay(nowET);
456
- const currentMinutes = nowET.getHours() * 60 + nowET.getMinutes();
457
- const marketOpenMinutes = MARKET_TIMES.REGULAR.START.HOUR * 60 + MARKET_TIMES.REGULAR.START.MINUTE;
458
- if (isMarketDayToday && currentMinutes >= marketOpenMinutes) {
459
- // After market open on a market day, return today
460
- return nowET;
292
+ getTradingDate(time) {
293
+ let date;
294
+ if (typeof time === 'number') {
295
+ date = new Date(time);
296
+ }
297
+ else if (typeof time === 'string') {
298
+ date = new Date(time);
461
299
  }
462
300
  else {
463
- // Before market open, or not a market day, return previous trading day
464
- let lastTradingDate = sub(nowET, { days: 1 });
465
- while (!this.isMarketDay(lastTradingDate)) {
466
- lastTradingDate = sub(lastTradingDate, { days: 1 });
467
- }
468
- return lastTradingDate;
301
+ date = time;
469
302
  }
303
+ return formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
470
304
  }
471
- getLastMarketDay(date) {
472
- let currentDate = sub(date, { days: 1 });
473
- while (!this.isMarketDay(currentDate)) {
474
- currentDate = sub(currentDate, { days: 1 });
305
+ }
306
+ // ===== MARKET TIME CALCULATOR =====
307
+ /**
308
+ * Service for core market time calculations
309
+ */
310
+ class MarketTimeCalculator {
311
+ calendar;
312
+ formatter;
313
+ timezone;
314
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
315
+ this.timezone = timezone;
316
+ this.calendar = new MarketCalendar(timezone);
317
+ this.formatter = new TimeFormatter(timezone);
318
+ }
319
+ /**
320
+ * Get market open/close times for a date
321
+ */
322
+ getMarketTimes(date) {
323
+ const zonedDate = toZonedTime(date, this.timezone);
324
+ // Market closed on weekends and holidays
325
+ if (!this.calendar.isMarketDay(zonedDate)) {
326
+ return {
327
+ marketOpen: false,
328
+ open: null,
329
+ close: null,
330
+ openExt: null,
331
+ closeExt: null,
332
+ };
333
+ }
334
+ const dayStart = startOfDay(zonedDate);
335
+ // Regular market times
336
+ const open = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour, minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute }), this.timezone);
337
+ let close = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute }), this.timezone);
338
+ // Extended hours
339
+ const openExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute }), this.timezone);
340
+ let closeExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute }), this.timezone);
341
+ // Handle early close days
342
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
343
+ close = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute }), this.timezone);
344
+ closeExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute }), this.timezone);
345
+ }
346
+ return {
347
+ marketOpen: true,
348
+ open,
349
+ close,
350
+ openExt,
351
+ closeExt,
352
+ };
353
+ }
354
+ /**
355
+ * Check if a time is within market hours based on intraday reporting mode
356
+ */
357
+ isWithinMarketHours(date, intradayReporting = 'market_hours') {
358
+ const zonedDate = toZonedTime(date, this.timezone);
359
+ // Not a market day
360
+ if (!this.calendar.isMarketDay(zonedDate)) {
361
+ return false;
362
+ }
363
+ const timeInMinutes = zonedDate.getHours() * 60 + zonedDate.getMinutes();
364
+ switch (intradayReporting) {
365
+ case 'extended_hours': {
366
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
367
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
368
+ // Handle early close
369
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
370
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
371
+ }
372
+ return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
373
+ }
374
+ case 'continuous':
375
+ return true;
376
+ default: {
377
+ // 'market_hours'
378
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
379
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
380
+ // Handle early close
381
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
382
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
383
+ }
384
+ return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
385
+ }
475
386
  }
476
- return currentDate;
477
387
  }
388
+ /**
389
+ * Get the last full trading date
390
+ */
478
391
  getLastFullTradingDate(currentDate = new Date()) {
479
392
  const nowET = toZonedTime(currentDate, this.timezone);
480
- // If today is a market day and we're after extended hours close
481
- // then return today since it's a completed trading day
482
- if (this.isMarketDay(nowET)) {
393
+ // If today is a market day and we're after extended hours close, return today
394
+ if (this.calendar.isMarketDay(nowET)) {
483
395
  const timeInMinutes = nowET.getHours() * 60 + nowET.getMinutes();
484
- const extendedEndMinutes = MARKET_TIMES.EXTENDED.END.HOUR * 60 + MARKET_TIMES.EXTENDED.END.MINUTE;
485
- // Check if we're after market close (including extended hours)
396
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
397
+ if (this.calendar.isEarlyCloseDay(nowET)) {
398
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
399
+ }
486
400
  if (timeInMinutes >= extendedEndMinutes) {
487
- // Set to midnight ET while preserving the date
488
401
  return fromZonedTime(set(nowET, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
489
402
  }
490
403
  }
491
- // In all other cases (during trading hours, before market open, holidays, weekends),
492
- // we want the last completed trading day
493
- let lastFullDate = this.getLastMarketDay(nowET);
494
- // Set to midnight ET while preserving the date
495
- return fromZonedTime(set(lastFullDate, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
404
+ // Return the last completed trading day
405
+ const lastMarketDay = this.calendar.getPreviousMarketDay(nowET);
406
+ return fromZonedTime(set(lastMarketDay, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
496
407
  }
497
408
  /**
498
- * Gets the next market day from a reference date
499
- * @param {Object} [options] - Options object
500
- * @param {Date} [options.referenceDate] - The reference date (defaults to current date)
501
- * @returns {Object} The next market day information
502
- * @property {Date} date - The date object (start of day in NY time)
503
- * @property {string} yyyymmdd - The date in YYYY-MM-DD format
504
- * @property {string} dateISOString - Full ISO date string
409
+ * Get day boundaries based on intraday reporting mode
505
410
  */
506
- getNextMarketDay(date) {
507
- let currentDate = add(date, { days: 1 });
508
- while (!this.isMarketDay(currentDate)) {
509
- currentDate = add(currentDate, { days: 1 });
510
- }
511
- return currentDate;
512
- }
513
- getDayBoundaries(date) {
411
+ getDayBoundaries(date, intradayReporting = 'market_hours') {
412
+ const zonedDate = toZonedTime(date, this.timezone);
514
413
  let start;
515
414
  let end;
516
- switch (this.intradayReporting) {
415
+ switch (intradayReporting) {
517
416
  case 'extended_hours': {
518
- start = set(date, {
519
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
520
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
417
+ start = set(zonedDate, {
418
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
419
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
521
420
  seconds: 0,
522
421
  milliseconds: 0,
523
422
  });
524
- end = set(date, {
525
- hours: MARKET_TIMES.EXTENDED.END.HOUR,
526
- minutes: MARKET_TIMES.EXTENDED.END.MINUTE,
423
+ end = set(zonedDate, {
424
+ hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour,
425
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute,
527
426
  seconds: 59,
528
427
  milliseconds: 999,
529
428
  });
429
+ // Handle early close
430
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
431
+ end = set(zonedDate, {
432
+ hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour,
433
+ minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute,
434
+ seconds: 59,
435
+ milliseconds: 999,
436
+ });
437
+ }
530
438
  break;
531
439
  }
532
440
  case 'continuous': {
533
- start = startOfDay(date);
534
- end = endOfDay(date);
441
+ start = startOfDay(zonedDate);
442
+ end = endOfDay(zonedDate);
535
443
  break;
536
444
  }
537
445
  default: {
538
- // market_hours
539
- start = set(date, {
540
- hours: MARKET_TIMES.REGULAR.START.HOUR,
541
- minutes: MARKET_TIMES.REGULAR.START.MINUTE,
446
+ // 'market_hours'
447
+ start = set(zonedDate, {
448
+ hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
449
+ minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
542
450
  seconds: 0,
543
451
  milliseconds: 0,
544
452
  });
545
- // Check for early close
546
- if (this.isEarlyCloseDay(date)) {
547
- const earlyCloseMinutes = this.getEarlyCloseTime(date);
548
- if (earlyCloseMinutes !== null) {
549
- const earlyCloseHours = Math.floor(earlyCloseMinutes / 60);
550
- const earlyCloseMinutesRemainder = earlyCloseMinutes % 60;
551
- end = set(date, {
552
- hours: earlyCloseHours,
553
- minutes: earlyCloseMinutesRemainder,
554
- seconds: 59,
555
- milliseconds: 999,
556
- });
557
- break;
558
- }
559
- }
560
- end = set(date, {
561
- hours: MARKET_TIMES.REGULAR.END.HOUR,
562
- minutes: MARKET_TIMES.REGULAR.END.MINUTE,
453
+ end = set(zonedDate, {
454
+ hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
455
+ minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
563
456
  seconds: 59,
564
457
  milliseconds: 999,
565
458
  });
459
+ // Handle early close
460
+ if (this.calendar.isEarlyCloseDay(zonedDate)) {
461
+ end = set(zonedDate, {
462
+ hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour,
463
+ minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute,
464
+ seconds: 59,
465
+ milliseconds: 999,
466
+ });
467
+ }
566
468
  break;
567
469
  }
568
470
  }
569
- return { start, end };
471
+ return {
472
+ start: fromZonedTime(start, this.timezone),
473
+ end: fromZonedTime(end, this.timezone),
474
+ };
475
+ }
476
+ }
477
+ // ===== PERIOD CALCULATOR =====
478
+ /**
479
+ * Service for calculating time periods
480
+ */
481
+ class PeriodCalculator {
482
+ calendar;
483
+ timeCalculator;
484
+ formatter;
485
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
486
+ this.calendar = new MarketCalendar(timezone);
487
+ this.timeCalculator = new MarketTimeCalculator(timezone);
488
+ this.formatter = new TimeFormatter(timezone);
570
489
  }
490
+ /**
491
+ * Calculate the start date for a given period
492
+ */
571
493
  calculatePeriodStartDate(endDate, period) {
572
494
  let startDate;
573
495
  switch (period) {
@@ -575,7 +497,7 @@ class MarketTimeUtil {
575
497
  startDate = set(endDate, { month: 0, date: 1 });
576
498
  break;
577
499
  case '1D':
578
- startDate = this.getLastMarketDay(endDate);
500
+ startDate = this.calendar.getPreviousMarketDay(endDate);
579
501
  break;
580
502
  case '3D':
581
503
  startDate = sub(endDate, { days: 3 });
@@ -601,405 +523,286 @@ class MarketTimeUtil {
601
523
  default:
602
524
  throw new Error(`Invalid period: ${period}`);
603
525
  }
604
- while (!this.isMarketDay(startDate)) {
605
- startDate = this.getNextMarketDay(startDate);
526
+ // Ensure start date is a market day
527
+ while (!this.calendar.isMarketDay(startDate)) {
528
+ startDate = this.calendar.getNextMarketDay(startDate);
606
529
  }
607
530
  return startDate;
608
531
  }
609
- getMarketTimePeriod({ period, end = new Date(), intraday_reporting, outputFormat = 'iso', }) {
532
+ /**
533
+ * Get period dates for market time calculations
534
+ */
535
+ getMarketTimePeriod(params) {
536
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
610
537
  if (!period) {
611
538
  throw new Error('Period is required');
612
539
  }
613
- if (intraday_reporting) {
614
- this.intradayReporting = intraday_reporting;
615
- }
616
- // Convert end date to specified timezone
617
- const zonedEndDate = toZonedTime(end, this.timezone);
618
- let startDate;
540
+ const zonedEndDate = toZonedTime(end, MARKET_CONFIG.TIMEZONE);
541
+ // Determine effective end date based on current market conditions
619
542
  let endDate;
620
- const isCurrentMarketDay = this.isMarketDay(zonedEndDate);
621
- const isWithinHours = this.isWithinMarketHours(zonedEndDate);
622
- const isBeforeHours = this.isBeforeMarketHours(zonedEndDate);
623
- // First determine the end date based on current market conditions
543
+ const isCurrentMarketDay = this.calendar.isMarketDay(zonedEndDate);
544
+ const isWithinHours = this.timeCalculator.isWithinMarketHours(end, intraday_reporting);
545
+ const timeInMinutes = zonedEndDate.getHours() * 60 + zonedEndDate.getMinutes();
546
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
624
547
  if (isCurrentMarketDay) {
625
- if (isBeforeHours) {
626
- // Case 1: Market day before open hours - use previous full trading day
627
- const lastMarketDay = this.getLastMarketDay(zonedEndDate);
628
- const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
548
+ if (timeInMinutes < marketStartMinutes) {
549
+ // Before market open - use previous day's close
550
+ const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
551
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
629
552
  endDate = dayEnd;
630
553
  }
631
554
  else if (isWithinHours) {
632
- // Case 2: Market day during hours - use current time
633
- endDate = zonedEndDate;
555
+ // During market hours - use current time
556
+ endDate = end;
634
557
  }
635
558
  else {
636
- // Case 3: Market day after close - use today's close
637
- const { end: dayEnd } = this.getDayBoundaries(zonedEndDate);
559
+ // After market close - use today's close
560
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(zonedEndDate, intraday_reporting);
638
561
  endDate = dayEnd;
639
562
  }
640
563
  }
641
564
  else {
642
- // Case 4: Not a market day - use previous market day's close
643
- const lastMarketDay = this.getLastMarketDay(zonedEndDate);
644
- const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
565
+ // Not a market day - use previous market day's close
566
+ const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
567
+ const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
645
568
  endDate = dayEnd;
646
569
  }
647
- // Now calculate the start date based on the period
570
+ // Calculate start date
648
571
  const periodStartDate = this.calculatePeriodStartDate(endDate, period);
649
- const { start: dayStart } = this.getDayBoundaries(periodStartDate);
650
- startDate = dayStart;
651
- // Convert boundaries back to UTC for final output
652
- const utcStart = fromZonedTime(startDate, this.timezone);
653
- const utcEnd = fromZonedTime(endDate, this.timezone);
572
+ const { start: dayStart } = this.timeCalculator.getDayBoundaries(periodStartDate, intraday_reporting);
654
573
  // Ensure start is not after end
655
- if (isBefore(utcEnd, utcStart)) {
574
+ if (isBefore(endDate, dayStart)) {
656
575
  throw new Error('Start date cannot be after end date');
657
576
  }
658
577
  return {
659
- start: this.formatDate(utcStart, outputFormat),
660
- end: this.formatDate(utcEnd, outputFormat),
578
+ start: this.formatter.formatDate(dayStart, outputFormat),
579
+ end: this.formatter.formatDate(endDate, outputFormat),
661
580
  };
662
581
  }
663
- getMarketOpenClose(options = {}) {
664
- const { date = new Date() } = options;
665
- const zonedDate = toZonedTime(date, this.timezone);
666
- // Check if market is closed for the day
667
- if (this.isWeekend(zonedDate) || this.isHoliday(zonedDate)) {
668
- return {
669
- marketOpen: false,
670
- open: null,
671
- close: null,
672
- openExt: null,
673
- closeExt: null,
674
- };
582
+ }
583
+ // ===== MARKET STATUS SERVICE =====
584
+ /**
585
+ * Service for determining market status
586
+ */
587
+ class MarketStatusService {
588
+ calendar;
589
+ timeCalculator;
590
+ timezone;
591
+ constructor(timezone = MARKET_CONFIG.TIMEZONE) {
592
+ this.timezone = timezone;
593
+ this.calendar = new MarketCalendar(timezone);
594
+ this.timeCalculator = new MarketTimeCalculator(timezone);
595
+ }
596
+ /**
597
+ * Get current market status
598
+ */
599
+ getMarketStatus(date = new Date()) {
600
+ const nyTime = toZonedTime(date, this.timezone);
601
+ const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
602
+ const isMarketDay = this.calendar.isMarketDay(nyTime);
603
+ const isEarlyCloseDay = this.calendar.isEarlyCloseDay(nyTime);
604
+ // Time boundaries
605
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
606
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
607
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
608
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
609
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
610
+ if (isEarlyCloseDay) {
611
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
612
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
613
+ }
614
+ let status;
615
+ let nextStatus;
616
+ let nextStatusTime;
617
+ let marketPeriod;
618
+ if (!isMarketDay) {
619
+ // Market is closed (holiday/weekend)
620
+ status = 'closed';
621
+ nextStatus = 'extended hours';
622
+ marketPeriod = 'closed';
623
+ const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
624
+ nextStatusTime = set(nextMarketDay, {
625
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
626
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
627
+ });
675
628
  }
676
- const dayStart = startOfDay(zonedDate);
677
- const regularOpenTime = MARKET_TIMES.REGULAR.START;
678
- let regularCloseTime = MARKET_TIMES.REGULAR.END;
679
- const extendedOpenTime = MARKET_TIMES.EXTENDED.START;
680
- let extendedCloseTime = MARKET_TIMES.EXTENDED.END;
681
- // Check for early close
682
- const isEarlyClose = this.isEarlyCloseDay(zonedDate);
683
- if (isEarlyClose) {
684
- const earlyCloseMinutes = this.getEarlyCloseTime(zonedDate);
685
- if (earlyCloseMinutes !== null) {
686
- // For regular hours, use the early close time
687
- regularCloseTime = {
688
- HOUR: Math.floor(earlyCloseMinutes / 60),
689
- MINUTE: earlyCloseMinutes % 60,
690
- MINUTES: earlyCloseMinutes,
691
- };
692
- // For extended hours on early close days, close at 5:00 PM
693
- extendedCloseTime = {
694
- HOUR: 17,
695
- MINUTE: 0,
696
- MINUTES: 1020,
697
- };
698
- }
629
+ else if (timeInMinutes < extendedStartMinutes) {
630
+ // Before extended hours
631
+ status = 'closed';
632
+ nextStatus = 'extended hours';
633
+ marketPeriod = 'closed';
634
+ nextStatusTime = set(nyTime, {
635
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
636
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
637
+ });
699
638
  }
700
- const open = fromZonedTime(set(dayStart, { hours: regularOpenTime.HOUR, minutes: regularOpenTime.MINUTE }), this.timezone);
701
- const close = fromZonedTime(set(dayStart, { hours: regularCloseTime.HOUR, minutes: regularCloseTime.MINUTE }), this.timezone);
702
- const openExt = fromZonedTime(set(dayStart, { hours: extendedOpenTime.HOUR, minutes: extendedOpenTime.MINUTE }), this.timezone);
703
- const closeExt = fromZonedTime(set(dayStart, { hours: extendedCloseTime.HOUR, minutes: extendedCloseTime.MINUTE }), this.timezone);
639
+ else if (timeInMinutes < marketStartMinutes) {
640
+ // Pre-market extended hours
641
+ status = 'extended hours';
642
+ nextStatus = 'open';
643
+ marketPeriod = 'preMarket';
644
+ nextStatusTime = set(nyTime, {
645
+ hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
646
+ minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
647
+ });
648
+ }
649
+ else if (timeInMinutes < marketCloseMinutes) {
650
+ // Market is open
651
+ status = 'open';
652
+ nextStatus = 'extended hours';
653
+ marketPeriod = timeInMinutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
654
+ nextStatusTime = set(nyTime, {
655
+ hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.hour : MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
656
+ minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.minute : MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
657
+ });
658
+ }
659
+ else if (timeInMinutes < extendedEndMinutes) {
660
+ // After-market extended hours
661
+ status = 'extended hours';
662
+ nextStatus = 'closed';
663
+ marketPeriod = 'afterMarket';
664
+ nextStatusTime = set(nyTime, {
665
+ hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour : MARKET_CONFIG.TIMES.EXTENDED_END.hour,
666
+ minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute : MARKET_CONFIG.TIMES.EXTENDED_END.minute,
667
+ });
668
+ }
669
+ else {
670
+ // After extended hours
671
+ status = 'closed';
672
+ nextStatus = 'extended hours';
673
+ marketPeriod = 'closed';
674
+ const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
675
+ nextStatusTime = set(nextMarketDay, {
676
+ hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
677
+ minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
678
+ });
679
+ }
680
+ const nextStatusTimeUTC = fromZonedTime(nextStatusTime, this.timezone);
681
+ const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
704
682
  return {
705
- marketOpen: true,
706
- open,
707
- close,
708
- openExt,
709
- closeExt,
683
+ time: date,
684
+ timeString: format(nyTime, dateFormat),
685
+ status,
686
+ nextStatus,
687
+ marketPeriod,
688
+ nextStatusTime: nextStatusTimeUTC,
689
+ nextStatusTimeDifference: differenceInMilliseconds(nextStatusTime, nyTime),
690
+ nextStatusTimeString: format(nextStatusTime, dateFormat),
710
691
  };
711
692
  }
712
693
  }
694
+ // ===== FUNCTIONAL API =====
713
695
  /**
714
- * Creates a new MarketTimeUtil instance
715
- * @param {string} [timezone] - The timezone to use for market time calculations
716
- * @param {IntradayReporting} [intraday_reporting] - The intraday reporting mode
717
- * @returns {MarketTimeUtil} A new MarketTimeUtil instance
718
- */
719
- function createMarketTimeUtil(timezone, intraday_reporting) {
720
- return new MarketTimeUtil(timezone, intraday_reporting);
721
- }
722
- /**
723
- * Gets start and end timestamps for a given market time period
724
- * @param {MarketTimeParams} [params] - The market time parameters
725
- * @returns {PeriodDates} The start and end timestamps
696
+ * Simple functional API that uses the services above
726
697
  */
727
- function getStartAndEndTimestamps(params = {}) {
728
- const util = createMarketTimeUtil(params.timezone, params.intraday_reporting);
729
- const effectiveParams = {
730
- ...params,
731
- end: params.referenceDate || params.end || new Date(),
732
- };
733
- return util.getMarketTimePeriod(effectiveParams);
734
- }
698
+ // Create service instances
699
+ const marketCalendar = new MarketCalendar();
700
+ const marketTimeCalculator = new MarketTimeCalculator();
701
+ const periodCalculator = new PeriodCalculator();
702
+ const marketStatusService = new MarketStatusService();
703
+ const timeFormatter = new TimeFormatter();
735
704
  /**
736
- * Gets the market open/close times for a given date
737
- * @param {Object} [options] - Options object
738
- * @param {Date} [options.date] - The date to check (defaults to current date)
739
- * @returns {MarketOpenCloseResult} The market open/close times
705
+ * Get market open/close times for a given date
740
706
  */
741
707
  function getMarketOpenClose(options = {}) {
742
- const marketTimeUtil = new MarketTimeUtil();
743
- return marketTimeUtil.getMarketOpenClose(options);
708
+ const { date = new Date() } = options;
709
+ return marketTimeCalculator.getMarketTimes(date);
744
710
  }
745
711
  /**
746
- * Gets the start and end dates for a given market time period
747
- * @param {MarketTimeParams} [params] - The market time parameters
748
- * @returns {Object} The start and end dates
749
- * @property {Date} start - The start date
750
- * @property {Date} end - The end date
712
+ * Get start and end dates for a given market time period
751
713
  */
752
714
  function getStartAndEndDates(params = {}) {
753
- const util = createMarketTimeUtil(params.timezone, params.intraday_reporting);
754
- const effectiveParams = {
755
- ...params,
756
- end: params.referenceDate || params.end || new Date(),
757
- };
758
- const { start, end } = util.getMarketTimePeriod(effectiveParams);
759
- // Ensure the returned values are Dates
715
+ const { start, end } = periodCalculator.getMarketTimePeriod(params);
760
716
  return {
761
717
  start: new Date(start),
762
718
  end: new Date(end),
763
719
  };
764
720
  }
765
721
  /**
766
- * Gets the last trading date in YYYY-MM-DD format
767
- * @returns {string} The last trading date in YYYY-MM-DD format
768
- */
769
- function getLastTradingDateYYYYMMDD() {
770
- const util = new MarketTimeUtil();
771
- const lastTradingDate = util.getLastTradingDate();
772
- return format(lastTradingDate, 'yyyy-MM-dd');
773
- }
774
- /**
775
- * Gets the last full trading date
776
- * @param {Date} [currentDate] - The current date (defaults to now)
777
- * @returns {Object} The last full trading date
778
- * @property {Date} date - The date object
779
- * @property {string} YYYYMMDD - The date in YYYY-MM-DD format
722
+ * Get the last full trading date
780
723
  */
781
724
  function getLastFullTradingDate(currentDate = new Date()) {
782
- const util = new MarketTimeUtil();
783
- const date = util.getLastFullTradingDate(currentDate);
784
- // Format the date in NY timezone to ensure consistency
725
+ const date = marketTimeCalculator.getLastFullTradingDate(currentDate);
785
726
  return {
786
727
  date,
787
- YYYYMMDD: formatInTimeZone(date, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd'),
728
+ YYYYMMDD: timeFormatter.getTradingDate(date),
788
729
  };
789
730
  }
790
731
  /**
791
- * Gets the next market day from a reference date
792
- * @param {Object} [options] - Options object
793
- * @param {Date} [options.referenceDate] - The reference date (defaults to current date)
794
- * @returns {Object} The next market day information
795
- * @property {Date} date - The date object (start of day in NY time)
796
- * @property {string} yyyymmdd - The date in YYYY-MM-DD format
797
- * @property {string} dateISOString - Full ISO date string
732
+ * Get the next market day
798
733
  */
799
734
  function getNextMarketDay({ referenceDate } = {}) {
800
- const util = new MarketTimeUtil();
801
735
  const startDate = referenceDate || new Date();
802
- const nextDate = util.getNextMarketDay(startDate);
736
+ const nextDate = marketCalendar.getNextMarketDay(startDate);
803
737
  // Convert to start of day in NY time
804
- const startOfDayNY = startOfDay(toZonedTime(nextDate, MARKET_TIMES.TIMEZONE));
805
- const dateInET = fromZonedTime(startOfDayNY, MARKET_TIMES.TIMEZONE);
738
+ const startOfDayNY = startOfDay(toZonedTime(nextDate, MARKET_CONFIG.TIMEZONE));
739
+ const dateInET = fromZonedTime(startOfDayNY, MARKET_CONFIG.TIMEZONE);
806
740
  return {
807
741
  date: dateInET,
808
- yyyymmdd: formatInTimeZone(dateInET, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd'),
742
+ yyyymmdd: timeFormatter.getTradingDate(dateInET),
809
743
  dateISOString: dateInET.toISOString(),
810
744
  };
811
745
  }
812
746
  /**
813
- * Gets the current time in Eastern Time
814
- * @returns {Date} The current time in Eastern Time
747
+ * Get trading date in YYYY-MM-DD format
815
748
  */
816
- const currentTimeET = () => {
817
- return toZonedTime(new Date(), MARKET_TIMES.TIMEZONE);
818
- };
749
+ function getTradingDate(time) {
750
+ return timeFormatter.getTradingDate(time);
751
+ }
819
752
  /**
820
- * Gets a date in New York timezone, rezoned using date-fns-tz
821
- * @param {number|string|Date} time - The time to convert
822
- * @returns {Date} The date in New York timezone
753
+ * Get New York timezone offset
823
754
  */
824
- function getDateInNY(time) {
825
- let date;
826
- if (typeof time === 'number' || typeof time === 'string' || time instanceof Date) {
827
- // Assuming Unix timestamp in epoch milliseconds, string date, or Date object
828
- date = new Date(time);
829
- }
830
- else {
831
- // Assuming object with year, month, and day
832
- date = new Date(time.year, time.month - 1, time.day);
833
- }
834
- return toZonedTime(date, 'America/New_York');
755
+ function getNYTimeZone(date) {
756
+ return timeFormatter.getNYTimeZone(date);
835
757
  }
836
758
  /**
837
- * Gets the trading date in YYYY-MM-DD format for New York timezone, for grouping of data
838
- * @param {string|number|Date} time - The time to convert (string, unix timestamp in ms, or Date object)
839
- * @returns {string} The trading date in YYYY-MM-DD format
759
+ * Get current market status
840
760
  */
841
- function getTradingDate(time) {
842
- let date;
843
- if (typeof time === 'number') {
844
- // Assuming Unix timestamp in milliseconds
845
- date = new Date(time);
846
- }
847
- else if (typeof time === 'string') {
848
- date = new Date(time);
849
- }
850
- else {
851
- date = time;
852
- }
853
- // Convert to NY timezone and format as YYYY-MM-DD
854
- return formatInTimeZone(date, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd');
761
+ function getMarketStatus(options = {}) {
762
+ const { date = new Date() } = options;
763
+ return marketStatusService.getMarketStatus(date);
855
764
  }
856
765
  /**
857
- * Returns the New York timezone offset based on whether daylight savings is active
858
- * @param dateString - The date string to check
859
- * @returns "-04:00" during daylight savings (EDT) or "-05:00" during standard time (EST)
766
+ * Check if a date is a market day
860
767
  */
861
- const getNYTimeZone = (date) => {
862
- if (!date) {
863
- date = new Date();
864
- }
865
- const dtf = new Intl.DateTimeFormat('en-US', {
866
- timeZone: 'America/New_York',
867
- timeZoneName: 'shortOffset',
868
- });
869
- const parts = dtf.formatToParts(date);
870
- const tz = parts.find((p) => p.type === 'timeZoneName')?.value;
871
- // tz will be "GMT-5" or "GMT-4"
872
- if (!tz) {
873
- throw new Error('Could not determine New York offset');
874
- }
875
- // extract the -4 or -5 from the string
876
- const shortOffset = tz.replace('GMT', '');
877
- // return the correct offset
878
- if (shortOffset === '-4') {
879
- console.log(`New York is on EDT; using -04:00. Full date: ${date.toLocaleString('en-US', {
880
- timeZone: 'America/New_York',
881
- })}, time zone part: ${tz}`);
882
- return '-04:00';
883
- }
884
- else if (shortOffset === '-5') {
885
- console.log(`New York is on EST; using -05:00. Full date: ${date.toLocaleString('en-US', {
886
- timeZone: 'America/New_York',
887
- })}, time zone part: ${tz}`);
888
- return '-05:00';
889
- }
890
- else {
891
- throw new Error('Could not determine New York offset');
892
- }
893
- };
768
+ function isMarketDay(date) {
769
+ return marketCalendar.isMarketDay(date);
770
+ }
894
771
  /**
895
- * Gets the current market status
896
- * @param {Object} [options] - Options object
897
- * @param {Date} [options.date] - The date to check (defaults to current date)
898
- * @returns {MarketStatus} The current market status
772
+ * Function to find complete trading date periods, starting at the beginning of one trading date and ending at the last.
773
+ * By default, it gets the last trading date, returning the beginning and end. But we can also a) define the end date
774
+ * 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),
775
+ * 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).
776
+ * @param options.endDate - The end date to use, defaults to today
777
+ * @param options.days - The number of days to go back, defaults to 1
778
+ * @returns The start and end dates with proper market open/close times
899
779
  */
900
- function getMarketStatus(options = {}) {
901
- const util = new MarketTimeUtil();
902
- const now = options.date || new Date();
903
- const nyTime = toZonedTime(now, MARKET_TIMES.TIMEZONE);
904
- const isEarlyCloseDay = util.isEarlyCloseDay(nyTime);
905
- const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
906
- const extendedStartMinutes = MARKET_TIMES.EXTENDED.START.MINUTES;
907
- const marketStartMinutes = MARKET_TIMES.REGULAR.START.MINUTES;
908
- MARKET_TIMES.EARLY_MORNING.END.MINUTES;
909
- const marketRegularCloseMinutes = isEarlyCloseDay
910
- ? MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTES
911
- : MARKET_TIMES.REGULAR.END.MINUTES;
912
- const extendedEndMinutes = isEarlyCloseDay
913
- ? MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTES
914
- : MARKET_TIMES.EXTENDED.END.MINUTES;
915
- let status;
916
- let nextStatus;
917
- let nextStatusTime;
918
- let marketPeriod;
919
- const nextMarketDay = util.getNextMarketDay(nyTime);
920
- // Determine current status and market period
921
- if (!util.isMarketDay(nyTime)) {
922
- // Not a market day! market is closed
923
- marketPeriod = 'closed';
924
- status = 'closed';
925
- nextStatus = 'extended hours';
926
- // Find next market day and set to extended hours start time
927
- nextStatusTime = set(nextMarketDay, {
928
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
929
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
930
- });
931
- } // check if the market isn't in extended hours yet
932
- else if (timeInMinutes >= 0 && timeInMinutes < extendedStartMinutes) {
933
- marketPeriod = 'closed';
934
- status = 'closed';
935
- nextStatus = 'extended hours';
936
- nextStatusTime = set(nyTime, {
937
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
938
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
939
- });
940
- // check if we're in pre-market hours
941
- }
942
- else if (timeInMinutes >= extendedStartMinutes && timeInMinutes < marketStartMinutes) {
943
- marketPeriod = 'preMarket';
944
- status = 'extended hours';
945
- nextStatus = 'open';
946
- nextStatusTime = set(nyTime, {
947
- hours: MARKET_TIMES.REGULAR.START.HOUR,
948
- minutes: MARKET_TIMES.REGULAR.START.MINUTE,
949
- });
950
- // check if market is open
951
- }
952
- else if (timeInMinutes >= marketStartMinutes && timeInMinutes < marketRegularCloseMinutes) {
953
- status = 'open';
954
- nextStatus = 'extended hours';
955
- // market is open, but just check the marketPeriod - could be earlyMarket or regularMarket
956
- marketPeriod = timeInMinutes < MARKET_TIMES.EARLY_MORNING.END.MINUTES ? 'earlyMarket' : 'regularMarket';
957
- nextStatusTime = isEarlyCloseDay
958
- ? set(nyTime, {
959
- hours: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.HOUR,
960
- minutes: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTE,
961
- })
962
- : set(nyTime, {
963
- hours: MARKET_TIMES.REGULAR.END.HOUR,
964
- minutes: MARKET_TIMES.REGULAR.END.MINUTE,
965
- });
966
- // check if it's after-market extended hours
967
- }
968
- else if (timeInMinutes >= marketRegularCloseMinutes && timeInMinutes < extendedEndMinutes) {
969
- status = 'extended hours';
970
- nextStatus = 'closed';
971
- marketPeriod = 'afterMarket';
972
- nextStatusTime = isEarlyCloseDay
973
- ? set(nyTime, {
974
- hours: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.HOUR,
975
- minutes: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTE,
976
- })
977
- : set(nyTime, {
978
- hours: MARKET_TIMES.EXTENDED.END.HOUR,
979
- minutes: MARKET_TIMES.EXTENDED.END.MINUTE,
980
- });
981
- // otherwise, the market is closed
780
+ function getTradingStartAndEndDates(options = {}) {
781
+ const { endDate = new Date(), days = 1 } = options;
782
+ // Get the last full trading date for the end date
783
+ const endTradingDate = getLastFullTradingDate(endDate).date;
784
+ // Get the market close time for the end date (4:00 PM ET or 1:00 PM on short days)
785
+ const endMarketClose = getMarketOpenClose({ date: endTradingDate }).close;
786
+ let startDate;
787
+ if (days <= 1) {
788
+ // For 1 day, start is market open of the same trading day as end
789
+ startDate = getMarketOpenClose({ date: endTradingDate }).open;
982
790
  }
983
791
  else {
984
- status = 'closed';
985
- nextStatus = 'extended hours';
986
- marketPeriod = 'closed';
987
- nextStatusTime = set(nextMarketDay, {
988
- hours: MARKET_TIMES.EXTENDED.START.HOUR,
989
- minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
990
- });
792
+ // For multiple days, go back the specified number of trading days
793
+ let currentDate = new Date(endTradingDate);
794
+ let tradingDaysBack = 0;
795
+ // Count back trading days
796
+ while (tradingDaysBack < days - 1) {
797
+ currentDate = sub(currentDate, { days: 1 });
798
+ if (isMarketDay(currentDate)) {
799
+ tradingDaysBack++;
800
+ }
801
+ }
802
+ // Get the market open time for the start date
803
+ startDate = getMarketOpenClose({ date: currentDate }).open;
991
804
  }
992
- const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
993
- return {
994
- time: now,
995
- timeString: format(nyTime, dateFormat),
996
- status,
997
- nextStatus,
998
- marketPeriod,
999
- nextStatusTime: fromZonedTime(nextStatusTime, MARKET_TIMES.TIMEZONE),
1000
- nextStatusTimeDifference: differenceInMilliseconds(nextStatusTime, nyTime),
1001
- nextStatusTimeString: format(nextStatusTime, dateFormat),
1002
- };
805
+ return { startDate, endDate: endMarketClose };
1003
806
  }
1004
807
 
1005
808
  // format-tools.ts
@@ -2429,7 +2232,7 @@ const safeJSON = (text) => {
2429
2232
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2430
2233
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2431
2234
 
2432
- const VERSION = '5.8.2'; // x-release-please-version
2235
+ const VERSION = '5.8.3'; // x-release-please-version
2433
2236
 
2434
2237
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2435
2238
  const isRunningInBrowser = () => {
@@ -3181,13 +2984,95 @@ function findDoubleNewlineIndex(buffer) {
3181
2984
  return -1;
3182
2985
  }
3183
2986
 
2987
+ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2988
+ const levelNumbers = {
2989
+ off: 0,
2990
+ error: 200,
2991
+ warn: 300,
2992
+ info: 400,
2993
+ debug: 500,
2994
+ };
2995
+ const parseLogLevel = (maybeLevel, sourceName, client) => {
2996
+ if (!maybeLevel) {
2997
+ return undefined;
2998
+ }
2999
+ if (hasOwn(levelNumbers, maybeLevel)) {
3000
+ return maybeLevel;
3001
+ }
3002
+ loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
3003
+ return undefined;
3004
+ };
3005
+ function noop() { }
3006
+ function makeLogFn(fnLevel, logger, logLevel) {
3007
+ if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
3008
+ return noop;
3009
+ }
3010
+ else {
3011
+ // Don't wrap logger functions, we want the stacktrace intact!
3012
+ return logger[fnLevel].bind(logger);
3013
+ }
3014
+ }
3015
+ const noopLogger = {
3016
+ error: noop,
3017
+ warn: noop,
3018
+ info: noop,
3019
+ debug: noop,
3020
+ };
3021
+ let cachedLoggers = /* @__PURE__ */ new WeakMap();
3022
+ function loggerFor(client) {
3023
+ const logger = client.logger;
3024
+ const logLevel = client.logLevel ?? 'off';
3025
+ if (!logger) {
3026
+ return noopLogger;
3027
+ }
3028
+ const cachedLogger = cachedLoggers.get(logger);
3029
+ if (cachedLogger && cachedLogger[0] === logLevel) {
3030
+ return cachedLogger[1];
3031
+ }
3032
+ const levelLogger = {
3033
+ error: makeLogFn('error', logger, logLevel),
3034
+ warn: makeLogFn('warn', logger, logLevel),
3035
+ info: makeLogFn('info', logger, logLevel),
3036
+ debug: makeLogFn('debug', logger, logLevel),
3037
+ };
3038
+ cachedLoggers.set(logger, [logLevel, levelLogger]);
3039
+ return levelLogger;
3040
+ }
3041
+ const formatRequestDetails = (details) => {
3042
+ if (details.options) {
3043
+ details.options = { ...details.options };
3044
+ delete details.options['headers']; // redundant + leaks internals
3045
+ }
3046
+ if (details.headers) {
3047
+ details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
3048
+ name,
3049
+ (name.toLowerCase() === 'authorization' ||
3050
+ name.toLowerCase() === 'cookie' ||
3051
+ name.toLowerCase() === 'set-cookie') ?
3052
+ '***'
3053
+ : value,
3054
+ ]));
3055
+ }
3056
+ if ('retryOfRequestLogID' in details) {
3057
+ if (details.retryOfRequestLogID) {
3058
+ details.retryOf = details.retryOfRequestLogID;
3059
+ }
3060
+ delete details.retryOfRequestLogID;
3061
+ }
3062
+ return details;
3063
+ };
3064
+
3065
+ var _Stream_client;
3184
3066
  class Stream {
3185
- constructor(iterator, controller) {
3067
+ constructor(iterator, controller, client) {
3186
3068
  this.iterator = iterator;
3069
+ _Stream_client.set(this, void 0);
3187
3070
  this.controller = controller;
3071
+ __classPrivateFieldSet(this, _Stream_client, client);
3188
3072
  }
3189
- static fromSSEResponse(response, controller) {
3073
+ static fromSSEResponse(response, controller, client) {
3190
3074
  let consumed = false;
3075
+ const logger = client ? loggerFor(client) : console;
3191
3076
  async function* iterator() {
3192
3077
  if (consumed) {
3193
3078
  throw new OpenAIError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
@@ -3210,8 +3095,8 @@ class Stream {
3210
3095
  data = JSON.parse(sse.data);
3211
3096
  }
3212
3097
  catch (e) {
3213
- console.error(`Could not parse message into JSON:`, sse.data);
3214
- console.error(`From chunk:`, sse.raw);
3098
+ logger.error(`Could not parse message into JSON:`, sse.data);
3099
+ logger.error(`From chunk:`, sse.raw);
3215
3100
  throw e;
3216
3101
  }
3217
3102
  if (data && data.error) {
@@ -3250,13 +3135,13 @@ class Stream {
3250
3135
  controller.abort();
3251
3136
  }
3252
3137
  }
3253
- return new Stream(iterator, controller);
3138
+ return new Stream(iterator, controller, client);
3254
3139
  }
3255
3140
  /**
3256
3141
  * Generates a Stream from a newline-separated ReadableStream
3257
3142
  * where each item is a JSON value.
3258
3143
  */
3259
- static fromReadableStream(readableStream, controller) {
3144
+ static fromReadableStream(readableStream, controller, client) {
3260
3145
  let consumed = false;
3261
3146
  async function* iterLines() {
3262
3147
  const lineDecoder = new LineDecoder();
@@ -3297,9 +3182,9 @@ class Stream {
3297
3182
  controller.abort();
3298
3183
  }
3299
3184
  }
3300
- return new Stream(iterator, controller);
3185
+ return new Stream(iterator, controller, client);
3301
3186
  }
3302
- [Symbol.asyncIterator]() {
3187
+ [(_Stream_client = new WeakMap(), Symbol.asyncIterator)]() {
3303
3188
  return this.iterator();
3304
3189
  }
3305
3190
  /**
@@ -3323,8 +3208,8 @@ class Stream {
3323
3208
  };
3324
3209
  };
3325
3210
  return [
3326
- new Stream(() => teeIterator(left), this.controller),
3327
- new Stream(() => teeIterator(right), this.controller),
3211
+ new Stream(() => teeIterator(left), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
3212
+ new Stream(() => teeIterator(right), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
3328
3213
  ];
3329
3214
  }
3330
3215
  /**
@@ -3444,97 +3329,19 @@ class SSEDecoder {
3444
3329
  if (fieldname === 'event') {
3445
3330
  this.event = value;
3446
3331
  }
3447
- else if (fieldname === 'data') {
3448
- this.data.push(value);
3449
- }
3450
- return null;
3451
- }
3452
- }
3453
- function partition(str, delimiter) {
3454
- const index = str.indexOf(delimiter);
3455
- if (index !== -1) {
3456
- return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
3457
- }
3458
- return [str, '', ''];
3459
- }
3460
-
3461
- // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
3462
- const levelNumbers = {
3463
- off: 0,
3464
- error: 200,
3465
- warn: 300,
3466
- info: 400,
3467
- debug: 500,
3468
- };
3469
- const parseLogLevel = (maybeLevel, sourceName, client) => {
3470
- if (!maybeLevel) {
3471
- return undefined;
3472
- }
3473
- if (hasOwn(levelNumbers, maybeLevel)) {
3474
- return maybeLevel;
3475
- }
3476
- loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
3477
- return undefined;
3478
- };
3479
- function noop() { }
3480
- function makeLogFn(fnLevel, logger, logLevel) {
3481
- if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
3482
- return noop;
3483
- }
3484
- else {
3485
- // Don't wrap logger functions, we want the stacktrace intact!
3486
- return logger[fnLevel].bind(logger);
3487
- }
3488
- }
3489
- const noopLogger = {
3490
- error: noop,
3491
- warn: noop,
3492
- info: noop,
3493
- debug: noop,
3494
- };
3495
- let cachedLoggers = /** @__PURE__ */ new WeakMap();
3496
- function loggerFor(client) {
3497
- const logger = client.logger;
3498
- const logLevel = client.logLevel ?? 'off';
3499
- if (!logger) {
3500
- return noopLogger;
3501
- }
3502
- const cachedLogger = cachedLoggers.get(logger);
3503
- if (cachedLogger && cachedLogger[0] === logLevel) {
3504
- return cachedLogger[1];
3332
+ else if (fieldname === 'data') {
3333
+ this.data.push(value);
3334
+ }
3335
+ return null;
3505
3336
  }
3506
- const levelLogger = {
3507
- error: makeLogFn('error', logger, logLevel),
3508
- warn: makeLogFn('warn', logger, logLevel),
3509
- info: makeLogFn('info', logger, logLevel),
3510
- debug: makeLogFn('debug', logger, logLevel),
3511
- };
3512
- cachedLoggers.set(logger, [logLevel, levelLogger]);
3513
- return levelLogger;
3514
3337
  }
3515
- const formatRequestDetails = (details) => {
3516
- if (details.options) {
3517
- details.options = { ...details.options };
3518
- delete details.options['headers']; // redundant + leaks internals
3519
- }
3520
- if (details.headers) {
3521
- details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
3522
- name,
3523
- (name.toLowerCase() === 'authorization' ||
3524
- name.toLowerCase() === 'cookie' ||
3525
- name.toLowerCase() === 'set-cookie') ?
3526
- '***'
3527
- : value,
3528
- ]));
3529
- }
3530
- if ('retryOfRequestLogID' in details) {
3531
- if (details.retryOfRequestLogID) {
3532
- details.retryOf = details.retryOfRequestLogID;
3533
- }
3534
- delete details.retryOfRequestLogID;
3338
+ function partition(str, delimiter) {
3339
+ const index = str.indexOf(delimiter);
3340
+ if (index !== -1) {
3341
+ return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
3535
3342
  }
3536
- return details;
3537
- };
3343
+ return [str, '', ''];
3344
+ }
3538
3345
 
3539
3346
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
3540
3347
  async function defaultParseResponse(client, props) {
@@ -3545,9 +3352,9 @@ async function defaultParseResponse(client, props) {
3545
3352
  // Note: there is an invariant here that isn't represented in the type system
3546
3353
  // that if you set `stream: true` the response type must also be `Stream<T>`
3547
3354
  if (props.options.__streamClass) {
3548
- return props.options.__streamClass.fromSSEResponse(response, props.controller);
3355
+ return props.options.__streamClass.fromSSEResponse(response, props.controller, client);
3549
3356
  }
3550
- return Stream.fromSSEResponse(response, props.controller);
3357
+ return Stream.fromSSEResponse(response, props.controller, client);
3551
3358
  }
3552
3359
  // fetch refuses to read the body when the status code is 204.
3553
3360
  if (response.status === 204) {
@@ -3801,7 +3608,7 @@ const isAsyncIterable = (value) => value != null && typeof value === 'object' &&
3801
3608
  const multipartFormRequestOptions = async (opts, fetch) => {
3802
3609
  return { ...opts, body: await createForm(opts.body, fetch) };
3803
3610
  };
3804
- const supportsFormDataMap = /** @__PURE__ */ new WeakMap();
3611
+ const supportsFormDataMap = /* @__PURE__ */ new WeakMap();
3805
3612
  /**
3806
3613
  * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
3807
3614
  * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
@@ -3977,21 +3784,38 @@ class APIResource {
3977
3784
  function encodeURIPath(str) {
3978
3785
  return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
3979
3786
  }
3787
+ const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
3980
3788
  const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(statics, ...params) {
3981
3789
  // If there are no params, no processing is needed.
3982
3790
  if (statics.length === 1)
3983
3791
  return statics[0];
3984
3792
  let postPath = false;
3793
+ const invalidSegments = [];
3985
3794
  const path = statics.reduce((previousValue, currentValue, index) => {
3986
3795
  if (/[?#]/.test(currentValue)) {
3987
3796
  postPath = true;
3988
3797
  }
3989
- return (previousValue +
3990
- currentValue +
3991
- (index === params.length ? '' : (postPath ? encodeURIComponent : pathEncoder)(String(params[index]))));
3798
+ const value = params[index];
3799
+ let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
3800
+ if (index !== params.length &&
3801
+ (value == null ||
3802
+ (typeof value === 'object' &&
3803
+ // handle values from other realms
3804
+ value.toString ===
3805
+ Object.getPrototypeOf(Object.getPrototypeOf(value.hasOwnProperty ?? EMPTY) ?? EMPTY)
3806
+ ?.toString))) {
3807
+ encoded = value + '';
3808
+ invalidSegments.push({
3809
+ start: previousValue.length + currentValue.length,
3810
+ length: encoded.length,
3811
+ error: `Value of type ${Object.prototype.toString
3812
+ .call(value)
3813
+ .slice(8, -1)} is not a valid path parameter`,
3814
+ });
3815
+ }
3816
+ return previousValue + currentValue + (index === params.length ? '' : encoded);
3992
3817
  }, '');
3993
3818
  const pathOnly = path.split(/[?#]/, 1)[0];
3994
- const invalidSegments = [];
3995
3819
  const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
3996
3820
  let match;
3997
3821
  // Find all invalid segments
@@ -3999,8 +3823,10 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
3999
3823
  invalidSegments.push({
4000
3824
  start: match.index,
4001
3825
  length: match[0].length,
3826
+ error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
4002
3827
  });
4003
3828
  }
3829
+ invalidSegments.sort((a, b) => a.start - b.start);
4004
3830
  if (invalidSegments.length > 0) {
4005
3831
  let lastEnd = 0;
4006
3832
  const underline = invalidSegments.reduce((acc, segment) => {
@@ -4009,7 +3835,9 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
4009
3835
  lastEnd = segment.start + segment.length;
4010
3836
  return acc + spaces + arrows;
4011
3837
  }, '');
4012
- throw new OpenAIError(`Path parameters result in path with invalid segments:\n${path}\n${underline}`);
3838
+ throw new OpenAIError(`Path parameters result in path with invalid segments:\n${invalidSegments
3839
+ .map((e) => e.error)
3840
+ .join('\n')}\n${path}\n${underline}`);
4013
3841
  }
4014
3842
  return path;
4015
3843
  };
@@ -12053,7 +11881,7 @@ function requireSender () {
12053
11881
  hasRequiredSender = 1;
12054
11882
 
12055
11883
  const { Duplex } = require$$0$2;
12056
- const { randomFillSync } = require$$1;
11884
+ const { randomFillSync } = require$$3;
12057
11885
 
12058
11886
  const PerMessageDeflate = requirePermessageDeflate();
12059
11887
  const { EMPTY_BUFFER, kWebSocket, NOOP } = requireConstants();
@@ -13174,11 +13002,11 @@ function requireWebsocket () {
13174
13002
  hasRequiredWebsocket = 1;
13175
13003
 
13176
13004
  const EventEmitter = require$$0$3;
13177
- const https = require$$1$1;
13005
+ const https = require$$1;
13178
13006
  const http = require$$2;
13179
- const net = require$$3;
13180
- const tls = require$$4;
13181
- const { randomBytes, createHash } = require$$1;
13007
+ const net = require$$3$1;
13008
+ const tls = require$$4$1;
13009
+ const { randomBytes, createHash } = require$$3;
13182
13010
  const { Duplex, Readable } = require$$0$2;
13183
13011
  const { URL } = require$$7;
13184
13012
 
@@ -14821,7 +14649,7 @@ function requireWebsocketServer () {
14821
14649
  const EventEmitter = require$$0$3;
14822
14650
  const http = require$$2;
14823
14651
  const { Duplex } = require$$0$2;
14824
- const { createHash } = require$$1;
14652
+ const { createHash } = require$$3;
14825
14653
 
14826
14654
  const extension = requireExtension();
14827
14655
  const PerMessageDeflate = requirePermessageDeflate();
@@ -15369,35 +15197,522 @@ function requireWebsocketServer () {
15369
15197
 
15370
15198
  requireWebsocketServer();
15371
15199
 
15372
- /**
15373
- * Logs a message to the console.
15374
- * @param message The message to log.
15375
- * @param options Optional options.
15376
- * @param options.source The source of the message.
15377
- * @param options.type The type of message to log.
15378
- * @param options.symbol The trading symbol associated with this log.
15379
- * @param options.account The account associated with this log.
15380
- */
15381
- function log$1(message, options = { source: 'App', type: 'info' }) {
15382
- // Format the timestamp
15383
- const date = new Date();
15384
- const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
15385
- const account = options?.account;
15386
- const symbol = options?.symbol;
15387
- // Build the log message
15388
- const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
15389
- // Use appropriate console method based on type
15390
- if (options?.type === 'error') {
15391
- console.error(logMessage);
15392
- }
15393
- else if (options?.type === 'warn') {
15394
- console.warn(logMessage);
15395
- }
15396
- else {
15397
- console.log(logMessage);
15398
- }
15200
+ var config = {};
15201
+
15202
+ var main = {exports: {}};
15203
+
15204
+ var version = "17.1.0";
15205
+ var require$$4 = {
15206
+ version: version};
15207
+
15208
+ var hasRequiredMain;
15209
+
15210
+ function requireMain () {
15211
+ if (hasRequiredMain) return main.exports;
15212
+ hasRequiredMain = 1;
15213
+ const fs = require$$0$4;
15214
+ const path = require$$1$1;
15215
+ const os = require$$2$1;
15216
+ const crypto = require$$3;
15217
+ const packageJson = require$$4;
15218
+
15219
+ const version = packageJson.version;
15220
+
15221
+ // Array of tips to display randomly
15222
+ const TIPS = [
15223
+ '🔐 encrypt with dotenvx: https://dotenvx.com',
15224
+ '🔐 prevent committing .env to code: https://dotenvx.com/precommit',
15225
+ '🔐 prevent building .env in docker: https://dotenvx.com/prebuild',
15226
+ '🛠️ run anywhere with `dotenvx run -- yourcommand`',
15227
+ '⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }',
15228
+ '⚙️ enable debug logging with { debug: true }',
15229
+ '⚙️ override existing env vars with { override: true }',
15230
+ '⚙️ suppress all logs with { quiet: true }',
15231
+ '⚙️ write to custom object with { processEnv: myObject }',
15232
+ '⚙️ load multiple .env files with { path: [\'.env.local\', \'.env\'] }'
15233
+ ];
15234
+
15235
+ // Get a random tip from the tips array
15236
+ function _getRandomTip () {
15237
+ return TIPS[Math.floor(Math.random() * TIPS.length)]
15238
+ }
15239
+
15240
+ function supportsAnsi () {
15241
+ return process.stdout.isTTY // && process.env.TERM !== 'dumb'
15242
+ }
15243
+
15244
+ function dim (text) {
15245
+ return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text
15246
+ }
15247
+
15248
+ const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
15249
+
15250
+ // Parse src into an Object
15251
+ function parse (src) {
15252
+ const obj = {};
15253
+
15254
+ // Convert buffer to string
15255
+ let lines = src.toString();
15256
+
15257
+ // Convert line breaks to same format
15258
+ lines = lines.replace(/\r\n?/mg, '\n');
15259
+
15260
+ let match;
15261
+ while ((match = LINE.exec(lines)) != null) {
15262
+ const key = match[1];
15263
+
15264
+ // Default undefined or null to empty string
15265
+ let value = (match[2] || '');
15266
+
15267
+ // Remove whitespace
15268
+ value = value.trim();
15269
+
15270
+ // Check if double quoted
15271
+ const maybeQuote = value[0];
15272
+
15273
+ // Remove surrounding quotes
15274
+ value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2');
15275
+
15276
+ // Expand newlines if double quoted
15277
+ if (maybeQuote === '"') {
15278
+ value = value.replace(/\\n/g, '\n');
15279
+ value = value.replace(/\\r/g, '\r');
15280
+ }
15281
+
15282
+ // Add to object
15283
+ obj[key] = value;
15284
+ }
15285
+
15286
+ return obj
15287
+ }
15288
+
15289
+ function _parseVault (options) {
15290
+ options = options || {};
15291
+
15292
+ const vaultPath = _vaultPath(options);
15293
+ options.path = vaultPath; // parse .env.vault
15294
+ const result = DotenvModule.configDotenv(options);
15295
+ if (!result.parsed) {
15296
+ const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
15297
+ err.code = 'MISSING_DATA';
15298
+ throw err
15299
+ }
15300
+
15301
+ // handle scenario for comma separated keys - for use with key rotation
15302
+ // example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
15303
+ const keys = _dotenvKey(options).split(',');
15304
+ const length = keys.length;
15305
+
15306
+ let decrypted;
15307
+ for (let i = 0; i < length; i++) {
15308
+ try {
15309
+ // Get full key
15310
+ const key = keys[i].trim();
15311
+
15312
+ // Get instructions for decrypt
15313
+ const attrs = _instructions(result, key);
15314
+
15315
+ // Decrypt
15316
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
15317
+
15318
+ break
15319
+ } catch (error) {
15320
+ // last key
15321
+ if (i + 1 >= length) {
15322
+ throw error
15323
+ }
15324
+ // try next key
15325
+ }
15326
+ }
15327
+
15328
+ // Parse decrypted .env string
15329
+ return DotenvModule.parse(decrypted)
15330
+ }
15331
+
15332
+ function _warn (message) {
15333
+ console.error(`[dotenv@${version}][WARN] ${message}`);
15334
+ }
15335
+
15336
+ function _debug (message) {
15337
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
15338
+ }
15339
+
15340
+ function _log (message) {
15341
+ console.log(`[dotenv@${version}] ${message}`);
15342
+ }
15343
+
15344
+ function _dotenvKey (options) {
15345
+ // prioritize developer directly setting options.DOTENV_KEY
15346
+ if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
15347
+ return options.DOTENV_KEY
15348
+ }
15349
+
15350
+ // secondary infra already contains a DOTENV_KEY environment variable
15351
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
15352
+ return process.env.DOTENV_KEY
15353
+ }
15354
+
15355
+ // fallback to empty string
15356
+ return ''
15357
+ }
15358
+
15359
+ function _instructions (result, dotenvKey) {
15360
+ // Parse DOTENV_KEY. Format is a URI
15361
+ let uri;
15362
+ try {
15363
+ uri = new URL(dotenvKey);
15364
+ } catch (error) {
15365
+ if (error.code === 'ERR_INVALID_URL') {
15366
+ const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development');
15367
+ err.code = 'INVALID_DOTENV_KEY';
15368
+ throw err
15369
+ }
15370
+
15371
+ throw error
15372
+ }
15373
+
15374
+ // Get decrypt key
15375
+ const key = uri.password;
15376
+ if (!key) {
15377
+ const err = new Error('INVALID_DOTENV_KEY: Missing key part');
15378
+ err.code = 'INVALID_DOTENV_KEY';
15379
+ throw err
15380
+ }
15381
+
15382
+ // Get environment
15383
+ const environment = uri.searchParams.get('environment');
15384
+ if (!environment) {
15385
+ const err = new Error('INVALID_DOTENV_KEY: Missing environment part');
15386
+ err.code = 'INVALID_DOTENV_KEY';
15387
+ throw err
15388
+ }
15389
+
15390
+ // Get ciphertext payload
15391
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
15392
+ const ciphertext = result.parsed[environmentKey]; // DOTENV_VAULT_PRODUCTION
15393
+ if (!ciphertext) {
15394
+ const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
15395
+ err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT';
15396
+ throw err
15397
+ }
15398
+
15399
+ return { ciphertext, key }
15400
+ }
15401
+
15402
+ function _vaultPath (options) {
15403
+ let possibleVaultPath = null;
15404
+
15405
+ if (options && options.path && options.path.length > 0) {
15406
+ if (Array.isArray(options.path)) {
15407
+ for (const filepath of options.path) {
15408
+ if (fs.existsSync(filepath)) {
15409
+ possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`;
15410
+ }
15411
+ }
15412
+ } else {
15413
+ possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`;
15414
+ }
15415
+ } else {
15416
+ possibleVaultPath = path.resolve(process.cwd(), '.env.vault');
15417
+ }
15418
+
15419
+ if (fs.existsSync(possibleVaultPath)) {
15420
+ return possibleVaultPath
15421
+ }
15422
+
15423
+ return null
15424
+ }
15425
+
15426
+ function _resolveHome (envPath) {
15427
+ return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
15428
+ }
15429
+
15430
+ function _configVault (options) {
15431
+ const debug = Boolean(options && options.debug);
15432
+ const quiet = Boolean(options && options.quiet);
15433
+
15434
+ if (debug || !quiet) {
15435
+ _log('Loading env from encrypted .env.vault');
15436
+ }
15437
+
15438
+ const parsed = DotenvModule._parseVault(options);
15439
+
15440
+ let processEnv = process.env;
15441
+ if (options && options.processEnv != null) {
15442
+ processEnv = options.processEnv;
15443
+ }
15444
+
15445
+ DotenvModule.populate(processEnv, parsed, options);
15446
+
15447
+ return { parsed }
15448
+ }
15449
+
15450
+ function configDotenv (options) {
15451
+ const dotenvPath = path.resolve(process.cwd(), '.env');
15452
+ let encoding = 'utf8';
15453
+ const debug = Boolean(options && options.debug);
15454
+ const quiet = Boolean(options && options.quiet);
15455
+
15456
+ if (options && options.encoding) {
15457
+ encoding = options.encoding;
15458
+ } else {
15459
+ if (debug) {
15460
+ _debug('No encoding is specified. UTF-8 is used by default');
15461
+ }
15462
+ }
15463
+
15464
+ let optionPaths = [dotenvPath]; // default, look for .env
15465
+ if (options && options.path) {
15466
+ if (!Array.isArray(options.path)) {
15467
+ optionPaths = [_resolveHome(options.path)];
15468
+ } else {
15469
+ optionPaths = []; // reset default
15470
+ for (const filepath of options.path) {
15471
+ optionPaths.push(_resolveHome(filepath));
15472
+ }
15473
+ }
15474
+ }
15475
+
15476
+ // Build the parsed data in a temporary object (because we need to return it). Once we have the final
15477
+ // parsed data, we will combine it with process.env (or options.processEnv if provided).
15478
+ let lastError;
15479
+ const parsedAll = {};
15480
+ for (const path of optionPaths) {
15481
+ try {
15482
+ // Specifying an encoding returns a string instead of a buffer
15483
+ const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }));
15484
+
15485
+ DotenvModule.populate(parsedAll, parsed, options);
15486
+ } catch (e) {
15487
+ if (debug) {
15488
+ _debug(`Failed to load ${path} ${e.message}`);
15489
+ }
15490
+ lastError = e;
15491
+ }
15492
+ }
15493
+
15494
+ let processEnv = process.env;
15495
+ if (options && options.processEnv != null) {
15496
+ processEnv = options.processEnv;
15497
+ }
15498
+
15499
+ const populated = DotenvModule.populate(processEnv, parsedAll, options);
15500
+
15501
+ if (debug || !quiet) {
15502
+ const keysCount = Object.keys(populated).length;
15503
+ const shortPaths = [];
15504
+ for (const filePath of optionPaths) {
15505
+ try {
15506
+ const relative = path.relative(process.cwd(), filePath);
15507
+ shortPaths.push(relative);
15508
+ } catch (e) {
15509
+ if (debug) {
15510
+ _debug(`Failed to load ${filePath} ${e.message}`);
15511
+ }
15512
+ lastError = e;
15513
+ }
15514
+ }
15515
+
15516
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`(tip: ${_getRandomTip()})`)}`);
15517
+ }
15518
+
15519
+ if (lastError) {
15520
+ return { parsed: parsedAll, error: lastError }
15521
+ } else {
15522
+ return { parsed: parsedAll }
15523
+ }
15524
+ }
15525
+
15526
+ // Populates process.env from .env file
15527
+ function config (options) {
15528
+ // fallback to original dotenv if DOTENV_KEY is not set
15529
+ if (_dotenvKey(options).length === 0) {
15530
+ return DotenvModule.configDotenv(options)
15531
+ }
15532
+
15533
+ const vaultPath = _vaultPath(options);
15534
+
15535
+ // dotenvKey exists but .env.vault file does not exist
15536
+ if (!vaultPath) {
15537
+ _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
15538
+
15539
+ return DotenvModule.configDotenv(options)
15540
+ }
15541
+
15542
+ return DotenvModule._configVault(options)
15543
+ }
15544
+
15545
+ function decrypt (encrypted, keyStr) {
15546
+ const key = Buffer.from(keyStr.slice(-64), 'hex');
15547
+ let ciphertext = Buffer.from(encrypted, 'base64');
15548
+
15549
+ const nonce = ciphertext.subarray(0, 12);
15550
+ const authTag = ciphertext.subarray(-16);
15551
+ ciphertext = ciphertext.subarray(12, -16);
15552
+
15553
+ try {
15554
+ const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce);
15555
+ aesgcm.setAuthTag(authTag);
15556
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
15557
+ } catch (error) {
15558
+ const isRange = error instanceof RangeError;
15559
+ const invalidKeyLength = error.message === 'Invalid key length';
15560
+ const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data';
15561
+
15562
+ if (isRange || invalidKeyLength) {
15563
+ const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)');
15564
+ err.code = 'INVALID_DOTENV_KEY';
15565
+ throw err
15566
+ } else if (decryptionFailed) {
15567
+ const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY');
15568
+ err.code = 'DECRYPTION_FAILED';
15569
+ throw err
15570
+ } else {
15571
+ throw error
15572
+ }
15573
+ }
15574
+ }
15575
+
15576
+ // Populate process.env with parsed values
15577
+ function populate (processEnv, parsed, options = {}) {
15578
+ const debug = Boolean(options && options.debug);
15579
+ const override = Boolean(options && options.override);
15580
+ const populated = {};
15581
+
15582
+ if (typeof parsed !== 'object') {
15583
+ const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate');
15584
+ err.code = 'OBJECT_REQUIRED';
15585
+ throw err
15586
+ }
15587
+
15588
+ // Set process.env
15589
+ for (const key of Object.keys(parsed)) {
15590
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
15591
+ if (override === true) {
15592
+ processEnv[key] = parsed[key];
15593
+ populated[key] = parsed[key];
15594
+ }
15595
+
15596
+ if (debug) {
15597
+ if (override === true) {
15598
+ _debug(`"${key}" is already defined and WAS overwritten`);
15599
+ } else {
15600
+ _debug(`"${key}" is already defined and was NOT overwritten`);
15601
+ }
15602
+ }
15603
+ } else {
15604
+ processEnv[key] = parsed[key];
15605
+ populated[key] = parsed[key];
15606
+ }
15607
+ }
15608
+
15609
+ return populated
15610
+ }
15611
+
15612
+ const DotenvModule = {
15613
+ configDotenv,
15614
+ _configVault,
15615
+ _parseVault,
15616
+ config,
15617
+ decrypt,
15618
+ parse,
15619
+ populate
15620
+ };
15621
+
15622
+ main.exports.configDotenv = DotenvModule.configDotenv;
15623
+ main.exports._configVault = DotenvModule._configVault;
15624
+ main.exports._parseVault = DotenvModule._parseVault;
15625
+ main.exports.config = DotenvModule.config;
15626
+ main.exports.decrypt = DotenvModule.decrypt;
15627
+ main.exports.parse = DotenvModule.parse;
15628
+ main.exports.populate = DotenvModule.populate;
15629
+
15630
+ main.exports = DotenvModule;
15631
+ return main.exports;
15632
+ }
15633
+
15634
+ var envOptions;
15635
+ var hasRequiredEnvOptions;
15636
+
15637
+ function requireEnvOptions () {
15638
+ if (hasRequiredEnvOptions) return envOptions;
15639
+ hasRequiredEnvOptions = 1;
15640
+ // ../config.js accepts options via environment variables
15641
+ const options = {};
15642
+
15643
+ if (process.env.DOTENV_CONFIG_ENCODING != null) {
15644
+ options.encoding = process.env.DOTENV_CONFIG_ENCODING;
15645
+ }
15646
+
15647
+ if (process.env.DOTENV_CONFIG_PATH != null) {
15648
+ options.path = process.env.DOTENV_CONFIG_PATH;
15649
+ }
15650
+
15651
+ if (process.env.DOTENV_CONFIG_QUIET != null) {
15652
+ options.quiet = process.env.DOTENV_CONFIG_QUIET;
15653
+ }
15654
+
15655
+ if (process.env.DOTENV_CONFIG_DEBUG != null) {
15656
+ options.debug = process.env.DOTENV_CONFIG_DEBUG;
15657
+ }
15658
+
15659
+ if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
15660
+ options.override = process.env.DOTENV_CONFIG_OVERRIDE;
15661
+ }
15662
+
15663
+ if (process.env.DOTENV_CONFIG_DOTENV_KEY != null) {
15664
+ options.DOTENV_KEY = process.env.DOTENV_CONFIG_DOTENV_KEY;
15665
+ }
15666
+
15667
+ envOptions = options;
15668
+ return envOptions;
15669
+ }
15670
+
15671
+ var cliOptions;
15672
+ var hasRequiredCliOptions;
15673
+
15674
+ function requireCliOptions () {
15675
+ if (hasRequiredCliOptions) return cliOptions;
15676
+ hasRequiredCliOptions = 1;
15677
+ const re = /^dotenv_config_(encoding|path|quiet|debug|override|DOTENV_KEY)=(.+)$/;
15678
+
15679
+ cliOptions = function optionMatcher (args) {
15680
+ const options = args.reduce(function (acc, cur) {
15681
+ const matches = cur.match(re);
15682
+ if (matches) {
15683
+ acc[matches[1]] = matches[2];
15684
+ }
15685
+ return acc
15686
+ }, {});
15687
+
15688
+ if (!('quiet' in options)) {
15689
+ options.quiet = 'true';
15690
+ }
15691
+
15692
+ return options
15693
+ };
15694
+ return cliOptions;
15695
+ }
15696
+
15697
+ var hasRequiredConfig;
15698
+
15699
+ function requireConfig () {
15700
+ if (hasRequiredConfig) return config;
15701
+ hasRequiredConfig = 1;
15702
+ (function () {
15703
+ requireMain().config(
15704
+ Object.assign(
15705
+ {},
15706
+ requireEnvOptions(),
15707
+ requireCliOptions()(process.argv)
15708
+ )
15709
+ );
15710
+ })();
15711
+ return config;
15399
15712
  }
15400
15713
 
15714
+ requireConfig();
15715
+
15401
15716
  const log = (message, options = { type: 'info' }) => {
15402
15717
  log$1(message, { ...options, source: 'AlpacaMarketDataAPI' });
15403
15718
  };
@@ -15421,22 +15736,16 @@ class AlpacaMarketDataAPI extends EventEmitter {
15421
15736
  optionWs = null;
15422
15737
  stockSubscriptions = { trades: [], quotes: [], bars: [] };
15423
15738
  optionSubscriptions = { trades: [], quotes: [], bars: [] };
15424
- apiKey;
15425
- secretKey;
15426
- accountType;
15427
15739
  setMode(mode = 'production') {
15428
- if (mode === 'sandbox') {
15429
- // sandbox mode
15740
+ if (mode === 'sandbox') { // sandbox mode
15430
15741
  this.stockStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v2/sip';
15431
15742
  this.optionStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v1beta3/options';
15432
15743
  }
15433
- else if (mode === 'test') {
15434
- // test mode, can only use ticker FAKEPACA
15744
+ else if (mode === 'test') { // test mode, can only use ticker FAKEPACA
15435
15745
  this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/test';
15436
15746
  this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options'; // there's no test mode for options
15437
15747
  }
15438
- else {
15439
- // production
15748
+ else { // production
15440
15749
  this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/sip';
15441
15750
  this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options';
15442
15751
  }
@@ -15452,30 +15761,24 @@ class AlpacaMarketDataAPI extends EventEmitter {
15452
15761
  return 'production';
15453
15762
  }
15454
15763
  }
15455
- constructor(config) {
15764
+ constructor() {
15456
15765
  super();
15457
- this.apiKey = config.apiKey;
15458
- this.secretKey = config.secretKey;
15459
- this.accountType = config.accountType || 'LIVE';
15460
15766
  this.dataURL = 'https://data.alpaca.markets/v2';
15461
15767
  this.apiURL =
15462
- this.accountType === 'PAPER'
15768
+ process.env.ALPACA_ACCOUNT_TYPE === 'PAPER'
15463
15769
  ? 'https://paper-api.alpaca.markets/v2'
15464
15770
  : 'https://api.alpaca.markets/v2'; // used by some, e.g. getAssets
15465
15771
  this.v1beta1url = 'https://data.alpaca.markets/v1beta1'; // used for options endpoints
15466
15772
  this.setMode('production'); // sets stockStreamUrl and optionStreamUrl
15467
15773
  this.headers = {
15468
- 'APCA-API-KEY-ID': this.apiKey,
15469
- 'APCA-API-SECRET-KEY': this.secretKey,
15774
+ 'APCA-API-KEY-ID': process.env.ALPACA_API_KEY,
15775
+ 'APCA-API-SECRET-KEY': process.env.ALPACA_SECRET_KEY,
15470
15776
  'Content-Type': 'application/json',
15471
15777
  };
15472
15778
  }
15473
- static getInstance(config) {
15779
+ static getInstance() {
15474
15780
  if (!AlpacaMarketDataAPI.instance) {
15475
- if (!config) {
15476
- throw new Error('AlpacaMarketDataAPI config is required for first initialization');
15477
- }
15478
- AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI(config);
15781
+ AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI();
15479
15782
  }
15480
15783
  return AlpacaMarketDataAPI.instance;
15481
15784
  }
@@ -15498,8 +15801,8 @@ class AlpacaMarketDataAPI extends EventEmitter {
15498
15801
  log(`${streamType} stream connected`, { type: 'info' });
15499
15802
  const authMessage = {
15500
15803
  action: 'auth',
15501
- key: this.apiKey,
15502
- secret: this.secretKey,
15804
+ key: process.env.ALPACA_API_KEY,
15805
+ secret: process.env.ALPACA_SECRET_KEY,
15503
15806
  };
15504
15807
  ws.send(JSON.stringify(authMessage));
15505
15808
  });
@@ -15590,7 +15893,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
15590
15893
  const currentSubscriptions = streamType === 'stock' ? this.stockSubscriptions : this.optionSubscriptions;
15591
15894
  Object.entries(subscriptions).forEach(([key, value]) => {
15592
15895
  if (value) {
15593
- currentSubscriptions[key] = (currentSubscriptions[key] || []).filter((s) => !value.includes(s));
15896
+ currentSubscriptions[key] = (currentSubscriptions[key] || []).filter(s => !value.includes(s));
15594
15897
  }
15595
15898
  });
15596
15899
  const unsubMessage = {
@@ -15653,11 +15956,11 @@ class AlpacaMarketDataAPI extends EventEmitter {
15653
15956
  let pageCount = 0;
15654
15957
  let currency = '';
15655
15958
  // Initialize bar arrays for each symbol
15656
- symbols.forEach((symbol) => {
15959
+ symbols.forEach(symbol => {
15657
15960
  allBars[symbol] = [];
15658
15961
  });
15659
15962
  log(`Starting historical bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
15660
- type: 'info',
15963
+ type: 'info'
15661
15964
  });
15662
15965
  while (hasMorePages) {
15663
15966
  pageCount++;
@@ -15685,7 +15988,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
15685
15988
  allBars[symbol] = [...allBars[symbol], ...bars];
15686
15989
  pageBarsCount += bars.length;
15687
15990
  // Track date range for this page
15688
- bars.forEach((bar) => {
15991
+ bars.forEach(bar => {
15689
15992
  const barDate = new Date(bar.t);
15690
15993
  if (!earliestTimestamp || barDate < earliestTimestamp) {
15691
15994
  earliestTimestamp = barDate;
@@ -15704,7 +16007,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
15704
16007
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
15705
16008
  : 'unknown range';
15706
16009
  log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
15707
- type: 'info',
16010
+ type: 'info'
15708
16011
  });
15709
16012
  // Prevent infinite loops
15710
16013
  if (pageCount > 1000) {
@@ -15713,11 +16016,9 @@ class AlpacaMarketDataAPI extends EventEmitter {
15713
16016
  }
15714
16017
  }
15715
16018
  // Final summary
15716
- const symbolCounts = Object.entries(allBars)
15717
- .map(([symbol, bars]) => `${symbol}: ${bars.length}`)
15718
- .join(', ');
16019
+ const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
15719
16020
  log(`Historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
15720
- type: 'info',
16021
+ type: 'info'
15721
16022
  });
15722
16023
  return {
15723
16024
  bars: allBars,
@@ -15985,11 +16286,11 @@ class AlpacaMarketDataAPI extends EventEmitter {
15985
16286
  let totalBarsCount = 0;
15986
16287
  let pageCount = 0;
15987
16288
  // Initialize bar arrays for each symbol
15988
- symbols.forEach((symbol) => {
16289
+ symbols.forEach(symbol => {
15989
16290
  allBars[symbol] = [];
15990
16291
  });
15991
16292
  log(`Starting historical options bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
15992
- type: 'info',
16293
+ type: 'info'
15993
16294
  });
15994
16295
  while (hasMorePages) {
15995
16296
  pageCount++;
@@ -16011,7 +16312,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
16011
16312
  allBars[symbol] = [...allBars[symbol], ...bars];
16012
16313
  pageBarsCount += bars.length;
16013
16314
  // Track date range for this page
16014
- bars.forEach((bar) => {
16315
+ bars.forEach(bar => {
16015
16316
  const barDate = new Date(bar.t);
16016
16317
  if (!earliestTimestamp || barDate < earliestTimestamp) {
16017
16318
  earliestTimestamp = barDate;
@@ -16030,7 +16331,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
16030
16331
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
16031
16332
  : 'unknown range';
16032
16333
  log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} option bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
16033
- type: 'info',
16334
+ type: 'info'
16034
16335
  });
16035
16336
  // Prevent infinite loops
16036
16337
  if (pageCount > 1000) {
@@ -16039,11 +16340,9 @@ class AlpacaMarketDataAPI extends EventEmitter {
16039
16340
  }
16040
16341
  }
16041
16342
  // Final summary
16042
- const symbolCounts = Object.entries(allBars)
16043
- .map(([symbol, bars]) => `${symbol}: ${bars.length}`)
16044
- .join(', ');
16343
+ const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
16045
16344
  log(`Historical options bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
16046
- type: 'info',
16345
+ type: 'info'
16047
16346
  });
16048
16347
  return {
16049
16348
  bars: allBars,
@@ -16067,11 +16366,11 @@ class AlpacaMarketDataAPI extends EventEmitter {
16067
16366
  let totalTradesCount = 0;
16068
16367
  let pageCount = 0;
16069
16368
  // Initialize trades arrays for each symbol
16070
- symbols.forEach((symbol) => {
16369
+ symbols.forEach(symbol => {
16071
16370
  allTrades[symbol] = [];
16072
16371
  });
16073
16372
  log(`Starting historical options trades fetch for ${symbolsStr} (${params.start || 'no start'} to ${params.end || 'no end'})`, {
16074
- type: 'info',
16373
+ type: 'info'
16075
16374
  });
16076
16375
  while (hasMorePages) {
16077
16376
  pageCount++;
@@ -16093,7 +16392,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
16093
16392
  allTrades[symbol] = [...allTrades[symbol], ...trades];
16094
16393
  pageTradesCount += trades.length;
16095
16394
  // Track date range for this page
16096
- trades.forEach((trade) => {
16395
+ trades.forEach(trade => {
16097
16396
  const tradeDate = new Date(trade.t);
16098
16397
  if (!earliestTimestamp || tradeDate < earliestTimestamp) {
16099
16398
  earliestTimestamp = tradeDate;
@@ -16112,7 +16411,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
16112
16411
  ? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
16113
16412
  : 'unknown range';
16114
16413
  log(`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} option trades (total: ${totalTradesCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
16115
- type: 'info',
16414
+ type: 'info'
16116
16415
  });
16117
16416
  // Prevent infinite loops
16118
16417
  if (pageCount > 1000) {
@@ -16121,11 +16420,9 @@ class AlpacaMarketDataAPI extends EventEmitter {
16121
16420
  }
16122
16421
  }
16123
16422
  // Final summary
16124
- const symbolCounts = Object.entries(allTrades)
16125
- .map(([symbol, trades]) => `${symbol}: ${trades.length}`)
16126
- .join(', ');
16423
+ const symbolCounts = Object.entries(allTrades).map(([symbol, trades]) => `${symbol}: ${trades.length}`).join(', ');
16127
16424
  log(`Historical options trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages (${symbolCounts})`, {
16128
- type: 'info',
16425
+ type: 'info'
16129
16426
  });
16130
16427
  return {
16131
16428
  trades: allTrades,
@@ -16270,9 +16567,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
16270
16567
  ...(symbol && { symbols: symbol }),
16271
16568
  ...(mergedParams.limit && { limit: Math.min(50, maxLimit - fetchedCount).toString() }),
16272
16569
  ...(mergedParams.sort && { sort: mergedParams.sort }),
16273
- ...(mergedParams.include_content !== undefined
16274
- ? { include_content: mergedParams.include_content.toString() }
16275
- : {}),
16570
+ ...(mergedParams.include_content !== undefined ? { include_content: mergedParams.include_content.toString() } : {}),
16276
16571
  ...(pageToken && { page_token: pageToken }),
16277
16572
  });
16278
16573
  const url = `${this.v1beta1url}/news?${queryParams}`;
@@ -16316,6 +16611,8 @@ class AlpacaMarketDataAPI extends EventEmitter {
16316
16611
  return newsArticles;
16317
16612
  }
16318
16613
  }
16614
+ // Export the singleton instance
16615
+ const marketDataAPI = AlpacaMarketDataAPI.getInstance();
16319
16616
 
16320
16617
  const limitPriceSlippagePercent100 = 0.1; // 0.1%
16321
16618
  /**
@@ -16327,11 +16624,11 @@ Websocket example
16327
16624
  alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
16328
16625
  */
16329
16626
  class AlpacaTradingAPI {
16330
- static new(credentials, marketDataConfig) {
16331
- return new AlpacaTradingAPI(credentials, marketDataConfig);
16627
+ static new(credentials) {
16628
+ return new AlpacaTradingAPI(credentials);
16332
16629
  }
16333
- static getInstance(credentials, marketDataConfig) {
16334
- return new AlpacaTradingAPI(credentials, marketDataConfig);
16630
+ static getInstance(credentials) {
16631
+ return new AlpacaTradingAPI(credentials);
16335
16632
  }
16336
16633
  ws = null;
16337
16634
  headers;
@@ -16345,7 +16642,6 @@ class AlpacaTradingAPI {
16345
16642
  reconnectTimeout = null;
16346
16643
  messageHandlers = new Map();
16347
16644
  debugLogging = false;
16348
- marketDataAPI;
16349
16645
  /**
16350
16646
  * Constructor for AlpacaTradingAPI
16351
16647
  * @param credentials - Alpaca credentials,
@@ -16354,14 +16650,11 @@ class AlpacaTradingAPI {
16354
16650
  * apiSecret: string; // Alpaca API secret
16355
16651
  * type: AlpacaAccountType;
16356
16652
  * orderType: AlpacaOrderType;
16357
- * @param marketDataConfig - Market data API configuration
16358
16653
  * @param options - Optional options
16359
16654
  * debugLogging: boolean; // Whether to log messages of type 'debug'
16360
16655
  */
16361
- constructor(credentials, marketDataConfig, options) {
16656
+ constructor(credentials, options) {
16362
16657
  this.credentials = credentials;
16363
- // Initialize market data API instance
16364
- this.marketDataAPI = AlpacaMarketDataAPI.getInstance(marketDataConfig);
16365
16658
  // Set URLs based on account type
16366
16659
  this.apiBaseUrl =
16367
16660
  credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
@@ -16900,7 +17193,7 @@ class AlpacaTradingAPI {
16900
17193
  this.log(`Found ${positions.length} positions to close`);
16901
17194
  // Get latest quotes for all positions
16902
17195
  const symbols = positions.map((position) => position.symbol);
16903
- const quotesResponse = await this.marketDataAPI.getLatestQuotes(symbols);
17196
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
16904
17197
  const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
16905
17198
  if (lengthOfQuotes === 0) {
16906
17199
  this.log('No quotes available for positions, received 0 quotes', {
@@ -16967,7 +17260,7 @@ class AlpacaTradingAPI {
16967
17260
  this.log(`Cancelled all open orders`);
16968
17261
  // Get latest quotes for all positions
16969
17262
  const symbols = positions.map((position) => position.symbol);
16970
- const quotesResponse = await this.marketDataAPI.getLatestQuotes(symbols);
17263
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
16971
17264
  // Create limit orders to close each position
16972
17265
  for (const position of positions) {
16973
17266
  const quote = quotesResponse.quotes[position.symbol];
@@ -17650,13 +17943,6 @@ class AlpacaTradingAPI {
17650
17943
  }
17651
17944
  }
17652
17945
 
17653
- // Export factory functions for easier instantiation
17654
- const createAlpacaTradingAPI = (credentials, marketDataConfig) => {
17655
- return new AlpacaTradingAPI(credentials, marketDataConfig);
17656
- };
17657
- const createAlpacaMarketDataAPI = (config) => {
17658
- return AlpacaMarketDataAPI.getInstance(config);
17659
- };
17660
17946
  const disco = {
17661
17947
  types: Types,
17662
17948
  alpaca: {
@@ -17704,31 +17990,14 @@ const disco = {
17704
17990
  calculateFibonacciLevels: calculateFibonacciLevels,
17705
17991
  },
17706
17992
  time: {
17707
- toUnixTimestamp: toUnixTimestamp,
17708
- getTimeAgo: getTimeAgo,
17709
- timeAgo: timeAgo,
17710
- normalizeDate: normalizeDate,
17711
- getDateInNY: getDateInNY,
17712
- createMarketTimeUtil: createMarketTimeUtil,
17713
- getStartAndEndTimestamps: getStartAndEndTimestamps,
17714
17993
  getStartAndEndDates: getStartAndEndDates,
17715
17994
  getMarketOpenClose: getMarketOpenClose,
17716
- calculateTimeRange: calculateTimeRange,
17717
- calculateDaysLeft: calculateDaysLeft,
17718
- formatDate: formatDate /* move to format, keeping here for compatibility */,
17719
- currentTimeET: currentTimeET,
17720
- MarketTimeUtil: MarketTimeUtil,
17721
- MARKET_TIMES: MARKET_TIMES,
17722
- getLastTradingDateYYYYMMDD: getLastTradingDateYYYYMMDD,
17723
17995
  getLastFullTradingDate: getLastFullTradingDate,
17724
17996
  getNextMarketDay: getNextMarketDay,
17725
- parseETDateFromAV: parseETDateFromAV,
17726
- formatToUSEastern: formatToUSEastern,
17727
- unixTimetoUSEastern: unixTimetoUSEastern,
17728
17997
  getMarketStatus: getMarketStatus,
17729
- timeDiffString: timeDiffString,
17730
17998
  getNYTimeZone: getNYTimeZone,
17731
17999
  getTradingDate: getTradingDate,
18000
+ getTradingStartAndEndDates: getTradingStartAndEndDates,
17732
18001
  },
17733
18002
  utils: {
17734
18003
  logIfDebug: logIfDebug,
@@ -17737,5 +18006,5 @@ const disco = {
17737
18006
  },
17738
18007
  };
17739
18008
 
17740
- export { AlpacaMarketDataAPI, AlpacaTradingAPI, createAlpacaMarketDataAPI, createAlpacaTradingAPI, disco };
18009
+ export { AlpacaMarketDataAPI, AlpacaTradingAPI, disco };
17741
18010
  //# sourceMappingURL=index.mjs.map