hedgequantx 1.5.7 → 1.5.8

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.8",
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": {
@@ -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 = {
@@ -19,115 +19,78 @@ const dashboardMenu = async (service) => {
19
19
  // Ensure stdin is ready for prompts
20
20
  prepareStdin();
21
21
 
22
- const user = service.user;
23
22
  const boxWidth = getLogoWidth();
24
- const W = boxWidth - 2; // Same width as logo (inner width)
23
+ const W = boxWidth - 2; // Inner width (without borders)
25
24
 
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));
25
+ // Helper to create a line that fits exactly in the box
26
+ const makeLine = (content, align = 'left') => {
27
+ const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
28
+ const padding = W - plainLen;
29
+ if (align === 'center') {
30
+ const leftPad = Math.floor(padding / 2);
31
+ const rightPad = padding - leftPad;
32
+ return chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║');
33
+ }
34
+ return chalk.cyan('║') + content + ' '.repeat(Math.max(0, padding)) + chalk.cyan('║');
35
35
  };
36
36
 
37
37
  // Dashboard box header
38
38
  console.log();
39
39
  console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
40
- console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan(''));
40
+ console.log(makeLine(chalk.yellow.bold('Welcome, HQX Trader!'), 'center'));
41
41
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
42
42
 
43
- // Connection info - show all active connections in boxes (max 3 per row)
43
+ // Show connected propfirms centered on one line (max 3)
44
44
  const allConns = connections.getAll();
45
45
  if (allConns.length > 0) {
46
- const maxPerRow = 3;
47
- const boxPadding = 2; // padding inside each mini-box
48
- const gap = 2; // gap between boxes
49
-
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
54
-
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('║'));
92
- }
46
+ const propfirms = allConns.slice(0, 3).map(c => c.propfirm || c.type || 'Connected');
47
+ const propfirmText = propfirms.map(p => chalk.green('● ') + chalk.white(p)).join(' ');
48
+ console.log(makeLine(propfirmText, 'center'));
93
49
  }
94
50
 
95
51
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
96
52
 
97
53
  // Menu options in 2 columns
98
54
  const col1Width = Math.floor(W / 2);
99
- const col2Width = W - col1Width;
100
55
 
101
56
  const menuRow = (left, right) => {
102
57
  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('║'));
58
+ const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '');
59
+ const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
60
+ const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain.length));
61
+ console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
107
62
  };
108
63
 
64
+ // Display menu items in 2 columns inside the box
65
+ menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
66
+ menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.magenta('[A] Algo-Trading'));
67
+ menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
68
+
109
69
  console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
110
70
  console.log();
111
-
112
- // Use list type instead of input - more stable stdin handling
113
- const { action } = await inquirer.prompt([
71
+
72
+ // Input prompt
73
+ const { choice } = await inquirer.prompt([
114
74
  {
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
75
+ type: 'input',
76
+ name: 'choice',
77
+ message: chalk.cyan('Select:'),
78
+ prefix: ''
127
79
  }
128
80
  ]);
129
-
130
- return action;
81
+
82
+ // Map input to action
83
+ const input = (choice || '').toString().toLowerCase().trim();
84
+ const actionMap = {
85
+ '1': 'accounts',
86
+ '2': 'stats',
87
+ '+': 'add_prop_account',
88
+ 'a': 'algotrading',
89
+ 'u': 'update',
90
+ 'x': 'disconnect'
91
+ };
92
+
93
+ return actionMap[input] || null;
131
94
  };
132
95
 
133
96
  /**
@@ -188,43 +151,19 @@ const handleUpdate = async () => {
188
151
 
189
152
  // Compare versions
190
153
  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}`));
154
+ spinner.succeed(`Already up to date! (v${currentVersion})`);
194
155
  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'));
216
- console.log();
217
- await waitForEnter();
156
+ await new Promise(r => setTimeout(r, 2000));
218
157
  return;
219
158
  }
220
159
 
221
- // Update via npm
222
- spinner = ora({ text: `Updating v${currentVersion} -> v${latestVersion}...`, color: 'yellow' }).start();
160
+ // Show version info and update automatically
161
+ spinner.text = `Updating v${currentVersion} v${latestVersion}...`;
223
162
 
224
163
  try {
225
164
  execSync('npm install -g hedgequantx@latest 2>/dev/null', {
226
165
  stdio: 'pipe',
227
- timeout: 120000, // 2 minute timeout for install
166
+ timeout: 120000,
228
167
  encoding: 'utf8'
229
168
  });
230
169
  } catch (e) {
@@ -233,52 +172,29 @@ const handleUpdate = async () => {
233
172
  console.log(chalk.yellow(' Try manually:'));
234
173
  console.log(chalk.white(' npm install -g hedgequantx@latest'));
235
174
  console.log();
236
- if (e.message) {
237
- console.log(chalk.gray(` Error: ${e.message.substring(0, 100)}`));
238
- console.log();
239
- }
240
175
  await waitForEnter();
241
176
  return;
242
177
  }
243
178
 
244
- spinner.succeed('Update complete!');
179
+ spinner.succeed(`Updated: v${currentVersion} → v${latestVersion}`);
245
180
  console.log();
246
- console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
181
+ console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
247
182
  console.log();
248
183
 
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
- }]);
184
+ // Auto restart after 2 seconds
185
+ await new Promise(r => setTimeout(r, 2000));
257
186
 
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'));
187
+ // Restart the CLI
188
+ try {
189
+ const child = spawn('hedgequantx', [], {
190
+ stdio: 'inherit',
191
+ detached: true,
192
+ shell: true
193
+ });
194
+ child.unref();
195
+ process.exit(0);
196
+ } catch (e) {
197
+ console.log(chalk.yellow(' Could not auto-restart. Please run: hedgequantx'));
282
198
  console.log();
283
199
  await waitForEnter();
284
200
  }
@@ -449,7 +449,7 @@ class ProjectXService {
449
449
  async getContracts() {
450
450
  try {
451
451
  // Search for popular futures symbols
452
- const symbols = ['ES', 'NQ', 'MES', 'MNQ', 'CL', 'GC', 'RTY', 'YM'];
452
+ const symbols = ['ES', 'NQ', 'MES', 'MNQ', 'CL', 'GC', 'RTY', 'YM', 'SI', 'ZB', 'ZN', 'NG'];
453
453
  const allContracts = [];
454
454
 
455
455
  for (const sym of symbols) {
@@ -461,7 +461,12 @@ class ProjectXService {
461
461
  const contracts = response.data.contracts || response.data || [];
462
462
  // Take first contract for each symbol (front month)
463
463
  if (contracts.length > 0) {
464
- allContracts.push(contracts[0]);
464
+ const contract = contracts[0];
465
+ // Ensure name is set properly
466
+ if (!contract.name || contract.name === contract.symbol) {
467
+ contract.name = this._getContractDisplayName(contract.symbol) || contract.symbol;
468
+ }
469
+ allContracts.push(contract);
465
470
  }
466
471
  }
467
472
  }
@@ -472,6 +477,46 @@ class ProjectXService {
472
477
  }
473
478
  }
474
479
 
480
+ /**
481
+ * Get display name for contract symbol
482
+ */
483
+ _getContractDisplayName(symbol) {
484
+ const baseSymbol = symbol.replace(/[A-Z][0-9]$/, '').replace(/[0-9]+$/, '');
485
+ const names = {
486
+ 'ES': 'E-mini S&P 500',
487
+ 'NQ': 'E-mini NASDAQ-100',
488
+ 'MES': 'Micro E-mini S&P 500',
489
+ 'MNQ': 'Micro E-mini NASDAQ-100',
490
+ 'RTY': 'E-mini Russell 2000',
491
+ 'M2K': 'Micro E-mini Russell 2000',
492
+ 'YM': 'E-mini Dow Jones',
493
+ 'MYM': 'Micro E-mini Dow Jones',
494
+ 'CL': 'Crude Oil',
495
+ 'MCL': 'Micro Crude Oil',
496
+ 'GC': 'Gold',
497
+ 'MGC': 'Micro Gold',
498
+ 'SI': 'Silver',
499
+ 'SIL': 'Micro Silver',
500
+ 'NG': 'Natural Gas',
501
+ 'ZB': '30-Year Treasury Bond',
502
+ 'ZN': '10-Year Treasury Note',
503
+ 'ZF': '5-Year Treasury Note',
504
+ 'ZC': 'Corn',
505
+ 'ZS': 'Soybeans',
506
+ 'ZW': 'Wheat',
507
+ '6E': 'Euro FX',
508
+ '6B': 'British Pound',
509
+ '6J': 'Japanese Yen',
510
+ };
511
+ const baseName = names[baseSymbol];
512
+ if (baseName) {
513
+ // Extract month/year from symbol
514
+ const monthYear = symbol.slice(baseSymbol.length);
515
+ return `${baseName} (${monthYear})`;
516
+ }
517
+ return symbol;
518
+ }
519
+
475
520
  async searchContracts(searchText) {
476
521
  try {
477
522
  const response = await this._request(
@@ -211,21 +211,59 @@ class RithmicService extends EventEmitter {
211
211
  }
212
212
 
213
213
  // All available contracts for Rithmic
214
+ // TODO: Fetch from TICKER_PLANT API instead of static list
214
215
  _getAvailableContracts() {
216
+ // Current front-month contracts (update monthly)
215
217
  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' },
218
+ // Index Futures
219
+ { symbol: 'ESH5', name: 'E-mini S&P 500 (Mar 25)', exchange: 'CME', group: 'Index' },
220
+ { symbol: 'NQH5', name: 'E-mini NASDAQ-100 (Mar 25)', exchange: 'CME', group: 'Index' },
221
+ { symbol: 'RTYH5', name: 'E-mini Russell 2000 (Mar 25)', exchange: 'CME', group: 'Index' },
222
+ { symbol: 'YMH5', name: 'E-mini Dow Jones (Mar 25)', exchange: 'CBOT', group: 'Index' },
223
+
224
+ // Micro Index Futures
225
+ { symbol: 'MESH5', name: 'Micro E-mini S&P 500 (Mar 25)', exchange: 'CME', group: 'Micro Index' },
226
+ { symbol: 'MNQH5', name: 'Micro E-mini NASDAQ-100 (Mar 25)', exchange: 'CME', group: 'Micro Index' },
227
+ { symbol: 'M2KH5', name: 'Micro E-mini Russell 2000 (Mar 25)', exchange: 'CME', group: 'Micro Index' },
228
+ { symbol: 'MYMH5', name: 'Micro E-mini Dow Jones (Mar 25)', exchange: 'CBOT', group: 'Micro Index' },
229
+
230
+ // Energy Futures
231
+ { symbol: 'CLG5', name: 'Crude Oil (Feb 25)', exchange: 'NYMEX', group: 'Energy' },
232
+ { symbol: 'CLH5', name: 'Crude Oil (Mar 25)', exchange: 'NYMEX', group: 'Energy' },
233
+ { symbol: 'NGG5', name: 'Natural Gas (Feb 25)', exchange: 'NYMEX', group: 'Energy' },
234
+ { symbol: 'NGH5', name: 'Natural Gas (Mar 25)', exchange: 'NYMEX', group: 'Energy' },
235
+
236
+ // Micro Energy
237
+ { symbol: 'MCLG5', name: 'Micro Crude Oil (Feb 25)', exchange: 'NYMEX', group: 'Micro Energy' },
238
+ { symbol: 'MCLH5', name: 'Micro Crude Oil (Mar 25)', exchange: 'NYMEX', group: 'Micro Energy' },
239
+
240
+ // Metals Futures
241
+ { symbol: 'GCG5', name: 'Gold (Feb 25)', exchange: 'COMEX', group: 'Metals' },
242
+ { symbol: 'GCJ5', name: 'Gold (Apr 25)', exchange: 'COMEX', group: 'Metals' },
243
+ { symbol: 'SIH5', name: 'Silver (Mar 25)', exchange: 'COMEX', group: 'Metals' },
244
+ { symbol: 'HGH5', name: 'Copper (Mar 25)', exchange: 'COMEX', group: 'Metals' },
245
+
246
+ // Micro Metals
247
+ { symbol: 'MGCG5', name: 'Micro Gold (Feb 25)', exchange: 'COMEX', group: 'Micro Metals' },
248
+ { symbol: 'MGCJ5', name: 'Micro Gold (Apr 25)', exchange: 'COMEX', group: 'Micro Metals' },
249
+ { symbol: 'SILU5', name: 'Micro Silver (Sep 25)', exchange: 'COMEX', group: 'Micro Metals' },
250
+
251
+ // Treasury Futures
252
+ { symbol: 'ZBH5', name: '30-Year US Treasury Bond (Mar 25)', exchange: 'CBOT', group: 'Bonds' },
253
+ { symbol: 'ZNH5', name: '10-Year US Treasury Note (Mar 25)', exchange: 'CBOT', group: 'Bonds' },
254
+ { symbol: 'ZFH5', name: '5-Year US Treasury Note (Mar 25)', exchange: 'CBOT', group: 'Bonds' },
255
+ { symbol: 'ZTH5', name: '2-Year US Treasury Note (Mar 25)', exchange: 'CBOT', group: 'Bonds' },
256
+
257
+ // Agriculture Futures
258
+ { symbol: 'ZCH5', name: 'Corn (Mar 25)', exchange: 'CBOT', group: 'Agriculture' },
259
+ { symbol: 'ZSH5', name: 'Soybeans (Mar 25)', exchange: 'CBOT', group: 'Agriculture' },
260
+ { symbol: 'ZWH5', name: 'Wheat (Mar 25)', exchange: 'CBOT', group: 'Agriculture' },
261
+
262
+ // Currency Futures
263
+ { symbol: '6EH5', name: 'Euro FX (Mar 25)', exchange: 'CME', group: 'Currency' },
264
+ { symbol: '6BH5', name: 'British Pound (Mar 25)', exchange: 'CME', group: 'Currency' },
265
+ { symbol: '6JH5', name: 'Japanese Yen (Mar 25)', exchange: 'CME', group: 'Currency' },
266
+ { symbol: '6AH5', name: 'Australian Dollar (Mar 25)', exchange: 'CME', group: 'Currency' },
229
267
  ];
230
268
  }
231
269