@discomedia/utils 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import { startOfDay, set, endOfDay, add, sub, format, differenceInMilliseconds, isBefore } from 'date-fns';
2
- import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz';
3
1
  import require$$0$3, { EventEmitter } from 'events';
4
2
  import require$$1 from 'https';
5
3
  import require$$2 from 'http';
@@ -146,13 +144,11 @@ const marketEarlyCloses = {
146
144
  },
147
145
  };
148
146
 
149
- // market-time.ts - Refactored for better organization and usability
150
- // ===== CONFIGURATION =====
151
- /**
152
- * Market configuration constants
153
- */
147
+ // Constants for NY market times (Eastern Time)
154
148
  const MARKET_CONFIG = {
155
149
  TIMEZONE: 'America/New_York',
150
+ UTC_OFFSET_STANDARD: -5, // EST
151
+ UTC_OFFSET_DST: -4, // EDT
156
152
  TIMES: {
157
153
  EXTENDED_START: { hour: 4, minute: 0 },
158
154
  MARKET_OPEN: { hour: 9, minute: 30 },
@@ -163,48 +159,154 @@ const MARKET_CONFIG = {
163
159
  EARLY_EXTENDED_END: { hour: 17, minute: 0 },
164
160
  },
165
161
  };
166
- // ===== MARKET CALENDAR SERVICE =====
162
+ // Helper: Get NY offset for a given UTC date (DST rules for US)
167
163
  /**
168
- * Service for handling market calendar operations (holidays, early closes, market days)
164
+ * Returns the NY timezone offset (in hours) for a given UTC date, accounting for US DST rules.
165
+ * @param date - UTC date
166
+ * @returns offset in hours (-5 for EST, -4 for EDT)
169
167
  */
170
- class MarketCalendar {
171
- timezone;
172
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
173
- this.timezone = timezone;
168
+ function getNYOffset(date) {
169
+ // US DST starts 2nd Sunday in March, ends 1st Sunday in November
170
+ const year = date.getUTCFullYear();
171
+ const dstStart = getNthWeekdayOfMonth(year, 3, 0, 2); // March, Sunday, 2nd
172
+ const dstEnd = getNthWeekdayOfMonth(year, 11, 0, 1); // November, Sunday, 1st
173
+ const utcTime = date.getTime();
174
+ if (utcTime >= dstStart.getTime() && utcTime < dstEnd.getTime()) {
175
+ return MARKET_CONFIG.UTC_OFFSET_DST;
176
+ }
177
+ return MARKET_CONFIG.UTC_OFFSET_STANDARD;
178
+ }
179
+ // Helper: Get nth weekday of month in UTC
180
+ /**
181
+ * Returns the nth weekday of a given month in UTC.
182
+ * @param year - Year
183
+ * @param month - 1-based month (e.g. March = 3)
184
+ * @param weekday - 0=Sunday, 1=Monday, ...
185
+ * @param n - nth occurrence
186
+ * @returns Date object for the nth weekday
187
+ */
188
+ function getNthWeekdayOfMonth(year, month, weekday, n) {
189
+ let count = 0;
190
+ for (let d = 1; d <= 31; d++) {
191
+ const date = new Date(Date.UTC(year, month - 1, d));
192
+ if (date.getUTCMonth() !== month - 1)
193
+ break;
194
+ if (date.getUTCDay() === weekday) {
195
+ count++;
196
+ if (count === n)
197
+ return date;
198
+ }
174
199
  }
200
+ // fallback: last day of month
201
+ return new Date(Date.UTC(year, month - 1, 28));
202
+ }
203
+ // Helper: Convert UTC date to NY time (returns new Date object)
204
+ /**
205
+ * Converts a UTC date to NY time (returns a new Date object).
206
+ * @param date - UTC date
207
+ * @returns Date object in NY time
208
+ */
209
+ function toNYTime(date) {
210
+ const offset = getNYOffset(date);
211
+ // NY offset in hours
212
+ const utcMillis = date.getTime();
213
+ const nyMillis = utcMillis + offset * 60 * 60 * 1000;
214
+ return new Date(nyMillis);
215
+ }
216
+ // Helper: Convert NY time to UTC (returns new Date object)
217
+ /**
218
+ * Converts a NY time date to UTC (returns a new Date object).
219
+ * @param date - NY time date
220
+ * @returns Date object in UTC
221
+ */
222
+ function fromNYTime(date) {
223
+ const offset = getNYOffset(date);
224
+ const nyMillis = date.getTime();
225
+ const utcMillis = nyMillis - offset * 60 * 60 * 1000;
226
+ return new Date(utcMillis);
227
+ }
228
+ // Helper: Format date in ISO, unix, etc.
229
+ /**
230
+ * Formats a date in ISO, unix-seconds, or unix-ms format.
231
+ * @param date - Date object
232
+ * @param outputFormat - Output format ('iso', 'unix-seconds', 'unix-ms')
233
+ * @returns Formatted date string or number
234
+ */
235
+ function formatDate(date, outputFormat = 'iso') {
236
+ switch (outputFormat) {
237
+ case 'unix-seconds':
238
+ return Math.floor(date.getTime() / 1000);
239
+ case 'unix-ms':
240
+ return date.getTime();
241
+ case 'iso':
242
+ default:
243
+ return date.toISOString();
244
+ }
245
+ }
246
+ // Helper: Format date in NY locale string
247
+ /**
248
+ * Formats a date in NY locale string.
249
+ * @param date - Date object
250
+ * @returns NY locale string
251
+ */
252
+ function formatNYLocale(date) {
253
+ return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
254
+ }
255
+ // Market calendar logic
256
+ /**
257
+ * Market calendar logic for holidays, weekends, and market days.
258
+ */
259
+ class MarketCalendar {
175
260
  /**
176
- * Check if a date is a weekend
261
+ * Checks if a date is a weekend in NY time.
262
+ * @param date - Date object
263
+ * @returns true if weekend, false otherwise
177
264
  */
178
265
  isWeekend(date) {
179
- const day = date.getDay();
180
- return day === 0 || day === 6; // Sunday or Saturday
266
+ const day = toNYTime(date).getUTCDay();
267
+ return day === 0 || day === 6;
181
268
  }
182
269
  /**
183
- * Check if a date is a market holiday
270
+ * Checks if a date is a market holiday in NY time.
271
+ * @param date - Date object
272
+ * @returns true if holiday, false otherwise
184
273
  */
185
274
  isHoliday(date) {
186
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
187
- const year = toZonedTime(date, this.timezone).getFullYear();
275
+ const nyDate = toNYTime(date);
276
+ const year = nyDate.getUTCFullYear();
277
+ const month = nyDate.getUTCMonth() + 1;
278
+ const day = nyDate.getUTCDate();
279
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
188
280
  const yearHolidays = marketHolidays[year];
189
281
  if (!yearHolidays)
190
282
  return false;
191
283
  return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
192
284
  }
193
285
  /**
194
- * Check if a date is an early close day
286
+ * Checks if a date is an early close day in NY time.
287
+ * @param date - Date object
288
+ * @returns true if early close, false otherwise
195
289
  */
196
290
  isEarlyCloseDay(date) {
197
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
198
- const year = toZonedTime(date, this.timezone).getFullYear();
291
+ const nyDate = toNYTime(date);
292
+ const year = nyDate.getUTCFullYear();
293
+ const month = nyDate.getUTCMonth() + 1;
294
+ const day = nyDate.getUTCDate();
295
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
199
296
  const yearEarlyCloses = marketEarlyCloses[year];
200
297
  return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
201
298
  }
202
299
  /**
203
- * Get the early close time for a date (in minutes from midnight)
300
+ * Gets the early close time (in minutes from midnight) for a given date.
301
+ * @param date - Date object
302
+ * @returns minutes from midnight or null
204
303
  */
205
304
  getEarlyCloseTime(date) {
206
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
207
- const year = toZonedTime(date, this.timezone).getFullYear();
305
+ const nyDate = toNYTime(date);
306
+ const year = nyDate.getUTCFullYear();
307
+ const month = nyDate.getUTCMonth() + 1;
308
+ const day = nyDate.getUTCDate();
309
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
208
310
  const yearEarlyCloses = marketEarlyCloses[year];
209
311
  if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
210
312
  const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
@@ -213,617 +315,572 @@ class MarketCalendar {
213
315
  return null;
214
316
  }
215
317
  /**
216
- * Check if a date is a market day (not weekend or holiday)
318
+ * Checks if a date is a market day (not weekend or holiday).
319
+ * @param date - Date object
320
+ * @returns true if market day, false otherwise
217
321
  */
218
322
  isMarketDay(date) {
219
323
  return !this.isWeekend(date) && !this.isHoliday(date);
220
324
  }
221
325
  /**
222
- * Get the next market day from a given date
326
+ * Gets the next market day after the given date.
327
+ * @param date - Date object
328
+ * @returns Date object for next market day
223
329
  */
224
330
  getNextMarketDay(date) {
225
- let nextDay = add(date, { days: 1 });
331
+ let nextDay = new Date(date.getTime() + 24 * 60 * 60 * 1000);
226
332
  while (!this.isMarketDay(nextDay)) {
227
- nextDay = add(nextDay, { days: 1 });
333
+ nextDay = new Date(nextDay.getTime() + 24 * 60 * 60 * 1000);
228
334
  }
229
335
  return nextDay;
230
336
  }
231
337
  /**
232
- * Get the previous market day from a given date
338
+ * Gets the previous market day before the given date.
339
+ * @param date - Date object
340
+ * @returns Date object for previous market day
233
341
  */
234
342
  getPreviousMarketDay(date) {
235
- let prevDay = sub(date, { days: 1 });
343
+ let prevDay = new Date(date.getTime() - 24 * 60 * 60 * 1000);
236
344
  while (!this.isMarketDay(prevDay)) {
237
- prevDay = sub(prevDay, { days: 1 });
345
+ prevDay = new Date(prevDay.getTime() - 24 * 60 * 60 * 1000);
238
346
  }
239
347
  return prevDay;
240
348
  }
241
349
  }
242
- // ===== TIME FORMATTER SERVICE =====
350
+ // Market open/close times
243
351
  /**
244
- * Service for formatting time outputs
352
+ * Returns market open/close times for a given date, including extended and early closes.
353
+ * @param date - Date object
354
+ * @returns MarketOpenCloseResult
245
355
  */
246
- class TimeFormatter {
247
- timezone;
248
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
249
- this.timezone = timezone;
250
- }
251
- /**
252
- * Format a date based on the output format
253
- */
254
- formatDate(date, outputFormat = 'iso') {
255
- switch (outputFormat) {
256
- case 'unix-seconds':
257
- return Math.floor(date.getTime() / 1000);
258
- case 'unix-ms':
259
- return date.getTime();
260
- case 'iso':
261
- default:
262
- return formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
263
- }
264
- }
265
- /**
266
- * Get New York timezone offset
267
- */
268
- getNYTimeZone(date = new Date()) {
269
- const dtf = new Intl.DateTimeFormat('en-US', {
270
- timeZone: this.timezone,
271
- timeZoneName: 'shortOffset',
272
- });
273
- const parts = dtf.formatToParts(date);
274
- const tz = parts.find(p => p.type === 'timeZoneName')?.value;
275
- if (!tz) {
276
- throw new Error('Could not determine New York offset');
277
- }
278
- const shortOffset = tz.replace('GMT', '');
279
- if (shortOffset === '-4') {
280
- return '-04:00';
281
- }
282
- else if (shortOffset === '-5') {
283
- return '-05:00';
284
- }
285
- else {
286
- throw new Error(`Unexpected timezone offset: ${shortOffset}`);
287
- }
356
+ function getMarketTimes(date) {
357
+ const calendar = new MarketCalendar();
358
+ const nyDate = toNYTime(date);
359
+ if (!calendar.isMarketDay(date)) {
360
+ return {
361
+ marketOpen: false,
362
+ open: null,
363
+ close: null,
364
+ openExt: null,
365
+ closeExt: null,
366
+ };
288
367
  }
289
- /**
290
- * Get trading date in YYYY-MM-DD format
291
- */
292
- getTradingDate(time) {
293
- let date;
294
- if (typeof time === 'number') {
295
- date = new Date(time);
296
- }
297
- else if (typeof time === 'string') {
298
- date = new Date(time);
299
- }
300
- else {
301
- date = time;
302
- }
303
- return formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
368
+ const year = nyDate.getUTCFullYear();
369
+ const month = nyDate.getUTCMonth();
370
+ const day = nyDate.getUTCDate();
371
+ // Helper to build NY time for a given hour/minute
372
+ function buildNYTime(hour, minute) {
373
+ const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
374
+ return fromNYTime(d);
375
+ }
376
+ let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
377
+ let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
378
+ let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
379
+ let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
380
+ if (calendar.isEarlyCloseDay(date)) {
381
+ close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
382
+ closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
304
383
  }
384
+ return {
385
+ marketOpen: true,
386
+ open,
387
+ close,
388
+ openExt,
389
+ closeExt,
390
+ };
305
391
  }
306
- // ===== MARKET TIME CALCULATOR =====
392
+ // Is within market hours
307
393
  /**
308
- * Service for core market time calculations
394
+ * Checks if a date/time is within market hours, extended hours, or continuous.
395
+ * @param date - Date object
396
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
397
+ * @returns true if within hours, false otherwise
309
398
  */
310
- class MarketTimeCalculator {
311
- calendar;
312
- timezone;
313
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
314
- this.timezone = timezone;
315
- this.calendar = new MarketCalendar(timezone);
316
- }
317
- /**
318
- * Get market open/close times for a date
319
- */
320
- getMarketTimes(date) {
321
- const zonedDate = toZonedTime(date, this.timezone);
322
- // Market closed on weekends and holidays
323
- if (!this.calendar.isMarketDay(zonedDate)) {
324
- return {
325
- marketOpen: false,
326
- open: null,
327
- close: null,
328
- openExt: null,
329
- closeExt: null,
330
- };
331
- }
332
- const dayStart = startOfDay(zonedDate);
333
- // Regular market times
334
- const open = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour, minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute }), this.timezone);
335
- let close = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute }), this.timezone);
336
- // Extended hours
337
- const openExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute }), this.timezone);
338
- let closeExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute }), this.timezone);
339
- // Handle early close days
340
- if (this.calendar.isEarlyCloseDay(zonedDate)) {
341
- close = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute }), this.timezone);
342
- closeExt = fromZonedTime(set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute }), this.timezone);
343
- }
344
- return {
345
- marketOpen: true,
346
- open,
347
- close,
348
- openExt,
349
- closeExt,
350
- };
351
- }
352
- /**
353
- * Check if a time is within market hours based on intraday reporting mode
354
- */
355
- isWithinMarketHours(date, intradayReporting = 'market_hours') {
356
- const zonedDate = toZonedTime(date, this.timezone);
357
- // Not a market day
358
- if (!this.calendar.isMarketDay(zonedDate)) {
359
- return false;
360
- }
361
- const timeInMinutes = zonedDate.getHours() * 60 + zonedDate.getMinutes();
362
- switch (intradayReporting) {
363
- case 'extended_hours': {
364
- const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
365
- let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
366
- // Handle early close
367
- if (this.calendar.isEarlyCloseDay(zonedDate)) {
368
- endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
369
- }
370
- return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
371
- }
372
- case 'continuous':
373
- return true;
374
- default: {
375
- // 'market_hours'
376
- const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
377
- let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
378
- // Handle early close
379
- if (this.calendar.isEarlyCloseDay(zonedDate)) {
380
- endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
381
- }
382
- return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
399
+ function isWithinMarketHoursImpl(date, intradayReporting = 'market_hours') {
400
+ const calendar = new MarketCalendar();
401
+ if (!calendar.isMarketDay(date))
402
+ return false;
403
+ const nyDate = toNYTime(date);
404
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
405
+ switch (intradayReporting) {
406
+ case 'extended_hours': {
407
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
408
+ if (calendar.isEarlyCloseDay(date)) {
409
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
383
410
  }
411
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
412
+ return minutes >= startMinutes && minutes <= endMinutes;
384
413
  }
385
- }
386
- /**
387
- * Get the last full trading date
388
- */
389
- getLastFullTradingDate(currentDate = new Date(), extendedHours = true) {
390
- const nowET = toZonedTime(currentDate, this.timezone);
391
- if (this.calendar.isMarketDay(nowET)) {
392
- const timeInMinutes = nowET.getHours() * 60 + nowET.getMinutes();
393
- let endMinutes;
394
- if (extendedHours) {
395
- endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
396
- if (this.calendar.isEarlyCloseDay(nowET)) {
397
- endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
398
- }
399
- }
400
- else {
401
- endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
402
- if (this.calendar.isEarlyCloseDay(nowET)) {
403
- endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
404
- }
405
- }
406
- if (timeInMinutes >= endMinutes) {
407
- return fromZonedTime(set(nowET, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
414
+ case 'continuous':
415
+ return true;
416
+ default: {
417
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
418
+ if (calendar.isEarlyCloseDay(date)) {
419
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
408
420
  }
421
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
422
+ return minutes >= startMinutes && minutes <= endMinutes;
409
423
  }
410
- // Return the last completed trading day
411
- const lastMarketDay = this.calendar.getPreviousMarketDay(nowET);
412
- return fromZonedTime(set(lastMarketDay, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
413
424
  }
414
- /**
415
- * Get day boundaries based on intraday reporting mode
416
- */
417
- getDayBoundaries(date, intradayReporting = 'market_hours') {
418
- const zonedDate = toZonedTime(date, this.timezone);
419
- let start;
420
- let end;
421
- switch (intradayReporting) {
422
- case 'extended_hours': {
423
- start = set(zonedDate, {
424
- hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
425
- minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
426
- seconds: 0,
427
- milliseconds: 0,
428
- });
429
- end = set(zonedDate, {
430
- hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour,
431
- minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute,
432
- seconds: 59,
433
- milliseconds: 999,
434
- });
435
- // Handle early close
436
- if (this.calendar.isEarlyCloseDay(zonedDate)) {
437
- end = set(zonedDate, {
438
- hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour,
439
- minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute,
440
- seconds: 59,
441
- milliseconds: 999,
442
- });
443
- }
444
- break;
445
- }
446
- case 'continuous': {
447
- start = startOfDay(zonedDate);
448
- end = endOfDay(zonedDate);
449
- break;
425
+ }
426
+ // Get last full trading date
427
+ /**
428
+ * Returns the last full trading date (market close) for a given date.
429
+ * @param currentDate - Date object (default: now)
430
+ * @returns Date object for last full trading date
431
+ */
432
+ function getLastFullTradingDateImpl(currentDate = new Date()) {
433
+ const calendar = new MarketCalendar();
434
+ const nyDate = toNYTime(currentDate);
435
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
436
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
437
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
438
+ if (calendar.isEarlyCloseDay(currentDate)) {
439
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
440
+ }
441
+ // If not a market day, or before open, or during market hours, return previous market day's close
442
+ if (!calendar.isMarketDay(currentDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
443
+ const prevMarketDay = calendar.getPreviousMarketDay(currentDate);
444
+ let prevCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
445
+ if (calendar.isEarlyCloseDay(prevMarketDay)) {
446
+ prevCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
447
+ }
448
+ const year = prevMarketDay.getUTCFullYear();
449
+ const month = prevMarketDay.getUTCMonth();
450
+ const day = prevMarketDay.getUTCDate();
451
+ const closeHour = Math.floor(prevCloseMinutes / 60);
452
+ const closeMinute = prevCloseMinutes % 60;
453
+ return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
454
+ }
455
+ // After market close or after extended hours, return today's close
456
+ const year = nyDate.getUTCFullYear();
457
+ const month = nyDate.getUTCMonth();
458
+ const day = nyDate.getUTCDate();
459
+ const closeHour = Math.floor(marketCloseMinutes / 60);
460
+ const closeMinute = marketCloseMinutes % 60;
461
+ return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
462
+ }
463
+ // Get day boundaries
464
+ /**
465
+ * Returns the start and end boundaries for a market day, extended hours, or continuous.
466
+ * @param date - Date object
467
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
468
+ * @returns Object with start and end Date
469
+ */
470
+ function getDayBoundaries(date, intradayReporting = 'market_hours') {
471
+ const calendar = new MarketCalendar();
472
+ const nyDate = toNYTime(date);
473
+ const year = nyDate.getUTCFullYear();
474
+ const month = nyDate.getUTCMonth();
475
+ const day = nyDate.getUTCDate();
476
+ function buildNYTime(hour, minute, sec = 0, ms = 0) {
477
+ const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
478
+ return fromNYTime(d);
479
+ }
480
+ let start;
481
+ let end;
482
+ switch (intradayReporting) {
483
+ case 'extended_hours':
484
+ start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
485
+ end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
486
+ if (calendar.isEarlyCloseDay(date)) {
487
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
450
488
  }
451
- default: {
452
- // 'market_hours'
453
- start = set(zonedDate, {
454
- hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
455
- minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
456
- seconds: 0,
457
- milliseconds: 0,
458
- });
459
- end = set(zonedDate, {
460
- hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
461
- minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
462
- seconds: 59,
463
- milliseconds: 999,
464
- });
465
- // Handle early close
466
- if (this.calendar.isEarlyCloseDay(zonedDate)) {
467
- end = set(zonedDate, {
468
- hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour,
469
- minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute,
470
- seconds: 59,
471
- milliseconds: 999,
472
- });
473
- }
474
- break;
489
+ break;
490
+ case 'continuous':
491
+ start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
492
+ end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
493
+ break;
494
+ default:
495
+ start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
496
+ end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
497
+ if (calendar.isEarlyCloseDay(date)) {
498
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
475
499
  }
476
- }
477
- return {
478
- start: fromZonedTime(start, this.timezone),
479
- end: fromZonedTime(end, this.timezone),
480
- };
500
+ break;
481
501
  }
502
+ return { start, end };
482
503
  }
483
- // ===== PERIOD CALCULATOR =====
504
+ // Period calculator
484
505
  /**
485
- * Service for calculating time periods
506
+ * Calculates the start date for a given period ending at endDate.
507
+ * @param endDate - Date object
508
+ * @param period - Period string
509
+ * @returns Date object for period start
486
510
  */
487
- class PeriodCalculator {
488
- calendar;
489
- timeCalculator;
490
- formatter;
491
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
492
- this.calendar = new MarketCalendar(timezone);
493
- this.timeCalculator = new MarketTimeCalculator(timezone);
494
- this.formatter = new TimeFormatter(timezone);
495
- }
496
- /**
497
- * Calculate the start date for a given period
498
- */
499
- calculatePeriodStartDate(endDate, period) {
500
- let startDate;
501
- switch (period) {
502
- case 'YTD':
503
- startDate = set(endDate, { month: 0, date: 1 });
504
- break;
505
- case '1D':
506
- startDate = this.calendar.getPreviousMarketDay(endDate);
507
- break;
508
- case '3D':
509
- startDate = sub(endDate, { days: 3 });
510
- break;
511
- case '1W':
512
- startDate = sub(endDate, { weeks: 1 });
513
- break;
514
- case '2W':
515
- startDate = sub(endDate, { weeks: 2 });
516
- break;
517
- case '1M':
518
- startDate = sub(endDate, { months: 1 });
519
- break;
520
- case '3M':
521
- startDate = sub(endDate, { months: 3 });
522
- break;
523
- case '6M':
524
- startDate = sub(endDate, { months: 6 });
525
- break;
526
- case '1Y':
527
- startDate = sub(endDate, { years: 1 });
528
- break;
529
- default:
530
- throw new Error(`Invalid period: ${period}`);
531
- }
532
- // Ensure start date is a market day
533
- while (!this.calendar.isMarketDay(startDate)) {
534
- startDate = this.calendar.getNextMarketDay(startDate);
535
- }
536
- return startDate;
511
+ function calculatePeriodStartDate(endDate, period) {
512
+ const calendar = new MarketCalendar();
513
+ let startDate;
514
+ switch (period) {
515
+ case 'YTD':
516
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
517
+ break;
518
+ case '1D':
519
+ startDate = calendar.getPreviousMarketDay(endDate);
520
+ break;
521
+ case '3D':
522
+ startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
523
+ break;
524
+ case '1W':
525
+ startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
526
+ break;
527
+ case '2W':
528
+ startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
529
+ break;
530
+ case '1M':
531
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
532
+ break;
533
+ case '3M':
534
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
535
+ break;
536
+ case '6M':
537
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
538
+ break;
539
+ case '1Y':
540
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
541
+ break;
542
+ default:
543
+ throw new Error(`Invalid period: ${period}`);
537
544
  }
538
- /**
539
- * Get period dates for market time calculations
540
- */
541
- getMarketTimePeriod(params) {
542
- const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
543
- if (!period) {
544
- throw new Error('Period is required');
545
- }
546
- const zonedEndDate = toZonedTime(end, MARKET_CONFIG.TIMEZONE);
547
- // Determine effective end date based on current market conditions
548
- let endDate;
549
- const isCurrentMarketDay = this.calendar.isMarketDay(zonedEndDate);
550
- const isWithinHours = this.timeCalculator.isWithinMarketHours(end, intraday_reporting);
551
- const timeInMinutes = zonedEndDate.getHours() * 60 + zonedEndDate.getMinutes();
552
- const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
553
- if (isCurrentMarketDay) {
554
- if (timeInMinutes < marketStartMinutes) {
555
- // Before market open - use previous day's close
556
- const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
557
- const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
558
- endDate = dayEnd;
559
- }
560
- else if (isWithinHours) {
561
- // During market hours - use current time
562
- endDate = end;
563
- }
564
- else {
565
- // After market close - use today's close
566
- const { end: dayEnd } = this.timeCalculator.getDayBoundaries(zonedEndDate, intraday_reporting);
567
- endDate = dayEnd;
568
- }
569
- }
570
- else {
571
- // Not a market day - use previous market day's close
572
- const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
573
- const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
574
- endDate = dayEnd;
575
- }
576
- // Calculate start date
577
- const periodStartDate = this.calculatePeriodStartDate(endDate, period);
578
- const { start: dayStart } = this.timeCalculator.getDayBoundaries(periodStartDate, intraday_reporting);
579
- // Ensure start is not after end
580
- if (isBefore(endDate, dayStart)) {
581
- throw new Error('Start date cannot be after end date');
582
- }
583
- return {
584
- start: this.formatter.formatDate(dayStart, outputFormat),
585
- end: this.formatter.formatDate(endDate, outputFormat),
586
- };
545
+ // Ensure start date is a market day
546
+ while (!calendar.isMarketDay(startDate)) {
547
+ startDate = calendar.getNextMarketDay(startDate);
587
548
  }
549
+ return startDate;
588
550
  }
589
- // ===== MARKET STATUS SERVICE =====
551
+ // Get market time period
590
552
  /**
591
- * Service for determining market status
553
+ * Returns the start and end dates for a market time period.
554
+ * @param params - MarketTimeParams
555
+ * @returns PeriodDates object
592
556
  */
593
- class MarketStatusService {
594
- calendar;
595
- timeCalculator;
596
- timezone;
597
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
598
- this.timezone = timezone;
599
- this.calendar = new MarketCalendar(timezone);
600
- this.timeCalculator = new MarketTimeCalculator(timezone);
601
- }
602
- /**
603
- * Get current market status
604
- */
605
- getMarketStatus(date = new Date()) {
606
- const nyTime = toZonedTime(date, this.timezone);
607
- const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
608
- const isMarketDay = this.calendar.isMarketDay(nyTime);
609
- const isEarlyCloseDay = this.calendar.isEarlyCloseDay(nyTime);
610
- // Time boundaries
611
- const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
612
- const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
613
- const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
614
- let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
615
- let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
616
- if (isEarlyCloseDay) {
617
- marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
618
- extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
619
- }
620
- let status;
621
- let nextStatus;
622
- let nextStatusTime;
623
- let marketPeriod;
624
- if (!isMarketDay) {
625
- // Market is closed (holiday/weekend)
626
- status = 'closed';
627
- nextStatus = 'extended hours';
628
- marketPeriod = 'closed';
629
- const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
630
- nextStatusTime = set(nextMarketDay, {
631
- hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
632
- minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
633
- });
634
- }
635
- else if (timeInMinutes < extendedStartMinutes) {
636
- // Before extended hours
637
- status = 'closed';
638
- nextStatus = 'extended hours';
639
- marketPeriod = 'closed';
640
- nextStatusTime = set(nyTime, {
641
- hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
642
- minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
643
- });
644
- }
645
- else if (timeInMinutes < marketStartMinutes) {
646
- // Pre-market extended hours
647
- status = 'extended hours';
648
- nextStatus = 'open';
649
- marketPeriod = 'preMarket';
650
- nextStatusTime = set(nyTime, {
651
- hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
652
- minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
653
- });
557
+ function getMarketTimePeriod(params) {
558
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
559
+ if (!period)
560
+ throw new Error('Period is required');
561
+ const calendar = new MarketCalendar();
562
+ const nyEndDate = toNYTime(end);
563
+ let endDate;
564
+ const isCurrentMarketDay = calendar.isMarketDay(end);
565
+ const isWithinHours = isWithinMarketHours(end, intraday_reporting);
566
+ const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
567
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
568
+ if (isCurrentMarketDay) {
569
+ if (minutes < marketStartMinutes) {
570
+ // Before market open - use previous day's close
571
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
572
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
573
+ endDate = dayEnd;
654
574
  }
655
- else if (timeInMinutes < marketCloseMinutes) {
656
- // Market is open
657
- status = 'open';
658
- nextStatus = 'extended hours';
659
- marketPeriod = timeInMinutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
660
- nextStatusTime = set(nyTime, {
661
- hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.hour : MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
662
- minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.minute : MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
663
- });
664
- }
665
- else if (timeInMinutes < extendedEndMinutes) {
666
- // After-market extended hours
667
- status = 'extended hours';
668
- nextStatus = 'closed';
669
- marketPeriod = 'afterMarket';
670
- nextStatusTime = set(nyTime, {
671
- hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour : MARKET_CONFIG.TIMES.EXTENDED_END.hour,
672
- minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute : MARKET_CONFIG.TIMES.EXTENDED_END.minute,
673
- });
575
+ else if (isWithinHours) {
576
+ // During market hours - use current time
577
+ endDate = end;
674
578
  }
675
579
  else {
676
- // After extended hours
677
- status = 'closed';
678
- nextStatus = 'extended hours';
679
- marketPeriod = 'closed';
680
- const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
681
- nextStatusTime = set(nextMarketDay, {
682
- hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
683
- minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
684
- });
580
+ // After market close - use today's close
581
+ const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
582
+ endDate = dayEnd;
685
583
  }
686
- const nextStatusTimeUTC = fromZonedTime(nextStatusTime, this.timezone);
687
- const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
688
- return {
689
- time: date,
690
- timeString: format(nyTime, dateFormat),
691
- status,
692
- nextStatus,
693
- marketPeriod,
694
- nextStatusTime: nextStatusTimeUTC,
695
- nextStatusTimeDifference: differenceInMilliseconds(nextStatusTime, nyTime),
696
- nextStatusTimeString: format(nextStatusTime, dateFormat),
697
- };
698
584
  }
585
+ else {
586
+ // Not a market day - use previous market day's close
587
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
588
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
589
+ endDate = dayEnd;
590
+ }
591
+ // Calculate start date
592
+ const periodStartDate = calculatePeriodStartDate(endDate, period);
593
+ const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
594
+ if (endDate.getTime() < dayStart.getTime()) {
595
+ throw new Error('Start date cannot be after end date');
596
+ }
597
+ return {
598
+ start: formatDate(dayStart, outputFormat),
599
+ end: formatDate(endDate, outputFormat),
600
+ };
699
601
  }
700
- // ===== FUNCTIONAL API =====
602
+ // Market status
701
603
  /**
702
- * Simple functional API that uses the services above
604
+ * Returns the current market status for a given date.
605
+ * @param date - Date object (default: now)
606
+ * @returns MarketStatus object
703
607
  */
704
- // Create service instances
705
- const marketCalendar = new MarketCalendar();
706
- const marketTimeCalculator = new MarketTimeCalculator();
707
- const periodCalculator = new PeriodCalculator();
708
- const marketStatusService = new MarketStatusService();
709
- const timeFormatter = new TimeFormatter();
608
+ function getMarketStatusImpl(date = new Date()) {
609
+ const calendar = new MarketCalendar();
610
+ const nyDate = toNYTime(date);
611
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
612
+ const isMarketDay = calendar.isMarketDay(date);
613
+ const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
614
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
615
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
616
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
617
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
618
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
619
+ if (isEarlyCloseDay) {
620
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
621
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
622
+ }
623
+ let status;
624
+ let nextStatus;
625
+ let nextStatusTime;
626
+ let marketPeriod;
627
+ if (!isMarketDay) {
628
+ status = 'closed';
629
+ nextStatus = 'extended hours';
630
+ marketPeriod = 'closed';
631
+ const nextMarketDay = calendar.getNextMarketDay(date);
632
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
633
+ }
634
+ else if (minutes < extendedStartMinutes) {
635
+ status = 'closed';
636
+ nextStatus = 'extended hours';
637
+ marketPeriod = 'closed';
638
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
639
+ }
640
+ else if (minutes < marketStartMinutes) {
641
+ status = 'extended hours';
642
+ nextStatus = 'open';
643
+ marketPeriod = 'preMarket';
644
+ nextStatusTime = getDayBoundaries(date, 'market_hours').start;
645
+ }
646
+ else if (minutes < marketCloseMinutes) {
647
+ status = 'open';
648
+ nextStatus = 'extended hours';
649
+ marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
650
+ nextStatusTime = getDayBoundaries(date, 'market_hours').end;
651
+ }
652
+ else if (minutes < extendedEndMinutes) {
653
+ status = 'extended hours';
654
+ nextStatus = 'closed';
655
+ marketPeriod = 'afterMarket';
656
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
657
+ }
658
+ else {
659
+ status = 'closed';
660
+ nextStatus = 'extended hours';
661
+ marketPeriod = 'closed';
662
+ const nextMarketDay = calendar.getNextMarketDay(date);
663
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
664
+ }
665
+ const nextStatusTimeDifference = nextStatusTime.getTime() - nyDate.getTime();
666
+ return {
667
+ time: date,
668
+ timeString: formatNYLocale(nyDate),
669
+ status,
670
+ nextStatus,
671
+ marketPeriod,
672
+ nextStatusTime,
673
+ nextStatusTimeDifference,
674
+ nextStatusTimeString: formatNYLocale(nextStatusTime),
675
+ };
676
+ }
677
+ // API exports
710
678
  /**
711
- * Get market open/close times for a given date
679
+ * Returns market open/close times for a given date.
680
+ * @param options - { date?: Date }
681
+ * @returns MarketOpenCloseResult
712
682
  */
713
683
  function getMarketOpenClose(options = {}) {
714
684
  const { date = new Date() } = options;
715
- return marketTimeCalculator.getMarketTimes(date);
685
+ return getMarketTimes(date);
716
686
  }
717
687
  /**
718
- * Get start and end dates for a given market time period
688
+ * Returns the start and end dates for a market time period as Date objects.
689
+ * @param params - MarketTimeParams
690
+ * @returns Object with start and end Date
719
691
  */
720
692
  function getStartAndEndDates(params = {}) {
721
- const { start, end } = periodCalculator.getMarketTimePeriod(params);
693
+ const { start, end } = getMarketTimePeriod(params);
722
694
  return {
723
- start: new Date(start),
724
- end: new Date(end),
695
+ start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
696
+ end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
725
697
  };
726
698
  }
727
699
  /**
728
- * Get the last full trading date
700
+ * Returns the last full trading date as a Date object.
701
+ */
702
+ /**
703
+ * Returns the last full trading date as a Date object.
704
+ * @param currentDate - Date object (default: now)
705
+ * @returns Date object for last full trading date
729
706
  */
730
707
  function getLastFullTradingDate(currentDate = new Date()) {
731
- const date = marketTimeCalculator.getLastFullTradingDate(currentDate);
732
- return {
733
- date,
734
- YYYYMMDD: timeFormatter.getTradingDate(date),
735
- };
708
+ return getLastFullTradingDateImpl(currentDate);
736
709
  }
737
710
  /**
738
- * Get the next market day
711
+ * Returns the next market day after the reference date.
712
+ * @param referenceDate - Date object (default: now)
713
+ * @returns Object with date, yyyymmdd string, and ISO string
739
714
  */
740
715
  function getNextMarketDay({ referenceDate } = {}) {
716
+ const calendar = new MarketCalendar();
741
717
  const startDate = referenceDate || new Date();
742
- const nextDate = marketCalendar.getNextMarketDay(startDate);
743
- // Convert to start of day in NY time
744
- const startOfDayNY = startOfDay(toZonedTime(nextDate, MARKET_CONFIG.TIMEZONE));
745
- const dateInET = fromZonedTime(startOfDayNY, MARKET_CONFIG.TIMEZONE);
718
+ const nextDate = calendar.getNextMarketDay(startDate);
719
+ const yyyymmdd = `${nextDate.getUTCFullYear()}-${String(nextDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nextDate.getUTCDate()).padStart(2, '0')}`;
746
720
  return {
747
- date: dateInET,
748
- yyyymmdd: timeFormatter.getTradingDate(dateInET),
749
- dateISOString: dateInET.toISOString(),
721
+ date: nextDate,
722
+ yyyymmdd,
723
+ dateISOString: nextDate.toISOString(),
750
724
  };
751
725
  }
752
726
  /**
753
- * Get trading date in YYYY-MM-DD format
727
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
728
+ * @param time - a string, number (unix timestamp), or Date object representing the time
729
+ * @returns the trading date as a string in YYYY-MM-DD format
730
+ */
731
+ /**
732
+ * Returns the trading date for a given time in YYYY-MM-DD format (NY time).
733
+ * @param time - string, number, or Date
734
+ * @returns trading date string
754
735
  */
755
736
  function getTradingDate(time) {
756
- return timeFormatter.getTradingDate(time);
737
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
738
+ const nyDate = toNYTime(date);
739
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
757
740
  }
758
741
  /**
759
- * Get New York timezone offset
742
+ * Returns the NY timezone offset string for a given date.
743
+ * @param date - Date object (default: now)
744
+ * @returns '-04:00' for EDT, '-05:00' for EST
760
745
  */
761
746
  function getNYTimeZone(date) {
762
- return timeFormatter.getNYTimeZone(date);
747
+ const offset = getNYOffset(date || new Date());
748
+ return offset === -4 ? '-04:00' : '-05:00';
763
749
  }
764
750
  /**
765
- * Get current market status
751
+ * Returns the current market status for a given date.
752
+ * @param options - { date?: Date }
753
+ * @returns MarketStatus object
766
754
  */
767
755
  function getMarketStatus(options = {}) {
768
756
  const { date = new Date() } = options;
769
- return marketStatusService.getMarketStatus(date);
757
+ return getMarketStatusImpl(date);
770
758
  }
771
759
  /**
772
- * Check if a date is a market day
760
+ * Checks if a date/time is within market hours, extended hours, or continuous.
761
+ * @param date - Date object
762
+ * @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
763
+ * @returns true if within hours, false otherwise
773
764
  */
774
- function isMarketDay(date) {
775
- return marketCalendar.isMarketDay(date);
765
+ function isWithinMarketHours(date, intradayReporting = 'market_hours') {
766
+ return isWithinMarketHoursImpl(date, intradayReporting);
776
767
  }
777
768
  /**
778
- * Check if a date is within market hours
769
+ * Returns full trading days from market open to market close.
770
+ * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
771
+ * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
779
772
  */
780
- function isWithinMarketHours(date, intradayReporting = 'market_hours') {
781
- return marketTimeCalculator.isWithinMarketHours(date, intradayReporting);
782
- }
783
773
  /**
784
- * Function to find complete trading date periods, starting at the beginning of one trading date and ending at the last.
785
- * By default, it gets the last trading date, returning the beginning and end. But we can also a) define the end date
786
- * 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),
787
- * 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).
788
- * @param options.endDate - The end date to use, defaults to today
789
- * @param options.days - The number of days to go back, defaults to 1
790
- * @returns The start and end dates with proper market open/close times
774
+ * Returns full trading days from market open to market close.
775
+ * @param options - { endDate?: Date, days?: number }
776
+ * @returns Object with startDate and endDate
791
777
  */
792
778
  function getTradingStartAndEndDates(options = {}) {
793
779
  const { endDate = new Date(), days = 1 } = options;
794
- // Get the last full trading date for the end date
795
- let endTradingDate;
796
- // If within market hours, on a trading day, use the current date as end
797
- if (isWithinMarketHours(endDate, 'market_hours')) {
798
- endTradingDate = endDate;
799
- }
800
- else {
801
- // If after market hours, use the last full trading date, which should be the previous trading day, or after extended hours, the same trading day
802
- const lastFullTradingDate = marketTimeCalculator.getLastFullTradingDate(endDate, false);
803
- endTradingDate = getMarketOpenClose({ date: lastFullTradingDate }).close;
804
- }
805
- let startDate;
806
- if (days <= 1) {
807
- // For 1 day, start is market open of the same trading day as end
808
- startDate = getMarketOpenClose({ date: endTradingDate }).open;
780
+ const calendar = new MarketCalendar();
781
+ // Find the most recent market close
782
+ let endMarketDay = endDate;
783
+ const nyEnd = toNYTime(endDate);
784
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
785
+ const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
786
+ const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
787
+ if (!calendar.isMarketDay(endDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
788
+ // Before market open, not a market day, or during market hours: use previous market day
789
+ endMarketDay = calendar.getPreviousMarketDay(endDate);
809
790
  }
810
791
  else {
811
- // For multiple days, go back the specified number of trading days
812
- let currentDate = new Date(endTradingDate);
813
- let tradingDaysBack = 0;
814
- // Count back trading days
815
- while (tradingDaysBack < days - 1) {
816
- currentDate = sub(currentDate, { days: 1 });
817
- if (isMarketDay(currentDate)) {
818
- tradingDaysBack++;
792
+ // After market close: use today
793
+ endMarketDay = endDate;
794
+ }
795
+ // Get market close for endMarketDay
796
+ const endClose = getMarketOpenClose({ date: endMarketDay }).close;
797
+ // Find start market day by iterating back over market days
798
+ let startMarketDay = endMarketDay;
799
+ let count = Math.max(1, days);
800
+ for (let i = 1; i < count; i++) {
801
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
802
+ }
803
+ // If days > 1, we need to go back (days-1) market days from endMarketDay
804
+ if (days > 1) {
805
+ startMarketDay = endMarketDay;
806
+ for (let i = 1; i < days; i++) {
807
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
808
+ }
809
+ }
810
+ const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
811
+ return { startDate: startOpen, endDate: endClose };
812
+ }
813
+ /**
814
+ * Counts trading time between two dates (passed as standard Date objects), excluding weekends and holidays, and closed market hours, using other functions in this library.
815
+ *
816
+ * This function calculates the actual trading time between two dates by:
817
+ * 1. Iterating through each calendar day between startDate and endDate (inclusive)
818
+ * 2. For each day that is a market day (not weekend/holiday), getting market open/close times
819
+ * 3. Calculating the overlap between the time range and market hours for that day
820
+ * 4. Summing up all the trading minutes across all days
821
+ *
822
+ * The function automatically handles:
823
+ * - Weekends (Saturday/Sunday) - skipped entirely
824
+ * - Market holidays - skipped entirely
825
+ * - Early close days (e.g. day before holidays) - uses early close time
826
+ * - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
827
+ *
828
+ * Examples:
829
+ * - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
830
+ * - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
831
+ * - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
832
+ *
833
+ * @param startDate - Start date/time
834
+ * @param endDate - End date/time (default: now)
835
+ * @returns Object containing:
836
+ * - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
837
+ * - hours: Trading time in hours
838
+ * - minutes: Trading time in minutes
839
+ */
840
+ function countTradingDays(startDate, endDate = new Date()) {
841
+ const calendar = new MarketCalendar();
842
+ // Ensure start is before end
843
+ if (startDate.getTime() > endDate.getTime()) {
844
+ throw new Error('Start date must be before end date');
845
+ }
846
+ let totalMinutes = 0;
847
+ // Get the NY dates for iteration
848
+ const startNY = toNYTime(startDate);
849
+ const endNY = toNYTime(endDate);
850
+ // Create date at start of first day (in NY time)
851
+ const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
852
+ // Iterate through each calendar day
853
+ while (currentNY.getTime() <= endNY.getTime()) {
854
+ const currentUTC = fromNYTime(currentNY);
855
+ // Check if this is a market day
856
+ if (calendar.isMarketDay(currentUTC)) {
857
+ // Get market hours for this day
858
+ const marketTimes = getMarketTimes(currentUTC);
859
+ if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
860
+ // Calculate the overlap between our time range and market hours
861
+ const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
862
+ const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
863
+ // Only count if there's actual overlap
864
+ if (dayStart < dayEnd) {
865
+ totalMinutes += (dayEnd - dayStart) / (1000 * 60);
866
+ }
819
867
  }
820
868
  }
821
- // Get the market open time for the start date
822
- startDate = getMarketOpenClose({ date: currentDate }).open;
869
+ // Move to next day
870
+ currentNY.setUTCDate(currentNY.getUTCDate() + 1);
823
871
  }
824
- return { startDate, endDate: endTradingDate };
872
+ // Convert to days, hours, minutes
873
+ const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
874
+ const days = totalMinutes / MINUTES_PER_TRADING_DAY;
875
+ const hours = totalMinutes / 60;
876
+ const minutes = totalMinutes;
877
+ return {
878
+ days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
879
+ hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
880
+ minutes: Math.round(minutes)
881
+ };
825
882
  }
826
- // Export the MARKET_TIMES constant for backward compatibility
883
+ // Export MARKET_TIMES for compatibility
827
884
  const MARKET_TIMES = {
828
885
  TIMEZONE: MARKET_CONFIG.TIMEZONE,
829
886
  PRE: {
@@ -1250,6 +1307,12 @@ class Queue {
1250
1307
  current = current.next;
1251
1308
  }
1252
1309
  }
1310
+
1311
+ * drain() {
1312
+ while (this.#head) {
1313
+ yield this.dequeue();
1314
+ }
1315
+ }
1253
1316
  }
1254
1317
 
1255
1318
  function pLimit(concurrency) {
@@ -1737,7 +1800,7 @@ symbol, date = new Date(), options) => {
1737
1800
  * @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
1738
1801
  */
1739
1802
  async function getPreviousClose(symbol, referenceDate, options) {
1740
- const previousDate = getLastFullTradingDate(referenceDate).date;
1803
+ const previousDate = getLastFullTradingDate(referenceDate);
1741
1804
  const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
1742
1805
  if (!lastOpenClose) {
1743
1806
  throw new Error(`Could not fetch last trade price for ${symbol}`);
@@ -2279,7 +2342,7 @@ const safeJSON = (text) => {
2279
2342
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2280
2343
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2281
2344
 
2282
- const VERSION = '5.10.1'; // x-release-please-version
2345
+ const VERSION = '5.10.2'; // x-release-please-version
2283
2346
 
2284
2347
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2285
2348
  const isRunningInBrowser = () => {
@@ -15399,7 +15462,7 @@ var config = {};
15399
15462
 
15400
15463
  var main = {exports: {}};
15401
15464
 
15402
- var version = "17.2.0";
15465
+ var version = "17.2.1";
15403
15466
  var require$$4 = {
15404
15467
  version: version};
15405
15468
 
@@ -15418,9 +15481,12 @@ function requireMain () {
15418
15481
 
15419
15482
  // Array of tips to display randomly
15420
15483
  const TIPS = [
15421
- '🔐 encrypt with dotenvx: https://dotenvx.com',
15484
+ '🔐 encrypt with Dotenvx: https://dotenvx.com',
15422
15485
  '🔐 prevent committing .env to code: https://dotenvx.com/precommit',
15423
15486
  '🔐 prevent building .env in docker: https://dotenvx.com/prebuild',
15487
+ '📡 observe env with Radar: https://dotenvx.com/radar',
15488
+ '📡 auto-backup env with Radar: https://dotenvx.com/radar',
15489
+ '📡 version env with Radar: https://dotenvx.com/radar',
15424
15490
  '🛠️ run anywhere with `dotenvx run -- yourcommand`',
15425
15491
  '⚙️ specify custom .env file path with { path: \'/custom/path/.env\' }',
15426
15492
  '⚙️ enable debug logging with { debug: true }',
@@ -15721,7 +15787,7 @@ function requireMain () {
15721
15787
  }
15722
15788
  }
15723
15789
 
15724
- _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`(tip: ${_getRandomTip()})`)}`);
15790
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(',')} ${dim(`-- tip: ${_getRandomTip()}`)}`);
15725
15791
  }
15726
15792
 
15727
15793
  if (lastError) {
@@ -16318,8 +16384,8 @@ class AlpacaMarketDataAPI extends EventEmitter {
16318
16384
  const response = await this.getHistoricalBars({
16319
16385
  symbols: [symbol],
16320
16386
  timeframe: '1Day',
16321
- start: prevMarketDate.date.toISOString(),
16322
- end: prevMarketDate.date.toISOString(),
16387
+ start: prevMarketDate.toISOString(),
16388
+ end: prevMarketDate.toISOString(),
16323
16389
  limit: 1,
16324
16390
  });
16325
16391
  if (!response.bars[symbol] || response.bars[symbol].length === 0) {
@@ -18206,6 +18272,7 @@ const disco = {
18206
18272
  getNYTimeZone: getNYTimeZone,
18207
18273
  getTradingDate: getTradingDate,
18208
18274
  getTradingStartAndEndDates: getTradingStartAndEndDates,
18275
+ countTradingDays: countTradingDays,
18209
18276
  MARKET_TIMES: MARKET_TIMES,
18210
18277
  },
18211
18278
  utils: {