hedgequantx 1.2.147 → 1.3.1
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 +115 -71
- package/package.json +1 -1
- package/src/app.js +1 -22
- package/src/menus/connect.js +2 -1
- package/src/menus/dashboard.js +135 -31
- package/src/services/index.js +1 -1
- package/src/services/projectx/index.js +345 -0
- package/src/services/projectx/market.js +145 -0
- package/src/services/projectx/stats.js +110 -0
- package/src/services/rithmic/accounts.js +183 -0
- package/src/services/rithmic/handlers.js +191 -0
- package/src/services/rithmic/index.js +69 -673
- package/src/services/rithmic/orders.js +192 -0
- package/src/ui/index.js +23 -1
- package/src/services/projectx.js +0 -771
package/src/services/projectx.js
DELETED
|
@@ -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 };
|