hedgequantx 1.5.7 → 1.5.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -189,51 +189,15 @@ const banner = async () => {
189
189
  const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
190
190
  console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
191
191
 
192
- // Stats bar if connected
193
- // STRICT: Only display verified values from API, show '--' for unavailable data
194
- if (statsInfo) {
195
- console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
196
-
197
- const connStr = `Connections: ${statsInfo.connections}`;
198
- const accStr = `Accounts: ${statsInfo.accounts}`;
199
-
200
- // Balance: show '--' if not available from API
201
- const balStr = statsInfo.balance !== null
202
- ? `Balance: $${statsInfo.balance.toLocaleString()}`
203
- : `Balance: --`;
204
- const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
205
-
206
- // P&L: show '--' if not available from API
207
- let pnlDisplay;
208
- let pnlColor;
209
- if (statsInfo.pnl !== null) {
210
- const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
211
- pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
212
- pnlDisplay = `$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnl.toFixed(1)})`;
213
- } else {
214
- pnlColor = chalk.gray;
215
- pnlDisplay = '--';
216
- }
217
-
218
- // Build full stats text and calculate padding
219
- const statsText = `${connStr} ${accStr} ${balStr} P&L: ${pnlDisplay}`;
220
- const statsLen = statsText.length;
221
- const statsLeftPad = Math.floor((innerWidth - statsLen) / 2);
222
- const statsRightPad = innerWidth - statsLen - statsLeftPad;
223
-
224
- console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
225
- chalk.white(connStr) + ' ' +
226
- chalk.white(accStr) + ' ' +
227
- chalk.white('Balance: ') + balColor(statsInfo.balance !== null ? `$${statsInfo.balance.toLocaleString()}` : '--') + ' ' +
228
- chalk.white('P&L: ') + pnlColor(pnlDisplay) +
229
- ' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║')
230
- );
231
- }
232
-
233
192
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
234
193
  console.log();
235
194
  };
236
195
 
196
+ /**
197
+ * Get cached stats for dashboard
198
+ */
199
+ const getCachedStats = () => cachedStats;
200
+
237
201
  /**
238
202
  * Main connection menu
239
203
  */
@@ -412,4 +376,4 @@ const run = async () => {
412
376
  }
413
377
  };
414
378
 
415
- module.exports = { run, banner, mainMenu, dashboardMenu };
379
+ module.exports = { run, banner, mainMenu, dashboardMenu, getCachedStats };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared contracts configuration
3
+ * Used by both Rithmic and ProjectX services
4
+ */
5
+
6
+ // Current front-month contracts (update monthly as contracts expire)
7
+ const CONTRACTS = [
8
+ // Index Futures - Most Popular
9
+ { symbol: 'ES', name: 'E-mini S&P 500', exchange: 'CME', group: 'Index' },
10
+ { symbol: 'NQ', name: 'E-mini NASDAQ-100', exchange: 'CME', group: 'Index' },
11
+ { symbol: 'RTY', name: 'E-mini Russell 2000', exchange: 'CME', group: 'Index' },
12
+ { symbol: 'YM', name: 'E-mini Dow Jones', exchange: 'CBOT', group: 'Index' },
13
+
14
+ // Micro Index Futures
15
+ { symbol: 'MES', name: 'Micro E-mini S&P 500', exchange: 'CME', group: 'Micro' },
16
+ { symbol: 'MNQ', name: 'Micro E-mini NASDAQ-100', exchange: 'CME', group: 'Micro' },
17
+ { symbol: 'M2K', name: 'Micro E-mini Russell 2000', exchange: 'CME', group: 'Micro' },
18
+ { symbol: 'MYM', name: 'Micro E-mini Dow Jones', exchange: 'CBOT', group: 'Micro' },
19
+
20
+ // Energy Futures
21
+ { symbol: 'CL', name: 'Crude Oil', exchange: 'NYMEX', group: 'Energy' },
22
+ { symbol: 'NG', name: 'Natural Gas', exchange: 'NYMEX', group: 'Energy' },
23
+ { symbol: 'MCL', name: 'Micro Crude Oil', exchange: 'NYMEX', group: 'Energy' },
24
+
25
+ // Metals Futures
26
+ { symbol: 'GC', name: 'Gold', exchange: 'COMEX', group: 'Metals' },
27
+ { symbol: 'SI', name: 'Silver', exchange: 'COMEX', group: 'Metals' },
28
+ { symbol: 'HG', name: 'Copper', exchange: 'COMEX', group: 'Metals' },
29
+ { symbol: 'MGC', name: 'Micro Gold', exchange: 'COMEX', group: 'Metals' },
30
+
31
+ // Treasury Futures
32
+ { symbol: 'ZB', name: '30-Year Treasury Bond', exchange: 'CBOT', group: 'Bonds' },
33
+ { symbol: 'ZN', name: '10-Year Treasury Note', exchange: 'CBOT', group: 'Bonds' },
34
+ { symbol: 'ZF', name: '5-Year Treasury Note', exchange: 'CBOT', group: 'Bonds' },
35
+
36
+ // Agriculture Futures
37
+ { symbol: 'ZC', name: 'Corn', exchange: 'CBOT', group: 'Agriculture' },
38
+ { symbol: 'ZS', name: 'Soybeans', exchange: 'CBOT', group: 'Agriculture' },
39
+ { symbol: 'ZW', name: 'Wheat', exchange: 'CBOT', group: 'Agriculture' },
40
+
41
+ // Currency Futures
42
+ { symbol: '6E', name: 'Euro FX', exchange: 'CME', group: 'Currency' },
43
+ { symbol: '6B', name: 'British Pound', exchange: 'CME', group: 'Currency' },
44
+ { symbol: '6J', name: 'Japanese Yen', exchange: 'CME', group: 'Currency' },
45
+ ];
46
+
47
+ /**
48
+ * Get current front-month code based on date
49
+ * Futures months: F(Jan), G(Feb), H(Mar), J(Apr), K(May), M(Jun),
50
+ * N(Jul), Q(Aug), U(Sep), V(Oct), X(Nov), Z(Dec)
51
+ */
52
+ const getMonthCode = (monthsAhead = 0) => {
53
+ const codes = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z'];
54
+ const now = new Date();
55
+ const month = (now.getMonth() + monthsAhead) % 12;
56
+ return codes[month];
57
+ };
58
+
59
+ /**
60
+ * Get year code (last digit)
61
+ */
62
+ const getYearCode = (monthsAhead = 0) => {
63
+ const now = new Date();
64
+ const futureDate = new Date(now.getFullYear(), now.getMonth() + monthsAhead, 1);
65
+ return futureDate.getFullYear() % 10;
66
+ };
67
+
68
+ /**
69
+ * Get contracts with current front-month symbols
70
+ * @param {number} monthsAhead - How many months ahead for the contract (default 2 for front month)
71
+ */
72
+ const getContractsWithMonthCode = (monthsAhead = 2) => {
73
+ const monthCode = getMonthCode(monthsAhead);
74
+ const yearCode = getYearCode(monthsAhead);
75
+
76
+ return CONTRACTS.map(c => ({
77
+ ...c,
78
+ symbol: `${c.symbol}${monthCode}${yearCode}`,
79
+ name: `${c.name}`,
80
+ baseSymbol: c.symbol
81
+ }));
82
+ };
83
+
84
+ /**
85
+ * Get display name for a symbol
86
+ */
87
+ const getContractDisplayName = (symbol) => {
88
+ // Extract base symbol (remove month/year code)
89
+ const baseSymbol = symbol.replace(/[A-Z][0-9]$/, '').replace(/[FGHJKMNQUVXZ][0-9]+$/, '');
90
+ const contract = CONTRACTS.find(c => c.symbol === baseSymbol);
91
+ return contract ? contract.name : symbol;
92
+ };
93
+
94
+ module.exports = {
95
+ CONTRACTS,
96
+ getMonthCode,
97
+ getYearCode,
98
+ getContractsWithMonthCode,
99
+ getContractDisplayName
100
+ };
@@ -65,9 +65,8 @@ const loginPrompt = async (propfirmName) => {
65
65
  const projectXMenu = async () => {
66
66
  const propfirms = getPropFirmsByPlatform('ProjectX');
67
67
  const boxWidth = getLogoWidth();
68
- const innerWidth = boxWidth - 2;
69
- const numCols = 3;
70
- const colWidth = Math.floor(innerWidth / numCols);
68
+ const W = boxWidth - 2; // Inner width
69
+ const col1Width = Math.floor(W / 2);
71
70
 
72
71
  // Build numbered list
73
72
  const numbered = propfirms.map((pf, i) => ({
@@ -78,61 +77,50 @@ const projectXMenu = async () => {
78
77
 
79
78
  // PropFirm selection box
80
79
  console.log();
81
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
82
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
83
- console.log(chalk.cyan('') + ' '.repeat(innerWidth) + chalk.cyan(''));
80
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
81
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (ProjectX)', W)) + chalk.cyan('║'));
82
+ console.log(chalk.cyan('' + ''.repeat(W) + ''));
84
83
 
85
- // Display in 3 columns with fixed width alignment
86
- const rows = Math.ceil(numbered.length / numCols);
87
- const maxNum = numbered.length;
88
- const numWidth = maxNum >= 10 ? 4 : 3; // [XX] or [X]
84
+ // Display in 2 columns
85
+ const menuRow = (left, right) => {
86
+ const leftPlain = left ? left.replace(/\x1b\[[0-9;]*m/g, '') : '';
87
+ const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
88
+ const leftPadded = ' ' + (left || '') + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
89
+ const rightPadded = (right || '') + ' '.repeat(Math.max(0, W - col1Width - rightPlain.length));
90
+ console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
91
+ };
89
92
 
90
- for (let row = 0; row < rows; row++) {
91
- let line = '';
92
- for (let col = 0; col < numCols; col++) {
93
- const idx = row + col * rows;
94
- if (idx < numbered.length) {
95
- const item = numbered[idx];
96
- const numStr = item.num.toString().padStart(2, ' ');
97
- const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
98
- const textLen = 4 + 1 + item.name.length; // [XX] + space + name
99
- const padding = colWidth - textLen - 2;
100
- line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
101
- } else {
102
- line += ' '.repeat(colWidth);
103
- }
104
- }
105
- // Adjust for exact width
106
- const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
107
- const adjust = innerWidth - lineLen;
108
- console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
93
+ // Display propfirms in 2 columns
94
+ for (let i = 0; i < numbered.length; i += 2) {
95
+ const left = numbered[i];
96
+ const right = numbered[i + 1];
97
+ const leftText = chalk.cyan(`[${left.num.toString().padStart(2, ' ')}]`) + ' ' + chalk.white(left.name);
98
+ const rightText = right ? chalk.cyan(`[${right.num.toString().padStart(2, ' ')}]`) + ' ' + chalk.white(right.name) : '';
99
+ menuRow(leftText, rightText);
109
100
  }
110
101
 
111
- console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
112
- const backText = ' ' + chalk.red('[X] Back');
113
- const backLen = '[X] Back'.length + 2;
114
- console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
115
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
102
+ // Back option
103
+ console.log(chalk.cyan('' + '═'.repeat(W) + '╣'));
104
+ const backLine = ' ' + chalk.red('[X] Back') + ' '.repeat(W - 10);
105
+ console.log(chalk.cyan('║') + backLine + chalk.cyan('║'));
106
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
116
107
  console.log();
117
108
 
118
- const validInputs = numbered.map(n => n.num.toString());
119
- validInputs.push('x', 'X');
120
-
121
109
  const { action } = await inquirer.prompt([
122
110
  {
123
111
  type: 'input',
124
112
  name: 'action',
125
- message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
126
- validate: (input) => {
127
- if (validInputs.includes(input)) return true;
128
- return `Please enter 1-${numbered.length} or X`;
129
- }
113
+ message: chalk.cyan('Select:'),
114
+ prefix: ''
130
115
  }
131
116
  ]);
132
117
 
133
- if (action.toLowerCase() === 'x') return null;
118
+ const input = (action || '').toLowerCase().trim();
119
+ if (input === 'x') return null;
120
+
121
+ const selectedIdx = parseInt(input) - 1;
122
+ if (isNaN(selectedIdx) || selectedIdx < 0 || selectedIdx >= numbered.length) return null;
134
123
 
135
- const selectedIdx = parseInt(action) - 1;
136
124
  const selectedPropfirm = numbered[selectedIdx];
137
125
 
138
126
  const credentials = await loginPrompt(selectedPropfirm.name);
@@ -345,53 +333,46 @@ const tradovateMenu = async () => {
345
333
  */
346
334
  const addPropAccountMenu = async () => {
347
335
  const boxWidth = getLogoWidth();
348
- const innerWidth = boxWidth - 2;
349
- const col1Width = Math.floor(innerWidth / 2);
350
- const col2Width = innerWidth - col1Width;
336
+ const W = boxWidth - 2; // Inner width
337
+ const col1Width = Math.floor(W / 2);
351
338
 
352
339
  console.log();
353
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
354
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('ADD PROP ACCOUNT', innerWidth)) + chalk.cyan('║'));
355
- console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
340
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
341
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('ADD PROP ACCOUNT', W)) + chalk.cyan('║'));
342
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
356
343
 
357
344
  const menuRow = (left, right) => {
358
- const leftText = ' ' + left;
359
- const rightText = right ? ' ' + right : '';
360
- const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
361
- const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
362
- const leftPad = col1Width - leftLen;
363
- const rightPad = col2Width - rightLen;
364
- console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
345
+ const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
346
+ const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '');
347
+ const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
348
+ const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain.length));
349
+ console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
365
350
  };
366
351
 
367
352
  menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
368
353
  menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
369
354
 
370
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
355
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
371
356
  console.log();
372
357
 
373
358
  const { action } = await inquirer.prompt([
374
359
  {
375
360
  type: 'input',
376
361
  name: 'action',
377
- message: chalk.cyan('Enter choice (1/2/3/X):'),
378
- validate: (input) => {
379
- const valid = ['1', '2', '3', 'x', 'X'];
380
- if (valid.includes(input)) return true;
381
- return 'Please enter 1, 2, 3 or X';
382
- }
362
+ message: chalk.cyan('Select:'),
363
+ prefix: ''
383
364
  }
384
365
  ]);
385
366
 
367
+ const input = (action || '').toLowerCase().trim();
386
368
  const actionMap = {
387
369
  '1': 'projectx',
388
370
  '2': 'rithmic',
389
371
  '3': 'tradovate',
390
- 'x': null,
391
- 'X': null
372
+ 'x': null
392
373
  };
393
374
 
394
- return actionMap[action];
375
+ return actionMap[input] || null;
395
376
  };
396
377
 
397
378
  module.exports = {
@@ -10,6 +10,7 @@ const { execSync, spawn } = require('child_process');
10
10
 
11
11
  const { connections } = require('../services');
12
12
  const { getLogoWidth, centerText, prepareStdin } = require('../ui');
13
+ const { getCachedStats } = require('../app');
13
14
 
14
15
  /**
15
16
  * Dashboard menu after login
@@ -19,115 +20,116 @@ const dashboardMenu = async (service) => {
19
20
  // Ensure stdin is ready for prompts
20
21
  prepareStdin();
21
22
 
22
- const user = service.user;
23
23
  const boxWidth = getLogoWidth();
24
- const W = boxWidth - 2; // Same width as logo (inner width)
24
+ const W = boxWidth - 2; // Inner width (without borders)
25
25
 
26
- // Helper to center text
27
- const centerLine = (text, width) => {
28
- const pad = Math.floor((width - text.length) / 2);
29
- return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
30
- };
31
-
32
- // Helper to pad text left
33
- const padLine = (text, width) => {
34
- return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
26
+ // Helper to create a line that fits exactly in the box
27
+ const makeLine = (content, align = 'left') => {
28
+ const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
29
+ const padding = W - plainLen;
30
+ if (align === 'center') {
31
+ const leftPad = Math.floor(padding / 2);
32
+ const rightPad = padding - leftPad;
33
+ return chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║');
34
+ }
35
+ return chalk.cyan('║') + content + ' '.repeat(Math.max(0, padding)) + chalk.cyan('║');
35
36
  };
36
37
 
37
38
  // Dashboard box header
38
39
  console.log();
39
40
  console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
40
- console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan(''));
41
+ console.log(makeLine(chalk.yellow.bold('Welcome, HQX Trader!'), 'center'));
41
42
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
42
43
 
43
- // Connection info - show all active connections in boxes (max 3 per row)
44
+ // Show connected propfirms centered on one line (max 3)
44
45
  const allConns = connections.getAll();
45
46
  if (allConns.length > 0) {
46
- const maxPerRow = 3;
47
- const boxPadding = 2; // padding inside each mini-box
48
- const gap = 2; // gap between boxes
47
+ const propfirms = allConns.slice(0, 3).map(c => c.propfirm || c.type || 'Connected');
48
+ const propfirmText = propfirms.map(p => chalk.green('● ') + chalk.white(p)).join(' ');
49
+ console.log(makeLine(propfirmText, 'center'));
50
+ }
51
+
52
+ // Show stats bar (Connections, Accounts, Balance, P&L)
53
+ let statsInfo = null;
54
+ try { statsInfo = getCachedStats(); } catch (e) {}
55
+
56
+ if (statsInfo) {
57
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
49
58
 
50
- // Calculate box width based on number of connections (max 3)
51
- const numBoxes = Math.min(allConns.length, maxPerRow);
52
- const totalGaps = (numBoxes - 1) * gap;
53
- const connBoxWidth = Math.floor((W - totalGaps - 2) / numBoxes); // -2 for outer padding
59
+ const connStr = `Connections: ${statsInfo.connections}`;
60
+ const accStr = `Accounts: ${statsInfo.accounts}`;
54
61
 
55
- // Process connections in rows of 3
56
- for (let rowStart = 0; rowStart < allConns.length; rowStart += maxPerRow) {
57
- const rowConns = allConns.slice(rowStart, rowStart + maxPerRow);
58
- const numInRow = rowConns.length;
59
- const rowBoxWidth = Math.floor((W - (numInRow - 1) * gap - 2) / numInRow);
60
-
61
- // Top border of boxes
62
- let topLine = ' ';
63
- for (let i = 0; i < numInRow; i++) {
64
- topLine += '┌' + '─'.repeat(rowBoxWidth - 2) + '┐';
65
- if (i < numInRow - 1) topLine += ' '.repeat(gap);
66
- }
67
- const topPad = W - topLine.length;
68
- console.log(chalk.cyan('║') + chalk.green(topLine) + ' '.repeat(Math.max(0, topPad)) + chalk.cyan('║'));
69
-
70
- // Content of boxes
71
- let contentLine = ' ';
72
- for (let i = 0; i < numInRow; i++) {
73
- const connText = rowConns[i].propfirm || rowConns[i].type || 'Connected';
74
- const truncated = connText.length > rowBoxWidth - 4 ? connText.slice(0, rowBoxWidth - 7) + '...' : connText;
75
- const innerWidth = rowBoxWidth - 4; // -2 for borders, -2 for padding
76
- const textPad = Math.floor((innerWidth - truncated.length) / 2);
77
- const textPadRight = innerWidth - truncated.length - textPad;
78
- contentLine += '│ ' + ' '.repeat(textPad) + truncated + ' '.repeat(textPadRight) + ' │';
79
- if (i < numInRow - 1) contentLine += ' '.repeat(gap);
80
- }
81
- const contentPad = W - contentLine.length;
82
- console.log(chalk.cyan('║') + chalk.green(contentLine) + ' '.repeat(Math.max(0, contentPad)) + chalk.cyan('║'));
83
-
84
- // Bottom border of boxes
85
- let bottomLine = ' ';
86
- for (let i = 0; i < numInRow; i++) {
87
- bottomLine += '└' + '─'.repeat(rowBoxWidth - 2) + '┘';
88
- if (i < numInRow - 1) bottomLine += ' '.repeat(gap);
89
- }
90
- const bottomPad = W - bottomLine.length;
91
- console.log(chalk.cyan('║') + chalk.green(bottomLine) + ' '.repeat(Math.max(0, bottomPad)) + chalk.cyan('║'));
62
+ const balStr = statsInfo.balance !== null
63
+ ? `$${statsInfo.balance.toLocaleString()}`
64
+ : '--';
65
+ const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
66
+
67
+ let pnlDisplay, pnlColor;
68
+ if (statsInfo.pnl !== null) {
69
+ const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
70
+ pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
71
+ pnlDisplay = `${pnlSign}$${Math.abs(statsInfo.pnl).toLocaleString()}`;
72
+ } else {
73
+ pnlColor = chalk.gray;
74
+ pnlDisplay = '--';
92
75
  }
76
+
77
+ const statsText = connStr + ' ' + accStr + ' Balance: ' + balStr + ' P&L: ' + pnlDisplay;
78
+ const statsPlain = `${connStr} ${accStr} Balance: ${balStr} P&L: ${pnlDisplay}`;
79
+ const statsLeftPad = Math.floor((W - statsPlain.length) / 2);
80
+ const statsRightPad = W - statsPlain.length - statsLeftPad;
81
+
82
+ console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
83
+ chalk.white(connStr) + ' ' +
84
+ chalk.white(accStr) + ' ' +
85
+ chalk.white('Balance: ') + balColor(balStr) + ' ' +
86
+ chalk.white('P&L: ') + pnlColor(pnlDisplay) +
87
+ ' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║'));
93
88
  }
94
89
 
95
90
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
96
91
 
97
92
  // Menu options in 2 columns
98
93
  const col1Width = Math.floor(W / 2);
99
- const col2Width = W - col1Width;
100
94
 
101
95
  const menuRow = (left, right) => {
102
96
  const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
103
- const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
104
- const leftPad = ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
105
- const rightPad = ' '.repeat(Math.max(0, col2Width - rightPlain.length - 2));
106
- console.log(chalk.cyan('║') + ' ' + left + leftPad + ' ' + (right || '') + rightPad + chalk.cyan('║'));
97
+ const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '');
98
+ const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
99
+ const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain.length));
100
+ console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
107
101
  };
108
102
 
103
+ // Display menu items in 2 columns inside the box
104
+ menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
105
+ menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.magenta('[A] Algo-Trading'));
106
+ menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
107
+
109
108
  console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
110
109
  console.log();
111
-
112
- // Use list type instead of input - more stable stdin handling
113
- const { action } = await inquirer.prompt([
110
+
111
+ // Input prompt
112
+ const { choice } = await inquirer.prompt([
114
113
  {
115
- type: 'list',
116
- name: 'action',
117
- message: chalk.cyan('Select action:'),
118
- choices: [
119
- { name: chalk.cyan('[1] View Accounts'), value: 'accounts' },
120
- { name: chalk.cyan('[2] View Stats'), value: 'stats' },
121
- { name: chalk.cyan('[+] Add Prop-Account'), value: 'add_prop_account' },
122
- { name: chalk.magenta('[A] Algo-Trading'), value: 'algotrading' },
123
- { name: chalk.yellow('[U] Update HQX'), value: 'update' },
124
- { name: chalk.red('[X] Disconnect'), value: 'disconnect' }
125
- ],
126
- loop: false
114
+ type: 'input',
115
+ name: 'choice',
116
+ message: chalk.cyan('Select:'),
117
+ prefix: ''
127
118
  }
128
119
  ]);
129
-
130
- return action;
120
+
121
+ // Map input to action
122
+ const input = (choice || '').toString().toLowerCase().trim();
123
+ const actionMap = {
124
+ '1': 'accounts',
125
+ '2': 'stats',
126
+ '+': 'add_prop_account',
127
+ 'a': 'algotrading',
128
+ 'u': 'update',
129
+ 'x': 'disconnect'
130
+ };
131
+
132
+ return actionMap[input] || null;
131
133
  };
132
134
 
133
135
  /**
@@ -188,43 +190,19 @@ const handleUpdate = async () => {
188
190
 
189
191
  // Compare versions
190
192
  if (currentVersion === latestVersion) {
191
- spinner.succeed('Already up to date!');
192
- console.log();
193
- console.log(chalk.green(` You have the latest version: v${currentVersion}`));
194
- console.log();
195
- await waitForEnter();
196
- return;
197
- }
198
-
199
- // Ask user before updating
200
- spinner.stop();
201
- console.log();
202
- console.log(chalk.cyan(` Current version: v${currentVersion}`));
203
- console.log(chalk.green(` Latest version: v${latestVersion}`));
204
- console.log();
205
-
206
- prepareStdin();
207
- const { confirm } = await inquirer.prompt([{
208
- type: 'confirm',
209
- name: 'confirm',
210
- message: 'Do you want to update now?',
211
- default: true
212
- }]);
213
-
214
- if (!confirm) {
215
- console.log(chalk.gray(' Update cancelled'));
193
+ spinner.succeed(`Already up to date! (v${currentVersion})`);
216
194
  console.log();
217
- await waitForEnter();
195
+ await new Promise(r => setTimeout(r, 2000));
218
196
  return;
219
197
  }
220
198
 
221
- // Update via npm
222
- spinner = ora({ text: `Updating v${currentVersion} -> v${latestVersion}...`, color: 'yellow' }).start();
199
+ // Show version info and update automatically
200
+ spinner.text = `Updating v${currentVersion} v${latestVersion}...`;
223
201
 
224
202
  try {
225
203
  execSync('npm install -g hedgequantx@latest 2>/dev/null', {
226
204
  stdio: 'pipe',
227
- timeout: 120000, // 2 minute timeout for install
205
+ timeout: 120000,
228
206
  encoding: 'utf8'
229
207
  });
230
208
  } catch (e) {
@@ -233,52 +211,29 @@ const handleUpdate = async () => {
233
211
  console.log(chalk.yellow(' Try manually:'));
234
212
  console.log(chalk.white(' npm install -g hedgequantx@latest'));
235
213
  console.log();
236
- if (e.message) {
237
- console.log(chalk.gray(` Error: ${e.message.substring(0, 100)}`));
238
- console.log();
239
- }
240
214
  await waitForEnter();
241
215
  return;
242
216
  }
243
217
 
244
- spinner.succeed('Update complete!');
218
+ spinner.succeed(`Updated: v${currentVersion} → v${latestVersion}`);
245
219
  console.log();
246
- console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
220
+ console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
247
221
  console.log();
248
222
 
249
- // Ask if user wants to restart
250
- prepareStdin();
251
- const { restart } = await inquirer.prompt([{
252
- type: 'confirm',
253
- name: 'restart',
254
- message: 'Restart HQX now?',
255
- default: true
256
- }]);
223
+ // Auto restart after 2 seconds
224
+ await new Promise(r => setTimeout(r, 2000));
257
225
 
258
- if (restart) {
259
- console.log();
260
- console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
261
- console.log();
262
-
263
- // Small delay so user can see the message
264
- await new Promise(resolve => setTimeout(resolve, 1000));
265
-
266
- // Restart the CLI
267
- try {
268
- const child = spawn('hedgequantx', [], {
269
- stdio: 'inherit',
270
- detached: true,
271
- shell: true
272
- });
273
- child.unref();
274
- process.exit(0);
275
- } catch (e) {
276
- console.log(chalk.yellow(' Could not auto-restart. Please run: hedgequantx'));
277
- console.log();
278
- await waitForEnter();
279
- }
280
- } else {
281
- console.log(chalk.gray(' Run "hedgequantx" to use the new version'));
226
+ // Restart the CLI
227
+ try {
228
+ const child = spawn('hedgequantx', [], {
229
+ stdio: 'inherit',
230
+ detached: true,
231
+ shell: true
232
+ });
233
+ child.unref();
234
+ process.exit(0);
235
+ } catch (e) {
236
+ console.log(chalk.yellow(' Could not auto-restart. Please run: hedgequantx'));
282
237
  console.log();
283
238
  await waitForEnter();
284
239
  }
@@ -173,12 +173,16 @@ const copyTradingMenu = async () => {
173
173
  const maxRisk = parseInt(maxRiskInput) || 200;
174
174
 
175
175
  // Step 6: Privacy
176
- const { showNames } = await inquirer.prompt([{
177
- type: 'confirm',
178
- name: 'showNames',
179
- message: 'Show account names?',
180
- default: false
176
+ const { privacyChoice } = await inquirer.prompt([{
177
+ type: 'list',
178
+ name: 'privacyChoice',
179
+ message: 'Account names:',
180
+ choices: [
181
+ { name: 'Hide account names', value: false },
182
+ { name: 'Show account names', value: true }
183
+ ]
181
184
  }]);
185
+ const showNames = privacyChoice;
182
186
 
183
187
  // Confirm
184
188
  console.log();
@@ -283,9 +287,12 @@ const launchCopyTrading = async (config) => {
283
287
 
284
288
  // Combined stats
285
289
  const stats = {
286
- accountName: `${leadName} -> ${followerName}`,
287
- symbol: `${lead.symbol.name} / ${follower.symbol.name}`,
288
- contracts: `${lead.contracts}/${follower.contracts}`,
290
+ leadName,
291
+ followerName,
292
+ leadSymbol: lead.symbol.name,
293
+ followerSymbol: follower.symbol.name,
294
+ leadQty: lead.contracts,
295
+ followerQty: follower.contracts,
289
296
  target: dailyTarget,
290
297
  risk: maxRisk,
291
298
  pnl: 0,
@@ -146,37 +146,55 @@ class AlgoUI {
146
146
 
147
147
  this._line(chalk.cyan(GT));
148
148
 
149
- // Row 1: Account | Symbol (truncate long values)
150
- const accName = (stats.accountName || 'N/A').substring(0, 35);
151
- const symName = (stats.symbol || 'N/A').substring(0, 25);
152
- const qtyStr = stats.contracts || '1/1';
149
+ // Row 1: Lead Account | Follower Account
150
+ const leadName = (stats.leadName || stats.accountName || 'N/A').substring(0, 40);
151
+ const followerName = (stats.followerName || 'N/A').substring(0, 40);
153
152
 
154
- const r1c1 = buildCell('Account', accName, chalk.cyan, colL);
155
- const r1c2t = ` Symbol: ${chalk.yellow(symName)} Qty: ${chalk.cyan(qtyStr)}`;
156
- const r1c2p = ` Symbol: ${symName} Qty: ${qtyStr}`;
157
- row(r1c1.padded, r1c2t + pad(Math.max(0, colR - r1c2p.length)));
153
+ const r1c1 = buildCell('Lead', leadName, chalk.cyan, colL);
154
+ const r1c2 = buildCell('Follower', followerName, chalk.magenta, colR);
155
+ row(r1c1.padded, r1c2.padded);
158
156
 
159
157
  this._line(chalk.cyan(GM));
160
158
 
161
- // Row 2: Target | Risk
162
- const r2c1 = buildCell('Target', '$' + (stats.target || 0).toFixed(2), chalk.green, colL);
163
- const r2c2 = buildCell('Risk', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR);
159
+ // Row 2: Lead Symbol | Follower Symbol
160
+ const leadSymbol = (stats.leadSymbol || stats.symbol || 'N/A').substring(0, 35);
161
+ const followerSymbol = (stats.followerSymbol || 'N/A').substring(0, 35);
162
+
163
+ const r2c1 = buildCell('Symbol', leadSymbol, chalk.yellow, colL);
164
+ const r2c2 = buildCell('Symbol', followerSymbol, chalk.yellow, colR);
164
165
  row(r2c1.padded, r2c2.padded);
165
166
 
166
167
  this._line(chalk.cyan(GM));
167
168
 
168
- // Row 3: P&L | Server
169
- const r3c1 = buildCell('P&L', pnlStr, pnlColor, colL);
170
- const r3c2 = buildCell('Server', stats.connected ? 'ON' : 'OFF', serverColor, colR);
169
+ // Row 3: Lead Qty | Follower Qty
170
+ const leadQty = stats.leadQty || '1';
171
+ const followerQty = stats.followerQty || '1';
172
+
173
+ const r3c1 = buildCell('Qty', leadQty.toString(), chalk.cyan, colL);
174
+ const r3c2 = buildCell('Qty', followerQty.toString(), chalk.cyan, colR);
171
175
  row(r3c1.padded, r3c2.padded);
172
176
 
173
177
  this._line(chalk.cyan(GM));
174
178
 
175
- // Row 4: Trades | Latency
176
- const r4c1t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
177
- const r4c1p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
178
- const r4c2 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colR);
179
- row(r4c1t + pad(colL - r4c1p.length), r4c2.padded);
179
+ // Row 4: Target | Risk
180
+ const r4c1 = buildCell('Target', '$' + (stats.target || 0).toFixed(2), chalk.green, colL);
181
+ const r4c2 = buildCell('Risk', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR);
182
+ row(r4c1.padded, r4c2.padded);
183
+
184
+ this._line(chalk.cyan(GM));
185
+
186
+ // Row 5: P&L | Server
187
+ const r5c1 = buildCell('P&L', pnlStr, pnlColor, colL);
188
+ const r5c2 = buildCell('Server', stats.connected ? 'ON' : 'OFF', serverColor, colR);
189
+ row(r5c1.padded, r5c2.padded);
190
+
191
+ this._line(chalk.cyan(GM));
192
+
193
+ // Row 6: Trades | Latency
194
+ const r6c1t = ` Trades: ${chalk.cyan(stats.trades || 0)} W/L: ${chalk.green(stats.wins || 0)}/${chalk.red(stats.losses || 0)}`;
195
+ const r6c1p = ` Trades: ${stats.trades || 0} W/L: ${stats.wins || 0}/${stats.losses || 0}`;
196
+ const r6c2 = buildCell('Latency', `${stats.latency || 0}ms`, latencyColor, colR);
197
+ row(r6c1t + pad(colL - r6c1p.length), r6c2.padded);
180
198
 
181
199
  this._line(chalk.cyan(GB));
182
200
  }
@@ -445,31 +445,11 @@ class ProjectXService {
445
445
 
446
446
  /**
447
447
  * Get popular contracts for trading
448
+ * Uses shared contract list for consistency with Rithmic
448
449
  */
449
450
  async getContracts() {
450
- try {
451
- // Search for popular futures symbols
452
- const symbols = ['ES', 'NQ', 'MES', 'MNQ', 'CL', 'GC', 'RTY', 'YM'];
453
- const allContracts = [];
454
-
455
- for (const sym of symbols) {
456
- const response = await this._request(
457
- this.propfirm.gatewayApi, '/api/Contract/search', 'POST',
458
- { searchText: sym, live: false }
459
- );
460
- if (response.statusCode === 200) {
461
- const contracts = response.data.contracts || response.data || [];
462
- // Take first contract for each symbol (front month)
463
- if (contracts.length > 0) {
464
- allContracts.push(contracts[0]);
465
- }
466
- }
467
- }
468
-
469
- return { success: true, contracts: allContracts };
470
- } catch (error) {
471
- return { success: false, contracts: [], error: error.message };
472
- }
451
+ const { getContractsWithMonthCode } = require('../../config/contracts');
452
+ return { success: true, contracts: getContractsWithMonthCode() };
473
453
  }
474
454
 
475
455
  async searchContracts(searchText) {
@@ -210,31 +210,14 @@ class RithmicService extends EventEmitter {
210
210
  };
211
211
  }
212
212
 
213
- // All available contracts for Rithmic
214
- _getAvailableContracts() {
215
- return [
216
- { symbol: 'ESH5', name: 'E-mini S&P 500 Mar 2025', exchange: 'CME', group: 'Index' },
217
- { symbol: 'NQH5', name: 'E-mini NASDAQ-100 Mar 2025', exchange: 'CME', group: 'Index' },
218
- { symbol: 'MESH5', name: 'Micro E-mini S&P 500 Mar 2025', exchange: 'CME', group: 'Micro' },
219
- { symbol: 'MNQH5', name: 'Micro E-mini NASDAQ-100 Mar 2025', exchange: 'CME', group: 'Micro' },
220
- { symbol: 'MCLE5', name: 'Micro Crude Oil Mar 2025', exchange: 'NYMEX', group: 'Micro' },
221
- { symbol: 'MGCG5', name: 'Micro Gold Feb 2025', exchange: 'COMEX', group: 'Micro' },
222
- { symbol: 'CLH5', name: 'Crude Oil Mar 2025', exchange: 'NYMEX', group: 'Energy' },
223
- { symbol: 'GCG5', name: 'Gold Feb 2025', exchange: 'COMEX', group: 'Metals' },
224
- { symbol: 'SIH5', name: 'Silver Mar 2025', exchange: 'COMEX', group: 'Metals' },
225
- { symbol: 'RTYH5', name: 'E-mini Russell 2000 Mar 2025', exchange: 'CME', group: 'Index' },
226
- { symbol: 'YMH5', name: 'E-mini Dow Jones Mar 2025', exchange: 'CBOT', group: 'Index' },
227
- { symbol: 'ZBH5', name: '30-Year US Treasury Bond Mar 2025', exchange: 'CBOT', group: 'Bonds' },
228
- { symbol: 'ZNH5', name: '10-Year US Treasury Note Mar 2025', exchange: 'CBOT', group: 'Bonds' },
229
- ];
230
- }
231
-
232
213
  async getContracts() {
233
- return { success: true, contracts: this._getAvailableContracts() };
214
+ const { getContractsWithMonthCode } = require('../../config/contracts');
215
+ return { success: true, contracts: getContractsWithMonthCode() };
234
216
  }
235
217
 
236
218
  async searchContracts(searchText) {
237
- const contracts = this._getAvailableContracts();
219
+ const { getContractsWithMonthCode } = require('../../config/contracts');
220
+ const contracts = getContractsWithMonthCode();
238
221
  if (!searchText) return contracts;
239
222
  const search = searchText.toUpperCase();
240
223
  return contracts.filter(c => c.symbol.includes(search) || c.name.toUpperCase().includes(search));