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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX API Service
|
|
3
|
+
* Main service for ProjectX prop firm connections
|
|
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
|
+
const { getMarketHolidays, checkHoliday, checkMarketHours } = require('./market');
|
|
18
|
+
const { calculateLifetimeStats, calculateDailyPnL, formatTrades } = require('./stats');
|
|
19
|
+
|
|
20
|
+
class ProjectXService {
|
|
21
|
+
constructor(propfirmKey = 'topstep') {
|
|
22
|
+
this.propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
|
|
23
|
+
this.propfirmKey = propfirmKey;
|
|
24
|
+
this.token = null;
|
|
25
|
+
this.user = null;
|
|
26
|
+
this.rateLimiter = getLimiter('api');
|
|
27
|
+
this.loginLimiter = getLimiter('login');
|
|
28
|
+
this.orderLimiter = getLimiter('orders');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getToken() { return this.token; }
|
|
32
|
+
getPropfirm() { return this.propfirmKey; }
|
|
33
|
+
|
|
34
|
+
// ==================== HTTP ====================
|
|
35
|
+
|
|
36
|
+
async _request(host, path, method = 'GET', data = null, limiterType = 'api') {
|
|
37
|
+
const limiter = limiterType === 'login' ? this.loginLimiter :
|
|
38
|
+
limiterType === 'orders' ? this.orderLimiter : this.rateLimiter;
|
|
39
|
+
return limiter.execute(() => this._doRequest(host, path, method, data));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async _doRequest(host, path, method, data) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const options = {
|
|
45
|
+
hostname: host,
|
|
46
|
+
port: 443,
|
|
47
|
+
path: path,
|
|
48
|
+
method: method,
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'Accept': 'application/json',
|
|
52
|
+
'User-Agent': 'HedgeQuantX-CLI/1.3.0'
|
|
53
|
+
},
|
|
54
|
+
timeout: 15000
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (this.token) {
|
|
58
|
+
options.headers['Authorization'] = `Bearer ${this.token}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const req = https.request(options, (res) => {
|
|
62
|
+
let body = '';
|
|
63
|
+
res.on('data', chunk => body += chunk);
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
resolve({ statusCode: res.statusCode, data: body });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
75
|
+
if (data) req.write(JSON.stringify(data));
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ==================== AUTH ====================
|
|
81
|
+
|
|
82
|
+
async login(userName, password) {
|
|
83
|
+
try {
|
|
84
|
+
validateUsername(userName);
|
|
85
|
+
validatePassword(password);
|
|
86
|
+
|
|
87
|
+
const response = await this._request(
|
|
88
|
+
this.propfirm.userApi, '/Login', 'POST',
|
|
89
|
+
{ userName: sanitizeString(userName), password },
|
|
90
|
+
'login'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (response.statusCode === 200 && response.data.token) {
|
|
94
|
+
this.token = response.data.token;
|
|
95
|
+
return { success: true, token: maskSensitive(this.token) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { success: false, error: response.data.errorMessage || 'Invalid credentials' };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return { success: false, error: error.message };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async loginWithApiKey(userName, apiKey) {
|
|
105
|
+
try {
|
|
106
|
+
validateUsername(userName);
|
|
107
|
+
validateApiKey(apiKey);
|
|
108
|
+
|
|
109
|
+
const response = await this._request(
|
|
110
|
+
this.propfirm.userApi, '/Login/key', 'POST',
|
|
111
|
+
{ userName: sanitizeString(userName), apiKey },
|
|
112
|
+
'login'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (response.statusCode === 200 && response.data.token) {
|
|
116
|
+
this.token = response.data.token;
|
|
117
|
+
return { success: true, token: maskSensitive(this.token) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { success: false, error: response.data.errorMessage || 'Invalid API key' };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return { success: false, error: error.message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logout() {
|
|
127
|
+
this.token = null;
|
|
128
|
+
this.user = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==================== USER ====================
|
|
132
|
+
|
|
133
|
+
async getUser() {
|
|
134
|
+
try {
|
|
135
|
+
const response = await this._request(this.propfirm.userApi, '/User', 'GET');
|
|
136
|
+
if (response.statusCode === 200) {
|
|
137
|
+
this.user = response.data;
|
|
138
|
+
return { success: true, user: response.data };
|
|
139
|
+
}
|
|
140
|
+
return { success: false, error: 'Failed to get user info' };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { success: false, error: error.message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ==================== ACCOUNTS ====================
|
|
147
|
+
|
|
148
|
+
async getTradingAccounts() {
|
|
149
|
+
try {
|
|
150
|
+
const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
|
|
151
|
+
if (response.statusCode === 200) {
|
|
152
|
+
return { success: true, accounts: Array.isArray(response.data) ? response.data : [] };
|
|
153
|
+
}
|
|
154
|
+
return { success: false, accounts: [], error: 'Failed to get accounts' };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { success: false, accounts: [], error: error.message };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ==================== TRADING ====================
|
|
161
|
+
|
|
162
|
+
async getPositions(accountId) {
|
|
163
|
+
try {
|
|
164
|
+
const id = validateAccountId(accountId);
|
|
165
|
+
const response = await this._request(
|
|
166
|
+
this.propfirm.gatewayApi, '/api/Position/searchOpen', 'POST', { accountId: id }
|
|
167
|
+
);
|
|
168
|
+
if (response.statusCode === 200) {
|
|
169
|
+
const positions = response.data.positions || response.data || [];
|
|
170
|
+
return { success: true, positions: Array.isArray(positions) ? positions : [] };
|
|
171
|
+
}
|
|
172
|
+
return { success: true, positions: [] };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return { success: true, positions: [], error: error.message };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getOrders(accountId) {
|
|
179
|
+
try {
|
|
180
|
+
const id = validateAccountId(accountId);
|
|
181
|
+
const response = await this._request(
|
|
182
|
+
this.propfirm.gatewayApi, '/api/Order/searchOpen', 'POST', { accountId: id }
|
|
183
|
+
);
|
|
184
|
+
if (response.statusCode === 200) {
|
|
185
|
+
const orders = response.data.orders || response.data || [];
|
|
186
|
+
return { success: true, orders: Array.isArray(orders) ? orders : [] };
|
|
187
|
+
}
|
|
188
|
+
return { success: true, orders: [] };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return { success: true, orders: [], error: error.message };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async placeOrder(orderData) {
|
|
195
|
+
try {
|
|
196
|
+
const response = await this._request(
|
|
197
|
+
this.propfirm.gatewayApi, '/api/Order/place', 'POST', orderData, 'orders'
|
|
198
|
+
);
|
|
199
|
+
if (response.statusCode === 200 && response.data.success) {
|
|
200
|
+
return { success: true, order: response.data };
|
|
201
|
+
}
|
|
202
|
+
return { success: false, error: response.data.errorMessage || 'Order failed' };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return { success: false, error: error.message };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async cancelOrder(orderId) {
|
|
209
|
+
try {
|
|
210
|
+
const response = await this._request(
|
|
211
|
+
this.propfirm.gatewayApi, '/api/Order/cancel', 'POST',
|
|
212
|
+
{ orderId: parseInt(orderId, 10) }, 'orders'
|
|
213
|
+
);
|
|
214
|
+
return { success: response.statusCode === 200 && response.data.success };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return { success: false, error: error.message };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async cancelAllOrders(accountId) {
|
|
221
|
+
try {
|
|
222
|
+
const id = validateAccountId(accountId);
|
|
223
|
+
const ordersResult = await this.getOrders(id);
|
|
224
|
+
if (!ordersResult.success || !ordersResult.orders) {
|
|
225
|
+
return { success: true, cancelled: 0 };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pendingOrders = ordersResult.orders.filter(o =>
|
|
229
|
+
o.status === 'Working' || o.status === 'Pending' || o.status === 0 || o.status === 1
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let cancelled = 0;
|
|
233
|
+
for (const order of pendingOrders) {
|
|
234
|
+
const result = await this.cancelOrder(order.orderId || order.id);
|
|
235
|
+
if (result.success) cancelled++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { success: true, cancelled };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { success: false, error: error.message };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async closePosition(accountId, contractId) {
|
|
245
|
+
try {
|
|
246
|
+
const id = validateAccountId(accountId);
|
|
247
|
+
const response = await this._request(
|
|
248
|
+
this.propfirm.gatewayApi, '/api/Position/closeContract', 'POST',
|
|
249
|
+
{ accountId: id, contractId }, 'orders'
|
|
250
|
+
);
|
|
251
|
+
return { success: response.statusCode === 200 && response.data.success };
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return { success: false, error: error.message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ==================== TRADES & STATS ====================
|
|
258
|
+
|
|
259
|
+
async getTradeHistory(accountId, days = 30) {
|
|
260
|
+
try {
|
|
261
|
+
const id = validateAccountId(accountId);
|
|
262
|
+
const endDate = new Date();
|
|
263
|
+
const startDate = new Date();
|
|
264
|
+
startDate.setDate(startDate.getDate() - days);
|
|
265
|
+
|
|
266
|
+
const response = await this._request(
|
|
267
|
+
this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
|
|
268
|
+
{ accountId: id, startTimestamp: startDate.toISOString(), endTimestamp: endDate.toISOString() }
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (response.statusCode === 200 && response.data) {
|
|
272
|
+
let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
|
|
273
|
+
return { success: true, trades: formatTrades(trades) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { success: true, trades: [] };
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return { success: true, trades: [], error: error.message };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async getDailyStats(accountId) {
|
|
283
|
+
try {
|
|
284
|
+
const id = validateAccountId(accountId);
|
|
285
|
+
const now = new Date();
|
|
286
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
287
|
+
|
|
288
|
+
const response = await this._request(
|
|
289
|
+
this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
|
|
290
|
+
{ accountId: id, startTimestamp: startOfMonth.toISOString(), endTimestamp: now.toISOString() }
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (response.statusCode === 200 && response.data) {
|
|
294
|
+
let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
|
|
295
|
+
return { success: true, stats: calculateDailyPnL(trades) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { success: false, stats: [] };
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return { success: false, stats: [], error: error.message };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async getLifetimeStats(accountId) {
|
|
305
|
+
try {
|
|
306
|
+
const tradesResult = await this.getTradeHistory(accountId, 90);
|
|
307
|
+
if (!tradesResult.success || tradesResult.trades.length === 0) {
|
|
308
|
+
return { success: true, stats: null };
|
|
309
|
+
}
|
|
310
|
+
return { success: true, stats: calculateLifetimeStats(tradesResult.trades) };
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return { success: false, stats: null, error: error.message };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ==================== CONTRACTS ====================
|
|
317
|
+
|
|
318
|
+
async searchContracts(searchText) {
|
|
319
|
+
try {
|
|
320
|
+
const response = await this._request(
|
|
321
|
+
this.propfirm.gatewayApi, '/api/Contract/search', 'POST',
|
|
322
|
+
{ searchText: sanitizeString(searchText), live: false }
|
|
323
|
+
);
|
|
324
|
+
if (response.statusCode === 200) {
|
|
325
|
+
return { success: true, contracts: response.data.contracts || response.data || [] };
|
|
326
|
+
}
|
|
327
|
+
return { success: false, contracts: [] };
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return { success: false, contracts: [], error: error.message };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ==================== MARKET STATUS ====================
|
|
334
|
+
|
|
335
|
+
getMarketHolidays() { return getMarketHolidays(); }
|
|
336
|
+
checkHoliday() { return checkHoliday(); }
|
|
337
|
+
checkMarketHours() { return checkMarketHours(); }
|
|
338
|
+
|
|
339
|
+
async getMarketStatus(accountId) {
|
|
340
|
+
const hours = checkMarketHours();
|
|
341
|
+
return { success: true, isOpen: hours.isOpen, message: hours.message };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = { ProjectXService };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX Market Hours & Holidays
|
|
3
|
+
* CME Futures trading hours and US market holidays
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get nth weekday of a month
|
|
8
|
+
*/
|
|
9
|
+
const getNthWeekday = (year, month, weekday, n) => {
|
|
10
|
+
let count = 0;
|
|
11
|
+
for (let day = 1; day <= 31; day++) {
|
|
12
|
+
const d = new Date(year, month, day);
|
|
13
|
+
if (d.getMonth() !== month) break;
|
|
14
|
+
if (d.getDay() === weekday) {
|
|
15
|
+
count++;
|
|
16
|
+
if (count === n) return d.toISOString().split('T')[0];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get last weekday of a month
|
|
24
|
+
*/
|
|
25
|
+
const getLastWeekday = (year, month, weekday) => {
|
|
26
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
27
|
+
for (let day = lastDay.getDate(); day >= 1; day--) {
|
|
28
|
+
const d = new Date(year, month, day);
|
|
29
|
+
if (d.getDay() === weekday) return d.toISOString().split('T')[0];
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get Good Friday (Friday before Easter)
|
|
36
|
+
*/
|
|
37
|
+
const getGoodFriday = (year) => {
|
|
38
|
+
const a = year % 19;
|
|
39
|
+
const b = Math.floor(year / 100);
|
|
40
|
+
const c = year % 100;
|
|
41
|
+
const d = Math.floor(b / 4);
|
|
42
|
+
const e = b % 4;
|
|
43
|
+
const f = Math.floor((b + 8) / 25);
|
|
44
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
45
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
46
|
+
const i = Math.floor(c / 4);
|
|
47
|
+
const k = c % 4;
|
|
48
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
49
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
50
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31) - 1;
|
|
51
|
+
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
|
52
|
+
|
|
53
|
+
const easter = new Date(year, month, day);
|
|
54
|
+
const goodFriday = new Date(easter);
|
|
55
|
+
goodFriday.setDate(easter.getDate() - 2);
|
|
56
|
+
return goodFriday.toISOString().split('T')[0];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get day after a date
|
|
61
|
+
*/
|
|
62
|
+
const getDayAfter = (dateStr) => {
|
|
63
|
+
const d = new Date(dateStr);
|
|
64
|
+
d.setDate(d.getDate() + 1);
|
|
65
|
+
return d.toISOString().split('T')[0];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get US market holidays for the current year
|
|
70
|
+
*/
|
|
71
|
+
const getMarketHolidays = () => {
|
|
72
|
+
const year = new Date().getFullYear();
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
{ date: `${year}-01-01`, name: "New Year's Day", earlyClose: false },
|
|
76
|
+
{ date: getNthWeekday(year, 0, 1, 3), name: 'MLK Day', earlyClose: false },
|
|
77
|
+
{ date: getNthWeekday(year, 1, 1, 3), name: "Presidents' Day", earlyClose: false },
|
|
78
|
+
{ date: getGoodFriday(year), name: 'Good Friday', earlyClose: false },
|
|
79
|
+
{ date: getLastWeekday(year, 4, 1), name: 'Memorial Day', earlyClose: false },
|
|
80
|
+
{ date: `${year}-06-19`, name: 'Juneteenth', earlyClose: false },
|
|
81
|
+
{ date: `${year}-07-04`, name: 'Independence Day', earlyClose: false },
|
|
82
|
+
{ date: `${year}-07-03`, name: 'Independence Day Eve', earlyClose: true },
|
|
83
|
+
{ date: getNthWeekday(year, 8, 1, 1), name: 'Labor Day', earlyClose: false },
|
|
84
|
+
{ date: getNthWeekday(year, 10, 4, 4), name: 'Thanksgiving', earlyClose: false },
|
|
85
|
+
{ date: getDayAfter(getNthWeekday(year, 10, 4, 4)), name: 'Black Friday', earlyClose: true },
|
|
86
|
+
{ date: `${year}-12-25`, name: 'Christmas Day', earlyClose: false },
|
|
87
|
+
{ date: `${year}-12-24`, name: 'Christmas Eve', earlyClose: true },
|
|
88
|
+
{ date: `${year}-12-31`, name: "New Year's Eve", earlyClose: true },
|
|
89
|
+
];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if today is a market holiday
|
|
94
|
+
*/
|
|
95
|
+
const checkHoliday = () => {
|
|
96
|
+
const today = new Date().toISOString().split('T')[0];
|
|
97
|
+
const holidays = getMarketHolidays();
|
|
98
|
+
const holiday = holidays.find(h => h.date === today);
|
|
99
|
+
|
|
100
|
+
if (holiday) {
|
|
101
|
+
return { isHoliday: !holiday.earlyClose, holiday };
|
|
102
|
+
}
|
|
103
|
+
return { isHoliday: false };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if futures market is open based on CME hours and holidays
|
|
108
|
+
*/
|
|
109
|
+
const checkMarketHours = () => {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const utcDay = now.getUTCDay();
|
|
112
|
+
const utcHour = now.getUTCHours();
|
|
113
|
+
|
|
114
|
+
const holidayCheck = checkHoliday();
|
|
115
|
+
if (holidayCheck.isHoliday) {
|
|
116
|
+
return { isOpen: false, message: `Market closed - ${holidayCheck.holiday.name}` };
|
|
117
|
+
}
|
|
118
|
+
if (holidayCheck.holiday && holidayCheck.holiday.earlyClose && utcHour >= 18) {
|
|
119
|
+
return { isOpen: false, message: `Market closed early - ${holidayCheck.holiday.name}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (utcDay === 6) {
|
|
123
|
+
return { isOpen: false, message: 'Market closed - Weekend (Saturday)' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (utcDay === 0 && utcHour < 23) {
|
|
127
|
+
return { isOpen: false, message: 'Market closed - Opens Sunday 6:00 PM ET' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (utcDay === 5 && utcHour >= 22) {
|
|
131
|
+
return { isOpen: false, message: 'Market closed - Weekend' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (utcHour === 22 && utcDay !== 5) {
|
|
135
|
+
return { isOpen: false, message: 'Market closed - Daily maintenance (5:00-6:00 PM ET)' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { isOpen: true, message: 'Market is open' };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
getMarketHolidays,
|
|
143
|
+
checkHoliday,
|
|
144
|
+
checkMarketHours
|
|
145
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX Statistics Module
|
|
3
|
+
* Trade statistics calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate lifetime statistics from trades
|
|
8
|
+
* @param {Array} trades - Array of trade objects
|
|
9
|
+
* @returns {Object} Calculated statistics
|
|
10
|
+
*/
|
|
11
|
+
const calculateLifetimeStats = (trades) => {
|
|
12
|
+
if (!trades || trades.length === 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stats = {
|
|
17
|
+
totalTrades: trades.length,
|
|
18
|
+
winningTrades: 0,
|
|
19
|
+
losingTrades: 0,
|
|
20
|
+
totalWinAmount: 0,
|
|
21
|
+
totalLossAmount: 0,
|
|
22
|
+
bestTrade: 0,
|
|
23
|
+
worstTrade: 0,
|
|
24
|
+
totalVolume: 0,
|
|
25
|
+
maxConsecutiveWins: 0,
|
|
26
|
+
maxConsecutiveLosses: 0,
|
|
27
|
+
longTrades: 0,
|
|
28
|
+
shortTrades: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let consecutiveWins = 0;
|
|
32
|
+
let consecutiveLosses = 0;
|
|
33
|
+
|
|
34
|
+
trades.forEach(t => {
|
|
35
|
+
const pnl = t.profitAndLoss || t.pnl || 0;
|
|
36
|
+
const size = t.size || t.quantity || 1;
|
|
37
|
+
|
|
38
|
+
stats.totalVolume += Math.abs(size);
|
|
39
|
+
|
|
40
|
+
if (t.side === 0) stats.longTrades++;
|
|
41
|
+
else if (t.side === 1) stats.shortTrades++;
|
|
42
|
+
|
|
43
|
+
if (pnl > 0) {
|
|
44
|
+
stats.winningTrades++;
|
|
45
|
+
stats.totalWinAmount += pnl;
|
|
46
|
+
if (pnl > stats.bestTrade) stats.bestTrade = pnl;
|
|
47
|
+
consecutiveWins++;
|
|
48
|
+
consecutiveLosses = 0;
|
|
49
|
+
if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
|
|
50
|
+
} else if (pnl < 0) {
|
|
51
|
+
stats.losingTrades++;
|
|
52
|
+
stats.totalLossAmount += Math.abs(pnl);
|
|
53
|
+
if (pnl < stats.worstTrade) stats.worstTrade = pnl;
|
|
54
|
+
consecutiveLosses++;
|
|
55
|
+
consecutiveWins = 0;
|
|
56
|
+
if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
stats.profitFactor = stats.totalLossAmount > 0 ? stats.totalWinAmount / stats.totalLossAmount : 0;
|
|
61
|
+
stats.avgWin = stats.winningTrades > 0 ? stats.totalWinAmount / stats.winningTrades : 0;
|
|
62
|
+
stats.avgLoss = stats.losingTrades > 0 ? stats.totalLossAmount / stats.losingTrades : 0;
|
|
63
|
+
|
|
64
|
+
return stats;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate daily P&L from trades
|
|
69
|
+
* @param {Array} trades - Array of trade objects
|
|
70
|
+
* @returns {Array} Daily P&L array
|
|
71
|
+
*/
|
|
72
|
+
const calculateDailyPnL = (trades) => {
|
|
73
|
+
if (!trades || trades.length === 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dailyPnL = {};
|
|
78
|
+
|
|
79
|
+
trades.forEach(t => {
|
|
80
|
+
const ts = t.creationTimestamp || t.timestamp;
|
|
81
|
+
if (ts) {
|
|
82
|
+
const d = new Date(ts);
|
|
83
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
84
|
+
dailyPnL[key] = (dailyPnL[key] || 0) + (t.profitAndLoss || t.pnl || 0);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return Object.entries(dailyPnL).map(([date, pnl]) => ({ date, profitAndLoss: pnl }));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format trades for consistent output
|
|
93
|
+
* @param {Array} trades - Raw trades from API
|
|
94
|
+
* @returns {Array} Formatted trades
|
|
95
|
+
*/
|
|
96
|
+
const formatTrades = (trades) => {
|
|
97
|
+
if (!Array.isArray(trades)) return [];
|
|
98
|
+
|
|
99
|
+
return trades.map(t => ({
|
|
100
|
+
...t,
|
|
101
|
+
timestamp: t.creationTimestamp || t.timestamp,
|
|
102
|
+
pnl: t.profitAndLoss || t.pnl || 0
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
calculateLifetimeStats,
|
|
108
|
+
calculateDailyPnL,
|
|
109
|
+
formatTrades
|
|
110
|
+
};
|