hedgequantx 1.1.1 → 1.2.32

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.
@@ -1,22 +1,60 @@
1
1
  /**
2
- * ProjectX API Service
3
- * Handles all API communication with PropFirm platforms
2
+ * @fileoverview ProjectX API Service with security features
3
+ * @module services/projectx
4
4
  */
5
5
 
6
6
  const https = require('https');
7
7
  const { PROPFIRMS } = require('../config');
8
+ const {
9
+ validateUsername,
10
+ validatePassword,
11
+ validateApiKey,
12
+ validateAccountId,
13
+ sanitizeString,
14
+ maskSensitive
15
+ } = require('../security');
16
+ const { getLimiter } = require('../security/rateLimit');
8
17
 
18
+ /**
19
+ * ProjectX API Service
20
+ * Handles all API communication with PropFirm platforms
21
+ */
9
22
  class ProjectXService {
23
+ /**
24
+ * Creates a new ProjectX service instance
25
+ * @param {string} [propfirmKey='topstep'] - PropFirm identifier
26
+ */
10
27
  constructor(propfirmKey = 'topstep') {
11
28
  this.propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
12
29
  this.token = null;
13
30
  this.user = null;
31
+ this.rateLimiter = getLimiter('api');
32
+ this.loginLimiter = getLimiter('login');
33
+ this.orderLimiter = getLimiter('orders');
14
34
  }
15
35
 
16
36
  /**
17
- * Make HTTPS request
37
+ * Makes a rate-limited HTTPS request
38
+ * @param {string} host - API host
39
+ * @param {string} path - API path
40
+ * @param {string} [method='GET'] - HTTP method
41
+ * @param {Object} [data=null] - Request body
42
+ * @param {string} [limiterType='api'] - Rate limiter to use
43
+ * @returns {Promise<{statusCode: number, data: any}>}
44
+ * @private
18
45
  */
19
- async _request(host, path, method = 'GET', data = null) {
46
+ async _request(host, path, method = 'GET', data = null, limiterType = 'api') {
47
+ const limiter = limiterType === 'login' ? this.loginLimiter :
48
+ limiterType === 'orders' ? this.orderLimiter : this.rateLimiter;
49
+
50
+ return limiter.execute(() => this._doRequest(host, path, method, data));
51
+ }
52
+
53
+ /**
54
+ * Performs the actual HTTPS request
55
+ * @private
56
+ */
57
+ async _doRequest(host, path, method, data) {
20
58
  return new Promise((resolve, reject) => {
21
59
  const options = {
22
60
  hostname: host,
@@ -25,8 +63,10 @@ class ProjectXService {
25
63
  method: method,
26
64
  headers: {
27
65
  'Content-Type': 'application/json',
28
- 'Accept': 'application/json'
29
- }
66
+ 'Accept': 'application/json',
67
+ 'User-Agent': 'HedgeQuantX-CLI/1.1.1'
68
+ },
69
+ timeout: 15000
30
70
  };
31
71
 
32
72
  if (this.token) {
@@ -46,7 +86,7 @@ class ProjectXService {
46
86
  });
47
87
 
48
88
  req.on('error', reject);
49
- req.setTimeout(10000, () => {
89
+ req.on('timeout', () => {
50
90
  req.destroy();
51
91
  reject(new Error('Request timeout'));
52
92
  });
@@ -58,32 +98,70 @@ class ProjectXService {
58
98
 
59
99
  // ==================== AUTH ====================
60
100
 
101
+ /**
102
+ * Authenticates with username and password
103
+ * @param {string} userName - Username
104
+ * @param {string} password - Password
105
+ * @returns {Promise<{success: boolean, token?: string, error?: string}>}
106
+ */
61
107
  async login(userName, password) {
62
108
  try {
63
- const response = await this._request(this.propfirm.userApi, '/Login', 'POST', { userName, password });
109
+ // Validate inputs
110
+ validateUsername(userName);
111
+ validatePassword(password);
112
+
113
+ const response = await this._request(
114
+ this.propfirm.userApi,
115
+ '/Login',
116
+ 'POST',
117
+ { userName: sanitizeString(userName), password },
118
+ 'login'
119
+ );
120
+
64
121
  if (response.statusCode === 200 && response.data.token) {
65
122
  this.token = response.data.token;
66
- return { success: true, token: this.token };
123
+ return { success: true, token: maskSensitive(this.token) };
67
124
  }
125
+
68
126
  return { success: false, error: response.data.errorMessage || 'Invalid credentials' };
69
127
  } catch (error) {
70
128
  return { success: false, error: error.message };
71
129
  }
72
130
  }
73
131
 
132
+ /**
133
+ * Authenticates with API key
134
+ * @param {string} userName - Username
135
+ * @param {string} apiKey - API key
136
+ * @returns {Promise<{success: boolean, token?: string, error?: string}>}
137
+ */
74
138
  async loginWithApiKey(userName, apiKey) {
75
139
  try {
76
- const response = await this._request(this.propfirm.userApi, '/Login/key', 'POST', { userName, apiKey });
140
+ validateUsername(userName);
141
+ validateApiKey(apiKey);
142
+
143
+ const response = await this._request(
144
+ this.propfirm.userApi,
145
+ '/Login/key',
146
+ 'POST',
147
+ { userName: sanitizeString(userName), apiKey },
148
+ 'login'
149
+ );
150
+
77
151
  if (response.statusCode === 200 && response.data.token) {
78
152
  this.token = response.data.token;
79
- return { success: true, token: this.token };
153
+ return { success: true, token: maskSensitive(this.token) };
80
154
  }
155
+
81
156
  return { success: false, error: response.data.errorMessage || 'Invalid API key' };
82
157
  } catch (error) {
83
158
  return { success: false, error: error.message };
84
159
  }
85
160
  }
86
161
 
162
+ /**
163
+ * Logs out and clears credentials
164
+ */
87
165
  logout() {
88
166
  this.token = null;
89
167
  this.user = null;
@@ -91,13 +169,19 @@ class ProjectXService {
91
169
 
92
170
  // ==================== USER ====================
93
171
 
172
+ /**
173
+ * Gets current user information
174
+ * @returns {Promise<{success: boolean, user?: Object, error?: string}>}
175
+ */
94
176
  async getUser() {
95
177
  try {
96
178
  const response = await this._request(this.propfirm.userApi, '/User', 'GET');
179
+
97
180
  if (response.statusCode === 200) {
98
181
  this.user = response.data;
99
182
  return { success: true, user: response.data };
100
183
  }
184
+
101
185
  return { success: false, error: 'Failed to get user info' };
102
186
  } catch (error) {
103
187
  return { success: false, error: error.message };
@@ -106,13 +190,19 @@ class ProjectXService {
106
190
 
107
191
  // ==================== ACCOUNTS ====================
108
192
 
193
+ /**
194
+ * Gets all trading accounts
195
+ * @returns {Promise<{success: boolean, accounts?: Array, error?: string}>}
196
+ */
109
197
  async getTradingAccounts() {
110
198
  try {
111
199
  const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
200
+
112
201
  if (response.statusCode === 200) {
113
202
  const accounts = Array.isArray(response.data) ? response.data : [];
114
203
  return { success: true, accounts };
115
204
  }
205
+
116
206
  return { success: false, accounts: [], error: 'Failed to get accounts' };
117
207
  } catch (error) {
118
208
  return { success: false, accounts: [], error: error.message };
@@ -121,81 +211,124 @@ class ProjectXService {
121
211
 
122
212
  // ==================== TRADING (GatewayAPI) ====================
123
213
 
214
+ /**
215
+ * Gets open positions for an account
216
+ * @param {number|string} accountId - Account ID
217
+ * @returns {Promise<{success: boolean, positions?: Array, error?: string}>}
218
+ */
124
219
  async getPositions(accountId) {
125
220
  try {
221
+ const id = validateAccountId(accountId);
222
+
126
223
  const response = await this._request(
127
224
  this.propfirm.gatewayApi,
128
225
  '/api/Position/searchOpen',
129
226
  'POST',
130
- { accountId: parseInt(accountId) }
227
+ { accountId: id }
131
228
  );
229
+
132
230
  if (response.statusCode === 200) {
133
231
  const positions = response.data.positions || response.data || [];
134
232
  return { success: true, positions: Array.isArray(positions) ? positions : [] };
135
233
  }
234
+
136
235
  return { success: true, positions: [] };
137
236
  } catch (error) {
138
237
  return { success: true, positions: [], error: error.message };
139
238
  }
140
239
  }
141
240
 
241
+ /**
242
+ * Gets open orders for an account
243
+ * @param {number|string} accountId - Account ID
244
+ * @returns {Promise<{success: boolean, orders?: Array, error?: string}>}
245
+ */
142
246
  async getOrders(accountId) {
143
247
  try {
248
+ const id = validateAccountId(accountId);
249
+
144
250
  const response = await this._request(
145
251
  this.propfirm.gatewayApi,
146
252
  '/api/Order/searchOpen',
147
253
  'POST',
148
- { accountId: parseInt(accountId) }
254
+ { accountId: id }
149
255
  );
256
+
150
257
  if (response.statusCode === 200) {
151
258
  const orders = response.data.orders || response.data || [];
152
259
  return { success: true, orders: Array.isArray(orders) ? orders : [] };
153
260
  }
261
+
154
262
  return { success: true, orders: [] };
155
263
  } catch (error) {
156
264
  return { success: true, orders: [], error: error.message };
157
265
  }
158
266
  }
159
267
 
268
+ /**
269
+ * Places an order
270
+ * @param {Object} orderData - Order data
271
+ * @returns {Promise<{success: boolean, order?: Object, error?: string}>}
272
+ */
160
273
  async placeOrder(orderData) {
161
274
  try {
162
275
  const response = await this._request(
163
276
  this.propfirm.gatewayApi,
164
277
  '/api/Order/place',
165
278
  'POST',
166
- orderData
279
+ orderData,
280
+ 'orders'
167
281
  );
282
+
168
283
  if (response.statusCode === 200 && response.data.success) {
169
284
  return { success: true, order: response.data };
170
285
  }
286
+
171
287
  return { success: false, error: response.data.errorMessage || 'Order failed' };
172
288
  } catch (error) {
173
289
  return { success: false, error: error.message };
174
290
  }
175
291
  }
176
292
 
293
+ /**
294
+ * Cancels an order
295
+ * @param {number|string} orderId - Order ID
296
+ * @returns {Promise<{success: boolean, error?: string}>}
297
+ */
177
298
  async cancelOrder(orderId) {
178
299
  try {
179
300
  const response = await this._request(
180
301
  this.propfirm.gatewayApi,
181
302
  '/api/Order/cancel',
182
303
  'POST',
183
- { orderId: parseInt(orderId) }
304
+ { orderId: parseInt(orderId, 10) },
305
+ 'orders'
184
306
  );
307
+
185
308
  return { success: response.statusCode === 200 && response.data.success };
186
309
  } catch (error) {
187
310
  return { success: false, error: error.message };
188
311
  }
189
312
  }
190
313
 
314
+ /**
315
+ * Closes a position
316
+ * @param {number|string} accountId - Account ID
317
+ * @param {string} contractId - Contract ID
318
+ * @returns {Promise<{success: boolean, error?: string}>}
319
+ */
191
320
  async closePosition(accountId, contractId) {
192
321
  try {
322
+ const id = validateAccountId(accountId);
323
+
193
324
  const response = await this._request(
194
325
  this.propfirm.gatewayApi,
195
326
  '/api/Position/closeContract',
196
327
  'POST',
197
- { accountId: parseInt(accountId), contractId }
328
+ { accountId: id, contractId },
329
+ 'orders'
198
330
  );
331
+
199
332
  return { success: response.statusCode === 200 && response.data.success };
200
333
  } catch (error) {
201
334
  return { success: false, error: error.message };
@@ -204,18 +337,25 @@ class ProjectXService {
204
337
 
205
338
  // ==================== TRADES & STATS ====================
206
339
 
340
+ /**
341
+ * Gets trade history for an account
342
+ * @param {number|string} accountId - Account ID
343
+ * @param {number} [days=30] - Number of days to fetch
344
+ * @returns {Promise<{success: boolean, trades?: Array, error?: string}>}
345
+ */
207
346
  async getTradeHistory(accountId, days = 30) {
208
347
  try {
348
+ const id = validateAccountId(accountId);
209
349
  const endDate = new Date();
210
350
  const startDate = new Date();
211
351
  startDate.setDate(startDate.getDate() - days);
212
-
352
+
213
353
  const response = await this._request(
214
354
  this.propfirm.gatewayApi,
215
355
  '/api/Trade/search',
216
356
  'POST',
217
- {
218
- accountId: parseInt(accountId),
357
+ {
358
+ accountId: id,
219
359
  startTimestamp: startDate.toISOString(),
220
360
  endTimestamp: endDate.toISOString()
221
361
  }
@@ -228,9 +368,9 @@ class ProjectXService {
228
368
  } else if (response.data.trades) {
229
369
  trades = response.data.trades;
230
370
  }
231
-
232
- return {
233
- success: true,
371
+
372
+ return {
373
+ success: true,
234
374
  trades: trades.map(t => ({
235
375
  ...t,
236
376
  timestamp: t.creationTimestamp || t.timestamp,
@@ -238,23 +378,30 @@ class ProjectXService {
238
378
  }))
239
379
  };
240
380
  }
381
+
241
382
  return { success: true, trades: [] };
242
383
  } catch (error) {
243
384
  return { success: true, trades: [], error: error.message };
244
385
  }
245
386
  }
246
387
 
388
+ /**
389
+ * Gets daily statistics for an account
390
+ * @param {number|string} accountId - Account ID
391
+ * @returns {Promise<{success: boolean, stats?: Array, error?: string}>}
392
+ */
247
393
  async getDailyStats(accountId) {
248
394
  try {
395
+ const id = validateAccountId(accountId);
249
396
  const now = new Date();
250
397
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
251
-
398
+
252
399
  const response = await this._request(
253
400
  this.propfirm.gatewayApi,
254
401
  '/api/Trade/search',
255
402
  'POST',
256
- {
257
- accountId: parseInt(accountId),
403
+ {
404
+ accountId: id,
258
405
  startTimestamp: startOfMonth.toISOString(),
259
406
  endTimestamp: now.toISOString()
260
407
  }
@@ -262,7 +409,7 @@ class ProjectXService {
262
409
 
263
410
  if (response.statusCode === 200 && response.data) {
264
411
  let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
265
-
412
+
266
413
  // Group by day
267
414
  const dailyPnL = {};
268
415
  trades.forEach(t => {
@@ -273,28 +420,34 @@ class ProjectXService {
273
420
  dailyPnL[key] = (dailyPnL[key] || 0) + (t.profitAndLoss || t.pnl || 0);
274
421
  }
275
422
  });
276
-
277
- return {
278
- success: true,
423
+
424
+ return {
425
+ success: true,
279
426
  stats: Object.entries(dailyPnL).map(([date, pnl]) => ({ date, profitAndLoss: pnl }))
280
427
  };
281
428
  }
429
+
282
430
  return { success: false, stats: [] };
283
431
  } catch (error) {
284
432
  return { success: false, stats: [], error: error.message };
285
433
  }
286
434
  }
287
435
 
436
+ /**
437
+ * Gets lifetime statistics for an account
438
+ * @param {number|string} accountId - Account ID
439
+ * @returns {Promise<{success: boolean, stats?: Object, error?: string}>}
440
+ */
288
441
  async getLifetimeStats(accountId) {
289
442
  try {
290
443
  const tradesResult = await this.getTradeHistory(accountId, 90);
291
-
444
+
292
445
  if (!tradesResult.success || tradesResult.trades.length === 0) {
293
446
  return { success: true, stats: null };
294
447
  }
295
448
 
296
449
  const trades = tradesResult.trades;
297
- let stats = {
450
+ const stats = {
298
451
  totalTrades: trades.length,
299
452
  winningTrades: 0,
300
453
  losingTrades: 0,
@@ -309,16 +462,18 @@ class ProjectXService {
309
462
  shortTrades: 0
310
463
  };
311
464
 
312
- let consecutiveWins = 0, consecutiveLosses = 0;
465
+ let consecutiveWins = 0;
466
+ let consecutiveLosses = 0;
313
467
 
314
468
  trades.forEach(t => {
315
469
  const pnl = t.profitAndLoss || t.pnl || 0;
316
470
  const size = t.size || t.quantity || 1;
317
-
471
+
318
472
  stats.totalVolume += Math.abs(size);
473
+
319
474
  if (t.side === 0) stats.longTrades++;
320
475
  else if (t.side === 1) stats.shortTrades++;
321
-
476
+
322
477
  if (pnl > 0) {
323
478
  stats.winningTrades++;
324
479
  stats.totalWinAmount += pnl;
@@ -348,22 +503,215 @@ class ProjectXService {
348
503
 
349
504
  // ==================== CONTRACTS ====================
350
505
 
506
+ /**
507
+ * Searches for contracts
508
+ * @param {string} searchText - Search text
509
+ * @returns {Promise<{success: boolean, contracts?: Array, error?: string}>}
510
+ */
351
511
  async searchContracts(searchText) {
352
512
  try {
353
513
  const response = await this._request(
354
514
  this.propfirm.gatewayApi,
355
515
  '/api/Contract/search',
356
516
  'POST',
357
- { searchText, live: false }
517
+ { searchText: sanitizeString(searchText), live: false }
358
518
  );
519
+
359
520
  if (response.statusCode === 200) {
360
521
  return { success: true, contracts: response.data.contracts || response.data || [] };
361
522
  }
523
+
362
524
  return { success: false, contracts: [] };
363
525
  } catch (error) {
364
526
  return { success: false, contracts: [], error: error.message };
365
527
  }
366
528
  }
529
+
530
+ // ==================== MARKET STATUS ====================
531
+
532
+ /**
533
+ * Gets US market holidays for the current year
534
+ * @returns {Array<{date: string, name: string, earlyClose: boolean}>}
535
+ */
536
+ getMarketHolidays() {
537
+ const year = new Date().getFullYear();
538
+
539
+ // CME Futures holidays - markets closed or early close
540
+ // Dates are approximate, actual dates may vary slightly
541
+ const holidays = [
542
+ // New Year's Day
543
+ { date: `${year}-01-01`, name: "New Year's Day", earlyClose: false },
544
+ // Martin Luther King Jr. Day (3rd Monday of January)
545
+ { date: this._getNthWeekday(year, 0, 1, 3), name: 'MLK Day', earlyClose: false },
546
+ // Presidents Day (3rd Monday of February)
547
+ { date: this._getNthWeekday(year, 1, 1, 3), name: "Presidents' Day", earlyClose: false },
548
+ // Good Friday (Friday before Easter) - calculated dynamically
549
+ { date: this._getGoodFriday(year), name: 'Good Friday', earlyClose: false },
550
+ // Memorial Day (Last Monday of May)
551
+ { date: this._getLastWeekday(year, 4, 1), name: 'Memorial Day', earlyClose: false },
552
+ // Juneteenth (June 19)
553
+ { date: `${year}-06-19`, name: 'Juneteenth', earlyClose: false },
554
+ // Independence Day (July 4)
555
+ { date: `${year}-07-04`, name: 'Independence Day', earlyClose: false },
556
+ { date: `${year}-07-03`, name: 'Independence Day Eve', earlyClose: true },
557
+ // Labor Day (1st Monday of September)
558
+ { date: this._getNthWeekday(year, 8, 1, 1), name: 'Labor Day', earlyClose: false },
559
+ // Thanksgiving (4th Thursday of November)
560
+ { date: this._getNthWeekday(year, 10, 4, 4), name: 'Thanksgiving', earlyClose: false },
561
+ { date: this._getDayAfter(this._getNthWeekday(year, 10, 4, 4)), name: 'Black Friday', earlyClose: true },
562
+ // Christmas
563
+ { date: `${year}-12-25`, name: 'Christmas Day', earlyClose: false },
564
+ { date: `${year}-12-24`, name: 'Christmas Eve', earlyClose: true },
565
+ // New Year's Eve
566
+ { date: `${year}-12-31`, name: "New Year's Eve", earlyClose: true },
567
+ ];
568
+
569
+ return holidays;
570
+ }
571
+
572
+ /**
573
+ * Helper: Get nth weekday of a month
574
+ * @private
575
+ */
576
+ _getNthWeekday(year, month, weekday, n) {
577
+ const firstDay = new Date(year, month, 1);
578
+ let count = 0;
579
+ for (let day = 1; day <= 31; day++) {
580
+ const d = new Date(year, month, day);
581
+ if (d.getMonth() !== month) break;
582
+ if (d.getDay() === weekday) {
583
+ count++;
584
+ if (count === n) {
585
+ return d.toISOString().split('T')[0];
586
+ }
587
+ }
588
+ }
589
+ return null;
590
+ }
591
+
592
+ /**
593
+ * Helper: Get last weekday of a month
594
+ * @private
595
+ */
596
+ _getLastWeekday(year, month, weekday) {
597
+ const lastDay = new Date(year, month + 1, 0);
598
+ for (let day = lastDay.getDate(); day >= 1; day--) {
599
+ const d = new Date(year, month, day);
600
+ if (d.getDay() === weekday) {
601
+ return d.toISOString().split('T')[0];
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+
607
+ /**
608
+ * Helper: Get Good Friday (Friday before Easter)
609
+ * @private
610
+ */
611
+ _getGoodFriday(year) {
612
+ // Easter calculation (Anonymous Gregorian algorithm)
613
+ const a = year % 19;
614
+ const b = Math.floor(year / 100);
615
+ const c = year % 100;
616
+ const d = Math.floor(b / 4);
617
+ const e = b % 4;
618
+ const f = Math.floor((b + 8) / 25);
619
+ const g = Math.floor((b - f + 1) / 3);
620
+ const h = (19 * a + b - d - g + 15) % 30;
621
+ const i = Math.floor(c / 4);
622
+ const k = c % 4;
623
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
624
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
625
+ const month = Math.floor((h + l - 7 * m + 114) / 31) - 1;
626
+ const day = ((h + l - 7 * m + 114) % 31) + 1;
627
+
628
+ // Good Friday is 2 days before Easter
629
+ const easter = new Date(year, month, day);
630
+ const goodFriday = new Date(easter);
631
+ goodFriday.setDate(easter.getDate() - 2);
632
+ return goodFriday.toISOString().split('T')[0];
633
+ }
634
+
635
+ /**
636
+ * Helper: Get day after a date
637
+ * @private
638
+ */
639
+ _getDayAfter(dateStr) {
640
+ const d = new Date(dateStr);
641
+ d.setDate(d.getDate() + 1);
642
+ return d.toISOString().split('T')[0];
643
+ }
644
+
645
+ /**
646
+ * Checks if today is a market holiday
647
+ * @returns {{isHoliday: boolean, holiday?: {date: string, name: string, earlyClose: boolean}}}
648
+ */
649
+ checkHoliday() {
650
+ const today = new Date().toISOString().split('T')[0];
651
+ const holidays = this.getMarketHolidays();
652
+ const holiday = holidays.find(h => h.date === today);
653
+
654
+ if (holiday) {
655
+ return { isHoliday: !holiday.earlyClose, holiday };
656
+ }
657
+ return { isHoliday: false };
658
+ }
659
+
660
+ /**
661
+ * Checks if futures market is open based on CME hours and holidays
662
+ * @returns {{isOpen: boolean, message: string}}
663
+ */
664
+ checkMarketHours() {
665
+ const now = new Date();
666
+ const utcDay = now.getUTCDay();
667
+ const utcHour = now.getUTCHours();
668
+
669
+ // Check holidays first
670
+ const holidayCheck = this.checkHoliday();
671
+ if (holidayCheck.isHoliday) {
672
+ return { isOpen: false, message: `Market closed - ${holidayCheck.holiday.name}` };
673
+ }
674
+ if (holidayCheck.holiday && holidayCheck.holiday.earlyClose && utcHour >= 18) {
675
+ return { isOpen: false, message: `Market closed early - ${holidayCheck.holiday.name}` };
676
+ }
677
+
678
+ // CME Futures hours (in UTC):
679
+ // Open: Sunday 23:00 UTC (6:00 PM ET)
680
+ // Close: Friday 22:00 UTC (5:00 PM ET)
681
+ // Daily maintenance: 22:00-23:00 UTC (5:00-6:00 PM ET)
682
+
683
+ // Saturday - closed all day
684
+ if (utcDay === 6) {
685
+ return { isOpen: false, message: 'Market closed - Weekend (Saturday)' };
686
+ }
687
+
688
+ // Sunday before 23:00 UTC - closed
689
+ if (utcDay === 0 && utcHour < 23) {
690
+ return { isOpen: false, message: 'Market closed - Opens Sunday 6:00 PM ET' };
691
+ }
692
+
693
+ // Friday after 22:00 UTC - closed
694
+ if (utcDay === 5 && utcHour >= 22) {
695
+ return { isOpen: false, message: 'Market closed - Weekend' };
696
+ }
697
+
698
+ // Daily maintenance 22:00-23:00 UTC (except Friday close)
699
+ if (utcHour === 22 && utcDay !== 5) {
700
+ return { isOpen: false, message: 'Market closed - Daily maintenance (5:00-6:00 PM ET)' };
701
+ }
702
+
703
+ return { isOpen: true, message: 'Market is open' };
704
+ }
705
+
706
+ /**
707
+ * Gets market status for an account
708
+ * @param {number|string} accountId - Account ID
709
+ * @returns {Promise<{success: boolean, isOpen: boolean, message: string}>}
710
+ */
711
+ async getMarketStatus(accountId) {
712
+ const hours = this.checkMarketHours();
713
+ return { success: true, isOpen: hours.isOpen, message: hours.message };
714
+ }
367
715
  }
368
716
 
369
717
  module.exports = { ProjectXService };