hedgequantx 1.3.0 → 1.3.2

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