hedgequantx 1.8.48 → 2.3.0

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.
Files changed (105) hide show
  1. package/README.md +7 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/accounts.js +2 -3
  90. package/src/pages/algo/copy-trading.js +293 -200
  91. package/src/pages/algo/one-account.js +1 -1
  92. package/src/security/encryption.js +81 -46
  93. package/src/security/index.js +12 -8
  94. package/src/security/rateLimit.js +68 -65
  95. package/src/security/validation.js +93 -79
  96. package/src/services/hqx-server.js +538 -206
  97. package/src/services/projectx/index.js +327 -204
  98. package/src/services/rithmic/index.js +288 -285
  99. package/src/services/session.js +184 -114
  100. package/src/services/tradovate/index.js +286 -297
  101. package/src/ui/index.js +53 -1
  102. package/src/utils/http.js +236 -0
  103. package/src/utils/index.js +11 -2
  104. package/src/utils/logger.js +64 -33
  105. package/src/utils/prompts.js +79 -71
@@ -1,94 +1,97 @@
1
1
  /**
2
- * ProjectX API Service
3
- * Main service for ProjectX prop firm connections
2
+ * @fileoverview ProjectX API Service
3
+ * @module services/projectx
4
4
  *
5
5
  * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
6
6
  */
7
7
 
8
- const https = require('https');
8
+ const { request } = require('../../utils/http');
9
9
  const { PROPFIRMS } = require('../../config');
10
-
11
- // Debug mode - set HQX_DEBUG=1 to enable
12
- const DEBUG = process.env.HQX_DEBUG === '1';
13
- const debug = (...args) => DEBUG && console.log('[ProjectX]', ...args);
14
- const {
15
- validateUsername,
16
- validatePassword,
10
+ const { TIMEOUTS, DEBUG } = require('../../config/settings');
11
+ const { getLimiter } = require('../../security/rateLimit');
12
+ const {
13
+ validateUsername,
14
+ validatePassword,
17
15
  validateApiKey,
18
16
  validateAccountId,
19
17
  sanitizeString,
20
- maskSensitive
18
+ maskSensitive,
21
19
  } = require('../../security');
22
- const { getLimiter } = require('../../security/rateLimit');
23
20
  const { getMarketHolidays, checkHoliday, checkMarketHours } = require('./market');
24
21
  const { calculateLifetimeStats, calculateDailyPnL, formatTrades } = require('./stats');
22
+ const { logger } = require('../../utils/logger');
25
23
 
24
+ const log = logger.scope('ProjectX');
25
+
26
+ /**
27
+ * ProjectX API Service for prop firm connections
28
+ */
26
29
  class ProjectXService {
30
+ /**
31
+ * @param {string} [propfirmKey='topstep'] - PropFirm identifier
32
+ */
27
33
  constructor(propfirmKey = 'topstep') {
28
34
  this.propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
29
35
  this.propfirmKey = propfirmKey;
30
36
  this.token = null;
31
37
  this.user = null;
32
- this.rateLimiter = getLimiter('api');
33
- this.loginLimiter = getLimiter('login');
34
- this.orderLimiter = getLimiter('orders');
38
+ this._limiters = {
39
+ api: getLimiter('api'),
40
+ login: getLimiter('login'),
41
+ orders: getLimiter('orders'),
42
+ };
35
43
  }
36
44
 
45
+ // ==================== GETTERS ====================
46
+
37
47
  getToken() { return this.token; }
38
48
  getPropfirm() { return this.propfirmKey; }
39
49
 
40
50
  // ==================== HTTP ====================
41
51
 
52
+ /**
53
+ * Make an API request with rate limiting
54
+ * @private
55
+ */
42
56
  async _request(host, path, method = 'GET', data = null, limiterType = 'api') {
43
- const limiter = limiterType === 'login' ? this.loginLimiter :
44
- limiterType === 'orders' ? this.orderLimiter : this.rateLimiter;
57
+ const limiter = this._limiters[limiterType] || this._limiters.api;
45
58
  return limiter.execute(() => this._doRequest(host, path, method, data));
46
59
  }
47
60
 
61
+ /**
62
+ * Execute the actual HTTP request
63
+ * @private
64
+ */
48
65
  async _doRequest(host, path, method, data) {
49
- return new Promise((resolve, reject) => {
50
- const options = {
51
- hostname: host,
52
- port: 443,
53
- path: path,
54
- method: method,
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- 'Accept': 'application/json',
58
- 'User-Agent': 'HedgeQuantX-CLI/1.3.0'
59
- },
60
- timeout: 15000
61
- };
62
-
63
- if (this.token) {
64
- options.headers['Authorization'] = `Bearer ${this.token}`;
65
- }
66
-
67
- const req = https.request(options, (res) => {
68
- let body = '';
69
- res.on('data', chunk => body += chunk);
70
- res.on('end', () => {
71
- try {
72
- resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
73
- } catch (e) {
74
- resolve({ statusCode: res.statusCode, data: body });
75
- }
76
- });
66
+ const url = `https://${host}${path}`;
67
+
68
+ try {
69
+ const response = await request(url, {
70
+ method,
71
+ body: data,
72
+ token: this.token,
73
+ timeout: TIMEOUTS.API_REQUEST,
77
74
  });
78
-
79
- req.on('error', reject);
80
- req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
81
- if (data) req.write(JSON.stringify(data));
82
- req.end();
83
- });
75
+
76
+ return response;
77
+ } catch (err) {
78
+ log.error('Request failed', { path, error: err.message });
79
+ throw err;
80
+ }
84
81
  }
85
82
 
86
83
  // ==================== AUTH ====================
87
84
 
85
+ /**
86
+ * Login with username and password
87
+ * @param {string} userName - Username
88
+ * @param {string} password - Password
89
+ * @returns {Promise<{success: boolean, token?: string, error?: string}>}
90
+ */
88
91
  async login(userName, password) {
89
92
  try {
90
93
  validateUsername(userName);
91
- validatePassword(password);
94
+ validatePassword(password, { requireUppercase: false, requireNumber: false });
92
95
 
93
96
  const response = await this._request(
94
97
  this.propfirm.userApi, '/Login', 'POST',
@@ -98,15 +101,25 @@ class ProjectXService {
98
101
 
99
102
  if (response.statusCode === 200 && response.data.token) {
100
103
  this.token = response.data.token;
104
+ log.info('Login successful', { user: sanitizeString(userName) });
101
105
  return { success: true, token: maskSensitive(this.token) };
102
106
  }
103
107
 
104
- return { success: false, error: response.data.errorMessage || 'Invalid credentials' };
105
- } catch (error) {
106
- return { success: false, error: error.message };
108
+ const error = response.data.errorMessage || 'Invalid credentials';
109
+ log.warn('Login failed', { error });
110
+ return { success: false, error };
111
+ } catch (err) {
112
+ log.error('Login error', { error: err.message });
113
+ return { success: false, error: err.message };
107
114
  }
108
115
  }
109
116
 
117
+ /**
118
+ * Login with API key
119
+ * @param {string} userName - Username
120
+ * @param {string} apiKey - API key
121
+ * @returns {Promise<{success: boolean, token?: string, error?: string}>}
122
+ */
110
123
  async loginWithApiKey(userName, apiKey) {
111
124
  try {
112
125
  validateUsername(userName);
@@ -120,32 +133,44 @@ class ProjectXService {
120
133
 
121
134
  if (response.statusCode === 200 && response.data.token) {
122
135
  this.token = response.data.token;
136
+ log.info('API key login successful');
123
137
  return { success: true, token: maskSensitive(this.token) };
124
138
  }
125
139
 
126
140
  return { success: false, error: response.data.errorMessage || 'Invalid API key' };
127
- } catch (error) {
128
- return { success: false, error: error.message };
141
+ } catch (err) {
142
+ log.error('API key login error', { error: err.message });
143
+ return { success: false, error: err.message };
129
144
  }
130
145
  }
131
146
 
147
+ /**
148
+ * Logout and clear token
149
+ */
132
150
  logout() {
133
151
  this.token = null;
134
152
  this.user = null;
153
+ log.debug('Logged out');
135
154
  }
136
155
 
137
156
  // ==================== USER ====================
138
157
 
158
+ /**
159
+ * Get user information
160
+ * @returns {Promise<{success: boolean, user?: Object, error?: string}>}
161
+ */
139
162
  async getUser() {
140
163
  try {
141
164
  const response = await this._request(this.propfirm.userApi, '/User', 'GET');
165
+
142
166
  if (response.statusCode === 200) {
143
167
  this.user = response.data;
144
168
  return { success: true, user: response.data };
145
169
  }
170
+
146
171
  return { success: false, error: 'Failed to get user info' };
147
- } catch (error) {
148
- return { success: false, error: error.message };
172
+ } catch (err) {
173
+ return { success: false, error: err.message };
149
174
  }
150
175
  }
151
176
 
@@ -154,18 +179,16 @@ class ProjectXService {
154
179
  /**
155
180
  * Get trading accounts with REAL P&L from API
156
181
  *
157
- * Data sources (all from userApi):
182
+ * Data sources:
158
183
  * - /TradingAccount: accountId, accountName, balance, status, type
159
184
  * - /AccountTemplate/userTemplates: startingBalance
160
- * - /Position?accountId=X: profitAndLoss (unrealized P&L from open positions)
185
+ * - /Position?accountId=X: profitAndLoss (unrealized P&L)
161
186
  *
162
- * All values come from API. No estimation.
187
+ * @returns {Promise<{success: boolean, accounts: Array, error?: string}>}
163
188
  */
164
189
  async getTradingAccounts() {
165
190
  try {
166
- // 1. Get accounts
167
191
  const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
168
- debug('getTradingAccounts response:', JSON.stringify(response.data, null, 2));
169
192
 
170
193
  if (response.statusCode !== 200) {
171
194
  return { success: false, accounts: [], error: 'Failed to get accounts' };
@@ -173,188 +196,233 @@ class ProjectXService {
173
196
 
174
197
  const accounts = Array.isArray(response.data) ? response.data : [];
175
198
 
176
- // 2. Get account templates (for startingBalance)
199
+ // Get account templates for startingBalance
177
200
  let templates = [];
178
201
  try {
179
202
  const templateRes = await this._request(this.propfirm.userApi, '/AccountTemplate/userTemplates', 'GET');
180
203
  if (templateRes.statusCode === 200 && Array.isArray(templateRes.data)) {
181
204
  templates = templateRes.data;
182
- debug('Templates:', JSON.stringify(templates, null, 2));
183
205
  }
184
- } catch (e) {
185
- debug('Failed to get templates:', e.message);
206
+ } catch {
207
+ log.debug('Failed to get templates');
186
208
  }
187
209
 
188
- const enrichedAccounts = [];
210
+ const enrichedAccounts = await Promise.all(
211
+ accounts.map(account => this._enrichAccount(account, templates))
212
+ );
189
213
 
190
- for (const account of accounts) {
191
- // Find matching template for startingBalance
192
- const template = templates.find(t =>
193
- account.accountName && (
194
- account.accountName.includes(t.title) ||
195
- t.title.includes(account.accountName)
196
- )
197
- );
198
-
199
- const enriched = {
200
- accountId: account.accountId,
201
- accountName: account.accountName,
202
- balance: account.balance, // From /TradingAccount
203
- status: account.status, // From /TradingAccount
204
- type: account.type, // From /TradingAccount
205
- startingBalance: template?.startingBalance || null, // From /AccountTemplate
206
- platform: 'ProjectX',
207
- propfirm: this.propfirm.name,
208
- openPnL: null,
209
- profitAndLoss: null,
210
- };
211
-
212
- // Get P&L for active accounts only
213
- if (account.status === 0) {
214
- let openPnL = 0;
215
- let todayPnL = 0;
216
-
217
- // 1. Get unrealized P&L from open positions
218
- try {
219
- const posRes = await this._request(
220
- this.propfirm.userApi,
221
- `/Position?accountId=${account.accountId}`,
222
- 'GET'
223
- );
224
- debug(`Positions for ${account.accountId}:`, JSON.stringify(posRes.data, null, 2));
225
-
226
- if (posRes.statusCode === 200 && Array.isArray(posRes.data)) {
227
- for (const pos of posRes.data) {
228
- if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
229
- openPnL += pos.profitAndLoss;
230
- }
231
- }
232
- }
233
- } catch (e) {
234
- debug('Failed to get positions:', e.message);
235
- }
236
-
237
- // 2. Get realized P&L from today's closed trades
238
- try {
239
- const today = new Date();
240
- today.setHours(0, 0, 0, 0);
241
- const now = new Date();
242
-
243
- const tradesRes = await this._request(
244
- this.propfirm.gatewayApi,
245
- '/api/Trade/search',
246
- 'POST',
247
- {
248
- accountId: account.accountId,
249
- startTimestamp: today.toISOString(),
250
- endTimestamp: now.toISOString()
251
- }
252
- );
253
- debug(`Today trades for ${account.accountId}:`, JSON.stringify(tradesRes.data, null, 2));
254
-
255
- if (tradesRes.statusCode === 200) {
256
- const trades = Array.isArray(tradesRes.data) ? tradesRes.data : (tradesRes.data.trades || []);
257
- for (const trade of trades) {
258
- if (trade.profitAndLoss !== undefined && trade.profitAndLoss !== null) {
259
- todayPnL += trade.profitAndLoss;
260
- }
261
- }
262
- }
263
- } catch (e) {
264
- debug('Failed to get today trades:', e.message);
265
- }
266
-
267
- enriched.openPnL = openPnL;
268
- enriched.todayPnL = todayPnL;
269
- enriched.profitAndLoss = openPnL + todayPnL; // Total day P&L = unrealized + realized
270
- }
214
+ return { success: true, accounts: enrichedAccounts };
215
+ } catch (err) {
216
+ log.error('Failed to get accounts', { error: err.message });
217
+ return { success: false, accounts: [], error: err.message };
218
+ }
219
+ }
271
220
 
272
- debug(`Account ${account.accountId}:`, {
273
- balance: enriched.balance,
274
- startingBalance: enriched.startingBalance,
275
- openPnL: enriched.openPnL,
276
- profitAndLoss: enriched.profitAndLoss
277
- });
221
+ /**
222
+ * Enrich account with P&L data
223
+ * @private
224
+ */
225
+ async _enrichAccount(account, templates) {
226
+ const template = templates.find(t =>
227
+ account.accountName && (
228
+ account.accountName.includes(t.title) ||
229
+ t.title.includes(account.accountName)
230
+ )
231
+ );
232
+
233
+ const enriched = {
234
+ accountId: account.accountId,
235
+ accountName: account.accountName,
236
+ balance: account.balance,
237
+ status: account.status,
238
+ type: account.type,
239
+ startingBalance: template?.startingBalance || null,
240
+ platform: 'ProjectX',
241
+ propfirm: this.propfirm.name,
242
+ openPnL: null,
243
+ todayPnL: null,
244
+ profitAndLoss: null,
245
+ };
246
+
247
+ // Only fetch P&L for active accounts
248
+ if (account.status !== 0) {
249
+ return enriched;
250
+ }
278
251
 
279
- enrichedAccounts.push(enriched);
252
+ // Get unrealized P&L from open positions
253
+ let openPnL = 0;
254
+ try {
255
+ const posRes = await this._request(
256
+ this.propfirm.userApi,
257
+ `/Position?accountId=${account.accountId}`,
258
+ 'GET'
259
+ );
260
+
261
+ if (posRes.statusCode === 200 && Array.isArray(posRes.data)) {
262
+ for (const pos of posRes.data) {
263
+ if (pos.profitAndLoss != null) {
264
+ openPnL += pos.profitAndLoss;
265
+ }
266
+ }
280
267
  }
268
+ } catch {
269
+ log.debug('Failed to get positions', { accountId: account.accountId });
270
+ }
281
271
 
282
- return { success: true, accounts: enrichedAccounts };
283
- } catch (error) {
284
- return { success: false, accounts: [], error: error.message };
272
+ // Get realized P&L from today's trades
273
+ let todayPnL = 0;
274
+ try {
275
+ const today = new Date();
276
+ today.setHours(0, 0, 0, 0);
277
+
278
+ const tradesRes = await this._request(
279
+ this.propfirm.gatewayApi,
280
+ '/api/Trade/search',
281
+ 'POST',
282
+ {
283
+ accountId: account.accountId,
284
+ startTimestamp: today.toISOString(),
285
+ endTimestamp: new Date().toISOString(),
286
+ }
287
+ );
288
+
289
+ if (tradesRes.statusCode === 200) {
290
+ const trades = Array.isArray(tradesRes.data) ? tradesRes.data : (tradesRes.data.trades || []);
291
+ for (const trade of trades) {
292
+ if (trade.profitAndLoss != null) {
293
+ todayPnL += trade.profitAndLoss;
294
+ }
295
+ }
296
+ }
297
+ } catch {
298
+ log.debug('Failed to get today trades', { accountId: account.accountId });
285
299
  }
300
+
301
+ enriched.openPnL = openPnL;
302
+ enriched.todayPnL = todayPnL;
303
+ enriched.profitAndLoss = openPnL + todayPnL;
304
+
305
+ return enriched;
286
306
  }
287
307
 
288
308
  // ==================== TRADING ====================
289
309
 
310
+ /**
311
+ * Get open positions
312
+ * @param {number|string} accountId - Account ID
313
+ * @returns {Promise<{success: boolean, positions: Array, error?: string}>}
314
+ */
290
315
  async getPositions(accountId) {
291
316
  try {
292
317
  const id = validateAccountId(accountId);
293
318
  const response = await this._request(
294
- this.propfirm.gatewayApi, '/api/Position/searchOpen', 'POST', { accountId: id }
319
+ this.propfirm.gatewayApi,
320
+ '/api/Position/searchOpen',
321
+ 'POST',
322
+ { accountId: id }
295
323
  );
324
+
296
325
  if (response.statusCode === 200) {
297
326
  const positions = response.data.positions || response.data || [];
298
327
  return { success: true, positions: Array.isArray(positions) ? positions : [] };
299
328
  }
329
+
300
330
  return { success: true, positions: [] };
301
- } catch (error) {
302
- return { success: true, positions: [], error: error.message };
331
+ } catch (err) {
332
+ return { success: true, positions: [], error: err.message };
303
333
  }
304
334
  }
305
335
 
336
+ /**
337
+ * Get open orders
338
+ * @param {number|string} accountId - Account ID
339
+ * @returns {Promise<{success: boolean, orders: Array, error?: string}>}
340
+ */
306
341
  async getOrders(accountId) {
307
342
  try {
308
343
  const id = validateAccountId(accountId);
309
344
  const response = await this._request(
310
- this.propfirm.gatewayApi, '/api/Order/searchOpen', 'POST', { accountId: id }
345
+ this.propfirm.gatewayApi,
346
+ '/api/Order/searchOpen',
347
+ 'POST',
348
+ { accountId: id }
311
349
  );
350
+
312
351
  if (response.statusCode === 200) {
313
352
  const orders = response.data.orders || response.data || [];
314
353
  return { success: true, orders: Array.isArray(orders) ? orders : [] };
315
354
  }
355
+
316
356
  return { success: true, orders: [] };
317
- } catch (error) {
318
- return { success: true, orders: [], error: error.message };
357
+ } catch (err) {
358
+ return { success: true, orders: [], error: err.message };
319
359
  }
320
360
  }
321
361
 
362
+ /**
363
+ * Place an order
364
+ * @param {Object} orderData - Order details
365
+ * @returns {Promise<{success: boolean, order?: Object, error?: string}>}
366
+ */
322
367
  async placeOrder(orderData) {
323
368
  try {
324
369
  const response = await this._request(
325
- this.propfirm.gatewayApi, '/api/Order/place', 'POST', orderData, 'orders'
370
+ this.propfirm.gatewayApi,
371
+ '/api/Order/place',
372
+ 'POST',
373
+ orderData,
374
+ 'orders'
326
375
  );
376
+
327
377
  if (response.statusCode === 200 && response.data.success) {
378
+ log.info('Order placed', { orderId: response.data.orderId });
328
379
  return { success: true, order: response.data };
329
380
  }
381
+
330
382
  return { success: false, error: response.data.errorMessage || 'Order failed' };
331
- } catch (error) {
332
- return { success: false, error: error.message };
383
+ } catch (err) {
384
+ return { success: false, error: err.message };
333
385
  }
334
386
  }
335
387
 
388
+ /**
389
+ * Cancel an order
390
+ * @param {number|string} orderId - Order ID
391
+ * @returns {Promise<{success: boolean, error?: string}>}
392
+ */
336
393
  async cancelOrder(orderId) {
337
394
  try {
338
395
  const response = await this._request(
339
- this.propfirm.gatewayApi, '/api/Order/cancel', 'POST',
340
- { orderId: parseInt(orderId, 10) }, 'orders'
396
+ this.propfirm.gatewayApi,
397
+ '/api/Order/cancel',
398
+ 'POST',
399
+ { orderId: parseInt(orderId, 10) },
400
+ 'orders'
341
401
  );
402
+
342
403
  return { success: response.statusCode === 200 && response.data.success };
343
- } catch (error) {
344
- return { success: false, error: error.message };
404
+ } catch (err) {
405
+ return { success: false, error: err.message };
345
406
  }
346
407
  }
347
408
 
409
+ /**
410
+ * Cancel all open orders for an account
411
+ * @param {number|string} accountId - Account ID
412
+ * @returns {Promise<{success: boolean, cancelled: number, error?: string}>}
413
+ */
348
414
  async cancelAllOrders(accountId) {
349
415
  try {
350
416
  const id = validateAccountId(accountId);
351
417
  const ordersResult = await this.getOrders(id);
352
- if (!ordersResult.success || !ordersResult.orders) {
418
+
419
+ if (!ordersResult.success || !ordersResult.orders?.length) {
353
420
  return { success: true, cancelled: 0 };
354
421
  }
355
422
 
356
- const pendingOrders = ordersResult.orders.filter(o =>
357
- o.status === 'Working' || o.status === 'Pending' || o.status === 0 || o.status === 1
423
+ const pendingOrders = ordersResult.orders.filter(o =>
424
+ o.status === 'Working' || o.status === 'Pending' ||
425
+ o.status === 0 || o.status === 1
358
426
  );
359
427
 
360
428
  let cancelled = 0;
@@ -364,26 +432,42 @@ class ProjectXService {
364
432
  }
365
433
 
366
434
  return { success: true, cancelled };
367
- } catch (error) {
368
- return { success: false, error: error.message };
435
+ } catch (err) {
436
+ return { success: false, cancelled: 0, error: err.message };
369
437
  }
370
438
  }
371
439
 
440
+ /**
441
+ * Close a position
442
+ * @param {number|string} accountId - Account ID
443
+ * @param {number|string} contractId - Contract ID
444
+ * @returns {Promise<{success: boolean, error?: string}>}
445
+ */
372
446
  async closePosition(accountId, contractId) {
373
447
  try {
374
448
  const id = validateAccountId(accountId);
375
449
  const response = await this._request(
376
- this.propfirm.gatewayApi, '/api/Position/closeContract', 'POST',
377
- { accountId: id, contractId }, 'orders'
450
+ this.propfirm.gatewayApi,
451
+ '/api/Position/closeContract',
452
+ 'POST',
453
+ { accountId: id, contractId },
454
+ 'orders'
378
455
  );
456
+
379
457
  return { success: response.statusCode === 200 && response.data.success };
380
- } catch (error) {
381
- return { success: false, error: error.message };
458
+ } catch (err) {
459
+ return { success: false, error: err.message };
382
460
  }
383
461
  }
384
462
 
385
463
  // ==================== TRADES & STATS ====================
386
464
 
465
+ /**
466
+ * Get trade history
467
+ * @param {number|string} accountId - Account ID
468
+ * @param {number} [days=30] - Days of history
469
+ * @returns {Promise<{success: boolean, trades: Array, error?: string}>}
470
+ */
387
471
  async getTradeHistory(accountId, days = 30) {
388
472
  try {
389
473
  const id = validateAccountId(accountId);
@@ -392,21 +476,32 @@ class ProjectXService {
392
476
  startDate.setDate(startDate.getDate() - days);
393
477
 
394
478
  const response = await this._request(
395
- this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
396
- { accountId: id, startTimestamp: startDate.toISOString(), endTimestamp: endDate.toISOString() }
479
+ this.propfirm.gatewayApi,
480
+ '/api/Trade/search',
481
+ 'POST',
482
+ {
483
+ accountId: id,
484
+ startTimestamp: startDate.toISOString(),
485
+ endTimestamp: endDate.toISOString(),
486
+ }
397
487
  );
398
488
 
399
489
  if (response.statusCode === 200 && response.data) {
400
- let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
490
+ const trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
401
491
  return { success: true, trades: formatTrades(trades) };
402
492
  }
403
493
 
404
494
  return { success: true, trades: [] };
405
- } catch (error) {
406
- return { success: true, trades: [], error: error.message };
495
+ } catch (err) {
496
+ return { success: true, trades: [], error: err.message };
407
497
  }
408
498
  }
409
499
 
500
+ /**
501
+ * Get daily statistics
502
+ * @param {number|string} accountId - Account ID
503
+ * @returns {Promise<{success: boolean, stats: Array, error?: string}>}
504
+ */
410
505
  async getDailyStats(accountId) {
411
506
  try {
412
507
  const id = validateAccountId(accountId);
@@ -414,48 +509,63 @@ class ProjectXService {
414
509
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
415
510
 
416
511
  const response = await this._request(
417
- this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
418
- { accountId: id, startTimestamp: startOfMonth.toISOString(), endTimestamp: now.toISOString() }
512
+ this.propfirm.gatewayApi,
513
+ '/api/Trade/search',
514
+ 'POST',
515
+ {
516
+ accountId: id,
517
+ startTimestamp: startOfMonth.toISOString(),
518
+ endTimestamp: now.toISOString(),
519
+ }
419
520
  );
420
521
 
421
522
  if (response.statusCode === 200 && response.data) {
422
- let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
523
+ const trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
423
524
  return { success: true, stats: calculateDailyPnL(trades) };
424
525
  }
425
526
 
426
527
  return { success: false, stats: [] };
427
- } catch (error) {
428
- return { success: false, stats: [], error: error.message };
528
+ } catch (err) {
529
+ return { success: false, stats: [], error: err.message };
429
530
  }
430
531
  }
431
532
 
533
+ /**
534
+ * Get lifetime statistics
535
+ * @param {number|string} accountId - Account ID
536
+ * @returns {Promise<{success: boolean, stats: Object|null, error?: string}>}
537
+ */
432
538
  async getLifetimeStats(accountId) {
433
539
  try {
434
540
  const tradesResult = await this.getTradeHistory(accountId, 90);
435
- if (!tradesResult.success || tradesResult.trades.length === 0) {
541
+
542
+ if (!tradesResult.success || !tradesResult.trades.length) {
436
543
  return { success: true, stats: null };
437
544
  }
545
+
438
546
  return { success: true, stats: calculateLifetimeStats(tradesResult.trades) };
439
- } catch (error) {
440
- return { success: false, stats: null, error: error.message };
547
+ } catch (err) {
548
+ return { success: false, stats: null, error: err.message };
441
549
  }
442
550
  }
443
551
 
444
552
  // ==================== CONTRACTS ====================
445
553
 
446
554
  /**
447
- * Get all available contracts from API
555
+ * Get available contracts
556
+ * @returns {Promise<{success: boolean, contracts: Array, error?: string}>}
448
557
  */
449
558
  async getContracts() {
450
559
  try {
451
560
  const response = await this._request(
452
- this.propfirm.gatewayApi, '/api/Contract/available', 'POST',
561
+ this.propfirm.gatewayApi,
562
+ '/api/Contract/available',
563
+ 'POST',
453
564
  { live: false }
454
565
  );
455
566
 
456
567
  if (response.statusCode === 200) {
457
568
  const contracts = response.data.contracts || response.data || [];
458
- // Filter only active contracts and sort by description
459
569
  const activeContracts = contracts
460
570
  .filter(c => c.activeContract === true)
461
571
  .sort((a, b) => (a.description || '').localeCompare(b.description || ''));
@@ -464,23 +574,32 @@ class ProjectXService {
464
574
  }
465
575
 
466
576
  return { success: false, contracts: [], error: 'Failed to fetch contracts' };
467
- } catch (error) {
468
- return { success: false, contracts: [], error: error.message };
577
+ } catch (err) {
578
+ return { success: false, contracts: [], error: err.message };
469
579
  }
470
580
  }
471
581
 
582
+ /**
583
+ * Search contracts
584
+ * @param {string} searchText - Search text
585
+ * @returns {Promise<{success: boolean, contracts: Array, error?: string}>}
586
+ */
472
587
  async searchContracts(searchText) {
473
588
  try {
474
589
  const response = await this._request(
475
- this.propfirm.gatewayApi, '/api/Contract/search', 'POST',
590
+ this.propfirm.gatewayApi,
591
+ '/api/Contract/search',
592
+ 'POST',
476
593
  { searchText: sanitizeString(searchText), live: false }
477
594
  );
595
+
478
596
  if (response.statusCode === 200) {
479
597
  return { success: true, contracts: response.data.contracts || response.data || [] };
480
598
  }
599
+
481
600
  return { success: false, contracts: [] };
482
- } catch (error) {
483
- return { success: false, contracts: [], error: error.message };
601
+ } catch (err) {
602
+ return { success: false, contracts: [], error: err.message };
484
603
  }
485
604
  }
486
605
 
@@ -490,7 +609,11 @@ class ProjectXService {
490
609
  checkHoliday() { return checkHoliday(); }
491
610
  checkMarketHours() { return checkMarketHours(); }
492
611
 
493
- async getMarketStatus(accountId) {
612
+ /**
613
+ * Get market status
614
+ * @returns {Promise<{success: boolean, isOpen: boolean, message: string}>}
615
+ */
616
+ async getMarketStatus() {
494
617
  const hours = checkMarketHours();
495
618
  return { success: true, isOpen: hours.isOpen, message: hours.message };
496
619
  }