hedgequantx 2.4.32 → 2.4.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.4.32",
3
+ "version": "2.4.34",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -206,6 +206,8 @@ const launchAlgo = async (service, account, contract, config) => {
206
206
  let currentPosition = 0; // Current position qty (+ long, - short)
207
207
  let pendingOrder = false; // Prevent duplicate orders
208
208
  let tickCount = 0;
209
+ let lastTradeCount = 0; // Track number of trades from API
210
+ let lastPositionQty = 0; // Track position changes
209
211
 
210
212
  // Initialize Strategy (M1 is singleton instance)
211
213
  const strategy = M1;
@@ -375,24 +377,20 @@ const launchAlgo = async (service, account, contract, config) => {
375
377
  algoLogger.error(ui, 'CONNECTION ERROR', e.message.substring(0, 50));
376
378
  }
377
379
 
378
- // Poll account P&L from API
380
+ // Poll account P&L and sync with real trades from API
379
381
  const pollPnL = async () => {
380
382
  try {
383
+ // Get account P&L
381
384
  const accountResult = await service.getTradingAccounts();
382
385
  if (accountResult.success && accountResult.accounts) {
383
386
  const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
384
387
  if (acc && acc.profitAndLoss !== undefined) {
385
388
  if (startingPnL === null) startingPnL = acc.profitAndLoss;
386
389
  stats.pnl = acc.profitAndLoss - startingPnL;
387
-
388
- // Record trade result in strategy
389
- if (stats.pnl !== 0) {
390
- strategy.recordTradeResult(stats.pnl);
391
- }
392
390
  }
393
391
  }
394
392
 
395
- // Check positions
393
+ // Check positions - detect when position closes
396
394
  const posResult = await service.getPositions(account.accountId);
397
395
  if (posResult.success && posResult.positions) {
398
396
  const pos = posResult.positions.find(p => {
@@ -400,24 +398,67 @@ const launchAlgo = async (service, account, contract, config) => {
400
398
  return sym.includes(contract.name) || sym.includes(contractId);
401
399
  });
402
400
 
403
- if (pos && pos.quantity !== 0) {
404
- currentPosition = pos.quantity;
405
- const side = pos.quantity > 0 ? 'LONG' : 'SHORT';
406
- const pnl = pos.profitAndLoss || 0;
401
+ const newPositionQty = pos?.quantity || 0;
402
+
403
+ // Position just closed - cancel remaining orders and log result
404
+ if (lastPositionQty !== 0 && newPositionQty === 0) {
405
+ // Cancel all open orders to prevent new positions
406
+ try {
407
+ await service.cancelAllOrders(account.accountId);
408
+ algoLogger.info(ui, 'ORDERS CANCELLED', 'Position closed - brackets removed');
409
+ } catch (e) {
410
+ // Silent fail
411
+ }
407
412
 
408
- // Check if position closed (win/loss)
409
- if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
410
- else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
411
- } else {
412
- currentPosition = 0;
413
+ // Get real trade data from API
414
+ try {
415
+ const tradesResult = await service.getTrades(account.accountId);
416
+ if (tradesResult.success && tradesResult.trades?.length > 0) {
417
+ // Count completed trades (those with profitAndLoss not null)
418
+ const completedTrades = tradesResult.trades.filter(t => t.profitAndLoss !== null);
419
+
420
+ // Update stats from real trades
421
+ let wins = 0, losses = 0;
422
+ for (const trade of completedTrades) {
423
+ if (trade.profitAndLoss > 0) wins++;
424
+ else if (trade.profitAndLoss < 0) losses++;
425
+ }
426
+ stats.trades = completedTrades.length;
427
+ stats.wins = wins;
428
+ stats.losses = losses;
429
+
430
+ // Log the trade that just closed
431
+ const lastTrade = completedTrades[completedTrades.length - 1];
432
+ if (lastTrade) {
433
+ const pnl = lastTrade.profitAndLoss || 0;
434
+ const side = lastTrade.side === 0 ? 'LONG' : 'SHORT';
435
+ const exitPrice = lastTrade.price || 0;
436
+
437
+ if (pnl >= 0) {
438
+ algoLogger.targetHit(ui, symbolName, exitPrice, pnl);
439
+ } else {
440
+ algoLogger.stopHit(ui, symbolName, exitPrice, Math.abs(pnl));
441
+ }
442
+ algoLogger.positionClosed(ui, symbolName, side, contracts, exitPrice, pnl);
443
+
444
+ // Record in strategy for adaptation
445
+ strategy.recordTradeResult(pnl);
446
+ }
447
+ }
448
+ } catch (e) {
449
+ // Silent fail - trades API might not be available
450
+ }
413
451
  }
452
+
453
+ lastPositionQty = newPositionQty;
454
+ currentPosition = newPositionQty;
414
455
  }
415
456
 
416
457
  // Check target/risk
417
458
  if (stats.pnl >= dailyTarget) {
418
459
  stopReason = 'target';
419
460
  running = false;
420
- algoLogger.targetHit(ui, symbolName, 0, stats.pnl);
461
+ algoLogger.info(ui, 'DAILY TARGET REACHED', `+$${stats.pnl.toFixed(2)}`);
421
462
  } else if (stats.pnl <= -maxRisk) {
422
463
  stopReason = 'risk';
423
464
  running = false;
@@ -186,8 +186,8 @@ const showStats = async (service) => {
186
186
  } catch (e) {}
187
187
  }
188
188
 
189
- // ========== AGGREGATE STATS FROM API DATA ==========
190
- // Stats come from API (lifetimeStats) or calculated from API trade data
189
+ // ========== AGGREGATE STATS FROM TRADE HISTORY (API DATA) ==========
190
+ // Calculate stats from actual trades to ensure consistency with displayed trades
191
191
 
192
192
  let stats = {
193
193
  totalTrades: 0, winningTrades: 0, losingTrades: 0,
@@ -197,27 +197,8 @@ const showStats = async (service) => {
197
197
  longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
198
198
  };
199
199
 
200
- // First: aggregate lifetimeStats from APIs (ProjectX)
201
- for (const account of activeAccounts) {
202
- if (account.lifetimeStats) {
203
- const s = account.lifetimeStats;
204
- stats.totalTrades += s.totalTrades || 0;
205
- stats.winningTrades += s.winningTrades || 0;
206
- stats.losingTrades += s.losingTrades || 0;
207
- stats.totalWinAmount += s.totalWinAmount || 0;
208
- stats.totalLossAmount += s.totalLossAmount || 0;
209
- stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
210
- stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
211
- stats.totalVolume += s.totalVolume || 0;
212
- stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
213
- stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
214
- stats.longTrades += s.longTrades || 0;
215
- stats.shortTrades += s.shortTrades || 0;
216
- }
217
- }
218
-
219
- // If no lifetimeStats, calculate from trade history (still 100% API data)
220
- if (stats.totalTrades === 0 && allTrades.length > 0) {
200
+ // Calculate stats from trade history (100% API data, consistent with displayed trades)
201
+ if (allTrades.length > 0) {
221
202
  stats.totalTrades = allTrades.length;
222
203
  let consecutiveWins = 0, consecutiveLosses = 0;
223
204
 
@@ -464,18 +445,31 @@ const showStats = async (service) => {
464
445
  // Header
465
446
  const header = ` ${'Time'.padEnd(colTime)}| ${'Symbol'.padEnd(colSymbol)}| ${'Price'.padEnd(colPrice)}| ${'P&L'.padEnd(colPnl)}| ${'Side'.padEnd(colSide)}| ${'Account'.padEnd(colAccount - 2)}`;
466
447
  console.log(chalk.cyan('\u2551') + chalk.white(header) + chalk.cyan('\u2551'));
467
- console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
448
+ console.log(chalk.cyan('\u255F') + chalk.cyan('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2562'));
449
+
450
+ // Show only COMPLETED trades (with P&L), sorted by time (most recent first)
451
+ // Filter out entry fills (P&L = 0 or null) - only show exit fills with real P&L
452
+ const completedTrades = allTrades.filter(t => {
453
+ const pnl = t.profitAndLoss || t.pnl;
454
+ return pnl !== null && pnl !== undefined && pnl !== 0;
455
+ });
468
456
 
469
- const recentTrades = allTrades.slice(-10).reverse();
457
+ const sortedTrades = [...completedTrades].sort((a, b) => {
458
+ const timeA = new Date(a.creationTimestamp || a.timestamp || 0).getTime();
459
+ const timeB = new Date(b.creationTimestamp || b.timestamp || 0).getTime();
460
+ return timeB - timeA;
461
+ });
470
462
 
471
- for (const trade of recentTrades) {
463
+ for (const trade of sortedTrades) {
472
464
  const timestamp = trade.creationTimestamp || trade.timestamp;
473
465
  const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
474
466
  const symbol = extractSymbol(trade.contractId || trade.symbol);
475
467
  const price = (trade.price || 0).toFixed(2);
476
468
  const pnl = trade.profitAndLoss || trade.pnl || 0;
477
469
  const pnlText = pnl >= 0 ? `+$${pnl.toFixed(0)}` : `-$${Math.abs(pnl).toFixed(0)}`;
478
- const side = trade.side === 0 ? 'BUY' : trade.side === 1 ? 'SELL' : 'N/A';
470
+ // For completed trades, show the original direction (opposite of exit side)
471
+ const exitSide = trade.side; // 0=BUY exit means was SHORT, 1=SELL exit means was LONG
472
+ const tradeSide = exitSide === 0 ? 'SHORT' : 'LONG';
479
473
  const accountName = (trade.accountName || 'N/A').substring(0, colAccount - 3);
480
474
 
481
475
  // Build row with exact widths
@@ -483,20 +477,20 @@ const showStats = async (service) => {
483
477
  const symbolStr = symbol.padEnd(colSymbol);
484
478
  const priceStr = price.padEnd(colPrice);
485
479
  const pnlStr = pnlText.padEnd(colPnl);
486
- const sideStr = side.padEnd(colSide);
480
+ const sideStr = tradeSide.padEnd(colSide);
487
481
  const accountStr = accountName.padEnd(colAccount - 2);
488
482
 
489
483
  // Colored versions
490
484
  const pnlColored = pnl >= 0 ? chalk.green(pnlStr) : chalk.red(pnlStr);
491
- const sideColored = trade.side === 0 ? chalk.green(sideStr) : chalk.red(sideStr);
485
+ const sideColored = tradeSide === 'LONG' ? chalk.green(sideStr) : chalk.red(sideStr);
492
486
 
493
487
  const row = ` ${timeStr}| ${symbolStr}| ${priceStr}| ${pnlColored}| ${sideColored}| ${accountStr}`;
494
488
  console.log(chalk.cyan('\u2551') + row + chalk.cyan('\u2551'));
495
489
  }
496
490
 
497
- if (allTrades.length > 10) {
498
- const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
499
- console.log(chalk.cyan('\u2551') + moreMsg.padEnd(innerWidth) + chalk.cyan('\u2551'));
491
+ if (sortedTrades.length === 0) {
492
+ const msg = ' No completed trades yet';
493
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
500
494
  }
501
495
  } else {
502
496
  const msg = connectionTypes.rithmic > 0
@@ -384,6 +384,42 @@ class ProjectXService {
384
384
  }
385
385
  }
386
386
 
387
+ /**
388
+ * Get trades history for today
389
+ * @param {number|string} accountId - Account ID
390
+ * @returns {Promise<{success: boolean, trades: Array, error?: string}>}
391
+ */
392
+ async getTrades(accountId) {
393
+ try {
394
+ const id = validateAccountId(accountId);
395
+
396
+ // Get today's trades (from midnight UTC)
397
+ const now = new Date();
398
+ const startOfDay = new Date(now);
399
+ startOfDay.setUTCHours(0, 0, 0, 0);
400
+
401
+ const response = await this._request(
402
+ this.propfirm.gatewayApi,
403
+ '/api/Trade/search',
404
+ 'POST',
405
+ {
406
+ accountId: id,
407
+ startTimestamp: startOfDay.toISOString(),
408
+ endTimestamp: now.toISOString()
409
+ }
410
+ );
411
+
412
+ if (response.statusCode === 200) {
413
+ const trades = response.data.trades || response.data || [];
414
+ return { success: true, trades: Array.isArray(trades) ? trades : [] };
415
+ }
416
+
417
+ return { success: true, trades: [] };
418
+ } catch (err) {
419
+ return { success: true, trades: [], error: err.message };
420
+ }
421
+ }
422
+
387
423
  /**
388
424
  * Place an order
389
425
  * @param {Object} orderData - Order details