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/package.json +1 -1
- package/src/app.js +10 -597
- package/src/menus/connect.js +402 -0
- package/src/menus/dashboard.js +214 -0
- package/src/menus/index.js +8 -0
- package/src/pages/algo/copy-trading.js +404 -0
- package/src/pages/algo/index.js +51 -0
- package/src/pages/algo/one-account.js +352 -0
- package/src/pages/algo/ui.js +271 -0
- package/src/pages/algo.js +3 -2277
- package/src/services/hqx-server.js +27 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -6,22 +6,18 @@
|
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const inquirer = require('inquirer');
|
|
8
8
|
const ora = require('ora');
|
|
9
|
-
const figlet = require('figlet');
|
|
10
|
-
const { execSync, spawn } = require('child_process');
|
|
11
|
-
const path = require('path');
|
|
12
9
|
|
|
13
|
-
const {
|
|
14
|
-
const {
|
|
15
|
-
const { TradovateService } = require('./services/tradovate');
|
|
16
|
-
const { PROPFIRM_CHOICES, getPropFirmsByPlatform, getPropFirm } = require('./config');
|
|
17
|
-
const { getDevice, getSeparator, printLogo, getLogoWidth, drawBoxHeader, drawBoxFooter, centerText, createBoxMenu } = require('./ui');
|
|
18
|
-
const { validateUsername, validatePassword, maskSensitive } = require('./security');
|
|
10
|
+
const { connections } = require('./services');
|
|
11
|
+
const { getLogoWidth, centerText } = require('./ui');
|
|
19
12
|
|
|
20
13
|
// Pages
|
|
21
14
|
const { showStats } = require('./pages/stats');
|
|
22
15
|
const { showAccounts } = require('./pages/accounts');
|
|
23
16
|
const { algoTradingMenu } = require('./pages/algo');
|
|
24
17
|
|
|
18
|
+
// Menus
|
|
19
|
+
const { projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu, dashboardMenu, handleUpdate } = require('./menus');
|
|
20
|
+
|
|
25
21
|
// Current service reference
|
|
26
22
|
let currentService = null;
|
|
27
23
|
let currentPlatform = null; // 'projectx' or 'rithmic'
|
|
@@ -87,7 +83,10 @@ process.on('unhandledRejection', (reason) => {
|
|
|
87
83
|
*/
|
|
88
84
|
const banner = async () => {
|
|
89
85
|
console.clear();
|
|
90
|
-
const
|
|
86
|
+
const termWidth = process.stdout.columns || 100;
|
|
87
|
+
const isMobile = termWidth < 60;
|
|
88
|
+
// Logo HEDGEQUANTX + X = 94 chars, need 98 for box (94 + 2 borders + 2 padding)
|
|
89
|
+
const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
|
|
91
90
|
const innerWidth = boxWidth - 2;
|
|
92
91
|
const version = require('../package.json').version;
|
|
93
92
|
|
|
@@ -123,8 +122,6 @@ const banner = async () => {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
// Draw logo - compact for mobile, full for desktop
|
|
126
|
-
const termWidth = process.stdout.columns || 80;
|
|
127
|
-
const isMobile = termWidth < 60;
|
|
128
125
|
|
|
129
126
|
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
130
127
|
|
|
@@ -222,391 +219,6 @@ const banner = async () => {
|
|
|
222
219
|
console.log();
|
|
223
220
|
};
|
|
224
221
|
|
|
225
|
-
/**
|
|
226
|
-
* Login prompt with validation
|
|
227
|
-
* @param {string} propfirmName - PropFirm display name
|
|
228
|
-
* @returns {Promise<{username: string, password: string}>}
|
|
229
|
-
*/
|
|
230
|
-
const loginPrompt = async (propfirmName) => {
|
|
231
|
-
const device = getDevice();
|
|
232
|
-
console.log();
|
|
233
|
-
console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
|
|
234
|
-
console.log();
|
|
235
|
-
|
|
236
|
-
const credentials = await inquirer.prompt([
|
|
237
|
-
{
|
|
238
|
-
type: 'input',
|
|
239
|
-
name: 'username',
|
|
240
|
-
message: chalk.white.bold('Username:'),
|
|
241
|
-
validate: (input) => {
|
|
242
|
-
try {
|
|
243
|
-
validateUsername(input);
|
|
244
|
-
return true;
|
|
245
|
-
} catch (e) {
|
|
246
|
-
return e.message;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
type: 'password',
|
|
252
|
-
name: 'password',
|
|
253
|
-
message: chalk.white.bold('Password:'),
|
|
254
|
-
mask: '*',
|
|
255
|
-
validate: (input) => {
|
|
256
|
-
try {
|
|
257
|
-
validatePassword(input);
|
|
258
|
-
return true;
|
|
259
|
-
} catch (e) {
|
|
260
|
-
return e.message;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
]);
|
|
265
|
-
|
|
266
|
-
return credentials;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* ProjectX platform connection menu
|
|
271
|
-
*/
|
|
272
|
-
const projectXMenu = async () => {
|
|
273
|
-
const propfirms = getPropFirmsByPlatform('ProjectX');
|
|
274
|
-
const boxWidth = getLogoWidth();
|
|
275
|
-
const innerWidth = boxWidth - 2;
|
|
276
|
-
const numCols = 3;
|
|
277
|
-
const colWidth = Math.floor(innerWidth / numCols);
|
|
278
|
-
|
|
279
|
-
// Build numbered list
|
|
280
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
281
|
-
num: i + 1,
|
|
282
|
-
key: pf.key,
|
|
283
|
-
name: pf.displayName
|
|
284
|
-
}));
|
|
285
|
-
|
|
286
|
-
// PropFirm selection box
|
|
287
|
-
console.log();
|
|
288
|
-
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
289
|
-
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
|
|
290
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
291
|
-
|
|
292
|
-
// Display in 3 columns with fixed width alignment
|
|
293
|
-
const rows = Math.ceil(numbered.length / numCols);
|
|
294
|
-
const maxNum = numbered.length;
|
|
295
|
-
const numWidth = maxNum >= 10 ? 4 : 3; // [XX] or [X]
|
|
296
|
-
|
|
297
|
-
for (let row = 0; row < rows; row++) {
|
|
298
|
-
let line = '';
|
|
299
|
-
for (let col = 0; col < numCols; col++) {
|
|
300
|
-
const idx = row + col * rows;
|
|
301
|
-
if (idx < numbered.length) {
|
|
302
|
-
const item = numbered[idx];
|
|
303
|
-
const numStr = item.num.toString().padStart(2, ' ');
|
|
304
|
-
const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
305
|
-
const textLen = 4 + 1 + item.name.length; // [XX] + space + name
|
|
306
|
-
const padding = colWidth - textLen - 2;
|
|
307
|
-
line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
|
|
308
|
-
} else {
|
|
309
|
-
line += ' '.repeat(colWidth);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Adjust for exact width
|
|
313
|
-
const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
314
|
-
const adjust = innerWidth - lineLen;
|
|
315
|
-
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
319
|
-
const backText = ' ' + chalk.red('[X] Back');
|
|
320
|
-
const backLen = '[X] Back'.length + 2;
|
|
321
|
-
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
322
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
323
|
-
console.log();
|
|
324
|
-
|
|
325
|
-
const validInputs = numbered.map(n => n.num.toString());
|
|
326
|
-
validInputs.push('x', 'X');
|
|
327
|
-
|
|
328
|
-
const { action } = await inquirer.prompt([
|
|
329
|
-
{
|
|
330
|
-
type: 'input',
|
|
331
|
-
name: 'action',
|
|
332
|
-
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
333
|
-
validate: (input) => {
|
|
334
|
-
if (validInputs.includes(input)) return true;
|
|
335
|
-
return `Please enter 1-${numbered.length} or X`;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
]);
|
|
339
|
-
|
|
340
|
-
if (action.toLowerCase() === 'x') return null;
|
|
341
|
-
|
|
342
|
-
const selectedIdx = parseInt(action) - 1;
|
|
343
|
-
const selectedPropfirm = numbered[selectedIdx];
|
|
344
|
-
|
|
345
|
-
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
346
|
-
const spinner = ora('Authenticating...').start();
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const service = new ProjectXService(selectedPropfirm.key);
|
|
350
|
-
const result = await service.login(credentials.username, credentials.password);
|
|
351
|
-
|
|
352
|
-
if (result.success) {
|
|
353
|
-
await service.getUser();
|
|
354
|
-
connections.add('projectx', service, service.propfirm.name);
|
|
355
|
-
currentService = service;
|
|
356
|
-
currentPlatform = 'projectx';
|
|
357
|
-
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
358
|
-
return service;
|
|
359
|
-
} else {
|
|
360
|
-
spinner.fail(result.error || 'Authentication failed');
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
} catch (error) {
|
|
364
|
-
spinner.fail(error.message);
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Rithmic platform connection menu
|
|
371
|
-
*/
|
|
372
|
-
const rithmicMenu = async () => {
|
|
373
|
-
const propfirms = getPropFirmsByPlatform('Rithmic');
|
|
374
|
-
const boxWidth = getLogoWidth();
|
|
375
|
-
const innerWidth = boxWidth - 2;
|
|
376
|
-
const numCols = 3;
|
|
377
|
-
const colWidth = Math.floor(innerWidth / numCols);
|
|
378
|
-
|
|
379
|
-
// Build numbered list
|
|
380
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
381
|
-
num: i + 1,
|
|
382
|
-
key: pf.key,
|
|
383
|
-
name: pf.displayName,
|
|
384
|
-
systemName: pf.rithmicSystem
|
|
385
|
-
}));
|
|
386
|
-
|
|
387
|
-
// PropFirm selection box
|
|
388
|
-
console.log();
|
|
389
|
-
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
390
|
-
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
|
|
391
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
392
|
-
|
|
393
|
-
// Display in 3 columns with fixed width alignment
|
|
394
|
-
const rows = Math.ceil(numbered.length / numCols);
|
|
395
|
-
|
|
396
|
-
for (let row = 0; row < rows; row++) {
|
|
397
|
-
let line = '';
|
|
398
|
-
for (let col = 0; col < numCols; col++) {
|
|
399
|
-
const idx = row + col * rows;
|
|
400
|
-
if (idx < numbered.length) {
|
|
401
|
-
const item = numbered[idx];
|
|
402
|
-
const numStr = item.num.toString().padStart(2, ' ');
|
|
403
|
-
const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
404
|
-
const textLen = 4 + 1 + item.name.length;
|
|
405
|
-
const padding = colWidth - textLen - 2;
|
|
406
|
-
line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
|
|
407
|
-
} else {
|
|
408
|
-
line += ' '.repeat(colWidth);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
412
|
-
const adjust = innerWidth - lineLen;
|
|
413
|
-
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
417
|
-
const backText = ' ' + chalk.red('[X] Back');
|
|
418
|
-
const backLen = '[X] Back'.length + 2;
|
|
419
|
-
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
420
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
421
|
-
console.log();
|
|
422
|
-
|
|
423
|
-
const validInputs = numbered.map(n => n.num.toString());
|
|
424
|
-
validInputs.push('x', 'X');
|
|
425
|
-
|
|
426
|
-
const { action } = await inquirer.prompt([
|
|
427
|
-
{
|
|
428
|
-
type: 'input',
|
|
429
|
-
name: 'action',
|
|
430
|
-
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
431
|
-
validate: (input) => {
|
|
432
|
-
if (validInputs.includes(input)) return true;
|
|
433
|
-
return `Please enter 1-${numbered.length} or X`;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
]);
|
|
437
|
-
|
|
438
|
-
if (action.toLowerCase() === 'x') return null;
|
|
439
|
-
|
|
440
|
-
const selectedIdx = parseInt(action) - 1;
|
|
441
|
-
const selectedPropfirm = numbered[selectedIdx];
|
|
442
|
-
|
|
443
|
-
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
444
|
-
const spinner = ora('Connecting to Rithmic...').start();
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const service = new RithmicService(selectedPropfirm.key);
|
|
448
|
-
const result = await service.login(credentials.username, credentials.password);
|
|
449
|
-
|
|
450
|
-
if (result.success) {
|
|
451
|
-
spinner.text = 'Fetching accounts...';
|
|
452
|
-
const accResult = await service.getTradingAccounts();
|
|
453
|
-
|
|
454
|
-
connections.add('rithmic', service, service.propfirm.name);
|
|
455
|
-
currentService = service;
|
|
456
|
-
currentPlatform = 'rithmic';
|
|
457
|
-
spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
|
|
458
|
-
|
|
459
|
-
// Small pause to see the success message
|
|
460
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
461
|
-
return service;
|
|
462
|
-
} else {
|
|
463
|
-
spinner.fail(result.error || 'Authentication failed');
|
|
464
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
} catch (error) {
|
|
468
|
-
spinner.fail(`Connection error: ${error.message}`);
|
|
469
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Tradovate platform connection menu
|
|
476
|
-
*/
|
|
477
|
-
const tradovateMenu = async () => {
|
|
478
|
-
const propfirms = getPropFirmsByPlatform('Tradovate');
|
|
479
|
-
const boxWidth = getLogoWidth();
|
|
480
|
-
const innerWidth = boxWidth - 2;
|
|
481
|
-
|
|
482
|
-
// Build numbered list
|
|
483
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
484
|
-
num: i + 1,
|
|
485
|
-
key: pf.key,
|
|
486
|
-
name: pf.displayName
|
|
487
|
-
}));
|
|
488
|
-
|
|
489
|
-
// PropFirm selection box
|
|
490
|
-
console.log();
|
|
491
|
-
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
492
|
-
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
|
|
493
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
494
|
-
|
|
495
|
-
// Display propfirms
|
|
496
|
-
for (const item of numbered) {
|
|
497
|
-
const numStr = item.num.toString().padStart(2, ' ');
|
|
498
|
-
const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
499
|
-
const textLen = 4 + 1 + item.name.length + 2;
|
|
500
|
-
console.log(chalk.cyan('║') + text + ' '.repeat(innerWidth - textLen) + chalk.cyan('║'));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
504
|
-
const backText = ' ' + chalk.red('[X] Back');
|
|
505
|
-
const backLen = '[X] Back'.length + 2;
|
|
506
|
-
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
507
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
508
|
-
console.log();
|
|
509
|
-
|
|
510
|
-
const validInputs = numbered.map(n => n.num.toString());
|
|
511
|
-
validInputs.push('x', 'X');
|
|
512
|
-
|
|
513
|
-
const { action } = await inquirer.prompt([
|
|
514
|
-
{
|
|
515
|
-
type: 'input',
|
|
516
|
-
name: 'action',
|
|
517
|
-
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
518
|
-
validate: (input) => {
|
|
519
|
-
if (validInputs.includes(input)) return true;
|
|
520
|
-
return `Please enter 1-${numbered.length} or X`;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
]);
|
|
524
|
-
|
|
525
|
-
if (action.toLowerCase() === 'x') return null;
|
|
526
|
-
|
|
527
|
-
const selectedIdx = parseInt(action) - 1;
|
|
528
|
-
const selectedPropfirm = numbered[selectedIdx];
|
|
529
|
-
|
|
530
|
-
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
531
|
-
const spinner = ora('Connecting to Tradovate...').start();
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const service = new TradovateService(selectedPropfirm.key);
|
|
535
|
-
const result = await service.login(credentials.username, credentials.password);
|
|
536
|
-
|
|
537
|
-
if (result.success) {
|
|
538
|
-
spinner.text = 'Fetching accounts...';
|
|
539
|
-
await service.getTradingAccounts();
|
|
540
|
-
|
|
541
|
-
connections.add('tradovate', service, service.propfirm.name);
|
|
542
|
-
currentService = service;
|
|
543
|
-
currentPlatform = 'tradovate';
|
|
544
|
-
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
545
|
-
return service;
|
|
546
|
-
} else {
|
|
547
|
-
spinner.fail(result.error || 'Authentication failed');
|
|
548
|
-
return null;
|
|
549
|
-
}
|
|
550
|
-
} catch (error) {
|
|
551
|
-
spinner.fail(error.message);
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Add Prop Account menu (select platform)
|
|
558
|
-
*/
|
|
559
|
-
const addPropAccountMenu = async () => {
|
|
560
|
-
const boxWidth = getLogoWidth();
|
|
561
|
-
const innerWidth = boxWidth - 2;
|
|
562
|
-
const col1Width = Math.floor(innerWidth / 2);
|
|
563
|
-
const col2Width = innerWidth - col1Width;
|
|
564
|
-
|
|
565
|
-
console.log();
|
|
566
|
-
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
567
|
-
console.log(chalk.cyan('║') + chalk.white.bold(centerText('ADD PROP ACCOUNT', innerWidth)) + chalk.cyan('║'));
|
|
568
|
-
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
569
|
-
|
|
570
|
-
const menuRow = (left, right) => {
|
|
571
|
-
const leftText = ' ' + left;
|
|
572
|
-
const rightText = right ? ' ' + right : '';
|
|
573
|
-
const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
574
|
-
const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
575
|
-
const leftPad = col1Width - leftLen;
|
|
576
|
-
const rightPad = col2Width - rightLen;
|
|
577
|
-
console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
|
|
581
|
-
menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
|
|
582
|
-
|
|
583
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
584
|
-
console.log();
|
|
585
|
-
|
|
586
|
-
const { action } = await inquirer.prompt([
|
|
587
|
-
{
|
|
588
|
-
type: 'input',
|
|
589
|
-
name: 'action',
|
|
590
|
-
message: chalk.cyan('Enter choice (1/2/3/X):'),
|
|
591
|
-
validate: (input) => {
|
|
592
|
-
const valid = ['1', '2', '3', 'x', 'X'];
|
|
593
|
-
if (valid.includes(input)) return true;
|
|
594
|
-
return 'Please enter 1, 2, 3 or X';
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
]);
|
|
598
|
-
|
|
599
|
-
const actionMap = {
|
|
600
|
-
'1': 'projectx',
|
|
601
|
-
'2': 'rithmic',
|
|
602
|
-
'3': 'tradovate',
|
|
603
|
-
'x': null,
|
|
604
|
-
'X': null
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
return actionMap[action];
|
|
608
|
-
};
|
|
609
|
-
|
|
610
222
|
/**
|
|
611
223
|
* Main connection menu
|
|
612
224
|
*/
|
|
@@ -663,205 +275,6 @@ const mainMenu = async () => {
|
|
|
663
275
|
return actionMap[action] || 'exit';
|
|
664
276
|
};
|
|
665
277
|
|
|
666
|
-
/**
|
|
667
|
-
* Dashboard menu after login
|
|
668
|
-
* @param {Object} service - Connected service
|
|
669
|
-
*/
|
|
670
|
-
const dashboardMenu = async (service) => {
|
|
671
|
-
const user = service.user;
|
|
672
|
-
const boxWidth = getLogoWidth();
|
|
673
|
-
const W = boxWidth - 2; // Same width as logo (inner width)
|
|
674
|
-
|
|
675
|
-
// Helper to center text
|
|
676
|
-
const centerLine = (text, width) => {
|
|
677
|
-
const pad = Math.floor((width - text.length) / 2);
|
|
678
|
-
return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
// Helper to pad text left
|
|
682
|
-
const padLine = (text, width) => {
|
|
683
|
-
return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
// Dashboard box header
|
|
687
|
-
console.log();
|
|
688
|
-
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
689
|
-
console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
|
|
690
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
691
|
-
|
|
692
|
-
// Connection info - show all active connections in boxes (max 3 per row)
|
|
693
|
-
const allConns = connections.getAll();
|
|
694
|
-
if (allConns.length > 0) {
|
|
695
|
-
const maxPerRow = 3;
|
|
696
|
-
const boxPadding = 2; // padding inside each mini-box
|
|
697
|
-
const gap = 2; // gap between boxes
|
|
698
|
-
|
|
699
|
-
// Calculate box width based on number of connections (max 3)
|
|
700
|
-
const numBoxes = Math.min(allConns.length, maxPerRow);
|
|
701
|
-
const totalGaps = (numBoxes - 1) * gap;
|
|
702
|
-
const boxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
|
|
703
|
-
|
|
704
|
-
// Process connections in rows of 3
|
|
705
|
-
for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
|
|
706
|
-
const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
|
|
707
|
-
const numInRow = rowConns.length;
|
|
708
|
-
const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
|
|
709
|
-
|
|
710
|
-
// Top border of boxes
|
|
711
|
-
let topLine = ' ';
|
|
712
|
-
for (let i = 0; i < numInRow; i++) {
|
|
713
|
-
topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
|
|
714
|
-
if (i < numInRow - 1) topLine += ' '.repeat(gap);
|
|
715
|
-
}
|
|
716
|
-
const topPad = W - topLine.length;
|
|
717
|
-
console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
|
|
718
|
-
|
|
719
|
-
// Content of boxes
|
|
720
|
-
let contentLine = ' ';
|
|
721
|
-
for (let i = 0; i < numInRow; i++) {
|
|
722
|
-
const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
|
|
723
|
-
const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
|
|
724
|
-
const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
|
|
725
|
-
const textPad = Math.floor((innerWidth - truncated.length) / 2);
|
|
726
|
-
const textPadRight = innerWidth - truncated.length - textPad;
|
|
727
|
-
contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
|
|
728
|
-
if (i < numInRow - 1) contentLine += ' '.repeat(gap);
|
|
729
|
-
}
|
|
730
|
-
const contentPad = W - contentLine.length;
|
|
731
|
-
console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
|
|
732
|
-
|
|
733
|
-
// Bottom border of boxes
|
|
734
|
-
let bottomLine = ' ';
|
|
735
|
-
for (let i = 0; i < numInRow; i++) {
|
|
736
|
-
bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
|
|
737
|
-
if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
|
|
738
|
-
}
|
|
739
|
-
const bottomPad = W - bottomLine.length;
|
|
740
|
-
console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
745
|
-
|
|
746
|
-
// Menu options in 2 columns
|
|
747
|
-
const col1Width = Math.floor(W / 2);
|
|
748
|
-
const col2Width = W - col1Width;
|
|
749
|
-
|
|
750
|
-
const menuRow = (left, right) => {
|
|
751
|
-
const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
|
|
752
|
-
const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
|
|
753
|
-
const leftPad = ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
|
|
754
|
-
const rightPad = ' '.repeat(Math.max(0, col2Width - rightPlain.length - 2));
|
|
755
|
-
console.log(chalk.cyan('║') + ' ' + left + leftPad + ' ' + (right || '') + rightPad + chalk.cyan('║'));
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
|
|
759
|
-
menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
|
|
760
|
-
menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
|
|
761
|
-
|
|
762
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
763
|
-
console.log();
|
|
764
|
-
|
|
765
|
-
const { action } = await inquirer.prompt([
|
|
766
|
-
{
|
|
767
|
-
type: 'input',
|
|
768
|
-
name: 'action',
|
|
769
|
-
message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
|
|
770
|
-
validate: (input) => {
|
|
771
|
-
const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
|
|
772
|
-
if (valid.includes(input)) return true;
|
|
773
|
-
return 'Please enter a valid option';
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
]);
|
|
777
|
-
|
|
778
|
-
// Map input to action
|
|
779
|
-
const actionMap = {
|
|
780
|
-
'1': 'accounts',
|
|
781
|
-
'2': 'stats',
|
|
782
|
-
'+': 'add_prop_account',
|
|
783
|
-
'a': 'algotrading',
|
|
784
|
-
'A': 'algotrading',
|
|
785
|
-
'u': 'update',
|
|
786
|
-
'U': 'update',
|
|
787
|
-
'x': 'disconnect',
|
|
788
|
-
'X': 'disconnect'
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
return actionMap[action] || 'accounts';
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Handles the update process with auto-restart
|
|
796
|
-
*/
|
|
797
|
-
const handleUpdate = async () => {
|
|
798
|
-
const { execSync: exec } = require('child_process');
|
|
799
|
-
const pkg = require('../package.json');
|
|
800
|
-
const currentVersion = pkg.version;
|
|
801
|
-
const spinner = ora('Checking for updates...').start();
|
|
802
|
-
|
|
803
|
-
try {
|
|
804
|
-
// Check latest version on npm
|
|
805
|
-
spinner.text = 'Checking npm registry...';
|
|
806
|
-
let latestVersion;
|
|
807
|
-
try {
|
|
808
|
-
latestVersion = exec('npm view hedgequantx version', { stdio: 'pipe' }).toString().trim();
|
|
809
|
-
} catch (e) {
|
|
810
|
-
spinner.fail('Cannot reach npm registry');
|
|
811
|
-
console.log();
|
|
812
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (currentVersion === latestVersion) {
|
|
817
|
-
spinner.succeed('Already up to date!');
|
|
818
|
-
console.log();
|
|
819
|
-
console.log(chalk.green(` ✓ You have the latest version of HedgeQuantX CLI: v${currentVersion}`));
|
|
820
|
-
console.log();
|
|
821
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Update via npm
|
|
826
|
-
spinner.text = `Updating v${currentVersion} -> v${latestVersion}...`;
|
|
827
|
-
try {
|
|
828
|
-
exec('npm install -g hedgequantx@latest', { stdio: 'pipe' });
|
|
829
|
-
} catch (e) {
|
|
830
|
-
spinner.fail('Update failed - try manually: npm install -g hedgequantx@latest');
|
|
831
|
-
console.log(chalk.gray(` Error: ${e.message}`));
|
|
832
|
-
console.log();
|
|
833
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
spinner.succeed('CLI updated!');
|
|
838
|
-
console.log();
|
|
839
|
-
console.log(chalk.green(` ✓ Updated: v${currentVersion} -> v${latestVersion}`));
|
|
840
|
-
console.log();
|
|
841
|
-
console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
|
|
842
|
-
console.log();
|
|
843
|
-
|
|
844
|
-
// Small delay so user can see the message
|
|
845
|
-
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
846
|
-
|
|
847
|
-
// Restart the CLI automatically
|
|
848
|
-
const { spawn } = require('child_process');
|
|
849
|
-
const child = spawn('hedgequantx', [], {
|
|
850
|
-
stdio: 'inherit',
|
|
851
|
-
detached: true,
|
|
852
|
-
shell: true
|
|
853
|
-
});
|
|
854
|
-
child.unref();
|
|
855
|
-
process.exit(0);
|
|
856
|
-
|
|
857
|
-
} catch (error) {
|
|
858
|
-
spinner.fail('Update failed: ' + error.message);
|
|
859
|
-
console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
|
|
860
|
-
console.log();
|
|
861
|
-
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
|
|
865
278
|
/**
|
|
866
279
|
* Main application loop
|
|
867
280
|
*/
|
|
@@ -965,4 +378,4 @@ const run = async () => {
|
|
|
965
378
|
}
|
|
966
379
|
};
|
|
967
380
|
|
|
968
|
-
module.exports = { run, banner,
|
|
381
|
+
module.exports = { run, banner, mainMenu, dashboardMenu };
|