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