hedgequantx 1.8.49 → 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 (103) 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/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -1,57 +1,79 @@
1
1
  /**
2
- * Tradovate Service
3
- * Main service for Tradovate prop firm connections (Apex, TakeProfitTrader)
2
+ * @fileoverview Tradovate Service - Main service for Tradovate prop firm connections
3
+ * @module services/tradovate
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 crypto = require('crypto');
9
+ const os = require('os');
9
10
  const EventEmitter = require('events');
10
- const { TRADOVATE_URLS, API_PATHS, WS_EVENTS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
11
+ const { request } = require('../../utils/http');
12
+ const { TIMEOUTS } = require('../../config/settings');
13
+ const { TRADOVATE_URLS, API_PATHS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
11
14
  const { checkMarketHours, isDST } = require('./market');
12
15
  const { connectWebSocket, wsSend, disconnectWebSocket } = require('./websocket');
16
+ const { logger } = require('../../utils/logger');
13
17
 
18
+ const log = logger.scope('Tradovate');
19
+
20
+ /** PropFirm configurations */
21
+ const PROPFIRM_CONFIGS = {
22
+ apex_tradovate: { name: 'Apex (Tradovate)', isDemo: false },
23
+ takeprofittrader: { name: 'TakeProfitTrader', isDemo: false },
24
+ myfundedfutures: { name: 'MyFundedFutures', isDemo: false },
25
+ };
26
+
27
+ /**
28
+ * Tradovate Service for prop firm trading
29
+ */
14
30
  class TradovateService extends EventEmitter {
31
+ /**
32
+ * @param {string} propfirmKey - PropFirm identifier
33
+ */
15
34
  constructor(propfirmKey) {
16
35
  super();
17
36
  this.propfirmKey = propfirmKey;
18
- this.propfirm = this.getPropFirmConfig(propfirmKey);
37
+ this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || { name: propfirmKey, isDemo: false };
38
+
39
+ // Auth
19
40
  this.accessToken = null;
20
41
  this.mdAccessToken = null;
21
42
  this.userId = null;
22
43
  this.tokenExpiration = null;
44
+
45
+ // State
23
46
  this.accounts = [];
24
47
  this.user = null;
25
- this.isDemo = true; // Default to demo
48
+ this.isDemo = true;
49
+ this.credentials = null;
50
+
51
+ // WebSocket
26
52
  this.ws = null;
27
53
  this.wsRequestId = 1;
28
54
  this.renewalTimer = null;
29
- this.credentials = null; // Store for session restore
55
+
56
+ // Device ID cache
57
+ this._deviceId = null;
30
58
  }
31
59
 
32
- /**
33
- * Get PropFirm configuration
34
- */
35
- getPropFirmConfig(key) {
36
- const propfirms = {
37
- 'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false },
38
- 'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false },
39
- 'myfundedfutures': { name: 'MyFundedFutures', isDemo: false },
40
- };
41
- return propfirms[key] || { name: key, isDemo: false };
42
- }
60
+ // ==================== AUTH ====================
43
61
 
44
62
  /**
45
63
  * Login to Tradovate
64
+ * @param {string} username - Username
65
+ * @param {string} password - Password
66
+ * @param {Object} [options] - Additional options (cid, sec)
67
+ * @returns {Promise<{success: boolean, error?: string}>}
46
68
  */
47
69
  async login(username, password, options = {}) {
48
70
  try {
49
71
  const authData = {
50
72
  name: username,
51
- password: password,
73
+ password,
52
74
  appId: 'HQX-CLI',
53
- appVersion: '1.0.0',
54
- deviceId: this.generateDeviceId(),
75
+ appVersion: '2.0.0',
76
+ deviceId: this._getDeviceId(),
55
77
  };
56
78
 
57
79
  if (options.cid) authData.cid = options.cid;
@@ -59,81 +81,132 @@ class TradovateService extends EventEmitter {
59
81
 
60
82
  const result = await this._request(API_PATHS.AUTH_TOKEN_REQUEST, 'POST', authData);
61
83
 
62
- if (result.errorText) {
63
- return { success: false, error: result.errorText };
84
+ if (result.data.errorText) {
85
+ log.warn('Login failed', { error: result.data.errorText });
86
+ return { success: false, error: result.data.errorText };
64
87
  }
65
88
 
66
- if (!result.accessToken) {
89
+ if (!result.data.accessToken) {
67
90
  return { success: false, error: 'No access token received' };
68
91
  }
69
92
 
70
- this.accessToken = result.accessToken;
71
- this.mdAccessToken = result.mdAccessToken;
72
- this.userId = result.userId;
73
- this.tokenExpiration = new Date(result.expirationTime);
74
- this.user = { userName: result.name, userId: result.userId };
93
+ this.accessToken = result.data.accessToken;
94
+ this.mdAccessToken = result.data.mdAccessToken;
95
+ this.userId = result.data.userId;
96
+ this.tokenExpiration = new Date(result.data.expirationTime);
97
+ this.user = { userName: result.data.name, userId: result.data.userId };
75
98
  this.credentials = { username, password };
76
99
 
77
- this.setupTokenRenewal();
78
- await this.fetchAccounts();
100
+ this._setupTokenRenewal();
101
+ await this._fetchAccounts();
79
102
 
103
+ log.info('Login successful', { accounts: this.accounts.length });
80
104
  return { success: true };
81
- } catch (error) {
82
- return { success: false, error: error.message };
105
+ } catch (err) {
106
+ log.error('Login error', { error: err.message });
107
+ return { success: false, error: err.message };
83
108
  }
84
109
  }
85
110
 
86
111
  /**
87
- * Fetch accounts
112
+ * Generate device ID (cached)
113
+ * @private
88
114
  */
89
- async fetchAccounts() {
90
- try {
91
- const accounts = await this._request(API_PATHS.ACCOUNT_LIST, 'GET');
92
-
93
- if (Array.isArray(accounts)) {
94
- this.accounts = accounts;
95
-
96
- for (const acc of this.accounts) {
97
- try {
98
- const cashBalance = await this._request(
99
- API_PATHS.CASH_BALANCE_SNAPSHOT,
100
- 'POST',
101
- { accountId: acc.id }
102
- );
103
- acc.cashBalance = cashBalance;
104
- } catch (e) {
105
- acc.cashBalance = null;
106
- }
107
- }
115
+ _getDeviceId() {
116
+ if (this._deviceId) return this._deviceId;
117
+
118
+ const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
119
+ this._deviceId = crypto.createHash('md5').update(data).digest('hex');
120
+ return this._deviceId;
121
+ }
122
+
123
+ /**
124
+ * Setup automatic token renewal
125
+ * @private
126
+ */
127
+ _setupTokenRenewal() {
128
+ if (this.renewalTimer) {
129
+ clearTimeout(this.renewalTimer);
130
+ }
131
+
132
+ // Renew 15 minutes before expiration
133
+ const renewInMs = (90 - 15) * 60 * 1000;
134
+
135
+ this.renewalTimer = setTimeout(async () => {
136
+ try {
137
+ await this._renewToken();
138
+ } catch (err) {
139
+ log.warn('Token renewal failed', { error: err.message });
108
140
  }
141
+ }, renewInMs);
142
+ }
143
+
144
+ /**
145
+ * Renew access token
146
+ * @private
147
+ */
148
+ async _renewToken() {
149
+ if (!this.accessToken) return;
150
+
151
+ const result = await this._request(API_PATHS.AUTH_RENEW_TOKEN, 'GET');
152
+
153
+ if (result.data.accessToken) {
154
+ this.accessToken = result.data.accessToken;
155
+ this.mdAccessToken = result.data.mdAccessToken;
156
+ this.tokenExpiration = new Date(result.data.expirationTime);
157
+ this._setupTokenRenewal();
158
+ log.debug('Token renewed');
159
+ }
160
+ }
161
+
162
+ // ==================== ACCOUNTS ====================
163
+
164
+ /**
165
+ * Fetch accounts from API
166
+ * @private
167
+ */
168
+ async _fetchAccounts() {
169
+ const result = await this._request(API_PATHS.ACCOUNT_LIST, 'GET');
170
+
171
+ if (!Array.isArray(result.data)) {
172
+ return;
173
+ }
174
+
175
+ this.accounts = result.data;
109
176
 
110
- return this.accounts;
111
- } catch (error) {
112
- throw error;
177
+ // Fetch cash balance for each account
178
+ for (const acc of this.accounts) {
179
+ try {
180
+ const balanceResult = await this._request(
181
+ API_PATHS.CASH_BALANCE_SNAPSHOT,
182
+ 'POST',
183
+ { accountId: acc.id }
184
+ );
185
+ acc.cashBalance = balanceResult.data;
186
+ } catch {
187
+ acc.cashBalance = null;
188
+ }
113
189
  }
114
190
  }
115
191
 
116
192
  /**
117
- * Get trading accounts - ONLY returns values from API
118
- * No estimation, no simulation
193
+ * Get trading accounts with REAL P&L from API
194
+ * @returns {Promise<{success: boolean, accounts: Array}>}
119
195
  */
120
196
  async getTradingAccounts() {
121
- if (this.accounts.length === 0) {
122
- await this.fetchAccounts();
197
+ if (!this.accounts.length) {
198
+ await this._fetchAccounts();
123
199
  }
124
200
 
125
201
  const tradingAccounts = this.accounts.map((acc) => {
126
202
  const cb = acc.cashBalance || {};
127
-
203
+
128
204
  // ONLY use values from API - null if not available
129
- const balance = cb.totalCashValue !== undefined ? cb.totalCashValue :
130
- (cb.netLiquidatingValue !== undefined ? cb.netLiquidatingValue : null);
131
-
132
- // P&L from API only - null if not available
133
- const realizedPnL = cb.realizedPnL !== undefined ? cb.realizedPnL : null;
134
- const openPnL = cb.openPnL !== undefined ? cb.openPnL : null;
135
-
136
- // Total P&L from API
205
+ const balance = cb.totalCashValue ?? cb.netLiquidatingValue ?? null;
206
+ const realizedPnL = cb.realizedPnL ?? null;
207
+ const openPnL = cb.openPnL ?? null;
208
+
209
+ // Total P&L
137
210
  let profitAndLoss = null;
138
211
  if (cb.totalPnL !== undefined) {
139
212
  profitAndLoss = cb.totalPnL;
@@ -146,12 +219,11 @@ class TradovateService extends EventEmitter {
146
219
  tradovateAccountId: acc.id,
147
220
  accountName: acc.name,
148
221
  name: acc.name,
149
- // From API only - null if not available
150
- balance: balance,
222
+ balance,
151
223
  todayPnL: realizedPnL,
152
- openPnL: openPnL,
153
- profitAndLoss: profitAndLoss,
154
- startingBalance: null, // API doesn't provide this
224
+ openPnL,
225
+ profitAndLoss,
226
+ startingBalance: null,
155
227
  status: acc.active ? 0 : 3,
156
228
  platform: 'Tradovate',
157
229
  propfirm: this.propfirm.name,
@@ -162,31 +234,61 @@ class TradovateService extends EventEmitter {
162
234
  return { success: true, accounts: tradingAccounts };
163
235
  }
164
236
 
237
+ // ==================== POSITIONS ====================
238
+
165
239
  /**
166
240
  * Get positions for an account
241
+ * @param {number} accountId - Account ID
242
+ * @returns {Promise<Array>}
167
243
  */
168
244
  async getPositions(accountId) {
169
245
  try {
170
- const positions = await this._request(API_PATHS.POSITION_DEPS, 'GET', null, { masterid: accountId });
171
- return positions.filter(p => p.netPos !== 0);
172
- } catch (error) {
246
+ const result = await this._request(API_PATHS.POSITION_DEPS, 'GET', null, { masterid: accountId });
247
+ return result.data.filter(p => p.netPos !== 0);
248
+ } catch {
173
249
  return [];
174
250
  }
175
251
  }
176
252
 
253
+ // ==================== ORDERS ====================
254
+
177
255
  /**
178
- * Get fills/trades
256
+ * Get orders
257
+ * @param {number} [accountId] - Optional account filter
258
+ * @returns {Promise<{success: boolean, orders: Array, error?: string}>}
179
259
  */
180
- async getFills() {
260
+ async getOrders(accountId) {
181
261
  try {
182
- return await this._request(API_PATHS.FILL_LIST, 'GET');
183
- } catch (error) {
184
- return [];
262
+ const result = await this._request(API_PATHS.ORDER_LIST, 'GET');
263
+ const orders = Array.isArray(result.data) ? result.data : [];
264
+
265
+ const filtered = accountId
266
+ ? orders.filter(o => o.accountId === accountId)
267
+ : orders;
268
+
269
+ return {
270
+ success: true,
271
+ orders: filtered.map(o => ({
272
+ orderId: o.id,
273
+ accountId: o.accountId,
274
+ symbol: o.contractId,
275
+ side: o.action === 'Buy' ? 0 : 1,
276
+ quantity: o.orderQty,
277
+ filledQuantity: o.filledQty || 0,
278
+ price: o.price,
279
+ status: o.ordStatus === 'Working' ? 1 : (o.ordStatus === 'Filled' ? 2 : 0),
280
+ orderType: o.orderType,
281
+ })),
282
+ };
283
+ } catch (err) {
284
+ return { success: false, error: err.message, orders: [] };
185
285
  }
186
286
  }
187
287
 
188
288
  /**
189
289
  * Place an order
290
+ * @param {Object} orderData - Order details
291
+ * @returns {Promise<{success: boolean, orderId?: number, error?: string}>}
190
292
  */
191
293
  async placeOrder(orderData) {
192
294
  try {
@@ -200,159 +302,93 @@ class TradovateService extends EventEmitter {
200
302
  isAutomated: true,
201
303
  });
202
304
 
203
- if (result.errorText || result.failureReason) {
204
- return { success: false, error: result.errorText || result.failureText };
305
+ if (result.data.errorText || result.data.failureReason) {
306
+ return { success: false, error: result.data.errorText || result.data.failureText };
205
307
  }
206
308
 
207
- return { success: true, orderId: result.orderId };
208
- } catch (error) {
209
- return { success: false, error: error.message };
309
+ log.info('Order placed', { orderId: result.data.orderId });
310
+ return { success: true, orderId: result.data.orderId };
311
+ } catch (err) {
312
+ return { success: false, error: err.message };
210
313
  }
211
314
  }
212
315
 
213
316
  /**
214
317
  * Cancel an order
318
+ * @param {number} orderId - Order ID
319
+ * @returns {Promise<{success: boolean, error?: string}>}
215
320
  */
216
321
  async cancelOrder(orderId) {
217
322
  try {
218
323
  const result = await this._request(API_PATHS.ORDER_CANCEL, 'POST', {
219
- orderId: orderId,
324
+ orderId,
220
325
  isAutomated: true,
221
326
  });
222
327
 
223
- if (result.errorText) {
224
- return { success: false, error: result.errorText };
328
+ if (result.data.errorText) {
329
+ return { success: false, error: result.data.errorText };
225
330
  }
226
331
 
227
332
  return { success: true };
228
- } catch (error) {
229
- return { success: false, error: error.message };
333
+ } catch (err) {
334
+ return { success: false, error: err.message };
230
335
  }
231
336
  }
232
337
 
233
338
  /**
234
339
  * Close a position
340
+ * @param {number} accountId - Account ID
341
+ * @param {number} contractId - Contract ID
342
+ * @returns {Promise<{success: boolean, error?: string}>}
235
343
  */
236
344
  async closePosition(accountId, contractId) {
237
345
  try {
238
346
  const result = await this._request(API_PATHS.ORDER_LIQUIDATE_POSITION, 'POST', {
239
- accountId: accountId,
240
- contractId: contractId,
347
+ accountId,
348
+ contractId,
241
349
  isAutomated: true,
242
350
  });
243
351
 
244
- if (result.errorText) {
245
- return { success: false, error: result.errorText };
352
+ if (result.data.errorText) {
353
+ return { success: false, error: result.data.errorText };
246
354
  }
247
355
 
248
356
  return { success: true };
249
- } catch (error) {
250
- return { success: false, error: error.message };
251
- }
252
- }
253
-
254
- /**
255
- * Search contracts
256
- */
257
- async searchContracts(text, limit = 10) {
258
- try {
259
- return await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
260
- } catch (error) {
261
- return [];
357
+ } catch (err) {
358
+ return { success: false, error: err.message };
262
359
  }
263
360
  }
264
361
 
265
- /**
266
- * Get user info
267
- */
268
- async getUser() {
269
- return this.user;
270
- }
271
-
272
- /**
273
- * Get market status
274
- */
275
- async getMarketStatus(accountId) {
276
- const marketHours = checkMarketHours();
277
- return {
278
- success: true,
279
- isOpen: marketHours.isOpen,
280
- message: marketHours.message,
281
- };
282
- }
283
-
284
- /**
285
- * Get token
286
- */
287
- getToken() {
288
- return this.accessToken;
289
- }
362
+ // ==================== TRADES ====================
290
363
 
291
364
  /**
292
- * Get orders for an account
293
- */
294
- async getOrders(accountId) {
295
- try {
296
- const orders = await this._request(API_PATHS.ORDER_LIST, 'GET');
297
- const filtered = accountId
298
- ? orders.filter(o => o.accountId === accountId)
299
- : orders;
300
- return {
301
- success: true,
302
- orders: filtered.map(o => ({
303
- orderId: o.id,
304
- accountId: o.accountId,
305
- symbol: o.contractId,
306
- side: o.action === 'Buy' ? 0 : 1,
307
- quantity: o.orderQty,
308
- filledQuantity: o.filledQty || 0,
309
- price: o.price,
310
- status: o.ordStatus === 'Working' ? 1 : (o.ordStatus === 'Filled' ? 2 : 0),
311
- orderType: o.orderType,
312
- }))
313
- };
314
- } catch (error) {
315
- return { success: false, error: error.message, orders: [] };
316
- }
317
- }
318
-
319
- /**
320
- * Get order history
365
+ * Get fills/trades
366
+ * @returns {Promise<Array>}
321
367
  */
322
- async getOrderHistory(days = 30) {
368
+ async getFills() {
323
369
  try {
324
- const orders = await this._request(API_PATHS.ORDER_LIST, 'GET');
325
- return { success: true, orders };
326
- } catch (error) {
327
- return { success: false, error: error.message, orders: [] };
370
+ const result = await this._request(API_PATHS.FILL_LIST, 'GET');
371
+ return result.data || [];
372
+ } catch {
373
+ return [];
328
374
  }
329
375
  }
330
376
 
331
- /**
332
- * Get lifetime stats (stub - Tradovate doesn't provide this directly)
333
- */
334
- async getLifetimeStats(accountId) {
335
- return { success: true, stats: null };
336
- }
337
-
338
- /**
339
- * Get daily stats (stub - Tradovate doesn't provide this directly)
340
- */
341
- async getDailyStats(accountId) {
342
- return { success: true, stats: [] };
343
- }
344
-
345
377
  /**
346
378
  * Get trade history
379
+ * @param {number} [accountId] - Account filter
380
+ * @param {number} [days=30] - Days of history
381
+ * @returns {Promise<{success: boolean, trades: Array, error?: string}>}
347
382
  */
348
383
  async getTradeHistory(accountId, days = 30) {
349
384
  try {
350
385
  const fills = await this.getFills();
351
- const filtered = accountId
386
+ const filtered = accountId
352
387
  ? fills.filter(f => f.accountId === accountId)
353
388
  : fills;
354
- return {
355
- success: true,
389
+
390
+ return {
391
+ success: true,
356
392
  trades: filtered.map(f => ({
357
393
  tradeId: f.id,
358
394
  accountId: f.accountId,
@@ -361,69 +397,92 @@ class TradovateService extends EventEmitter {
361
397
  quantity: f.qty,
362
398
  price: f.price,
363
399
  timestamp: f.timestamp,
364
- }))
400
+ })),
365
401
  };
366
- } catch (error) {
367
- return { success: false, error: error.message, trades: [] };
402
+ } catch (err) {
403
+ return { success: false, error: err.message, trades: [] };
368
404
  }
369
405
  }
370
406
 
371
407
  /**
372
- * Setup automatic token renewal
408
+ * Get order history
409
+ * @param {number} [days=30] - Days of history
410
+ * @returns {Promise<{success: boolean, orders: Array, error?: string}>}
373
411
  */
374
- setupTokenRenewal() {
375
- if (this.renewalTimer) {
376
- clearTimeout(this.renewalTimer);
412
+ async getOrderHistory(days = 30) {
413
+ try {
414
+ const result = await this._request(API_PATHS.ORDER_LIST, 'GET');
415
+ return { success: true, orders: result.data || [] };
416
+ } catch (err) {
417
+ return { success: false, error: err.message, orders: [] };
377
418
  }
378
-
379
- // Renew 15 minutes before expiration
380
- const renewInMs = (90 - 15) * 60 * 1000;
381
-
382
- this.renewalTimer = setTimeout(async () => {
383
- try {
384
- await this.renewToken();
385
- } catch (error) {
386
- // Silent fail
387
- }
388
- }, renewInMs);
389
419
  }
390
420
 
421
+ // ==================== CONTRACTS ====================
422
+
391
423
  /**
392
- * Renew access token
424
+ * Search contracts
425
+ * @param {string} text - Search text
426
+ * @param {number} [limit=10] - Result limit
427
+ * @returns {Promise<Array>}
393
428
  */
394
- async renewToken() {
395
- if (!this.accessToken) return;
396
-
429
+ async searchContracts(text, limit = 10) {
397
430
  try {
398
- const result = await this._request(API_PATHS.AUTH_RENEW_TOKEN, 'GET');
399
-
400
- if (result.accessToken) {
401
- this.accessToken = result.accessToken;
402
- this.mdAccessToken = result.mdAccessToken;
403
- this.tokenExpiration = new Date(result.expirationTime);
404
- this.setupTokenRenewal();
405
- }
406
- } catch (error) {
407
- // Silent fail - will need to re-login
431
+ const result = await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
432
+ return result.data || [];
433
+ } catch {
434
+ return [];
408
435
  }
409
436
  }
410
437
 
411
- /**
412
- * Connect to WebSocket for real-time updates
413
- */
414
- async connectWebSocket() {
415
- return connectWebSocket(this);
438
+ // ==================== STUBS ====================
439
+
440
+ async getUser() { return this.user; }
441
+ async getLifetimeStats() { return { success: true, stats: null }; }
442
+ async getDailyStats() { return { success: true, stats: [] }; }
443
+
444
+ getToken() { return this.accessToken; }
445
+
446
+ async getMarketStatus() {
447
+ const hours = checkMarketHours();
448
+ return { success: true, isOpen: hours.isOpen, message: hours.message };
416
449
  }
417
450
 
418
- /**
419
- * Send WebSocket message
420
- */
451
+ // ==================== WEBSOCKET ====================
452
+
453
+ async connectWebSocket() { return connectWebSocket(this); }
454
+
421
455
  wsSend(url, query = '', body = null) {
422
456
  return wsSend(this, url, query, body);
423
457
  }
424
458
 
459
+ // ==================== HTTP ====================
460
+
461
+ /**
462
+ * Make an HTTP request
463
+ * @private
464
+ */
465
+ async _request(path, method = 'GET', body = null, queryParams = null) {
466
+ const baseUrl = getBaseUrl(this.isDemo);
467
+ let url = `${baseUrl}${path}`;
468
+
469
+ if (queryParams) {
470
+ const params = new URLSearchParams(queryParams).toString();
471
+ url += `?${params}`;
472
+ }
473
+
474
+ return request(url, {
475
+ method,
476
+ body,
477
+ token: this.accessToken,
478
+ timeout: TIMEOUTS.API_REQUEST,
479
+ });
480
+ }
481
+
482
+ // ==================== CLEANUP ====================
483
+
425
484
  /**
426
- * Disconnect
485
+ * Disconnect and cleanup
427
486
  */
428
487
  async disconnect() {
429
488
  if (this.renewalTimer) {
@@ -438,78 +497,8 @@ class TradovateService extends EventEmitter {
438
497
  this.accounts = [];
439
498
  this.user = null;
440
499
  this.credentials = null;
441
- }
442
-
443
- /**
444
- * Generate device ID
445
- */
446
- generateDeviceId() {
447
- const crypto = require('crypto');
448
- const os = require('os');
449
- const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
450
- return crypto.createHash('md5').update(data).digest('hex');
451
- }
452
-
453
- /**
454
- * HTTP request helper
455
- */
456
- _request(path, method = 'GET', body = null, queryParams = null) {
457
- return new Promise((resolve, reject) => {
458
- const baseUrl = getBaseUrl(this.isDemo);
459
- const url = new URL(baseUrl + path);
460
-
461
- if (queryParams) {
462
- Object.entries(queryParams).forEach(([key, value]) => {
463
- url.searchParams.append(key, value);
464
- });
465
- }
466
-
467
- const postData = body ? JSON.stringify(body) : null;
468
-
469
- const options = {
470
- hostname: url.hostname,
471
- port: 443,
472
- path: url.pathname + url.search,
473
- method: method,
474
- headers: {
475
- 'Content-Type': 'application/json',
476
- 'Accept': 'application/json',
477
- },
478
- };
479
-
480
- if (postData) {
481
- options.headers['Content-Length'] = Buffer.byteLength(postData);
482
- }
483
-
484
- if (this.accessToken) {
485
- options.headers['Authorization'] = `Bearer ${this.accessToken}`;
486
- }
487
-
488
- const req = https.request(options, (res) => {
489
- let data = '';
490
- res.on('data', chunk => data += chunk);
491
- res.on('end', () => {
492
- try {
493
- const json = JSON.parse(data);
494
- resolve(json);
495
- } catch (e) {
496
- resolve(data);
497
- }
498
- });
499
- });
500
-
501
- req.on('error', reject);
502
- req.setTimeout(30000, () => {
503
- req.destroy();
504
- reject(new Error('Request timeout'));
505
- });
506
500
 
507
- if (postData) {
508
- req.write(postData);
509
- }
510
-
511
- req.end();
512
- });
501
+ log.info('Disconnected');
513
502
  }
514
503
  }
515
504