hedgequantx 1.2.146 → 1.3.0

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.
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Connection Menus - PropFirm platform selection and login
3
+ * Handles ProjectX, Rithmic, and Tradovate connections
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+ const ora = require('ora');
9
+
10
+ const { ProjectXService, connections } = require('../services');
11
+ const { RithmicService } = require('../services/rithmic');
12
+ const { TradovateService } = require('../services/tradovate');
13
+ const { getPropFirmsByPlatform } = require('../config');
14
+ const { getDevice, getLogoWidth, centerText, prepareStdin } = require('../ui');
15
+ const { validateUsername, validatePassword } = require('../security');
16
+
17
+ /**
18
+ * Login prompt with validation
19
+ * @param {string} propfirmName - PropFirm display name
20
+ * @returns {Promise<{username: string, password: string}>}
21
+ */
22
+ const loginPrompt = async (propfirmName) => {
23
+ prepareStdin();
24
+ const device = getDevice();
25
+ console.log();
26
+ console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
27
+ console.log();
28
+
29
+ const credentials = await inquirer.prompt([
30
+ {
31
+ type: 'input',
32
+ name: 'username',
33
+ message: chalk.white.bold('Username:'),
34
+ validate: (input) => {
35
+ try {
36
+ validateUsername(input);
37
+ return true;
38
+ } catch (e) {
39
+ return e.message;
40
+ }
41
+ }
42
+ },
43
+ {
44
+ type: 'password',
45
+ name: 'password',
46
+ message: chalk.white.bold('Password:'),
47
+ mask: '*',
48
+ validate: (input) => {
49
+ try {
50
+ validatePassword(input);
51
+ return true;
52
+ } catch (e) {
53
+ return e.message;
54
+ }
55
+ }
56
+ }
57
+ ]);
58
+
59
+ return credentials;
60
+ };
61
+
62
+ /**
63
+ * ProjectX platform connection menu
64
+ */
65
+ const projectXMenu = async () => {
66
+ const propfirms = getPropFirmsByPlatform('ProjectX');
67
+ const boxWidth = getLogoWidth();
68
+ const innerWidth = boxWidth - 2;
69
+ const numCols = 3;
70
+ const colWidth = Math.floor(innerWidth / numCols);
71
+
72
+ // Build numbered list
73
+ const numbered = propfirms.map((pf, i) => ({
74
+ num: i + 1,
75
+ key: pf.key,
76
+ name: pf.displayName
77
+ }));
78
+
79
+ // PropFirm selection box
80
+ 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('║'));
84
+
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]
89
+
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('║'));
109
+ }
110
+
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) + '╝'));
116
+ console.log();
117
+
118
+ const validInputs = numbered.map(n => n.num.toString());
119
+ validInputs.push('x', 'X');
120
+
121
+ const { action } = await inquirer.prompt([
122
+ {
123
+ type: 'input',
124
+ 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
+ }
130
+ }
131
+ ]);
132
+
133
+ if (action.toLowerCase() === 'x') return null;
134
+
135
+ const selectedIdx = parseInt(action) - 1;
136
+ const selectedPropfirm = numbered[selectedIdx];
137
+
138
+ const credentials = await loginPrompt(selectedPropfirm.name);
139
+ const spinner = ora('Authenticating...').start();
140
+
141
+ try {
142
+ const service = new ProjectXService(selectedPropfirm.key);
143
+ const result = await service.login(credentials.username, credentials.password);
144
+
145
+ if (result.success) {
146
+ await service.getUser();
147
+ connections.add('projectx', service, service.propfirm.name);
148
+ spinner.succeed(`Connected to ${service.propfirm.name}`);
149
+ return service;
150
+ } else {
151
+ spinner.fail(result.error || 'Authentication failed');
152
+ return null;
153
+ }
154
+ } catch (error) {
155
+ spinner.fail(error.message);
156
+ return null;
157
+ }
158
+ };
159
+
160
+ /**
161
+ * Rithmic platform connection menu
162
+ */
163
+ const rithmicMenu = async () => {
164
+ const propfirms = getPropFirmsByPlatform('Rithmic');
165
+ const boxWidth = getLogoWidth();
166
+ const innerWidth = boxWidth - 2;
167
+ const numCols = 3;
168
+ const colWidth = Math.floor(innerWidth / numCols);
169
+
170
+ // Build numbered list
171
+ const numbered = propfirms.map((pf, i) => ({
172
+ num: i + 1,
173
+ key: pf.key,
174
+ name: pf.displayName,
175
+ systemName: pf.rithmicSystem
176
+ }));
177
+
178
+ // PropFirm selection box
179
+ console.log();
180
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
181
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
182
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
183
+
184
+ // Display in 3 columns with fixed width alignment
185
+ const rows = Math.ceil(numbered.length / numCols);
186
+
187
+ for (let row = 0; row < rows; row++) {
188
+ let line = '';
189
+ for (let col = 0; col < numCols; col++) {
190
+ const idx = row + col * rows;
191
+ if (idx < numbered.length) {
192
+ const item = numbered[idx];
193
+ const numStr = item.num.toString().padStart(2, ' ');
194
+ const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
195
+ const textLen = 4 + 1 + item.name.length;
196
+ const padding = colWidth - textLen - 2;
197
+ line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
198
+ } else {
199
+ line += ' '.repeat(colWidth);
200
+ }
201
+ }
202
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
203
+ const adjust = innerWidth - lineLen;
204
+ console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
205
+ }
206
+
207
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
208
+ const backText = ' ' + chalk.red('[X] Back');
209
+ const backLen = '[X] Back'.length + 2;
210
+ console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
211
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
212
+ console.log();
213
+
214
+ const validInputs = numbered.map(n => n.num.toString());
215
+ validInputs.push('x', 'X');
216
+
217
+ const { action } = await inquirer.prompt([
218
+ {
219
+ type: 'input',
220
+ name: 'action',
221
+ message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
222
+ validate: (input) => {
223
+ if (validInputs.includes(input)) return true;
224
+ return `Please enter 1-${numbered.length} or X`;
225
+ }
226
+ }
227
+ ]);
228
+
229
+ if (action.toLowerCase() === 'x') return null;
230
+
231
+ const selectedIdx = parseInt(action) - 1;
232
+ const selectedPropfirm = numbered[selectedIdx];
233
+
234
+ const credentials = await loginPrompt(selectedPropfirm.name);
235
+ const spinner = ora('Connecting to Rithmic...').start();
236
+
237
+ try {
238
+ const service = new RithmicService(selectedPropfirm.key);
239
+ const result = await service.login(credentials.username, credentials.password);
240
+
241
+ if (result.success) {
242
+ spinner.text = 'Fetching accounts...';
243
+ const accResult = await service.getTradingAccounts();
244
+
245
+ connections.add('rithmic', service, service.propfirm.name);
246
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
247
+
248
+ // Small pause to see the success message
249
+ await new Promise(r => setTimeout(r, 1500));
250
+ return service;
251
+ } else {
252
+ spinner.fail(result.error || 'Authentication failed');
253
+ await new Promise(r => setTimeout(r, 2000));
254
+ return null;
255
+ }
256
+ } catch (error) {
257
+ spinner.fail(`Connection error: ${error.message}`);
258
+ await new Promise(r => setTimeout(r, 2000));
259
+ return null;
260
+ }
261
+ };
262
+
263
+ /**
264
+ * Tradovate platform connection menu
265
+ */
266
+ const tradovateMenu = async () => {
267
+ const propfirms = getPropFirmsByPlatform('Tradovate');
268
+ const boxWidth = getLogoWidth();
269
+ const innerWidth = boxWidth - 2;
270
+
271
+ // Build numbered list
272
+ const numbered = propfirms.map((pf, i) => ({
273
+ num: i + 1,
274
+ key: pf.key,
275
+ name: pf.displayName
276
+ }));
277
+
278
+ // PropFirm selection box
279
+ console.log();
280
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
281
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
282
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
283
+
284
+ // Display propfirms
285
+ for (const item of numbered) {
286
+ const numStr = item.num.toString().padStart(2, ' ');
287
+ const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
288
+ const textLen = 4 + 1 + item.name.length + 2;
289
+ console.log(chalk.cyan('║') + text + ' '.repeat(innerWidth - textLen) + chalk.cyan('║'));
290
+ }
291
+
292
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
293
+ const backText = ' ' + chalk.red('[X] Back');
294
+ const backLen = '[X] Back'.length + 2;
295
+ console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
296
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
297
+ console.log();
298
+
299
+ const validInputs = numbered.map(n => n.num.toString());
300
+ validInputs.push('x', 'X');
301
+
302
+ const { action } = await inquirer.prompt([
303
+ {
304
+ type: 'input',
305
+ name: 'action',
306
+ message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
307
+ validate: (input) => {
308
+ if (validInputs.includes(input)) return true;
309
+ return `Please enter 1-${numbered.length} or X`;
310
+ }
311
+ }
312
+ ]);
313
+
314
+ if (action.toLowerCase() === 'x') return null;
315
+
316
+ const selectedIdx = parseInt(action) - 1;
317
+ const selectedPropfirm = numbered[selectedIdx];
318
+
319
+ const credentials = await loginPrompt(selectedPropfirm.name);
320
+ const spinner = ora('Connecting to Tradovate...').start();
321
+
322
+ try {
323
+ const service = new TradovateService(selectedPropfirm.key);
324
+ const result = await service.login(credentials.username, credentials.password);
325
+
326
+ if (result.success) {
327
+ spinner.text = 'Fetching accounts...';
328
+ await service.getTradingAccounts();
329
+
330
+ connections.add('tradovate', service, service.propfirm.name);
331
+ spinner.succeed(`Connected to ${service.propfirm.name}`);
332
+ return service;
333
+ } else {
334
+ spinner.fail(result.error || 'Authentication failed');
335
+ return null;
336
+ }
337
+ } catch (error) {
338
+ spinner.fail(error.message);
339
+ return null;
340
+ }
341
+ };
342
+
343
+ /**
344
+ * Add Prop Account menu (select platform)
345
+ */
346
+ const addPropAccountMenu = async () => {
347
+ const boxWidth = getLogoWidth();
348
+ const innerWidth = boxWidth - 2;
349
+ const col1Width = Math.floor(innerWidth / 2);
350
+ const col2Width = innerWidth - col1Width;
351
+
352
+ 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) + '╣'));
356
+
357
+ 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('║'));
365
+ };
366
+
367
+ menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
368
+ menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
369
+
370
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
371
+ console.log();
372
+
373
+ const { action } = await inquirer.prompt([
374
+ {
375
+ type: 'input',
376
+ 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
+ }
383
+ }
384
+ ]);
385
+
386
+ const actionMap = {
387
+ '1': 'projectx',
388
+ '2': 'rithmic',
389
+ '3': 'tradovate',
390
+ 'x': null,
391
+ 'X': null
392
+ };
393
+
394
+ return actionMap[action];
395
+ };
396
+
397
+ module.exports = {
398
+ loginPrompt,
399
+ projectXMenu,
400
+ rithmicMenu,
401
+ tradovateMenu,
402
+ addPropAccountMenu
403
+ };