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