hedgequantx 2.3.12 → 2.3.14

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/dist/lib/api.jsc CHANGED
Binary file
package/dist/lib/api2.jsc CHANGED
Binary file
package/dist/lib/core.jsc CHANGED
Binary file
Binary file
package/dist/lib/data.jsc CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/lib/n/r1.jsc CHANGED
Binary file
package/dist/lib/n/r2.jsc CHANGED
Binary file
package/dist/lib/n/r3.jsc CHANGED
Binary file
package/dist/lib/n/r4.jsc CHANGED
Binary file
package/dist/lib/n/r5.jsc CHANGED
Binary file
package/dist/lib/n/r6.jsc CHANGED
Binary file
package/dist/lib/n/r7.jsc CHANGED
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.3.12",
3
+ "version": "2.3.14",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -8,7 +8,6 @@ const ora = require('ora');
8
8
  const readline = require('readline');
9
9
 
10
10
  const { connections } = require('../../services');
11
- const { HQXServerService } = require('../../services/hqx-server');
12
11
  const { AlgoUI, renderSessionSummary } = require('./ui');
13
12
  const { logger, prompts } = require('../../utils');
14
13
  const { checkMarketHours } = require('../../services/projectx/market');
@@ -283,48 +282,100 @@ const launchCopyTrading = async (config) => {
283
282
  losses: 0,
284
283
  latency: 0,
285
284
  connected: false,
285
+ platform: lead.account.platform || 'ProjectX',
286
286
  };
287
287
 
288
288
  let running = true;
289
289
  let stopReason = null;
290
+
291
+ // Measure API latency (CLI <-> API)
292
+ const measureLatency = async () => {
293
+ try {
294
+ const start = Date.now();
295
+ await lead.service.getPositions(lead.account.accountId);
296
+ stats.latency = Date.now() - start;
297
+ } catch (e) {
298
+ stats.latency = 0;
299
+ }
300
+ };
290
301
 
291
- // Connect to HQX Server
292
- const hqx = new HQXServerService();
293
- const spinner = ora({ text: 'Connecting to HQX Server...', color: 'yellow' }).start();
294
-
295
- try {
296
- const auth = await hqx.authenticate(
297
- lead.account.accountId.toString(),
298
- lead.propfirm || 'topstep'
299
- );
300
- if (!auth.success) throw new Error(auth.error);
301
-
302
- const conn = await hqx.connect();
303
- if (!conn.success) throw new Error('WebSocket failed');
304
-
305
- spinner.succeed('Connected');
306
- stats.connected = true;
307
- } catch (err) {
308
- spinner.warn('HQX Server unavailable');
309
- log.warn('HQX connection failed', { error: err.message });
310
- }
311
-
312
- // Event handlers
313
- setupEventHandlers(hqx, ui, stats, lead, follower, showNames, () => {
314
- running = false;
315
- }, (reason) => {
316
- stopReason = reason;
317
- }, dailyTarget, maxRisk);
318
-
319
- // Start algo on server
320
- if (stats.connected) {
321
- startCopyTradingOnServer(hqx, lead, follower, dailyTarget, maxRisk, ui);
322
- }
302
+ // Local copy trading - no external server needed
303
+ ui.addLog('info', `Starting copy trading on ${stats.platform}...`);
304
+ ui.addLog('info', `Lead: ${stats.leadName} -> Follower: ${stats.followerName}`);
305
+ ui.addLog('info', `Symbol: ${stats.symbol} | Target: $${dailyTarget} | Risk: $${maxRisk}`);
306
+ stats.connected = true;
307
+
308
+ // Track lead positions and copy to follower
309
+ let lastLeadPositions = [];
310
+
311
+ const pollAndCopy = async () => {
312
+ try {
313
+ // Get lead positions
314
+ const leadResult = await lead.service.getPositions(lead.account.accountId);
315
+ if (!leadResult.success) return;
316
+
317
+ const currentPositions = leadResult.positions || [];
318
+
319
+ // Detect new positions on lead
320
+ for (const pos of currentPositions) {
321
+ const existing = lastLeadPositions.find(p => p.contractId === pos.contractId);
322
+ if (!existing && pos.quantity !== 0) {
323
+ // New position opened - copy to follower
324
+ ui.addLog('trade', `Lead opened: ${pos.quantity > 0 ? 'LONG' : 'SHORT'} ${Math.abs(pos.quantity)}x ${pos.symbol || pos.contractId}`);
325
+ // TODO: Place order on follower account
326
+ }
327
+ }
328
+
329
+ // Detect closed positions
330
+ for (const oldPos of lastLeadPositions) {
331
+ const stillOpen = currentPositions.find(p => p.contractId === oldPos.contractId);
332
+ if (!stillOpen || stillOpen.quantity === 0) {
333
+ ui.addLog('info', `Lead closed: ${oldPos.symbol || oldPos.contractId}`);
334
+ // TODO: Close position on follower account
335
+ }
336
+ }
337
+
338
+ lastLeadPositions = currentPositions;
339
+
340
+ // Update P&L from lead
341
+ const leadPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
342
+ if (leadPnL !== stats.pnl) {
343
+ const diff = leadPnL - stats.pnl;
344
+ if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
345
+ stats.trades++;
346
+ if (diff >= 0) stats.wins++;
347
+ else stats.losses++;
348
+ }
349
+ stats.pnl = leadPnL;
350
+ }
351
+
352
+ // Check target/risk limits
353
+ if (stats.pnl >= dailyTarget) {
354
+ stopReason = 'target';
355
+ running = false;
356
+ ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
357
+ } else if (stats.pnl <= -maxRisk) {
358
+ stopReason = 'risk';
359
+ running = false;
360
+ ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
361
+ }
362
+ } catch (e) {
363
+ // Silent fail - will retry
364
+ }
365
+ };
323
366
 
324
367
  // UI refresh loop
325
368
  const refreshInterval = setInterval(() => {
326
369
  if (running) ui.render(stats);
327
370
  }, 250);
371
+
372
+ // Measure API latency every 5 seconds
373
+ measureLatency(); // Initial measurement
374
+ const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
375
+
376
+ // Poll and copy every 2 seconds
377
+ pollAndCopy(); // Initial poll
378
+ const copyInterval = setInterval(() => { if (running) pollAndCopy(); }, 2000);
328
379
 
329
380
  // Keyboard handling
330
381
  const cleanupKeys = setupKeyboardHandler(() => {
@@ -344,11 +395,9 @@ const launchCopyTrading = async (config) => {
344
395
 
345
396
  // Cleanup
346
397
  clearInterval(refreshInterval);
398
+ clearInterval(latencyInterval);
399
+ clearInterval(copyInterval);
347
400
  if (cleanupKeys) cleanupKeys();
348
- if (stats.connected) {
349
- hqx.stopAlgo();
350
- hqx.disconnect();
351
- }
352
401
  ui.cleanup();
353
402
 
354
403
  // Show summary
@@ -356,88 +405,6 @@ const launchCopyTrading = async (config) => {
356
405
  await prompts.waitForEnter();
357
406
  };
358
407
 
359
- /**
360
- * Setup HQX event handlers
361
- */
362
- const setupEventHandlers = (hqx, ui, stats, lead, follower, showNames, stop, setReason, dailyTarget, maxRisk) => {
363
- hqx.on('latency', (d) => {
364
- stats.latency = d.latency || 0;
365
- });
366
-
367
- hqx.on('log', (d) => {
368
- let msg = d.message;
369
- if (!showNames) {
370
- if (lead.account.accountName) {
371
- msg = msg.replace(new RegExp(lead.account.accountName, 'gi'), 'Lead *****');
372
- }
373
- if (follower.account.accountName) {
374
- msg = msg.replace(new RegExp(follower.account.accountName, 'gi'), 'Follower *****');
375
- }
376
- }
377
- ui.addLog(d.type || 'info', msg);
378
- });
379
-
380
- hqx.on('trade', (d) => {
381
- stats.trades++;
382
- stats.pnl += d.pnl || 0;
383
- d.pnl >= 0 ? stats.wins++ : stats.losses++;
384
- ui.addLog(d.pnl >= 0 ? 'trade' : 'loss', `${d.pnl >= 0 ? '+' : ''}$${d.pnl.toFixed(2)}`);
385
-
386
- if (stats.pnl >= dailyTarget) {
387
- setReason('target');
388
- stop();
389
- ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
390
- hqx.stopAlgo();
391
- } else if (stats.pnl <= -maxRisk) {
392
- setReason('risk');
393
- stop();
394
- ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
395
- hqx.stopAlgo();
396
- }
397
- });
398
-
399
- hqx.on('copy', (d) => {
400
- ui.addLog('trade', `COPIED: ${d.side} ${d.quantity}x`);
401
- });
402
-
403
- hqx.on('error', (d) => {
404
- ui.addLog('error', d.message);
405
- });
406
-
407
- hqx.on('disconnected', () => {
408
- stats.connected = false;
409
- });
410
- };
411
-
412
- /**
413
- * Start copy trading on HQX server
414
- */
415
- const startCopyTradingOnServer = (hqx, lead, follower, dailyTarget, maxRisk, ui) => {
416
- ui.addLog('info', 'Starting Copy Trading...');
417
-
418
- const leadCreds = lead.service.getRithmicCredentials?.() || null;
419
- const followerCreds = follower.service.getRithmicCredentials?.() || null;
420
-
421
- hqx.startCopyTrading({
422
- leadAccountId: lead.account.accountId,
423
- leadContractId: lead.symbol.id || lead.symbol.contractId,
424
- leadSymbol: lead.symbol.symbol || lead.symbol.name,
425
- leadContracts: lead.contracts,
426
- leadPropfirm: lead.propfirm,
427
- leadToken: lead.service.getToken?.() || null,
428
- leadRithmicCredentials: leadCreds,
429
- followerAccountId: follower.account.accountId,
430
- followerContractId: follower.symbol.id || follower.symbol.contractId,
431
- followerSymbol: follower.symbol.symbol || follower.symbol.name,
432
- followerContracts: follower.contracts,
433
- followerPropfirm: follower.propfirm,
434
- followerToken: follower.service.getToken?.() || null,
435
- followerRithmicCredentials: followerCreds,
436
- dailyTarget,
437
- maxRisk,
438
- });
439
- };
440
-
441
408
  /**
442
409
  * Setup keyboard handler
443
410
  * @param {Function} onStop - Stop callback
@@ -7,7 +7,6 @@ const ora = require('ora');
7
7
  const readline = require('readline');
8
8
 
9
9
  const { connections } = require('../../services');
10
- const { HQXServerService } = require('../../services/hqx-server');
11
10
  const { AlgoUI, renderSessionSummary } = require('./ui');
12
11
  const { prompts } = require('../../utils');
13
12
  const { checkMarketHours } = require('../../services/projectx/market');
@@ -79,7 +78,7 @@ const oneAccountMenu = async (service) => {
79
78
  };
80
79
 
81
80
  /**
82
- * Symbol selection - RAW API data only
81
+ * Symbol selection - sorted with popular indices first
83
82
  */
84
83
  const selectSymbol = async (service, account) => {
85
84
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
@@ -90,10 +89,32 @@ const selectSymbol = async (service, account) => {
90
89
  return null;
91
90
  }
92
91
 
93
- const contracts = contractsResult.contracts;
92
+ let contracts = contractsResult.contracts;
93
+
94
+ // Sort: Popular indices first (ES, NQ, MES, MNQ, RTY, YM, etc.)
95
+ const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
96
+
97
+ contracts.sort((a, b) => {
98
+ const nameA = a.name || '';
99
+ const nameB = b.name || '';
100
+
101
+ // Check if names start with popular prefixes
102
+ const idxA = popularPrefixes.findIndex(p => nameA.startsWith(p));
103
+ const idxB = popularPrefixes.findIndex(p => nameB.startsWith(p));
104
+
105
+ // Both are popular - sort by popularity order
106
+ if (idxA !== -1 && idxB !== -1) return idxA - idxB;
107
+ // Only A is popular - A first
108
+ if (idxA !== -1) return -1;
109
+ // Only B is popular - B first
110
+ if (idxB !== -1) return 1;
111
+ // Neither - alphabetical
112
+ return nameA.localeCompare(nameB);
113
+ });
114
+
94
115
  spinner.succeed(`Found ${contracts.length} contracts`);
95
116
 
96
- // Display EXACTLY what API returns - no modifications
117
+ // Display sorted contracts from API
97
118
  const options = contracts.map(c => ({
98
119
  label: `${c.name} - ${c.description}`,
99
120
  value: c
@@ -148,74 +169,85 @@ const launchAlgo = async (service, account, contract, config) => {
148
169
  accountName, symbol: symbolName, contracts,
149
170
  target: dailyTarget, risk: maxRisk,
150
171
  propfirm: account.propfirm || 'Unknown',
172
+ platform: account.platform || 'ProjectX',
151
173
  pnl: 0, trades: 0, wins: 0, losses: 0,
152
174
  latency: 0, connected: false,
153
175
  startTime: Date.now() // Track start time for duration
154
176
  };
155
177
 
178
+ // Measure API latency (CLI <-> API)
179
+ const measureLatency = async () => {
180
+ try {
181
+ const start = Date.now();
182
+ await service.getPositions(account.accountId);
183
+ stats.latency = Date.now() - start;
184
+ } catch (e) {
185
+ stats.latency = 0;
186
+ }
187
+ };
188
+
156
189
  let running = true;
157
190
  let stopReason = null;
158
191
 
159
- const hqx = new HQXServerService();
160
- const spinner = ora({ text: 'Connecting to HQX Server...', color: 'yellow' }).start();
161
-
162
- try {
163
- const auth = await hqx.authenticate(account.accountId.toString(), account.propfirm || 'topstep');
164
- if (!auth.success) throw new Error(auth.error || 'Auth failed');
165
-
166
- spinner.text = 'Connecting WebSocket...';
167
- const conn = await hqx.connect();
168
- if (!conn.success) throw new Error('WebSocket failed');
169
-
170
- spinner.succeed('Connected to HQX Server');
171
- stats.connected = true;
172
- } catch (err) {
173
- spinner.warn('HQX Server unavailable - offline mode');
174
- }
175
-
176
- // Event handlers
177
- hqx.on('latency', (d) => { stats.latency = d.latency || 0; });
178
- hqx.on('log', (d) => {
179
- let msg = d.message;
180
- if (!showName && account.accountName) msg = msg.replace(new RegExp(account.accountName, 'gi'), 'HQX *****');
181
- ui.addLog(d.type || 'info', msg);
182
- });
183
-
184
- // REAL P&L direct from Rithmic - no calculation
185
- hqx.on('stats', (d) => {
186
- if (d.realTimePnL) {
187
- stats.pnl = d.realTimePnL.totalPnL;
192
+ // Local algo - no external server needed
193
+ ui.addLog('info', `Starting algo on ${stats.platform}...`);
194
+ ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
195
+ ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
196
+ stats.connected = true;
197
+
198
+ // Poll P&L from API every 2 seconds
199
+ const pollPnL = async () => {
200
+ try {
201
+ // Get positions to check P&L
202
+ const posResult = await service.getPositions(account.accountId);
203
+ if (posResult.success && posResult.positions) {
204
+ // Find position for our symbol
205
+ const pos = posResult.positions.find(p =>
206
+ (p.contractId || p.symbol || '').includes(contract.name || contract.symbol)
207
+ );
208
+ if (pos && pos.profitAndLoss !== undefined) {
209
+ const prevPnL = stats.pnl;
210
+ stats.pnl = pos.profitAndLoss;
211
+
212
+ // Detect trade completion
213
+ if (Math.abs(stats.pnl - prevPnL) > 0.01 && prevPnL !== 0) {
214
+ const tradePnL = stats.pnl - prevPnL;
215
+ stats.trades++;
216
+ if (tradePnL >= 0) {
217
+ stats.wins++;
218
+ ui.addLog('trade', `Trade closed: +$${tradePnL.toFixed(2)}`);
219
+ } else {
220
+ stats.losses++;
221
+ ui.addLog('loss', `Trade closed: -$${Math.abs(tradePnL).toFixed(2)}`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // Check target/risk limits
228
+ if (stats.pnl >= dailyTarget) {
229
+ stopReason = 'target';
230
+ running = false;
231
+ ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
232
+ } else if (stats.pnl <= -maxRisk) {
233
+ stopReason = 'risk';
234
+ running = false;
235
+ ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
236
+ }
237
+ } catch (e) {
238
+ // Silent fail - will retry
188
239
  }
189
- stats.trades = d.trades;
190
- stats.wins = d.wins;
191
- stats.losses = d.losses;
192
- });
240
+ };
193
241
 
194
- hqx.on('error', (d) => { ui.addLog('error', d.message || 'Error'); });
195
- hqx.on('disconnected', () => { stats.connected = false; ui.addLog('warning', 'Disconnected'); });
242
+ const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
196
243
 
197
- // Start on server
198
- if (stats.connected) {
199
- ui.addLog('info', 'Starting algo...');
200
-
201
- // Get Rithmic credentials from the account's service
202
- let rithmicCreds = null;
203
- if (service && service.getRithmicCredentials) {
204
- rithmicCreds = service.getRithmicCredentials();
205
- }
206
-
207
- hqx.startAlgo({
208
- accountId: account.accountId,
209
- contractId: contract.id || contract.contractId,
210
- symbol: contract.symbol || contract.name,
211
- contracts, dailyTarget, maxRisk,
212
- propfirm: account.propfirm || 'topstep',
213
- propfirmToken: service.getToken?.() || null,
214
- rithmicCredentials: rithmicCreds
215
- });
216
- }
244
+ // Measure API latency every 5 seconds
245
+ measureLatency(); // Initial measurement
246
+ const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
217
247
 
218
- const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
248
+ // Poll P&L from API every 2 seconds
249
+ pollPnL(); // Initial poll
250
+ const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
219
251
 
220
252
  // Keyboard
221
253
  const setupKeyHandler = () => {
@@ -243,8 +275,9 @@ const launchAlgo = async (service, account, contract, config) => {
243
275
  });
244
276
 
245
277
  clearInterval(refreshInterval);
278
+ clearInterval(latencyInterval);
279
+ clearInterval(pnlInterval);
246
280
  if (cleanupKeys) cleanupKeys();
247
- if (stats.connected) { hqx.stopAlgo(); hqx.disconnect(); }
248
281
  ui.cleanup();
249
282
 
250
283
  // Calculate duration
@@ -189,12 +189,20 @@ class AlgoUI {
189
189
 
190
190
  this._line(chalk.cyan(GM));
191
191
 
192
- // Row 4: Trades | Propfirm
192
+ // Row 4: Trades | Latency (API response time)
193
193
  const r4c1t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
194
194
  const r4c1p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
195
- const r4c2 = buildCell('Propfirm', stats.propfirm || 'N/A', chalk.cyan, colR);
195
+ const r4c2 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colR);
196
196
  row(r4c1t + pad(colL - r4c1p.length), r4c2.padded);
197
197
 
198
+ this._line(chalk.cyan(GM));
199
+
200
+ // Row 5: Platform | Propfirm
201
+ const platform = stats.platform || 'ProjectX';
202
+ const r5c1 = buildCell('Platform', platform, chalk.white, colL);
203
+ const r5c2 = buildCell('Propfirm', stats.propfirm || 'N/A', chalk.cyan, colR);
204
+ row(r5c1.padded, r5c2.padded);
205
+
198
206
  this._line(chalk.cyan(GB));
199
207
  }
200
208