hedgequantx 2.6.88 → 2.6.90

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.6.88",
3
+ "version": "2.6.90",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -399,84 +399,68 @@ const launchAlgo = async (service, account, contract, config) => {
399
399
  }
400
400
 
401
401
  // ═══════════════════════════════════════════════════════════════════════════
402
- // REAL-TIME P&L VIA WEBSOCKET (like R Trader)
403
- // Listen to pnlUpdate events from Rithmic PNL_PLANT WebSocket
402
+ // REAL-TIME P&L VIA WEBSOCKET
403
+ // - Rithmic: Uses EventEmitter (service.on) for real-time updates
404
+ // - ProjectX: Uses HTTP polling (handled in pollPnL function below)
404
405
  // ═══════════════════════════════════════════════════════════════════════════
405
406
  const rithmicAccountId = account.rithmicAccountId || account.accountId;
407
+ const serviceHasEvents = typeof service.on === 'function';
406
408
 
407
- service.on('pnlUpdate', (pnlData) => {
408
- // Only update for our account
409
- if (pnlData.accountId !== rithmicAccountId) return;
410
-
411
- // ═══════════════════════════════════════════════════════════════════════════
412
- // ACCOUNT_PNL_UPDATE (451) - All R Trader account-level metrics
413
- // ═══════════════════════════════════════════════════════════════════════════
414
-
415
- // Closed P&L (realized) - from closed trades today
416
- if (pnlData.closedPositionPnl !== undefined) {
417
- stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
418
- }
419
-
420
- // Account Balance
421
- if (pnlData.accountBalance !== undefined) {
422
- stats.balance = parseFloat(pnlData.accountBalance);
423
- }
424
-
425
- // Buying Power (Available)
426
- if (pnlData.availableBuyingPower !== undefined) {
427
- stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
428
- }
429
-
430
- // Margin Balance
431
- if (pnlData.marginBalance !== undefined) {
432
- stats.margin = parseFloat(pnlData.marginBalance);
433
- }
434
-
435
- // Net Liquidation Value (balance + openPnl) - same as R Trader
436
- if (pnlData.netLiquidation !== undefined) {
437
- stats.netLiquidation = parseFloat(pnlData.netLiquidation);
438
- } else if (stats.balance !== null) {
439
- // Calculate if not provided directly
440
- stats.netLiquidation = stats.balance + (stats.openPnl || 0);
441
- }
442
-
443
- // Total P&L = openPnl (from positionUpdate) + closedPnl (from pnlUpdate)
444
- // This matches R Trader's "Today's P&L" calculation
445
- stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
446
- });
447
-
448
- // Listen to position updates for real-time position tracking (like R Trader)
449
- // INSTRUMENT_PNL_UPDATE (450) provides per-instrument P&L in real-time
450
- service.on('positionUpdate', (pos) => {
451
- if (!pos || pos.accountId !== rithmicAccountId) return;
452
- if (pos.symbol !== symbolName && !pos.symbol?.includes(symbolName.replace(/[A-Z]\d+$/, ''))) return;
453
-
454
- // Update position info (like R Trader Positions panel)
455
- const wasFlat = stats.position === 0;
456
- stats.position = pos.quantity || 0;
457
- stats.entryPrice = pos.averagePrice || 0;
458
- currentPosition = pos.quantity || 0;
459
-
460
- // Track entry time when opening a new position
461
- if (wasFlat && stats.position !== 0) {
462
- stats.entryTime = Date.now();
463
- } else if (stats.position === 0) {
464
- stats.entryTime = null;
465
- }
466
-
467
- // CRITICAL: Update Open P&L from instrument-level data (real-time, same as R Trader)
468
- // pos.openPnl comes from INSTRUMENT_PNL_UPDATE (450) - this is the unrealized P&L
469
- if (pos.openPnl !== undefined && pos.openPnl !== null) {
470
- stats.openPnl = pos.openPnl;
471
- // Recalculate total P&L whenever Open P&L changes
472
- // Total P&L = openPnl + closedPnl (same formula as R Trader)
409
+ if (serviceHasEvents) {
410
+ // RITHMIC ONLY: Real-time P&L via WebSocket
411
+ service.on('pnlUpdate', (pnlData) => {
412
+ // Only update for our account
413
+ if (pnlData.accountId !== rithmicAccountId) return;
414
+
415
+ // ACCOUNT_PNL_UPDATE (451) - All R Trader account-level metrics
416
+ if (pnlData.closedPositionPnl !== undefined) {
417
+ stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
418
+ }
419
+ if (pnlData.accountBalance !== undefined) {
420
+ stats.balance = parseFloat(pnlData.accountBalance);
421
+ }
422
+ if (pnlData.availableBuyingPower !== undefined) {
423
+ stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
424
+ }
425
+ if (pnlData.marginBalance !== undefined) {
426
+ stats.margin = parseFloat(pnlData.marginBalance);
427
+ }
428
+ if (pnlData.netLiquidation !== undefined) {
429
+ stats.netLiquidation = parseFloat(pnlData.netLiquidation);
430
+ } else if (stats.balance !== null) {
431
+ stats.netLiquidation = stats.balance + (stats.openPnl || 0);
432
+ }
433
+
434
+ // Total P&L = openPnl + closedPnl (same as R Trader)
473
435
  stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
474
- // Also update Net Liquidation (balance + openPnl)
475
- if (stats.balance !== null) {
476
- stats.netLiquidation = stats.balance + stats.openPnl;
436
+ });
437
+
438
+ // RITHMIC ONLY: Real-time position updates
439
+ service.on('positionUpdate', (pos) => {
440
+ if (!pos || pos.accountId !== rithmicAccountId) return;
441
+ if (pos.symbol !== symbolName && !pos.symbol?.includes(symbolName.replace(/[A-Z]\d+$/, ''))) return;
442
+
443
+ const wasFlat = stats.position === 0;
444
+ stats.position = pos.quantity || 0;
445
+ stats.entryPrice = pos.averagePrice || 0;
446
+ currentPosition = pos.quantity || 0;
447
+
448
+ if (wasFlat && stats.position !== 0) {
449
+ stats.entryTime = Date.now();
450
+ } else if (stats.position === 0) {
451
+ stats.entryTime = null;
477
452
  }
478
- }
479
- });
453
+
454
+ // Update Open P&L from INSTRUMENT_PNL_UPDATE (450)
455
+ if (pos.openPnl !== undefined && pos.openPnl !== null) {
456
+ stats.openPnl = pos.openPnl;
457
+ stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
458
+ if (stats.balance !== null) {
459
+ stats.netLiquidation = stats.balance + stats.openPnl;
460
+ }
461
+ }
462
+ });
463
+ }
480
464
 
481
465
  // Initialize AI Strategy Supervisor - agents observe, learn & optimize
482
466
  // Only if user enabled AI in config
@@ -869,20 +853,32 @@ const launchAlgo = async (service, account, contract, config) => {
869
853
  algoLogger.error(ui, 'CONNECTION ERROR', e.message.substring(0, 50));
870
854
  }
871
855
 
872
- // Poll account P&L and sync with real trades from API (fallback, WebSocket is primary)
856
+ // Poll account P&L and sync with real trades from API
857
+ // For ProjectX: This is the PRIMARY source of P&L data (no WebSocket events)
858
+ // For Rithmic: This is a fallback (WebSocket events are primary)
873
859
  const pollPnL = async () => {
874
860
  try {
875
- // Get account P&L (fallback if WebSocket not updating)
861
+ // Get account P&L
876
862
  const accountResult = await service.getTradingAccounts();
877
863
  if (accountResult.success && accountResult.accounts) {
878
864
  const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
879
- if (acc && acc.profitAndLoss !== undefined && acc.profitAndLoss !== null) {
880
- // Use day P&L directly (same as R Trader)
881
- stats.pnl = acc.profitAndLoss;
865
+ if (acc) {
866
+ // Total day P&L
867
+ if (acc.profitAndLoss !== undefined && acc.profitAndLoss !== null) {
868
+ stats.pnl = acc.profitAndLoss;
869
+ }
870
+ // Closed P&L (realized)
871
+ if (acc.realizedPnl !== undefined) {
872
+ stats.closedPnl = acc.realizedPnl;
873
+ }
874
+ // Balance
875
+ if (acc.balance !== undefined) {
876
+ stats.balance = acc.balance;
877
+ }
882
878
  }
883
879
  }
884
880
 
885
- // Check positions - detect when position closes
881
+ // Check positions - get Open P&L and detect when position closes
886
882
  const posResult = await service.getPositions(account.accountId);
887
883
  if (posResult.success && posResult.positions) {
888
884
  const pos = posResult.positions.find(p => {
@@ -892,6 +888,20 @@ const launchAlgo = async (service, account, contract, config) => {
892
888
 
893
889
  const newPositionQty = pos?.quantity || 0;
894
890
 
891
+ // Update Open P&L from position (unrealized P&L)
892
+ if (pos && pos.unrealizedPnl !== undefined) {
893
+ stats.openPnl = pos.unrealizedPnl;
894
+ } else if (pos && pos.profitAndLoss !== undefined) {
895
+ stats.openPnl = pos.profitAndLoss;
896
+ } else if (newPositionQty === 0) {
897
+ stats.openPnl = 0; // Flat = no open P&L
898
+ }
899
+
900
+ // Recalculate total P&L if we have components
901
+ if (stats.openPnl !== null && stats.closedPnl !== null) {
902
+ stats.pnl = stats.openPnl + stats.closedPnl;
903
+ }
904
+
895
905
  // Position just closed - cancel remaining orders and log result
896
906
  if (lastPositionQty !== 0 && newPositionQty === 0) {
897
907
  // Cancel all open orders to prevent new positions
@@ -244,7 +244,11 @@ function getMarketBiasLog(direction, delta, ofi) {
244
244
  * Get a momentum log
245
245
  */
246
246
  function getMomentumLog(momentum, zscore) {
247
- const isUp = momentum > 0;
247
+ // Handle undefined/NaN values
248
+ const mom = typeof momentum === 'number' && !isNaN(momentum) ? momentum : 0;
249
+ const z = typeof zscore === 'number' && !isNaN(zscore) ? zscore : 0;
250
+
251
+ const isUp = mom > 0;
248
252
  const pool = isUp ? MOMENTUM_UP_MESSAGES : MOMENTUM_DOWN_MESSAGES;
249
253
  const category = isUp ? 'mom_up' : 'mom_down';
250
254
  const message = getVariedMessage(category, pool, isUp ? 'Momentum up' : 'Momentum down');
@@ -252,7 +256,7 @@ function getMomentumLog(momentum, zscore) {
252
256
 
253
257
  return {
254
258
  message: `${arrow} ${message}`,
255
- details: `Mom=${momentum.toFixed(1)} | Z=${zscore.toFixed(2)}`,
259
+ details: `Mom=${mom.toFixed(1)} | Z=${z.toFixed(2)}`,
256
260
  };
257
261
  }
258
262
 
@@ -260,7 +264,10 @@ function getMomentumLog(momentum, zscore) {
260
264
  * Get a mean reversion log
261
265
  */
262
266
  function getMeanReversionLog(zscore) {
263
- const isOverbought = zscore > 0;
267
+ // Handle undefined/NaN values
268
+ const z = typeof zscore === 'number' && !isNaN(zscore) ? zscore : 0;
269
+
270
+ const isOverbought = z > 0;
264
271
  const pool = isOverbought ? OVERBOUGHT_MESSAGES : OVERSOLD_MESSAGES;
265
272
  const category = isOverbought ? 'mr_overbought' : 'mr_oversold';
266
273
  const message = getVariedMessage(category, pool, isOverbought ? 'Overbought' : 'Oversold');
@@ -268,7 +275,7 @@ function getMeanReversionLog(zscore) {
268
275
 
269
276
  return {
270
277
  message: `${arrow} ${message}`,
271
- details: `Z-Score: ${zscore.toFixed(2)} | Looking for ${isOverbought ? 'SHORT' : 'LONG'}`,
278
+ details: `Z-Score: ${z.toFixed(2)} | Looking for ${isOverbought ? 'SHORT' : 'LONG'}`,
272
279
  };
273
280
  }
274
281