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