hedgequantx 2.6.45 → 2.6.47

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.45",
3
+ "version": "2.6.47",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -245,14 +245,21 @@ const launchAlgo = async (service, account, contract, config) => {
245
245
  risk: maxRisk,
246
246
  propfirm: account.propfirm || 'Unknown',
247
247
  platform: connectionType,
248
- pnl: 0,
249
- // R Trader style P&L breakdown
250
- openPnl: 0, // Unrealized P&L (current position)
251
- closedPnl: 0, // Realized P&L (closed trades today)
252
- // Position info (like R Trader)
253
- position: 0, // Current position qty (+ long, - short)
254
- entryPrice: 0, // Average entry price
255
- lastPrice: 0, // Last market price
248
+ // ═══════════════════════════════════════════════════════════════════════════
249
+ // R TRADER METRICS - All from Rithmic API (ACCOUNT_PNL_UPDATE 451)
250
+ // ═══════════════════════════════════════════════════════════════════════════
251
+ pnl: null, // Today's P&L (openPnl + closedPnl)
252
+ openPnl: null, // Unrealized P&L (current position) - from INSTRUMENT_PNL_UPDATE 450
253
+ closedPnl: null, // Realized P&L (closed trades today)
254
+ balance: null, // Account Balance
255
+ buyingPower: null, // Available Buying Power
256
+ margin: null, // Margin Balance
257
+ netLiquidation: null, // Net Liquidation Value (balance + openPnl)
258
+ // Position info (like R Trader Positions panel) - from INSTRUMENT_PNL_UPDATE 450
259
+ position: 0, // Current position qty (+ long, - short)
260
+ entryPrice: 0, // Average entry price
261
+ lastPrice: 0, // Last market price (from ticker)
262
+ // Trading stats
256
263
  trades: 0,
257
264
  wins: 0,
258
265
  losses: 0,
@@ -379,12 +386,37 @@ const launchAlgo = async (service, account, contract, config) => {
379
386
  // Only update for our account
380
387
  if (pnlData.accountId !== rithmicAccountId) return;
381
388
 
382
- // ACCOUNT_PNL_UPDATE (451) provides account-level totals
383
- // closedPositionPnl = realized P&L from all closed trades today
384
- const closedPnl = parseFloat(pnlData.closedPositionPnl || 0);
389
+ // ═══════════════════════════════════════════════════════════════════════════
390
+ // ACCOUNT_PNL_UPDATE (451) - All R Trader account-level metrics
391
+ // ═══════════════════════════════════════════════════════════════════════════
385
392
 
386
- // Update closed P&L (realized) from account-level data
387
- stats.closedPnl = closedPnl;
393
+ // Closed P&L (realized) - from closed trades today
394
+ if (pnlData.closedPositionPnl !== undefined) {
395
+ stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
396
+ }
397
+
398
+ // Account Balance
399
+ if (pnlData.accountBalance !== undefined) {
400
+ stats.balance = parseFloat(pnlData.accountBalance);
401
+ }
402
+
403
+ // Buying Power (Available)
404
+ if (pnlData.availableBuyingPower !== undefined) {
405
+ stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
406
+ }
407
+
408
+ // Margin Balance
409
+ if (pnlData.marginBalance !== undefined) {
410
+ stats.margin = parseFloat(pnlData.marginBalance);
411
+ }
412
+
413
+ // Net Liquidation Value (balance + openPnl) - same as R Trader
414
+ if (pnlData.netLiquidation !== undefined) {
415
+ stats.netLiquidation = parseFloat(pnlData.netLiquidation);
416
+ } else if (stats.balance !== null) {
417
+ // Calculate if not provided directly
418
+ stats.netLiquidation = stats.balance + (stats.openPnl || 0);
419
+ }
388
420
 
389
421
  // Total P&L = openPnl (from positionUpdate) + closedPnl (from pnlUpdate)
390
422
  // This matches R Trader's "Today's P&L" calculation
@@ -406,11 +438,13 @@ const launchAlgo = async (service, account, contract, config) => {
406
438
  // pos.openPnl comes from INSTRUMENT_PNL_UPDATE (450) - this is the unrealized P&L
407
439
  if (pos.openPnl !== undefined && pos.openPnl !== null) {
408
440
  stats.openPnl = pos.openPnl;
409
- }
410
- // Update day P&L if available
411
- if (pos.dayPnl !== undefined && pos.dayPnl !== null) {
441
+ // Recalculate total P&L whenever Open P&L changes
412
442
  // Total P&L = openPnl + closedPnl (same formula as R Trader)
413
443
  stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
444
+ // Also update Net Liquidation (balance + openPnl)
445
+ if (stats.balance !== null) {
446
+ stats.netLiquidation = stats.balance + stats.openPnl;
447
+ }
414
448
  }
415
449
  });
416
450
 
@@ -210,29 +210,55 @@ class AlgoUI {
210
210
 
211
211
  this._line(chalk.cyan(GM));
212
212
 
213
- // Row 4: Target | Risk
213
+ // Row 4: Balance | Net Liquidation (R Trader style)
214
+ const balanceStr = stats.balance !== null && stats.balance !== undefined
215
+ ? '$' + stats.balance.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
216
+ : '--';
217
+ const netLiqStr = stats.netLiquidation !== null && stats.netLiquidation !== undefined
218
+ ? '$' + stats.netLiquidation.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
219
+ : '--';
220
+ const r4c1 = buildCell('BALANCE', balanceStr, chalk.white, colL);
221
+ const r4c2 = buildCell('NET LIQ', netLiqStr, chalk.cyan, colR);
222
+ row(r4c1.padded, r4c2.padded);
223
+
224
+ this._line(chalk.cyan(GM));
225
+
226
+ // Row 5: Buying Power | Margin (R Trader style)
227
+ const bpStr = stats.buyingPower !== null && stats.buyingPower !== undefined
228
+ ? '$' + stats.buyingPower.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
229
+ : '--';
230
+ const marginStr = stats.margin !== null && stats.margin !== undefined
231
+ ? '$' + stats.margin.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
232
+ : '--';
233
+ const r5c1 = buildCell('BUYING PWR', bpStr, chalk.green, colL);
234
+ const r5c2 = buildCell('MARGIN', marginStr, chalk.yellow, colR);
235
+ row(r5c1.padded, r5c2.padded);
236
+
237
+ this._line(chalk.cyan(GM));
238
+
239
+ // Row 6: Target | Risk
214
240
  const targetStr = stats.target !== null && stats.target !== undefined ? '$' + stats.target.toFixed(2) : '--';
215
241
  const riskStr = stats.risk !== null && stats.risk !== undefined ? '$' + stats.risk.toFixed(2) : '--';
216
- const r4c1 = buildCell('TARGET', targetStr, chalk.green, colL);
217
- const r4c2 = buildCell('RISK', riskStr, chalk.red, colR);
218
- row(r4c1.padded, r4c2.padded);
242
+ const r6c1 = buildCell('TARGET', targetStr, chalk.green, colL);
243
+ const r6c2 = buildCell('RISK', riskStr, chalk.red, colR);
244
+ row(r6c1.padded, r6c2.padded);
219
245
 
220
246
  this._line(chalk.cyan(GM));
221
247
 
222
- // Row 5: Trades W/L | Entry Price (if in position)
223
- const r5c1t = ` ${chalk.bold('TRADES')}: ${chalk.cyan.bold(stats.trades || 0)} ${chalk.bold('W/L')}: ${chalk.green.bold(stats.wins || 0)}/${chalk.red.bold(stats.losses || 0)}`;
224
- const r5c1p = ` TRADES: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
248
+ // Row 7: Trades W/L | Entry Price (if in position)
249
+ const r7c1t = ` ${chalk.bold('TRADES')}: ${chalk.cyan.bold(stats.trades || 0)} ${chalk.bold('W/L')}: ${chalk.green.bold(stats.wins || 0)}/${chalk.red.bold(stats.losses || 0)}`;
250
+ const r7c1p = ` TRADES: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
225
251
  const entryStr = stats.entryPrice > 0 ? stats.entryPrice.toFixed(2) : '--';
226
- const r5c2 = buildCell('ENTRY', entryStr, chalk.yellow, colR);
227
- row(r5c1t + pad(colL - r5c1p.length), r5c2.padded);
252
+ const r7c2 = buildCell('ENTRY', entryStr, chalk.yellow, colR);
253
+ row(r7c1t + pad(colL - r7c1p.length), r7c2.padded);
228
254
 
229
255
  this._line(chalk.cyan(GM));
230
256
 
231
- // Row 6: Connection | Propfirm
257
+ // Row 8: Connection | Propfirm
232
258
  const connection = stats.platform || 'ProjectX';
233
- const r6c1 = buildCell('CONNECTION', connection, chalk.cyan, colL);
234
- const r6c2 = buildCell('PROPFIRM', stats.propfirm || 'N/A', chalk.cyan, colR);
235
- row(r6c1.padded, r6c2.padded);
259
+ const r8c1 = buildCell('CONNECTION', connection, chalk.cyan, colL);
260
+ const r8c2 = buildCell('PROPFIRM', stats.propfirm || 'N/A', chalk.cyan, colR);
261
+ row(r8c1.padded, r8c2.padded);
236
262
 
237
263
  this._line(chalk.cyan(GB));
238
264
  }
@@ -109,28 +109,46 @@ const getTradingAccounts = async (service) => {
109
109
  debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
110
110
  debug(` accountPnL map size:`, service.accountPnL.size);
111
111
 
112
- // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
113
- const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
114
- const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
115
- const closedPnL = pnlData.closedPositionPnl ? parseFloat(pnlData.closedPositionPnl) : null;
116
- const dayPnL = pnlData.dayPnl ? parseFloat(pnlData.dayPnl) : null;
112
+ // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS (RULES.md compliant)
113
+ // All values are null if not available from API
114
+ const accountBalance = pnlData.accountBalance !== undefined ? parseFloat(pnlData.accountBalance) : null;
115
+ const openPnL = pnlData.openPositionPnl !== undefined ? parseFloat(pnlData.openPositionPnl) : null;
116
+ const closedPnL = pnlData.closedPositionPnl !== undefined ? parseFloat(pnlData.closedPositionPnl) : null;
117
+ const dayPnL = pnlData.dayPnl !== undefined ? parseFloat(pnlData.dayPnl) : null;
118
+
119
+ // R Trader additional metrics (from ACCOUNT_PNL_UPDATE 451)
120
+ const buyingPower = pnlData.availableBuyingPower !== undefined ? parseFloat(pnlData.availableBuyingPower) : null;
121
+ const margin = pnlData.marginBalance !== undefined ? parseFloat(pnlData.marginBalance) : null;
122
+ const cashOnHand = pnlData.cashOnHand !== undefined ? parseFloat(pnlData.cashOnHand) : null;
117
123
 
118
124
  // Calculate P&L like R Trader: openPositionPnl + closedPositionPnl
119
125
  // This matches what R Trader shows as "Today's P&L"
120
126
  const totalPnL = (openPnL !== null || closedPnL !== null)
121
127
  ? (openPnL || 0) + (closedPnL || 0)
122
128
  : null;
129
+
130
+ // Net Liquidation Value = Account Balance + Open P&L (same as R Trader)
131
+ const netLiquidation = (accountBalance !== null || openPnL !== null)
132
+ ? (accountBalance || 0) + (openPnL || 0)
133
+ : null;
123
134
 
124
135
  return {
125
136
  accountId: hashAccountId(acc.accountId),
126
137
  rithmicAccountId: acc.accountId,
127
138
  accountName: acc.accountId, // Never expose real name - only account ID
128
139
  name: acc.accountId, // Never expose real name - only account ID
140
+ // Core metrics (same as R Trader)
129
141
  balance: accountBalance,
130
142
  profitAndLoss: totalPnL, // Same as R Trader: open + closed
131
143
  openPnL: openPnL,
132
144
  closedPnL: closedPnL,
133
- dayPnL: dayPnL, // Keep for reference
145
+ dayPnL: dayPnL,
146
+ // R Trader additional metrics
147
+ buyingPower: buyingPower,
148
+ margin: margin,
149
+ cashOnHand: cashOnHand,
150
+ netLiquidation: netLiquidation,
151
+ // Meta
134
152
  status: 0,
135
153
  platform: 'Rithmic',
136
154
  propfirm: service.propfirm.name,
@@ -332,17 +332,23 @@ const handleAccountPnLUpdate = (service, data) => {
332
332
  debug('Decoded Account PNL:', JSON.stringify(pnl));
333
333
 
334
334
  if (pnl.accountId) {
335
+ // Store ALL R Trader metrics from ACCOUNT_PNL_UPDATE (451)
335
336
  const pnlData = {
337
+ // Core P&L
336
338
  accountBalance: parseFloat(pnl.accountBalance || 0),
337
- cashOnHand: parseFloat(pnl.cashOnHand || 0),
338
- marginBalance: parseFloat(pnl.marginBalance || 0),
339
339
  openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
340
340
  closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
341
341
  dayPnl: parseFloat(pnl.dayPnl || 0),
342
+ // R Trader additional metrics
343
+ availableBuyingPower: parseFloat(pnl.availableBuyingPower || 0),
344
+ marginBalance: parseFloat(pnl.marginBalance || 0),
345
+ cashOnHand: parseFloat(pnl.cashOnHand || 0),
346
+ // Net Liquidation = Account Balance + Open P&L (same as R Trader)
347
+ netLiquidation: parseFloat(pnl.accountBalance || 0) + parseFloat(pnl.openPositionPnl || 0),
342
348
  };
343
349
  debug('Storing PNL for account:', pnl.accountId, pnlData);
344
350
  service.accountPnL.set(pnl.accountId, pnlData);
345
- service.emit('pnlUpdate', pnl);
351
+ service.emit('pnlUpdate', { accountId: pnl.accountId, ...pnlData });
346
352
  } else {
347
353
  debug('No accountId in PNL response');
348
354
  }
@@ -353,6 +359,8 @@ const handleAccountPnLUpdate = (service, data) => {
353
359
 
354
360
  /**
355
361
  * Handle instrument PnL update (positions)
362
+ * INSTRUMENT_PNL_UPDATE (450) - Real-time position and P&L per instrument
363
+ * This is the PRIMARY source for unrealized P&L (same as R Trader Positions panel)
356
364
  */
357
365
  const handleInstrumentPnLUpdate = (service, data) => {
358
366
  try {
@@ -361,26 +369,33 @@ const handleInstrumentPnLUpdate = (service, data) => {
361
369
  const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
362
370
  const netQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
363
371
 
372
+ // Build position data - ALWAYS emit, even when FLAT (netQty === 0)
373
+ // This ensures Open P&L resets to 0 when position closes (like R Trader)
374
+ const positionData = {
375
+ accountId: pos.accountId,
376
+ symbol: pos.symbol,
377
+ exchange: pos.exchange || 'CME',
378
+ quantity: netQty,
379
+ averagePrice: netQty !== 0 ? (pos.avgOpenFillPrice || 0) : 0,
380
+ // Open P&L from API - this is the real-time unrealized P&L (same as R Trader)
381
+ openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
382
+ closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
383
+ dayPnl: parseFloat(pos.dayPnl || 0),
384
+ isSnapshot: pos.isSnapshot || false,
385
+ };
386
+
364
387
  if (netQty !== 0) {
365
- service.positions.set(key, {
366
- accountId: pos.accountId,
367
- symbol: pos.symbol,
368
- exchange: pos.exchange || 'CME',
369
- quantity: netQty,
370
- averagePrice: pos.avgOpenFillPrice || 0,
371
- openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
372
- closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
373
- dayPnl: parseFloat(pos.dayPnl || 0),
374
- isSnapshot: pos.isSnapshot || false,
375
- });
388
+ service.positions.set(key, positionData);
376
389
  } else {
377
390
  service.positions.delete(key);
378
391
  }
379
392
 
380
- service.emit('positionUpdate', service.positions.get(key));
393
+ // ALWAYS emit positionUpdate - even when FLAT
394
+ // This ensures UI updates Open P&L to 0 when position closes
395
+ service.emit('positionUpdate', positionData);
381
396
  }
382
397
  } catch (e) {
383
- // Ignore decode errors
398
+ debug('Error decoding Instrument PNL:', e.message);
384
399
  }
385
400
  };
386
401