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