hedgequantx 1.2.128 → 1.2.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": "1.2.128",
3
+ "version": "1.2.130",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/pages/algo.js CHANGED
@@ -1465,6 +1465,7 @@ const launchCopyTrading = async (config) => {
1465
1465
  copiedTrades: 0,
1466
1466
  leadTrades: 0,
1467
1467
  followerTrades: 0,
1468
+ signals: 0,
1468
1469
  errors: 0,
1469
1470
  pnl: 0
1470
1471
  };
@@ -1475,14 +1476,18 @@ const launchCopyTrading = async (config) => {
1475
1476
  success: chalk.green,
1476
1477
  trade: chalk.green.bold,
1477
1478
  copy: chalk.yellow.bold,
1479
+ signal: chalk.magenta.bold,
1480
+ loss: chalk.red.bold,
1478
1481
  error: chalk.red,
1479
1482
  warning: chalk.yellow
1480
1483
  };
1481
1484
 
1482
1485
  const getIcon = (type) => {
1483
1486
  switch(type) {
1487
+ case 'signal': return '[~]';
1484
1488
  case 'trade': return '[>]';
1485
1489
  case 'copy': return '[+]';
1490
+ case 'loss': return '[-]';
1486
1491
  case 'error': return '[X]';
1487
1492
  case 'success': return '[OK]';
1488
1493
  default: return '[.]';
@@ -1495,171 +1500,513 @@ const launchCopyTrading = async (config) => {
1495
1500
  if (logs.length > MAX_LOGS) logs.shift();
1496
1501
  };
1497
1502
 
1503
+ // Build entire screen as a single string buffer to write atomically
1504
+ let screenBuffer = '';
1505
+ let firstDraw = true;
1506
+ let isDrawing = false;
1507
+ let spinnerFrame = 0;
1508
+ const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
1509
+
1510
+ const bufferLine = (text) => {
1511
+ screenBuffer += text + '\x1B[K\n';
1512
+ };
1513
+
1498
1514
  const displayUI = () => {
1499
- console.clear();
1500
-
1501
- // Logo
1502
- const logo = [
1503
- '██╗ ██╗ ██████╗ ██╗ ██╗',
1504
- '██║ ██║██╔═══██╗╚██╗██╔╝',
1505
- '███████║██║ ██║ ╚███╔╝ ',
1506
- '██╔══██║██║▄▄ ██║ ██╔██╗ ',
1507
- '██║ ██║╚██████╔╝██╔╝ ██╗',
1508
- '╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝'
1509
- ];
1515
+ // Prevent concurrent draws
1516
+ if (isDrawing) return;
1517
+ isDrawing = true;
1510
1518
 
1511
- console.log();
1512
- logo.forEach(line => {
1513
- console.log(chalk.cyan(' ' + line));
1514
- });
1515
- console.log(chalk.gray(' Copy Trading System'));
1516
- console.log();
1517
-
1518
- // Info Box
1519
+ // Reset buffer
1520
+ screenBuffer = '';
1521
+
1522
+ if (firstDraw) {
1523
+ screenBuffer += '\x1B[?1049h'; // Enter alternate screen
1524
+ screenBuffer += '\x1B[?25l'; // Hide cursor
1525
+ screenBuffer += '\x1B[2J'; // Clear screen
1526
+ firstDraw = false;
1527
+ }
1528
+
1529
+ // Move cursor to home position
1530
+ screenBuffer += '\x1B[H';
1531
+
1532
+ // Stats
1519
1533
  const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
1520
1534
  const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
1521
1535
 
1522
- console.log(chalk.cyan(' ╔════════════════════════════════════════════════════════════════════╗'));
1523
- console.log(chalk.cyan(' ║') + chalk.white(` LEAD: ${chalk.cyan((lead.account.accountName || '').substring(0, 20).padEnd(20))} ${chalk.yellow((lead.symbol.value || '').padEnd(10))} x${lead.contracts}`) + chalk.cyan(' ║'));
1524
- console.log(chalk.cyan('') + chalk.white(` FOLLOWER: ${chalk.cyan((follower.account.accountName || '').substring(0, 20).padEnd(20))} ${chalk.yellow((follower.symbol.value || '').padEnd(10))} x${follower.contracts}`) + chalk.cyan(' ║'));
1525
- console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════════════╣'));
1526
- console.log(chalk.cyan(' ║') + chalk.white(` Target: ${chalk.green(('$' + dailyTarget.toFixed(2)).padEnd(10))} Risk: ${chalk.red(('$' + maxRisk.toFixed(2)).padEnd(10))} P&L: ${pnlColor(pnlStr.padEnd(12))}`) + chalk.cyan(' ║'));
1527
- console.log(chalk.cyan(' ║') + chalk.white(` Lead Trades: ${chalk.cyan(stats.leadTrades.toString().padEnd(4))} Copied: ${chalk.green(stats.copiedTrades.toString().padEnd(4))} Errors: ${chalk.red(stats.errors.toString().padEnd(4))}`) + chalk.cyan(' ║'));
1528
- console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════════════╣'));
1529
- console.log(chalk.cyan(' ║') + chalk.white(' Activity Log ') + chalk.yellow('Press X to stop') + chalk.cyan(' ║'));
1530
- console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════════════╣'));
1531
-
1532
- // Logs
1536
+ // Current date
1537
+ const now = new Date();
1538
+ const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
1539
+
1540
+ // Get package version
1541
+ const version = require('../../package.json').version;
1542
+
1543
+ // Fixed width = 96 inner chars
1544
+ const W = 96;
1545
+ const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
1546
+ const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
1547
+ const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
1548
+ const V = '\u2551';
1549
+
1550
+ // Center text helper
1551
+ const center = (text, width) => {
1552
+ const pad = Math.floor((width - text.length) / 2);
1553
+ return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
1554
+ };
1555
+
1556
+ // Safe padding function
1557
+ const safePad = (len) => ' '.repeat(Math.max(0, len));
1558
+
1559
+ // Build cell helper
1560
+ const buildCell = (label, value, valueColor, width) => {
1561
+ const text = ` ${label}: ${valueColor(value)}`;
1562
+ const plain = ` ${label}: ${value}`;
1563
+ return { text, plain, padded: text + safePad(width - plain.length) };
1564
+ };
1565
+
1566
+ bufferLine('');
1567
+ bufferLine(chalk.cyan(TOP));
1568
+ // Logo HEDGEQUANTX
1569
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
1570
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
1571
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
1572
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
1573
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
1574
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
1575
+ bufferLine(chalk.cyan(MID));
1576
+
1577
+ // Centered title
1578
+ const title1 = `Copy Trading System v${version}`;
1579
+ bufferLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
1580
+ bufferLine(chalk.cyan(MID));
1581
+
1582
+ // Grid layout - 2 columns
1583
+ const VS = '\u2502'; // Vertical separator (thin)
1584
+ const colL = 48, colR = 47;
1585
+
1586
+ // Row 1: Lead Account | Lead Symbol
1587
+ const leadName = (lead.account.accountName || '').substring(0, 30);
1588
+ const leadSym = lead.symbol.value || lead.symbol.name || '';
1589
+ const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
1590
+ const r1c2text = ` Symbol: ${chalk.yellow(leadSym)} Qty: ${chalk.cyan(lead.contracts)}`;
1591
+ const r1c2plain = ` Symbol: ${leadSym} Qty: ${lead.contracts}`;
1592
+ const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
1593
+
1594
+ // Row 2: Follower Account | Follower Symbol
1595
+ const followerName = (follower.account.accountName || '').substring(0, 30);
1596
+ const followerSym = follower.symbol.value || follower.symbol.name || '';
1597
+ const r2c1 = buildCell('Follower', followerName, chalk.magenta, colL);
1598
+ const r2c2text = ` Symbol: ${chalk.yellow(followerSym)} Qty: ${chalk.cyan(follower.contracts)}`;
1599
+ const r2c2plain = ` Symbol: ${followerSym} Qty: ${follower.contracts}`;
1600
+ const r2c2 = r2c2text + safePad(colR - r2c2plain.length);
1601
+
1602
+ // Row 3: Target | Risk
1603
+ const r3c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
1604
+ const r3c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
1605
+
1606
+ // Row 4: P&L | Server Status
1607
+ const r4c1 = buildCell('P&L', pnlStr, pnlColor, colL);
1608
+ const serverStr = hqxConnected ? 'HQX ON' : 'MONITOR';
1609
+ const serverColor = hqxConnected ? chalk.green : chalk.yellow;
1610
+ const r4c2 = buildCell('Server', serverStr, serverColor, colR);
1611
+
1612
+ // Row 5: Signals + Lead Trades | Copied + Errors
1613
+ const r5c1text = ` Signals: ${chalk.magenta(stats.signals || 0)} Lead: ${chalk.cyan(stats.leadTrades)}`;
1614
+ const r5c1plain = ` Signals: ${stats.signals || 0} Lead: ${stats.leadTrades}`;
1615
+ const r5c1 = r5c1text + safePad(colL - r5c1plain.length);
1616
+ const r5c2text = ` Copied: ${chalk.green(stats.copiedTrades)} Errors: ${chalk.red(stats.errors)}`;
1617
+ const r5c2plain = ` Copied: ${stats.copiedTrades} Errors: ${stats.errors}`;
1618
+ const r5c2 = r5c2text + safePad(colR - r5c2plain.length);
1619
+
1620
+ // Grid separators
1621
+ const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
1622
+ const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
1623
+ const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
1624
+
1625
+ // Print grid
1626
+ bufferLine(chalk.cyan(GRID_TOP));
1627
+ bufferLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
1628
+ bufferLine(chalk.cyan(GRID_MID));
1629
+ bufferLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2 + chalk.cyan(V));
1630
+ bufferLine(chalk.cyan(GRID_MID));
1631
+ bufferLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
1632
+ bufferLine(chalk.cyan(GRID_MID));
1633
+ bufferLine(chalk.cyan(V) + r4c1.padded + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
1634
+ bufferLine(chalk.cyan(GRID_MID));
1635
+ bufferLine(chalk.cyan(V) + r5c1 + chalk.cyan(VS) + r5c2 + chalk.cyan(V));
1636
+ bufferLine(chalk.cyan(GRID_BOT));
1637
+
1638
+ // Activity log header with spinner and centered date
1639
+ spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
1640
+ const spinnerChar = spinnerChars[spinnerFrame];
1641
+ const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
1642
+ const actLeftPlain = ` Activity Log ${spinnerChar}`;
1643
+ const actRight = 'Press X to stop ';
1644
+ const dateCentered = `- ${dateStr} -`;
1645
+ const leftLen = actLeftPlain.length;
1646
+ const rightLen = actRight.length;
1647
+ const midSpace = Math.max(0, W - leftLen - rightLen);
1648
+ const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
1649
+ const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
1650
+ const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
1651
+ bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
1652
+ bufferLine(chalk.cyan(MID));
1653
+
1654
+ // Helper to strip ANSI codes for length calculation
1655
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
1656
+
1657
+ // Helper to truncate and pad text to exact width W
1658
+ const fitToWidth = (text, width) => {
1659
+ const plainText = stripAnsi(text);
1660
+ if (plainText.length > width) {
1661
+ let count = 0;
1662
+ let cutIndex = 0;
1663
+ for (let i = 0; i < text.length && count < width - 3; i++) {
1664
+ if (text[i] === '\x1B') {
1665
+ while (i < text.length && text[i] !== 'm') i++;
1666
+ } else {
1667
+ count++;
1668
+ cutIndex = i + 1;
1669
+ }
1670
+ }
1671
+ return text.substring(0, cutIndex) + '...';
1672
+ }
1673
+ return text + ' '.repeat(width - plainText.length);
1674
+ };
1675
+
1676
+ // Logs inside the rectangle - newest first, max 30 lines
1677
+ const MAX_VISIBLE_LOGS = 30;
1678
+
1533
1679
  if (logs.length === 0) {
1534
- console.log(chalk.cyan(' ║') + chalk.gray(' Monitoring lead account for trades...'.padEnd(68)) + chalk.cyan('║'));
1680
+ const emptyLine = ' Waiting for activity...';
1681
+ bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
1682
+ for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
1683
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1684
+ }
1535
1685
  } else {
1536
- logs.forEach(log => {
1686
+ const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
1687
+ reversedLogs.forEach(log => {
1537
1688
  const color = typeColors[log.type] || chalk.white;
1538
1689
  const icon = getIcon(log.type);
1539
- const logLine = `[${log.timestamp}] ${icon} ${log.message}`;
1540
- const truncated = logLine.length > 66 ? logLine.substring(0, 63) + '...' : logLine;
1541
- console.log(chalk.cyan(' ║') + ' ' + color(truncated.padEnd(67)) + chalk.cyan('║'));
1690
+ const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
1691
+ const fitted = fitToWidth(logContent, W);
1692
+ bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
1542
1693
  });
1694
+ for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
1695
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1696
+ }
1543
1697
  }
1544
-
1545
- console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════════════╝'));
1698
+
1699
+ // Bottom border
1700
+ bufferLine(chalk.cyan(BOT));
1701
+
1702
+ // Write entire buffer atomically
1703
+ process.stdout.write(screenBuffer);
1704
+ isDrawing = false;
1546
1705
  };
1706
+
1707
+ // Spinner interval for animation
1708
+ const spinnerInterval = setInterval(() => {
1709
+ if (isRunning) displayUI();
1710
+ }, 250);
1547
1711
 
1548
1712
  addLog('info', 'Copy trading initialized');
1549
- addLog('info', `Monitoring ${lead.account.accountName} for position changes`);
1550
1713
  displayUI();
1551
1714
 
1552
- // Position monitoring loop
1715
+ // Connect to HQX Server for Ultra-Scalping signals
1716
+ const hqxServer = new HQXServerService();
1717
+ let hqxConnected = false;
1718
+ let latency = 0;
1719
+
1720
+ // Authenticate with HQX Server
1721
+ addLog('info', 'Connecting to HQX Server...');
1722
+ displayUI();
1723
+
1724
+ try {
1725
+ const authResult = await hqxServer.authenticate(
1726
+ lead.account.accountId.toString(),
1727
+ lead.account.propfirm || 'projectx'
1728
+ );
1729
+
1730
+ if (authResult.success) {
1731
+ const connectResult = await hqxServer.connect();
1732
+ if (connectResult.success) {
1733
+ hqxConnected = true;
1734
+ addLog('success', 'Connected to HQX Server');
1735
+ } else {
1736
+ addLog('warning', 'HQX Server unavailable - Running in monitor mode');
1737
+ }
1738
+ } else {
1739
+ addLog('warning', 'HQX Auth failed - Running in monitor mode');
1740
+ }
1741
+ } catch (error) {
1742
+ addLog('warning', 'HQX Server unavailable - Running in monitor mode');
1743
+ }
1744
+
1745
+ displayUI();
1746
+
1747
+ // Helper function to execute signal on both accounts
1748
+ const executeSignalOnBothAccounts = async (signal) => {
1749
+ const side = signal.side === 'long' ? 0 : 1; // 0=Buy, 1=Sell
1750
+ const sideStr = signal.side === 'long' ? 'LONG' : 'SHORT';
1751
+
1752
+ // Execute on Lead account
1753
+ try {
1754
+ const leadResult = await lead.service.placeOrder({
1755
+ accountId: lead.account.rithmicAccountId || lead.account.accountId,
1756
+ symbol: lead.symbol.value,
1757
+ exchange: 'CME',
1758
+ size: lead.contracts,
1759
+ side: side,
1760
+ type: 2 // Market
1761
+ });
1762
+
1763
+ if (leadResult.success) {
1764
+ stats.leadTrades++;
1765
+ addLog('trade', `Lead: ${sideStr} ${lead.contracts} ${lead.symbol.value} @ MKT`);
1766
+ } else {
1767
+ throw new Error(leadResult.error || 'Lead order failed');
1768
+ }
1769
+ } catch (e) {
1770
+ stats.errors++;
1771
+ addLog('error', `Lead order failed: ${e.message}`);
1772
+ return; // Don't copy if lead fails
1773
+ }
1774
+
1775
+ // Execute on Follower account (copy)
1776
+ try {
1777
+ const followerResult = await follower.service.placeOrder({
1778
+ accountId: follower.account.rithmicAccountId || follower.account.accountId,
1779
+ symbol: follower.symbol.value,
1780
+ exchange: 'CME',
1781
+ size: follower.contracts,
1782
+ side: side,
1783
+ type: 2 // Market
1784
+ });
1785
+
1786
+ if (followerResult.success) {
1787
+ stats.copiedTrades++;
1788
+ addLog('copy', `Follower: ${sideStr} ${follower.contracts} ${follower.symbol.value} @ MKT`);
1789
+ } else {
1790
+ throw new Error(followerResult.error || 'Follower order failed');
1791
+ }
1792
+ } catch (e) {
1793
+ stats.errors++;
1794
+ addLog('error', `Follower order failed: ${e.message}`);
1795
+ }
1796
+ };
1797
+
1798
+ // Helper function to close positions on both accounts
1799
+ const closePositionsOnBothAccounts = async (reason) => {
1800
+ // Close Lead position
1801
+ try {
1802
+ await lead.service.closePosition(
1803
+ lead.account.rithmicAccountId || lead.account.accountId,
1804
+ lead.symbol.value
1805
+ );
1806
+ addLog('trade', `Lead: Position closed (${reason})`);
1807
+ } catch (e) {
1808
+ // Position may already be closed
1809
+ }
1810
+
1811
+ // Close Follower position
1812
+ try {
1813
+ await follower.service.closePosition(
1814
+ follower.account.rithmicAccountId || follower.account.accountId,
1815
+ follower.symbol.value
1816
+ );
1817
+ addLog('copy', `Follower: Position closed (${reason})`);
1818
+ } catch (e) {
1819
+ // Position may already be closed
1820
+ }
1821
+ };
1822
+
1823
+ // Setup HQX Server event handlers
1824
+ if (hqxConnected) {
1825
+ hqxServer.on('latency', (data) => {
1826
+ latency = data.latency || 0;
1827
+ });
1828
+
1829
+ hqxServer.on('log', (data) => {
1830
+ addLog(data.type || 'info', data.message);
1831
+ });
1832
+
1833
+ hqxServer.on('signal', async (data) => {
1834
+ stats.signals = (stats.signals || 0) + 1;
1835
+ const side = data.side === 'long' ? 'BUY' : 'SELL';
1836
+ addLog('signal', `${side} Signal @ ${data.entry?.toFixed(2) || 'N/A'} | SL: ${data.stop?.toFixed(2) || 'N/A'} | TP: ${data.target?.toFixed(2) || 'N/A'}`);
1837
+
1838
+ // Execute on both accounts
1839
+ await executeSignalOnBothAccounts(data);
1840
+ displayUI();
1841
+ });
1842
+
1843
+ hqxServer.on('trade', async (data) => {
1844
+ stats.pnl += data.pnl || 0;
1845
+ if (data.pnl > 0) {
1846
+ addLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
1847
+ } else {
1848
+ addLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
1849
+ }
1850
+
1851
+ // Check daily target
1852
+ if (stats.pnl >= dailyTarget) {
1853
+ stopReason = 'target';
1854
+ addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
1855
+ isRunning = false;
1856
+ hqxServer.stopAlgo();
1857
+ await closePositionsOnBothAccounts('target');
1858
+ }
1859
+
1860
+ // Check max risk
1861
+ if (stats.pnl <= -maxRisk) {
1862
+ stopReason = 'risk';
1863
+ addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
1864
+ isRunning = false;
1865
+ hqxServer.stopAlgo();
1866
+ await closePositionsOnBothAccounts('risk');
1867
+ }
1868
+
1869
+ displayUI();
1870
+ });
1871
+
1872
+ hqxServer.on('stats', (data) => {
1873
+ const realizedPnl = data.pnl || 0;
1874
+ const unrealizedPnl = data.position?.pnl || 0;
1875
+ stats.pnl = realizedPnl + unrealizedPnl;
1876
+ });
1877
+
1878
+ hqxServer.on('error', (data) => {
1879
+ addLog('error', data.message || 'Unknown error');
1880
+ });
1881
+
1882
+ hqxServer.on('disconnected', () => {
1883
+ hqxConnected = false;
1884
+ if (!stopReason) {
1885
+ addLog('error', 'HQX Server disconnected');
1886
+ }
1887
+ });
1888
+
1889
+ // Start the Ultra-Scalping algo
1890
+ addLog('info', 'Starting HQX Ultra-Scalping...');
1891
+ addLog('info', `Target: $${dailyTarget.toFixed(2)} | Risk: $${maxRisk.toFixed(2)}`);
1892
+
1893
+ const propfirmToken = lead.service.getToken ? lead.service.getToken() : null;
1894
+ const propfirmId = lead.service.getPropfirm ? lead.service.getPropfirm() : (lead.account.propfirm || 'topstep');
1895
+
1896
+ hqxServer.startAlgo({
1897
+ accountId: lead.account.accountId,
1898
+ contractId: lead.symbol.id || lead.symbol.contractId,
1899
+ symbol: lead.symbol.value,
1900
+ contracts: lead.contracts,
1901
+ dailyTarget: dailyTarget,
1902
+ maxRisk: maxRisk,
1903
+ propfirm: propfirmId,
1904
+ propfirmToken: propfirmToken,
1905
+ copyTrading: true, // Flag for copy trading mode
1906
+ followerSymbol: follower.symbol.value,
1907
+ followerContracts: follower.contracts
1908
+ });
1909
+
1910
+ displayUI();
1911
+ }
1912
+
1913
+ // Position monitoring loop (for P&L tracking and fallback copy)
1553
1914
  const monitorInterval = setInterval(async () => {
1554
1915
  if (!isRunning) return;
1555
1916
 
1556
1917
  try {
1557
- // Get follower positions for P&L tracking
1558
- const followerPositions = await follower.service.getPositions(follower.account.rithmicAccountId || follower.account.accountId);
1559
-
1918
+ // Get positions from both accounts for P&L tracking
1919
+ const [leadPositions, followerPositions] = await Promise.all([
1920
+ lead.service.getPositions(lead.account.rithmicAccountId || lead.account.accountId),
1921
+ follower.service.getPositions(follower.account.rithmicAccountId || follower.account.accountId)
1922
+ ]);
1923
+
1924
+ // Calculate combined P&L
1925
+ let leadPnl = 0, followerPnl = 0;
1926
+
1927
+ if (leadPositions.success && leadPositions.positions) {
1928
+ const leadPos = leadPositions.positions.find(p =>
1929
+ p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
1930
+ );
1931
+ if (leadPos && typeof leadPos.unrealizedPnl === 'number') {
1932
+ leadPnl = leadPos.unrealizedPnl;
1933
+ }
1934
+ }
1935
+
1560
1936
  if (followerPositions.success && followerPositions.positions) {
1561
1937
  const followerPos = followerPositions.positions.find(p =>
1562
- p.symbol === follower.symbol.value ||
1563
- p.symbol?.includes(follower.symbol.searchText)
1938
+ p.symbol === follower.symbol.value || p.symbol?.includes(follower.symbol.searchText)
1564
1939
  );
1565
-
1566
- // Update P&L from follower position
1567
1940
  if (followerPos && typeof followerPos.unrealizedPnl === 'number') {
1568
- stats.pnl = followerPos.unrealizedPnl;
1941
+ followerPnl = followerPos.unrealizedPnl;
1569
1942
  }
1570
1943
  }
1571
1944
 
1945
+ // Update combined P&L (or just follower if HQX handles lead)
1946
+ stats.pnl = leadPnl + followerPnl;
1947
+
1572
1948
  // Check if daily target reached
1573
- if (stats.pnl >= dailyTarget) {
1949
+ if (stats.pnl >= dailyTarget && !stopReason) {
1574
1950
  isRunning = false;
1575
1951
  stopReason = 'target';
1576
1952
  addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
1577
1953
 
1578
- // Close follower position
1579
- try {
1580
- await follower.service.closePosition(
1581
- follower.account.rithmicAccountId || follower.account.accountId,
1582
- follower.symbol.value
1583
- );
1584
- addLog('info', 'Follower position closed');
1585
- } catch (e) {
1586
- // Position may already be closed
1587
- }
1588
-
1954
+ if (hqxConnected) hqxServer.stopAlgo();
1955
+ await closePositionsOnBothAccounts('target');
1589
1956
  displayUI();
1590
1957
  return;
1591
1958
  }
1592
1959
 
1593
1960
  // Check if max risk reached
1594
- if (stats.pnl <= -maxRisk) {
1961
+ if (stats.pnl <= -maxRisk && !stopReason) {
1595
1962
  isRunning = false;
1596
1963
  stopReason = 'risk';
1597
1964
  addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
1598
1965
 
1599
- // Close follower position
1600
- try {
1601
- await follower.service.closePosition(
1602
- follower.account.rithmicAccountId || follower.account.accountId,
1603
- follower.symbol.value
1604
- );
1605
- addLog('info', 'Follower position closed');
1606
- } catch (e) {
1607
- // Position may already be closed
1608
- }
1609
-
1966
+ if (hqxConnected) hqxServer.stopAlgo();
1967
+ await closePositionsOnBothAccounts('risk');
1610
1968
  displayUI();
1611
1969
  return;
1612
1970
  }
1613
1971
 
1614
- // Get lead positions
1615
- const leadPositions = await lead.service.getPositions(lead.account.rithmicAccountId || lead.account.accountId);
1616
-
1617
- let currentLeadPosition = null;
1618
- if (leadPositions.success && leadPositions.positions) {
1619
- currentLeadPosition = leadPositions.positions.find(p =>
1620
- p.symbol === lead.symbol.value ||
1621
- p.symbol?.includes(lead.symbol.searchText)
1622
- );
1623
- }
1624
-
1625
- // Detect position changes
1626
- const hadPosition = lastLeadPosition && lastLeadPosition.quantity !== 0;
1627
- const hasPosition = currentLeadPosition && currentLeadPosition.quantity !== 0;
1628
-
1629
- if (!hadPosition && hasPosition) {
1630
- // New position opened
1631
- stats.leadTrades++;
1632
- const side = currentLeadPosition.quantity > 0 ? 'LONG' : 'SHORT';
1633
- addLog('trade', `Lead opened ${side} ${Math.abs(currentLeadPosition.quantity)} @ ${currentLeadPosition.averagePrice || 'MKT'}`);
1634
-
1635
- // Copy to follower
1636
- await copyTradeToFollower(follower, currentLeadPosition, 'open');
1637
- stats.copiedTrades++;
1638
- displayUI();
1972
+ // Fallback: If HQX not connected, monitor lead and copy manually
1973
+ if (!hqxConnected) {
1974
+ let currentLeadPosition = null;
1975
+ if (leadPositions.success && leadPositions.positions) {
1976
+ currentLeadPosition = leadPositions.positions.find(p =>
1977
+ p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
1978
+ );
1979
+ }
1639
1980
 
1640
- } else if (hadPosition && !hasPosition) {
1641
- // Position closed
1642
- addLog('trade', `Lead closed position`);
1643
-
1644
- // Close follower position
1645
- await copyTradeToFollower(follower, lastLeadPosition, 'close');
1646
- stats.copiedTrades++;
1647
- displayUI();
1981
+ const hadPosition = lastLeadPosition && lastLeadPosition.quantity !== 0;
1982
+ const hasPosition = currentLeadPosition && currentLeadPosition.quantity !== 0;
1983
+
1984
+ if (!hadPosition && hasPosition) {
1985
+ stats.leadTrades++;
1986
+ const side = currentLeadPosition.quantity > 0 ? 'LONG' : 'SHORT';
1987
+ addLog('trade', `Lead opened ${side} ${Math.abs(currentLeadPosition.quantity)} @ ${currentLeadPosition.averagePrice || 'MKT'}`);
1988
+ await copyTradeToFollower(follower, currentLeadPosition, 'open');
1989
+ stats.copiedTrades++;
1990
+ displayUI();
1991
+
1992
+ } else if (hadPosition && !hasPosition) {
1993
+ addLog('trade', `Lead closed position`);
1994
+ await copyTradeToFollower(follower, lastLeadPosition, 'close');
1995
+ stats.copiedTrades++;
1996
+ displayUI();
1997
+
1998
+ } else if (hadPosition && hasPosition && lastLeadPosition.quantity !== currentLeadPosition.quantity) {
1999
+ const diff = currentLeadPosition.quantity - lastLeadPosition.quantity;
2000
+ const action = diff > 0 ? 'added' : 'reduced';
2001
+ addLog('trade', `Lead ${action} ${Math.abs(diff)} contracts`);
2002
+ await copyTradeToFollower(follower, { ...currentLeadPosition, quantityChange: diff }, 'adjust');
2003
+ stats.copiedTrades++;
2004
+ displayUI();
2005
+ }
1648
2006
 
1649
- } else if (hadPosition && hasPosition && lastLeadPosition.quantity !== currentLeadPosition.quantity) {
1650
- // Position size changed
1651
- const diff = currentLeadPosition.quantity - lastLeadPosition.quantity;
1652
- const action = diff > 0 ? 'added' : 'reduced';
1653
- addLog('trade', `Lead ${action} ${Math.abs(diff)} contracts`);
1654
-
1655
- // Adjust follower position
1656
- await copyTradeToFollower(follower, { ...currentLeadPosition, quantityChange: diff }, 'adjust');
1657
- stats.copiedTrades++;
1658
- displayUI();
2007
+ lastLeadPosition = currentLeadPosition ? { ...currentLeadPosition } : null;
1659
2008
  }
1660
2009
 
1661
- lastLeadPosition = currentLeadPosition ? { ...currentLeadPosition } : null;
1662
-
1663
2010
  } catch (error) {
1664
2011
  stats.errors++;
1665
2012
  addLog('error', `Monitor error: ${error.message}`);
@@ -1701,8 +2048,34 @@ const launchCopyTrading = async (config) => {
1701
2048
  });
1702
2049
 
1703
2050
  // Cleanup
2051
+ clearInterval(spinnerInterval);
1704
2052
  isRunning = false;
1705
2053
 
2054
+ // Stop HQX Server and close positions
2055
+ if (hqxConnected) {
2056
+ hqxServer.stopAlgo();
2057
+ hqxServer.disconnect();
2058
+ }
2059
+
2060
+ // Cancel all pending orders and close positions on both accounts
2061
+ try {
2062
+ await Promise.all([
2063
+ lead.service.cancelAllOrders(lead.account.rithmicAccountId || lead.account.accountId),
2064
+ follower.service.cancelAllOrders(follower.account.rithmicAccountId || follower.account.accountId)
2065
+ ]);
2066
+ } catch (e) {
2067
+ // Ignore cancel errors
2068
+ }
2069
+
2070
+ if (!stopReason) {
2071
+ // User stopped manually, close positions
2072
+ await closePositionsOnBothAccounts('user_stop');
2073
+ }
2074
+
2075
+ // Exit alternate screen buffer and show cursor
2076
+ process.stdout.write('\x1B[?1049l');
2077
+ process.stdout.write('\x1B[?25h');
2078
+
1706
2079
  console.log();
1707
2080
  if (stopReason === 'target') {
1708
2081
  console.log(chalk.green.bold(' [OK] Daily target reached! Copy trading stopped.'));
@@ -0,0 +1,34 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
3
+ iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
4
+ cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
5
+ BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
6
+ MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
7
+ BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
8
+ aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
9
+ dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
10
+ AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
11
+ 3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
12
+ tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
13
+ Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
14
+ VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
15
+ 79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
16
+ c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
17
+ Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
18
+ c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
19
+ UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
20
+ Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
21
+ BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
22
+ A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
23
+ Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
24
+ VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
25
+ ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
26
+ 8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
27
+ iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
28
+ Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
29
+ XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
30
+ qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
31
+ VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
32
+ L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
33
+ jjxDah2nGN59PRbxYvnKkKj9
34
+ -----END CERTIFICATE-----
@@ -7,7 +7,18 @@ const RITHMIC_ENDPOINTS = {
7
7
  TEST: 'wss://rituz00100.rithmic.com:443',
8
8
  PAPER: 'wss://ritpa11120.11.rithmic.com:443',
9
9
  LIVE: 'wss://ritpz01000.01.rithmic.com:443',
10
- APEX: 'wss://ritpz01000.01.rithmic.com:443', // Apex uses live server
10
+ // Production gateways for prop firms (discovered via RequestRithmicSystemGatewayInfo)
11
+ CHICAGO: 'wss://rprotocol.rithmic.com:443', // Chicago Area (primary for US)
12
+ EUROPE: 'wss://rprotocol-ie.rithmic.com:443', // Europe (Ireland)
13
+ FRANKFURT: 'wss://rprotocol-de.rithmic.com:443', // Frankfurt
14
+ SINGAPORE: 'wss://rprotocol-sg.rithmic.com:443', // Singapore
15
+ SYDNEY: 'wss://rprotocol-au.rithmic.com:443', // Sydney
16
+ TOKYO: 'wss://rprotocol-jp.rithmic.com:443', // Tokyo
17
+ HONG_KONG: 'wss://rprotocol-hk.rithmic.com:443', // Hong Kong
18
+ SEOUL: 'wss://rprotocol-kr.rithmic.com:443', // Seoul
19
+ MUMBAI: 'wss://rprotocol-in.rithmic.com:443', // Mumbai
20
+ SAO_PAULO: 'wss://rprotocol-br.rithmic.com:443', // Sao Paulo
21
+ CAPE_TOWN: 'wss://rprotocol-za.rithmic.com:443', // Cape Town
11
22
  };
12
23
 
13
24
  // System names for PropFirms
@@ -26,27 +26,28 @@ class RithmicService extends EventEmitter {
26
26
 
27
27
  /**
28
28
  * Get PropFirm configuration
29
+ * Note: Apex and other prop firms use the Chicago gateway (rprotocol.rithmic.com), NOT Paper Trading endpoint
29
30
  */
30
31
  getPropFirmConfig(key) {
31
32
  const propfirms = {
32
- 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000 },
33
- 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000 },
34
- 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, defaultBalance: 150000 },
35
- 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, defaultBalance: 150000 },
36
- 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, defaultBalance: 150000 },
37
- 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, defaultBalance: 150000 },
38
- 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, defaultBalance: 150000 },
39
- 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, defaultBalance: 150000 },
40
- 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, defaultBalance: 150000 },
41
- 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, defaultBalance: 150000 },
42
- '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, defaultBalance: 150000 },
43
- 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, defaultBalance: 150000 },
44
- '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, defaultBalance: 150000 },
45
- 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, defaultBalance: 150000 },
46
- 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, defaultBalance: 150000 },
47
- 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, defaultBalance: 150000 },
33
+ 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
34
+ 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
35
+ 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
36
+ 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
37
+ 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
38
+ 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
39
+ 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
40
+ 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
41
+ 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
42
+ 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
43
+ '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
44
+ 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
45
+ '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
46
+ 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
47
+ 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
48
+ 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
48
49
  };
49
- return propfirms[key] || { name: key, systemName: 'Rithmic Paper Trading', defaultBalance: 150000 };
50
+ return propfirms[key] || { name: key, systemName: 'Rithmic Paper Trading', defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.PAPER };
50
51
  }
51
52
 
52
53
  /**
@@ -57,14 +58,11 @@ class RithmicService extends EventEmitter {
57
58
  // Connect to ORDER_PLANT
58
59
  this.orderConn = new RithmicConnection();
59
60
 
60
- // Determine endpoint based on propfirm
61
- let endpoint = RITHMIC_ENDPOINTS.PAPER;
62
- if (this.propfirmKey === 'apex' || this.propfirmKey === 'apex_rithmic') {
63
- endpoint = RITHMIC_ENDPOINTS.LIVE; // Apex uses live server
64
- }
61
+ // Use propfirm-specific gateway (Chicago for Apex and most prop firms)
62
+ const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
65
63
 
66
64
  const config = {
67
- uri: endpoint,
65
+ uri: gateway,
68
66
  systemName: this.propfirm.systemName,
69
67
  userId: username,
70
68
  password: password,
@@ -145,14 +143,11 @@ class RithmicService extends EventEmitter {
145
143
  try {
146
144
  this.pnlConn = new RithmicConnection();
147
145
 
148
- // Determine endpoint based on propfirm
149
- let endpoint = RITHMIC_ENDPOINTS.PAPER;
150
- if (this.propfirmKey === 'apex' || this.propfirmKey === 'apex_rithmic') {
151
- endpoint = RITHMIC_ENDPOINTS.LIVE; // Apex uses live server
152
- }
146
+ // Use propfirm-specific gateway (Chicago for Apex and most prop firms)
147
+ const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
153
148
 
154
149
  const config = {
155
- uri: endpoint,
150
+ uri: gateway,
156
151
  systemName: this.propfirm.systemName,
157
152
  userId: username,
158
153
  password: password,