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.
- package/package.json +1 -1
- package/src/pages/algo.js +489 -116
package/package.json
CHANGED
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
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const logo = [
|
|
1503
|
-
'██╗ ██╗ ██████╗ ██╗ ██╗',
|
|
1504
|
-
'██║ ██║██╔═══██╗╚██╗██╔╝',
|
|
1505
|
-
'███████║██║ ██║ ╚███╔╝ ',
|
|
1506
|
-
'██╔══██║██║▄▄ ██║ ██╔██╗ ',
|
|
1507
|
-
'██║ ██║╚██████╔╝██╔╝ ██╗',
|
|
1508
|
-
'╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝'
|
|
1509
|
-
];
|
|
1520
|
+
// Prevent concurrent draws
|
|
1521
|
+
if (isDrawing) return;
|
|
1522
|
+
isDrawing = true;
|
|
1510
1523
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1540
|
-
const
|
|
1541
|
-
|
|
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
|
-
|
|
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',
|
|
1718
|
+
addLog('info', 'Connecting to HQX Server...');
|
|
1719
|
+
displayUI();
|
|
1720
|
+
|
|
1721
|
+
// Authenticate with HQX Server
|
|
1550
1722
|
displayUI();
|
|
1551
1723
|
|
|
1552
|
-
|
|
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
|
|
1558
|
-
const followerPositions = await
|
|
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
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
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
|
-
//
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
-
|
|
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.'));
|