hedgequantx 2.3.11 → 2.3.13

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.11",
3
+ "version": "2.3.13",
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');
@@ -148,74 +147,85 @@ const launchAlgo = async (service, account, contract, config) => {
148
147
  accountName, symbol: symbolName, contracts,
149
148
  target: dailyTarget, risk: maxRisk,
150
149
  propfirm: account.propfirm || 'Unknown',
150
+ platform: account.platform || 'ProjectX',
151
151
  pnl: 0, trades: 0, wins: 0, losses: 0,
152
152
  latency: 0, connected: false,
153
153
  startTime: Date.now() // Track start time for duration
154
154
  };
155
155
 
156
+ // Measure API latency (CLI <-> API)
157
+ const measureLatency = async () => {
158
+ try {
159
+ const start = Date.now();
160
+ await service.getPositions(account.accountId);
161
+ stats.latency = Date.now() - start;
162
+ } catch (e) {
163
+ stats.latency = 0;
164
+ }
165
+ };
166
+
156
167
  let running = true;
157
168
  let stopReason = null;
158
169
 
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;
170
+ // Local algo - no external server needed
171
+ ui.addLog('info', `Starting algo on ${stats.platform}...`);
172
+ ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
173
+ ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
174
+ stats.connected = true;
175
+
176
+ // Poll P&L from API every 2 seconds
177
+ const pollPnL = async () => {
178
+ try {
179
+ // Get positions to check P&L
180
+ const posResult = await service.getPositions(account.accountId);
181
+ if (posResult.success && posResult.positions) {
182
+ // Find position for our symbol
183
+ const pos = posResult.positions.find(p =>
184
+ (p.contractId || p.symbol || '').includes(contract.name || contract.symbol)
185
+ );
186
+ if (pos && pos.profitAndLoss !== undefined) {
187
+ const prevPnL = stats.pnl;
188
+ stats.pnl = pos.profitAndLoss;
189
+
190
+ // Detect trade completion
191
+ if (Math.abs(stats.pnl - prevPnL) > 0.01 && prevPnL !== 0) {
192
+ const tradePnL = stats.pnl - prevPnL;
193
+ stats.trades++;
194
+ if (tradePnL >= 0) {
195
+ stats.wins++;
196
+ ui.addLog('trade', `Trade closed: +$${tradePnL.toFixed(2)}`);
197
+ } else {
198
+ stats.losses++;
199
+ ui.addLog('loss', `Trade closed: -$${Math.abs(tradePnL).toFixed(2)}`);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // Check target/risk limits
206
+ if (stats.pnl >= dailyTarget) {
207
+ stopReason = 'target';
208
+ running = false;
209
+ ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
210
+ } else if (stats.pnl <= -maxRisk) {
211
+ stopReason = 'risk';
212
+ running = false;
213
+ ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
214
+ }
215
+ } catch (e) {
216
+ // Silent fail - will retry
188
217
  }
189
- stats.trades = d.trades;
190
- stats.wins = d.wins;
191
- stats.losses = d.losses;
192
- });
218
+ };
193
219
 
194
- hqx.on('error', (d) => { ui.addLog('error', d.message || 'Error'); });
195
- hqx.on('disconnected', () => { stats.connected = false; ui.addLog('warning', 'Disconnected'); });
220
+ const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
196
221
 
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
- }
222
+ // Measure API latency every 5 seconds
223
+ measureLatency(); // Initial measurement
224
+ const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
217
225
 
218
- const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
226
+ // Poll P&L from API every 2 seconds
227
+ pollPnL(); // Initial poll
228
+ const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
219
229
 
220
230
  // Keyboard
221
231
  const setupKeyHandler = () => {
@@ -243,8 +253,9 @@ const launchAlgo = async (service, account, contract, config) => {
243
253
  });
244
254
 
245
255
  clearInterval(refreshInterval);
256
+ clearInterval(latencyInterval);
257
+ clearInterval(pnlInterval);
246
258
  if (cleanupKeys) cleanupKeys();
247
- if (stats.connected) { hqx.stopAlgo(); hqx.disconnect(); }
248
259
  ui.cleanup();
249
260
 
250
261
  // Calculate duration
@@ -189,16 +189,17 @@ class AlgoUI {
189
189
 
190
190
  this._line(chalk.cyan(GM));
191
191
 
192
- // Row 4: Trades | Server
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('Server', stats.connected ? 'ON' : 'OFF', serverColor, 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
198
  this._line(chalk.cyan(GM));
199
199
 
200
- // Row 5: Latency | Propfirm
201
- const r5c1 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colL);
200
+ // Row 5: Platform | Propfirm
201
+ const platform = stats.platform || 'ProjectX';
202
+ const r5c1 = buildCell('Platform', platform, chalk.white, colL);
202
203
  const r5c2 = buildCell('Propfirm', stats.propfirm || 'N/A', chalk.cyan, colR);
203
204
  row(r5c1.padded, r5c2.padded);
204
205
 
@@ -254,18 +255,11 @@ class AlgoUI {
254
255
 
255
256
  this._line(chalk.cyan(GM));
256
257
 
257
- // Row 5: P&L | Server
258
+ // Row 5: P&L | Trades
258
259
  const r5c1 = buildCell('P&L', pnlStr, pnlColor, colL);
259
- const r5c2 = buildCell('Server', stats.connected ? 'ON' : 'OFF', serverColor, colR);
260
- row(r5c1.padded, r5c2.padded);
261
-
262
- this._line(chalk.cyan(GM));
263
-
264
- // Row 6: Trades | Latency
265
- const r6c1t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
266
- const r6c1p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
267
- const r6c2 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colR);
268
- row(r6c1t + pad(colL - r6c1p.length), r6c2.padded);
260
+ const r5c2t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
261
+ const r5c2p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
262
+ row(r5c1.padded, r5c2t + pad(colR - r5c2p.length));
269
263
 
270
264
  this._line(chalk.cyan(GB));
271
265
  }
@@ -280,11 +274,13 @@ class AlgoUI {
280
274
  const timeStr = now.toLocaleTimeString('en-US', { hour12: false });
281
275
  const dateStr = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
282
276
 
277
+ const leftText = ` EXECUTION LOG ${spinner}`;
278
+ const rightText = `${dateStr} ${timeStr} [X] STOP `;
279
+ const totalLen = leftText.length + rightText.length;
280
+ const space = W - totalLen;
281
+
283
282
  const left = ` EXECUTION LOG ${chalk.yellow(spinner)}`;
284
283
  const right = `${chalk.gray(dateStr)} ${chalk.white(timeStr)} ${chalk.yellow('[X] STOP')} `;
285
- const leftPlain = stripAnsi(left);
286
- const rightPlain = ` ${dateStr} ${timeStr} [X] STOP `;
287
- const space = W - leftPlain.length - rightPlain.length;
288
284
 
289
285
  this._line(chalk.cyan(BOX.V) + chalk.white.bold(left) + ' '.repeat(Math.max(0, space)) + right + chalk.cyan(BOX.V));
290
286
  this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));