hedgequantx 2.6.128 → 2.6.130

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.128",
3
+ "version": "2.6.130",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -181,17 +181,18 @@ const selectSymbol = async (service, account, allowMultiple = false) => {
181
181
  }
182
182
 
183
183
  // Multi-symbol selection mode
184
+ // Each contract will have a 'qty' property for number of contracts
184
185
  const selectedContracts = [];
185
186
 
186
187
  while (selectedContracts.length < MAX_MULTI_SYMBOLS) {
187
188
  console.log();
188
189
 
189
- // Show already selected symbols
190
+ // Show already selected symbols with quantities
190
191
  if (selectedContracts.length > 0) {
191
192
  console.log(chalk.cyan(` SELECTED (${selectedContracts.length}/${MAX_MULTI_SYMBOLS}):`));
192
193
  selectedContracts.forEach((c, i) => {
193
194
  const name = c.name || c.symbol;
194
- console.log(chalk.green(` ${i + 1}. ${name}`));
195
+ console.log(chalk.green(` ${i + 1}. ${name} x${c.qty}`));
195
196
  });
196
197
  console.log();
197
198
  }
@@ -225,6 +226,13 @@ const selectSymbol = async (service, account, allowMultiple = false) => {
225
226
 
226
227
  if (choice === 'done') break;
227
228
 
229
+ // Ask for number of contracts for this symbol
230
+ const symbolName = choice.name || choice.symbol;
231
+ const qty = await prompts.numberInput(`CONTRACTS FOR ${symbolName}:`, 1, 1, 10);
232
+ if (qty === null) continue; // User cancelled, don't add symbol
233
+
234
+ // Add qty to contract object
235
+ choice.qty = qty;
228
236
  selectedContracts.push(choice);
229
237
 
230
238
  if (selectedContracts.length >= MAX_MULTI_SYMBOLS) {
@@ -243,21 +251,29 @@ const selectSymbol = async (service, account, allowMultiple = false) => {
243
251
  */
244
252
  const configureAlgo = async (account, contractOrContracts) => {
245
253
  const contractList = Array.isArray(contractOrContracts) ? contractOrContracts : [contractOrContracts];
254
+ const isMultiSymbol = contractList.length > 1;
246
255
 
247
256
  console.log();
248
257
  console.log(chalk.cyan(' CONFIGURE ALGO PARAMETERS'));
249
258
 
250
- // Show selected symbols
251
- if (contractList.length > 1) {
259
+ // Show selected symbols with quantities (multi-symbol mode)
260
+ if (isMultiSymbol) {
252
261
  console.log(chalk.white(` Trading ${contractList.length} symbols:`));
253
262
  contractList.forEach((c, i) => {
254
- console.log(chalk.yellow(` ${i + 1}. ${c.name || c.symbol}`));
263
+ const name = c.name || c.symbol;
264
+ const qty = c.qty || 1;
265
+ console.log(chalk.yellow(` ${i + 1}. ${name} x${qty}`));
255
266
  });
256
267
  }
257
268
  console.log();
258
269
 
259
- const contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
260
- if (contracts === null) return null;
270
+ // Only ask for contracts in single-symbol mode
271
+ // In multi-symbol mode, qty is already set per symbol
272
+ let contracts = 1;
273
+ if (!isMultiSymbol) {
274
+ contracts = await prompts.numberInput('NUMBER OF CONTRACTS:', 1, 1, 10);
275
+ if (contracts === null) return null;
276
+ }
261
277
 
262
278
  const dailyTarget = await prompts.numberInput('DAILY TARGET ($):', 1000, 1, 10000);
263
279
  if (dailyTarget === null) return null;
@@ -1385,33 +1401,41 @@ const launchAlgo = async (service, account, contract, config) => {
1385
1401
  * Uses single market feed connection with multiple subscriptions
1386
1402
  * Each symbol has its own PositionManager and strategy instance
1387
1403
  *
1404
+ * Same logic as launchAlgo but for multiple symbols
1405
+ *
1388
1406
  * @param {Object} service - Rithmic trading service
1389
1407
  * @param {Object} account - Trading account
1390
1408
  * @param {Array} contracts - Array of contracts to trade
1391
1409
  * @param {Object} config - Algo configuration
1392
1410
  */
1393
1411
  const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
1394
- const { contracts: qty, dailyTarget, maxRisk, showName, enableAI } = config;
1412
+ const { dailyTarget, maxRisk, showName, enableAI } = config;
1395
1413
 
1396
1414
  const accountName = showName
1397
1415
  ? (account.accountName || account.rithmicAccountId || account.accountId)
1398
1416
  : 'HQX *****';
1399
1417
  const rithmicAccountId = account.rithmicAccountId || account.accountId;
1400
1418
 
1401
- // Build symbols string for UI
1402
- const symbolNames = contracts.map(c => c.name || c.symbol);
1403
- const symbolsDisplay = symbolNames.join(', ');
1419
+ // Build symbols string for UI (with quantities)
1420
+ const symbolsDisplay = contracts.map(c => {
1421
+ const name = c.name || c.symbol;
1422
+ const qty = c.qty || 1;
1423
+ return `${name}x${qty}`;
1424
+ }).join(', ');
1404
1425
 
1405
1426
  const ui = new AlgoUI({
1406
1427
  subtitle: `MULTI-SYMBOL (${contracts.length})`,
1407
1428
  mode: 'one-account'
1408
1429
  });
1409
1430
 
1410
- // Shared stats
1431
+ // Calculate total qty across all symbols
1432
+ const totalQty = contracts.reduce((sum, c) => sum + (c.qty || 1), 0);
1433
+
1434
+ // Shared stats (same structure as launchAlgo)
1411
1435
  const stats = {
1412
1436
  accountName,
1413
1437
  symbol: symbolsDisplay,
1414
- qty,
1438
+ qty: totalQty,
1415
1439
  target: dailyTarget,
1416
1440
  risk: maxRisk,
1417
1441
  propfirm: account.propfirm || 'Unknown',
@@ -1420,7 +1444,12 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1420
1444
  openPnl: null,
1421
1445
  closedPnl: null,
1422
1446
  balance: null,
1447
+ buyingPower: null,
1448
+ margin: null,
1449
+ netLiquidation: null,
1423
1450
  position: 0,
1451
+ entryPrice: 0,
1452
+ lastPrice: 0,
1424
1453
  trades: 0,
1425
1454
  wins: 0,
1426
1455
  losses: 0,
@@ -1429,7 +1458,12 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1429
1458
  connected: false,
1430
1459
  startTime: Date.now(),
1431
1460
  aiSupervision: enableAI || false,
1461
+ aiMode: null,
1462
+ agentCount: 0,
1432
1463
  fastPath: true,
1464
+ avgEntryLatency: 0,
1465
+ avgFillLatency: 0,
1466
+ entryLatencies: [],
1433
1467
  // Per-symbol tracking
1434
1468
  symbolStats: {},
1435
1469
  };
@@ -1437,23 +1471,43 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1437
1471
  // Initialize per-symbol stats
1438
1472
  contracts.forEach(c => {
1439
1473
  const name = c.name || c.symbol;
1440
- stats.symbolStats[name] = { position: 0, trades: 0, wins: 0, losses: 0, pnl: 0 };
1474
+ stats.symbolStats[name] = {
1475
+ position: 0,
1476
+ trades: 0,
1477
+ wins: 0,
1478
+ losses: 0,
1479
+ pnl: 0,
1480
+ tickCount: 0,
1481
+ };
1441
1482
  });
1442
1483
 
1443
1484
  let running = true;
1444
1485
  let stopReason = null;
1486
+ let tickCount = 0;
1487
+
1488
+ // Store contract info for later use (including qty per symbol)
1489
+ const contractInfoMap = {};
1490
+ contracts.forEach(c => {
1491
+ const name = c.name || c.symbol;
1492
+ contractInfoMap[name] = {
1493
+ tickSize: c.tickSize ?? null,
1494
+ tickValue: c.tickValue ?? null,
1495
+ contractId: c.id || c.symbol || c.name,
1496
+ exchange: c.exchange || 'CME',
1497
+ qty: c.qty || 1, // Per-symbol quantity
1498
+ };
1499
+ });
1445
1500
 
1446
1501
  // ═══════════════════════════════════════════════════════════════════════════
1447
- // POSITION MANAGERS - One per symbol
1502
+ // POSITION MANAGERS & STRATEGIES - One per symbol
1448
1503
  // ═══════════════════════════════════════════════════════════════════════════
1449
1504
  const positionManagers = {};
1450
1505
  const strategies = {};
1506
+ const pendingOrders = {}; // Track pending orders per symbol
1451
1507
 
1452
1508
  contracts.forEach(contract => {
1453
1509
  const symbolName = contract.name || contract.symbol;
1454
- const contractId = contract.id || contract.symbol || contract.name;
1455
- const tickSize = contract.tickSize ?? null;
1456
- const tickValue = contract.tickValue ?? null;
1510
+ const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
1457
1511
 
1458
1512
  // Create strategy instance for this symbol
1459
1513
  const strategy = Object.create(hftStrategy);
@@ -1461,6 +1515,7 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1461
1515
  strategy.initialize(contractId, tickSize, tickValue);
1462
1516
  }
1463
1517
  strategies[symbolName] = strategy;
1518
+ pendingOrders[symbolName] = false;
1464
1519
 
1465
1520
  // Create position manager for this symbol
1466
1521
  const pm = new PositionManager(service, strategy);
@@ -1470,11 +1525,16 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1470
1525
  pm.start();
1471
1526
  positionManagers[symbolName] = pm;
1472
1527
 
1473
- // Position manager events
1528
+ // ═══════════════════════════════════════════════════════════════════════
1529
+ // POSITION MANAGER EVENTS (same as launchAlgo)
1530
+ // ═══════════════════════════════════════════════════════════════════════
1474
1531
  pm.on('entryFilled', ({ position, fillLatencyMs }) => {
1532
+ stats.entryLatencies.push(fillLatencyMs);
1533
+ stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
1475
1534
  const side = position.side === 0 ? 'LONG' : 'SHORT';
1476
1535
  ui.addLog('filled', `[${symbolName}] ${side} ${position.size}x @ ${position.entryPrice} | ${fillLatencyMs}ms`);
1477
1536
  stats.symbolStats[symbolName].position = position.side === 0 ? position.size : -position.size;
1537
+ ui.render(stats);
1478
1538
  });
1479
1539
 
1480
1540
  pm.on('exitFilled', ({ exitPrice, pnlTicks, holdDurationMs }) => {
@@ -1486,44 +1546,112 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1486
1546
  stats.symbolStats[symbolName].trades++;
1487
1547
  stats.trades++;
1488
1548
 
1549
+ // Record trade for Recovery Math
1550
+ recoveryMath.recordTrade({
1551
+ pnl: pnlDollars,
1552
+ ticks: pnlTicks,
1553
+ side: pnlTicks >= 0 ? 'win' : 'loss',
1554
+ duration: holdDurationMs,
1555
+ });
1556
+
1557
+ // Update Recovery Mode state
1558
+ const recovery = recoveryMath.updateSessionPnL(
1559
+ stats.sessionPnl,
1560
+ FAST_SCALPING.RECOVERY?.ACTIVATION_PNL || -300,
1561
+ FAST_SCALPING.RECOVERY?.DEACTIVATION_PNL || -100
1562
+ );
1563
+
1564
+ if (recovery.justActivated) {
1565
+ stats.recoveryMode = true;
1566
+ ui.addLog('warning', `RECOVERY MODE ON - Kelly: ${(recoveryMath.calcKelly() * 100).toFixed(0)}%`);
1567
+ } else if (recovery.justDeactivated) {
1568
+ stats.recoveryMode = false;
1569
+ ui.addLog('success', `RECOVERY MODE OFF - Session P&L: $${stats.sessionPnl.toFixed(2)}`);
1570
+ }
1571
+
1489
1572
  if (pnlDollars >= 0) {
1490
1573
  stats.wins++;
1491
1574
  stats.symbolStats[symbolName].wins++;
1492
- ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} | ${holdSec}s`);
1575
+ ui.addLog('win', `[${symbolName}] +$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1493
1576
  } else {
1494
1577
  stats.losses++;
1495
1578
  stats.symbolStats[symbolName].losses++;
1496
- ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} | ${holdSec}s`);
1579
+ ui.addLog('loss', `[${symbolName}] -$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
1580
+ }
1581
+ } else if (pnlTicks !== null) {
1582
+ // Log with ticks only
1583
+ stats.trades++;
1584
+ stats.symbolStats[symbolName].trades++;
1585
+ if (pnlTicks >= 0) {
1586
+ stats.wins++;
1587
+ stats.symbolStats[symbolName].wins++;
1588
+ ui.addLog('win', `[${symbolName}] +${pnlTicks} ticks | ${holdSec}s`);
1589
+ } else {
1590
+ stats.losses++;
1591
+ stats.symbolStats[symbolName].losses++;
1592
+ ui.addLog('loss', `[${symbolName}] ${pnlTicks} ticks | ${holdSec}s`);
1497
1593
  }
1498
1594
  }
1499
1595
  stats.symbolStats[symbolName].position = 0;
1596
+ pendingOrders[symbolName] = false;
1500
1597
  ui.render(stats);
1501
1598
  });
1502
1599
 
1503
- // Strategy signals
1600
+ pm.on('holdComplete', () => {
1601
+ ui.addLog('ready', `[${symbolName}] Hold complete - monitoring exit`);
1602
+ });
1603
+
1604
+ pm.on('breakevenActivated', ({ breakevenPrice, pnlTicks }) => {
1605
+ ui.addLog('be', `[${symbolName}] BE @ ${breakevenPrice} | +${pnlTicks} ticks`);
1606
+ });
1607
+
1608
+ // ═══════════════════════════════════════════════════════════════════════
1609
+ // STRATEGY SIGNALS
1610
+ // ═══════════════════════════════════════════════════════════════════════
1504
1611
  strategy.on('signal', async (signal) => {
1505
1612
  if (!running) return;
1613
+ if (pendingOrders[symbolName]) return;
1506
1614
  if (!pm.canEnter(symbolName)) return;
1507
1615
 
1508
- const { direction } = signal;
1616
+ const { direction, confidence } = signal;
1509
1617
  const orderSide = direction === 'long' ? 0 : 1;
1510
1618
  const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
1511
1619
 
1512
- ui.addLog('entry', `[${symbolName}] ${sideStr} signal`);
1620
+ // Use per-symbol quantity
1621
+ const symbolQty = contractInfoMap[symbolName].qty;
1622
+
1623
+ // Calculate risk amount
1624
+ const kelly = Math.min(0.25, confidence || 0.15);
1625
+ const riskAmount = Math.round(maxRisk * kelly);
1626
+ const riskPct = Math.round((riskAmount / maxRisk) * 100);
1627
+
1628
+ pendingOrders[symbolName] = true;
1629
+ ui.addLog('entry', `[${symbolName}] ${sideStr} ${symbolQty}x | risk: $${riskAmount} (${riskPct}%)`);
1513
1630
 
1514
1631
  const orderData = {
1515
1632
  accountId: rithmicAccountId,
1516
1633
  symbol: symbolName,
1517
- exchange: contract.exchange || 'CME',
1518
- size: qty,
1634
+ exchange: contractInfoMap[symbolName].exchange,
1635
+ size: symbolQty,
1519
1636
  side: orderSide,
1520
1637
  };
1521
1638
 
1522
- const entryResult = service.fastEntry(orderData);
1523
- if (entryResult.success) {
1524
- pm.registerEntry(entryResult, orderData, { tickSize, tickValue, contractId });
1525
- } else {
1526
- ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
1639
+ try {
1640
+ const entryResult = service.fastEntry(orderData);
1641
+ if (entryResult.success) {
1642
+ pm.registerEntry(entryResult, orderData, contractInfoMap[symbolName]);
1643
+
1644
+ // Update avg entry latency
1645
+ stats.avgEntryLatency = stats.entryLatencies.length > 0
1646
+ ? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
1647
+ : entryResult.latencyMs;
1648
+ } else {
1649
+ ui.addLog('error', `[${symbolName}] Entry failed: ${entryResult.error}`);
1650
+ pendingOrders[symbolName] = false;
1651
+ }
1652
+ } catch (e) {
1653
+ ui.addLog('error', `[${symbolName}] Order error: ${e.message}`);
1654
+ pendingOrders[symbolName] = false;
1527
1655
  }
1528
1656
  });
1529
1657
  });
@@ -1535,13 +1663,14 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1535
1663
 
1536
1664
  marketFeed.on('connected', () => {
1537
1665
  stats.connected = true;
1538
- ui.addLog('success', 'Market feed connected');
1539
- ui.render(stats);
1666
+ algoLogger.dataConnected(ui, 'RTC');
1667
+ algoLogger.algoOperational(ui, 'RITHMIC');
1540
1668
  });
1541
1669
 
1542
1670
  marketFeed.on('tick', (tick) => {
1543
1671
  if (!running) return;
1544
1672
 
1673
+ tickCount++;
1545
1674
  stats.latency = tick.latency || 0;
1546
1675
 
1547
1676
  // Route tick to correct strategy based on symbol
@@ -1551,14 +1680,29 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1551
1680
  for (const [symbolName, strategy] of Object.entries(strategies)) {
1552
1681
  const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
1553
1682
  if (tickSymbol === baseSymbol || tickSymbol === symbolName || symbolName.startsWith(tickSymbol)) {
1683
+ stats.symbolStats[symbolName].tickCount++;
1684
+
1685
+ // Log first tick per symbol
1686
+ if (stats.symbolStats[symbolName].tickCount === 1) {
1687
+ algoLogger.info(ui, 'FIRST TICK', `[${symbolName}] price=${tick.price} bid=${tick.bid} ask=${tick.ask}`);
1688
+ } else if (stats.symbolStats[symbolName].tickCount === 100) {
1689
+ algoLogger.info(ui, 'DATA FLOWING', `[${symbolName}] 100 ticks received`);
1690
+ }
1691
+
1554
1692
  const tickData = {
1555
1693
  contractId: tick.contractId || symbolName,
1556
1694
  price: tick.price || tick.lastPrice || tick.bid,
1557
1695
  bid: tick.bid,
1558
1696
  ask: tick.ask,
1559
1697
  volume: tick.volume || tick.size || 1,
1698
+ side: tick.lastTradeSide || tick.side || 'unknown',
1560
1699
  timestamp: tick.timestamp || Date.now()
1561
1700
  };
1701
+
1702
+ // Update last price
1703
+ stats.lastPrice = tickData.price;
1704
+
1705
+ // Feed tick to strategy
1562
1706
  strategy.processTick(tickData);
1563
1707
 
1564
1708
  // Update price for position manager
@@ -1567,6 +1711,18 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1567
1711
  price: tickData.price,
1568
1712
  timestamp: tickData.timestamp,
1569
1713
  });
1714
+
1715
+ // Get momentum data from strategy for position manager
1716
+ const modelValues = strategy.getModelValues?.() || strategy.getModelValues?.(symbolName);
1717
+ if (modelValues && positionManagers[symbolName] && typeof positionManagers[symbolName].updateMomentum === 'function') {
1718
+ positionManagers[symbolName].updateMomentum(symbolName, {
1719
+ ofi: modelValues.ofi || 0,
1720
+ zscore: modelValues.zscore || 0,
1721
+ delta: modelValues.delta || 0,
1722
+ timestamp: tickData.timestamp,
1723
+ });
1724
+ }
1725
+
1570
1726
  break;
1571
1727
  }
1572
1728
  }
@@ -1575,14 +1731,31 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1575
1731
  });
1576
1732
 
1577
1733
  marketFeed.on('error', (err) => {
1578
- ui.addLog('error', `Feed error: ${err.message}`);
1734
+ algoLogger.error(ui, 'MARKET ERROR', err.message);
1579
1735
  });
1580
1736
 
1581
- // Connect and subscribe
1737
+ marketFeed.on('disconnected', (err) => {
1738
+ stats.connected = false;
1739
+ algoLogger.dataDisconnected(ui, 'WEBSOCKET', err?.message);
1740
+ });
1741
+
1742
+ // ═══════════════════════════════════════════════════════════════════════════
1743
+ // STARTUP LOGS (same as launchAlgo)
1744
+ // ═══════════════════════════════════════════════════════════════════════════
1745
+ const market = checkMarketHours();
1746
+ const sessionName = market.session || 'AMERICAN';
1747
+ const etTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/New_York' });
1748
+
1749
+ algoLogger.connectingToEngine(ui, account.accountId);
1750
+ algoLogger.engineStarting(ui, 'RITHMIC', dailyTarget, maxRisk);
1751
+ algoLogger.marketOpen(ui, sessionName.toUpperCase(), etTime);
1752
+ algoLogger.info(ui, 'FAST PATH', `Rithmic direct | ${contracts.length} symbols | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms`);
1753
+
1582
1754
  ui.render(stats);
1583
- ui.addLog('info', `MULTI-SYMBOL: ${contracts.length} symbols`);
1584
1755
 
1756
+ // Connect and subscribe
1585
1757
  try {
1758
+ algoLogger.info(ui, 'CONNECTING', `RITHMIC TICKER | ${contracts.length} symbols`);
1586
1759
  await marketFeed.connect();
1587
1760
  await new Promise(r => setTimeout(r, 1000));
1588
1761
 
@@ -1591,28 +1764,68 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1591
1764
  const symbolName = contract.name || contract.symbol;
1592
1765
  const exchange = contract.exchange || 'CME';
1593
1766
  marketFeed.subscribe(symbolName, exchange);
1594
- ui.addLog('success', `[${symbolName}] Subscribed`);
1767
+ algoLogger.info(ui, 'SUBSCRIBED', `${symbolName} (${exchange})`);
1595
1768
  }
1596
1769
  } else {
1597
- ui.addLog('error', 'Failed to connect market feed');
1770
+ algoLogger.error(ui, 'CONNECTION', 'Failed to connect market feed');
1598
1771
  }
1599
1772
  } catch (e) {
1600
- ui.addLog('error', `Connection error: ${e.message}`);
1773
+ algoLogger.error(ui, 'CONNECTION ERROR', e.message);
1601
1774
  }
1602
1775
 
1603
1776
  // ═══════════════════════════════════════════════════════════════════════════
1604
- // P&L UPDATES VIA WEBSOCKET
1777
+ // REAL-TIME P&L VIA WEBSOCKET (same as launchAlgo)
1605
1778
  // ═══════════════════════════════════════════════════════════════════════════
1606
1779
  if (typeof service.on === 'function') {
1780
+ // Account-level P&L updates
1607
1781
  service.on('pnlUpdate', (pnlData) => {
1608
1782
  if (pnlData.accountId !== rithmicAccountId) return;
1783
+
1609
1784
  if (pnlData.closedPositionPnl !== undefined) {
1610
1785
  stats.closedPnl = parseFloat(pnlData.closedPositionPnl);
1611
1786
  }
1612
1787
  if (pnlData.accountBalance !== undefined) {
1613
1788
  stats.balance = parseFloat(pnlData.accountBalance);
1614
1789
  }
1790
+ if (pnlData.availableBuyingPower !== undefined) {
1791
+ stats.buyingPower = parseFloat(pnlData.availableBuyingPower);
1792
+ }
1793
+ if (pnlData.marginBalance !== undefined) {
1794
+ stats.margin = parseFloat(pnlData.marginBalance);
1795
+ }
1796
+ if (pnlData.netLiquidation !== undefined) {
1797
+ stats.netLiquidation = parseFloat(pnlData.netLiquidation);
1798
+ } else if (stats.balance !== null) {
1799
+ stats.netLiquidation = stats.balance + (stats.openPnl || 0);
1800
+ }
1801
+
1615
1802
  stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
1803
+ ui.render(stats);
1804
+ });
1805
+
1806
+ // Position-level updates (for Open P&L)
1807
+ service.on('positionUpdate', (pos) => {
1808
+ if (!pos || pos.accountId !== rithmicAccountId) return;
1809
+
1810
+ const posSymbol = pos.symbol;
1811
+ for (const symbolName of Object.keys(stats.symbolStats)) {
1812
+ const baseSymbol = symbolName.replace(/[A-Z]\d+$/, '');
1813
+ if (posSymbol === baseSymbol || posSymbol === symbolName || symbolName.startsWith(posSymbol)) {
1814
+ stats.symbolStats[symbolName].position = pos.quantity || 0;
1815
+
1816
+ // Update Open P&L from position
1817
+ if (pos.openPnl !== undefined && pos.openPnl !== null) {
1818
+ stats.openPnl = pos.openPnl;
1819
+ stats.pnl = (stats.openPnl || 0) + (stats.closedPnl || 0);
1820
+
1821
+ if (stats.balance !== null) {
1822
+ stats.netLiquidation = stats.balance + stats.openPnl;
1823
+ }
1824
+ }
1825
+ break;
1826
+ }
1827
+ }
1828
+ ui.render(stats);
1616
1829
  });
1617
1830
  }
1618
1831