hedgequantx 1.1.1

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/bin/cli.js ADDED
@@ -0,0 +1,2096 @@
1
+ #!/usr/bin/env node
2
+
3
+ const chalk = require('chalk');
4
+ const figlet = require('figlet');
5
+ const inquirer = require('inquirer');
6
+ const ora = require('ora');
7
+ const asciichart = require('asciichart');
8
+ const { execSync } = require('child_process');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const { program } = require('commander');
13
+
14
+ // Import modular services
15
+ const { ProjectXService, connections: connMgr, storage: sessionStorage } = require('../src/services');
16
+ const { HQXServerService } = require('../src/services/hqx-server');
17
+ const { PROPFIRMS, PROPFIRM_CHOICES, ACCOUNT_STATUS, ACCOUNT_TYPE, ORDER_STATUS, ORDER_TYPE, ORDER_SIDE, FUTURES_SYMBOLS } = require('../src/config');
18
+ const { getDevice, getSeparator, getLogoWidth, visibleLength, centerText, padText, drawBoxHeader, drawBoxFooter, drawBoxRow, drawBoxSeparator, getColWidths, draw2ColHeader, draw2ColRow, draw2ColRowRaw, draw2ColSeparator, fmtRow, printLogo } = require('../src/ui');
19
+ const { showStats } = require('../src/pages');
20
+
21
+ // Alias for connections module
22
+ const connections = connMgr;
23
+
24
+ // Session courante (pour compatibilité)
25
+ let currentService = null;
26
+
27
+ /**
28
+ * Format text for current device width
29
+ */
30
+ const formatForDevice = (text, indent = 2) => {
31
+ const device = getDevice();
32
+ const maxWidth = device.maxContentWidth - indent;
33
+
34
+ if (text.length <= maxWidth) {
35
+ return ' '.repeat(indent) + text;
36
+ }
37
+
38
+ // Word wrap for mobile
39
+ const words = text.split(' ');
40
+ const lines = [];
41
+ let currentLine = '';
42
+
43
+ for (const word of words) {
44
+ if ((currentLine + ' ' + word).trim().length <= maxWidth) {
45
+ currentLine = (currentLine + ' ' + word).trim();
46
+ } else {
47
+ if (currentLine) lines.push(' '.repeat(indent) + currentLine);
48
+ currentLine = word;
49
+ }
50
+ }
51
+ if (currentLine) lines.push(' '.repeat(indent) + currentLine);
52
+
53
+ return lines.join('\n');
54
+ };
55
+
56
+ /**
57
+ * Show device info (for debugging)
58
+ */
59
+ const showDeviceInfo = () => {
60
+ const device = getDevice();
61
+ console.log();
62
+ console.log(chalk.gray(getSeparator()));
63
+ console.log(chalk.white(` ${device.deviceIcon} Device: ${chalk.cyan(device.deviceType.toUpperCase())}`));
64
+ console.log(chalk.white(` 📐 Size: ${chalk.cyan(device.width + 'x' + device.height)}`));
65
+ console.log(chalk.white(` [>] Platform: ${chalk.cyan(device.platform)}`));
66
+ if (device.isRemote) {
67
+ console.log(chalk.white(` 🌐 Remote: ${chalk.yellow('SSH Connection')}`));
68
+ }
69
+ console.log(chalk.gray(getSeparator()));
70
+ console.log();
71
+ };
72
+
73
+ // Liste des PropFirms ProjectX
74
+ const projectXPropfirms = [
75
+ { name: 'Topstep', value: 'topstep' },
76
+ { name: 'Alpha Futures', value: 'alpha_futures' },
77
+ { name: 'TickTickTrader', value: 'tickticktrader' },
78
+ { name: 'Bulenox', value: 'bulenox' },
79
+ { name: 'TradeDay', value: 'tradeday' },
80
+ { name: 'Blusky', value: 'blusky' },
81
+ { name: 'Goat Futures', value: 'goat_futures' },
82
+ { name: 'The Futures Desk', value: 'futures_desk' },
83
+ { name: 'DayTraders', value: 'daytraders' },
84
+ { name: 'E8 Futures', value: 'e8_futures' },
85
+ { name: 'Blue Guardian Futures', value: 'blue_guardian' },
86
+ { name: 'FuturesElite', value: 'futures_elite' },
87
+ { name: 'FXIFY', value: 'fxify' },
88
+ { name: 'Hola Prime', value: 'hola_prime' },
89
+ { name: 'Top One Futures', value: 'top_one_futures' },
90
+ { name: 'Funding Futures', value: 'funding_futures' },
91
+ { name: 'TX3 Funding', value: 'tx3_funding' },
92
+ { name: 'Lucid Trading', value: 'lucid_trading' },
93
+ { name: 'Tradeify', value: 'tradeify' },
94
+ { name: 'Earn2Trade (Coming Soon!)', value: 'earn2trade', disabled: 'Coming Soon' },
95
+ ];
96
+
97
+ // Banner - Responsive for all devices
98
+ const banner = async () => {
99
+ // Clear screen properly with ANSI escape codes
100
+ process.stdout.write('\x1b[2J\x1b[0f');
101
+ const device = getDevice();
102
+
103
+ // Get stats if connected
104
+ let statsInfo = null;
105
+ if (connections.count() > 0) {
106
+ try {
107
+ const allAccounts = await connections.getAllAccounts();
108
+ let totalBalance = 0;
109
+ let totalStartingBalance = 0;
110
+ let totalPnl = 0;
111
+
112
+ allAccounts.forEach(account => {
113
+ totalBalance += account.balance || 0;
114
+ totalStartingBalance += account.startingBalance || 0;
115
+ totalPnl += account.profitAndLoss || 0;
116
+ });
117
+
118
+ // Use API P&L if available, otherwise calculate
119
+ const pnl = totalPnl !== 0 ? totalPnl : (totalBalance - totalStartingBalance);
120
+ const pnlPercent = totalStartingBalance > 0 ? ((pnl / totalStartingBalance) * 100).toFixed(1) : '0.0';
121
+
122
+ statsInfo = {
123
+ connections: connections.count(),
124
+ accounts: allAccounts.length,
125
+ balance: totalBalance,
126
+ pnl: pnl,
127
+ pnlPercent: pnlPercent
128
+ };
129
+ } catch (e) {
130
+ // Ignore errors
131
+ }
132
+ }
133
+
134
+ if (device.isMobile) {
135
+ // 📱 MOBILE: Adaptive to screen width
136
+ const width = device.width - 2; // Leave margin
137
+ const innerWidth = width - 4; // Account for borders and spaces
138
+
139
+ const centerText = (text, w) => {
140
+ const padding = Math.max(0, w - text.length);
141
+ const left = Math.floor(padding / 2);
142
+ const right = padding - left;
143
+ return ' '.repeat(left) + text + ' '.repeat(right);
144
+ };
145
+
146
+ console.log();
147
+ console.log(chalk.cyan.bold(' ┌' + '─'.repeat(innerWidth + 2) + '┐'));
148
+ console.log(chalk.cyan.bold(' │' + centerText('HEDGEQUANTX', innerWidth + 2) + '│'));
149
+ console.log(chalk.cyan.bold(' │' + centerText('═'.repeat(Math.min(11, innerWidth)), innerWidth + 2) + '│'));
150
+
151
+ if (statsInfo) {
152
+ const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
153
+ const balStr = `$${(statsInfo.balance/1000).toFixed(0)}K`;
154
+ const accStr = `${statsInfo.accounts} acc`;
155
+ const pnlStr = `${statsInfo.pnl >= 0 ? '+' : ''}${statsInfo.pnlPercent}%`;
156
+ const infoText = `${balStr} | ${accStr} | ${pnlStr}`;
157
+ console.log(chalk.cyan.bold(' │') + centerText(infoText, innerWidth + 2) + chalk.cyan.bold('│'));
158
+ } else {
159
+ console.log(chalk.cyan.bold(' │') + chalk.yellow.bold(centerText('Algo Trading', innerWidth + 2)) + chalk.cyan.bold('│'));
160
+ }
161
+
162
+ console.log(chalk.cyan.bold(' └' + '─'.repeat(innerWidth + 2) + '┘'));
163
+ console.log();
164
+
165
+ } else if (device.isTablet) {
166
+ // 📲 TABLET: Medium compact
167
+ const pkg = require('../package.json');
168
+ console.log();
169
+ console.log(
170
+ chalk.cyan(
171
+ figlet.textSync('HQX', {
172
+ font: 'Small',
173
+ horizontalLayout: 'fitted'
174
+ })
175
+ )
176
+ );
177
+ console.log(chalk.gray(getSeparator()));
178
+ if (statsInfo) {
179
+ const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
180
+ console.log(
181
+ chalk.white(` Conn: ${chalk.cyan(statsInfo.connections)}`) +
182
+ chalk.gray(' | ') +
183
+ chalk.white(`Acc: ${chalk.cyan(statsInfo.accounts)}`) +
184
+ chalk.gray(' | ') +
185
+ chalk.white(`Bal: ${chalk.green('$' + statsInfo.balance.toLocaleString())}`) +
186
+ chalk.gray(' | ') +
187
+ chalk.white(`P&L: ${pnlColor((statsInfo.pnl >= 0 ? '+' : '') + '$' + statsInfo.pnl.toLocaleString())}`)
188
+ );
189
+ } else {
190
+ console.log(chalk.yellow.bold(' Prop Futures Algo Trading') + chalk.gray(` v${pkg.version}`));
191
+ }
192
+ console.log(chalk.gray(getSeparator()));
193
+ console.log();
194
+
195
+ } else {
196
+ // 💻 DESKTOP & LARGE DESKTOP
197
+ const logoText = figlet.textSync('HEDGEQUANTX', {
198
+ font: 'ANSI Shadow',
199
+ horizontalLayout: 'default',
200
+ verticalLayout: 'default'
201
+ });
202
+
203
+ // Remove trailing empty lines from logo
204
+ const logoLines = logoText.split('\n').filter(line => line.trim().length > 0);
205
+
206
+ // Get max width of all logo lines
207
+ const maxLogoWidth = Math.max(...logoLines.map(line => line.length));
208
+
209
+ // Box width = logo width + 2 for borders
210
+ const boxWidth = maxLogoWidth + 2;
211
+
212
+ // Draw top border
213
+ console.log(chalk.cyan('╔' + '═'.repeat(maxLogoWidth) + '╗'));
214
+
215
+ // Draw logo lines inside box - pad each line to max width
216
+ logoLines.forEach(line => {
217
+ const paddedLine = line.padEnd(maxLogoWidth);
218
+ console.log(chalk.cyan('║') + chalk.cyan(paddedLine) + chalk.cyan('║'));
219
+ });
220
+
221
+ // Inner width (content area between ║ and ║)
222
+ const innerWidth = maxLogoWidth;
223
+
224
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
225
+
226
+ // Always show tagline centered
227
+ const tagline = 'Prop Futures Algo Trading';
228
+ const pkg = require('../package.json');
229
+ const version = 'v' + pkg.version;
230
+ const taglineText = chalk.yellow.bold(tagline) + ' ' + chalk.gray(version);
231
+ const taglineLen = tagline.length + 2 + version.length;
232
+ const taglineLeftPad = Math.floor((innerWidth - taglineLen) / 2);
233
+ const taglineRightPad = innerWidth - taglineLen - taglineLeftPad;
234
+ console.log(chalk.cyan('║') + ' '.repeat(taglineLeftPad) + taglineText + ' '.repeat(taglineRightPad) + chalk.cyan('║'));
235
+
236
+ // Show stats if connected
237
+ if (statsInfo) {
238
+ // Separator between tagline and stats
239
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
240
+
241
+ const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
242
+ const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
243
+
244
+ // Build info line
245
+ const connStr = `Connections: ${statsInfo.connections}`;
246
+ const accStr = `Accounts: ${statsInfo.accounts}`;
247
+ const balStr = `Balance: $${statsInfo.balance.toLocaleString()}`;
248
+ const pnlStr = `P&L: ${pnlSign}$${statsInfo.pnl.toLocaleString()} (${statsInfo.pnlPercent}%)`;
249
+
250
+ const statsLen = connStr.length + 4 + accStr.length + 4 + balStr.length + 4 + pnlStr.length;
251
+ const statsLeftPad = Math.floor((innerWidth - statsLen) / 2);
252
+ const statsRightPad = innerWidth - statsLen - statsLeftPad;
253
+
254
+ console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
255
+ chalk.white(connStr) + ' ' +
256
+ chalk.white(accStr) + ' ' +
257
+ chalk.green(balStr) + ' ' +
258
+ pnlColor(pnlStr) + ' '.repeat(statsRightPad) + chalk.cyan('║')
259
+ );
260
+ }
261
+
262
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
263
+ console.log();
264
+ }
265
+ };
266
+
267
+ // Menu principal - Choix de connexion (Responsive)
268
+ const mainMenu = async () => {
269
+ const device = getDevice();
270
+
271
+ const { connection } = await inquirer.prompt([
272
+ {
273
+ type: 'list',
274
+ name: 'connection',
275
+ message: device.isMobile
276
+ ? chalk.white.bold('Connection:')
277
+ : chalk.white.bold('Choose Your Connection:'),
278
+ choices: [
279
+ { name: chalk.cyan('ProjectX'), value: 'projectx' },
280
+ { name: chalk.gray('Rithmic (Soon)'), value: 'rithmic', disabled: device.isMobile ? '' : 'Coming Soon' },
281
+ { name: chalk.gray('Tradovate (Soon)'), value: 'tradovate', disabled: device.isMobile ? '' : 'Coming Soon' },
282
+ new inquirer.Separator(),
283
+ { name: chalk.red('Exit'), value: 'exit' }
284
+ ],
285
+ pageSize: device.menuPageSize,
286
+ loop: false
287
+ }
288
+ ]);
289
+
290
+ return connection;
291
+ };
292
+
293
+ // Menu PropFirm pour ProjectX (Responsive)
294
+ const projectXMenu = async () => {
295
+ const device = getDevice();
296
+ console.log();
297
+
298
+ // Sur mobile, afficher des noms plus courts
299
+ const formatPropfirmName = (name) => {
300
+ if (device.isMobile && name.length > 15) {
301
+ // Raccourcir les noms longs sur mobile
302
+ const shortNames = {
303
+ 'TickTickTrader': 'TickTick',
304
+ 'Blue Guardian Futures': 'BlueGuardian',
305
+ 'The Futures Desk': 'FuturesDesk',
306
+ 'Top One Futures': 'TopOne',
307
+ 'Funding Futures': 'FundingFut',
308
+ 'Lucid Trading': 'Lucid',
309
+ 'Earn2Trade (Coming Soon!)': 'Earn2Trade'
310
+ };
311
+ return shortNames[name] || name.substring(0, 12);
312
+ }
313
+ return name;
314
+ };
315
+
316
+ const { propfirm } = await inquirer.prompt([
317
+ {
318
+ type: 'list',
319
+ name: 'propfirm',
320
+ message: device.isMobile
321
+ ? chalk.white.bold('Propfirm:')
322
+ : chalk.white.bold('Choose Your Propfirm:'),
323
+ choices: [
324
+ ...projectXPropfirms.map(pf => ({
325
+ name: pf.disabled
326
+ ? chalk.gray(formatPropfirmName(pf.name))
327
+ : chalk.cyan(formatPropfirmName(pf.name)),
328
+ value: pf.value,
329
+ disabled: pf.disabled
330
+ })),
331
+ new inquirer.Separator(),
332
+ { name: chalk.yellow('< Back'), value: 'back' }
333
+ ],
334
+ pageSize: device.menuPageSize,
335
+ loop: false
336
+ }
337
+ ]);
338
+
339
+ return propfirm;
340
+ };
341
+
342
+ // Login prompt (Responsive)
343
+ const loginPrompt = async (propfirmName) => {
344
+ const device = getDevice();
345
+ console.log();
346
+
347
+ if (device.isMobile) {
348
+ console.log(chalk.cyan(`→ ${propfirmName}`));
349
+ } else {
350
+ console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
351
+ }
352
+ console.log();
353
+
354
+ const credentials = await inquirer.prompt([
355
+ {
356
+ type: 'input',
357
+ name: 'username',
358
+ message: device.isMobile ? chalk.white.bold('Username:') : chalk.white.bold('Enter Your Username:'),
359
+ validate: (input) => input.length > 0 || 'Required'
360
+ },
361
+ {
362
+ type: 'password',
363
+ name: 'password',
364
+ message: device.isMobile ? chalk.white.bold('Password:') : chalk.white.bold('Enter Your Password:'),
365
+ mask: '*',
366
+ validate: (input) => input.length > 0 || 'Required'
367
+ }
368
+ ]);
369
+
370
+ return credentials;
371
+ };
372
+
373
+ // Menu après connexion (Responsive)
374
+ const dashboardMenu = async (service) => {
375
+ const device = getDevice();
376
+ const user = service.user;
377
+ const propfirmName = service.getPropfirmName();
378
+
379
+ console.log();
380
+ console.log(chalk.gray(getSeparator()));
381
+
382
+ if (device.isMobile) {
383
+ console.log(chalk.green.bold(` ✓ ${propfirmName}`));
384
+ if (user) {
385
+ console.log(chalk.white(` ${user.userName.toUpperCase()}`));
386
+ }
387
+ } else {
388
+ console.log(chalk.green.bold(` Connected to ${propfirmName}`));
389
+ if (user) {
390
+ console.log(chalk.white(` Welcome, ${user.userName.toUpperCase()}!`));
391
+ }
392
+ }
393
+ console.log(chalk.gray(getSeparator()));
394
+ console.log();
395
+
396
+ // Choix adaptatifs selon le device
397
+ let choices;
398
+ if (device.isMobile) {
399
+ choices = [
400
+ { name: chalk.cyan('Accounts'), value: 'accounts' },
401
+ { name: chalk.cyan('Positions'), value: 'positions' },
402
+ { name: chalk.cyan('Orders'), value: 'orders' },
403
+ { name: chalk.cyan('Stats'), value: 'stats' },
404
+ { name: chalk.cyan('Add Prop-Account'), value: 'add_prop_account' },
405
+ new inquirer.Separator(),
406
+ { name: chalk.cyan('Algo'), value: 'algotrading' },
407
+ new inquirer.Separator(),
408
+ { name: chalk.yellow('Update HQX'), value: 'refresh' },
409
+ { name: chalk.red('Disconnect'), value: 'disconnect' }
410
+ ];
411
+ } else {
412
+ choices = [
413
+ { name: chalk.cyan('View Accounts'), value: 'accounts' },
414
+ { name: chalk.cyan('View Positions'), value: 'positions' },
415
+ { name: chalk.cyan('View Orders'), value: 'orders' },
416
+ { name: chalk.cyan('View Stats'), value: 'stats' },
417
+ { name: chalk.cyan('Add Prop-Account'), value: 'add_prop_account' },
418
+ { name: chalk.cyan('User Info'), value: 'userinfo' },
419
+ new inquirer.Separator(),
420
+ { name: chalk.cyan('Algo-Trading'), value: 'algotrading' },
421
+ new inquirer.Separator(),
422
+ { name: chalk.yellow('Update HQX'), value: 'refresh' },
423
+ { name: chalk.red('Disconnect'), value: 'disconnect' }
424
+ ];
425
+ }
426
+
427
+ const { action } = await inquirer.prompt([
428
+ {
429
+ type: 'list',
430
+ name: 'action',
431
+ message: device.isMobile ? chalk.white.bold('Menu:') : chalk.white.bold('What would you like to do?'),
432
+ choices,
433
+ pageSize: device.menuPageSize,
434
+ loop: false
435
+ }
436
+ ]);
437
+
438
+ return action;
439
+ };
440
+
441
+ // Afficher les comptes (toutes connexions)
442
+ const showAccounts = async (service) => {
443
+ const spinner = ora('Fetching accounts...').start();
444
+ const boxWidth = getLogoWidth();
445
+ const { col1, col2 } = getColWidths(boxWidth);
446
+
447
+ // Helper to format a row with label and value
448
+ const fmtRow = (label, value, colW) => {
449
+ const labelStr = ' ' + label.padEnd(12);
450
+ const valueVisible = (value || '').toString().replace(/\x1b\[[0-9;]*m/g, '');
451
+ const totalVisible = labelStr.length + valueVisible.length;
452
+ const padding = Math.max(0, colW - totalVisible);
453
+ return chalk.white(labelStr) + value + ' '.repeat(padding);
454
+ };
455
+
456
+ // Collecter les comptes de TOUTES les connexions
457
+ let allAccountsData = [];
458
+
459
+ if (connections.count() > 0) {
460
+ for (const conn of connections.getAll()) {
461
+ try {
462
+ const result = await conn.service.getTradingAccounts();
463
+ if (result.success && result.accounts) {
464
+ result.accounts.forEach(account => {
465
+ allAccountsData.push({
466
+ ...account,
467
+ propfirm: conn.propfirm || conn.type,
468
+ service: conn.service
469
+ });
470
+ });
471
+ }
472
+ } catch (e) {}
473
+ }
474
+ } else if (service) {
475
+ const result = await service.getTradingAccounts();
476
+ if (result.success && result.accounts) {
477
+ allAccountsData = result.accounts.map(a => ({ ...a, propfirm: service.getPropfirmName(), service }));
478
+ }
479
+ }
480
+
481
+ spinner.succeed('Accounts loaded');
482
+ console.log();
483
+
484
+ if (allAccountsData.length === 0) {
485
+ drawBoxHeader('ACCOUNTS', boxWidth);
486
+ draw2ColRowRaw(chalk.yellow(' No accounts found.'), '', boxWidth);
487
+ drawBoxFooter(boxWidth);
488
+ } else {
489
+ const totalConns = connections.count() || 1;
490
+ drawBoxHeader(`ACCOUNTS (${allAccountsData.length} accounts, ${totalConns} connection${totalConns > 1 ? 's' : ''})`, boxWidth);
491
+
492
+ // Display 2 accounts per row
493
+ for (let i = 0; i < allAccountsData.length; i += 2) {
494
+ const acc1 = allAccountsData[i];
495
+ const acc2 = allAccountsData[i + 1];
496
+
497
+ const name1 = acc1.accountName || acc1.name || `Account #${acc1.accountId}`;
498
+ const name2 = acc2 ? (acc2.accountName || acc2.name || `Account #${acc2.accountId}`) : '';
499
+
500
+ // Account name header
501
+ draw2ColHeader(name1.substring(0, col1 - 4), name2.substring(0, col2 - 4), boxWidth);
502
+
503
+ // PropFirm
504
+ const st1 = ACCOUNT_STATUS[acc1.status] || { text: 'Unknown', color: 'gray' };
505
+ const st2 = acc2 ? (ACCOUNT_STATUS[acc2.status] || { text: 'Unknown', color: 'gray' }) : null;
506
+ const tp1 = ACCOUNT_TYPE[acc1.type] || { text: 'Unknown', color: 'white' };
507
+ const tp2 = acc2 ? (ACCOUNT_TYPE[acc2.type] || { text: 'Unknown', color: 'white' }) : null;
508
+
509
+ console.log(chalk.cyan('║') + fmtRow('PropFirm:', chalk.magenta(acc1.propfirm), col1) + chalk.cyan('│') + (acc2 ? fmtRow('PropFirm:', chalk.magenta(acc2.propfirm), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
510
+ console.log(chalk.cyan('║') + fmtRow('Balance:', acc1.balance >= 0 ? chalk.green('$' + acc1.balance.toLocaleString()) : chalk.red('$' + acc1.balance.toLocaleString()), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', acc2.balance >= 0 ? chalk.green('$' + acc2.balance.toLocaleString()) : chalk.red('$' + acc2.balance.toLocaleString()), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
511
+ console.log(chalk.cyan('║') + fmtRow('Status:', chalk[st1.color](st1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Status:', chalk[st2.color](st2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
512
+ console.log(chalk.cyan('║') + fmtRow('Type:', chalk[tp1.color](tp1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Type:', chalk[tp2.color](tp2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
513
+
514
+ if (i + 2 < allAccountsData.length) {
515
+ draw2ColSeparator(boxWidth);
516
+ }
517
+ }
518
+
519
+ drawBoxFooter(boxWidth);
520
+ }
521
+
522
+ console.log();
523
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
524
+ };
525
+
526
+ // Afficher les infos utilisateur
527
+ const showUserInfo = async (service) => {
528
+ const boxWidth = getLogoWidth();
529
+ const spinner = ora('Fetching user info...').start();
530
+
531
+ // Collecter les infos de TOUTES les connexions
532
+ let allUsers = [];
533
+
534
+ if (connections.count() > 0) {
535
+ for (const conn of connections.getAll()) {
536
+ try {
537
+ const result = await conn.service.getUser();
538
+ if (result.success && result.user) {
539
+ allUsers.push({
540
+ ...result.user,
541
+ propfirm: conn.propfirm || conn.type
542
+ });
543
+ }
544
+ } catch (e) {}
545
+ }
546
+ } else if (service) {
547
+ const result = await service.getUser();
548
+ if (result.success && result.user) {
549
+ allUsers.push({
550
+ ...result.user,
551
+ propfirm: service.getPropfirmName()
552
+ });
553
+ }
554
+ }
555
+
556
+ spinner.succeed('User info loaded');
557
+ console.log();
558
+
559
+ // Inner width for content
560
+ const innerWidth = boxWidth - 2;
561
+
562
+ // Helper to format row
563
+ const fmtRow = (label, value, totalW) => {
564
+ const labelStr = ' ' + label.padEnd(14);
565
+ const valueVisible = (value || '').toString().replace(/\x1b\[[0-9;]*m/g, '');
566
+ const totalVisible = labelStr.length + valueVisible.length;
567
+ const padding = Math.max(0, totalW - totalVisible - 1);
568
+ return chalk.white(labelStr) + value + ' '.repeat(padding);
569
+ };
570
+
571
+ if (allUsers.length === 0) {
572
+ drawBoxHeader('USER INFO', boxWidth);
573
+ console.log(chalk.cyan('║') + padText(chalk.yellow(' No user information available.'), innerWidth) + chalk.cyan('║'));
574
+ drawBoxFooter(boxWidth);
575
+ } else {
576
+ drawBoxHeader(`USER INFO (${allUsers.length} connection${allUsers.length > 1 ? 's' : ''})`, boxWidth);
577
+
578
+ allUsers.forEach((user, index) => {
579
+ // PropFirm header
580
+ const pfHeader = `── ${user.propfirm} ──`;
581
+ console.log(chalk.cyan('║') + chalk.magenta.bold(centerText(pfHeader, innerWidth)) + chalk.cyan('║'));
582
+
583
+ // Username
584
+ console.log(chalk.cyan('║') + fmtRow('Username:', chalk.cyan(user.userName || 'N/A'), innerWidth) + chalk.cyan('║'));
585
+
586
+ // Full Name
587
+ const fullName = `${user.firstName || ''} ${user.lastName || ''}`.trim() || 'N/A';
588
+ console.log(chalk.cyan('║') + fmtRow('Name:', chalk.white(fullName), innerWidth) + chalk.cyan('║'));
589
+
590
+ // Email
591
+ console.log(chalk.cyan('║') + fmtRow('Email:', chalk.white(user.email || 'N/A'), innerWidth) + chalk.cyan('║'));
592
+
593
+ // User ID
594
+ console.log(chalk.cyan('║') + fmtRow('User ID:', chalk.gray(user.userId || 'N/A'), innerWidth) + chalk.cyan('║'));
595
+
596
+ // Separator between users if there are more
597
+ if (index < allUsers.length - 1) {
598
+ console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
599
+ }
600
+ });
601
+
602
+ drawBoxFooter(boxWidth);
603
+ }
604
+
605
+ console.log();
606
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
607
+ };
608
+
609
+ // Afficher les positions
610
+ const showPositions = async (service) => {
611
+ const spinner = ora('Fetching positions...').start();
612
+ const boxWidth = getLogoWidth();
613
+ const { col1, col2 } = getColWidths(boxWidth);
614
+
615
+ // Collecter les positions de TOUTES les connexions
616
+ let allPositions = [];
617
+
618
+ if (connections.count() > 0) {
619
+ for (const conn of connections.getAll()) {
620
+ try {
621
+ const accountsResult = await conn.service.getTradingAccounts();
622
+ if (accountsResult.success && accountsResult.accounts) {
623
+ for (const account of accountsResult.accounts) {
624
+ const result = await conn.service.getPositions(account.accountId);
625
+ if (result.success && result.positions) {
626
+ allPositions = allPositions.concat(result.positions.map(p => ({
627
+ ...p,
628
+ accountName: account.accountName || account.name || `Account #${account.accountId}`,
629
+ propfirm: conn.propfirm || conn.type
630
+ })));
631
+ }
632
+ }
633
+ }
634
+ } catch (e) {}
635
+ }
636
+ } else if (service) {
637
+ const accountsResult = await service.getTradingAccounts();
638
+ if (accountsResult.success && accountsResult.accounts) {
639
+ for (const account of accountsResult.accounts) {
640
+ const result = await service.getPositions(account.accountId);
641
+ if (result.success && result.positions) {
642
+ allPositions = allPositions.concat(result.positions.map(p => ({
643
+ ...p,
644
+ accountName: account.accountName || account.name || `Account #${account.accountId}`,
645
+ propfirm: service.getPropfirmName()
646
+ })));
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ spinner.succeed('Positions loaded');
653
+ console.log();
654
+
655
+ // Helper to format row for positions
656
+ const fmtRow = (label, value, colW) => {
657
+ const labelStr = ' ' + label.padEnd(12);
658
+ const valueVisible = (value || '').toString().replace(/\x1b\[[0-9;]*m/g, '');
659
+ const totalVisible = labelStr.length + valueVisible.length;
660
+ const padding = Math.max(0, colW - totalVisible);
661
+ return chalk.white(labelStr) + value + ' '.repeat(padding);
662
+ };
663
+
664
+ if (allPositions.length === 0) {
665
+ drawBoxHeader('OPEN POSITIONS', boxWidth);
666
+ draw2ColRowRaw(chalk.yellow(' No open positions.'), '', boxWidth);
667
+ drawBoxFooter(boxWidth);
668
+ } else {
669
+ drawBoxHeader(`OPEN POSITIONS (${allPositions.length})`, boxWidth);
670
+
671
+ // Display 2 positions per row
672
+ for (let i = 0; i < allPositions.length; i += 2) {
673
+ const pos1 = allPositions[i];
674
+ const pos2 = allPositions[i + 1];
675
+
676
+ const symbol1 = pos1.symbolId || pos1.contractId || 'Unknown';
677
+ const symbol2 = pos2 ? (pos2.symbolId || pos2.contractId || 'Unknown') : '';
678
+
679
+ // Symbol header
680
+ draw2ColHeader(symbol1.substring(0, col1 - 4), symbol2.substring(0, col2 - 4), boxWidth);
681
+
682
+ // Position details
683
+ const size1 = pos1.positionSize || pos1.size || 0;
684
+ const size2 = pos2 ? (pos2.positionSize || pos2.size || 0) : 0;
685
+ const sizeColor1 = size1 > 0 ? chalk.green : (size1 < 0 ? chalk.red : chalk.white);
686
+ const sizeColor2 = size2 > 0 ? chalk.green : (size2 < 0 ? chalk.red : chalk.white);
687
+ const price1 = pos1.averagePrice ? '$' + pos1.averagePrice.toFixed(2) : 'N/A';
688
+ const price2 = pos2 && pos2.averagePrice ? '$' + pos2.averagePrice.toFixed(2) : 'N/A';
689
+ const pnl1 = pos1.profitAndLoss || 0;
690
+ const pnl2 = pos2 ? (pos2.profitAndLoss || 0) : 0;
691
+ const pnlColor1 = pnl1 >= 0 ? chalk.green : chalk.red;
692
+ const pnlColor2 = pnl2 >= 0 ? chalk.green : chalk.red;
693
+
694
+ console.log(chalk.cyan('║') + fmtRow('Account:', chalk.cyan(pos1.accountName.substring(0, 15)), col1) + chalk.cyan('│') + (pos2 ? fmtRow('Account:', chalk.cyan(pos2.accountName.substring(0, 15)), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
695
+ console.log(chalk.cyan('║') + fmtRow('PropFirm:', chalk.magenta(pos1.propfirm), col1) + chalk.cyan('│') + (pos2 ? fmtRow('PropFirm:', chalk.magenta(pos2.propfirm), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
696
+ console.log(chalk.cyan('║') + fmtRow('Size:', sizeColor1(size1.toString()), col1) + chalk.cyan('│') + (pos2 ? fmtRow('Size:', sizeColor2(size2.toString()), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
697
+ console.log(chalk.cyan('║') + fmtRow('Avg Price:', chalk.white(price1), col1) + chalk.cyan('│') + (pos2 ? fmtRow('Avg Price:', chalk.white(price2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
698
+ console.log(chalk.cyan('║') + fmtRow('P&L:', pnlColor1((pnl1 >= 0 ? '+' : '') + '$' + pnl1.toFixed(2)), col1) + chalk.cyan('│') + (pos2 ? fmtRow('P&L:', pnlColor2((pnl2 >= 0 ? '+' : '') + '$' + pnl2.toFixed(2)), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
699
+
700
+ if (i + 2 < allPositions.length) {
701
+ draw2ColSeparator(boxWidth);
702
+ }
703
+ }
704
+
705
+ drawBoxFooter(boxWidth);
706
+ }
707
+
708
+ console.log();
709
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
710
+ };
711
+
712
+ // Afficher les ordres
713
+ const showOrders = async (service) => {
714
+ const spinner = ora('Fetching orders...').start();
715
+ const boxWidth = getLogoWidth();
716
+ const { col1, col2 } = getColWidths(boxWidth);
717
+
718
+ // Order status mapping
719
+ const ORDER_STATUS = {
720
+ 0: { text: 'Pending', color: 'gray' },
721
+ 1: { text: 'Open', color: 'yellow' },
722
+ 2: { text: 'Filled', color: 'green' },
723
+ 3: { text: 'Cancelled', color: 'red' }
724
+ };
725
+
726
+ // Order side mapping
727
+ const ORDER_SIDE = {
728
+ 1: { text: 'BUY', color: 'green' },
729
+ 2: { text: 'SELL', color: 'red' }
730
+ };
731
+
732
+ // Collecter les ordres de TOUTES les connexions
733
+ let allOrders = [];
734
+
735
+ if (connections.count() > 0) {
736
+ for (const conn of connections.getAll()) {
737
+ try {
738
+ const accountsResult = await conn.service.getTradingAccounts();
739
+ if (accountsResult.success && accountsResult.accounts) {
740
+ for (const account of accountsResult.accounts) {
741
+ const result = await conn.service.getOrders(account.accountId);
742
+ if (result.success && result.orders) {
743
+ allOrders = allOrders.concat(result.orders.map(o => ({
744
+ ...o,
745
+ accountName: account.accountName || account.name || `Account #${account.accountId}`,
746
+ propfirm: conn.propfirm || conn.type
747
+ })));
748
+ }
749
+ }
750
+ }
751
+ } catch (e) {}
752
+ }
753
+ } else if (service) {
754
+ const accountsResult = await service.getTradingAccounts();
755
+ if (accountsResult.success && accountsResult.accounts) {
756
+ for (const account of accountsResult.accounts) {
757
+ const result = await service.getOrders(account.accountId);
758
+ if (result.success && result.orders) {
759
+ allOrders = allOrders.concat(result.orders.map(o => ({
760
+ ...o,
761
+ accountName: account.accountName || account.name || `Account #${account.accountId}`,
762
+ propfirm: service.getPropfirmName()
763
+ })));
764
+ }
765
+ }
766
+ }
767
+ }
768
+
769
+ spinner.succeed('Orders loaded');
770
+ console.log();
771
+
772
+ // Limit to recent 20 orders
773
+ const recentOrders = allOrders.slice(0, 20);
774
+
775
+ // Helper to format row for orders
776
+ const fmtRow = (label, value, colW) => {
777
+ const labelStr = ' ' + label.padEnd(12);
778
+ const valueVisible = (value || '').toString().replace(/\x1b\[[0-9;]*m/g, '');
779
+ const totalVisible = labelStr.length + valueVisible.length;
780
+ const padding = Math.max(0, colW - totalVisible);
781
+ return chalk.white(labelStr) + value + ' '.repeat(padding);
782
+ };
783
+
784
+ if (recentOrders.length === 0) {
785
+ drawBoxHeader('ORDERS', boxWidth);
786
+ draw2ColRowRaw(chalk.yellow(' No recent orders.'), '', boxWidth);
787
+ drawBoxFooter(boxWidth);
788
+ } else {
789
+ drawBoxHeader(`ORDERS (${recentOrders.length} of ${allOrders.length})`, boxWidth);
790
+
791
+ // Display 2 orders per row
792
+ for (let i = 0; i < recentOrders.length; i += 2) {
793
+ const ord1 = recentOrders[i];
794
+ const ord2 = recentOrders[i + 1];
795
+
796
+ const symbol1 = ord1.symbolId || 'Unknown';
797
+ const symbol2 = ord2 ? (ord2.symbolId || 'Unknown') : '';
798
+
799
+ // Symbol header
800
+ draw2ColHeader(symbol1.substring(0, col1 - 4), symbol2.substring(0, col2 - 4), boxWidth);
801
+
802
+ // Order details
803
+ const side1 = ORDER_SIDE[ord1.side || ord1.orderSide] || { text: 'N/A', color: 'white' };
804
+ const side2 = ord2 ? (ORDER_SIDE[ord2.side || ord2.orderSide] || { text: 'N/A', color: 'white' }) : null;
805
+ const st1 = ORDER_STATUS[ord1.status] || { text: 'Unknown', color: 'gray' };
806
+ const st2 = ord2 ? (ORDER_STATUS[ord2.status] || { text: 'Unknown', color: 'gray' }) : null;
807
+ const size1 = ord1.positionSize || ord1.size || ord1.quantity || 0;
808
+ const size2 = ord2 ? (ord2.positionSize || ord2.size || ord2.quantity || 0) : 0;
809
+ const price1 = ord1.price ? '$' + ord1.price.toFixed(2) : 'Market';
810
+ const price2 = ord2 && ord2.price ? '$' + ord2.price.toFixed(2) : (ord2 ? 'Market' : '');
811
+
812
+ console.log(chalk.cyan('║') + fmtRow('Account:', chalk.cyan(ord1.accountName.substring(0, 15)), col1) + chalk.cyan('│') + (ord2 ? fmtRow('Account:', chalk.cyan(ord2.accountName.substring(0, 15)), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
813
+ console.log(chalk.cyan('║') + fmtRow('Side:', chalk[side1.color](side1.text), col1) + chalk.cyan('│') + (ord2 ? fmtRow('Side:', chalk[side2.color](side2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
814
+ console.log(chalk.cyan('║') + fmtRow('Size:', chalk.white(size1.toString()), col1) + chalk.cyan('│') + (ord2 ? fmtRow('Size:', chalk.white(size2.toString()), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
815
+ console.log(chalk.cyan('║') + fmtRow('Price:', chalk.white(price1), col1) + chalk.cyan('│') + (ord2 ? fmtRow('Price:', chalk.white(price2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
816
+ console.log(chalk.cyan('║') + fmtRow('Status:', chalk[st1.color](st1.text), col1) + chalk.cyan('│') + (ord2 ? fmtRow('Status:', chalk[st2.color](st2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
817
+
818
+ if (i + 2 < recentOrders.length) {
819
+ draw2ColSeparator(boxWidth);
820
+ }
821
+ }
822
+
823
+ drawBoxFooter(boxWidth);
824
+ }
825
+
826
+ console.log();
827
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
828
+ };
829
+
830
+ // Ajouter un compte PropFirm supplémentaire
831
+ const addPropAccount = async () => {
832
+ const device = getDevice();
833
+
834
+ // Afficher les connexions actives
835
+ if (connections.count() > 0) {
836
+ console.log();
837
+ console.log(chalk.cyan.bold(' Active Connections:'));
838
+ connections.getAll().forEach((conn, i) => {
839
+ console.log(chalk.green(` ${i + 1}. ${conn.propfirm || conn.type}`));
840
+ });
841
+ console.log();
842
+ }
843
+
844
+ // Menu pour choisir le type de connexion
845
+ const { connectionType } = await inquirer.prompt([
846
+ {
847
+ type: 'list',
848
+ name: 'connectionType',
849
+ message: chalk.white.bold('Add Connection:'),
850
+ choices: [
851
+ { name: chalk.cyan('ProjectX (19 PropFirms)'), value: 'projectx' },
852
+ { name: chalk.gray('Rithmic (Coming Soon)'), value: 'rithmic', disabled: 'Coming Soon' },
853
+ { name: chalk.gray('Tradovate (Coming Soon)'), value: 'tradovate', disabled: 'Coming Soon' },
854
+ new inquirer.Separator(),
855
+ { name: chalk.yellow('< Back'), value: 'back' }
856
+ ],
857
+ pageSize: device.menuPageSize,
858
+ loop: false
859
+ }
860
+ ]);
861
+
862
+ if (connectionType === 'back') {
863
+ return;
864
+ }
865
+
866
+ if (connectionType === 'projectx') {
867
+ // Sélection de la PropFirm
868
+ const propfirm = await projectXMenu();
869
+ if (propfirm === 'back') {
870
+ return;
871
+ }
872
+
873
+ // Demander les credentials
874
+ console.log();
875
+ const credentials = await inquirer.prompt([
876
+ {
877
+ type: 'input',
878
+ name: 'username',
879
+ message: chalk.white('Username/Email:'),
880
+ validate: (input) => input.length > 0 || 'Username is required'
881
+ },
882
+ {
883
+ type: 'password',
884
+ name: 'password',
885
+ message: chalk.white('Password:'),
886
+ mask: '*',
887
+ validate: (input) => input.length > 0 || 'Password is required'
888
+ }
889
+ ]);
890
+
891
+ // Tenter la connexion
892
+ const spinner = ora('Connecting to PropFirm...').start();
893
+
894
+ try {
895
+ const newService = new ProjectXService(propfirm);
896
+ const loginResult = await newService.login(credentials.username, credentials.password);
897
+
898
+ if (loginResult.success) {
899
+ await newService.getUser();
900
+ const accountsResult = await newService.getTradingAccounts();
901
+
902
+ // Ajouter au connection manager
903
+ connections.add('projectx', newService, newService.getPropfirmName());
904
+
905
+ spinner.succeed('Connection added!');
906
+ console.log();
907
+ console.log(chalk.green(` Connected to ${newService.getPropfirmName()}`));
908
+
909
+ if (accountsResult.success && accountsResult.accounts) {
910
+ console.log(chalk.cyan(` Found ${accountsResult.accounts.length} trading account(s)`));
911
+ }
912
+
913
+ console.log(chalk.white(` Total connections: ${connections.count()}`));
914
+ } else {
915
+ spinner.fail('Connection failed');
916
+ console.log(chalk.red(` Error: ${loginResult.error}`));
917
+ }
918
+ } catch (error) {
919
+ spinner.fail('Connection failed');
920
+ console.log(chalk.red(` Error: ${error.message}`));
921
+ }
922
+ }
923
+
924
+ console.log();
925
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
926
+ };
927
+
928
+ // Session de trading active
929
+ let activeAlgoSession = null;
930
+
931
+ // Menu Algo-Trading principal
932
+ const algoTradingMenu = async (service) => {
933
+ const device = getDevice();
934
+ console.log();
935
+ console.log(chalk.gray(getSeparator()));
936
+ console.log(chalk.magenta.bold(' Algo-Trading'));
937
+ console.log(chalk.gray(getSeparator()));
938
+ console.log();
939
+
940
+ const { action } = await inquirer.prompt([
941
+ {
942
+ type: 'list',
943
+ name: 'action',
944
+ message: chalk.white.bold('Select Mode:'),
945
+ choices: [
946
+ { name: chalk.cyan('One Account'), value: 'one_account' },
947
+ { name: chalk.gray('Copy Trading (Coming Soon)'), value: 'copy_trading', disabled: 'Coming Soon' },
948
+ new inquirer.Separator(),
949
+ { name: chalk.yellow('< Back'), value: 'back' }
950
+ ],
951
+ pageSize: device.menuPageSize,
952
+ loop: false
953
+ }
954
+ ]);
955
+
956
+ switch (action) {
957
+ case 'one_account':
958
+ await oneAccountMenu(service);
959
+ break;
960
+ case 'copy_trading':
961
+ // Disabled - Coming Soon
962
+ break;
963
+ case 'back':
964
+ return 'back';
965
+ }
966
+
967
+ return action;
968
+ };
969
+
970
+ // Menu One Account - Sélection du compte actif
971
+ const oneAccountMenu = async (service) => {
972
+ const spinner = ora('Fetching active accounts...').start();
973
+
974
+ // Récupérer les comptes via getTradingAccounts (plus fiable)
975
+ const result = await service.getTradingAccounts();
976
+
977
+ if (!result.success || !result.accounts || result.accounts.length === 0) {
978
+ spinner.fail('No active accounts found');
979
+ console.log(chalk.yellow(' You need at least one active trading account.'));
980
+ console.log();
981
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
982
+ return;
983
+ }
984
+
985
+ // Filtrer seulement les comptes actifs (status === 0 = Active)
986
+ const activeAccounts = result.accounts.filter(acc => acc.status === 0);
987
+
988
+ if (activeAccounts.length === 0) {
989
+ spinner.fail('No active accounts found');
990
+ console.log(chalk.yellow(' You need at least one active trading account (status: Active).'));
991
+ console.log();
992
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
993
+ return;
994
+ }
995
+
996
+ spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
997
+ console.log();
998
+
999
+ // Afficher seulement les comptes actifs
1000
+ const accountChoices = activeAccounts.map(account => ({
1001
+ name: chalk.cyan(`${account.accountName || account.name || 'Account #' + account.accountId} - Balance: $${account.balance.toLocaleString()}`),
1002
+ value: account
1003
+ }));
1004
+
1005
+ accountChoices.push(new inquirer.Separator());
1006
+ accountChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
1007
+
1008
+ const { selectedAccount } = await inquirer.prompt([
1009
+ {
1010
+ type: 'list',
1011
+ name: 'selectedAccount',
1012
+ message: chalk.white.bold('Select Account:'),
1013
+ choices: accountChoices,
1014
+ pageSize: 15,
1015
+ loop: false
1016
+ }
1017
+ ]);
1018
+
1019
+ if (selectedAccount === 'back') {
1020
+ return;
1021
+ }
1022
+
1023
+ // Vérifier si le marché est ouvert
1024
+ const accountName = selectedAccount.accountName || selectedAccount.name || 'Account #' + selectedAccount.accountId;
1025
+ console.log();
1026
+ const marketSpinner = ora('Checking market status...').start();
1027
+
1028
+ // Vérifier les heures de marché
1029
+ const marketHours = service.checkMarketHours();
1030
+
1031
+ // Vérifier aussi via l'API si le compte peut trader
1032
+ const marketStatus = await service.getMarketStatus(selectedAccount.accountId);
1033
+
1034
+ if (!marketHours.isOpen) {
1035
+ marketSpinner.fail('Market is CLOSED');
1036
+ console.log();
1037
+ console.log(chalk.red.bold(' [X] ' + marketHours.message));
1038
+ console.log();
1039
+ console.log(chalk.gray(' Futures markets (CME) trading hours:'));
1040
+ console.log(chalk.gray(' Sunday 5:00 PM CT - Friday 4:00 PM CT'));
1041
+ console.log(chalk.gray(' Daily maintenance: 4:00 PM - 5:00 PM CT'));
1042
+ console.log();
1043
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1044
+ return;
1045
+ }
1046
+
1047
+ if (marketStatus.success && !marketStatus.isOpen) {
1048
+ marketSpinner.fail('Cannot trade on this account');
1049
+ console.log();
1050
+ console.log(chalk.red.bold(' [X] ' + marketStatus.message));
1051
+ console.log();
1052
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1053
+ return;
1054
+ }
1055
+
1056
+ marketSpinner.succeed('Market is OPEN - Ready to trade!');
1057
+
1058
+ // Passer à la sélection du symbole
1059
+ await selectSymbolMenu(service, selectedAccount);
1060
+ };
1061
+
1062
+ // Menu de sélection du symbole futures
1063
+ const selectSymbolMenu = async (service, account) => {
1064
+ const device = getDevice();
1065
+ const accountName = account.accountName || account.name || 'Account #' + account.accountId;
1066
+
1067
+ console.log();
1068
+ console.log(chalk.gray(getSeparator()));
1069
+ console.log(chalk.cyan.bold(` Account: ${accountName}`));
1070
+ console.log(chalk.gray(getSeparator()));
1071
+ console.log();
1072
+
1073
+ const symbolChoices = FUTURES_SYMBOLS.map(symbol => ({
1074
+ name: chalk.cyan(device.isMobile ? symbol.value : symbol.name),
1075
+ value: symbol
1076
+ }));
1077
+
1078
+ symbolChoices.push(new inquirer.Separator());
1079
+ symbolChoices.push({ name: chalk.yellow('< Back'), value: 'back' });
1080
+
1081
+ const { selectedSymbol } = await inquirer.prompt([
1082
+ {
1083
+ type: 'list',
1084
+ name: 'selectedSymbol',
1085
+ message: chalk.white.bold('Select Symbol:'),
1086
+ choices: symbolChoices,
1087
+ pageSize: device.menuPageSize,
1088
+ loop: false
1089
+ }
1090
+ ]);
1091
+
1092
+ if (selectedSymbol === 'back') {
1093
+ return;
1094
+ }
1095
+
1096
+ // Rechercher le contrat via Gateway API
1097
+ const spinner = ora(`Searching for ${selectedSymbol.value} contract...`).start();
1098
+ const contractResult = await service.searchContracts(selectedSymbol.searchText, false);
1099
+
1100
+ let contract = null;
1101
+ if (contractResult.success && contractResult.contracts && contractResult.contracts.length > 0) {
1102
+ // Trouver le contrat actif ou prendre le premier
1103
+ contract = contractResult.contracts.find(c => c.activeContract) || contractResult.contracts[0];
1104
+ spinner.succeed(`Found: ${contract.name || selectedSymbol.value}`);
1105
+ if (contract.tickSize && contract.tickValue) {
1106
+ console.log(chalk.gray(` Tick Size: ${contract.tickSize} | Tick Value: $${contract.tickValue}`));
1107
+ }
1108
+ } else {
1109
+ // Fallback: utiliser le symbole directement si l'API ne retourne rien
1110
+ spinner.warn(`Using ${selectedSymbol.value} (contract details unavailable)`);
1111
+ contract = {
1112
+ id: selectedSymbol.value,
1113
+ name: selectedSymbol.name,
1114
+ symbol: selectedSymbol.value
1115
+ };
1116
+ }
1117
+
1118
+ console.log();
1119
+
1120
+ // Demander le nombre de contrats
1121
+ const { contracts } = await inquirer.prompt([
1122
+ {
1123
+ type: 'input',
1124
+ name: 'contracts',
1125
+ message: chalk.white.bold('Number of Contracts:'),
1126
+ default: '1',
1127
+ validate: (input) => {
1128
+ const num = parseInt(input);
1129
+ if (isNaN(num) || num <= 0 || num > 100) {
1130
+ return 'Please enter a valid number between 1 and 100';
1131
+ }
1132
+ return true;
1133
+ },
1134
+ filter: (input) => parseInt(input)
1135
+ }
1136
+ ]);
1137
+
1138
+ // Confirmation et lancement
1139
+ console.log();
1140
+ console.log(chalk.gray(getSeparator()));
1141
+ console.log(chalk.white.bold(' Algo Configuration:'));
1142
+ console.log(chalk.gray(getSeparator()));
1143
+ console.log(chalk.white(` Account: ${chalk.cyan(accountName)}`));
1144
+ console.log(chalk.white(` Symbol: ${chalk.cyan(contract.name || selectedSymbol.value)}`));
1145
+ console.log(chalk.white(` Contracts: ${chalk.cyan(contracts)}`));
1146
+ console.log(chalk.gray(getSeparator()));
1147
+ console.log();
1148
+
1149
+ const { launch } = await inquirer.prompt([
1150
+ {
1151
+ type: 'list',
1152
+ name: 'launch',
1153
+ message: chalk.white.bold('Ready to launch?'),
1154
+ choices: [
1155
+ { name: chalk.green.bold('[>] Launch Algo'), value: 'launch' },
1156
+ { name: chalk.yellow('< Back'), value: 'back' }
1157
+ ],
1158
+ loop: false
1159
+ }
1160
+ ]);
1161
+
1162
+ if (launch === 'back') {
1163
+ return;
1164
+ }
1165
+
1166
+ // Lancer l'algo
1167
+ await launchAlgo(service, account, contract, contracts);
1168
+ };
1169
+
1170
+ // Lancer l'algo
1171
+ const launchAlgo = async (service, account, contract, numContracts) => {
1172
+ const accountName = account.accountName || account.name || 'Account #' + account.accountId;
1173
+ const symbolName = contract.name || contract.symbol || contract.id;
1174
+
1175
+ console.log();
1176
+ console.log(chalk.green.bold(' [>] Launching HQX Algo...'));
1177
+ console.log();
1178
+
1179
+ const spinner = ora('Connecting to HQX Server...').start();
1180
+
1181
+ // Essayer de se connecter au serveur HQX
1182
+ let hqxConnected = false;
1183
+ try {
1184
+ if (hqxServer) {
1185
+ await hqxServer.connect();
1186
+ hqxConnected = hqxServer.isConnected();
1187
+ }
1188
+ } catch (e) {
1189
+ // Ignore connection errors
1190
+ }
1191
+
1192
+ if (hqxConnected) {
1193
+ spinner.succeed('Connected to HQX Server');
1194
+ } else {
1195
+ spinner.warn('HQX Server unavailable - Running in Demo Mode');
1196
+ }
1197
+
1198
+ console.log();
1199
+ console.log(chalk.gray(getSeparator()));
1200
+ console.log(chalk.cyan.bold(' HQX Ultra-Scalping Algo'));
1201
+ console.log(chalk.gray(getSeparator()));
1202
+ console.log(chalk.white(` Status: ${chalk.green('RUNNING')}`));
1203
+ console.log(chalk.white(` Account: ${chalk.cyan(accountName)}`));
1204
+ console.log(chalk.white(` Symbol: ${chalk.cyan(symbolName)}`));
1205
+ console.log(chalk.white(` Contracts: ${chalk.cyan(numContracts)}`));
1206
+ console.log(chalk.white(` Mode: ${hqxConnected ? chalk.green('LIVE') : chalk.yellow('DEMO')}`));
1207
+ console.log(chalk.gray(getSeparator()));
1208
+ console.log();
1209
+
1210
+ // Afficher les logs en temps réel
1211
+ let running = true;
1212
+ let logs = [];
1213
+
1214
+ const addLog = (type, message) => {
1215
+ const timestamp = new Date().toLocaleTimeString();
1216
+ const typeColors = {
1217
+ info: chalk.cyan,
1218
+ signal: chalk.yellow,
1219
+ trade: chalk.green,
1220
+ error: chalk.red,
1221
+ warning: chalk.yellow
1222
+ };
1223
+ const color = typeColors[type] || chalk.white;
1224
+ logs.push({ timestamp, type, message, color });
1225
+ if (logs.length > 10) logs.shift();
1226
+ return logs;
1227
+ };
1228
+
1229
+ const displayLogs = () => {
1230
+ console.log(chalk.gray(' Recent Activity:'));
1231
+ logs.forEach(log => {
1232
+ console.log(chalk.gray(` [${log.timestamp}]`) + ' ' + log.color(log.message));
1233
+ });
1234
+ };
1235
+
1236
+ // Simulation de l'algo en mode demo
1237
+ addLog('info', 'Algo initialized');
1238
+ addLog('info', `Monitoring ${symbolName}...`);
1239
+ displayLogs();
1240
+
1241
+ if (!hqxConnected) {
1242
+ // Mode demo - simulation
1243
+ console.log();
1244
+ console.log(chalk.yellow(' Demo mode: No real trades will be executed.'));
1245
+ }
1246
+
1247
+ console.log();
1248
+
1249
+ // Menu pour arrêter l'algo
1250
+ const { stopAction } = await inquirer.prompt([
1251
+ {
1252
+ type: 'list',
1253
+ name: 'stopAction',
1254
+ message: chalk.red.bold(''),
1255
+ choices: [
1256
+ { name: chalk.red.bold('[X] Stop Algo'), value: 'stop' }
1257
+ ],
1258
+ pageSize: 1,
1259
+ loop: false
1260
+ }
1261
+ ]);
1262
+
1263
+ if (stopAction === 'stop') {
1264
+ running = false;
1265
+ console.log();
1266
+ console.log(chalk.yellow(' Stopping algo...'));
1267
+
1268
+ if (hqxConnected && hqxServer) {
1269
+ hqxServer.stopAlgo();
1270
+ hqxServer.disconnect();
1271
+ }
1272
+
1273
+ console.log(chalk.green(' [OK] Algo stopped successfully'));
1274
+ console.log();
1275
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1276
+ }
1277
+ };
1278
+
1279
+ // Menu des paramètres de trading (legacy - peut être supprimé)
1280
+ const tradingSettingsMenu = async (service, account, contract) => {
1281
+ console.log(chalk.gray('─'.repeat(60)));
1282
+ console.log(chalk.white.bold(' Trading Settings'));
1283
+ console.log(chalk.gray('─'.repeat(60)));
1284
+ console.log();
1285
+
1286
+ const settings = await inquirer.prompt([
1287
+ {
1288
+ type: 'input',
1289
+ name: 'dailyTarget',
1290
+ message: chalk.white.bold('Daily Target ($):'),
1291
+ default: '500',
1292
+ validate: (input) => {
1293
+ const num = parseFloat(input);
1294
+ if (isNaN(num) || num <= 0) {
1295
+ return 'Please enter a valid positive number';
1296
+ }
1297
+ return true;
1298
+ },
1299
+ filter: (input) => parseFloat(input)
1300
+ },
1301
+ {
1302
+ type: 'input',
1303
+ name: 'maxRisk',
1304
+ message: chalk.white.bold('Max Risk ($):'),
1305
+ default: '250',
1306
+ validate: (input) => {
1307
+ const num = parseFloat(input);
1308
+ if (isNaN(num) || num <= 0) {
1309
+ return 'Please enter a valid positive number';
1310
+ }
1311
+ return true;
1312
+ },
1313
+ filter: (input) => parseFloat(input)
1314
+ },
1315
+ {
1316
+ type: 'input',
1317
+ name: 'contracts',
1318
+ message: chalk.white.bold('Number of Contracts:'),
1319
+ default: '1',
1320
+ validate: (input) => {
1321
+ const num = parseInt(input);
1322
+ if (isNaN(num) || num <= 0 || num > 100) {
1323
+ return 'Please enter a valid number between 1 and 100';
1324
+ }
1325
+ return true;
1326
+ },
1327
+ filter: (input) => parseInt(input)
1328
+ }
1329
+ ]);
1330
+
1331
+ // Afficher le résumé
1332
+ const device = getDevice();
1333
+ console.log();
1334
+ console.log(chalk.gray(getSeparator()));
1335
+ console.log(chalk.green.bold(' Trading Session Summary'));
1336
+ console.log(chalk.gray(getSeparator()));
1337
+ console.log();
1338
+
1339
+ if (device.isMobile) {
1340
+ console.log(chalk.white(` ${chalk.cyan(account.name)}`));
1341
+ console.log(chalk.white(` ${chalk.cyan(contract.name)}`));
1342
+ console.log(chalk.white(` Target: ${chalk.green('$' + settings.dailyTarget)} | Risk: ${chalk.red('$' + settings.maxRisk)}`));
1343
+ console.log(chalk.white(` Contracts: ${chalk.yellow(settings.contracts)}`));
1344
+ } else {
1345
+ console.log(chalk.white(` Account: ${chalk.cyan(account.name)}`));
1346
+ console.log(chalk.white(` Symbol: ${chalk.cyan(contract.name)} (${contract.description})`));
1347
+ console.log(chalk.white(` Daily Target: ${chalk.green('$' + settings.dailyTarget.toLocaleString())}`));
1348
+ console.log(chalk.white(` Max Risk: ${chalk.red('$' + settings.maxRisk.toLocaleString())}`));
1349
+ console.log(chalk.white(` Contracts: ${chalk.yellow(settings.contracts)}`));
1350
+ console.log(chalk.white(` Tick Value: ${chalk.gray('$' + contract.tickValue)}`));
1351
+ }
1352
+ console.log();
1353
+
1354
+ // Menu d'action
1355
+ const { action } = await inquirer.prompt([
1356
+ {
1357
+ type: 'list',
1358
+ name: 'action',
1359
+ message: chalk.white.bold('Action:'),
1360
+ choices: [
1361
+ { name: chalk.cyan.bold('Launch Algo'), value: 'launch' },
1362
+ { name: chalk.yellow('< Back'), value: 'back' }
1363
+ ],
1364
+ pageSize: 5,
1365
+ loop: false
1366
+ }
1367
+ ]);
1368
+
1369
+ if (action === 'launch') {
1370
+ // Sauvegarder la session active
1371
+ activeAlgoSession = {
1372
+ account,
1373
+ contract,
1374
+ settings,
1375
+ startTime: new Date(),
1376
+ status: 'active',
1377
+ pnl: 0,
1378
+ trades: 0,
1379
+ wins: 0,
1380
+ losses: 0
1381
+ };
1382
+
1383
+ // Lancer l'écran de logs
1384
+ await algoLogsScreen(service);
1385
+ }
1386
+ };
1387
+
1388
+ // Fonction pour formater le timestamp (Responsive)
1389
+ const formatTimestamp = () => {
1390
+ const device = getDevice();
1391
+ const now = new Date();
1392
+
1393
+ if (device.isMobile) {
1394
+ // Format court pour mobile: HH:MM
1395
+ return chalk.gray(`[${now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}]`);
1396
+ }
1397
+ return chalk.gray(`[${now.toLocaleTimeString()}]`);
1398
+ };
1399
+
1400
+ // Fonction pour ajouter un log (Responsive)
1401
+ const addLog = (logs, type, message) => {
1402
+ const device = getDevice();
1403
+ const timestamp = formatTimestamp();
1404
+ let coloredMessage;
1405
+
1406
+ // Labels courts pour mobile
1407
+ const labels = device.isMobile ? {
1408
+ info: 'i',
1409
+ success: '[+]',
1410
+ warning: '!',
1411
+ error: '✗',
1412
+ trade: '$',
1413
+ signal: '[>]'
1414
+ } : {
1415
+ info: 'INFO',
1416
+ success: 'SUCCESS',
1417
+ warning: 'WARNING',
1418
+ error: 'ERROR',
1419
+ trade: 'TRADE',
1420
+ signal: 'SIGNAL'
1421
+ };
1422
+
1423
+ switch (type) {
1424
+ case 'info':
1425
+ coloredMessage = chalk.blue(`[${labels.info}] ${message}`);
1426
+ break;
1427
+ case 'success':
1428
+ coloredMessage = chalk.green(`[${labels.success}] ${message}`);
1429
+ break;
1430
+ case 'warning':
1431
+ coloredMessage = chalk.yellow(`[${labels.warning}] ${message}`);
1432
+ break;
1433
+ case 'error':
1434
+ coloredMessage = chalk.red(`[${labels.error}] ${message}`);
1435
+ break;
1436
+ case 'trade':
1437
+ coloredMessage = chalk.magenta(`[${labels.trade}] ${message}`);
1438
+ break;
1439
+ case 'signal':
1440
+ coloredMessage = chalk.cyan(`[${labels.signal}] ${message}`);
1441
+ break;
1442
+ default:
1443
+ coloredMessage = chalk.white(message);
1444
+ }
1445
+
1446
+ logs.push(`${timestamp} ${coloredMessage}`);
1447
+ return logs;
1448
+ };
1449
+
1450
+ // Écran des logs de l'algo (Responsive) - Connected to HQX-Algo Server
1451
+ const algoLogsScreen = async (service) => {
1452
+ let logs = [];
1453
+ let running = true;
1454
+ const device = getDevice();
1455
+ let hqxServer = null;
1456
+ let refreshInterval = null;
1457
+
1458
+ // Header (Responsive)
1459
+ const showHeader = () => {
1460
+ console.clear();
1461
+ const sep = getSeparator('═');
1462
+ const sepLight = getSeparator('─');
1463
+
1464
+ if (device.isMobile) {
1465
+ // 📱 MOBILE: Compact header
1466
+ console.log(chalk.gray(sep));
1467
+ console.log(chalk.magenta.bold(' HQX ULTRA-SCALPING'));
1468
+ console.log(chalk.green.bold(' [*] LIVE'));
1469
+ console.log(chalk.gray(sep));
1470
+ console.log(chalk.cyan(` ${activeAlgoSession.contract.name}`) + chalk.gray(` x${activeAlgoSession.settings.contracts}`));
1471
+
1472
+ // Stats compactes sur une ligne
1473
+ const pnlColor = activeAlgoSession.pnl >= 0 ? chalk.green : chalk.red;
1474
+ console.log(pnlColor(`$${activeAlgoSession.pnl.toFixed(0)}`) +
1475
+ chalk.gray(` W:${activeAlgoSession.wins} L:${activeAlgoSession.losses}`));
1476
+ console.log(chalk.gray(sepLight));
1477
+
1478
+ } else if (device.isTablet) {
1479
+ // 📲 TABLET: Medium header
1480
+ console.log(chalk.gray(sep));
1481
+ console.log(chalk.magenta.bold(' HQX ULTRA-SCALPING') + chalk.green.bold(' - LIVE'));
1482
+ console.log(chalk.gray(sep));
1483
+ console.log();
1484
+ console.log(chalk.white(` ${chalk.cyan(activeAlgoSession.contract.name)} | ${chalk.yellow(activeAlgoSession.settings.contracts + ' contracts')}`));
1485
+ console.log(chalk.white(` Target: ${chalk.green('$' + activeAlgoSession.settings.dailyTarget)} | Risk: ${chalk.red('$' + activeAlgoSession.settings.maxRisk)}`));
1486
+ console.log();
1487
+
1488
+ const pnlColor = activeAlgoSession.pnl >= 0 ? chalk.green : chalk.red;
1489
+ console.log(chalk.gray(sepLight));
1490
+ console.log(chalk.white(` P&L: ${pnlColor('$' + activeAlgoSession.pnl.toFixed(2))} | T:${activeAlgoSession.trades} W:${chalk.green(activeAlgoSession.wins)} L:${chalk.red(activeAlgoSession.losses)}`));
1491
+ console.log(chalk.gray(sepLight));
1492
+ console.log();
1493
+
1494
+ } else {
1495
+ // 💻 DESKTOP: Full header
1496
+ console.log(chalk.gray(sep));
1497
+ console.log(chalk.magenta.bold(' HQX ULTRA-SCALPING') + chalk.green.bold(' - LIVE'));
1498
+ console.log(chalk.gray(sep));
1499
+ console.log();
1500
+ console.log(chalk.white(` Account: ${chalk.cyan(activeAlgoSession.account.name)}`));
1501
+ console.log(chalk.white(` Symbol: ${chalk.cyan(activeAlgoSession.contract.name)}`));
1502
+ console.log(chalk.white(` Contracts: ${chalk.yellow(activeAlgoSession.settings.contracts)}`));
1503
+ console.log(chalk.white(` Target: ${chalk.green('$' + activeAlgoSession.settings.dailyTarget)}`));
1504
+ console.log(chalk.white(` Max Risk: ${chalk.red('$' + activeAlgoSession.settings.maxRisk)}`));
1505
+ console.log();
1506
+
1507
+ const pnlColor = activeAlgoSession.pnl >= 0 ? chalk.green : chalk.red;
1508
+ console.log(chalk.gray(sepLight));
1509
+ console.log(chalk.white(` P&L: ${pnlColor('$' + activeAlgoSession.pnl.toFixed(2))} | Trades: ${chalk.white(activeAlgoSession.trades)} | Wins: ${chalk.green(activeAlgoSession.wins)} | Losses: ${chalk.red(activeAlgoSession.losses)}`));
1510
+ console.log(chalk.gray(sepLight));
1511
+ console.log();
1512
+ }
1513
+ };
1514
+
1515
+ // Afficher les logs (Responsive)
1516
+ const showLogs = () => {
1517
+ const maxLogs = device.isMobile ? 6 : (device.isTablet ? 10 : 15);
1518
+
1519
+ if (!device.isMobile) {
1520
+ console.log(chalk.white.bold(' Logs:'));
1521
+ console.log();
1522
+ }
1523
+
1524
+ const recentLogs = logs.slice(-maxLogs);
1525
+ recentLogs.forEach(log => {
1526
+ console.log(device.isMobile ? ` ${log}` : ` ${log}`);
1527
+ });
1528
+
1529
+ // Remplir les lignes vides
1530
+ for (let i = recentLogs.length; i < maxLogs; i++) {
1531
+ console.log();
1532
+ }
1533
+
1534
+ console.log();
1535
+ console.log(chalk.gray(getSeparator()));
1536
+ console.log(chalk.yellow(device.isMobile ? ' CTRL+C to stop' : ' Press CTRL+C or select Stop to exit'));
1537
+ console.log(chalk.gray(getSeparator()));
1538
+ };
1539
+
1540
+ // Refresh display
1541
+ const refreshDisplay = () => {
1542
+ if (running) {
1543
+ showHeader();
1544
+ showLogs();
1545
+ }
1546
+ };
1547
+
1548
+ // Initialize HQX Server connection
1549
+ const initHQXServer = async () => {
1550
+ hqxServer = new HQXServerService();
1551
+
1552
+ // Setup event listeners
1553
+ hqxServer.on('connected', () => {
1554
+ logs = addLog(logs, 'success', 'Connected to HQX Server');
1555
+ refreshDisplay();
1556
+ });
1557
+
1558
+ hqxServer.on('disconnected', () => {
1559
+ logs = addLog(logs, 'warning', 'Disconnected from HQX Server');
1560
+ refreshDisplay();
1561
+ });
1562
+
1563
+ hqxServer.on('signal', (data) => {
1564
+ const direction = data.direction === 'long' ? 'BUY' : 'SELL';
1565
+ logs = addLog(logs, 'signal', `${direction} @ ${data.price}`);
1566
+ refreshDisplay();
1567
+ });
1568
+
1569
+ hqxServer.on('trade', (data) => {
1570
+ const pnlStr = data.pnl >= 0 ? `+$${data.pnl.toFixed(2)}` : `-$${Math.abs(data.pnl).toFixed(2)}`;
1571
+ logs = addLog(logs, 'trade', `${data.type.toUpperCase()} | P&L: ${pnlStr}`);
1572
+
1573
+ // Update session stats
1574
+ activeAlgoSession.trades++;
1575
+ activeAlgoSession.pnl += data.pnl;
1576
+ if (data.pnl > 0) {
1577
+ activeAlgoSession.wins++;
1578
+ } else {
1579
+ activeAlgoSession.losses++;
1580
+ }
1581
+ refreshDisplay();
1582
+ });
1583
+
1584
+ hqxServer.on('log', (data) => {
1585
+ logs = addLog(logs, data.type || 'info', data.message);
1586
+ refreshDisplay();
1587
+ });
1588
+
1589
+ hqxServer.on('stats', (data) => {
1590
+ if (data.pnl !== undefined) activeAlgoSession.pnl = data.pnl;
1591
+ if (data.trades !== undefined) activeAlgoSession.trades = data.trades;
1592
+ if (data.wins !== undefined) activeAlgoSession.wins = data.wins;
1593
+ if (data.losses !== undefined) activeAlgoSession.losses = data.losses;
1594
+ refreshDisplay();
1595
+ });
1596
+
1597
+ hqxServer.on('error', (data) => {
1598
+ logs = addLog(logs, 'error', data.message || 'Unknown error');
1599
+ refreshDisplay();
1600
+ });
1601
+
1602
+ return hqxServer;
1603
+ };
1604
+
1605
+ // Logs initiaux
1606
+ logs = addLog(logs, 'info', 'Initializing HQX Ultra-Scalping...');
1607
+ logs = addLog(logs, 'info', `Connecting to ${service.getPropfirmName()}...`);
1608
+
1609
+ // Afficher l'écran initial
1610
+ showHeader();
1611
+ showLogs();
1612
+
1613
+ // Try to connect to HQX Server
1614
+ try {
1615
+ await initHQXServer();
1616
+
1617
+ // Get PropFirm token for market data
1618
+ const propfirmToken = service.getToken();
1619
+
1620
+ // Authenticate with HQX Server (using propfirm token as API key for now)
1621
+ logs = addLog(logs, 'info', 'Authenticating with HQX Server...');
1622
+ refreshDisplay();
1623
+
1624
+ const authResult = await hqxServer.authenticate(propfirmToken).catch(() => null);
1625
+
1626
+ if (authResult && authResult.success) {
1627
+ logs = addLog(logs, 'success', 'Authenticated');
1628
+
1629
+ // Connect WebSocket
1630
+ await hqxServer.connect().catch(() => null);
1631
+
1632
+ // Start algo
1633
+ hqxServer.startAlgo({
1634
+ accountId: activeAlgoSession.account.id,
1635
+ contractId: activeAlgoSession.contract.id,
1636
+ symbol: activeAlgoSession.contract.name,
1637
+ contracts: activeAlgoSession.settings.contracts,
1638
+ dailyTarget: activeAlgoSession.settings.dailyTarget,
1639
+ maxRisk: activeAlgoSession.settings.maxRisk,
1640
+ propfirm: service.propfirm,
1641
+ propfirmToken: propfirmToken
1642
+ });
1643
+
1644
+ logs = addLog(logs, 'success', 'Algo started');
1645
+ } else {
1646
+ // Fallback to simulation mode if HQX Server not available
1647
+ logs = addLog(logs, 'warning', 'HQX Server unavailable - Simulation mode');
1648
+ logs = addLog(logs, 'info', 'Running in demo mode');
1649
+
1650
+ // Start simulation
1651
+ startSimulation();
1652
+ }
1653
+ } catch (error) {
1654
+ // Fallback to simulation
1655
+ logs = addLog(logs, 'warning', 'HQX Server unavailable - Simulation mode');
1656
+ startSimulation();
1657
+ }
1658
+
1659
+ refreshDisplay();
1660
+
1661
+ // Simulation mode (when HQX Server not available)
1662
+ function startSimulation() {
1663
+ logs = addLog(logs, 'success', 'Engine started (Simulation)');
1664
+ refreshDisplay();
1665
+
1666
+ refreshInterval = setInterval(() => {
1667
+ if (!running) {
1668
+ clearInterval(refreshInterval);
1669
+ return;
1670
+ }
1671
+
1672
+ const randomMsgs = [
1673
+ { type: 'info', msg: 'Monitoring market...' },
1674
+ { type: 'signal', msg: 'Scanning for entry...' },
1675
+ { type: 'info', msg: 'No positions' },
1676
+ { type: 'info', msg: 'Risk: OK' },
1677
+ { type: 'signal', msg: 'Analyzing order flow...' },
1678
+ { type: 'info', msg: 'Volatility: Normal' }
1679
+ ];
1680
+ const randomMsg = randomMsgs[Math.floor(Math.random() * randomMsgs.length)];
1681
+ logs = addLog(logs, randomMsg.type, randomMsg.msg);
1682
+ refreshDisplay();
1683
+ }, 3000);
1684
+ }
1685
+
1686
+ // Wait for user to stop
1687
+ await new Promise(resolve => setTimeout(resolve, 2000));
1688
+
1689
+ const { stopAction } = await inquirer.prompt([
1690
+ {
1691
+ type: 'list',
1692
+ name: 'stopAction',
1693
+ message: chalk.red.bold(''),
1694
+ choices: [
1695
+ { name: chalk.red.bold('Stop Algo'), value: 'stop' }
1696
+ ],
1697
+ pageSize: 1,
1698
+ loop: false
1699
+ }
1700
+ ]);
1701
+
1702
+ if (stopAction === 'stop') {
1703
+ running = false;
1704
+ activeAlgoSession.status = 'stopped';
1705
+
1706
+ // Clear simulation interval if running
1707
+ if (refreshInterval) {
1708
+ clearInterval(refreshInterval);
1709
+ }
1710
+
1711
+ logs = addLog(logs, 'warning', 'Stop signal received');
1712
+ logs = addLog(logs, 'info', 'Closing all positions...');
1713
+
1714
+ // Stop algo on HQX Server
1715
+ if (hqxServer && hqxServer.isConnected()) {
1716
+ hqxServer.stopAlgo();
1717
+ }
1718
+
1719
+ await new Promise(resolve => setTimeout(resolve, 500));
1720
+ logs = addLog(logs, 'success', 'All positions closed');
1721
+ logs = addLog(logs, 'info', 'Disconnecting...');
1722
+
1723
+ // Disconnect from HQX Server
1724
+ if (hqxServer) {
1725
+ hqxServer.disconnect();
1726
+ }
1727
+
1728
+ await new Promise(resolve => setTimeout(resolve, 500));
1729
+ logs = addLog(logs, 'success', 'Algo stopped successfully');
1730
+
1731
+ showHeader();
1732
+ showLogs();
1733
+
1734
+ console.log();
1735
+ console.log(chalk.yellow.bold(' Algo stopped.'));
1736
+ console.log();
1737
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1738
+ }
1739
+ };
1740
+
1741
+ // Fonction pour gérer les mises à jour
1742
+ const handleUpdate = async () => {
1743
+ const pkg = require('../package.json');
1744
+ const currentVersion = pkg.version;
1745
+
1746
+ const spinnerRefresh = ora('Checking for updates...').start();
1747
+ try {
1748
+ const cliDir = path.resolve(__dirname, '..');
1749
+
1750
+ // Check if git repo exists
1751
+ try {
1752
+ execSync('git status', { cwd: cliDir, stdio: 'pipe' });
1753
+ } catch (e) {
1754
+ throw new Error('Not a git repository');
1755
+ }
1756
+
1757
+ // Check if remote exists
1758
+ let hasRemote = false;
1759
+ try {
1760
+ const gitRemoteUrl = execSync('git remote get-url origin', { cwd: cliDir, stdio: 'pipe' }).toString().trim();
1761
+ hasRemote = gitRemoteUrl.length > 0;
1762
+ } catch (e) {
1763
+ hasRemote = false;
1764
+ }
1765
+
1766
+ if (hasRemote) {
1767
+ // Get current commit before pull
1768
+ const beforeCommit = execSync('git rev-parse --short HEAD', { cwd: cliDir, stdio: 'pipe' }).toString().trim();
1769
+
1770
+ // Fetch first to check if updates available
1771
+ execSync('git fetch origin main', { cwd: cliDir, stdio: 'pipe' });
1772
+
1773
+ // Check if we're behind
1774
+ const behindCount = execSync('git rev-list HEAD..origin/main --count', { cwd: cliDir, stdio: 'pipe' }).toString().trim();
1775
+
1776
+ if (parseInt(behindCount) > 0) {
1777
+ // Check for local changes that might block pull
1778
+ let hasLocalChanges = false;
1779
+ try {
1780
+ const statusOutput = execSync('git status --porcelain', { cwd: cliDir, stdio: 'pipe' }).toString().trim();
1781
+ hasLocalChanges = statusOutput.length > 0;
1782
+ } catch (e) {
1783
+ hasLocalChanges = false;
1784
+ }
1785
+
1786
+ // If there are local changes, stash them or reset
1787
+ if (hasLocalChanges) {
1788
+ spinnerRefresh.text = 'Stashing local changes...';
1789
+ try {
1790
+ // Try to stash changes first
1791
+ execSync('git stash --include-untracked', { cwd: cliDir, stdio: 'pipe' });
1792
+ } catch (e) {
1793
+ // If stash fails, do a hard reset (for generated files like package-lock.json)
1794
+ spinnerRefresh.text = 'Resetting local changes...';
1795
+ execSync('git checkout -- .', { cwd: cliDir, stdio: 'pipe' });
1796
+ execSync('git clean -fd', { cwd: cliDir, stdio: 'pipe' });
1797
+ }
1798
+ }
1799
+
1800
+ spinnerRefresh.text = 'Downloading updates...';
1801
+
1802
+ // Pull from remote
1803
+ execSync('git pull origin main', { cwd: cliDir, stdio: 'pipe' });
1804
+ const afterCommit = execSync('git rev-parse --short HEAD', { cwd: cliDir, stdio: 'pipe' }).toString().trim();
1805
+
1806
+ // Reinstall dependencies if package.json changed
1807
+ spinnerRefresh.text = 'Installing dependencies...';
1808
+ try {
1809
+ execSync('npm install --silent', { cwd: cliDir, stdio: 'pipe' });
1810
+ } catch (e) {
1811
+ // Ignore npm install errors
1812
+ }
1813
+
1814
+ // Re-read package.json to get new version
1815
+ delete require.cache[require.resolve('../package.json')];
1816
+ const newPkg = require('../package.json');
1817
+ const newVersion = newPkg.version;
1818
+
1819
+ spinnerRefresh.succeed('CLI updated!');
1820
+ console.log();
1821
+ console.log(chalk.green(` Version: v${currentVersion} -> v${newVersion}`));
1822
+ console.log(chalk.gray(` Commits: ${beforeCommit} -> ${afterCommit} (${behindCount} new)`));
1823
+ console.log();
1824
+
1825
+ // Ask user if they want to restart
1826
+ const { restart } = await inquirer.prompt([
1827
+ {
1828
+ type: 'confirm',
1829
+ name: 'restart',
1830
+ message: chalk.yellow('Restart CLI to apply changes?'),
1831
+ default: true
1832
+ }
1833
+ ]);
1834
+
1835
+ if (restart) {
1836
+ console.log(chalk.cyan(' Restarting...'));
1837
+ console.log();
1838
+
1839
+ // Clear require cache to reload modules
1840
+ Object.keys(require.cache).forEach(key => {
1841
+ delete require.cache[key];
1842
+ });
1843
+
1844
+ // Restart by spawning a new process and replacing current one
1845
+ const { spawn } = require('child_process');
1846
+ const child = spawn(process.argv[0], [path.join(cliDir, 'bin', 'cli.js')], {
1847
+ cwd: cliDir,
1848
+ stdio: 'inherit',
1849
+ shell: true
1850
+ });
1851
+
1852
+ child.on('exit', (code) => {
1853
+ process.exit(code);
1854
+ });
1855
+
1856
+ // Prevent current process from continuing
1857
+ return;
1858
+ }
1859
+ } else {
1860
+ spinnerRefresh.succeed('Already up to date!');
1861
+ console.log(chalk.cyan(` Version: v${currentVersion}`));
1862
+ console.log(chalk.gray(` Commit: ${beforeCommit}`));
1863
+ }
1864
+ } else {
1865
+ spinnerRefresh.succeed('Data refreshed');
1866
+ console.log(chalk.cyan(` Version: v${currentVersion} (local dev mode)`));
1867
+ }
1868
+
1869
+ // Refresh user data
1870
+ if (currentService) {
1871
+ await currentService.getUser();
1872
+ }
1873
+
1874
+ } catch (err) {
1875
+ spinnerRefresh.fail('Update failed');
1876
+ console.log(chalk.red(` Error: ${err.message}`));
1877
+ console.log(chalk.gray(' Your session is still active.'));
1878
+ }
1879
+ console.log();
1880
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
1881
+ };
1882
+
1883
+ // Fonction principale
1884
+ const main = async () => {
1885
+ await banner();
1886
+
1887
+ // Essayer de restaurer les sessions précédentes
1888
+ const spinner = ora('Restoring session...').start();
1889
+ const restored = await connections.restoreFromStorage();
1890
+
1891
+ if (restored) {
1892
+ spinner.succeed('Session restored!');
1893
+ currentService = connections.services[0].service;
1894
+
1895
+ // Aller directement au dashboard
1896
+ let connected = true;
1897
+ while (connected) {
1898
+ await banner();
1899
+ const action = await dashboardMenu(currentService);
1900
+
1901
+ switch (action) {
1902
+ case 'accounts':
1903
+ await showAccounts(currentService);
1904
+ break;
1905
+ case 'positions':
1906
+ await showPositions(currentService);
1907
+ break;
1908
+ case 'orders':
1909
+ await showOrders(currentService);
1910
+ break;
1911
+ case 'stats':
1912
+ await showStats(currentService);
1913
+ break;
1914
+ case 'userinfo':
1915
+ await showUserInfo(currentService);
1916
+ break;
1917
+ case 'add_prop_account':
1918
+ await addPropAccount();
1919
+ break;
1920
+ case 'algotrading':
1921
+ let algoRunning = true;
1922
+ while (algoRunning) {
1923
+ await banner();
1924
+ const algoResult = await algoTradingMenu(currentService);
1925
+ if (algoResult === 'back') {
1926
+ algoRunning = false;
1927
+ }
1928
+ }
1929
+ break;
1930
+ case 'refresh':
1931
+ await handleUpdate();
1932
+ break;
1933
+ case 'disconnect':
1934
+ connections.disconnectAll();
1935
+ currentService = null;
1936
+ connected = false;
1937
+ await banner();
1938
+ console.log(chalk.yellow(' All connections disconnected.'));
1939
+ console.log();
1940
+ break;
1941
+ }
1942
+ }
1943
+ } else {
1944
+ spinner.stop();
1945
+ }
1946
+
1947
+ let running = true;
1948
+
1949
+ while (running) {
1950
+ const connection = await mainMenu();
1951
+
1952
+ switch (connection) {
1953
+ case 'projectx':
1954
+ const propfirm = await projectXMenu();
1955
+ if (propfirm === 'back') {
1956
+ await banner();
1957
+ continue;
1958
+ }
1959
+
1960
+ // Créer le service
1961
+ currentService = new ProjectXService(propfirm);
1962
+
1963
+ // Login
1964
+ const credentials = await loginPrompt(currentService.getPropfirmName());
1965
+
1966
+ const spinner = ora('Authenticating...').start();
1967
+ const loginResult = await currentService.login(credentials.username, credentials.password);
1968
+
1969
+ if (loginResult.success) {
1970
+ // Récupérer les infos utilisateur
1971
+ await currentService.getUser();
1972
+
1973
+ // Ajouter au connection manager
1974
+ connections.add('projectx', currentService, currentService.getPropfirmName());
1975
+
1976
+ spinner.succeed('Connected successfully!');
1977
+
1978
+ // Dashboard loop
1979
+ let connected = true;
1980
+ while (connected) {
1981
+ await banner();
1982
+ const action = await dashboardMenu(currentService);
1983
+
1984
+ switch (action) {
1985
+ case 'accounts':
1986
+ await showAccounts(currentService);
1987
+ break;
1988
+ case 'positions':
1989
+ await showPositions(currentService);
1990
+ break;
1991
+ case 'orders':
1992
+ await showOrders(currentService);
1993
+ break;
1994
+ case 'stats':
1995
+ await showStats(currentService);
1996
+ break;
1997
+ case 'userinfo':
1998
+ await showUserInfo(currentService);
1999
+ break;
2000
+ case 'algotrading':
2001
+ let algoRunning = true;
2002
+ while (algoRunning) {
2003
+ await banner();
2004
+ const algoResult = await algoTradingMenu(currentService);
2005
+ if (algoResult === 'back') {
2006
+ algoRunning = false;
2007
+ }
2008
+ }
2009
+ break;
2010
+ case 'add_prop_account':
2011
+ await addPropAccount();
2012
+ break;
2013
+ case 'refresh':
2014
+ await handleUpdate();
2015
+ break;
2016
+ case 'disconnect':
2017
+ // Déconnecter toutes les connexions
2018
+ connections.disconnectAll();
2019
+ currentService = null;
2020
+ connected = false;
2021
+ await banner();
2022
+ console.log(chalk.yellow(' All connections disconnected.'));
2023
+ console.log();
2024
+ break;
2025
+ }
2026
+ }
2027
+ } else {
2028
+ spinner.fail('Authentication failed');
2029
+ console.log(chalk.red(` Error: ${loginResult.error}`));
2030
+ console.log();
2031
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
2032
+ await banner();
2033
+ }
2034
+ break;
2035
+
2036
+ case 'rithmic':
2037
+ console.log();
2038
+ console.log(chalk.cyan('Rithmic connection...'));
2039
+ console.log(chalk.gray('Feature coming soon!'));
2040
+ console.log();
2041
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
2042
+ await banner();
2043
+ break;
2044
+
2045
+ case 'tradovate':
2046
+ console.log();
2047
+ console.log(chalk.cyan('Tradovate connection...'));
2048
+ console.log(chalk.gray('Feature coming soon!'));
2049
+ console.log();
2050
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
2051
+ await banner();
2052
+ break;
2053
+
2054
+ case 'exit':
2055
+ console.log();
2056
+ console.log(chalk.yellow('Goodbye!'));
2057
+ running = false;
2058
+ break;
2059
+ }
2060
+ }
2061
+ };
2062
+
2063
+ // Configuration CLI
2064
+ const packageInfo = require('../package.json');
2065
+ program
2066
+ .name('hedgequantx')
2067
+ .description('Prop Futures Algo Trading CLI')
2068
+ .version(packageInfo.version);
2069
+
2070
+ program
2071
+ .command('status')
2072
+ .description('Show system status')
2073
+ .action(() => {
2074
+ console.log(chalk.green('System Status: Online'));
2075
+ });
2076
+
2077
+ program
2078
+ .command('start')
2079
+ .description('Start trading')
2080
+ .action(() => {
2081
+ console.log(chalk.green('Starting trading engine...'));
2082
+ });
2083
+
2084
+ program
2085
+ .command('stop')
2086
+ .description('Stop trading')
2087
+ .action(() => {
2088
+ console.log(chalk.red('Stopping trading engine...'));
2089
+ });
2090
+
2091
+ // Si aucune commande, lancer le menu interactif
2092
+ if (!process.argv.slice(2).length) {
2093
+ main().catch(console.error);
2094
+ } else {
2095
+ program.parse();
2096
+ }