hedgequantx 1.2.145 → 1.2.147

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/src/pages/algo.js CHANGED
@@ -1,2282 +1,8 @@
1
1
  /**
2
- * Algo Trading Page
2
+ * Algo Trading Page - Lightweight Router
3
+ * Delegates to modular components
3
4
  */
4
5
 
5
- const chalk = require('chalk');
6
- const ora = require('ora');
7
- const inquirer = require('inquirer');
8
- const readline = require('readline');
9
-
10
- const { connections } = require('../services');
11
- const { HQXServerService } = require('../services/hqx-server');
12
- const { FUTURES_SYMBOLS } = require('../config');
13
- const { getDevice, getSeparator } = require('../ui');
14
-
15
- /**
16
- * Algo Trading Menu
17
- */
18
- const algoTradingMenu = async (service) => {
19
- const device = getDevice();
20
- console.log();
21
- console.log(chalk.gray(getSeparator()));
22
- console.log(chalk.magenta.bold(' Algo-Trading'));
23
- console.log(chalk.gray(getSeparator()));
24
- console.log();
25
-
26
- const { action } = await inquirer.prompt([
27
- {
28
- type: 'list',
29
- name: 'action',
30
- message: chalk.white.bold('Select Mode:'),
31
- choices: [
32
- { name: chalk.cyan('One Account'), value: 'one_account' },
33
- { name: chalk.green('Copy Trading'), value: 'copy_trading' },
34
- new inquirer.Separator(),
35
- { name: chalk.yellow('< Back'), value: 'back' }
36
- ],
37
- pageSize: 10,
38
- loop: false
39
- }
40
- ]);
41
-
42
- switch (action) {
43
- case 'one_account':
44
- await oneAccountMenu(service);
45
- break;
46
- case 'copy_trading':
47
- await copyTradingMenu();
48
- break;
49
- case 'back':
50
- return 'back';
51
- }
52
-
53
- return action;
54
- };
55
-
56
- /**
57
- * One Account Menu - Select active account
58
- */
59
- const oneAccountMenu = async (service) => {
60
- const spinner = ora('Fetching active accounts...').start();
61
-
62
- const result = await service.getTradingAccounts();
63
-
64
- if (!result.success || !result.accounts || result.accounts.length === 0) {
65
- spinner.fail('No accounts found');
66
- console.log(chalk.yellow(' You need at least one trading account.'));
67
- console.log();
68
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
69
- return;
70
- }
71
-
72
- // Filter only active accounts (status === 0)
73
- const activeAccounts = result.accounts.filter(acc => acc.status === 0);
74
-
75
- if (activeAccounts.length === 0) {
76
- spinner.fail('No active accounts found');
77
- console.log(chalk.yellow(' You need at least one active trading account (status: Active).'));
78
- console.log();
79
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
80
- return;
81
- }
82
-
83
- spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
84
- console.log();
85
-
86
- const accountChoices = activeAccounts.map(account => ({
87
- name: chalk.cyan(`${account.accountName || account.name || 'Account #' + account.accountId} - Balance: $${account.balance.toLocaleString()}`),
88
- value: account
89
- }));
90
-
91
- accountChoices.push(new inquirer.Separator());
92
- accountChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
93
-
94
- const { selectedAccount } = await inquirer.prompt([
95
- {
96
- type: 'list',
97
- name: 'selectedAccount',
98
- message: chalk.white.bold('Select Account:'),
99
- choices: accountChoices,
100
- pageSize: 15,
101
- loop: false
102
- }
103
- ]);
104
-
105
- if (selectedAccount === 'back') {
106
- return;
107
- }
108
-
109
- // Check market status
110
- console.log();
111
- const marketSpinner = ora('Checking market status...').start();
112
-
113
- const marketHours = service.checkMarketHours();
114
- const marketStatus = await service.getMarketStatus(selectedAccount.accountId);
115
-
116
- if (!marketHours.isOpen) {
117
- marketSpinner.fail('Market is CLOSED');
118
- console.log();
119
- console.log(chalk.red.bold(' [X] ' + marketHours.message));
120
- console.log();
121
- console.log(chalk.gray(' Futures markets (CME) trading hours:'));
122
- console.log(chalk.gray(' Sunday 5:00 PM CT - Friday 4:00 PM CT'));
123
- console.log(chalk.gray(' Daily maintenance: 4:00 PM - 5:00 PM CT'));
124
- console.log();
125
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
126
- return;
127
- }
128
-
129
- if (marketStatus.success && !marketStatus.isOpen) {
130
- marketSpinner.fail('Cannot trade on this account');
131
- console.log();
132
- console.log(chalk.red.bold(' [X] ' + marketStatus.message));
133
- console.log();
134
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
135
- return;
136
- }
137
-
138
- marketSpinner.succeed('Market is OPEN - Ready to trade!');
139
-
140
- await selectSymbolMenu(service, selectedAccount);
141
- };
142
-
143
- /**
144
- * Symbol Selection Menu
145
- */
146
- const selectSymbolMenu = async (service, account) => {
147
- const device = getDevice();
148
- const accountName = account.accountName || account.name || 'Account #' + account.accountId;
149
- const propfirm = account.propfirm || 'projectx';
150
-
151
- console.log();
152
- console.log(chalk.gray(getSeparator()));
153
- console.log(chalk.cyan.bold(` Account: ${accountName}`));
154
- console.log(chalk.gray(getSeparator()));
155
- console.log();
156
-
157
- // Fetch available symbols from API
158
- const spinner = ora('Loading available symbols...').start();
159
-
160
- let availableSymbols = [];
161
-
162
- // Search for common symbols to get available contracts (including micros)
163
- // Use various search terms to find all contracts
164
- const commonSearches = [
165
- 'NQ', 'ES', 'YM', 'RTY', // E-mini indices
166
- 'Micro', 'MNQ', 'MES', 'MYM', 'M2K', // Micro indices (try multiple search terms)
167
- 'CL', 'MCL', 'QM', // Crude Oil
168
- 'GC', 'MGC', // Gold
169
- 'SI', 'SIL', // Silver
170
- '6E', 'M6E', '6B', '6J', '6A', '6C', // Currencies
171
- 'ZB', 'ZN', 'ZF', 'ZT', // Treasuries
172
- 'NG', 'QG', // Natural Gas
173
- 'HG', 'PL' // Copper, Platinum
174
- ];
175
-
176
- try {
177
- const seenIds = new Set();
178
-
179
- for (const search of commonSearches) {
180
- const result = await service.searchContracts(search, false);
181
- if (result.success && result.contracts && result.contracts.length > 0) {
182
- for (const contract of result.contracts) {
183
- // Skip if already added (by contract ID)
184
- const contractId = contract.id || '';
185
- if (!contractId || seenIds.has(contractId)) continue;
186
- seenIds.add(contractId);
187
-
188
- // Add the raw contract data from API
189
- availableSymbols.push(contract);
190
- }
191
- }
192
- }
193
- } catch (e) {
194
- spinner.fail('Failed to load symbols from API: ' + e.message);
195
- return;
196
- }
197
-
198
- // Only use REAL data from API - no mock/static data
199
- if (availableSymbols.length === 0) {
200
- spinner.fail('No contracts available from API');
201
- console.log(chalk.red(' Please check your connection and try again'));
202
- return;
203
- }
204
-
205
- spinner.succeed(`Found ${availableSymbols.length} available contracts`);
206
-
207
- console.log();
208
-
209
- // Format symbols for display - show ALL contracts from API (REAL DATA ONLY)
210
- const symbolChoices = [];
211
-
212
- for (const contract of availableSymbols) {
213
- // Get symbol code and description directly from API
214
- const symbolCode = contract.name || contract.id || 'Unknown';
215
- const description = contract.description || symbolCode;
216
-
217
- // Format: "NQH6 E-mini NASDAQ-100: March 2026"
218
- symbolChoices.push({
219
- name: chalk.yellow(symbolCode.padEnd(12)) + chalk.white(description),
220
- value: contract
221
- });
222
- }
223
-
224
- // Sort by category: E-mini indices first, then Micro E-mini, then others
225
- const getSymbolPriority = (contract) => {
226
- const name = (contract.name || contract.symbol || '').toUpperCase();
227
- const desc = (contract.description || '').toLowerCase();
228
-
229
- // E-mini indices (NQ, ES, YM, RTY) - highest priority
230
- if (name.match(/^(NQ|ES|YM|RTY)[A-Z]\d/) && !name.startsWith('M')) {
231
- if (name.startsWith('NQ')) return 10;
232
- if (name.startsWith('ES')) return 11;
233
- if (name.startsWith('YM')) return 12;
234
- if (name.startsWith('RTY')) return 13;
235
- return 15;
236
- }
237
-
238
- // Micro E-mini indices (MNQ, MES, MYM, M2K)
239
- if (name.match(/^(MNQ|MES|MYM|M2K)/)) {
240
- if (name.startsWith('MNQ')) return 20;
241
- if (name.startsWith('MES')) return 21;
242
- if (name.startsWith('MYM')) return 22;
243
- if (name.startsWith('M2K')) return 23;
244
- return 25;
245
- }
246
-
247
- // Energy (CL, MCL, NG)
248
- if (name.match(/^(CL|MCL|NG|QG)/)) return 30;
249
-
250
- // Metals (GC, MGC, SI)
251
- if (name.match(/^(GC|MGC|SI|HG|PL)/)) return 40;
252
-
253
- // Currencies (6E, 6B, etc)
254
- if (name.match(/^(6E|6B|6J|6A|6C|M6E)/)) return 50;
255
-
256
- // Treasuries (ZB, ZN, ZF, ZT)
257
- if (name.match(/^(ZB|ZN|ZF|ZT)/)) return 60;
258
-
259
- // Everything else
260
- return 100;
261
- };
262
-
263
- symbolChoices.sort((a, b) => {
264
- const priorityA = getSymbolPriority(a.value);
265
- const priorityB = getSymbolPriority(b.value);
266
-
267
- if (priorityA !== priorityB) {
268
- return priorityA - priorityB;
269
- }
270
-
271
- // Same priority - sort alphabetically
272
- const aCode = a.value.name || a.value.symbol || '';
273
- const bCode = b.value.name || b.value.symbol || '';
274
- return aCode.localeCompare(bCode);
275
- });
276
-
277
- symbolChoices.push(new inquirer.Separator());
278
- symbolChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
279
-
280
- const { selectedSymbol } = await inquirer.prompt([
281
- {
282
- type: 'list',
283
- name: 'selectedSymbol',
284
- message: chalk.white.bold('Select Symbol:'),
285
- choices: symbolChoices,
286
- pageSize: 50,
287
- loop: false
288
- }
289
- ]);
290
-
291
- if (selectedSymbol === 'back') {
292
- return;
293
- }
294
-
295
- // Use the selected contract directly (already fetched from API)
296
- let contract = selectedSymbol;
297
-
298
- console.log();
299
- console.log(chalk.green(` [OK] Selected: ${contract.name || contract.symbol}`));
300
- if (contract.tickSize && contract.tickValue) {
301
- console.log(chalk.gray(` Tick Size: ${contract.tickSize} | Tick Value: $${contract.tickValue}`));
302
- }
303
-
304
- // If contract doesn't have full details, search again
305
- if (!contract.id || !contract.tickSize) {
306
- const searchSpinner = ora(`Getting contract details...`).start();
307
- const contractResult = await service.searchContracts(contract.symbol || contract.searchText, false);
308
-
309
- if (contractResult.success && contractResult.contracts && contractResult.contracts.length > 0) {
310
- const found = contractResult.contracts.find(c => c.activeContract) || contractResult.contracts[0];
311
- contract = { ...contract, ...found };
312
- searchSpinner.succeed(`Contract: ${contract.name || contract.symbol}`);
313
- } else {
314
- searchSpinner.warn('Using basic contract info');
315
- contract = {
316
- id: contract.symbol || contract.id,
317
- name: contract.name || contract.symbol,
318
- symbol: contract.symbol || contract.id
319
- };
320
- }
321
- }
322
-
323
- console.log();
324
-
325
- // Number of contracts
326
- const { contracts } = await inquirer.prompt([
327
- {
328
- type: 'input',
329
- name: 'contracts',
330
- message: chalk.white.bold('Number of Contracts:'),
331
- default: '1',
332
- validate: (input) => {
333
- const num = parseInt(input);
334
- if (isNaN(num) || num <= 0 || num > 100) {
335
- return 'Please enter a valid number between 1 and 100';
336
- }
337
- return true;
338
- },
339
- filter: (input) => parseInt(input)
340
- }
341
- ]);
342
-
343
- // Risk Management
344
- console.log();
345
- console.log(chalk.cyan.bold(' Risk Management'));
346
- console.log(chalk.gray(' Set your daily target and maximum risk to auto-stop the algo.'));
347
- console.log();
348
-
349
- const { dailyTarget } = await inquirer.prompt([
350
- {
351
- type: 'input',
352
- name: 'dailyTarget',
353
- message: chalk.white.bold('Daily Target ($):'),
354
- default: '500',
355
- validate: (input) => {
356
- const num = parseFloat(input);
357
- if (isNaN(num) || num <= 0) {
358
- return 'Please enter a valid amount greater than 0';
359
- }
360
- return true;
361
- },
362
- filter: (input) => parseFloat(input)
363
- }
364
- ]);
365
-
366
- const { maxRisk } = await inquirer.prompt([
367
- {
368
- type: 'input',
369
- name: 'maxRisk',
370
- message: chalk.white.bold('Max Risk ($):'),
371
- default: '200',
372
- validate: (input) => {
373
- const num = parseFloat(input);
374
- if (isNaN(num) || num <= 0) {
375
- return 'Please enter a valid amount greater than 0';
376
- }
377
- return true;
378
- },
379
- filter: (input) => parseFloat(input)
380
- }
381
- ]);
382
-
383
- // Privacy option - show or hide account name
384
- console.log();
385
- const { showAccountName } = await inquirer.prompt([
386
- {
387
- type: 'list',
388
- name: 'showAccountName',
389
- message: chalk.white.bold('Account name visibility:'),
390
- choices: [
391
- { name: chalk.cyan('[>] Show account name'), value: true },
392
- { name: chalk.gray('[.] Hide account name'), value: false }
393
- ],
394
- loop: false
395
- }
396
- ]);
397
-
398
- const displayAccountName = showAccountName ? accountName : 'HQX *****';
399
-
400
- // Confirmation
401
- console.log();
402
- console.log(chalk.gray(getSeparator()));
403
- console.log(chalk.white.bold(' Algo Configuration:'));
404
- console.log(chalk.gray(getSeparator()));
405
- console.log(chalk.white(` Account: ${chalk.cyan(displayAccountName)}`));
406
- console.log(chalk.white(` Symbol: ${chalk.cyan(contract.name || selectedSymbol.value)}`));
407
- console.log(chalk.white(` Contracts: ${chalk.cyan(contracts)}`));
408
- console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
409
- console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
410
- console.log(chalk.gray(getSeparator()));
411
- console.log();
412
-
413
- const { launch } = await inquirer.prompt([
414
- {
415
- type: 'list',
416
- name: 'launch',
417
- message: chalk.white.bold('Ready to launch?'),
418
- choices: [
419
- { name: chalk.green.bold('[>] Launch Algo'), value: 'launch' },
420
- { name: chalk.yellow('< Back'), value: 'back' }
421
- ],
422
- loop: false
423
- }
424
- ]);
425
-
426
- if (launch === 'back') {
427
- return;
428
- }
429
-
430
- await launchAlgo(service, account, contract, contracts, dailyTarget, maxRisk, showAccountName);
431
- };
432
-
433
- /**
434
- * Launch Algo with HQX Server Connection
435
- */
436
- const launchAlgo = async (service, account, contract, numContracts, dailyTarget, maxRisk, showAccountName = true) => {
437
- const realAccountName = account.accountName || account.name || 'Account #' + account.accountId;
438
- const accountName = showAccountName ? realAccountName : 'HQX *****';
439
- const symbolName = contract.name || contract.symbol || contract.id;
440
- const symbol = contract.symbol || contract.id;
441
-
442
- console.log();
443
- console.log(chalk.green.bold(' [>] Launching HQX Algo...'));
444
- console.log();
445
-
446
- // Initialize HQX Server connection
447
- const hqxServer = new HQXServerService();
448
- let hqxConnected = false;
449
- let algoRunning = false;
450
- let stopReason = null;
451
- let latency = 0;
452
- let spinnerFrame = 0;
453
- const spinnerChars = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
454
- const sessionStartTime = Date.now();
455
-
456
- // Stats
457
- let stats = {
458
- trades: 0,
459
- wins: 0,
460
- losses: 0,
461
- pnl: 0,
462
- signals: 0,
463
- winRate: '0.0'
464
- };
465
-
466
- // Logs buffer - newest first display, show many logs
467
- const logs = [];
468
- const MAX_LOGS = 50;
469
-
470
- // Log colors
471
- const typeColors = {
472
- info: chalk.cyan,
473
- success: chalk.green,
474
- signal: chalk.yellow.bold,
475
- trade: chalk.green.bold,
476
- loss: chalk.magenta.bold,
477
- error: chalk.red,
478
- warning: chalk.yellow
479
- };
480
-
481
- const getIcon = (type) => {
482
- // Fixed width tags (10 chars) for alignment
483
- switch(type) {
484
- case 'signal': return '[SIGNAL] ';
485
- case 'trade': return '[TRADE] ';
486
- case 'order': return '[ORDER] ';
487
- case 'position': return '[POSITION]';
488
- case 'error': return '[ERROR] ';
489
- case 'warning': return '[WARNING] ';
490
- case 'success': return '[OK] ';
491
- case 'analysis': return '[ANALYSIS]';
492
- default: return '[INFO] ';
493
- }
494
- };
495
-
496
- // Add log (oldest first, newest at bottom)
497
- const addLog = (type, message) => {
498
- const timestamp = new Date().toLocaleTimeString();
499
- logs.push({ timestamp, type, message }); // Add at end
500
- if (logs.length > MAX_LOGS) logs.shift(); // Remove oldest from top
501
- };
502
-
503
- // Print log - just add to buffer, spinner interval will refresh display
504
- // This prevents display flicker from multiple concurrent displayUI() calls
505
- const printLog = (type, message) => {
506
- addLog(type, message);
507
- // Don't call displayUI() here - let the spinner interval handle it
508
- // This prevents flickering when logs arrive rapidly
509
- };
510
-
511
- // Check market hours
512
- const checkMarketStatus = () => {
513
- const now = new Date();
514
- const utcDay = now.getUTCDay();
515
- const utcHour = now.getUTCHours();
516
- const isDST = (() => {
517
- const jan = new Date(now.getFullYear(), 0, 1);
518
- const jul = new Date(now.getFullYear(), 6, 1);
519
- return now.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
520
- })();
521
- const ctOffset = isDST ? 5 : 6;
522
- const ctHour = (utcHour - ctOffset + 24) % 24;
523
- const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
524
-
525
- if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
526
- if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
527
- if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
528
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
529
- return { isOpen: true, message: 'Market OPEN' };
530
- };
531
-
532
- // Display full UI with logs (newest first at top)
533
- let firstDraw = true;
534
- let isDrawing = false; // Mutex to prevent concurrent draws
535
-
536
- // Build entire screen as a single string buffer to write atomically
537
- let screenBuffer = '';
538
-
539
- const bufferLine = (text) => {
540
- screenBuffer += text + '\x1B[K\n'; // Add text + clear to EOL + newline
541
- };
542
-
543
- // Legacy function for compatibility
544
- const printLine = bufferLine;
545
-
546
- const displayUI = () => {
547
- // Prevent concurrent draws
548
- if (isDrawing) return;
549
- isDrawing = true;
550
-
551
- // Reset buffer
552
- screenBuffer = '';
553
-
554
- if (firstDraw) {
555
- // Switch to alternate screen buffer - isolates our display
556
- screenBuffer += '\x1B[?1049h'; // Enter alternate screen
557
- screenBuffer += '\x1B[?25l'; // Hide cursor
558
- screenBuffer += '\x1B[2J'; // Clear screen
559
- firstDraw = false;
560
- }
561
-
562
- // Move cursor to home position
563
- screenBuffer += '\x1B[H';
564
-
565
- // Stats
566
- const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
567
- const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
568
- // Always show latency in ms format
569
- const latencyMs = latency > 0 ? latency : 0;
570
- const latencyStr = `${latencyMs}ms`;
571
- const latencyColor = latencyMs < 100 ? chalk.green : (latencyMs < 300 ? chalk.yellow : chalk.red);
572
- const serverStatus = hqxConnected ? 'ON' : 'OFF';
573
- const serverColor = hqxConnected ? chalk.green : chalk.red;
574
-
575
- // Current date
576
- const now = new Date();
577
- const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
578
-
579
- // Get package version
580
- const version = require('../../package.json').version;
581
-
582
- // Fixed width = 96 inner chars
583
- const W = 96;
584
- const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
585
- const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
586
- const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
587
- const V = '\u2551';
588
-
589
- // Center text helper
590
- const center = (text, width) => {
591
- const pad = Math.floor((width - text.length) / 2);
592
- return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
593
- };
594
-
595
- // Pad text to exact width
596
- const padRight = (text, width) => {
597
- if (text.length >= width) return text.substring(0, width);
598
- return text + ' '.repeat(width - text.length);
599
- };
600
-
601
- printLine('');
602
- printLine(chalk.cyan(TOP));
603
- // Logo = 87 chars cyan + 9 chars yellow = 96 total
604
- printLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
605
- printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
606
- printLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
607
- printLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
608
- printLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
609
- printLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
610
- printLine(chalk.cyan(MID));
611
-
612
- // Centered title
613
- const title1 = `Prop Futures Algo Trading v${version}`;
614
- printLine(chalk.cyan(V) + chalk.white(center(title1, W)) + chalk.cyan(V));
615
- printLine(chalk.cyan(MID));
616
-
617
- // Centered subtitle
618
- const title2 = 'HQX Ultra-Scalping Algorithm';
619
- printLine(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
620
-
621
- // Grid layout for metrics - 2 columns per row, 4 rows
622
- // Row 1: Account | Symbol + Qty
623
- // Row 2: Target | Risk
624
- // Row 3: P&L | Server
625
- // Row 4: Trades + W/L | Latency
626
- const VS = '\u2502'; // Vertical separator (thin)
627
-
628
- // 2 columns: 48 + 47 + 1 separator = 96
629
- const colL = 48, colR = 47;
630
-
631
- // Safe padding function
632
- const safePad = (len) => ' '.repeat(Math.max(0, len));
633
-
634
- // Build cell helper
635
- const buildCell = (label, value, valueColor, width) => {
636
- const text = ` ${label}: ${valueColor(value)}`;
637
- const plain = ` ${label}: ${value}`;
638
- return { text, plain, padded: text + safePad(width - plain.length) };
639
- };
640
-
641
- // Row 1: Account | Symbol + Qty
642
- const accVal = accountName.length > 35 ? accountName.substring(0, 35) : accountName;
643
- const symVal = symbolName.length > 12 ? symbolName.substring(0, 12) : symbolName;
644
- const r1c1 = buildCell('Account', accVal, chalk.cyan, colL);
645
- const r1c2text = ` Symbol: ${chalk.yellow(symVal)} Qty: ${chalk.cyan(numContracts)}`;
646
- const r1c2plain = ` Symbol: ${symVal} Qty: ${numContracts}`;
647
- const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
648
-
649
- // Row 2: Target | Risk
650
- const r2c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
651
- const r2c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
652
-
653
- // Row 3: P&L | Server
654
- const r3c1 = buildCell('P&L', pnlStr, pnlColor, colL);
655
- const r3c2 = buildCell('Server', serverStatus, serverColor, colR);
656
-
657
- // Row 4: Trades + W/L | Latency
658
- const r4c1text = ` Trades: ${chalk.cyan(stats.trades)} W/L: ${chalk.green(stats.wins)}/${chalk.red(stats.losses)}`;
659
- const r4c1plain = ` Trades: ${stats.trades} W/L: ${stats.wins}/${stats.losses}`;
660
- const r4c1 = r4c1text + safePad(colL - r4c1plain.length);
661
- const r4c2 = buildCell('Latency', latencyStr, latencyColor, colR);
662
-
663
- // Grid separators
664
- const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
665
- const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
666
- const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
667
-
668
- // Print grid
669
- printLine(chalk.cyan(GRID_TOP));
670
- printLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
671
- printLine(chalk.cyan(GRID_MID));
672
- printLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2.padded + chalk.cyan(V));
673
- printLine(chalk.cyan(GRID_MID));
674
- printLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
675
- printLine(chalk.cyan(GRID_MID));
676
- printLine(chalk.cyan(V) + r4c1 + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
677
- printLine(chalk.cyan(GRID_BOT));
678
-
679
- // Activity log header with spinner and centered date
680
- spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
681
- const spinnerChar = spinnerChars[spinnerFrame];
682
- const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
683
- const actLeftPlain = ` Activity Log ${spinnerChar}`;
684
- const actRight = 'Press X to stop ';
685
- const dateCentered = `- ${dateStr} -`;
686
- const leftLen = actLeftPlain.length;
687
- const rightLen = actRight.length;
688
- const midSpace = Math.max(0, W - leftLen - rightLen);
689
- const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
690
- const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
691
- const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
692
- bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
693
- bufferLine(chalk.cyan(MID));
694
-
695
- // Helper to strip ANSI codes for length calculation
696
- const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
697
-
698
- // Helper to truncate and pad text to exact width W
699
- const fitToWidth = (text, width) => {
700
- const plainText = stripAnsi(text);
701
- if (plainText.length > width) {
702
- // Truncate - find where to cut in original string
703
- let count = 0;
704
- let cutIndex = 0;
705
- for (let i = 0; i < text.length && count < width - 3; i++) {
706
- if (text[i] === '\x1B') {
707
- // Skip ANSI sequence
708
- while (i < text.length && text[i] !== 'm') i++;
709
- } else {
710
- count++;
711
- cutIndex = i + 1;
712
- }
713
- }
714
- return text.substring(0, cutIndex) + '...';
715
- }
716
- return text + ' '.repeat(width - plainText.length);
717
- };
718
-
719
- // Logs inside the rectangle - newest first, max 50 lines
720
- const MAX_VISIBLE_LOGS = 50;
721
-
722
- if (logs.length === 0) {
723
- const emptyLine = ' Waiting for activity...';
724
- bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
725
- // Fill remaining lines
726
- for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
727
- bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
728
- }
729
- } else {
730
- // Show newest first (reverse), limited to MAX_VISIBLE_LOGS
731
- const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
732
- reversedLogs.forEach(log => {
733
- const color = typeColors[log.type] || chalk.white;
734
- const icon = getIcon(log.type);
735
- // Build log line content (plain text, no color yet)
736
- const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
737
- // Fit to width then apply color
738
- const fitted = fitToWidth(logContent, W);
739
- bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
740
- });
741
- // Fill remaining lines with empty to keep fixed height
742
- for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
743
- bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
744
- }
745
- }
746
-
747
- // Bottom border to close the rectangle
748
- bufferLine(chalk.cyan(BOT));
749
-
750
- // Write entire buffer atomically
751
- process.stdout.write(screenBuffer);
752
-
753
- isDrawing = false;
754
- };
755
-
756
- // Spinner interval to refresh UI - 250ms for stability
757
- const spinnerInterval = setInterval(() => {
758
- if (algoRunning && !isDrawing) {
759
- displayUI();
760
- }
761
- }, 250);
762
-
763
- // Connect to HQX Server
764
- const spinner = ora('Authenticating with HQX Server...').start();
765
-
766
- try {
767
- // Authenticate
768
- const authResult = await hqxServer.authenticate(account.accountId.toString(), account.propfirm || 'projectx');
769
-
770
- if (!authResult.success) {
771
- spinner.fail('Authentication failed: ' + (authResult.error || 'Unknown error'));
772
- addLog('error', 'Authentication failed');
773
-
774
- // Fallback to offline mode
775
- console.log(chalk.yellow(' Running in offline demo mode...'));
776
- console.log();
777
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
778
- return;
779
- }
780
-
781
- spinner.text = 'Connecting to WebSocket...';
782
-
783
- // Connect WebSocket
784
- const connectResult = await hqxServer.connect();
785
-
786
- if (connectResult.success) {
787
- spinner.succeed('Connected to HQX Server');
788
- hqxConnected = true;
789
- } else {
790
- throw new Error('WebSocket connection failed');
791
- }
792
-
793
- } catch (error) {
794
- spinner.warn('HQX Server unavailable - Running in offline mode');
795
- hqxConnected = false;
796
- }
797
-
798
- // Setup event handlers - logs scroll down naturally
799
- hqxServer.on('latency', (data) => {
800
- latency = data.latency || 0;
801
- // Don't call displayUI() - spinner interval will refresh
802
- });
803
-
804
- hqxServer.on('log', (data) => {
805
- let message = data.message;
806
- // If account name is hidden, filter it from logs too
807
- if (!showAccountName && realAccountName) {
808
- message = message.replace(new RegExp(realAccountName, 'gi'), 'HQX *****');
809
- }
810
- printLog(data.type || 'info', message);
811
- });
812
-
813
- hqxServer.on('signal', (data) => {
814
- stats.signals++;
815
- const side = data.side === 'long' ? 'BUY' : 'SELL';
816
- printLog('signal', `${side} Signal @ ${data.entry?.toFixed(2) || 'N/A'} | SL: ${data.stop?.toFixed(2) || 'N/A'} | TP: ${data.target?.toFixed(2) || 'N/A'}`);
817
-
818
- // Execute order via PropFirm API if connected
819
- if (hqxConnected && service) {
820
- executeSignal(service, account, contract, numContracts, data);
821
- }
822
- });
823
-
824
- hqxServer.on('trade', (data) => {
825
- stats.trades++;
826
- stats.pnl += data.pnl || 0;
827
- if (data.pnl > 0) {
828
- stats.wins++;
829
- printLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
830
- } else {
831
- stats.losses++;
832
- printLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
833
- }
834
- stats.winRate = stats.trades > 0 ? ((stats.wins / stats.trades) * 100).toFixed(1) : '0.0';
835
-
836
- // Print updated stats
837
- const statsType = stats.pnl >= 0 ? 'info' : 'loss';
838
- printLog(statsType, `Stats: Trades: ${stats.trades} | Wins: ${stats.wins} | P&L: $${stats.pnl.toFixed(2)}`);
839
-
840
- // Check daily target
841
- if (stats.pnl >= dailyTarget) {
842
- stopReason = 'target';
843
- printLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
844
- algoRunning = false;
845
- if (hqxConnected) {
846
- hqxServer.stopAlgo();
847
- }
848
- }
849
-
850
- // Check max risk
851
- if (stats.pnl <= -maxRisk) {
852
- stopReason = 'risk';
853
- printLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
854
- algoRunning = false;
855
- if (hqxConnected) {
856
- hqxServer.stopAlgo();
857
- }
858
- }
859
- });
860
-
861
- hqxServer.on('stats', (data) => {
862
- // Update stats from server
863
- stats.trades = data.trades || stats.trades;
864
- stats.wins = data.wins || stats.wins;
865
- stats.losses = data.losses || stats.losses;
866
- stats.signals = data.signals || stats.signals;
867
- stats.winRate = data.winRate || stats.winRate;
868
-
869
- // P&L = realized P&L + unrealized P&L from open position
870
- const realizedPnl = data.pnl || 0;
871
- const unrealizedPnl = data.position?.pnl || 0;
872
- stats.pnl = realizedPnl + unrealizedPnl;
873
- });
874
-
875
- hqxServer.on('error', (data) => {
876
- printLog('error', data.message || 'Unknown error');
877
- // Stop algo on connection error
878
- if (!stopReason) {
879
- stopReason = 'connection_error';
880
- algoRunning = false;
881
- }
882
- });
883
-
884
- hqxServer.on('disconnected', () => {
885
- hqxConnected = false;
886
- // Only log error if not intentionally stopped by user
887
- if (!stopReason || stopReason === 'user') {
888
- // Don't show error for user-initiated stop
889
- if (!stopReason) {
890
- printLog('error', 'Connection lost - Stopping algo');
891
- stopReason = 'disconnected';
892
- algoRunning = false;
893
- }
894
- }
895
- });
896
-
897
- // Display header once
898
- displayUI();
899
-
900
- // Start algo
901
- if (hqxConnected) {
902
- printLog('info', 'Starting HQX Ultra-Scalping...');
903
- printLog('info', `Target: $${dailyTarget.toFixed(2)} | Risk: $${maxRisk.toFixed(2)}`);
904
-
905
- // Get propfirm token for real market data
906
- const propfirmToken = service.getToken ? service.getToken() : null;
907
- const propfirmId = service.getPropfirm ? service.getPropfirm() : (account.propfirm || 'topstep');
908
-
909
- hqxServer.startAlgo({
910
- accountId: account.accountId,
911
- contractId: contract.id || contract.contractId,
912
- symbol: symbol,
913
- contracts: numContracts,
914
- dailyTarget: dailyTarget,
915
- maxRisk: maxRisk,
916
- propfirm: propfirmId,
917
- propfirmToken: propfirmToken
918
- });
919
- algoRunning = true;
920
- } else {
921
- printLog('warning', 'Running in offline demo mode');
922
- printLog('info', 'No real trades will be executed');
923
- algoRunning = true;
924
- }
925
-
926
- // Wait for X key OR auto-stop (target/risk reached)
927
- await new Promise((resolve) => {
928
- let resolved = false;
929
-
930
- const cleanup = () => {
931
- if (resolved) return;
932
- resolved = true;
933
- clearInterval(checkInterval);
934
- if (process.stdin.isTTY) {
935
- try {
936
- process.stdin.setRawMode(false);
937
- process.stdin.removeAllListeners('keypress');
938
- } catch (e) {}
939
- }
940
- resolve();
941
- };
942
-
943
- // Check for auto-stop every 500ms
944
- const checkInterval = setInterval(() => {
945
- if (!algoRunning || stopReason) {
946
- cleanup();
947
- }
948
- }, 500);
949
-
950
- // Listen for X key
951
- if (process.stdin.isTTY) {
952
- try {
953
- readline.emitKeypressEvents(process.stdin);
954
- process.stdin.setRawMode(true);
955
- process.stdin.resume();
956
-
957
- process.stdin.on('keypress', (str, key) => {
958
- if (!key) return;
959
- const keyName = key.name?.toLowerCase();
960
- if (keyName === 'x' || (key.ctrl && keyName === 'c')) {
961
- stopReason = 'user'; // Set stop reason before cleanup
962
- cleanup();
963
- }
964
- });
965
- } catch (e) {
966
- // Fallback: just wait for auto-stop
967
- }
968
- }
969
- });
970
-
971
- // Clear spinner interval
972
- clearInterval(spinnerInterval);
973
-
974
- // Exit alternate screen buffer and show cursor
975
- process.stdout.write('\x1B[?1049l'); // Exit alternate screen
976
- process.stdout.write('\x1B[?25h'); // Show cursor
977
-
978
- // Stop algo
979
- console.log();
980
- if (!stopReason) {
981
- printLog('warning', 'Stopping algo...');
982
- }
983
-
984
- // Cancel all pending orders and close positions
985
- printLog('info', 'Cancelling pending orders...');
986
-
987
- try {
988
- // Cancel all orders
989
- const cancelResult = await service.cancelAllOrders(account.accountId);
990
- if (cancelResult.success) {
991
- printLog('success', 'All pending orders cancelled');
992
- } else {
993
- printLog('warning', 'No pending orders to cancel');
994
- }
995
- } catch (e) {
996
- printLog('warning', 'Could not cancel orders: ' + e.message);
997
- }
998
-
999
- // Close all positions for this symbol
1000
- printLog('info', 'Closing open positions...');
1001
-
1002
- try {
1003
- const positions = await service.getPositions(account.accountId);
1004
- if (positions.success && positions.positions) {
1005
- const symbolPos = positions.positions.find(p =>
1006
- p.symbol === symbol ||
1007
- p.contractId === (contract.id || contract.contractId)
1008
- );
1009
-
1010
- if (symbolPos && symbolPos.quantity !== 0) {
1011
- const closeResult = await service.closePosition(account.accountId, symbolPos.contractId || symbolPos.symbol);
1012
- if (closeResult.success) {
1013
- printLog('success', `Position closed: ${Math.abs(symbolPos.quantity)} ${symbol}`);
1014
- } else {
1015
- printLog('error', 'Failed to close position: ' + (closeResult.error || 'Unknown'));
1016
- }
1017
- } else {
1018
- printLog('info', 'No open position to close');
1019
- }
1020
- }
1021
- } catch (e) {
1022
- printLog('warning', 'Could not close positions: ' + e.message);
1023
- }
1024
-
1025
- if (hqxConnected && algoRunning) {
1026
- hqxServer.stopAlgo();
1027
- }
1028
-
1029
- hqxServer.disconnect();
1030
- algoRunning = false;
1031
-
1032
- // Small delay to ensure all cleanup is done
1033
- await new Promise(r => setTimeout(r, 500));
1034
-
1035
- // Show cursor again (don't clear screen - show summary below logs)
1036
- process.stdout.write('\x1B[?25h');
1037
-
1038
- // Print stop reason message
1039
- console.log();
1040
- console.log();
1041
- if (stopReason === 'target') {
1042
- console.log(chalk.green.bold(' [OK] Daily target reached! Algo stopped.'));
1043
- } else if (stopReason === 'risk') {
1044
- console.log(chalk.red.bold(' [X] Max risk reached! Algo stopped.'));
1045
- } else if (stopReason === 'disconnected' || stopReason === 'connection_error') {
1046
- console.log(chalk.red.bold(' [X] Connection lost! Algo stopped.'));
1047
- } else if (stopReason === 'user') {
1048
- console.log(chalk.yellow(' [OK] Algo stopped by user'));
1049
- } else {
1050
- console.log(chalk.yellow(' [OK] Algo stopped by user'));
1051
- }
1052
- console.log();
1053
-
1054
- // Final stats in a grid box - must match main UI width of 96
1055
- const summaryV = '\u2551';
1056
- const summaryVS = '\u2502';
1057
- const summaryH = '\u2550';
1058
- const summaryW = 96; // Same as main UI
1059
-
1060
- // Calculate session duration
1061
- const sessionDuration = Date.now() - sessionStartTime;
1062
- const durationSec = Math.floor(sessionDuration / 1000);
1063
- const durationMin = Math.floor(durationSec / 60);
1064
- const durationHr = Math.floor(durationMin / 60);
1065
- const durationStr = durationHr > 0
1066
- ? `${durationHr}h ${durationMin % 60}m ${durationSec % 60}s`
1067
- : durationMin > 0
1068
- ? `${durationMin}m ${durationSec % 60}s`
1069
- : `${durationSec}s`;
1070
-
1071
- // 4 cells + 3 separators = 96 inner chars
1072
- // 96 - 3 separators = 93, divided by 4 = 23.25, so use 24+23+24+23 = 94... need 96
1073
- // Let's use: 24 + 24 + 24 + 21 = 93 + 3 sep = 96
1074
- const sc1 = 24, sc2 = 24, sc3 = 24, sc4 = 21;
1075
-
1076
- const summaryCell = (label, value, width) => {
1077
- const text = ` ${label}: ${value}`;
1078
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
1079
- const padding = Math.max(0, width - stripped.length);
1080
- return text + ' '.repeat(padding);
1081
- };
1082
-
1083
- const centerSummaryTitle = (text, width) => {
1084
- const pad = Math.floor((width - text.length) / 2);
1085
- return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
1086
- };
1087
-
1088
- const pnlValue = stats.pnl >= 0 ? chalk.green('+$' + stats.pnl.toFixed(2)) : chalk.red('-$' + Math.abs(stats.pnl).toFixed(2));
1089
-
1090
- // Build separator lines
1091
- const SUMMARY_TOP = '\u2554' + summaryH.repeat(summaryW) + '\u2557';
1092
- const SUMMARY_GRID_TOP = '\u2560' + summaryH.repeat(sc1) + '\u2564' + summaryH.repeat(sc2) + '\u2564' + summaryH.repeat(sc3) + '\u2564' + summaryH.repeat(sc4) + '\u2563';
1093
- const SUMMARY_GRID_MID = '\u2560' + summaryH.repeat(sc1) + '\u256A' + summaryH.repeat(sc2) + '\u256A' + summaryH.repeat(sc3) + '\u256A' + summaryH.repeat(sc4) + '\u2563';
1094
- const SUMMARY_BOT = '\u255A' + summaryH.repeat(sc1) + '\u2567' + summaryH.repeat(sc2) + '\u2567' + summaryH.repeat(sc3) + '\u2567' + summaryH.repeat(sc4) + '\u255D';
1095
-
1096
- console.log();
1097
- console.log(chalk.cyan(SUMMARY_TOP));
1098
- console.log(chalk.cyan(summaryV) + chalk.white.bold(centerSummaryTitle('Session Summary', summaryW)) + chalk.cyan(summaryV));
1099
- console.log(chalk.cyan(SUMMARY_GRID_TOP));
1100
-
1101
- // Row 1: Target | Risk | P&L | Win Rate
1102
- const r1c1 = summaryCell('Target', chalk.green('$' + dailyTarget.toFixed(2)), sc1);
1103
- const r1c2 = summaryCell('Risk', chalk.red('$' + maxRisk.toFixed(2)), sc2);
1104
- const r1c3 = summaryCell('P&L', pnlValue, sc3);
1105
- const r1c4 = summaryCell('Win Rate', chalk.yellow(stats.winRate + '%'), sc4);
1106
- console.log(chalk.cyan(summaryV) + r1c1 + chalk.cyan(summaryVS) + r1c2 + chalk.cyan(summaryVS) + r1c3 + chalk.cyan(summaryVS) + r1c4 + chalk.cyan(summaryV));
1107
-
1108
- console.log(chalk.cyan(SUMMARY_GRID_MID));
1109
-
1110
- // Row 2: Trades | Wins | Losses | Duration
1111
- const r2c1 = summaryCell('Trades', chalk.cyan(stats.trades.toString()), sc1);
1112
- const r2c2 = summaryCell('Wins', chalk.green(stats.wins.toString()), sc2);
1113
- const r2c3 = summaryCell('Losses', chalk.red(stats.losses.toString()), sc3);
1114
- const r2c4 = summaryCell('Duration', chalk.white(durationStr), sc4);
1115
- console.log(chalk.cyan(summaryV) + r2c1 + chalk.cyan(summaryVS) + r2c2 + chalk.cyan(summaryVS) + r2c3 + chalk.cyan(summaryVS) + r2c4 + chalk.cyan(summaryV));
1116
-
1117
- console.log(chalk.cyan(SUMMARY_BOT));
1118
- console.log();
1119
-
1120
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1121
- };
1122
-
1123
- /**
1124
- * Execute signal via PropFirm API
1125
- */
1126
- const executeSignal = async (service, account, contract, numContracts, signal) => {
1127
- try {
1128
- const orderData = {
1129
- accountId: account.accountId,
1130
- contractId: contract.id || contract.contractId,
1131
- type: 2, // Market order
1132
- side: signal.side === 'long' ? 0 : 1, // 0=Buy, 1=Sell
1133
- size: numContracts
1134
- };
1135
-
1136
- // Place order via ProjectX Gateway API
1137
- const result = await service.placeOrder(orderData);
1138
-
1139
- if (result.success) {
1140
- console.log(chalk.green(` [OK] Order executed: ${signal.side.toUpperCase()} ${numContracts} contracts`));
1141
- } else {
1142
- console.log(chalk.red(` [X] Order failed: ${result.error || 'Unknown error'}`));
1143
- }
1144
- } catch (error) {
1145
- console.log(chalk.red(` [X] Order error: ${error.message}`));
1146
- }
1147
- };
1148
-
1149
- /**
1150
- * Copy Trading Menu
1151
- */
1152
- const copyTradingMenu = async () => {
1153
- console.log();
1154
- console.log(chalk.gray(getSeparator()));
1155
- console.log(chalk.green.bold(' Copy Trading Setup'));
1156
- console.log(chalk.gray(getSeparator()));
1157
- console.log();
1158
-
1159
- // Check market status first
1160
- const marketSpinner = ora('Checking market status...').start();
1161
-
1162
- // Use a simple market hours check
1163
- const now = new Date();
1164
- const utcDay = now.getUTCDay();
1165
- const utcHour = now.getUTCHours();
1166
- const isDST = (() => {
1167
- const jan = new Date(now.getFullYear(), 0, 1);
1168
- const jul = new Date(now.getFullYear(), 6, 1);
1169
- return now.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
1170
- })();
1171
- const ctOffset = isDST ? 5 : 6;
1172
- const ctHour = (utcHour - ctOffset + 24) % 24;
1173
- const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
1174
-
1175
- let marketClosed = false;
1176
- let marketMessage = '';
1177
-
1178
- if (ctDay === 6) {
1179
- marketClosed = true;
1180
- marketMessage = 'Market closed (Saturday)';
1181
- } else if (ctDay === 0 && ctHour < 17) {
1182
- marketClosed = true;
1183
- marketMessage = 'Market opens Sunday 5:00 PM CT';
1184
- } else if (ctDay === 5 && ctHour >= 16) {
1185
- marketClosed = true;
1186
- marketMessage = 'Market closed (Friday after 4PM CT)';
1187
- } else if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
1188
- marketClosed = true;
1189
- marketMessage = 'Daily maintenance (4:00-5:00 PM CT)';
1190
- }
1191
-
1192
- if (marketClosed) {
1193
- marketSpinner.fail('Market is CLOSED');
1194
- console.log();
1195
- console.log(chalk.red.bold(' [X] ' + marketMessage));
1196
- console.log();
1197
- console.log(chalk.gray(' Futures markets (CME) trading hours:'));
1198
- console.log(chalk.gray(' Sunday 5:00 PM CT - Friday 4:00 PM CT'));
1199
- console.log(chalk.gray(' Daily maintenance: 4:00 PM - 5:00 PM CT'));
1200
- console.log();
1201
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1202
- return;
1203
- }
1204
-
1205
- marketSpinner.succeed('Market is OPEN - Ready to trade!');
1206
- console.log();
1207
-
1208
- // Get all active accounts from all connections
1209
- const allAccounts = await connections.getAllAccounts();
1210
- const activeAccounts = allAccounts.filter(acc => acc.status === 0);
1211
-
1212
- if (activeAccounts.length < 2) {
1213
- console.log(chalk.red(' [X] You need at least 2 active accounts for copy trading.'));
1214
- console.log(chalk.gray(' Connect more prop firm accounts first.'));
1215
- console.log();
1216
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1217
- return;
1218
- }
1219
-
1220
- // Step 1: Risk Management Settings
1221
- console.log(chalk.cyan.bold(' Step 1: Risk Management'));
1222
- console.log(chalk.gray(' Set your daily target and maximum risk to auto-stop copy trading.'));
1223
- console.log();
1224
-
1225
- const { dailyTarget } = await inquirer.prompt([
1226
- {
1227
- type: 'input',
1228
- name: 'dailyTarget',
1229
- message: chalk.white.bold('Daily Target ($):'),
1230
- default: '500',
1231
- validate: (input) => {
1232
- const num = parseFloat(input);
1233
- if (isNaN(num) || num <= 0) {
1234
- return 'Please enter a valid amount greater than 0';
1235
- }
1236
- return true;
1237
- },
1238
- filter: (input) => parseFloat(input)
1239
- }
1240
- ]);
1241
-
1242
- const { maxRisk } = await inquirer.prompt([
1243
- {
1244
- type: 'input',
1245
- name: 'maxRisk',
1246
- message: chalk.white.bold('Max Risk ($):'),
1247
- default: '200',
1248
- validate: (input) => {
1249
- const num = parseFloat(input);
1250
- if (isNaN(num) || num <= 0) {
1251
- return 'Please enter a valid amount greater than 0';
1252
- }
1253
- return true;
1254
- },
1255
- filter: (input) => parseFloat(input)
1256
- }
1257
- ]);
1258
-
1259
- console.log();
1260
- console.log(chalk.gray(' Daily Target: ') + chalk.green('$' + dailyTarget.toFixed(2)));
1261
- console.log(chalk.gray(' Max Risk: ') + chalk.red('$' + maxRisk.toFixed(2)));
1262
- console.log();
1263
-
1264
- // Step 2: Select Lead Account
1265
- console.log(chalk.cyan.bold(' Step 2: Select Lead Account'));
1266
- console.log(chalk.gray(' The lead account is the master account whose trades will be copied.'));
1267
- console.log();
1268
-
1269
- const leadChoices = activeAccounts.map(acc => ({
1270
- name: chalk.cyan(`${acc.accountName || acc.name} - ${acc.propfirm} - $${acc.balance.toLocaleString()}`),
1271
- value: acc
1272
- }));
1273
- leadChoices.push(new inquirer.Separator());
1274
- leadChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
1275
-
1276
- const { leadAccount } = await inquirer.prompt([
1277
- {
1278
- type: 'list',
1279
- name: 'leadAccount',
1280
- message: chalk.white.bold('Lead Account:'),
1281
- choices: leadChoices,
1282
- pageSize: 15,
1283
- loop: false
1284
- }
1285
- ]);
1286
-
1287
- if (leadAccount === 'back') return;
1288
-
1289
- // Step 3: Select Follower Account
1290
- console.log();
1291
- console.log(chalk.cyan.bold(' Step 3: Select Follower Account'));
1292
- console.log(chalk.gray(' The follower account will copy trades from the lead account.'));
1293
- console.log();
1294
-
1295
- const followerChoices = activeAccounts
1296
- .filter(acc => acc.accountId !== leadAccount.accountId)
1297
- .map(acc => ({
1298
- name: chalk.cyan(`${acc.accountName || acc.name} - ${acc.propfirm} - $${acc.balance.toLocaleString()}`),
1299
- value: acc
1300
- }));
1301
- followerChoices.push(new inquirer.Separator());
1302
- followerChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
1303
-
1304
- const { followerAccount } = await inquirer.prompt([
1305
- {
1306
- type: 'list',
1307
- name: 'followerAccount',
1308
- message: chalk.white.bold('Follower Account:'),
1309
- choices: followerChoices,
1310
- pageSize: 15,
1311
- loop: false
1312
- }
1313
- ]);
1314
-
1315
- if (followerAccount === 'back') return;
1316
-
1317
- // Step 4: Select Lead Symbol
1318
- console.log();
1319
- console.log(chalk.cyan.bold(' Step 4: Configure Lead Symbol'));
1320
- console.log();
1321
-
1322
- const { leadSymbol } = await inquirer.prompt([
1323
- {
1324
- type: 'list',
1325
- name: 'leadSymbol',
1326
- message: chalk.white.bold('Lead Symbol:'),
1327
- choices: FUTURES_SYMBOLS.map(s => ({
1328
- name: chalk.cyan(s.name),
1329
- value: s
1330
- })),
1331
- pageSize: 15,
1332
- loop: false
1333
- }
1334
- ]);
1335
-
1336
- const { leadContracts } = await inquirer.prompt([
1337
- {
1338
- type: 'number',
1339
- name: 'leadContracts',
1340
- message: chalk.white.bold('Lead Number of Contracts:'),
1341
- default: 1,
1342
- validate: (input) => {
1343
- if (isNaN(input) || input <= 0 || input > 100) {
1344
- return 'Please enter a valid number between 1 and 100';
1345
- }
1346
- return true;
1347
- }
1348
- }
1349
- ]);
1350
-
1351
- // Step 5: Select Follower Symbol
1352
- console.log();
1353
- console.log(chalk.cyan.bold(' Step 5: Configure Follower Symbol'));
1354
- console.log();
1355
-
1356
- const { followerSymbol } = await inquirer.prompt([
1357
- {
1358
- type: 'list',
1359
- name: 'followerSymbol',
1360
- message: chalk.white.bold('Follower Symbol:'),
1361
- choices: FUTURES_SYMBOLS.map(s => ({
1362
- name: chalk.cyan(s.name),
1363
- value: s
1364
- })),
1365
- pageSize: 15,
1366
- loop: false
1367
- }
1368
- ]);
1369
-
1370
- const { followerContracts } = await inquirer.prompt([
1371
- {
1372
- type: 'number',
1373
- name: 'followerContracts',
1374
- message: chalk.white.bold('Follower Number of Contracts:'),
1375
- default: 1,
1376
- validate: (input) => {
1377
- if (isNaN(input) || input <= 0 || input > 100) {
1378
- return 'Please enter a valid number between 1 and 100';
1379
- }
1380
- return true;
1381
- }
1382
- }
1383
- ]);
1384
-
1385
- // Privacy option - show or hide account names
1386
- console.log();
1387
- console.log(chalk.cyan.bold(' Step 6: Privacy Settings'));
1388
- console.log();
1389
-
1390
- const { showAccountNames } = await inquirer.prompt([
1391
- {
1392
- type: 'list',
1393
- name: 'showAccountNames',
1394
- message: chalk.white.bold('Account names visibility:'),
1395
- choices: [
1396
- { name: chalk.cyan('[>] Show account names'), value: true },
1397
- { name: chalk.gray('[.] Hide account names'), value: false }
1398
- ],
1399
- loop: false
1400
- }
1401
- ]);
1402
-
1403
- const displayLeadName = showAccountNames ? (leadAccount.accountName || leadAccount.name || 'N/A') : 'HQX Lead *****';
1404
- const displayFollowerName = showAccountNames ? (followerAccount.accountName || followerAccount.name || 'N/A') : 'HQX Follower *****';
1405
-
1406
- // Configuration Summary
1407
- console.log();
1408
- console.log(chalk.gray(getSeparator()));
1409
- console.log(chalk.white.bold(' Copy Trading Configuration'));
1410
- console.log(chalk.gray(getSeparator()));
1411
- console.log();
1412
- console.log(chalk.white(' RISK MANAGEMENT'));
1413
- console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
1414
- console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
1415
- console.log();
1416
- console.log(chalk.white(' LEAD ACCOUNT'));
1417
- console.log(chalk.white(` Account: ${chalk.cyan(displayLeadName)}`));
1418
- console.log(chalk.white(` PropFirm: ${chalk.magenta(leadAccount.propfirm || 'N/A')}`));
1419
- console.log(chalk.white(` Symbol: ${chalk.cyan(leadSymbol.name || 'N/A')}`));
1420
- console.log(chalk.white(` Contracts: ${chalk.cyan(leadContracts)}`));
1421
- console.log();
1422
- console.log(chalk.white(' FOLLOWER ACCOUNT'));
1423
- console.log(chalk.white(` Account: ${chalk.cyan(displayFollowerName)}`));
1424
- console.log(chalk.white(` PropFirm: ${chalk.magenta(followerAccount.propfirm || 'N/A')}`));
1425
- console.log(chalk.white(` Symbol: ${chalk.cyan(followerSymbol.name || 'N/A')}`));
1426
- console.log(chalk.white(` Contracts: ${chalk.cyan(followerContracts)}`));
1427
- console.log();
1428
- console.log(chalk.gray(getSeparator()));
1429
- console.log();
1430
-
1431
- const { launch } = await inquirer.prompt([
1432
- {
1433
- type: 'list',
1434
- name: 'launch',
1435
- message: chalk.white.bold('Ready to launch Copy Trading?'),
1436
- choices: [
1437
- { name: chalk.green.bold('[>] Launch Copy Trading'), value: 'launch' },
1438
- { name: chalk.yellow('< Back'), value: 'back' }
1439
- ],
1440
- loop: false
1441
- }
1442
- ]);
1443
-
1444
- if (launch === 'back') return;
1445
-
1446
- // Launch Copy Trading
1447
- await launchCopyTrading({
1448
- dailyTarget,
1449
- maxRisk,
1450
- lead: {
1451
- account: leadAccount,
1452
- symbol: leadSymbol,
1453
- contracts: leadContracts,
1454
- service: leadAccount.service,
1455
- displayName: displayLeadName
1456
- },
1457
- follower: {
1458
- account: followerAccount,
1459
- symbol: followerSymbol,
1460
- contracts: followerContracts,
1461
- service: followerAccount.service,
1462
- displayName: displayFollowerName
1463
- }
1464
- });
1465
- };
1466
-
1467
- /**
1468
- * Launch Copy Trading
1469
- */
1470
- const launchCopyTrading = async (config) => {
1471
- const { lead, follower, dailyTarget, maxRisk } = config;
1472
-
1473
- console.log();
1474
- console.log(chalk.green.bold(' [>] Launching Copy Trading...'));
1475
- console.log();
1476
-
1477
- let isRunning = true;
1478
- let stopReason = null;
1479
- let lastLeadPosition = null;
1480
- const logs = [];
1481
- const MAX_LOGS = 25;
1482
-
1483
- const stats = {
1484
- copiedTrades: 0,
1485
- leadTrades: 0,
1486
- followerTrades: 0,
1487
- signals: 0,
1488
- errors: 0,
1489
- pnl: 0,
1490
- trades: 0,
1491
- wins: 0,
1492
- losses: 0
1493
- };
1494
-
1495
- // Log colors
1496
- const typeColors = {
1497
- info: chalk.cyan,
1498
- success: chalk.green,
1499
- trade: chalk.green.bold,
1500
- copy: chalk.yellow.bold,
1501
- signal: chalk.magenta.bold,
1502
- loss: chalk.red.bold,
1503
- error: chalk.red,
1504
- warning: chalk.yellow
1505
- };
1506
-
1507
- const getIcon = (type) => {
1508
- switch(type) {
1509
- case 'signal': return '[~]';
1510
- case 'trade': return '[>]';
1511
- case 'copy': return '[+]';
1512
- case 'loss': return '[-]';
1513
- case 'error': return '[X]';
1514
- case 'success': return '[OK]';
1515
- default: return '[.]';
1516
- }
1517
- };
1518
-
1519
- const addLog = (type, message) => {
1520
- const timestamp = new Date().toLocaleTimeString();
1521
- logs.push({ timestamp, type, message });
1522
- if (logs.length > MAX_LOGS) logs.shift();
1523
- };
1524
-
1525
- // Build entire screen as a single string buffer to write atomically
1526
- let screenBuffer = '';
1527
- let firstDraw = true;
1528
- let isDrawing = false;
1529
- let spinnerFrame = 0;
1530
- const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
1531
-
1532
- // HQX Server connection state (declared here so displayUI can access it)
1533
- const hqxServer = new HQXServerService();
1534
- let hqxConnected = false;
1535
- let latency = 0;
1536
-
1537
- const bufferLine = (text) => {
1538
- screenBuffer += text + '\x1B[K\n';
1539
- };
1540
-
1541
- const displayUI = () => {
1542
- // Prevent concurrent draws
1543
- if (isDrawing) return;
1544
- isDrawing = true;
1545
-
1546
- // Reset buffer
1547
- screenBuffer = '';
1548
-
1549
- if (firstDraw) {
1550
- screenBuffer += '\x1B[?1049h'; // Enter alternate screen
1551
- screenBuffer += '\x1B[?25l'; // Hide cursor
1552
- screenBuffer += '\x1B[2J'; // Clear screen
1553
- firstDraw = false;
1554
- }
1555
-
1556
- // Move cursor to home position
1557
- screenBuffer += '\x1B[H';
1558
-
1559
- // Stats
1560
- const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
1561
- const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
1562
-
1563
- // Latency formatting
1564
- const latencyMs = latency > 0 ? latency : 0;
1565
- const latencyStr = `${latencyMs}ms`;
1566
- const latencyColor = latencyMs < 100 ? chalk.green : (latencyMs < 300 ? chalk.yellow : chalk.red);
1567
-
1568
- // Current date
1569
- const now = new Date();
1570
- const dateStr = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
1571
-
1572
- // Get package version
1573
- const version = require('../../package.json').version;
1574
-
1575
- // Fixed width = 96 inner chars
1576
- const W = 96;
1577
- const TOP = '\u2554' + '\u2550'.repeat(W) + '\u2557';
1578
- const MID = '\u2560' + '\u2550'.repeat(W) + '\u2563';
1579
- const BOT = '\u255A' + '\u2550'.repeat(W) + '\u255D';
1580
- const V = '\u2551';
1581
-
1582
- // Center text helper
1583
- const center = (text, width) => {
1584
- const pad = Math.floor((width - text.length) / 2);
1585
- return ' '.repeat(pad) + text + ' '.repeat(width - pad - text.length);
1586
- };
1587
-
1588
- // Safe padding function
1589
- const safePad = (len) => ' '.repeat(Math.max(0, len));
1590
-
1591
- // Build cell helper
1592
- const buildCell = (label, value, valueColor, width) => {
1593
- const text = ` ${label}: ${valueColor(value)}`;
1594
- const plain = ` ${label}: ${value}`;
1595
- return { text, plain, padded: text + safePad(width - plain.length) };
1596
- };
1597
-
1598
- bufferLine('');
1599
- bufferLine(chalk.cyan(TOP));
1600
- // Logo HEDGEQUANTX
1601
- bufferLine(chalk.cyan(V) + chalk.cyan(' ██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗') + chalk.yellow('██╗ ██╗') + ' ' + chalk.cyan(V));
1602
- bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝') + chalk.yellow('╚██╗██╔╝') + ' ' + chalk.cyan(V));
1603
- bufferLine(chalk.cyan(V) + chalk.cyan(' ███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ') + chalk.yellow(' ╚███╔╝ ') + ' ' + chalk.cyan(V));
1604
- bufferLine(chalk.cyan(V) + chalk.cyan(' ██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ') + chalk.yellow(' ██╔██╗ ') + ' ' + chalk.cyan(V));
1605
- bufferLine(chalk.cyan(V) + chalk.cyan(' ██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ') + chalk.yellow('██╔╝ ██╗') + ' ' + chalk.cyan(V));
1606
- bufferLine(chalk.cyan(V) + chalk.cyan(' ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ') + chalk.yellow('╚═╝ ╚═╝') + ' ' + chalk.cyan(V));
1607
- bufferLine(chalk.cyan(MID));
1608
-
1609
- // Centered subtitle only
1610
- const title2 = 'HQX Ultra-Scalping';
1611
- bufferLine(chalk.cyan(V) + chalk.yellow(center(title2, W)) + chalk.cyan(V));
1612
-
1613
- // Grid layout - 2 columns
1614
- const VS = '\u2502'; // Vertical separator (thin)
1615
- const colL = 48, colR = 47;
1616
-
1617
- // Row 1: Lead Account | Lead Symbol
1618
- const leadName = (lead.displayName || lead.account.accountName || '').substring(0, 30);
1619
- const leadSym = lead.symbol.value || lead.symbol.name || '';
1620
- const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
1621
- const r1c2text = ` Symbol: ${chalk.yellow(leadSym)} Qty: ${chalk.cyan(lead.contracts)}`;
1622
- const r1c2plain = ` Symbol: ${leadSym} Qty: ${lead.contracts}`;
1623
- const r1c2 = r1c2text + safePad(colR - r1c2plain.length);
1624
-
1625
- // Row 2: Follower Account | Follower Symbol
1626
- const followerName = (follower.displayName || follower.account.accountName || '').substring(0, 30);
1627
- const followerSym = follower.symbol.value || follower.symbol.name || '';
1628
- const r2c1 = buildCell('Follower', followerName, chalk.magenta, colL);
1629
- const r2c2text = ` Symbol: ${chalk.yellow(followerSym)} Qty: ${chalk.cyan(follower.contracts)}`;
1630
- const r2c2plain = ` Symbol: ${followerSym} Qty: ${follower.contracts}`;
1631
- const r2c2 = r2c2text + safePad(colR - r2c2plain.length);
1632
-
1633
- // Row 3: Target | Risk
1634
- const r3c1 = buildCell('Target', '$' + dailyTarget.toFixed(2), chalk.green, colL);
1635
- const r3c2 = buildCell('Risk', '$' + maxRisk.toFixed(2), chalk.red, colR);
1636
-
1637
- // Row 4: P&L | Server Status
1638
- const r4c1 = buildCell('P&L', pnlStr, pnlColor, colL);
1639
- const serverStr = hqxConnected ? 'HQX ON' : 'MONITOR';
1640
- const serverColor = hqxConnected ? chalk.green : chalk.yellow;
1641
- const r4c2 = buildCell('Server', serverStr, serverColor, colR);
1642
-
1643
- // Row 5: Signals + Lead Trades | Copied + Errors
1644
- const r5c1text = ` Signals: ${chalk.magenta(stats.signals || 0)} Lead: ${chalk.cyan(stats.leadTrades)}`;
1645
- const r5c1plain = ` Signals: ${stats.signals || 0} Lead: ${stats.leadTrades}`;
1646
- const r5c1 = r5c1text + safePad(colL - r5c1plain.length);
1647
- const r5c2text = ` Copied: ${chalk.green(stats.copiedTrades)} Errors: ${chalk.red(stats.errors)}`;
1648
- const r5c2plain = ` Copied: ${stats.copiedTrades} Errors: ${stats.errors}`;
1649
- const r5c2 = r5c2text + safePad(colR - r5c2plain.length);
1650
-
1651
- // Row 6: Trades + W/L | Latency
1652
- const r6c1text = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
1653
- const r6c1plain = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
1654
- const r6c1 = r6c1text + safePad(colL - r6c1plain.length);
1655
- const r6c2 = buildCell('Latency', latencyStr, latencyColor, colR);
1656
-
1657
- // Grid separators
1658
- const GRID_TOP = '\u2560' + '\u2550'.repeat(colL) + '\u2564' + '\u2550'.repeat(colR) + '\u2563';
1659
- const GRID_MID = '\u2560' + '\u2550'.repeat(colL) + '\u256A' + '\u2550'.repeat(colR) + '\u2563';
1660
- const GRID_BOT = '\u2560' + '\u2550'.repeat(colL) + '\u2567' + '\u2550'.repeat(colR) + '\u2563';
1661
-
1662
- // Print grid
1663
- bufferLine(chalk.cyan(GRID_TOP));
1664
- bufferLine(chalk.cyan(V) + r1c1.padded + chalk.cyan(VS) + r1c2 + chalk.cyan(V));
1665
- bufferLine(chalk.cyan(GRID_MID));
1666
- bufferLine(chalk.cyan(V) + r2c1.padded + chalk.cyan(VS) + r2c2 + chalk.cyan(V));
1667
- bufferLine(chalk.cyan(GRID_MID));
1668
- bufferLine(chalk.cyan(V) + r3c1.padded + chalk.cyan(VS) + r3c2.padded + chalk.cyan(V));
1669
- bufferLine(chalk.cyan(GRID_MID));
1670
- bufferLine(chalk.cyan(V) + r4c1.padded + chalk.cyan(VS) + r4c2.padded + chalk.cyan(V));
1671
- bufferLine(chalk.cyan(GRID_MID));
1672
- bufferLine(chalk.cyan(V) + r5c1 + chalk.cyan(VS) + r5c2 + chalk.cyan(V));
1673
- bufferLine(chalk.cyan(GRID_MID));
1674
- bufferLine(chalk.cyan(V) + r6c1 + chalk.cyan(VS) + r6c2.padded + chalk.cyan(V));
1675
- bufferLine(chalk.cyan(GRID_BOT));
1676
-
1677
- // Activity log header with spinner and centered date
1678
- spinnerFrame = (spinnerFrame + 1) % spinnerChars.length;
1679
- const spinnerChar = spinnerChars[spinnerFrame];
1680
- const actLeft = ` Activity Log ${chalk.yellow(spinnerChar)}`;
1681
- const actLeftPlain = ` Activity Log ${spinnerChar}`;
1682
- const actRight = 'Press X to stop ';
1683
- const dateCentered = `- ${dateStr} -`;
1684
- const leftLen = actLeftPlain.length;
1685
- const rightLen = actRight.length;
1686
- const midSpace = Math.max(0, W - leftLen - rightLen);
1687
- const datePad = Math.max(0, Math.floor((midSpace - dateCentered.length) / 2));
1688
- const remainingPad = Math.max(0, midSpace - datePad - dateCentered.length);
1689
- const dateSection = ' '.repeat(datePad) + chalk.cyan(dateCentered) + ' '.repeat(remainingPad);
1690
- bufferLine(chalk.cyan(V) + chalk.white(actLeft) + dateSection + chalk.yellow(actRight) + chalk.cyan(V));
1691
- bufferLine(chalk.cyan(MID));
1692
-
1693
- // Helper to strip ANSI codes for length calculation
1694
- const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, '');
1695
-
1696
- // Helper to truncate and pad text to exact width W
1697
- const fitToWidth = (text, width) => {
1698
- const plainText = stripAnsi(text);
1699
- if (plainText.length > width) {
1700
- let count = 0;
1701
- let cutIndex = 0;
1702
- for (let i = 0; i < text.length && count < width - 3; i++) {
1703
- if (text[i] === '\x1B') {
1704
- while (i < text.length && text[i] !== 'm') i++;
1705
- } else {
1706
- count++;
1707
- cutIndex = i + 1;
1708
- }
1709
- }
1710
- return text.substring(0, cutIndex) + '...';
1711
- }
1712
- return text + ' '.repeat(width - plainText.length);
1713
- };
1714
-
1715
- // Logs inside the rectangle - newest first, max 30 lines
1716
- const MAX_VISIBLE_LOGS = 50;
1717
-
1718
- if (logs.length === 0) {
1719
- const emptyLine = ' Waiting for activity...';
1720
- bufferLine(chalk.cyan(V) + chalk.gray(fitToWidth(emptyLine, W)) + chalk.cyan(V));
1721
- for (let i = 0; i < MAX_VISIBLE_LOGS - 1; i++) {
1722
- bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1723
- }
1724
- } else {
1725
- const reversedLogs = [...logs].reverse().slice(0, MAX_VISIBLE_LOGS);
1726
- reversedLogs.forEach(log => {
1727
- const color = typeColors[log.type] || chalk.white;
1728
- const icon = getIcon(log.type);
1729
- const logContent = ` [${log.timestamp}] ${icon} ${log.message}`;
1730
- const fitted = fitToWidth(logContent, W);
1731
- bufferLine(chalk.cyan(V) + color(fitted) + chalk.cyan(V));
1732
- });
1733
- for (let i = reversedLogs.length; i < MAX_VISIBLE_LOGS; i++) {
1734
- bufferLine(chalk.cyan(V) + ' '.repeat(W) + chalk.cyan(V));
1735
- }
1736
- }
1737
-
1738
- // Bottom border
1739
- bufferLine(chalk.cyan(BOT));
1740
-
1741
- // Write entire buffer atomically
1742
- process.stdout.write(screenBuffer);
1743
- isDrawing = false;
1744
- };
1745
-
1746
- // Spinner interval for animation
1747
- const spinnerInterval = setInterval(() => {
1748
- if (isRunning) displayUI();
1749
- }, 250);
1750
-
1751
- addLog('info', 'Copy trading initialized');
1752
- addLog('info', 'Connecting to HQX Server...');
1753
- displayUI();
1754
-
1755
- // Authenticate with HQX Server
1756
- displayUI();
1757
-
1758
- try {
1759
- const authResult = await hqxServer.authenticate(
1760
- lead.account.accountId.toString(),
1761
- lead.account.propfirm || 'projectx'
1762
- );
1763
-
1764
- if (authResult.success) {
1765
- const connectResult = await hqxServer.connect();
1766
- if (connectResult.success) {
1767
- hqxConnected = true;
1768
- addLog('success', 'Connected to HQX Server');
1769
- } else {
1770
- addLog('warning', 'HQX Server unavailable - Running in monitor mode');
1771
- }
1772
- } else {
1773
- addLog('warning', 'HQX Auth failed - Running in monitor mode');
1774
- }
1775
- } catch (error) {
1776
- addLog('warning', 'HQX Server unavailable - Running in monitor mode');
1777
- }
1778
-
1779
- displayUI();
1780
-
1781
- // Helper function to execute signal on both accounts
1782
- const executeSignalOnBothAccounts = async (signal) => {
1783
- const side = signal.side === 'long' ? 0 : 1; // 0=Buy, 1=Sell
1784
- const sideStr = signal.side === 'long' ? 'LONG' : 'SHORT';
1785
-
1786
- // Execute on Lead account
1787
- try {
1788
- const leadResult = await lead.service.placeOrder({
1789
- accountId: lead.account.rithmicAccountId || lead.account.accountId,
1790
- symbol: lead.symbol.value,
1791
- exchange: 'CME',
1792
- size: lead.contracts,
1793
- side: side,
1794
- type: 2 // Market
1795
- });
1796
-
1797
- if (leadResult.success) {
1798
- stats.leadTrades++;
1799
- addLog('trade', `Lead: ${sideStr} ${lead.contracts} ${lead.symbol.value} @ MKT`);
1800
- } else {
1801
- throw new Error(leadResult.error || 'Lead order failed');
1802
- }
1803
- } catch (e) {
1804
- stats.errors++;
1805
- addLog('error', `Lead order failed: ${e.message}`);
1806
- return; // Don't copy if lead fails
1807
- }
1808
-
1809
- // Execute on Follower account (copy)
1810
- try {
1811
- const followerResult = await follower.service.placeOrder({
1812
- accountId: follower.account.rithmicAccountId || follower.account.accountId,
1813
- symbol: follower.symbol.value,
1814
- exchange: 'CME',
1815
- size: follower.contracts,
1816
- side: side,
1817
- type: 2 // Market
1818
- });
1819
-
1820
- if (followerResult.success) {
1821
- stats.copiedTrades++;
1822
- addLog('copy', `Follower: ${sideStr} ${follower.contracts} ${follower.symbol.value} @ MKT`);
1823
- } else {
1824
- throw new Error(followerResult.error || 'Follower order failed');
1825
- }
1826
- } catch (e) {
1827
- stats.errors++;
1828
- addLog('error', `Follower order failed: ${e.message}`);
1829
- }
1830
- };
1831
-
1832
- // Helper function to close positions on both accounts
1833
- const closePositionsOnBothAccounts = async (reason) => {
1834
- // Close Lead position
1835
- try {
1836
- await lead.service.closePosition(
1837
- lead.account.rithmicAccountId || lead.account.accountId,
1838
- lead.symbol.value
1839
- );
1840
- addLog('trade', `Lead: Position closed (${reason})`);
1841
- } catch (e) {
1842
- // Position may already be closed
1843
- }
1844
-
1845
- // Close Follower position
1846
- try {
1847
- await follower.service.closePosition(
1848
- follower.account.rithmicAccountId || follower.account.accountId,
1849
- follower.symbol.value
1850
- );
1851
- addLog('copy', `Follower: Position closed (${reason})`);
1852
- } catch (e) {
1853
- // Position may already be closed
1854
- }
1855
- };
1856
-
1857
- // Setup HQX Server event handlers (attach before connection, check hqxConnected inside)
1858
- hqxServer.on('latency', (data) => {
1859
- latency = data.latency || 0;
1860
- });
1861
-
1862
- hqxServer.on('log', (data) => {
1863
- addLog(data.type || 'info', data.message);
1864
- });
1865
-
1866
- hqxServer.on('signal', async (data) => {
1867
- stats.signals = (stats.signals || 0) + 1;
1868
- const side = data.side === 'long' ? 'BUY' : 'SELL';
1869
- addLog('signal', `${side} Signal @ ${data.entry?.toFixed(2) || 'N/A'} | SL: ${data.stop?.toFixed(2) || 'N/A'} | TP: ${data.target?.toFixed(2) || 'N/A'}`);
1870
-
1871
- // Execute on both accounts
1872
- if (hqxConnected) {
1873
- await executeSignalOnBothAccounts(data);
1874
- }
1875
- displayUI();
1876
- });
1877
-
1878
- hqxServer.on('trade', async (data) => {
1879
- stats.pnl += data.pnl || 0;
1880
- if (data.pnl > 0) {
1881
- stats.wins = (stats.wins || 0) + 1;
1882
- addLog('trade', `Closed +$${data.pnl.toFixed(2)} (${data.reason || 'take_profit'})`);
1883
- } else {
1884
- stats.losses = (stats.losses || 0) + 1;
1885
- addLog('loss', `Closed -$${Math.abs(data.pnl).toFixed(2)} (${data.reason || 'stop_loss'})`);
1886
- }
1887
- stats.trades = (stats.trades || 0) + 1;
1888
-
1889
- // Print updated stats like One Account
1890
- const statsType = stats.pnl >= 0 ? 'info' : 'loss';
1891
- addLog(statsType, `Stats: Trades: ${stats.trades} | Wins: ${stats.wins || 0} | P&L: $${stats.pnl.toFixed(2)}`);
1892
-
1893
- // Check daily target
1894
- if (stats.pnl >= dailyTarget) {
1895
- stopReason = 'target';
1896
- addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
1897
- isRunning = false;
1898
- if (hqxConnected) hqxServer.stopAlgo();
1899
- await closePositionsOnBothAccounts('target');
1900
- }
1901
-
1902
- // Check max risk
1903
- if (stats.pnl <= -maxRisk) {
1904
- stopReason = 'risk';
1905
- addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
1906
- isRunning = false;
1907
- if (hqxConnected) hqxServer.stopAlgo();
1908
- await closePositionsOnBothAccounts('risk');
1909
- }
1910
-
1911
- displayUI();
1912
- });
1913
-
1914
- hqxServer.on('stats', (data) => {
1915
- const realizedPnl = data.pnl || 0;
1916
- const unrealizedPnl = data.position?.pnl || 0;
1917
- stats.pnl = realizedPnl + unrealizedPnl;
1918
- stats.trades = data.trades || stats.trades;
1919
- stats.wins = data.wins || stats.wins;
1920
- stats.losses = data.losses || stats.losses;
1921
- });
1922
-
1923
- hqxServer.on('error', (data) => {
1924
- const errorMsg = data.message || 'Unknown error';
1925
- addLog('error', errorMsg);
1926
-
1927
- // If algo failed to start, switch to monitor mode
1928
- if (errorMsg.includes('Failed to start') || errorMsg.includes('WebSocket failed') || errorMsg.includes('Échec')) {
1929
- if (hqxConnected) {
1930
- hqxConnected = false;
1931
- addLog('warning', 'Switching to Monitor Mode (watching Lead positions)');
1932
- displayUI();
1933
- }
1934
- }
1935
- });
1936
-
1937
- hqxServer.on('disconnected', () => {
1938
- hqxConnected = false;
1939
- if (!stopReason) {
1940
- addLog('warning', 'HQX Server disconnected - Switching to Monitor Mode');
1941
- }
1942
- });
1943
-
1944
- // Start algo if connected
1945
- if (hqxConnected) {
1946
-
1947
- // Start the Ultra-Scalping algo
1948
- addLog('info', 'Starting HQX Ultra-Scalping...');
1949
- addLog('info', `Target: $${dailyTarget.toFixed(2)} | Risk: $${maxRisk.toFixed(2)}`);
1950
-
1951
- const propfirmToken = lead.service.getToken ? lead.service.getToken() : null;
1952
- const propfirmId = lead.service.getPropfirm ? lead.service.getPropfirm() : (lead.account.propfirm || 'topstep');
1953
-
1954
- // Get Rithmic credentials if this is a Rithmic account
1955
- let rithmicCredentials = null;
1956
- if (lead.service.getRithmicCredentials) {
1957
- rithmicCredentials = lead.service.getRithmicCredentials();
1958
- } else if (lead.account.rithmicUserId && lead.account.rithmicPassword) {
1959
- rithmicCredentials = {
1960
- userId: lead.account.rithmicUserId,
1961
- password: lead.account.rithmicPassword,
1962
- systemName: lead.account.rithmicSystem || 'Apex',
1963
- gateway: lead.account.rithmicGateway || 'wss://rprotocol.rithmic.com:443'
1964
- };
1965
- }
1966
-
1967
- hqxServer.startAlgo({
1968
- accountId: lead.account.accountId,
1969
- contractId: lead.symbol.id || lead.symbol.contractId,
1970
- symbol: lead.symbol.value,
1971
- contracts: lead.contracts,
1972
- dailyTarget: dailyTarget,
1973
- maxRisk: maxRisk,
1974
- propfirm: propfirmId,
1975
- propfirmToken: propfirmToken,
1976
- rithmicCredentials: rithmicCredentials,
1977
- copyTrading: true, // Flag for copy trading mode
1978
- followerSymbol: follower.symbol.value,
1979
- followerContracts: follower.contracts
1980
- });
1981
-
1982
- displayUI();
1983
- }
1984
-
1985
- // Position monitoring loop (for P&L tracking and fallback copy)
1986
- const monitorInterval = setInterval(async () => {
1987
- if (!isRunning) return;
1988
-
1989
- try {
1990
- // Get positions from both accounts for P&L tracking
1991
- const [leadPositions, followerPositions] = await Promise.all([
1992
- lead.service.getPositions(lead.account.rithmicAccountId || lead.account.accountId),
1993
- follower.service.getPositions(follower.account.rithmicAccountId || follower.account.accountId)
1994
- ]);
1995
-
1996
- // Calculate combined P&L
1997
- let leadPnl = 0, followerPnl = 0;
1998
-
1999
- if (leadPositions.success && leadPositions.positions) {
2000
- const leadPos = leadPositions.positions.find(p =>
2001
- p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
2002
- );
2003
- if (leadPos && typeof leadPos.unrealizedPnl === 'number') {
2004
- leadPnl = leadPos.unrealizedPnl;
2005
- }
2006
- }
2007
-
2008
- if (followerPositions.success && followerPositions.positions) {
2009
- const followerPos = followerPositions.positions.find(p =>
2010
- p.symbol === follower.symbol.value || p.symbol?.includes(follower.symbol.searchText)
2011
- );
2012
- if (followerPos && typeof followerPos.unrealizedPnl === 'number') {
2013
- followerPnl = followerPos.unrealizedPnl;
2014
- }
2015
- }
2016
-
2017
- // Update combined P&L (or just follower if HQX handles lead)
2018
- stats.pnl = leadPnl + followerPnl;
2019
-
2020
- // Check if daily target reached
2021
- if (stats.pnl >= dailyTarget && !stopReason) {
2022
- isRunning = false;
2023
- stopReason = 'target';
2024
- addLog('success', `Daily target reached! +$${stats.pnl.toFixed(2)}`);
2025
-
2026
- if (hqxConnected) hqxServer.stopAlgo();
2027
- await closePositionsOnBothAccounts('target');
2028
- displayUI();
2029
- return;
2030
- }
2031
-
2032
- // Check if max risk reached
2033
- if (stats.pnl <= -maxRisk && !stopReason) {
2034
- isRunning = false;
2035
- stopReason = 'risk';
2036
- addLog('error', `Max risk reached! -$${Math.abs(stats.pnl).toFixed(2)}`);
2037
-
2038
- if (hqxConnected) hqxServer.stopAlgo();
2039
- await closePositionsOnBothAccounts('risk');
2040
- displayUI();
2041
- return;
2042
- }
2043
-
2044
- // Fallback: If HQX not connected, monitor lead and copy manually
2045
- if (!hqxConnected) {
2046
- let currentLeadPosition = null;
2047
- if (leadPositions.success && leadPositions.positions) {
2048
- currentLeadPosition = leadPositions.positions.find(p =>
2049
- p.symbol === lead.symbol.value || p.symbol?.includes(lead.symbol.searchText)
2050
- );
2051
- }
2052
-
2053
- const hadPosition = lastLeadPosition && lastLeadPosition.quantity !== 0;
2054
- const hasPosition = currentLeadPosition && currentLeadPosition.quantity !== 0;
2055
-
2056
- if (!hadPosition && hasPosition) {
2057
- stats.leadTrades++;
2058
- const side = currentLeadPosition.quantity > 0 ? 'LONG' : 'SHORT';
2059
- addLog('trade', `Lead opened ${side} ${Math.abs(currentLeadPosition.quantity)} @ ${currentLeadPosition.averagePrice || 'MKT'}`);
2060
- await copyTradeToFollower(follower, currentLeadPosition, 'open');
2061
- stats.copiedTrades++;
2062
- displayUI();
2063
-
2064
- } else if (hadPosition && !hasPosition) {
2065
- addLog('trade', `Lead closed position`);
2066
- await copyTradeToFollower(follower, lastLeadPosition, 'close');
2067
- stats.copiedTrades++;
2068
- displayUI();
2069
-
2070
- } else if (hadPosition && hasPosition && lastLeadPosition.quantity !== currentLeadPosition.quantity) {
2071
- const diff = currentLeadPosition.quantity - lastLeadPosition.quantity;
2072
- const action = diff > 0 ? 'added' : 'reduced';
2073
- addLog('trade', `Lead ${action} ${Math.abs(diff)} contracts`);
2074
- await copyTradeToFollower(follower, { ...currentLeadPosition, quantityChange: diff }, 'adjust');
2075
- stats.copiedTrades++;
2076
- displayUI();
2077
- }
2078
-
2079
- lastLeadPosition = currentLeadPosition ? { ...currentLeadPosition } : null;
2080
- }
2081
-
2082
- } catch (error) {
2083
- stats.errors++;
2084
- addLog('error', `Monitor error: ${error.message}`);
2085
- displayUI();
2086
- }
2087
- }, 2000); // Check every 2 seconds
2088
-
2089
- // Wait for X key OR auto-stop (target/risk reached)
2090
- await new Promise((resolve) => {
2091
- // Check for auto-stop every 500ms
2092
- const checkInterval = setInterval(() => {
2093
- if (!isRunning || stopReason) {
2094
- clearInterval(checkInterval);
2095
- clearInterval(monitorInterval);
2096
- if (process.stdin.isTTY && process.stdin.isRaw) {
2097
- process.stdin.setRawMode(false);
2098
- }
2099
- resolve();
2100
- }
2101
- }, 500);
2102
-
2103
- // Also listen for X key
2104
- if (process.stdin.isTTY) {
2105
- try {
2106
- readline.emitKeypressEvents(process.stdin);
2107
- process.stdin.setRawMode(true);
2108
- process.stdin.resume();
2109
-
2110
- const onKeypress = (str, key) => {
2111
- if (!key) return;
2112
- const keyName = key.name?.toLowerCase();
2113
- if (keyName === 'x' || (key.ctrl && keyName === 'c')) {
2114
- clearInterval(checkInterval);
2115
- clearInterval(monitorInterval);
2116
- process.stdin.setRawMode(false);
2117
- process.stdin.removeListener('keypress', onKeypress);
2118
- resolve();
2119
- }
2120
- };
2121
-
2122
- process.stdin.on('keypress', onKeypress);
2123
- } catch (e) {
2124
- // Fallback: just wait for auto-stop
2125
- }
2126
- }
2127
- });
2128
-
2129
- // Cleanup
2130
- clearInterval(spinnerInterval);
2131
- isRunning = false;
2132
-
2133
- // Stop HQX Server and close positions
2134
- if (hqxConnected) {
2135
- hqxServer.stopAlgo();
2136
- hqxServer.disconnect();
2137
- }
2138
-
2139
- // Cancel all pending orders and close positions on both accounts
2140
- try {
2141
- await Promise.all([
2142
- lead.service.cancelAllOrders(lead.account.rithmicAccountId || lead.account.accountId),
2143
- follower.service.cancelAllOrders(follower.account.rithmicAccountId || follower.account.accountId)
2144
- ]);
2145
- } catch (e) {
2146
- // Ignore cancel errors
2147
- }
2148
-
2149
- if (!stopReason) {
2150
- // User stopped manually, close positions
2151
- await closePositionsOnBothAccounts('user_stop');
2152
- }
2153
-
2154
- // Restore stdin to normal mode
2155
- try {
2156
- if (process.stdin.isTTY) {
2157
- process.stdin.setRawMode(false);
2158
- }
2159
- process.stdin.removeAllListeners('keypress');
2160
- } catch (e) {
2161
- // Ignore stdin restoration errors
2162
- }
2163
-
2164
- // Exit alternate screen buffer and show cursor
2165
- process.stdout.write('\x1B[?1049l');
2166
- process.stdout.write('\x1B[?25h');
2167
-
2168
- console.log();
2169
- if (stopReason === 'target') {
2170
- console.log(chalk.green.bold(' [OK] Daily target reached! Copy trading stopped.'));
2171
- } else if (stopReason === 'risk') {
2172
- console.log(chalk.red.bold(' [X] Max risk reached! Copy trading stopped.'));
2173
- } else {
2174
- console.log(chalk.yellow(' [OK] Copy trading stopped by user'));
2175
- }
2176
- console.log();
2177
- console.log(chalk.gray(getSeparator()));
2178
- console.log(chalk.white.bold(' Session Summary'));
2179
- console.log(chalk.gray(getSeparator()));
2180
- console.log(chalk.white(` Daily Target: ${chalk.green('$' + dailyTarget.toFixed(2))}`));
2181
- console.log(chalk.white(` Max Risk: ${chalk.red('$' + maxRisk.toFixed(2))}`));
2182
- console.log(chalk.white(` Final P&L: ${stats.pnl >= 0 ? chalk.green('+$' + stats.pnl.toFixed(2)) : chalk.red('-$' + Math.abs(stats.pnl).toFixed(2))}`));
2183
- console.log(chalk.gray(getSeparator()));
2184
- console.log(chalk.white(` Lead Trades: ${chalk.cyan(stats.leadTrades)}`));
2185
- console.log(chalk.white(` Copied Trades: ${chalk.green(stats.copiedTrades)}`));
2186
- console.log(chalk.white(` Errors: ${chalk.red(stats.errors)}`));
2187
- console.log(chalk.gray(getSeparator()));
2188
- console.log();
2189
-
2190
- await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
2191
- };
2192
-
2193
- /**
2194
- * Copy trade to follower account
2195
- */
2196
- const copyTradeToFollower = async (follower, position, action) => {
2197
- try {
2198
- const service = follower.service;
2199
- const accountId = follower.account.rithmicAccountId || follower.account.accountId;
2200
-
2201
- if (action === 'open') {
2202
- // Open new position
2203
- const side = position.quantity > 0 ? 0 : 1; // 0=Buy, 1=Sell
2204
- const result = await service.placeOrder({
2205
- accountId: accountId,
2206
- symbol: follower.symbol.value,
2207
- exchange: 'CME',
2208
- size: follower.contracts,
2209
- side: side,
2210
- type: 2 // Market
2211
- });
2212
-
2213
- if (result.success) {
2214
- console.log(chalk.green(` [+] Follower: Opened ${side === 0 ? 'LONG' : 'SHORT'} ${follower.contracts} ${follower.symbol.value}`));
2215
- } else {
2216
- throw new Error(result.error || 'Order failed');
2217
- }
2218
-
2219
- } else if (action === 'close') {
2220
- // Close position
2221
- const result = await service.closePosition(accountId, follower.symbol.value);
2222
-
2223
- if (result.success) {
2224
- console.log(chalk.green(` [+] Follower: Closed position`));
2225
- } else {
2226
- throw new Error(result.error || 'Close failed');
2227
- }
2228
-
2229
- } else if (action === 'adjust') {
2230
- // Adjust position size
2231
- const side = position.quantityChange > 0 ? 0 : 1;
2232
- const size = Math.abs(position.quantityChange);
2233
- const adjustedSize = Math.round(size * (follower.contracts / Math.abs(position.quantity - position.quantityChange)));
2234
-
2235
- if (adjustedSize > 0) {
2236
- const result = await service.placeOrder({
2237
- accountId: accountId,
2238
- symbol: follower.symbol.value,
2239
- exchange: 'CME',
2240
- size: adjustedSize,
2241
- side: side,
2242
- type: 2
2243
- });
2244
-
2245
- if (result.success) {
2246
- console.log(chalk.green(` [+] Follower: Adjusted by ${side === 0 ? '+' : '-'}${adjustedSize}`));
2247
- }
2248
- }
2249
- }
2250
-
2251
- } catch (error) {
2252
- console.log(chalk.red(` [X] Follower error: ${error.message}`));
2253
- }
2254
- };
2255
-
2256
- /**
2257
- * Wait for X key to stop
2258
- */
2259
- const waitForStopKey = () => {
2260
- return new Promise((resolve) => {
2261
- // Enable raw mode to capture keypresses
2262
- if (process.stdin.isTTY) {
2263
- readline.emitKeypressEvents(process.stdin);
2264
- process.stdin.setRawMode(true);
2265
-
2266
- const onKeypress = (str, key) => {
2267
- if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
2268
- process.stdin.setRawMode(false);
2269
- process.stdin.removeListener('keypress', onKeypress);
2270
- resolve();
2271
- }
2272
- };
2273
-
2274
- process.stdin.on('keypress', onKeypress);
2275
- } else {
2276
- // Fallback: wait 30 seconds in non-TTY mode
2277
- setTimeout(resolve, 30000);
2278
- }
2279
- });
2280
- };
6
+ const { algoTradingMenu } = require('./algo/index');
2281
7
 
2282
8
  module.exports = { algoTradingMenu };