hedgequantx 1.1.1 → 1.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -136
- package/bin/cli.js +28 -2076
- package/package.json +3 -3
- package/src/app.js +550 -0
- package/src/config/index.js +16 -2
- package/src/config/propfirms.js +324 -12
- package/src/pages/accounts.js +115 -0
- package/src/pages/algo.js +538 -0
- package/src/pages/index.js +13 -2
- package/src/pages/orders.js +114 -0
- package/src/pages/positions.js +115 -0
- package/src/pages/stats.js +212 -3
- package/src/pages/user.js +92 -0
- package/src/security/encryption.js +168 -0
- package/src/security/index.js +61 -0
- package/src/security/rateLimit.js +155 -0
- package/src/security/validation.js +253 -0
- package/src/services/hqx-server.js +34 -17
- package/src/services/index.js +2 -1
- package/src/services/projectx.js +383 -35
- package/src/services/session.js +150 -38
- package/src/ui/index.js +4 -1
- package/src/ui/menu.js +154 -0
- package/src/services/local-storage.js +0 -309
package/src/services/projectx.js
CHANGED
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ProjectX API Service
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 };
|