hedgequantx 1.2.129 → 1.2.131

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/pages/algo.js +489 -116
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.2.129",
3
+ "version": "1.2.131",
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
+ // HQX Server connection state (declared here so displayUI can access it)
1511
+ const hqxServer = new HQXServerService();
1512
+ let hqxConnected = false;
1513
+ let latency = 0;
1514
+
1515
+ const bufferLine = (text) => {
1516
+ screenBuffer += text + '\x1B[K\n';
1517
+ };
1518
+
1498
1519
  const displayUI = () => {
1499
- console.clear();
1500
-
1501
- // Logo
1502
- const logo = [
1503
- '██╗ ██╗ ██████╗ ██╗ ██╗',
1504
- '██║ ██║██╔═══██╗╚██╗██╔╝',
1505
- '███████║██║ ██║ ╚███╔╝ ',
1506
- '██╔══██║██║▄▄ ██║ ██╔██╗ ',
1507
- '██║ ██║╚██████╔╝██╔╝ ██╗',
1508
- '╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝'
1509
- ];
1520
+ // Prevent concurrent draws
1521
+ if (isDrawing) return;
1522
+ isDrawing = true;
1510
1523
 
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
1524
+ // Reset buffer
1525
+ screenBuffer = '';
1526
+
1527
+ if (firstDraw) {
1528
+ screenBuffer += '\x1B[?1049h'; // Enter alternate screen
1529
+ screenBuffer += '\x1B[?25l'; // Hide cursor
1530
+ screenBuffer += '\x1B[2J'; // Clear screen
1531
+ firstDraw = false;
1532
+ }
1533
+
1534
+ // Move cursor to home position
1535
+ screenBuffer += '\x1B[H';
1536
+
1537
+ // Stats
1519
1538
  const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
1520
1539
  const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
1521
1540
 
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
1541
+ // Current date
1542
+ const now = new Date();
1543
+ const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
1544
+
1545
+ // Get package version
1546
+ const version = require('../../package.json').version;
1547
+
1548
+ // Fixed width = 96 inner chars
1549
+ const W = 96;
1550
+ const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
1551
+ const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
1552
+ const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
1553
+ const V = '\u2551';
1554
+
1555
+ // Center text helper
1556
+ const center = (text, width) => {
1557
+ const pad = Math.floor((width - text.length) / 2);
1558
+ return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
1559
+ };
1560
+
1561
+ // Safe padding function
1562
+ const safePad = (len) => ' '.repeat(Math.max(0, len));
1563
+
1564
+ // Build cell helper
1565
+ const buildCell = (label, value, valueColor, width) => {
1566
+ const text = ` ${label}: ${valueColor(value)}`;
1567
+ const plain = ` ${label}: ${value}`;
1568
+ return { text, plain, padded: text + safePad(width - plain.length) };
1569
+ };
1570
+
1571
+ bufferLine('');
1572
+ bufferLine(chalk.cyan(TOP));
1573
+ // Logo HEDGEQUANTX
1574
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
1575
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
1576
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
1577
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
1578
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
1579
+ bufferLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
1580
+ bufferLine(chalk.cyan(MID));
1581
+
1582
+ // Centered title
1583
+ const title1 = `Copy Trading System v${version}`;
1584
+ bufferLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
1585
+ bufferLine(chalk.cyan(MID));
1586
+
1587
+ // Grid layout - 2 columns
1588
+ const VS = '\u2502'; // Vertical separator (thin)
1589
+ const colL = 48, colR = 47;
1590
+
1591
+ // Row 1: Lead Account | Lead Symbol
1592
+ const leadName = (lead.account.accountName || '').substring(0, 30);
1593
+ const leadSym = lead.symbol.value || lead.symbol.name || '';
1594
+ const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
1595
+ const r1c2text = ` Symbol: ${chalk.yellow(leadSym)} Qty: ${chalk.cyan(lead.contracts)}`;
1596
+ const r1c2plain = ` Symbol: ${leadSym} Qty: ${lead.contracts}`;
1597
+ const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
1598
+
1599
+ // Row 2: Follower Account | Follower Symbol
1600
+ const followerName = (follower.account.accountName || '').substring(0, 30);
1601
+ const followerSym = follower.symbol.value || follower.symbol.name || '';
1602
+ const r2c1 = buildCell('Follower', followerName, chalk.magenta, colL);
1603
+ const r2c2text = ` Symbol: ${chalk.yellow(followerSym)} Qty: ${chalk.cyan(follower.contracts)}`;
1604
+ const r2c2plain = ` Symbol: ${followerSym} Qty: ${follower.contracts}`;
1605
+ const r2c2 = r2c2text + safePad(colR - r2c2plain.length);
1606
+
1607
+ // Row 3: Target | Risk
1608
+ const r3c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
1609
+ const r3c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
1610
+
1611
+ // Row 4: P&L | Server Status
1612
+ const r4c1 = buildCell('P&L', pnlStr, pnlColor, colL);
1613
+ const serverStr = hqxConnected ? 'HQX ON' : 'MONITOR';
1614
+ const serverColor = hqxConnected ? chalk.green : chalk.yellow;
1615
+ const r4c2 = buildCell('Server', serverStr, serverColor, colR);
1616
+
1617
+ // Row 5: Signals + Lead Trades | Copied + Errors
1618
+ const r5c1text = ` Signals: ${chalk.magenta(stats.signals || 0)} Lead: ${chalk.cyan(stats.leadTrades)}`;
1619
+ const r5c1plain = ` Signals: ${stats.signals || 0} Lead: ${stats.leadTrades}`;
1620
+ const r5c1 = r5c1text + safePad(colL - r5c1plain.length);
1621
+ const r5c2text = ` Copied: ${chalk.green(stats.copiedTrades)} Errors: ${chalk.red(stats.errors)}`;
1622
+ const r5c2plain = ` Copied: ${stats.copiedTrades} Errors: ${stats.errors}`;
1623
+ const r5c2 = r5c2text + safePad(colR - r5c2plain.length);
1624
+
1625
+ // Grid separators
1626
+ const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
1627
+ const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
1628
+ const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
1629
+
1630
+ // Print grid
1631
+ bufferLine(chalk.cyan(GRID_TOP));
1632
+ bufferLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
1633
+ bufferLine(chalk.cyan(GRID_MID));
1634
+ bufferLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2 + chalk.cyan(V));
1635
+ bufferLine(chalk.cyan(GRID_MID));
1636
+ bufferLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
1637
+ bufferLine(chalk.cyan(GRID_MID));
1638
+ bufferLine(chalk.cyan(V) + r4c1.padded + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
1639
+ bufferLine(chalk.cyan(GRID_MID));
1640
+ bufferLine(chalk.cyan(V) + r5c1 + chalk.cyan(VS) + r5c2 + chalk.cyan(V));
1641
+ bufferLine(chalk.cyan(GRID_BOT));
1642
+
1643
+ // Activity log header with spinner and centered date
1644
+ spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
1645
+ const spinnerChar = spinnerChars[spinnerFrame];
1646
+ const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
1647
+ const actLeftPlain = ` Activity Log ${spinnerChar}`;
1648
+ const actRight = 'Press X to stop ';
1649
+ const dateCentered = `- ${dateStr} -`;
1650
+ const leftLen = actLeftPlain.length;
1651
+ const rightLen = actRight.length;
1652
+ const midSpace = Math.max(0, W - leftLen - rightLen);
1653
+ const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
1654
+ const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
1655
+ const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
1656
+ bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
1657
+ bufferLine(chalk.cyan(MID));
1658
+
1659
+ // Helper to strip ANSI codes for length calculation
1660
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
1661
+
1662
+ // Helper to truncate and pad text to exact width W
1663
+ const fitToWidth = (text, width) => {
1664
+ const plainText = stripAnsi(text);
1665
+ if (plainText.length > width) {
1666
+ let count = 0;
1667
+ let cutIndex = 0;
1668
+ for (let i = 0; i < text.length && count < width - 3; i++) {
1669
+ if (text[i] === '\x1B') {
1670
+ while (i < text.length && text[i] !== 'm') i++;
1671
+ } else {
1672
+ count++;
1673
+ cutIndex = i + 1;
1674
+ }
1675
+ }
1676
+ return text.substring(0, cutIndex) + '...';
1677
+ }
1678
+ return text + ' '.repeat(width - plainText.length);
1679
+ };
1680
+
1681
+ // Logs inside the rectangle - newest first, max 30 lines
1682
+ const MAX_VISIBLE_LOGS = 30;
1683
+
1533
1684
  if (logs.length === 0) {
1534
- console.log(chalk.cyan(' ║') + chalk.gray(' Monitoring lead account for trades...'.padEnd(68)) + chalk.cyan('║'));
1685
+ const emptyLine = ' Waiting for activity...';
1686
+ bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
1687
+ for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
1688
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1689
+ }
1535
1690
  } else {
1536
- logs.forEach(log => {
1691
+ const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
1692
+ reversedLogs.forEach(log => {
1537
1693
  const color = typeColors[log.type] || chalk.white;
1538
1694
  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('║'));
1695
+ const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
1696
+ const fitted = fitToWidth(logContent, W);
1697
+ bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
1542
1698
  });
1699
+ for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
1700
+ bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1701
+ }
1543
1702
  }
1544
-
1545
- console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════════════╝'));
1703
+
1704
+ // Bottom border
1705
+ bufferLine(chalk.cyan(BOT));
1706
+
1707
+ // Write entire buffer atomically
1708
+ process.stdout.write(screenBuffer);
1709
+ isDrawing = false;
1546
1710
  };
1711
+
1712
+ // Spinner interval for animation
1713
+ const spinnerInterval = setInterval(() => {
1714
+ if (isRunning) displayUI();
1715
+ }, 250);
1547
1716
 
1548
1717
  addLog('info', 'Copy trading initialized');
1549
- addLog('info', `Monitoring ${lead.account.accountName} for position changes`);
1718
+ addLog('info', 'Connecting to HQX Server...');
1719
+ displayUI();
1720
+
1721
+ // Authenticate with HQX Server
1550
1722
  displayUI();
1551
1723
 
1552
- // Position monitoring loop
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.'));