@discomedia/utils 1.0.3 → 1.0.6
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 +651 -903
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +653 -903
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +50 -0
- package/dist/test.js +755 -6495
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-market-data-api.d.ts +2 -15
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +3 -6
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -28
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/market-time.d.ts +187 -117
- package/dist/types/market-time.d.ts.map +1 -1
- package/dist/types/testing/market-time-refactor-test.d.ts +1 -0
- package/dist/types/testing/market-time-refactor-test.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/index-frontend.cjs +0 -7798
- package/dist/index-frontend.cjs.map +0 -1
- package/dist/index-frontend.mjs +0 -7792
- package/dist/index-frontend.mjs.map +0 -1
- package/dist/types/index-frontend.d.ts +0 -18
- package/dist/types/index-frontend.d.ts.map +0 -1
- package/dist/types/testing/frontend-test.d.ts +0 -2
- package/dist/types/testing/frontend-test.d.ts.map +0 -1
- package/dist/types/time-utils.d.ts +0 -17
- package/dist/types/time-utils.d.ts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var ms = require('ms');
|
|
4
|
-
var dateFnsTz = require('date-fns-tz');
|
|
5
3
|
var dateFns = require('date-fns');
|
|
4
|
+
var dateFnsTz = require('date-fns-tz');
|
|
6
5
|
var require$$0$3 = require('events');
|
|
7
6
|
var require$$1$1 = require('https');
|
|
8
7
|
var require$$2 = require('http');
|
|
@@ -14,170 +13,34 @@ var require$$7 = require('url');
|
|
|
14
13
|
var require$$0 = require('zlib');
|
|
15
14
|
var require$$0$1 = require('buffer');
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const date = new Date(
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (years > 0) {
|
|
38
|
-
return years === 1 ? '1 year ago' : `${years} years ago`;
|
|
39
|
-
}
|
|
40
|
-
else if (months > 0) {
|
|
41
|
-
return months === 1 ? '1 month ago' : `${months} months ago`;
|
|
42
|
-
}
|
|
43
|
-
else if (days > 0) {
|
|
44
|
-
return days === 1 ? '1 day ago' : `${days} days ago`;
|
|
45
|
-
}
|
|
46
|
-
else if (hours > 0) {
|
|
47
|
-
return hours === 1 ? '1 hr ago' : `${hours} hrs ago`;
|
|
48
|
-
}
|
|
49
|
-
else if (minutes > 0) {
|
|
50
|
-
return minutes === 1 ? '1 min ago' : `${minutes} mins ago`;
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
return 'A few seconds ago';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function normalizeDate(timestamp) {
|
|
57
|
-
const date = new Date(timestamp);
|
|
58
|
-
return date.toISOString().split('T')[0]; // Returns 'YYYY-MM-DD'
|
|
59
|
-
}
|
|
60
|
-
// the function formerly known as CalculateRange, like a camel with two humps. Gross
|
|
61
|
-
function calculateTimeRange(range) {
|
|
62
|
-
const currentDate = new Date();
|
|
63
|
-
switch (range) {
|
|
64
|
-
case '1d':
|
|
65
|
-
currentDate.setDate(currentDate.getDate() - 1);
|
|
66
|
-
break;
|
|
67
|
-
case '3d':
|
|
68
|
-
currentDate.setDate(currentDate.getDate() - 3);
|
|
69
|
-
break;
|
|
70
|
-
case '1w':
|
|
71
|
-
currentDate.setDate(currentDate.getDate() - 7);
|
|
72
|
-
break;
|
|
73
|
-
case '1m':
|
|
74
|
-
currentDate.setMonth(currentDate.getMonth() - 1);
|
|
75
|
-
break;
|
|
76
|
-
case '3m':
|
|
77
|
-
currentDate.setMonth(currentDate.getMonth() - 3);
|
|
78
|
-
break;
|
|
79
|
-
case '1y':
|
|
80
|
-
currentDate.setFullYear(currentDate.getFullYear() - 1);
|
|
81
|
-
break;
|
|
82
|
-
default:
|
|
83
|
-
throw new Error(`Invalid range: ${range}`);
|
|
16
|
+
/**
|
|
17
|
+
* Logs a message to the console.
|
|
18
|
+
* @param message The message to log.
|
|
19
|
+
* @param options Optional options.
|
|
20
|
+
* @param options.source The source of the message.
|
|
21
|
+
* @param options.type The type of message to log.
|
|
22
|
+
* @param options.symbol The trading symbol associated with this log.
|
|
23
|
+
* @param options.account The account associated with this log.
|
|
24
|
+
*/
|
|
25
|
+
function log$1(message, options = { source: 'App', type: 'info' }) {
|
|
26
|
+
// Format the timestamp
|
|
27
|
+
const date = new Date();
|
|
28
|
+
const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
|
|
29
|
+
const account = options?.account;
|
|
30
|
+
const symbol = options?.symbol;
|
|
31
|
+
// Build the log message
|
|
32
|
+
const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
|
|
33
|
+
// Use appropriate console method based on type
|
|
34
|
+
if (options?.type === 'error') {
|
|
35
|
+
console.error(logMessage);
|
|
84
36
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const daysLeft = (accountCreationDate, maxDays) => {
|
|
88
|
-
const now = new Date();
|
|
89
|
-
const endPeriodDate = new Date(accountCreationDate);
|
|
90
|
-
endPeriodDate.setDate(accountCreationDate.getDate() + maxDays);
|
|
91
|
-
const diffInMilliseconds = endPeriodDate.getTime() - now.getTime();
|
|
92
|
-
// Convert milliseconds to days and return
|
|
93
|
-
return Math.ceil(diffInMilliseconds / (1000 * 60 * 60 * 24));
|
|
94
|
-
};
|
|
95
|
-
const cutoffDate = new Date('2023-10-17T00:00:00.000Z');
|
|
96
|
-
const calculateDaysLeft = (accountCreationDate) => {
|
|
97
|
-
let maxDays;
|
|
98
|
-
if (accountCreationDate < cutoffDate) {
|
|
99
|
-
maxDays = 30;
|
|
100
|
-
accountCreationDate = new Date('2023-10-01T00:00:00.000Z');
|
|
37
|
+
else if (options?.type === 'warn') {
|
|
38
|
+
console.warn(logMessage);
|
|
101
39
|
}
|
|
102
40
|
else {
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
return daysLeft(accountCreationDate, maxDays);
|
|
106
|
-
};
|
|
107
|
-
const timeAgo = (timestamp) => {
|
|
108
|
-
if (!timestamp)
|
|
109
|
-
return 'Just now';
|
|
110
|
-
const diff = Date.now() - new Date(timestamp).getTime();
|
|
111
|
-
if (diff < 60000) {
|
|
112
|
-
// less than 1 second
|
|
113
|
-
return 'Just now';
|
|
114
|
-
}
|
|
115
|
-
else if (diff > 82800000) {
|
|
116
|
-
// more than 23 hours – similar to how Twitter displays timestamps
|
|
117
|
-
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
118
|
-
month: 'short',
|
|
119
|
-
day: 'numeric',
|
|
120
|
-
year: new Date(timestamp).getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return `${ms(diff)} ago`;
|
|
124
|
-
};
|
|
125
|
-
// returns date utc
|
|
126
|
-
const formatDate = (dateString, updateDate) => {
|
|
127
|
-
return new Date(dateString).toLocaleDateString('en-US', {
|
|
128
|
-
day: 'numeric',
|
|
129
|
-
month: 'long',
|
|
130
|
-
year: updateDate && new Date(dateString).getFullYear() === new Date().getFullYear() ? undefined : 'numeric',
|
|
131
|
-
timeZone: 'UTC',
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
const parseETDateFromAV = (dateString) => {
|
|
135
|
-
// Time zone identifier for Eastern Time
|
|
136
|
-
const timeZone = 'America/New_York';
|
|
137
|
-
// Split the input string into date and time components
|
|
138
|
-
const [datePart, timePart] = dateString.split(' ');
|
|
139
|
-
// Construct a full date-time string in ISO format
|
|
140
|
-
const fullString = `${datePart}T${timePart}`;
|
|
141
|
-
// Convert the string to a UTC Date object using date-fns-tz
|
|
142
|
-
const utcDate = dateFnsTz.fromZonedTime(fullString, timeZone); // Convert to UTC
|
|
143
|
-
return utcDate;
|
|
144
|
-
};
|
|
145
|
-
const formatToUSEastern = (date, justDate) => {
|
|
146
|
-
const options = {
|
|
147
|
-
timeZone: 'America/New_York',
|
|
148
|
-
month: 'short',
|
|
149
|
-
day: 'numeric',
|
|
150
|
-
year: 'numeric',
|
|
151
|
-
};
|
|
152
|
-
if (!justDate) {
|
|
153
|
-
options.hour = 'numeric';
|
|
154
|
-
options.minute = '2-digit';
|
|
155
|
-
options.hour12 = true;
|
|
41
|
+
console.log(logMessage);
|
|
156
42
|
}
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
const unixTimetoUSEastern = (timestamp) => {
|
|
160
|
-
const date = new Date(timestamp);
|
|
161
|
-
const timeString = formatToUSEastern(date);
|
|
162
|
-
const dateString = formatToUSEastern(date, true);
|
|
163
|
-
return { date, timeString, dateString };
|
|
164
|
-
};
|
|
165
|
-
const timeDiffString = (milliseconds) => {
|
|
166
|
-
const seconds = Math.floor(milliseconds / 1000);
|
|
167
|
-
const minutes = Math.floor(seconds / 60);
|
|
168
|
-
const hours = Math.floor(minutes / 60);
|
|
169
|
-
const days = Math.floor(hours / 24);
|
|
170
|
-
const remainingHours = hours % 24;
|
|
171
|
-
const remainingMinutes = minutes % 60;
|
|
172
|
-
const parts = [];
|
|
173
|
-
if (days > 0)
|
|
174
|
-
parts.push(`${days} day${days > 1 ? 's' : ''}`);
|
|
175
|
-
if (remainingHours > 0)
|
|
176
|
-
parts.push(`${remainingHours} hour${remainingHours > 1 ? 's' : ''}`);
|
|
177
|
-
if (remainingMinutes > 0)
|
|
178
|
-
parts.push(`${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''}`);
|
|
179
|
-
return parts.join(', ');
|
|
180
|
-
};
|
|
43
|
+
}
|
|
181
44
|
|
|
182
45
|
// market-hours.ts
|
|
183
46
|
const marketHolidays = {
|
|
@@ -282,99 +145,66 @@ const marketEarlyCloses = {
|
|
|
282
145
|
},
|
|
283
146
|
};
|
|
284
147
|
|
|
285
|
-
// market-time.ts
|
|
148
|
+
// market-time.ts - Refactored for better organization and usability
|
|
149
|
+
// ===== CONFIGURATION =====
|
|
286
150
|
/**
|
|
287
|
-
* Market
|
|
288
|
-
* Regular market hours are 9:30am-4:00pm
|
|
289
|
-
* Early market hours are 9:30am-10:00am (first 30 minutes)
|
|
290
|
-
* Extended market hours are 4:00am to 9:30am and 4:00pm-8:00pm
|
|
291
|
-
* On days before some holidays, the market closes early at 1:00pm
|
|
292
|
-
* Early extended market hours are 1:00pm-5:00pm on early close days
|
|
151
|
+
* Market configuration constants
|
|
293
152
|
*/
|
|
294
|
-
const
|
|
153
|
+
const MARKET_CONFIG = {
|
|
295
154
|
TIMEZONE: 'America/New_York',
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}, // extended hours trading on early close days
|
|
306
|
-
REGULAR: { START: { HOUR: 9, MINUTE: 30, MINUTES: 570 }, END: { HOUR: 16, MINUTE: 0, MINUTES: 960 } },
|
|
307
|
-
EXTENDED: { START: { HOUR: 4, MINUTE: 0, MINUTES: 240 }, END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 } },
|
|
155
|
+
TIMES: {
|
|
156
|
+
EXTENDED_START: { hour: 4, minute: 0 },
|
|
157
|
+
MARKET_OPEN: { hour: 9, minute: 30 },
|
|
158
|
+
EARLY_MARKET_END: { hour: 10, minute: 0 },
|
|
159
|
+
MARKET_CLOSE: { hour: 16, minute: 0 },
|
|
160
|
+
EARLY_CLOSE: { hour: 13, minute: 0 },
|
|
161
|
+
EXTENDED_END: { hour: 20, minute: 0 },
|
|
162
|
+
EARLY_EXTENDED_END: { hour: 17, minute: 0 },
|
|
163
|
+
},
|
|
308
164
|
};
|
|
165
|
+
// ===== MARKET CALENDAR SERVICE =====
|
|
309
166
|
/**
|
|
310
|
-
*
|
|
167
|
+
* Service for handling market calendar operations (holidays, early closes, market days)
|
|
311
168
|
*/
|
|
312
|
-
class
|
|
169
|
+
class MarketCalendar {
|
|
313
170
|
timezone;
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Creates a new MarketTimeUtil instance
|
|
317
|
-
* @param {string} [timezone='America/New_York'] - The timezone to use for market time calculations
|
|
318
|
-
* @param {IntradayReporting} [intradayReporting='market_hours'] - The intraday reporting mode
|
|
319
|
-
*/
|
|
320
|
-
constructor(timezone = MARKET_TIMES.TIMEZONE, intradayReporting = 'market_hours') {
|
|
321
|
-
this.validateTimezone(timezone);
|
|
171
|
+
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
322
172
|
this.timezone = timezone;
|
|
323
|
-
this.intradayReporting = intradayReporting;
|
|
324
173
|
}
|
|
325
174
|
/**
|
|
326
|
-
*
|
|
327
|
-
* @private
|
|
328
|
-
* @param {string} timezone - The timezone to validate
|
|
329
|
-
* @throws {Error} If the timezone is invalid
|
|
175
|
+
* Check if a date is a weekend
|
|
330
176
|
*/
|
|
331
|
-
validateTimezone(timezone) {
|
|
332
|
-
try {
|
|
333
|
-
Intl.DateTimeFormat(undefined, { timeZone: timezone });
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
throw new Error(`Invalid timezone: ${timezone}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
formatDate(date, outputFormat = 'iso') {
|
|
340
|
-
switch (outputFormat) {
|
|
341
|
-
case 'unix-seconds':
|
|
342
|
-
return Math.floor(date.getTime() / 1000);
|
|
343
|
-
case 'unix-ms':
|
|
344
|
-
return date.getTime();
|
|
345
|
-
case 'iso':
|
|
346
|
-
default:
|
|
347
|
-
// return with timezone offset
|
|
348
|
-
return dateFnsTz.formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
177
|
isWeekend(date) {
|
|
352
178
|
const day = date.getDay();
|
|
353
|
-
return day === 0 || day === 6;
|
|
179
|
+
return day === 0 || day === 6; // Sunday or Saturday
|
|
354
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if a date is a market holiday
|
|
183
|
+
*/
|
|
355
184
|
isHoliday(date) {
|
|
356
|
-
const formattedDate =
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
return false;
|
|
185
|
+
const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
|
|
186
|
+
const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
|
|
187
|
+
const yearHolidays = marketHolidays[year];
|
|
188
|
+
if (!yearHolidays)
|
|
189
|
+
return false;
|
|
190
|
+
return Object.values(yearHolidays).some(holiday => holiday.date === formattedDate);
|
|
364
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if a date is an early close day
|
|
194
|
+
*/
|
|
365
195
|
isEarlyCloseDay(date) {
|
|
366
|
-
const formattedDate =
|
|
367
|
-
const
|
|
196
|
+
const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
|
|
197
|
+
const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
|
|
198
|
+
const yearEarlyCloses = marketEarlyCloses[year];
|
|
368
199
|
return yearEarlyCloses && yearEarlyCloses[formattedDate] !== undefined;
|
|
369
200
|
}
|
|
370
201
|
/**
|
|
371
|
-
* Get the early close time for a
|
|
372
|
-
* @param date - The date to get the early close time for
|
|
373
|
-
* @returns The early close time in minutes from midnight, or null if there is no early close
|
|
202
|
+
* Get the early close time for a date (in minutes from midnight)
|
|
374
203
|
*/
|
|
375
204
|
getEarlyCloseTime(date) {
|
|
376
|
-
const formattedDate =
|
|
377
|
-
const
|
|
205
|
+
const formattedDate = dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
|
|
206
|
+
const year = dateFnsTz.toZonedTime(date, this.timezone).getFullYear();
|
|
207
|
+
const yearEarlyCloses = marketEarlyCloses[year];
|
|
378
208
|
if (yearEarlyCloses && yearEarlyCloses[formattedDate]) {
|
|
379
209
|
const [hours, minutes] = yearEarlyCloses[formattedDate].time.split(':').map(Number);
|
|
380
210
|
return hours * 60 + minutes;
|
|
@@ -382,194 +212,283 @@ class MarketTimeUtil {
|
|
|
382
212
|
return null;
|
|
383
213
|
}
|
|
384
214
|
/**
|
|
385
|
-
* Check if a
|
|
386
|
-
* @param date - The date to check
|
|
387
|
-
* @returns true if the date is a market day, false otherwise
|
|
215
|
+
* Check if a date is a market day (not weekend or holiday)
|
|
388
216
|
*/
|
|
389
217
|
isMarketDay(date) {
|
|
390
|
-
|
|
391
|
-
const isHolidayDay = this.isHoliday(date);
|
|
392
|
-
const returner = !isWeekendDay && !isHolidayDay;
|
|
393
|
-
return returner;
|
|
218
|
+
return !this.isWeekend(date) && !this.isHoliday(date);
|
|
394
219
|
}
|
|
395
220
|
/**
|
|
396
|
-
*
|
|
397
|
-
* @param date - The date to check
|
|
398
|
-
* @returns true if the date is within market hours, false otherwise
|
|
221
|
+
* Get the next market day from a given date
|
|
399
222
|
*/
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
223
|
+
getNextMarketDay(date) {
|
|
224
|
+
let nextDay = dateFns.add(date, { days: 1 });
|
|
225
|
+
while (!this.isMarketDay(nextDay)) {
|
|
226
|
+
nextDay = dateFns.add(nextDay, { days: 1 });
|
|
404
227
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
228
|
+
return nextDay;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the previous market day from a given date
|
|
232
|
+
*/
|
|
233
|
+
getPreviousMarketDay(date) {
|
|
234
|
+
let prevDay = dateFns.sub(date, { days: 1 });
|
|
235
|
+
while (!this.isMarketDay(prevDay)) {
|
|
236
|
+
prevDay = dateFns.sub(prevDay, { days: 1 });
|
|
412
237
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
238
|
+
return prevDay;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ===== TIME FORMATTER SERVICE =====
|
|
242
|
+
/**
|
|
243
|
+
* Service for formatting time outputs
|
|
244
|
+
*/
|
|
245
|
+
class TimeFormatter {
|
|
246
|
+
timezone;
|
|
247
|
+
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
248
|
+
this.timezone = timezone;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Format a date based on the output format
|
|
252
|
+
*/
|
|
253
|
+
formatDate(date, outputFormat = 'iso') {
|
|
254
|
+
switch (outputFormat) {
|
|
255
|
+
case 'unix-seconds':
|
|
256
|
+
return Math.floor(date.getTime() / 1000);
|
|
257
|
+
case 'unix-ms':
|
|
258
|
+
return date.getTime();
|
|
259
|
+
case 'iso':
|
|
260
|
+
default:
|
|
261
|
+
return dateFnsTz.formatInTimeZone(date, this.timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
435
262
|
}
|
|
436
|
-
return returner;
|
|
437
263
|
}
|
|
438
264
|
/**
|
|
439
|
-
*
|
|
440
|
-
* @param date - The date to check
|
|
441
|
-
* @returns true if the date is before market hours, false otherwise
|
|
265
|
+
* Get New York timezone offset
|
|
442
266
|
*/
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
267
|
+
getNYTimeZone(date = new Date()) {
|
|
268
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
269
|
+
timeZone: this.timezone,
|
|
270
|
+
timeZoneName: 'shortOffset',
|
|
271
|
+
});
|
|
272
|
+
const parts = dtf.formatToParts(date);
|
|
273
|
+
const tz = parts.find(p => p.type === 'timeZoneName')?.value;
|
|
274
|
+
if (!tz) {
|
|
275
|
+
throw new Error('Could not determine New York offset');
|
|
276
|
+
}
|
|
277
|
+
const shortOffset = tz.replace('GMT', '');
|
|
278
|
+
if (shortOffset === '-4') {
|
|
279
|
+
return '-04:00';
|
|
280
|
+
}
|
|
281
|
+
else if (shortOffset === '-5') {
|
|
282
|
+
return '-05:00';
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
throw new Error(`Unexpected timezone offset: ${shortOffset}`);
|
|
286
|
+
}
|
|
449
287
|
}
|
|
450
288
|
/**
|
|
451
|
-
* Get
|
|
452
|
-
* @param currentDate - The current date
|
|
453
|
-
* @returns The last trading date
|
|
289
|
+
* Get trading date in YYYY-MM-DD format
|
|
454
290
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
return nowET;
|
|
291
|
+
getTradingDate(time) {
|
|
292
|
+
let date;
|
|
293
|
+
if (typeof time === 'number') {
|
|
294
|
+
date = new Date(time);
|
|
295
|
+
}
|
|
296
|
+
else if (typeof time === 'string') {
|
|
297
|
+
date = new Date(time);
|
|
463
298
|
}
|
|
464
299
|
else {
|
|
465
|
-
|
|
466
|
-
let lastTradingDate = dateFns.sub(nowET, { days: 1 });
|
|
467
|
-
while (!this.isMarketDay(lastTradingDate)) {
|
|
468
|
-
lastTradingDate = dateFns.sub(lastTradingDate, { days: 1 });
|
|
469
|
-
}
|
|
470
|
-
return lastTradingDate;
|
|
300
|
+
date = time;
|
|
471
301
|
}
|
|
302
|
+
return dateFnsTz.formatInTimeZone(date, this.timezone, 'yyyy-MM-dd');
|
|
472
303
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
304
|
+
}
|
|
305
|
+
// ===== MARKET TIME CALCULATOR =====
|
|
306
|
+
/**
|
|
307
|
+
* Service for core market time calculations
|
|
308
|
+
*/
|
|
309
|
+
class MarketTimeCalculator {
|
|
310
|
+
calendar;
|
|
311
|
+
formatter;
|
|
312
|
+
timezone;
|
|
313
|
+
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
314
|
+
this.timezone = timezone;
|
|
315
|
+
this.calendar = new MarketCalendar(timezone);
|
|
316
|
+
this.formatter = new TimeFormatter(timezone);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get market open/close times for a date
|
|
320
|
+
*/
|
|
321
|
+
getMarketTimes(date) {
|
|
322
|
+
const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
|
|
323
|
+
// Market closed on weekends and holidays
|
|
324
|
+
if (!this.calendar.isMarketDay(zonedDate)) {
|
|
325
|
+
return {
|
|
326
|
+
marketOpen: false,
|
|
327
|
+
open: null,
|
|
328
|
+
close: null,
|
|
329
|
+
openExt: null,
|
|
330
|
+
closeExt: null,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const dayStart = dateFns.startOfDay(zonedDate);
|
|
334
|
+
// Regular market times
|
|
335
|
+
const open = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour, minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute }), this.timezone);
|
|
336
|
+
let close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute }), this.timezone);
|
|
337
|
+
// Extended hours
|
|
338
|
+
const openExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute }), this.timezone);
|
|
339
|
+
let closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute }), this.timezone);
|
|
340
|
+
// Handle early close days
|
|
341
|
+
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
342
|
+
close = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute }), this.timezone);
|
|
343
|
+
closeExt = dateFnsTz.fromZonedTime(dateFns.set(dayStart, { hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute }), this.timezone);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
marketOpen: true,
|
|
347
|
+
open,
|
|
348
|
+
close,
|
|
349
|
+
openExt,
|
|
350
|
+
closeExt,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if a time is within market hours based on intraday reporting mode
|
|
355
|
+
*/
|
|
356
|
+
isWithinMarketHours(date, intradayReporting = 'market_hours') {
|
|
357
|
+
const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
|
|
358
|
+
// Not a market day
|
|
359
|
+
if (!this.calendar.isMarketDay(zonedDate)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const timeInMinutes = zonedDate.getHours() * 60 + zonedDate.getMinutes();
|
|
363
|
+
switch (intradayReporting) {
|
|
364
|
+
case 'extended_hours': {
|
|
365
|
+
const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
366
|
+
let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
367
|
+
// Handle early close
|
|
368
|
+
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
369
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
370
|
+
}
|
|
371
|
+
return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
|
|
372
|
+
}
|
|
373
|
+
case 'continuous':
|
|
374
|
+
return true;
|
|
375
|
+
default: {
|
|
376
|
+
// 'market_hours'
|
|
377
|
+
const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
378
|
+
let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
379
|
+
// Handle early close
|
|
380
|
+
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
381
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
382
|
+
}
|
|
383
|
+
return timeInMinutes >= startMinutes && timeInMinutes <= endMinutes;
|
|
384
|
+
}
|
|
477
385
|
}
|
|
478
|
-
return currentDate;
|
|
479
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the last full trading date
|
|
389
|
+
*/
|
|
480
390
|
getLastFullTradingDate(currentDate = new Date()) {
|
|
481
391
|
const nowET = dateFnsTz.toZonedTime(currentDate, this.timezone);
|
|
482
|
-
// If today is a market day and we're after extended hours close
|
|
483
|
-
|
|
484
|
-
if (this.isMarketDay(nowET)) {
|
|
392
|
+
// If today is a market day and we're after extended hours close, return today
|
|
393
|
+
if (this.calendar.isMarketDay(nowET)) {
|
|
485
394
|
const timeInMinutes = nowET.getHours() * 60 + nowET.getMinutes();
|
|
486
|
-
|
|
487
|
-
|
|
395
|
+
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
396
|
+
if (this.calendar.isEarlyCloseDay(nowET)) {
|
|
397
|
+
extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
398
|
+
}
|
|
488
399
|
if (timeInMinutes >= extendedEndMinutes) {
|
|
489
|
-
// Set to midnight ET while preserving the date
|
|
490
400
|
return dateFnsTz.fromZonedTime(dateFns.set(nowET, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
|
|
491
401
|
}
|
|
492
402
|
}
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// Set to midnight ET while preserving the date
|
|
497
|
-
return dateFnsTz.fromZonedTime(dateFns.set(lastFullDate, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
|
|
403
|
+
// Return the last completed trading day
|
|
404
|
+
const lastMarketDay = this.calendar.getPreviousMarketDay(nowET);
|
|
405
|
+
return dateFnsTz.fromZonedTime(dateFns.set(lastMarketDay, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }), this.timezone);
|
|
498
406
|
}
|
|
499
407
|
/**
|
|
500
|
-
*
|
|
501
|
-
* @param {Object} [options] - Options object
|
|
502
|
-
* @param {Date} [options.referenceDate] - The reference date (defaults to current date)
|
|
503
|
-
* @returns {Object} The next market day information
|
|
504
|
-
* @property {Date} date - The date object (start of day in NY time)
|
|
505
|
-
* @property {string} yyyymmdd - The date in YYYY-MM-DD format
|
|
506
|
-
* @property {string} dateISOString - Full ISO date string
|
|
408
|
+
* Get day boundaries based on intraday reporting mode
|
|
507
409
|
*/
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
while (!this.isMarketDay(currentDate)) {
|
|
511
|
-
currentDate = dateFns.add(currentDate, { days: 1 });
|
|
512
|
-
}
|
|
513
|
-
return currentDate;
|
|
514
|
-
}
|
|
515
|
-
getDayBoundaries(date) {
|
|
410
|
+
getDayBoundaries(date, intradayReporting = 'market_hours') {
|
|
411
|
+
const zonedDate = dateFnsTz.toZonedTime(date, this.timezone);
|
|
516
412
|
let start;
|
|
517
413
|
let end;
|
|
518
|
-
switch (
|
|
414
|
+
switch (intradayReporting) {
|
|
519
415
|
case 'extended_hours': {
|
|
520
|
-
start = dateFns.set(
|
|
521
|
-
hours:
|
|
522
|
-
minutes:
|
|
416
|
+
start = dateFns.set(zonedDate, {
|
|
417
|
+
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
418
|
+
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
523
419
|
seconds: 0,
|
|
524
420
|
milliseconds: 0,
|
|
525
421
|
});
|
|
526
|
-
end = dateFns.set(
|
|
527
|
-
hours:
|
|
528
|
-
minutes:
|
|
422
|
+
end = dateFns.set(zonedDate, {
|
|
423
|
+
hours: MARKET_CONFIG.TIMES.EXTENDED_END.hour,
|
|
424
|
+
minutes: MARKET_CONFIG.TIMES.EXTENDED_END.minute,
|
|
529
425
|
seconds: 59,
|
|
530
426
|
milliseconds: 999,
|
|
531
427
|
});
|
|
428
|
+
// Handle early close
|
|
429
|
+
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
430
|
+
end = dateFns.set(zonedDate, {
|
|
431
|
+
hours: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour,
|
|
432
|
+
minutes: MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute,
|
|
433
|
+
seconds: 59,
|
|
434
|
+
milliseconds: 999,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
532
437
|
break;
|
|
533
438
|
}
|
|
534
439
|
case 'continuous': {
|
|
535
|
-
start = dateFns.startOfDay(
|
|
536
|
-
end = dateFns.endOfDay(
|
|
440
|
+
start = dateFns.startOfDay(zonedDate);
|
|
441
|
+
end = dateFns.endOfDay(zonedDate);
|
|
537
442
|
break;
|
|
538
443
|
}
|
|
539
444
|
default: {
|
|
540
|
-
// market_hours
|
|
541
|
-
start = dateFns.set(
|
|
542
|
-
hours:
|
|
543
|
-
minutes:
|
|
445
|
+
// 'market_hours'
|
|
446
|
+
start = dateFns.set(zonedDate, {
|
|
447
|
+
hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
|
|
448
|
+
minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
|
|
544
449
|
seconds: 0,
|
|
545
450
|
milliseconds: 0,
|
|
546
451
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (earlyCloseMinutes !== null) {
|
|
551
|
-
const earlyCloseHours = Math.floor(earlyCloseMinutes / 60);
|
|
552
|
-
const earlyCloseMinutesRemainder = earlyCloseMinutes % 60;
|
|
553
|
-
end = dateFns.set(date, {
|
|
554
|
-
hours: earlyCloseHours,
|
|
555
|
-
minutes: earlyCloseMinutesRemainder,
|
|
556
|
-
seconds: 59,
|
|
557
|
-
milliseconds: 999,
|
|
558
|
-
});
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
end = dateFns.set(date, {
|
|
563
|
-
hours: MARKET_TIMES.REGULAR.END.HOUR,
|
|
564
|
-
minutes: MARKET_TIMES.REGULAR.END.MINUTE,
|
|
452
|
+
end = dateFns.set(zonedDate, {
|
|
453
|
+
hours: MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
|
|
454
|
+
minutes: MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
|
|
565
455
|
seconds: 59,
|
|
566
456
|
milliseconds: 999,
|
|
567
457
|
});
|
|
458
|
+
// Handle early close
|
|
459
|
+
if (this.calendar.isEarlyCloseDay(zonedDate)) {
|
|
460
|
+
end = dateFns.set(zonedDate, {
|
|
461
|
+
hours: MARKET_CONFIG.TIMES.EARLY_CLOSE.hour,
|
|
462
|
+
minutes: MARKET_CONFIG.TIMES.EARLY_CLOSE.minute,
|
|
463
|
+
seconds: 59,
|
|
464
|
+
milliseconds: 999,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
568
467
|
break;
|
|
569
468
|
}
|
|
570
469
|
}
|
|
571
|
-
return {
|
|
470
|
+
return {
|
|
471
|
+
start: dateFnsTz.fromZonedTime(start, this.timezone),
|
|
472
|
+
end: dateFnsTz.fromZonedTime(end, this.timezone),
|
|
473
|
+
};
|
|
572
474
|
}
|
|
475
|
+
}
|
|
476
|
+
// ===== PERIOD CALCULATOR =====
|
|
477
|
+
/**
|
|
478
|
+
* Service for calculating time periods
|
|
479
|
+
*/
|
|
480
|
+
class PeriodCalculator {
|
|
481
|
+
calendar;
|
|
482
|
+
timeCalculator;
|
|
483
|
+
formatter;
|
|
484
|
+
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
485
|
+
this.calendar = new MarketCalendar(timezone);
|
|
486
|
+
this.timeCalculator = new MarketTimeCalculator(timezone);
|
|
487
|
+
this.formatter = new TimeFormatter(timezone);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Calculate the start date for a given period
|
|
491
|
+
*/
|
|
573
492
|
calculatePeriodStartDate(endDate, period) {
|
|
574
493
|
let startDate;
|
|
575
494
|
switch (period) {
|
|
@@ -577,7 +496,7 @@ class MarketTimeUtil {
|
|
|
577
496
|
startDate = dateFns.set(endDate, { month: 0, date: 1 });
|
|
578
497
|
break;
|
|
579
498
|
case '1D':
|
|
580
|
-
startDate = this.
|
|
499
|
+
startDate = this.calendar.getPreviousMarketDay(endDate);
|
|
581
500
|
break;
|
|
582
501
|
case '3D':
|
|
583
502
|
startDate = dateFns.sub(endDate, { days: 3 });
|
|
@@ -603,405 +522,286 @@ class MarketTimeUtil {
|
|
|
603
522
|
default:
|
|
604
523
|
throw new Error(`Invalid period: ${period}`);
|
|
605
524
|
}
|
|
606
|
-
|
|
607
|
-
|
|
525
|
+
// Ensure start date is a market day
|
|
526
|
+
while (!this.calendar.isMarketDay(startDate)) {
|
|
527
|
+
startDate = this.calendar.getNextMarketDay(startDate);
|
|
608
528
|
}
|
|
609
529
|
return startDate;
|
|
610
530
|
}
|
|
611
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Get period dates for market time calculations
|
|
533
|
+
*/
|
|
534
|
+
getMarketTimePeriod(params) {
|
|
535
|
+
const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso', } = params;
|
|
612
536
|
if (!period) {
|
|
613
537
|
throw new Error('Period is required');
|
|
614
538
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
// Convert end date to specified timezone
|
|
619
|
-
const zonedEndDate = dateFnsTz.toZonedTime(end, this.timezone);
|
|
620
|
-
let startDate;
|
|
539
|
+
const zonedEndDate = dateFnsTz.toZonedTime(end, MARKET_CONFIG.TIMEZONE);
|
|
540
|
+
// Determine effective end date based on current market conditions
|
|
621
541
|
let endDate;
|
|
622
|
-
const isCurrentMarketDay = this.isMarketDay(zonedEndDate);
|
|
623
|
-
const isWithinHours = this.isWithinMarketHours(
|
|
624
|
-
const
|
|
625
|
-
|
|
542
|
+
const isCurrentMarketDay = this.calendar.isMarketDay(zonedEndDate);
|
|
543
|
+
const isWithinHours = this.timeCalculator.isWithinMarketHours(end, intraday_reporting);
|
|
544
|
+
const timeInMinutes = zonedEndDate.getHours() * 60 + zonedEndDate.getMinutes();
|
|
545
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
626
546
|
if (isCurrentMarketDay) {
|
|
627
|
-
if (
|
|
628
|
-
//
|
|
629
|
-
const lastMarketDay = this.
|
|
630
|
-
const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
|
|
547
|
+
if (timeInMinutes < marketStartMinutes) {
|
|
548
|
+
// Before market open - use previous day's close
|
|
549
|
+
const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
|
|
550
|
+
const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
631
551
|
endDate = dayEnd;
|
|
632
552
|
}
|
|
633
553
|
else if (isWithinHours) {
|
|
634
|
-
//
|
|
635
|
-
endDate =
|
|
554
|
+
// During market hours - use current time
|
|
555
|
+
endDate = end;
|
|
636
556
|
}
|
|
637
557
|
else {
|
|
638
|
-
//
|
|
639
|
-
const { end: dayEnd } = this.getDayBoundaries(zonedEndDate);
|
|
558
|
+
// After market close - use today's close
|
|
559
|
+
const { end: dayEnd } = this.timeCalculator.getDayBoundaries(zonedEndDate, intraday_reporting);
|
|
640
560
|
endDate = dayEnd;
|
|
641
561
|
}
|
|
642
562
|
}
|
|
643
563
|
else {
|
|
644
|
-
//
|
|
645
|
-
const lastMarketDay = this.
|
|
646
|
-
const { end: dayEnd } = this.getDayBoundaries(lastMarketDay);
|
|
564
|
+
// Not a market day - use previous market day's close
|
|
565
|
+
const lastMarketDay = this.calendar.getPreviousMarketDay(zonedEndDate);
|
|
566
|
+
const { end: dayEnd } = this.timeCalculator.getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
647
567
|
endDate = dayEnd;
|
|
648
568
|
}
|
|
649
|
-
//
|
|
569
|
+
// Calculate start date
|
|
650
570
|
const periodStartDate = this.calculatePeriodStartDate(endDate, period);
|
|
651
|
-
const { start: dayStart } = this.getDayBoundaries(periodStartDate);
|
|
652
|
-
startDate = dayStart;
|
|
653
|
-
// Convert boundaries back to UTC for final output
|
|
654
|
-
const utcStart = dateFnsTz.fromZonedTime(startDate, this.timezone);
|
|
655
|
-
const utcEnd = dateFnsTz.fromZonedTime(endDate, this.timezone);
|
|
571
|
+
const { start: dayStart } = this.timeCalculator.getDayBoundaries(periodStartDate, intraday_reporting);
|
|
656
572
|
// Ensure start is not after end
|
|
657
|
-
if (dateFns.isBefore(
|
|
573
|
+
if (dateFns.isBefore(endDate, dayStart)) {
|
|
658
574
|
throw new Error('Start date cannot be after end date');
|
|
659
575
|
}
|
|
660
576
|
return {
|
|
661
|
-
start: this.formatDate(
|
|
662
|
-
end: this.formatDate(
|
|
577
|
+
start: this.formatter.formatDate(dayStart, outputFormat),
|
|
578
|
+
end: this.formatter.formatDate(endDate, outputFormat),
|
|
663
579
|
};
|
|
664
580
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
581
|
+
}
|
|
582
|
+
// ===== MARKET STATUS SERVICE =====
|
|
583
|
+
/**
|
|
584
|
+
* Service for determining market status
|
|
585
|
+
*/
|
|
586
|
+
class MarketStatusService {
|
|
587
|
+
calendar;
|
|
588
|
+
timeCalculator;
|
|
589
|
+
timezone;
|
|
590
|
+
constructor(timezone = MARKET_CONFIG.TIMEZONE) {
|
|
591
|
+
this.timezone = timezone;
|
|
592
|
+
this.calendar = new MarketCalendar(timezone);
|
|
593
|
+
this.timeCalculator = new MarketTimeCalculator(timezone);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get current market status
|
|
597
|
+
*/
|
|
598
|
+
getMarketStatus(date = new Date()) {
|
|
599
|
+
const nyTime = dateFnsTz.toZonedTime(date, this.timezone);
|
|
600
|
+
const timeInMinutes = nyTime.getHours() * 60 + nyTime.getMinutes();
|
|
601
|
+
const isMarketDay = this.calendar.isMarketDay(nyTime);
|
|
602
|
+
const isEarlyCloseDay = this.calendar.isEarlyCloseDay(nyTime);
|
|
603
|
+
// Time boundaries
|
|
604
|
+
const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
605
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
606
|
+
const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
|
|
607
|
+
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
608
|
+
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
609
|
+
if (isEarlyCloseDay) {
|
|
610
|
+
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
611
|
+
extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
612
|
+
}
|
|
613
|
+
let status;
|
|
614
|
+
let nextStatus;
|
|
615
|
+
let nextStatusTime;
|
|
616
|
+
let marketPeriod;
|
|
617
|
+
if (!isMarketDay) {
|
|
618
|
+
// Market is closed (holiday/weekend)
|
|
619
|
+
status = 'closed';
|
|
620
|
+
nextStatus = 'extended hours';
|
|
621
|
+
marketPeriod = 'closed';
|
|
622
|
+
const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
|
|
623
|
+
nextStatusTime = dateFns.set(nextMarketDay, {
|
|
624
|
+
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
625
|
+
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
626
|
+
});
|
|
677
627
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
628
|
+
else if (timeInMinutes < extendedStartMinutes) {
|
|
629
|
+
// Before extended hours
|
|
630
|
+
status = 'closed';
|
|
631
|
+
nextStatus = 'extended hours';
|
|
632
|
+
marketPeriod = 'closed';
|
|
633
|
+
nextStatusTime = dateFns.set(nyTime, {
|
|
634
|
+
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
635
|
+
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
else if (timeInMinutes < marketStartMinutes) {
|
|
639
|
+
// Pre-market extended hours
|
|
640
|
+
status = 'extended hours';
|
|
641
|
+
nextStatus = 'open';
|
|
642
|
+
marketPeriod = 'preMarket';
|
|
643
|
+
nextStatusTime = dateFns.set(nyTime, {
|
|
644
|
+
hours: MARKET_CONFIG.TIMES.MARKET_OPEN.hour,
|
|
645
|
+
minutes: MARKET_CONFIG.TIMES.MARKET_OPEN.minute,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
else if (timeInMinutes < marketCloseMinutes) {
|
|
649
|
+
// Market is open
|
|
650
|
+
status = 'open';
|
|
651
|
+
nextStatus = 'extended hours';
|
|
652
|
+
marketPeriod = timeInMinutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
|
|
653
|
+
nextStatusTime = dateFns.set(nyTime, {
|
|
654
|
+
hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.hour : MARKET_CONFIG.TIMES.MARKET_CLOSE.hour,
|
|
655
|
+
minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_CLOSE.minute : MARKET_CONFIG.TIMES.MARKET_CLOSE.minute,
|
|
656
|
+
});
|
|
701
657
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
658
|
+
else if (timeInMinutes < extendedEndMinutes) {
|
|
659
|
+
// After-market extended hours
|
|
660
|
+
status = 'extended hours';
|
|
661
|
+
nextStatus = 'closed';
|
|
662
|
+
marketPeriod = 'afterMarket';
|
|
663
|
+
nextStatusTime = dateFns.set(nyTime, {
|
|
664
|
+
hours: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour : MARKET_CONFIG.TIMES.EXTENDED_END.hour,
|
|
665
|
+
minutes: isEarlyCloseDay ? MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute : MARKET_CONFIG.TIMES.EXTENDED_END.minute,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
// After extended hours
|
|
670
|
+
status = 'closed';
|
|
671
|
+
nextStatus = 'extended hours';
|
|
672
|
+
marketPeriod = 'closed';
|
|
673
|
+
const nextMarketDay = this.calendar.getNextMarketDay(nyTime);
|
|
674
|
+
nextStatusTime = dateFns.set(nextMarketDay, {
|
|
675
|
+
hours: MARKET_CONFIG.TIMES.EXTENDED_START.hour,
|
|
676
|
+
minutes: MARKET_CONFIG.TIMES.EXTENDED_START.minute,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const nextStatusTimeUTC = dateFnsTz.fromZonedTime(nextStatusTime, this.timezone);
|
|
680
|
+
const dateFormat = 'MMMM dd, yyyy, HH:mm:ss a';
|
|
706
681
|
return {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
682
|
+
time: date,
|
|
683
|
+
timeString: dateFns.format(nyTime, dateFormat),
|
|
684
|
+
status,
|
|
685
|
+
nextStatus,
|
|
686
|
+
marketPeriod,
|
|
687
|
+
nextStatusTime: nextStatusTimeUTC,
|
|
688
|
+
nextStatusTimeDifference: dateFns.differenceInMilliseconds(nextStatusTime, nyTime),
|
|
689
|
+
nextStatusTimeString: dateFns.format(nextStatusTime, dateFormat),
|
|
712
690
|
};
|
|
713
691
|
}
|
|
714
692
|
}
|
|
693
|
+
// ===== FUNCTIONAL API =====
|
|
715
694
|
/**
|
|
716
|
-
*
|
|
717
|
-
* @param {string} [timezone] - The timezone to use for market time calculations
|
|
718
|
-
* @param {IntradayReporting} [intraday_reporting] - The intraday reporting mode
|
|
719
|
-
* @returns {MarketTimeUtil} A new MarketTimeUtil instance
|
|
695
|
+
* Simple functional API that uses the services above
|
|
720
696
|
*/
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
697
|
+
// Create service instances
|
|
698
|
+
const marketCalendar = new MarketCalendar();
|
|
699
|
+
const marketTimeCalculator = new MarketTimeCalculator();
|
|
700
|
+
const periodCalculator = new PeriodCalculator();
|
|
701
|
+
const marketStatusService = new MarketStatusService();
|
|
702
|
+
const timeFormatter = new TimeFormatter();
|
|
724
703
|
/**
|
|
725
|
-
*
|
|
726
|
-
* @param {MarketTimeParams} [params] - The market time parameters
|
|
727
|
-
* @returns {PeriodDates} The start and end timestamps
|
|
728
|
-
*/
|
|
729
|
-
function getStartAndEndTimestamps(params = {}) {
|
|
730
|
-
const util = createMarketTimeUtil(params.timezone, params.intraday_reporting);
|
|
731
|
-
const effectiveParams = {
|
|
732
|
-
...params,
|
|
733
|
-
end: params.referenceDate || params.end || new Date(),
|
|
734
|
-
};
|
|
735
|
-
return util.getMarketTimePeriod(effectiveParams);
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Gets the market open/close times for a given date
|
|
739
|
-
* @param {Object} [options] - Options object
|
|
740
|
-
* @param {Date} [options.date] - The date to check (defaults to current date)
|
|
741
|
-
* @returns {MarketOpenCloseResult} The market open/close times
|
|
704
|
+
* Get market open/close times for a given date
|
|
742
705
|
*/
|
|
743
706
|
function getMarketOpenClose(options = {}) {
|
|
744
|
-
const
|
|
745
|
-
return
|
|
707
|
+
const { date = new Date() } = options;
|
|
708
|
+
return marketTimeCalculator.getMarketTimes(date);
|
|
746
709
|
}
|
|
747
710
|
/**
|
|
748
|
-
*
|
|
749
|
-
* @param {MarketTimeParams} [params] - The market time parameters
|
|
750
|
-
* @returns {Object} The start and end dates
|
|
751
|
-
* @property {Date} start - The start date
|
|
752
|
-
* @property {Date} end - The end date
|
|
711
|
+
* Get start and end dates for a given market time period
|
|
753
712
|
*/
|
|
754
713
|
function getStartAndEndDates(params = {}) {
|
|
755
|
-
const
|
|
756
|
-
const effectiveParams = {
|
|
757
|
-
...params,
|
|
758
|
-
end: params.referenceDate || params.end || new Date(),
|
|
759
|
-
};
|
|
760
|
-
const { start, end } = util.getMarketTimePeriod(effectiveParams);
|
|
761
|
-
// Ensure the returned values are Dates
|
|
714
|
+
const { start, end } = periodCalculator.getMarketTimePeriod(params);
|
|
762
715
|
return {
|
|
763
716
|
start: new Date(start),
|
|
764
717
|
end: new Date(end),
|
|
765
718
|
};
|
|
766
719
|
}
|
|
767
720
|
/**
|
|
768
|
-
*
|
|
769
|
-
* @returns {string} The last trading date in YYYY-MM-DD format
|
|
770
|
-
*/
|
|
771
|
-
function getLastTradingDateYYYYMMDD() {
|
|
772
|
-
const util = new MarketTimeUtil();
|
|
773
|
-
const lastTradingDate = util.getLastTradingDate();
|
|
774
|
-
return dateFns.format(lastTradingDate, 'yyyy-MM-dd');
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Gets the last full trading date
|
|
778
|
-
* @param {Date} [currentDate] - The current date (defaults to now)
|
|
779
|
-
* @returns {Object} The last full trading date
|
|
780
|
-
* @property {Date} date - The date object
|
|
781
|
-
* @property {string} YYYYMMDD - The date in YYYY-MM-DD format
|
|
721
|
+
* Get the last full trading date
|
|
782
722
|
*/
|
|
783
723
|
function getLastFullTradingDate(currentDate = new Date()) {
|
|
784
|
-
const
|
|
785
|
-
const date = util.getLastFullTradingDate(currentDate);
|
|
786
|
-
// Format the date in NY timezone to ensure consistency
|
|
724
|
+
const date = marketTimeCalculator.getLastFullTradingDate(currentDate);
|
|
787
725
|
return {
|
|
788
726
|
date,
|
|
789
|
-
YYYYMMDD:
|
|
727
|
+
YYYYMMDD: timeFormatter.getTradingDate(date),
|
|
790
728
|
};
|
|
791
729
|
}
|
|
792
730
|
/**
|
|
793
|
-
*
|
|
794
|
-
* @param {Object} [options] - Options object
|
|
795
|
-
* @param {Date} [options.referenceDate] - The reference date (defaults to current date)
|
|
796
|
-
* @returns {Object} The next market day information
|
|
797
|
-
* @property {Date} date - The date object (start of day in NY time)
|
|
798
|
-
* @property {string} yyyymmdd - The date in YYYY-MM-DD format
|
|
799
|
-
* @property {string} dateISOString - Full ISO date string
|
|
731
|
+
* Get the next market day
|
|
800
732
|
*/
|
|
801
733
|
function getNextMarketDay({ referenceDate } = {}) {
|
|
802
|
-
const util = new MarketTimeUtil();
|
|
803
734
|
const startDate = referenceDate || new Date();
|
|
804
|
-
const nextDate =
|
|
735
|
+
const nextDate = marketCalendar.getNextMarketDay(startDate);
|
|
805
736
|
// Convert to start of day in NY time
|
|
806
|
-
const startOfDayNY = dateFns.startOfDay(dateFnsTz.toZonedTime(nextDate,
|
|
807
|
-
const dateInET = dateFnsTz.fromZonedTime(startOfDayNY,
|
|
737
|
+
const startOfDayNY = dateFns.startOfDay(dateFnsTz.toZonedTime(nextDate, MARKET_CONFIG.TIMEZONE));
|
|
738
|
+
const dateInET = dateFnsTz.fromZonedTime(startOfDayNY, MARKET_CONFIG.TIMEZONE);
|
|
808
739
|
return {
|
|
809
740
|
date: dateInET,
|
|
810
|
-
yyyymmdd:
|
|
741
|
+
yyyymmdd: timeFormatter.getTradingDate(dateInET),
|
|
811
742
|
dateISOString: dateInET.toISOString(),
|
|
812
743
|
};
|
|
813
744
|
}
|
|
814
745
|
/**
|
|
815
|
-
*
|
|
816
|
-
* @returns {Date} The current time in Eastern Time
|
|
746
|
+
* Get trading date in YYYY-MM-DD format
|
|
817
747
|
*/
|
|
818
|
-
|
|
819
|
-
return
|
|
820
|
-
}
|
|
748
|
+
function getTradingDate(time) {
|
|
749
|
+
return timeFormatter.getTradingDate(time);
|
|
750
|
+
}
|
|
821
751
|
/**
|
|
822
|
-
*
|
|
823
|
-
* @param {number|string|Date} time - The time to convert
|
|
824
|
-
* @returns {Date} The date in New York timezone
|
|
752
|
+
* Get New York timezone offset
|
|
825
753
|
*/
|
|
826
|
-
function
|
|
827
|
-
|
|
828
|
-
if (typeof time === 'number' || typeof time === 'string' || time instanceof Date) {
|
|
829
|
-
// Assuming Unix timestamp in epoch milliseconds, string date, or Date object
|
|
830
|
-
date = new Date(time);
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
// Assuming object with year, month, and day
|
|
834
|
-
date = new Date(time.year, time.month - 1, time.day);
|
|
835
|
-
}
|
|
836
|
-
return dateFnsTz.toZonedTime(date, 'America/New_York');
|
|
754
|
+
function getNYTimeZone(date) {
|
|
755
|
+
return timeFormatter.getNYTimeZone(date);
|
|
837
756
|
}
|
|
838
757
|
/**
|
|
839
|
-
*
|
|
840
|
-
* @param {string|number|Date} time - The time to convert (string, unix timestamp in ms, or Date object)
|
|
841
|
-
* @returns {string} The trading date in YYYY-MM-DD format
|
|
758
|
+
* Get current market status
|
|
842
759
|
*/
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
// Assuming Unix timestamp in milliseconds
|
|
847
|
-
date = new Date(time);
|
|
848
|
-
}
|
|
849
|
-
else if (typeof time === 'string') {
|
|
850
|
-
date = new Date(time);
|
|
851
|
-
}
|
|
852
|
-
else {
|
|
853
|
-
date = time;
|
|
854
|
-
}
|
|
855
|
-
// Convert to NY timezone and format as YYYY-MM-DD
|
|
856
|
-
return dateFnsTz.formatInTimeZone(date, MARKET_TIMES.TIMEZONE, 'yyyy-MM-dd');
|
|
760
|
+
function getMarketStatus(options = {}) {
|
|
761
|
+
const { date = new Date() } = options;
|
|
762
|
+
return marketStatusService.getMarketStatus(date);
|
|
857
763
|
}
|
|
858
764
|
/**
|
|
859
|
-
*
|
|
860
|
-
* @param dateString - The date string to check
|
|
861
|
-
* @returns "-04:00" during daylight savings (EDT) or "-05:00" during standard time (EST)
|
|
765
|
+
* Check if a date is a market day
|
|
862
766
|
*/
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
868
|
-
timeZone: 'America/New_York',
|
|
869
|
-
timeZoneName: 'shortOffset',
|
|
870
|
-
});
|
|
871
|
-
const parts = dtf.formatToParts(date);
|
|
872
|
-
const tz = parts.find((p) => p.type === 'timeZoneName')?.value;
|
|
873
|
-
// tz will be "GMT-5" or "GMT-4"
|
|
874
|
-
if (!tz) {
|
|
875
|
-
throw new Error('Could not determine New York offset');
|
|
876
|
-
}
|
|
877
|
-
// extract the -4 or -5 from the string
|
|
878
|
-
const shortOffset = tz.replace('GMT', '');
|
|
879
|
-
// return the correct offset
|
|
880
|
-
if (shortOffset === '-4') {
|
|
881
|
-
console.log(`New York is on EDT; using -04:00. Full date: ${date.toLocaleString('en-US', {
|
|
882
|
-
timeZone: 'America/New_York',
|
|
883
|
-
})}, time zone part: ${tz}`);
|
|
884
|
-
return '-04:00';
|
|
885
|
-
}
|
|
886
|
-
else if (shortOffset === '-5') {
|
|
887
|
-
console.log(`New York is on EST; using -05:00. Full date: ${date.toLocaleString('en-US', {
|
|
888
|
-
timeZone: 'America/New_York',
|
|
889
|
-
})}, time zone part: ${tz}`);
|
|
890
|
-
return '-05:00';
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
throw new Error('Could not determine New York offset');
|
|
894
|
-
}
|
|
895
|
-
};
|
|
767
|
+
function isMarketDay(date) {
|
|
768
|
+
return marketCalendar.isMarketDay(date);
|
|
769
|
+
}
|
|
896
770
|
/**
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
771
|
+
* Function to find complete trading date periods, starting at the beginning of one trading date and ending at the last.
|
|
772
|
+
* By default, it gets the last trading date, returning the beginning and end. But we can also a) define the end date
|
|
773
|
+
* 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),
|
|
774
|
+
* 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).
|
|
775
|
+
* @param options.endDate - The end date to use, defaults to today
|
|
776
|
+
* @param options.days - The number of days to go back, defaults to 1
|
|
777
|
+
* @returns The start and end dates with proper market open/close times
|
|
901
778
|
*/
|
|
902
|
-
function
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
? MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTES
|
|
913
|
-
: MARKET_TIMES.REGULAR.END.MINUTES;
|
|
914
|
-
const extendedEndMinutes = isEarlyCloseDay
|
|
915
|
-
? MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTES
|
|
916
|
-
: MARKET_TIMES.EXTENDED.END.MINUTES;
|
|
917
|
-
let status;
|
|
918
|
-
let nextStatus;
|
|
919
|
-
let nextStatusTime;
|
|
920
|
-
let marketPeriod;
|
|
921
|
-
const nextMarketDay = util.getNextMarketDay(nyTime);
|
|
922
|
-
// Determine current status and market period
|
|
923
|
-
if (!util.isMarketDay(nyTime)) {
|
|
924
|
-
// Not a market day! market is closed
|
|
925
|
-
marketPeriod = 'closed';
|
|
926
|
-
status = 'closed';
|
|
927
|
-
nextStatus = 'extended hours';
|
|
928
|
-
// Find next market day and set to extended hours start time
|
|
929
|
-
nextStatusTime = dateFns.set(nextMarketDay, {
|
|
930
|
-
hours: MARKET_TIMES.EXTENDED.START.HOUR,
|
|
931
|
-
minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
|
|
932
|
-
});
|
|
933
|
-
} // check if the market isn't in extended hours yet
|
|
934
|
-
else if (timeInMinutes >= 0 && timeInMinutes < extendedStartMinutes) {
|
|
935
|
-
marketPeriod = 'closed';
|
|
936
|
-
status = 'closed';
|
|
937
|
-
nextStatus = 'extended hours';
|
|
938
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
939
|
-
hours: MARKET_TIMES.EXTENDED.START.HOUR,
|
|
940
|
-
minutes: MARKET_TIMES.EXTENDED.START.MINUTE,
|
|
941
|
-
});
|
|
942
|
-
// check if we're in pre-market hours
|
|
943
|
-
}
|
|
944
|
-
else if (timeInMinutes >= extendedStartMinutes && timeInMinutes < marketStartMinutes) {
|
|
945
|
-
marketPeriod = 'preMarket';
|
|
946
|
-
status = 'extended hours';
|
|
947
|
-
nextStatus = 'open';
|
|
948
|
-
nextStatusTime = dateFns.set(nyTime, {
|
|
949
|
-
hours: MARKET_TIMES.REGULAR.START.HOUR,
|
|
950
|
-
minutes: MARKET_TIMES.REGULAR.START.MINUTE,
|
|
951
|
-
});
|
|
952
|
-
// check if market is open
|
|
953
|
-
}
|
|
954
|
-
else if (timeInMinutes >= marketStartMinutes && timeInMinutes < marketRegularCloseMinutes) {
|
|
955
|
-
status = 'open';
|
|
956
|
-
nextStatus = 'extended hours';
|
|
957
|
-
// market is open, but just check the marketPeriod - could be earlyMarket or regularMarket
|
|
958
|
-
marketPeriod = timeInMinutes < MARKET_TIMES.EARLY_MORNING.END.MINUTES ? 'earlyMarket' : 'regularMarket';
|
|
959
|
-
nextStatusTime = isEarlyCloseDay
|
|
960
|
-
? dateFns.set(nyTime, {
|
|
961
|
-
hours: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.HOUR,
|
|
962
|
-
minutes: MARKET_TIMES.EARLY_CLOSE_BEFORE_HOLIDAY.END.MINUTE,
|
|
963
|
-
})
|
|
964
|
-
: dateFns.set(nyTime, {
|
|
965
|
-
hours: MARKET_TIMES.REGULAR.END.HOUR,
|
|
966
|
-
minutes: MARKET_TIMES.REGULAR.END.MINUTE,
|
|
967
|
-
});
|
|
968
|
-
// check if it's after-market extended hours
|
|
969
|
-
}
|
|
970
|
-
else if (timeInMinutes >= marketRegularCloseMinutes && timeInMinutes < extendedEndMinutes) {
|
|
971
|
-
status = 'extended hours';
|
|
972
|
-
nextStatus = 'closed';
|
|
973
|
-
marketPeriod = 'afterMarket';
|
|
974
|
-
nextStatusTime = isEarlyCloseDay
|
|
975
|
-
? dateFns.set(nyTime, {
|
|
976
|
-
hours: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.HOUR,
|
|
977
|
-
minutes: MARKET_TIMES.EARLY_EXTENDED_BEFORE_HOLIDAY.END.MINUTE,
|
|
978
|
-
})
|
|
979
|
-
: dateFns.set(nyTime, {
|
|
980
|
-
hours: MARKET_TIMES.EXTENDED.END.HOUR,
|
|
981
|
-
minutes: MARKET_TIMES.EXTENDED.END.MINUTE,
|
|
982
|
-
});
|
|
983
|
-
// otherwise, the market is closed
|
|
779
|
+
function getTradingStartAndEndDates(options = {}) {
|
|
780
|
+
const { endDate = new Date(), days = 1 } = options;
|
|
781
|
+
// Get the last full trading date for the end date
|
|
782
|
+
const endTradingDate = getLastFullTradingDate(endDate).date;
|
|
783
|
+
// Get the market close time for the end date (4:00 PM ET or 1:00 PM on short days)
|
|
784
|
+
const endMarketClose = getMarketOpenClose({ date: endTradingDate }).close;
|
|
785
|
+
let startDate;
|
|
786
|
+
if (days <= 1) {
|
|
787
|
+
// For 1 day, start is market open of the same trading day as end
|
|
788
|
+
startDate = getMarketOpenClose({ date: endTradingDate }).open;
|
|
984
789
|
}
|
|
985
790
|
else {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
791
|
+
// For multiple days, go back the specified number of trading days
|
|
792
|
+
let currentDate = new Date(endTradingDate);
|
|
793
|
+
let tradingDaysBack = 0;
|
|
794
|
+
// Count back trading days
|
|
795
|
+
while (tradingDaysBack < days - 1) {
|
|
796
|
+
currentDate = dateFns.sub(currentDate, { days: 1 });
|
|
797
|
+
if (isMarketDay(currentDate)) {
|
|
798
|
+
tradingDaysBack++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// Get the market open time for the start date
|
|
802
|
+
startDate = getMarketOpenClose({ date: currentDate }).open;
|
|
993
803
|
}
|
|
994
|
-
|
|
995
|
-
return {
|
|
996
|
-
time: now,
|
|
997
|
-
timeString: dateFns.format(nyTime, dateFormat),
|
|
998
|
-
status,
|
|
999
|
-
nextStatus,
|
|
1000
|
-
marketPeriod,
|
|
1001
|
-
nextStatusTime: dateFnsTz.fromZonedTime(nextStatusTime, MARKET_TIMES.TIMEZONE),
|
|
1002
|
-
nextStatusTimeDifference: dateFns.differenceInMilliseconds(nextStatusTime, nyTime),
|
|
1003
|
-
nextStatusTimeString: dateFns.format(nextStatusTime, dateFormat),
|
|
1004
|
-
};
|
|
804
|
+
return { startDate, endDate: endMarketClose };
|
|
1005
805
|
}
|
|
1006
806
|
|
|
1007
807
|
// format-tools.ts
|
|
@@ -2431,7 +2231,7 @@ const safeJSON = (text) => {
|
|
|
2431
2231
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2432
2232
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2433
2233
|
|
|
2434
|
-
const VERSION = '5.8.
|
|
2234
|
+
const VERSION = '5.8.3'; // x-release-please-version
|
|
2435
2235
|
|
|
2436
2236
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2437
2237
|
const isRunningInBrowser = () => {
|
|
@@ -3183,13 +2983,95 @@ function findDoubleNewlineIndex(buffer) {
|
|
|
3183
2983
|
return -1;
|
|
3184
2984
|
}
|
|
3185
2985
|
|
|
2986
|
+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2987
|
+
const levelNumbers = {
|
|
2988
|
+
off: 0,
|
|
2989
|
+
error: 200,
|
|
2990
|
+
warn: 300,
|
|
2991
|
+
info: 400,
|
|
2992
|
+
debug: 500,
|
|
2993
|
+
};
|
|
2994
|
+
const parseLogLevel = (maybeLevel, sourceName, client) => {
|
|
2995
|
+
if (!maybeLevel) {
|
|
2996
|
+
return undefined;
|
|
2997
|
+
}
|
|
2998
|
+
if (hasOwn(levelNumbers, maybeLevel)) {
|
|
2999
|
+
return maybeLevel;
|
|
3000
|
+
}
|
|
3001
|
+
loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
|
|
3002
|
+
return undefined;
|
|
3003
|
+
};
|
|
3004
|
+
function noop() { }
|
|
3005
|
+
function makeLogFn(fnLevel, logger, logLevel) {
|
|
3006
|
+
if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
|
|
3007
|
+
return noop;
|
|
3008
|
+
}
|
|
3009
|
+
else {
|
|
3010
|
+
// Don't wrap logger functions, we want the stacktrace intact!
|
|
3011
|
+
return logger[fnLevel].bind(logger);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const noopLogger = {
|
|
3015
|
+
error: noop,
|
|
3016
|
+
warn: noop,
|
|
3017
|
+
info: noop,
|
|
3018
|
+
debug: noop,
|
|
3019
|
+
};
|
|
3020
|
+
let cachedLoggers = /* @__PURE__ */ new WeakMap();
|
|
3021
|
+
function loggerFor(client) {
|
|
3022
|
+
const logger = client.logger;
|
|
3023
|
+
const logLevel = client.logLevel ?? 'off';
|
|
3024
|
+
if (!logger) {
|
|
3025
|
+
return noopLogger;
|
|
3026
|
+
}
|
|
3027
|
+
const cachedLogger = cachedLoggers.get(logger);
|
|
3028
|
+
if (cachedLogger && cachedLogger[0] === logLevel) {
|
|
3029
|
+
return cachedLogger[1];
|
|
3030
|
+
}
|
|
3031
|
+
const levelLogger = {
|
|
3032
|
+
error: makeLogFn('error', logger, logLevel),
|
|
3033
|
+
warn: makeLogFn('warn', logger, logLevel),
|
|
3034
|
+
info: makeLogFn('info', logger, logLevel),
|
|
3035
|
+
debug: makeLogFn('debug', logger, logLevel),
|
|
3036
|
+
};
|
|
3037
|
+
cachedLoggers.set(logger, [logLevel, levelLogger]);
|
|
3038
|
+
return levelLogger;
|
|
3039
|
+
}
|
|
3040
|
+
const formatRequestDetails = (details) => {
|
|
3041
|
+
if (details.options) {
|
|
3042
|
+
details.options = { ...details.options };
|
|
3043
|
+
delete details.options['headers']; // redundant + leaks internals
|
|
3044
|
+
}
|
|
3045
|
+
if (details.headers) {
|
|
3046
|
+
details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
|
|
3047
|
+
name,
|
|
3048
|
+
(name.toLowerCase() === 'authorization' ||
|
|
3049
|
+
name.toLowerCase() === 'cookie' ||
|
|
3050
|
+
name.toLowerCase() === 'set-cookie') ?
|
|
3051
|
+
'***'
|
|
3052
|
+
: value,
|
|
3053
|
+
]));
|
|
3054
|
+
}
|
|
3055
|
+
if ('retryOfRequestLogID' in details) {
|
|
3056
|
+
if (details.retryOfRequestLogID) {
|
|
3057
|
+
details.retryOf = details.retryOfRequestLogID;
|
|
3058
|
+
}
|
|
3059
|
+
delete details.retryOfRequestLogID;
|
|
3060
|
+
}
|
|
3061
|
+
return details;
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
var _Stream_client;
|
|
3186
3065
|
class Stream {
|
|
3187
|
-
constructor(iterator, controller) {
|
|
3066
|
+
constructor(iterator, controller, client) {
|
|
3188
3067
|
this.iterator = iterator;
|
|
3068
|
+
_Stream_client.set(this, void 0);
|
|
3189
3069
|
this.controller = controller;
|
|
3070
|
+
__classPrivateFieldSet(this, _Stream_client, client);
|
|
3190
3071
|
}
|
|
3191
|
-
static fromSSEResponse(response, controller) {
|
|
3072
|
+
static fromSSEResponse(response, controller, client) {
|
|
3192
3073
|
let consumed = false;
|
|
3074
|
+
const logger = client ? loggerFor(client) : console;
|
|
3193
3075
|
async function* iterator() {
|
|
3194
3076
|
if (consumed) {
|
|
3195
3077
|
throw new OpenAIError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
|
|
@@ -3212,8 +3094,8 @@ class Stream {
|
|
|
3212
3094
|
data = JSON.parse(sse.data);
|
|
3213
3095
|
}
|
|
3214
3096
|
catch (e) {
|
|
3215
|
-
|
|
3216
|
-
|
|
3097
|
+
logger.error(`Could not parse message into JSON:`, sse.data);
|
|
3098
|
+
logger.error(`From chunk:`, sse.raw);
|
|
3217
3099
|
throw e;
|
|
3218
3100
|
}
|
|
3219
3101
|
if (data && data.error) {
|
|
@@ -3252,13 +3134,13 @@ class Stream {
|
|
|
3252
3134
|
controller.abort();
|
|
3253
3135
|
}
|
|
3254
3136
|
}
|
|
3255
|
-
return new Stream(iterator, controller);
|
|
3137
|
+
return new Stream(iterator, controller, client);
|
|
3256
3138
|
}
|
|
3257
3139
|
/**
|
|
3258
3140
|
* Generates a Stream from a newline-separated ReadableStream
|
|
3259
3141
|
* where each item is a JSON value.
|
|
3260
3142
|
*/
|
|
3261
|
-
static fromReadableStream(readableStream, controller) {
|
|
3143
|
+
static fromReadableStream(readableStream, controller, client) {
|
|
3262
3144
|
let consumed = false;
|
|
3263
3145
|
async function* iterLines() {
|
|
3264
3146
|
const lineDecoder = new LineDecoder();
|
|
@@ -3299,9 +3181,9 @@ class Stream {
|
|
|
3299
3181
|
controller.abort();
|
|
3300
3182
|
}
|
|
3301
3183
|
}
|
|
3302
|
-
return new Stream(iterator, controller);
|
|
3184
|
+
return new Stream(iterator, controller, client);
|
|
3303
3185
|
}
|
|
3304
|
-
[Symbol.asyncIterator]() {
|
|
3186
|
+
[(_Stream_client = new WeakMap(), Symbol.asyncIterator)]() {
|
|
3305
3187
|
return this.iterator();
|
|
3306
3188
|
}
|
|
3307
3189
|
/**
|
|
@@ -3325,8 +3207,8 @@ class Stream {
|
|
|
3325
3207
|
};
|
|
3326
3208
|
};
|
|
3327
3209
|
return [
|
|
3328
|
-
new Stream(() => teeIterator(left), this.controller),
|
|
3329
|
-
new Stream(() => teeIterator(right), this.controller),
|
|
3210
|
+
new Stream(() => teeIterator(left), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
|
|
3211
|
+
new Stream(() => teeIterator(right), this.controller, __classPrivateFieldGet(this, _Stream_client, "f")),
|
|
3330
3212
|
];
|
|
3331
3213
|
}
|
|
3332
3214
|
/**
|
|
@@ -3460,84 +3342,6 @@ function partition(str, delimiter) {
|
|
|
3460
3342
|
return [str, '', ''];
|
|
3461
3343
|
}
|
|
3462
3344
|
|
|
3463
|
-
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
3464
|
-
const levelNumbers = {
|
|
3465
|
-
off: 0,
|
|
3466
|
-
error: 200,
|
|
3467
|
-
warn: 300,
|
|
3468
|
-
info: 400,
|
|
3469
|
-
debug: 500,
|
|
3470
|
-
};
|
|
3471
|
-
const parseLogLevel = (maybeLevel, sourceName, client) => {
|
|
3472
|
-
if (!maybeLevel) {
|
|
3473
|
-
return undefined;
|
|
3474
|
-
}
|
|
3475
|
-
if (hasOwn(levelNumbers, maybeLevel)) {
|
|
3476
|
-
return maybeLevel;
|
|
3477
|
-
}
|
|
3478
|
-
loggerFor(client).warn(`${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(Object.keys(levelNumbers))}`);
|
|
3479
|
-
return undefined;
|
|
3480
|
-
};
|
|
3481
|
-
function noop() { }
|
|
3482
|
-
function makeLogFn(fnLevel, logger, logLevel) {
|
|
3483
|
-
if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
|
|
3484
|
-
return noop;
|
|
3485
|
-
}
|
|
3486
|
-
else {
|
|
3487
|
-
// Don't wrap logger functions, we want the stacktrace intact!
|
|
3488
|
-
return logger[fnLevel].bind(logger);
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
const noopLogger = {
|
|
3492
|
-
error: noop,
|
|
3493
|
-
warn: noop,
|
|
3494
|
-
info: noop,
|
|
3495
|
-
debug: noop,
|
|
3496
|
-
};
|
|
3497
|
-
let cachedLoggers = /** @__PURE__ */ new WeakMap();
|
|
3498
|
-
function loggerFor(client) {
|
|
3499
|
-
const logger = client.logger;
|
|
3500
|
-
const logLevel = client.logLevel ?? 'off';
|
|
3501
|
-
if (!logger) {
|
|
3502
|
-
return noopLogger;
|
|
3503
|
-
}
|
|
3504
|
-
const cachedLogger = cachedLoggers.get(logger);
|
|
3505
|
-
if (cachedLogger && cachedLogger[0] === logLevel) {
|
|
3506
|
-
return cachedLogger[1];
|
|
3507
|
-
}
|
|
3508
|
-
const levelLogger = {
|
|
3509
|
-
error: makeLogFn('error', logger, logLevel),
|
|
3510
|
-
warn: makeLogFn('warn', logger, logLevel),
|
|
3511
|
-
info: makeLogFn('info', logger, logLevel),
|
|
3512
|
-
debug: makeLogFn('debug', logger, logLevel),
|
|
3513
|
-
};
|
|
3514
|
-
cachedLoggers.set(logger, [logLevel, levelLogger]);
|
|
3515
|
-
return levelLogger;
|
|
3516
|
-
}
|
|
3517
|
-
const formatRequestDetails = (details) => {
|
|
3518
|
-
if (details.options) {
|
|
3519
|
-
details.options = { ...details.options };
|
|
3520
|
-
delete details.options['headers']; // redundant + leaks internals
|
|
3521
|
-
}
|
|
3522
|
-
if (details.headers) {
|
|
3523
|
-
details.headers = Object.fromEntries((details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(([name, value]) => [
|
|
3524
|
-
name,
|
|
3525
|
-
(name.toLowerCase() === 'authorization' ||
|
|
3526
|
-
name.toLowerCase() === 'cookie' ||
|
|
3527
|
-
name.toLowerCase() === 'set-cookie') ?
|
|
3528
|
-
'***'
|
|
3529
|
-
: value,
|
|
3530
|
-
]));
|
|
3531
|
-
}
|
|
3532
|
-
if ('retryOfRequestLogID' in details) {
|
|
3533
|
-
if (details.retryOfRequestLogID) {
|
|
3534
|
-
details.retryOf = details.retryOfRequestLogID;
|
|
3535
|
-
}
|
|
3536
|
-
delete details.retryOfRequestLogID;
|
|
3537
|
-
}
|
|
3538
|
-
return details;
|
|
3539
|
-
};
|
|
3540
|
-
|
|
3541
3345
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
3542
3346
|
async function defaultParseResponse(client, props) {
|
|
3543
3347
|
const { response, requestLogID, retryOfRequestLogID, startTime } = props;
|
|
@@ -3547,9 +3351,9 @@ async function defaultParseResponse(client, props) {
|
|
|
3547
3351
|
// Note: there is an invariant here that isn't represented in the type system
|
|
3548
3352
|
// that if you set `stream: true` the response type must also be `Stream<T>`
|
|
3549
3353
|
if (props.options.__streamClass) {
|
|
3550
|
-
return props.options.__streamClass.fromSSEResponse(response, props.controller);
|
|
3354
|
+
return props.options.__streamClass.fromSSEResponse(response, props.controller, client);
|
|
3551
3355
|
}
|
|
3552
|
-
return Stream.fromSSEResponse(response, props.controller);
|
|
3356
|
+
return Stream.fromSSEResponse(response, props.controller, client);
|
|
3553
3357
|
}
|
|
3554
3358
|
// fetch refuses to read the body when the status code is 204.
|
|
3555
3359
|
if (response.status === 204) {
|
|
@@ -3803,7 +3607,7 @@ const isAsyncIterable = (value) => value != null && typeof value === 'object' &&
|
|
|
3803
3607
|
const multipartFormRequestOptions = async (opts, fetch) => {
|
|
3804
3608
|
return { ...opts, body: await createForm(opts.body, fetch) };
|
|
3805
3609
|
};
|
|
3806
|
-
const supportsFormDataMap =
|
|
3610
|
+
const supportsFormDataMap = /* @__PURE__ */ new WeakMap();
|
|
3807
3611
|
/**
|
|
3808
3612
|
* node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
|
|
3809
3613
|
* properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
|
|
@@ -3979,21 +3783,38 @@ class APIResource {
|
|
|
3979
3783
|
function encodeURIPath(str) {
|
|
3980
3784
|
return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
|
|
3981
3785
|
}
|
|
3786
|
+
const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
|
|
3982
3787
|
const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(statics, ...params) {
|
|
3983
3788
|
// If there are no params, no processing is needed.
|
|
3984
3789
|
if (statics.length === 1)
|
|
3985
3790
|
return statics[0];
|
|
3986
3791
|
let postPath = false;
|
|
3792
|
+
const invalidSegments = [];
|
|
3987
3793
|
const path = statics.reduce((previousValue, currentValue, index) => {
|
|
3988
3794
|
if (/[?#]/.test(currentValue)) {
|
|
3989
3795
|
postPath = true;
|
|
3990
3796
|
}
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3797
|
+
const value = params[index];
|
|
3798
|
+
let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
|
|
3799
|
+
if (index !== params.length &&
|
|
3800
|
+
(value == null ||
|
|
3801
|
+
(typeof value === 'object' &&
|
|
3802
|
+
// handle values from other realms
|
|
3803
|
+
value.toString ===
|
|
3804
|
+
Object.getPrototypeOf(Object.getPrototypeOf(value.hasOwnProperty ?? EMPTY) ?? EMPTY)
|
|
3805
|
+
?.toString))) {
|
|
3806
|
+
encoded = value + '';
|
|
3807
|
+
invalidSegments.push({
|
|
3808
|
+
start: previousValue.length + currentValue.length,
|
|
3809
|
+
length: encoded.length,
|
|
3810
|
+
error: `Value of type ${Object.prototype.toString
|
|
3811
|
+
.call(value)
|
|
3812
|
+
.slice(8, -1)} is not a valid path parameter`,
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
return previousValue + currentValue + (index === params.length ? '' : encoded);
|
|
3994
3816
|
}, '');
|
|
3995
3817
|
const pathOnly = path.split(/[?#]/, 1)[0];
|
|
3996
|
-
const invalidSegments = [];
|
|
3997
3818
|
const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
|
|
3998
3819
|
let match;
|
|
3999
3820
|
// Find all invalid segments
|
|
@@ -4001,8 +3822,10 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
|
|
|
4001
3822
|
invalidSegments.push({
|
|
4002
3823
|
start: match.index,
|
|
4003
3824
|
length: match[0].length,
|
|
3825
|
+
error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
|
|
4004
3826
|
});
|
|
4005
3827
|
}
|
|
3828
|
+
invalidSegments.sort((a, b) => a.start - b.start);
|
|
4006
3829
|
if (invalidSegments.length > 0) {
|
|
4007
3830
|
let lastEnd = 0;
|
|
4008
3831
|
const underline = invalidSegments.reduce((acc, segment) => {
|
|
@@ -4011,7 +3834,9 @@ const createPathTagFunction = (pathEncoder = encodeURIPath) => function path(sta
|
|
|
4011
3834
|
lastEnd = segment.start + segment.length;
|
|
4012
3835
|
return acc + spaces + arrows;
|
|
4013
3836
|
}, '');
|
|
4014
|
-
throw new OpenAIError(`Path parameters result in path with invalid segments:\n${
|
|
3837
|
+
throw new OpenAIError(`Path parameters result in path with invalid segments:\n${invalidSegments
|
|
3838
|
+
.map((e) => e.error)
|
|
3839
|
+
.join('\n')}\n${path}\n${underline}`);
|
|
4015
3840
|
}
|
|
4016
3841
|
return path;
|
|
4017
3842
|
};
|
|
@@ -15371,35 +15196,6 @@ function requireWebsocketServer () {
|
|
|
15371
15196
|
|
|
15372
15197
|
requireWebsocketServer();
|
|
15373
15198
|
|
|
15374
|
-
/**
|
|
15375
|
-
* Logs a message to the console.
|
|
15376
|
-
* @param message The message to log.
|
|
15377
|
-
* @param options Optional options.
|
|
15378
|
-
* @param options.source The source of the message.
|
|
15379
|
-
* @param options.type The type of message to log.
|
|
15380
|
-
* @param options.symbol The trading symbol associated with this log.
|
|
15381
|
-
* @param options.account The account associated with this log.
|
|
15382
|
-
*/
|
|
15383
|
-
function log$1(message, options = { source: 'App', type: 'info' }) {
|
|
15384
|
-
// Format the timestamp
|
|
15385
|
-
const date = new Date();
|
|
15386
|
-
const timestamp = date.toLocaleString('en-US', { timeZone: 'America/New_York' });
|
|
15387
|
-
const account = options?.account;
|
|
15388
|
-
const symbol = options?.symbol;
|
|
15389
|
-
// Build the log message
|
|
15390
|
-
const logMessage = `[${timestamp}]${options?.source ? ` [${options.source}] ` : ''}${account ? ` [${account}] ` : ''}${symbol ? ` [${symbol}] ` : ''}${message}`;
|
|
15391
|
-
// Use appropriate console method based on type
|
|
15392
|
-
if (options?.type === 'error') {
|
|
15393
|
-
console.error(logMessage);
|
|
15394
|
-
}
|
|
15395
|
-
else if (options?.type === 'warn') {
|
|
15396
|
-
console.warn(logMessage);
|
|
15397
|
-
}
|
|
15398
|
-
else {
|
|
15399
|
-
console.log(logMessage);
|
|
15400
|
-
}
|
|
15401
|
-
}
|
|
15402
|
-
|
|
15403
15199
|
const log = (message, options = { type: 'info' }) => {
|
|
15404
15200
|
log$1(message, { ...options, source: 'AlpacaMarketDataAPI' });
|
|
15405
15201
|
};
|
|
@@ -15423,22 +15219,16 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15423
15219
|
optionWs = null;
|
|
15424
15220
|
stockSubscriptions = { trades: [], quotes: [], bars: [] };
|
|
15425
15221
|
optionSubscriptions = { trades: [], quotes: [], bars: [] };
|
|
15426
|
-
apiKey;
|
|
15427
|
-
secretKey;
|
|
15428
|
-
accountType;
|
|
15429
15222
|
setMode(mode = 'production') {
|
|
15430
|
-
if (mode === 'sandbox') {
|
|
15431
|
-
// sandbox mode
|
|
15223
|
+
if (mode === 'sandbox') { // sandbox mode
|
|
15432
15224
|
this.stockStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v2/sip';
|
|
15433
15225
|
this.optionStreamUrl = 'wss://stream.data.sandbox.alpaca.markets/v1beta3/options';
|
|
15434
15226
|
}
|
|
15435
|
-
else if (mode === 'test') {
|
|
15436
|
-
// test mode, can only use ticker FAKEPACA
|
|
15227
|
+
else if (mode === 'test') { // test mode, can only use ticker FAKEPACA
|
|
15437
15228
|
this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/test';
|
|
15438
15229
|
this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options'; // there's no test mode for options
|
|
15439
15230
|
}
|
|
15440
|
-
else {
|
|
15441
|
-
// production
|
|
15231
|
+
else { // production
|
|
15442
15232
|
this.stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/sip';
|
|
15443
15233
|
this.optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options';
|
|
15444
15234
|
}
|
|
@@ -15454,30 +15244,24 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15454
15244
|
return 'production';
|
|
15455
15245
|
}
|
|
15456
15246
|
}
|
|
15457
|
-
constructor(
|
|
15247
|
+
constructor() {
|
|
15458
15248
|
super();
|
|
15459
|
-
this.apiKey = config.apiKey;
|
|
15460
|
-
this.secretKey = config.secretKey;
|
|
15461
|
-
this.accountType = config.accountType || 'LIVE';
|
|
15462
15249
|
this.dataURL = 'https://data.alpaca.markets/v2';
|
|
15463
15250
|
this.apiURL =
|
|
15464
|
-
|
|
15251
|
+
process.env.ALPACA_ACCOUNT_TYPE === 'PAPER'
|
|
15465
15252
|
? 'https://paper-api.alpaca.markets/v2'
|
|
15466
15253
|
: 'https://api.alpaca.markets/v2'; // used by some, e.g. getAssets
|
|
15467
15254
|
this.v1beta1url = 'https://data.alpaca.markets/v1beta1'; // used for options endpoints
|
|
15468
15255
|
this.setMode('production'); // sets stockStreamUrl and optionStreamUrl
|
|
15469
15256
|
this.headers = {
|
|
15470
|
-
'APCA-API-KEY-ID':
|
|
15471
|
-
'APCA-API-SECRET-KEY':
|
|
15257
|
+
'APCA-API-KEY-ID': process.env.ALPACA_API_KEY,
|
|
15258
|
+
'APCA-API-SECRET-KEY': process.env.ALPACA_SECRET_KEY,
|
|
15472
15259
|
'Content-Type': 'application/json',
|
|
15473
15260
|
};
|
|
15474
15261
|
}
|
|
15475
|
-
static getInstance(
|
|
15262
|
+
static getInstance() {
|
|
15476
15263
|
if (!AlpacaMarketDataAPI.instance) {
|
|
15477
|
-
|
|
15478
|
-
throw new Error('AlpacaMarketDataAPI config is required for first initialization');
|
|
15479
|
-
}
|
|
15480
|
-
AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI(config);
|
|
15264
|
+
AlpacaMarketDataAPI.instance = new AlpacaMarketDataAPI();
|
|
15481
15265
|
}
|
|
15482
15266
|
return AlpacaMarketDataAPI.instance;
|
|
15483
15267
|
}
|
|
@@ -15500,8 +15284,8 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15500
15284
|
log(`${streamType} stream connected`, { type: 'info' });
|
|
15501
15285
|
const authMessage = {
|
|
15502
15286
|
action: 'auth',
|
|
15503
|
-
key:
|
|
15504
|
-
secret:
|
|
15287
|
+
key: process.env.ALPACA_API_KEY,
|
|
15288
|
+
secret: process.env.ALPACA_SECRET_KEY,
|
|
15505
15289
|
};
|
|
15506
15290
|
ws.send(JSON.stringify(authMessage));
|
|
15507
15291
|
});
|
|
@@ -15592,7 +15376,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15592
15376
|
const currentSubscriptions = streamType === 'stock' ? this.stockSubscriptions : this.optionSubscriptions;
|
|
15593
15377
|
Object.entries(subscriptions).forEach(([key, value]) => {
|
|
15594
15378
|
if (value) {
|
|
15595
|
-
currentSubscriptions[key] = (currentSubscriptions[key] || []).filter(
|
|
15379
|
+
currentSubscriptions[key] = (currentSubscriptions[key] || []).filter(s => !value.includes(s));
|
|
15596
15380
|
}
|
|
15597
15381
|
});
|
|
15598
15382
|
const unsubMessage = {
|
|
@@ -15655,11 +15439,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15655
15439
|
let pageCount = 0;
|
|
15656
15440
|
let currency = '';
|
|
15657
15441
|
// Initialize bar arrays for each symbol
|
|
15658
|
-
symbols.forEach(
|
|
15442
|
+
symbols.forEach(symbol => {
|
|
15659
15443
|
allBars[symbol] = [];
|
|
15660
15444
|
});
|
|
15661
15445
|
log(`Starting historical bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
|
|
15662
|
-
type: 'info'
|
|
15446
|
+
type: 'info'
|
|
15663
15447
|
});
|
|
15664
15448
|
while (hasMorePages) {
|
|
15665
15449
|
pageCount++;
|
|
@@ -15687,7 +15471,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15687
15471
|
allBars[symbol] = [...allBars[symbol], ...bars];
|
|
15688
15472
|
pageBarsCount += bars.length;
|
|
15689
15473
|
// Track date range for this page
|
|
15690
|
-
bars.forEach(
|
|
15474
|
+
bars.forEach(bar => {
|
|
15691
15475
|
const barDate = new Date(bar.t);
|
|
15692
15476
|
if (!earliestTimestamp || barDate < earliestTimestamp) {
|
|
15693
15477
|
earliestTimestamp = barDate;
|
|
@@ -15706,7 +15490,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15706
15490
|
? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
|
|
15707
15491
|
: 'unknown range';
|
|
15708
15492
|
log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
|
|
15709
|
-
type: 'info'
|
|
15493
|
+
type: 'info'
|
|
15710
15494
|
});
|
|
15711
15495
|
// Prevent infinite loops
|
|
15712
15496
|
if (pageCount > 1000) {
|
|
@@ -15715,11 +15499,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15715
15499
|
}
|
|
15716
15500
|
}
|
|
15717
15501
|
// Final summary
|
|
15718
|
-
const symbolCounts = Object.entries(allBars)
|
|
15719
|
-
.map(([symbol, bars]) => `${symbol}: ${bars.length}`)
|
|
15720
|
-
.join(', ');
|
|
15502
|
+
const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
|
|
15721
15503
|
log(`Historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
|
|
15722
|
-
type: 'info'
|
|
15504
|
+
type: 'info'
|
|
15723
15505
|
});
|
|
15724
15506
|
return {
|
|
15725
15507
|
bars: allBars,
|
|
@@ -15987,11 +15769,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
15987
15769
|
let totalBarsCount = 0;
|
|
15988
15770
|
let pageCount = 0;
|
|
15989
15771
|
// Initialize bar arrays for each symbol
|
|
15990
|
-
symbols.forEach(
|
|
15772
|
+
symbols.forEach(symbol => {
|
|
15991
15773
|
allBars[symbol] = [];
|
|
15992
15774
|
});
|
|
15993
15775
|
log(`Starting historical options bars fetch for ${symbolsStr} (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, {
|
|
15994
|
-
type: 'info'
|
|
15776
|
+
type: 'info'
|
|
15995
15777
|
});
|
|
15996
15778
|
while (hasMorePages) {
|
|
15997
15779
|
pageCount++;
|
|
@@ -16013,7 +15795,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16013
15795
|
allBars[symbol] = [...allBars[symbol], ...bars];
|
|
16014
15796
|
pageBarsCount += bars.length;
|
|
16015
15797
|
// Track date range for this page
|
|
16016
|
-
bars.forEach(
|
|
15798
|
+
bars.forEach(bar => {
|
|
16017
15799
|
const barDate = new Date(bar.t);
|
|
16018
15800
|
if (!earliestTimestamp || barDate < earliestTimestamp) {
|
|
16019
15801
|
earliestTimestamp = barDate;
|
|
@@ -16032,7 +15814,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16032
15814
|
? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
|
|
16033
15815
|
: 'unknown range';
|
|
16034
15816
|
log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} option bars (total: ${totalBarsCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
|
|
16035
|
-
type: 'info'
|
|
15817
|
+
type: 'info'
|
|
16036
15818
|
});
|
|
16037
15819
|
// Prevent infinite loops
|
|
16038
15820
|
if (pageCount > 1000) {
|
|
@@ -16041,11 +15823,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16041
15823
|
}
|
|
16042
15824
|
}
|
|
16043
15825
|
// Final summary
|
|
16044
|
-
const symbolCounts = Object.entries(allBars)
|
|
16045
|
-
.map(([symbol, bars]) => `${symbol}: ${bars.length}`)
|
|
16046
|
-
.join(', ');
|
|
15826
|
+
const symbolCounts = Object.entries(allBars).map(([symbol, bars]) => `${symbol}: ${bars.length}`).join(', ');
|
|
16047
15827
|
log(`Historical options bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages (${symbolCounts})`, {
|
|
16048
|
-
type: 'info'
|
|
15828
|
+
type: 'info'
|
|
16049
15829
|
});
|
|
16050
15830
|
return {
|
|
16051
15831
|
bars: allBars,
|
|
@@ -16069,11 +15849,11 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16069
15849
|
let totalTradesCount = 0;
|
|
16070
15850
|
let pageCount = 0;
|
|
16071
15851
|
// Initialize trades arrays for each symbol
|
|
16072
|
-
symbols.forEach(
|
|
15852
|
+
symbols.forEach(symbol => {
|
|
16073
15853
|
allTrades[symbol] = [];
|
|
16074
15854
|
});
|
|
16075
15855
|
log(`Starting historical options trades fetch for ${symbolsStr} (${params.start || 'no start'} to ${params.end || 'no end'})`, {
|
|
16076
|
-
type: 'info'
|
|
15856
|
+
type: 'info'
|
|
16077
15857
|
});
|
|
16078
15858
|
while (hasMorePages) {
|
|
16079
15859
|
pageCount++;
|
|
@@ -16095,7 +15875,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16095
15875
|
allTrades[symbol] = [...allTrades[symbol], ...trades];
|
|
16096
15876
|
pageTradesCount += trades.length;
|
|
16097
15877
|
// Track date range for this page
|
|
16098
|
-
trades.forEach(
|
|
15878
|
+
trades.forEach(trade => {
|
|
16099
15879
|
const tradeDate = new Date(trade.t);
|
|
16100
15880
|
if (!earliestTimestamp || tradeDate < earliestTimestamp) {
|
|
16101
15881
|
earliestTimestamp = tradeDate;
|
|
@@ -16114,7 +15894,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16114
15894
|
? `${earliestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })} to ${latestTimestamp.toLocaleDateString('en-US', { timeZone: 'America/New_York' })}`
|
|
16115
15895
|
: 'unknown range';
|
|
16116
15896
|
log(`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} option trades (total: ${totalTradesCount.toLocaleString()}) for ${symbolsStr}, date range: ${dateRangeStr}${hasMorePages ? ', more pages available' : ', complete'}`, {
|
|
16117
|
-
type: 'info'
|
|
15897
|
+
type: 'info'
|
|
16118
15898
|
});
|
|
16119
15899
|
// Prevent infinite loops
|
|
16120
15900
|
if (pageCount > 1000) {
|
|
@@ -16123,11 +15903,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16123
15903
|
}
|
|
16124
15904
|
}
|
|
16125
15905
|
// Final summary
|
|
16126
|
-
const symbolCounts = Object.entries(allTrades)
|
|
16127
|
-
.map(([symbol, trades]) => `${symbol}: ${trades.length}`)
|
|
16128
|
-
.join(', ');
|
|
15906
|
+
const symbolCounts = Object.entries(allTrades).map(([symbol, trades]) => `${symbol}: ${trades.length}`).join(', ');
|
|
16129
15907
|
log(`Historical options trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages (${symbolCounts})`, {
|
|
16130
|
-
type: 'info'
|
|
15908
|
+
type: 'info'
|
|
16131
15909
|
});
|
|
16132
15910
|
return {
|
|
16133
15911
|
trades: allTrades,
|
|
@@ -16272,9 +16050,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16272
16050
|
...(symbol && { symbols: symbol }),
|
|
16273
16051
|
...(mergedParams.limit && { limit: Math.min(50, maxLimit - fetchedCount).toString() }),
|
|
16274
16052
|
...(mergedParams.sort && { sort: mergedParams.sort }),
|
|
16275
|
-
...(mergedParams.include_content !== undefined
|
|
16276
|
-
? { include_content: mergedParams.include_content.toString() }
|
|
16277
|
-
: {}),
|
|
16053
|
+
...(mergedParams.include_content !== undefined ? { include_content: mergedParams.include_content.toString() } : {}),
|
|
16278
16054
|
...(pageToken && { page_token: pageToken }),
|
|
16279
16055
|
});
|
|
16280
16056
|
const url = `${this.v1beta1url}/news?${queryParams}`;
|
|
@@ -16318,6 +16094,8 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
|
|
|
16318
16094
|
return newsArticles;
|
|
16319
16095
|
}
|
|
16320
16096
|
}
|
|
16097
|
+
// Export the singleton instance
|
|
16098
|
+
const marketDataAPI = AlpacaMarketDataAPI.getInstance();
|
|
16321
16099
|
|
|
16322
16100
|
const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
16323
16101
|
/**
|
|
@@ -16329,11 +16107,11 @@ Websocket example
|
|
|
16329
16107
|
alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
|
|
16330
16108
|
*/
|
|
16331
16109
|
class AlpacaTradingAPI {
|
|
16332
|
-
static new(credentials
|
|
16333
|
-
return new AlpacaTradingAPI(credentials
|
|
16110
|
+
static new(credentials) {
|
|
16111
|
+
return new AlpacaTradingAPI(credentials);
|
|
16334
16112
|
}
|
|
16335
|
-
static getInstance(credentials
|
|
16336
|
-
return new AlpacaTradingAPI(credentials
|
|
16113
|
+
static getInstance(credentials) {
|
|
16114
|
+
return new AlpacaTradingAPI(credentials);
|
|
16337
16115
|
}
|
|
16338
16116
|
ws = null;
|
|
16339
16117
|
headers;
|
|
@@ -16347,7 +16125,6 @@ class AlpacaTradingAPI {
|
|
|
16347
16125
|
reconnectTimeout = null;
|
|
16348
16126
|
messageHandlers = new Map();
|
|
16349
16127
|
debugLogging = false;
|
|
16350
|
-
marketDataAPI;
|
|
16351
16128
|
/**
|
|
16352
16129
|
* Constructor for AlpacaTradingAPI
|
|
16353
16130
|
* @param credentials - Alpaca credentials,
|
|
@@ -16356,14 +16133,11 @@ class AlpacaTradingAPI {
|
|
|
16356
16133
|
* apiSecret: string; // Alpaca API secret
|
|
16357
16134
|
* type: AlpacaAccountType;
|
|
16358
16135
|
* orderType: AlpacaOrderType;
|
|
16359
|
-
* @param marketDataConfig - Market data API configuration
|
|
16360
16136
|
* @param options - Optional options
|
|
16361
16137
|
* debugLogging: boolean; // Whether to log messages of type 'debug'
|
|
16362
16138
|
*/
|
|
16363
|
-
constructor(credentials,
|
|
16139
|
+
constructor(credentials, options) {
|
|
16364
16140
|
this.credentials = credentials;
|
|
16365
|
-
// Initialize market data API instance
|
|
16366
|
-
this.marketDataAPI = AlpacaMarketDataAPI.getInstance(marketDataConfig);
|
|
16367
16141
|
// Set URLs based on account type
|
|
16368
16142
|
this.apiBaseUrl =
|
|
16369
16143
|
credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
|
|
@@ -16902,7 +16676,7 @@ class AlpacaTradingAPI {
|
|
|
16902
16676
|
this.log(`Found ${positions.length} positions to close`);
|
|
16903
16677
|
// Get latest quotes for all positions
|
|
16904
16678
|
const symbols = positions.map((position) => position.symbol);
|
|
16905
|
-
const quotesResponse = await
|
|
16679
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
16906
16680
|
const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
|
|
16907
16681
|
if (lengthOfQuotes === 0) {
|
|
16908
16682
|
this.log('No quotes available for positions, received 0 quotes', {
|
|
@@ -16969,7 +16743,7 @@ class AlpacaTradingAPI {
|
|
|
16969
16743
|
this.log(`Cancelled all open orders`);
|
|
16970
16744
|
// Get latest quotes for all positions
|
|
16971
16745
|
const symbols = positions.map((position) => position.symbol);
|
|
16972
|
-
const quotesResponse = await
|
|
16746
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
16973
16747
|
// Create limit orders to close each position
|
|
16974
16748
|
for (const position of positions) {
|
|
16975
16749
|
const quote = quotesResponse.quotes[position.symbol];
|
|
@@ -17652,13 +17426,6 @@ class AlpacaTradingAPI {
|
|
|
17652
17426
|
}
|
|
17653
17427
|
}
|
|
17654
17428
|
|
|
17655
|
-
// Export factory functions for easier instantiation
|
|
17656
|
-
const createAlpacaTradingAPI = (credentials, marketDataConfig) => {
|
|
17657
|
-
return new AlpacaTradingAPI(credentials, marketDataConfig);
|
|
17658
|
-
};
|
|
17659
|
-
const createAlpacaMarketDataAPI = (config) => {
|
|
17660
|
-
return AlpacaMarketDataAPI.getInstance(config);
|
|
17661
|
-
};
|
|
17662
17429
|
const disco = {
|
|
17663
17430
|
types: Types,
|
|
17664
17431
|
alpaca: {
|
|
@@ -17706,31 +17473,14 @@ const disco = {
|
|
|
17706
17473
|
calculateFibonacciLevels: calculateFibonacciLevels,
|
|
17707
17474
|
},
|
|
17708
17475
|
time: {
|
|
17709
|
-
toUnixTimestamp: toUnixTimestamp,
|
|
17710
|
-
getTimeAgo: getTimeAgo,
|
|
17711
|
-
timeAgo: timeAgo,
|
|
17712
|
-
normalizeDate: normalizeDate,
|
|
17713
|
-
getDateInNY: getDateInNY,
|
|
17714
|
-
createMarketTimeUtil: createMarketTimeUtil,
|
|
17715
|
-
getStartAndEndTimestamps: getStartAndEndTimestamps,
|
|
17716
17476
|
getStartAndEndDates: getStartAndEndDates,
|
|
17717
17477
|
getMarketOpenClose: getMarketOpenClose,
|
|
17718
|
-
calculateTimeRange: calculateTimeRange,
|
|
17719
|
-
calculateDaysLeft: calculateDaysLeft,
|
|
17720
|
-
formatDate: formatDate /* move to format, keeping here for compatibility */,
|
|
17721
|
-
currentTimeET: currentTimeET,
|
|
17722
|
-
MarketTimeUtil: MarketTimeUtil,
|
|
17723
|
-
MARKET_TIMES: MARKET_TIMES,
|
|
17724
|
-
getLastTradingDateYYYYMMDD: getLastTradingDateYYYYMMDD,
|
|
17725
17478
|
getLastFullTradingDate: getLastFullTradingDate,
|
|
17726
17479
|
getNextMarketDay: getNextMarketDay,
|
|
17727
|
-
parseETDateFromAV: parseETDateFromAV,
|
|
17728
|
-
formatToUSEastern: formatToUSEastern,
|
|
17729
|
-
unixTimetoUSEastern: unixTimetoUSEastern,
|
|
17730
17480
|
getMarketStatus: getMarketStatus,
|
|
17731
|
-
timeDiffString: timeDiffString,
|
|
17732
17481
|
getNYTimeZone: getNYTimeZone,
|
|
17733
17482
|
getTradingDate: getTradingDate,
|
|
17483
|
+
getTradingStartAndEndDates: getTradingStartAndEndDates,
|
|
17734
17484
|
},
|
|
17735
17485
|
utils: {
|
|
17736
17486
|
logIfDebug: logIfDebug,
|
|
@@ -17741,7 +17491,5 @@ const disco = {
|
|
|
17741
17491
|
|
|
17742
17492
|
exports.AlpacaMarketDataAPI = AlpacaMarketDataAPI;
|
|
17743
17493
|
exports.AlpacaTradingAPI = AlpacaTradingAPI;
|
|
17744
|
-
exports.createAlpacaMarketDataAPI = createAlpacaMarketDataAPI;
|
|
17745
|
-
exports.createAlpacaTradingAPI = createAlpacaTradingAPI;
|
|
17746
17494
|
exports.disco = disco;
|
|
17747
17495
|
//# sourceMappingURL=index.cjs.map
|