@discomedia/utils 1.0.18 → 1.0.19

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,97 @@ const MARKET_CONFIG = {
163
159
  EARLY_EXTENDED_END: { hour: 17, minute: 0 },
164
160
  },
165
161
  };
166
- // ===== MARKET CALENDAR SERVICE =====
167
- /**
168
- * Service for handling market calendar operations (holidays, early closes, market days)
169
- */
170
- class MarketCalendar {
171
- timezone;
172
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
173
- this.timezone = timezone;
162
+ // Helper: Get NY offset for a given UTC date (DST rules for US)
163
+ function getNYOffset(date) {
164
+ // US DST starts 2nd Sunday in March, ends 1st Sunday in November
165
+ const year = date.getUTCFullYear();
166
+ const dstStart = getNthWeekdayOfMonth(year, 3, 0, 2); // March, Sunday, 2nd
167
+ const dstEnd = getNthWeekdayOfMonth(year, 11, 0, 1); // November, Sunday, 1st
168
+ const utcTime = date.getTime();
169
+ if (utcTime >= dstStart.getTime() && utcTime < dstEnd.getTime()) {
170
+ return MARKET_CONFIG.UTC_OFFSET_DST;
171
+ }
172
+ return MARKET_CONFIG.UTC_OFFSET_STANDARD;
173
+ }
174
+ // Helper: Get nth weekday of month in UTC
175
+ function getNthWeekdayOfMonth(year, month, weekday, n) {
176
+ let count = 0;
177
+ for (let d = 1; d <= 31; d++) {
178
+ const date = new Date(Date.UTC(year, month - 1, d));
179
+ if (date.getUTCMonth() !== month - 1)
180
+ break;
181
+ if (date.getUTCDay() === weekday) {
182
+ count++;
183
+ if (count === n)
184
+ return date;
185
+ }
174
186
  }
175
- /**
176
- * Check if a date is a weekend
177
- */
187
+ // fallback: last day of month
188
+ return new Date(Date.UTC(year, month - 1, 28));
189
+ }
190
+ // Helper: Convert UTC date to NY time (returns new Date object)
191
+ function toNYTime(date) {
192
+ const offset = getNYOffset(date);
193
+ // NY offset in hours
194
+ const utcMillis = date.getTime();
195
+ const nyMillis = utcMillis + offset * 60 * 60 * 1000;
196
+ return new Date(nyMillis);
197
+ }
198
+ // Helper: Convert NY time to UTC (returns new Date object)
199
+ function fromNYTime(date) {
200
+ const offset = getNYOffset(date);
201
+ const nyMillis = date.getTime();
202
+ const utcMillis = nyMillis - offset * 60 * 60 * 1000;
203
+ return new Date(utcMillis);
204
+ }
205
+ // Helper: Format date in ISO, unix, etc.
206
+ function formatDate(date, outputFormat = 'iso') {
207
+ switch (outputFormat) {
208
+ case 'unix-seconds':
209
+ return Math.floor(date.getTime() / 1000);
210
+ case 'unix-ms':
211
+ return date.getTime();
212
+ case 'iso':
213
+ default:
214
+ return date.toISOString();
215
+ }
216
+ }
217
+ // Helper: Format date in NY locale string
218
+ function formatNYLocale(date) {
219
+ return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
220
+ }
221
+ // Market calendar logic
222
+ class MarketCalendar {
178
223
  isWeekend(date) {
179
- const day = date.getDay();
180
- return day === 0 || day === 6; // Sunday or Saturday
224
+ const day = toNYTime(date).getUTCDay();
225
+ return day === 0 || day === 6;
181
226
  }
182
- /**
183
- * Check if a date is a market holiday
184
- */
185
227
  isHoliday(date) {
186
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
187
- const year = toZonedTime(date, this.timezone).getFullYear();
228
+ const nyDate = toNYTime(date);
229
+ const year = nyDate.getUTCFullYear();
230
+ const month = nyDate.getUTCMonth() + 1;
231
+ const day = nyDate.getUTCDate();
232
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
188
233
  const yearHolidays = marketHolidays[year];
189
234
  if (!yearHolidays)
190
235
  return false;
191
236
  return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
192
237
  }
193
- /**
194
- * Check if a date is an early close day
195
- */
196
238
  isEarlyCloseDay(date) {
197
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
198
- const year = toZonedTime(date, this.timezone).getFullYear();
239
+ const nyDate = toNYTime(date);
240
+ const year = nyDate.getUTCFullYear();
241
+ const month = nyDate.getUTCMonth() + 1;
242
+ const day = nyDate.getUTCDate();
243
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
199
244
  const yearEarlyCloses = marketEarlyCloses[year];
200
245
  return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
201
246
  }
202
- /**
203
- * Get the early close time for a date (in minutes from midnight)
204
- */
205
247
  getEarlyCloseTime(date) {
206
- const formattedDate = formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
207
- const year = toZonedTime(date, this.timezone).getFullYear();
248
+ const nyDate = toNYTime(date);
249
+ const year = nyDate.getUTCFullYear();
250
+ const month = nyDate.getUTCMonth() + 1;
251
+ const day = nyDate.getUTCDate();
252
+ const formattedDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
208
253
  const yearEarlyCloses = marketEarlyCloses[year];
209
254
  if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
210
255
  const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
@@ -212,618 +257,404 @@ class MarketCalendar {
212
257
  }
213
258
  return null;
214
259
  }
215
- /**
216
- * Check if a date is a market day (not weekend or holiday)
217
- */
218
260
  isMarketDay(date) {
219
261
  return !this.isWeekend(date) && !this.isHoliday(date);
220
262
  }
221
- /**
222
- * Get the next market day from a given date
223
- */
224
263
  getNextMarketDay(date) {
225
- let nextDay = add(date, { days: 1 });
264
+ let nextDay = new Date(date.getTime() + 24 * 60 * 60 * 1000);
226
265
  while (!this.isMarketDay(nextDay)) {
227
- nextDay = add(nextDay, { days: 1 });
266
+ nextDay = new Date(nextDay.getTime() + 24 * 60 * 60 * 1000);
228
267
  }
229
268
  return nextDay;
230
269
  }
231
- /**
232
- * Get the previous market day from a given date
233
- */
234
270
  getPreviousMarketDay(date) {
235
- let prevDay = sub(date, { days: 1 });
271
+ let prevDay = new Date(date.getTime() - 24 * 60 * 60 * 1000);
236
272
  while (!this.isMarketDay(prevDay)) {
237
- prevDay = sub(prevDay, { days: 1 });
273
+ prevDay = new Date(prevDay.getTime() - 24 * 60 * 60 * 1000);
238
274
  }
239
275
  return prevDay;
240
276
  }
241
277
  }
242
- // ===== TIME FORMATTER SERVICE =====
243
- /**
244
- * Service for formatting time outputs
245
- */
246
- class TimeFormatter {
247
- timezone;
248
- constructor(timezone = MARKET_CONFIG.TIMEZONE) {
249
- this.timezone = timezone;
250
- }
251
- /**
252
- * Format a date based on the output format
253
- */
254
- formatDate(date, outputFormat = 'iso') {
255
- switch (outputFormat) {
256
- case 'unix-seconds':
257
- return Math.floor(date.getTime() / 1000);
258
- case 'unix-ms':
259
- return date.getTime();
260
- case 'iso':
261
- default:
262
- return formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
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
- }
288
- }
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');
304
- }
305
- }
306
- // ===== MARKET TIME CALCULATOR =====
307
- /**
308
- * Service for core market time calculations
309
- */
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
- }
278
+ // Market open/close times
279
+ function getMarketTimes(date) {
280
+ const calendar = new MarketCalendar();
281
+ const nyDate = toNYTime(date);
282
+ if (!calendar.isMarketDay(date)) {
344
283
  return {
345
- marketOpen: true,
346
- open,
347
- close,
348
- openExt,
349
- closeExt,
284
+ marketOpen: false,
285
+ open: null,
286
+ close: null,
287
+ openExt: null,
288
+ closeExt: null,
350
289
  };
351
290
  }
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;
383
- }
384
- }
291
+ const year = nyDate.getUTCFullYear();
292
+ const month = nyDate.getUTCMonth();
293
+ const day = nyDate.getUTCDate();
294
+ // Helper to build NY time for a given hour/minute
295
+ function buildNYTime(hour, minute) {
296
+ const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
297
+ return fromNYTime(d);
298
+ }
299
+ let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
300
+ let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
301
+ let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
302
+ let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
303
+ if (calendar.isEarlyCloseDay(date)) {
304
+ close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
305
+ closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
385
306
  }
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
- }
307
+ return {
308
+ marketOpen: true,
309
+ open,
310
+ close,
311
+ openExt,
312
+ closeExt,
313
+ };
314
+ }
315
+ // Is within market hours
316
+ function isWithinMarketHoursImpl(date, intradayReporting = 'market_hours') {
317
+ const calendar = new MarketCalendar();
318
+ if (!calendar.isMarketDay(date))
319
+ return false;
320
+ const nyDate = toNYTime(date);
321
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
322
+ switch (intradayReporting) {
323
+ case 'extended_hours': {
324
+ let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
325
+ if (calendar.isEarlyCloseDay(date)) {
326
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
405
327
  }
406
- if (timeInMinutes >= endMinutes) {
407
- return fromZonedTime(set(nowET, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
328
+ const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
329
+ return minutes >= startMinutes && minutes <= endMinutes;
330
+ }
331
+ case 'continuous':
332
+ return true;
333
+ default: {
334
+ let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
335
+ if (calendar.isEarlyCloseDay(date)) {
336
+ endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
408
337
  }
338
+ const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
339
+ return minutes >= startMinutes && minutes <= endMinutes;
409
340
  }
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
341
  }
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;
342
+ }
343
+ // Get last full trading date
344
+ function getLastFullTradingDateImpl(currentDate = new Date()) {
345
+ const calendar = new MarketCalendar();
346
+ const nyDate = toNYTime(currentDate);
347
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
348
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
349
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
350
+ if (calendar.isEarlyCloseDay(currentDate)) {
351
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
352
+ }
353
+ // If not a market day, or before open, or during market hours, return previous market day's close
354
+ if (!calendar.isMarketDay(currentDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
355
+ const prevMarketDay = calendar.getPreviousMarketDay(currentDate);
356
+ let prevCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
357
+ if (calendar.isEarlyCloseDay(prevMarketDay)) {
358
+ prevCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
359
+ }
360
+ const year = prevMarketDay.getUTCFullYear();
361
+ const month = prevMarketDay.getUTCMonth();
362
+ const day = prevMarketDay.getUTCDate();
363
+ const closeHour = Math.floor(prevCloseMinutes / 60);
364
+ const closeMinute = prevCloseMinutes % 60;
365
+ return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
366
+ }
367
+ // After market close or after extended hours, return today's close
368
+ const year = nyDate.getUTCFullYear();
369
+ const month = nyDate.getUTCMonth();
370
+ const day = nyDate.getUTCDate();
371
+ const closeHour = Math.floor(marketCloseMinutes / 60);
372
+ const closeMinute = marketCloseMinutes % 60;
373
+ return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
374
+ }
375
+ // Get day boundaries
376
+ function getDayBoundaries(date, intradayReporting = 'market_hours') {
377
+ const calendar = new MarketCalendar();
378
+ const nyDate = toNYTime(date);
379
+ const year = nyDate.getUTCFullYear();
380
+ const month = nyDate.getUTCMonth();
381
+ const day = nyDate.getUTCDate();
382
+ function buildNYTime(hour, minute, sec = 0, ms = 0) {
383
+ const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
384
+ return fromNYTime(d);
385
+ }
386
+ let start;
387
+ let end;
388
+ switch (intradayReporting) {
389
+ case 'extended_hours':
390
+ start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
391
+ end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
392
+ if (calendar.isEarlyCloseDay(date)) {
393
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
450
394
  }
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;
395
+ break;
396
+ case 'continuous':
397
+ start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
398
+ end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
399
+ break;
400
+ default:
401
+ start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
402
+ end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
403
+ if (calendar.isEarlyCloseDay(date)) {
404
+ end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
475
405
  }
476
- }
477
- return {
478
- start: fromZonedTime(start, this.timezone),
479
- end: fromZonedTime(end, this.timezone),
480
- };
406
+ break;
481
407
  }
408
+ return { start, end };
482
409
  }
483
- // ===== PERIOD CALCULATOR =====
484
- /**
485
- * Service for calculating time periods
486
- */
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);
410
+ // Period calculator
411
+ function calculatePeriodStartDate(endDate, period) {
412
+ const calendar = new MarketCalendar();
413
+ let startDate;
414
+ switch (period) {
415
+ case 'YTD':
416
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
417
+ break;
418
+ case '1D':
419
+ startDate = calendar.getPreviousMarketDay(endDate);
420
+ break;
421
+ case '3D':
422
+ startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
423
+ break;
424
+ case '1W':
425
+ startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
426
+ break;
427
+ case '2W':
428
+ startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
429
+ break;
430
+ case '1M':
431
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
432
+ break;
433
+ case '3M':
434
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
435
+ break;
436
+ case '6M':
437
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
438
+ break;
439
+ case '1Y':
440
+ startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
441
+ break;
442
+ default:
443
+ throw new Error(`Invalid period: ${period}`);
495
444
  }
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;
445
+ // Ensure start date is a market day
446
+ while (!calendar.isMarketDay(startDate)) {
447
+ startDate = calendar.getNextMarketDay(startDate);
537
448
  }
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
- }
449
+ return startDate;
450
+ }
451
+ // Get market time period
452
+ function getMarketTimePeriod(params) {
453
+ const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
454
+ if (!period)
455
+ throw new Error('Period is required');
456
+ const calendar = new MarketCalendar();
457
+ const nyEndDate = toNYTime(end);
458
+ let endDate;
459
+ const isCurrentMarketDay = calendar.isMarketDay(end);
460
+ const isWithinHours = isWithinMarketHours(end, intraday_reporting);
461
+ const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
462
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
463
+ if (isCurrentMarketDay) {
464
+ if (minutes < marketStartMinutes) {
465
+ // Before market open - use previous day's close
466
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
467
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
468
+ endDate = dayEnd;
469
+ }
470
+ else if (isWithinHours) {
471
+ // During market hours - use current time
472
+ endDate = end;
569
473
  }
570
474
  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);
475
+ // After market close - use today's close
476
+ const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
574
477
  endDate = dayEnd;
575
478
  }
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
- };
587
479
  }
480
+ else {
481
+ // Not a market day - use previous market day's close
482
+ const lastMarketDay = calendar.getPreviousMarketDay(end);
483
+ const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
484
+ endDate = dayEnd;
485
+ }
486
+ // Calculate start date
487
+ const periodStartDate = calculatePeriodStartDate(endDate, period);
488
+ const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
489
+ if (endDate.getTime() < dayStart.getTime()) {
490
+ throw new Error('Start date cannot be after end date');
491
+ }
492
+ return {
493
+ start: formatDate(dayStart, outputFormat),
494
+ end: formatDate(endDate, outputFormat),
495
+ };
588
496
  }
589
- // ===== MARKET STATUS SERVICE =====
590
- /**
591
- * Service for determining market status
592
- */
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);
497
+ // Market status
498
+ function getMarketStatusImpl(date = new Date()) {
499
+ const calendar = new MarketCalendar();
500
+ const nyDate = toNYTime(date);
501
+ const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
502
+ const isMarketDay = calendar.isMarketDay(date);
503
+ const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
504
+ const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
505
+ const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
506
+ const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
507
+ let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
508
+ let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
509
+ if (isEarlyCloseDay) {
510
+ marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
511
+ extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
512
+ }
513
+ let status;
514
+ let nextStatus;
515
+ let nextStatusTime;
516
+ let marketPeriod;
517
+ if (!isMarketDay) {
518
+ status = 'closed';
519
+ nextStatus = 'extended hours';
520
+ marketPeriod = 'closed';
521
+ const nextMarketDay = calendar.getNextMarketDay(date);
522
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
523
+ }
524
+ else if (minutes < extendedStartMinutes) {
525
+ status = 'closed';
526
+ nextStatus = 'extended hours';
527
+ marketPeriod = 'closed';
528
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
529
+ }
530
+ else if (minutes < marketStartMinutes) {
531
+ status = 'extended hours';
532
+ nextStatus = 'open';
533
+ marketPeriod = 'preMarket';
534
+ nextStatusTime = getDayBoundaries(date, 'market_hours').start;
535
+ }
536
+ else if (minutes < marketCloseMinutes) {
537
+ status = 'open';
538
+ nextStatus = 'extended hours';
539
+ marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
540
+ nextStatusTime = getDayBoundaries(date, 'market_hours').end;
541
+ }
542
+ else if (minutes < extendedEndMinutes) {
543
+ status = 'extended hours';
544
+ nextStatus = 'closed';
545
+ marketPeriod = 'afterMarket';
546
+ nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
601
547
  }
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
- });
654
- }
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
- });
674
- }
675
- 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
- });
685
- }
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
- };
548
+ else {
549
+ status = 'closed';
550
+ nextStatus = 'extended hours';
551
+ marketPeriod = 'closed';
552
+ const nextMarketDay = calendar.getNextMarketDay(date);
553
+ nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
698
554
  }
555
+ const nextStatusTimeDifference = nextStatusTime.getTime() - nyDate.getTime();
556
+ return {
557
+ time: date,
558
+ timeString: formatNYLocale(nyDate),
559
+ status,
560
+ nextStatus,
561
+ marketPeriod,
562
+ nextStatusTime,
563
+ nextStatusTimeDifference,
564
+ nextStatusTimeString: formatNYLocale(nextStatusTime),
565
+ };
699
566
  }
700
- // ===== FUNCTIONAL API =====
701
- /**
702
- * Simple functional API that uses the services above
703
- */
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();
710
- /**
711
- * Get market open/close times for a given date
712
- */
567
+ // API exports
713
568
  function getMarketOpenClose(options = {}) {
714
569
  const { date = new Date() } = options;
715
- return marketTimeCalculator.getMarketTimes(date);
570
+ return getMarketTimes(date);
716
571
  }
717
- /**
718
- * Get start and end dates for a given market time period
719
- */
720
572
  function getStartAndEndDates(params = {}) {
721
- const { start, end } = periodCalculator.getMarketTimePeriod(params);
573
+ const { start, end } = getMarketTimePeriod(params);
722
574
  return {
723
- start: new Date(start),
724
- end: new Date(end),
575
+ start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
576
+ end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
725
577
  };
726
578
  }
727
579
  /**
728
- * Get the last full trading date
580
+ * Returns the last full trading date as a Date object.
729
581
  */
730
582
  function getLastFullTradingDate(currentDate = new Date()) {
731
- const date = marketTimeCalculator.getLastFullTradingDate(currentDate);
732
- return {
733
- date,
734
- YYYYMMDD: timeFormatter.getTradingDate(date),
735
- };
583
+ return getLastFullTradingDateImpl(currentDate);
736
584
  }
737
- /**
738
- * Get the next market day
739
- */
740
585
  function getNextMarketDay({ referenceDate } = {}) {
586
+ const calendar = new MarketCalendar();
741
587
  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);
588
+ const nextDate = calendar.getNextMarketDay(startDate);
589
+ const yyyymmdd = `${nextDate.getUTCFullYear()}-${String(nextDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nextDate.getUTCDate()).padStart(2, '0')}`;
746
590
  return {
747
- date: dateInET,
748
- yyyymmdd: timeFormatter.getTradingDate(dateInET),
749
- dateISOString: dateInET.toISOString(),
591
+ date: nextDate,
592
+ yyyymmdd,
593
+ dateISOString: nextDate.toISOString(),
750
594
  };
751
595
  }
752
596
  /**
753
- * Get trading date in YYYY-MM-DD format
597
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
598
+ * @param time - a string, number (unix timestamp), or Date object representing the time
599
+ * @returns the trading date as a string in YYYY-MM-DD format
754
600
  */
755
601
  function getTradingDate(time) {
756
- return timeFormatter.getTradingDate(time);
602
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
603
+ const nyDate = toNYTime(date);
604
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
757
605
  }
758
- /**
759
- * Get New York timezone offset
760
- */
761
606
  function getNYTimeZone(date) {
762
- return timeFormatter.getNYTimeZone(date);
607
+ const offset = getNYOffset(date || new Date());
608
+ return offset === -4 ? '-04:00' : '-05:00';
763
609
  }
764
- /**
765
- * Get current market status
766
- */
767
610
  function getMarketStatus(options = {}) {
768
611
  const { date = new Date() } = options;
769
- return marketStatusService.getMarketStatus(date);
612
+ return getMarketStatusImpl(date);
770
613
  }
771
- /**
772
- * Check if a date is a market day
773
- */
774
- function isMarketDay(date) {
775
- return marketCalendar.isMarketDay(date);
776
- }
777
- /**
778
- * Check if a date is within market hours
779
- */
780
614
  function isWithinMarketHours(date, intradayReporting = 'market_hours') {
781
- return marketTimeCalculator.isWithinMarketHours(date, intradayReporting);
615
+ return isWithinMarketHoursImpl(date, intradayReporting);
782
616
  }
783
617
  /**
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
618
+ * Returns full trading days from market open to market close.
619
+ * endDate is always the most recent market close (previous day's close if before open, today's close if after open).
620
+ * days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
791
621
  */
792
622
  function getTradingStartAndEndDates(options = {}) {
793
623
  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;
624
+ const calendar = new MarketCalendar();
625
+ // Find the most recent market close
626
+ let endMarketDay = endDate;
627
+ const nyEnd = toNYTime(endDate);
628
+ const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
629
+ const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
630
+ const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
631
+ if (!calendar.isMarketDay(endDate) || minutes < marketOpenMinutes || (minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
632
+ // Before market open, not a market day, or during market hours: use previous market day
633
+ endMarketDay = calendar.getPreviousMarketDay(endDate);
809
634
  }
810
635
  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++;
819
- }
820
- }
821
- // Get the market open time for the start date
822
- startDate = getMarketOpenClose({ date: currentDate }).open;
823
- }
824
- return { startDate, endDate: endTradingDate };
636
+ // After market close: use today
637
+ endMarketDay = endDate;
638
+ }
639
+ // Get market close for endMarketDay
640
+ const endClose = getMarketOpenClose({ date: endMarketDay }).close;
641
+ // Find start market day by iterating back over market days
642
+ let startMarketDay = endMarketDay;
643
+ let count = Math.max(1, days);
644
+ for (let i = 1; i < count; i++) {
645
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
646
+ }
647
+ // If days > 1, we need to go back (days-1) market days from endMarketDay
648
+ if (days > 1) {
649
+ startMarketDay = endMarketDay;
650
+ for (let i = 1; i < days; i++) {
651
+ startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
652
+ }
653
+ }
654
+ const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
655
+ return { startDate: startOpen, endDate: endClose };
825
656
  }
826
- // Export the MARKET_TIMES constant for backward compatibility
657
+ // Export MARKET_TIMES for compatibility
827
658
  const MARKET_TIMES = {
828
659
  TIMEZONE: MARKET_CONFIG.TIMEZONE,
829
660
  PRE: {
@@ -1737,7 +1568,7 @@ symbol, date = new Date(), options) => {
1737
1568
  * @returns {Promise<{ close: number; date: Date }>} The previous close price and date.
1738
1569
  */
1739
1570
  async function getPreviousClose(symbol, referenceDate, options) {
1740
- const previousDate = getLastFullTradingDate(referenceDate).date;
1571
+ const previousDate = getLastFullTradingDate(referenceDate);
1741
1572
  const lastOpenClose = await fetchDailyOpenClose(symbol, previousDate, options);
1742
1573
  if (!lastOpenClose) {
1743
1574
  throw new Error(`Could not fetch last trade price for ${symbol}`);
@@ -16318,8 +16149,8 @@ class AlpacaMarketDataAPI extends EventEmitter {
16318
16149
  const response = await this.getHistoricalBars({
16319
16150
  symbols: [symbol],
16320
16151
  timeframe: '1Day',
16321
- start: prevMarketDate.date.toISOString(),
16322
- end: prevMarketDate.date.toISOString(),
16152
+ start: prevMarketDate.toISOString(),
16153
+ end: prevMarketDate.toISOString(),
16323
16154
  limit: 1,
16324
16155
  });
16325
16156
  if (!response.bars[symbol] || response.bars[symbol].length === 0) {