hedgequantx 1.7.5 → 1.7.7
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 +2 -2
- package/src/app.js +38 -159
- package/src/menus/connect.js +54 -166
- package/src/menus/dashboard.js +39 -122
- package/src/pages/accounts.js +11 -22
- package/src/pages/algo/copy-trading.js +63 -210
- package/src/pages/algo/index.js +10 -20
- package/src/pages/algo/one-account.js +66 -172
- package/src/pages/orders.js +11 -32
- package/src/pages/positions.js +11 -32
- package/src/pages/stats.js +5 -14
- package/src/pages/user.js +8 -22
- package/src/utils/index.js +2 -1
- package/src/utils/prompts.js +87 -0
package/src/menus/connect.js
CHANGED
|
@@ -1,87 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connection Menus - PropFirm platform selection and login
|
|
3
|
-
* Handles ProjectX, Rithmic, and Tradovate connections
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const chalk = require('chalk');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
6
|
const ora = require('ora');
|
|
9
7
|
|
|
10
8
|
const { ProjectXService, connections } = require('../services');
|
|
11
9
|
const { RithmicService } = require('../services/rithmic');
|
|
12
10
|
const { TradovateService } = require('../services/tradovate');
|
|
13
11
|
const { getPropFirmsByPlatform } = require('../config');
|
|
14
|
-
const {
|
|
12
|
+
const { getLogoWidth, centerText, prepareStdin } = require('../ui');
|
|
15
13
|
const { validateUsername, validatePassword } = require('../security');
|
|
14
|
+
const { prompts } = require('../utils');
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* Login prompt
|
|
19
|
-
* @param {string} propfirmName - PropFirm display name
|
|
20
|
-
* @returns {Promise<{username: string, password: string}>}
|
|
17
|
+
* Login prompt
|
|
21
18
|
*/
|
|
22
19
|
const loginPrompt = async (propfirmName) => {
|
|
23
20
|
prepareStdin();
|
|
24
|
-
const device = getDevice();
|
|
25
21
|
console.log();
|
|
26
22
|
console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
|
|
27
23
|
console.log();
|
|
28
24
|
|
|
29
|
-
const
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
]);
|
|
25
|
+
const username = await prompts.textInput('Username:', '', (input) => {
|
|
26
|
+
try { validateUsername(input); return undefined; } catch (e) { return e.message; }
|
|
27
|
+
});
|
|
28
|
+
if (!username) return null;
|
|
29
|
+
|
|
30
|
+
const pwd = await prompts.passwordInput('Password:', (input) => {
|
|
31
|
+
try { validatePassword(input); return undefined; } catch (e) { return e.message; }
|
|
32
|
+
});
|
|
33
|
+
if (!pwd) return null;
|
|
58
34
|
|
|
59
|
-
return
|
|
35
|
+
return { username, password: pwd };
|
|
60
36
|
};
|
|
61
37
|
|
|
62
38
|
/**
|
|
63
|
-
* ProjectX
|
|
39
|
+
* ProjectX menu
|
|
64
40
|
*/
|
|
65
41
|
const projectXMenu = async () => {
|
|
66
42
|
const propfirms = getPropFirmsByPlatform('ProjectX');
|
|
67
43
|
const boxWidth = getLogoWidth();
|
|
68
|
-
const W = boxWidth - 2;
|
|
44
|
+
const W = boxWidth - 2;
|
|
69
45
|
const col1Width = Math.floor(W / 2);
|
|
70
46
|
|
|
71
|
-
|
|
72
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
73
|
-
num: i + 1,
|
|
74
|
-
key: pf.key,
|
|
75
|
-
name: pf.displayName
|
|
76
|
-
}));
|
|
47
|
+
const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.key, name: pf.displayName }));
|
|
77
48
|
|
|
78
|
-
// PropFirm selection box
|
|
79
49
|
console.log();
|
|
80
50
|
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
81
51
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (ProjectX)', W)) + chalk.cyan('║'));
|
|
82
52
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
83
53
|
|
|
84
|
-
// Display in 2 columns
|
|
85
54
|
const menuRow = (left, right) => {
|
|
86
55
|
const leftPlain = left ? left.replace(/\x1b\[[0-9;]*m/g, '') : '';
|
|
87
56
|
const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
|
|
@@ -90,7 +59,6 @@ const projectXMenu = async () => {
|
|
|
90
59
|
console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
|
|
91
60
|
};
|
|
92
61
|
|
|
93
|
-
// Display propfirms in 2 columns
|
|
94
62
|
for (let i = 0; i < numbered.length; i += 2) {
|
|
95
63
|
const left = numbered[i];
|
|
96
64
|
const right = numbered[i + 1];
|
|
@@ -99,31 +67,20 @@ const projectXMenu = async () => {
|
|
|
99
67
|
menuRow(leftText, rightText);
|
|
100
68
|
}
|
|
101
69
|
|
|
102
|
-
// Back option
|
|
103
70
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
104
|
-
|
|
105
|
-
console.log(chalk.cyan('║') + backLine + chalk.cyan('║'));
|
|
71
|
+
console.log(chalk.cyan('║') + ' ' + chalk.red('[X] Back') + ' '.repeat(W - 10) + chalk.cyan('║'));
|
|
106
72
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
107
|
-
console.log();
|
|
108
|
-
|
|
109
|
-
const { action } = await inquirer.prompt([
|
|
110
|
-
{
|
|
111
|
-
type: 'input',
|
|
112
|
-
name: 'action',
|
|
113
|
-
message: chalk.cyan('Select:'),
|
|
114
|
-
prefix: ''
|
|
115
|
-
}
|
|
116
|
-
]);
|
|
117
73
|
|
|
118
|
-
const
|
|
119
|
-
|
|
74
|
+
const options = numbered.map(pf => ({ value: pf.num, label: `[${pf.num}] ${pf.name}` }));
|
|
75
|
+
options.push({ value: -1, label: '[X] Back' });
|
|
120
76
|
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
77
|
+
const action = await prompts.selectOption('Select:', options);
|
|
78
|
+
if (!action || action === -1) return null;
|
|
123
79
|
|
|
124
|
-
const selectedPropfirm = numbered[
|
|
125
|
-
|
|
80
|
+
const selectedPropfirm = numbered[action - 1];
|
|
126
81
|
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
82
|
+
if (!credentials) return null;
|
|
83
|
+
|
|
127
84
|
const spinner = ora({ text: 'Authenticating...', color: 'yellow' }).start();
|
|
128
85
|
|
|
129
86
|
try {
|
|
@@ -146,7 +103,7 @@ const projectXMenu = async () => {
|
|
|
146
103
|
};
|
|
147
104
|
|
|
148
105
|
/**
|
|
149
|
-
* Rithmic
|
|
106
|
+
* Rithmic menu
|
|
150
107
|
*/
|
|
151
108
|
const rithmicMenu = async () => {
|
|
152
109
|
const propfirms = getPropFirmsByPlatform('Rithmic');
|
|
@@ -155,23 +112,14 @@ const rithmicMenu = async () => {
|
|
|
155
112
|
const numCols = 3;
|
|
156
113
|
const colWidth = Math.floor(innerWidth / numCols);
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
160
|
-
num: i + 1,
|
|
161
|
-
key: pf.key,
|
|
162
|
-
name: pf.displayName,
|
|
163
|
-
systemName: pf.rithmicSystem
|
|
164
|
-
}));
|
|
115
|
+
const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.key, name: pf.displayName, systemName: pf.rithmicSystem }));
|
|
165
116
|
|
|
166
|
-
// PropFirm selection box
|
|
167
117
|
console.log();
|
|
168
118
|
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
169
119
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
|
|
170
120
|
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
171
121
|
|
|
172
|
-
// Display in 3 columns with fixed width alignment
|
|
173
122
|
const rows = Math.ceil(numbered.length / numCols);
|
|
174
|
-
|
|
175
123
|
for (let row = 0; row < rows; row++) {
|
|
176
124
|
let line = '';
|
|
177
125
|
for (let col = 0; col < numCols; col++) {
|
|
@@ -181,45 +129,29 @@ const rithmicMenu = async () => {
|
|
|
181
129
|
const numStr = item.num.toString().padStart(2, ' ');
|
|
182
130
|
const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
183
131
|
const textLen = 4 + 1 + item.name.length;
|
|
184
|
-
|
|
185
|
-
line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
|
|
132
|
+
line += ' ' + coloredText + ' '.repeat(Math.max(0, colWidth - textLen - 2));
|
|
186
133
|
} else {
|
|
187
134
|
line += ' '.repeat(colWidth);
|
|
188
135
|
}
|
|
189
136
|
}
|
|
190
137
|
const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
191
|
-
|
|
192
|
-
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
|
|
138
|
+
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, innerWidth - lineLen)) + chalk.cyan('║'));
|
|
193
139
|
}
|
|
194
140
|
|
|
195
141
|
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
196
|
-
|
|
197
|
-
const backLen = '[X] Back'.length + 2;
|
|
198
|
-
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
142
|
+
console.log(chalk.cyan('║') + ' ' + chalk.red('[X] Back') + ' '.repeat(innerWidth - 10) + chalk.cyan('║'));
|
|
199
143
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
200
|
-
console.log();
|
|
201
144
|
|
|
202
|
-
const
|
|
203
|
-
|
|
145
|
+
const options = numbered.map(pf => ({ value: pf.num, label: `[${pf.num}] ${pf.name}` }));
|
|
146
|
+
options.push({ value: -1, label: '[X] Back' });
|
|
204
147
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
type: 'input',
|
|
208
|
-
name: 'action',
|
|
209
|
-
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
210
|
-
validate: (input) => {
|
|
211
|
-
if (validInputs.includes(input)) return true;
|
|
212
|
-
return `Please enter 1-${numbered.length} or X`;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
if (action.toLowerCase() === 'x') return null;
|
|
148
|
+
const action = await prompts.selectOption('Select:', options);
|
|
149
|
+
if (!action || action === -1) return null;
|
|
218
150
|
|
|
219
|
-
const
|
|
220
|
-
const selectedPropfirm = numbered[selectedIdx];
|
|
221
|
-
|
|
151
|
+
const selectedPropfirm = numbered[action - 1];
|
|
222
152
|
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
153
|
+
if (!credentials) return null;
|
|
154
|
+
|
|
223
155
|
const spinner = ora({ text: 'Connecting to Rithmic...', color: 'yellow' }).start();
|
|
224
156
|
|
|
225
157
|
try {
|
|
@@ -229,11 +161,8 @@ const rithmicMenu = async () => {
|
|
|
229
161
|
if (result.success) {
|
|
230
162
|
spinner.text = 'Fetching accounts...';
|
|
231
163
|
const accResult = await service.getTradingAccounts();
|
|
232
|
-
|
|
233
164
|
connections.add('rithmic', service, service.propfirm.name);
|
|
234
165
|
spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
|
|
235
|
-
|
|
236
|
-
// Small pause to see the success message
|
|
237
166
|
await new Promise(r => setTimeout(r, 1500));
|
|
238
167
|
return service;
|
|
239
168
|
} else {
|
|
@@ -249,27 +178,20 @@ const rithmicMenu = async () => {
|
|
|
249
178
|
};
|
|
250
179
|
|
|
251
180
|
/**
|
|
252
|
-
* Tradovate
|
|
181
|
+
* Tradovate menu
|
|
253
182
|
*/
|
|
254
183
|
const tradovateMenu = async () => {
|
|
255
184
|
const propfirms = getPropFirmsByPlatform('Tradovate');
|
|
256
185
|
const boxWidth = getLogoWidth();
|
|
257
186
|
const innerWidth = boxWidth - 2;
|
|
258
187
|
|
|
259
|
-
|
|
260
|
-
const numbered = propfirms.map((pf, i) => ({
|
|
261
|
-
num: i + 1,
|
|
262
|
-
key: pf.key,
|
|
263
|
-
name: pf.displayName
|
|
264
|
-
}));
|
|
188
|
+
const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.key, name: pf.displayName }));
|
|
265
189
|
|
|
266
|
-
// PropFirm selection box
|
|
267
190
|
console.log();
|
|
268
191
|
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
269
192
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
|
|
270
193
|
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
271
194
|
|
|
272
|
-
// Display propfirms
|
|
273
195
|
for (const item of numbered) {
|
|
274
196
|
const numStr = item.num.toString().padStart(2, ' ');
|
|
275
197
|
const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
@@ -278,33 +200,19 @@ const tradovateMenu = async () => {
|
|
|
278
200
|
}
|
|
279
201
|
|
|
280
202
|
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
281
|
-
|
|
282
|
-
const backLen = '[X] Back'.length + 2;
|
|
283
|
-
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
203
|
+
console.log(chalk.cyan('║') + ' ' + chalk.red('[X] Back') + ' '.repeat(innerWidth - 10) + chalk.cyan('║'));
|
|
284
204
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
285
|
-
console.log();
|
|
286
205
|
|
|
287
|
-
const
|
|
288
|
-
|
|
206
|
+
const options = numbered.map(pf => ({ value: pf.num, label: `[${pf.num}] ${pf.name}` }));
|
|
207
|
+
options.push({ value: -1, label: '[X] Back' });
|
|
289
208
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
type: 'input',
|
|
293
|
-
name: 'action',
|
|
294
|
-
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
295
|
-
validate: (input) => {
|
|
296
|
-
if (validInputs.includes(input)) return true;
|
|
297
|
-
return `Please enter 1-${numbered.length} or X`;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
]);
|
|
301
|
-
|
|
302
|
-
if (action.toLowerCase() === 'x') return null;
|
|
209
|
+
const action = await prompts.selectOption('Select:', options);
|
|
210
|
+
if (!action || action === -1) return null;
|
|
303
211
|
|
|
304
|
-
const
|
|
305
|
-
const selectedPropfirm = numbered[selectedIdx];
|
|
306
|
-
|
|
212
|
+
const selectedPropfirm = numbered[action - 1];
|
|
307
213
|
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
214
|
+
if (!credentials) return null;
|
|
215
|
+
|
|
308
216
|
const spinner = ora({ text: 'Connecting to Tradovate...', color: 'yellow' }).start();
|
|
309
217
|
|
|
310
218
|
try {
|
|
@@ -314,7 +222,6 @@ const tradovateMenu = async () => {
|
|
|
314
222
|
if (result.success) {
|
|
315
223
|
spinner.text = 'Fetching accounts...';
|
|
316
224
|
await service.getTradingAccounts();
|
|
317
|
-
|
|
318
225
|
connections.add('tradovate', service, service.propfirm.name);
|
|
319
226
|
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
320
227
|
return service;
|
|
@@ -329,11 +236,11 @@ const tradovateMenu = async () => {
|
|
|
329
236
|
};
|
|
330
237
|
|
|
331
238
|
/**
|
|
332
|
-
* Add Prop Account menu
|
|
239
|
+
* Add Prop Account menu
|
|
333
240
|
*/
|
|
334
241
|
const addPropAccountMenu = async () => {
|
|
335
242
|
const boxWidth = getLogoWidth();
|
|
336
|
-
const W = boxWidth - 2;
|
|
243
|
+
const W = boxWidth - 2;
|
|
337
244
|
const col1Width = Math.floor(W / 2);
|
|
338
245
|
|
|
339
246
|
console.log();
|
|
@@ -353,32 +260,13 @@ const addPropAccountMenu = async () => {
|
|
|
353
260
|
menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Back'));
|
|
354
261
|
|
|
355
262
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
356
|
-
console.log();
|
|
357
263
|
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
prefix: ''
|
|
364
|
-
}
|
|
264
|
+
return await prompts.selectOption('Select:', [
|
|
265
|
+
{ value: 'projectx', label: '[1] ProjectX' },
|
|
266
|
+
{ value: 'rithmic', label: '[2] Rithmic' },
|
|
267
|
+
{ value: 'tradovate', label: '[3] Tradovate' },
|
|
268
|
+
{ value: null, label: '[X] Back' }
|
|
365
269
|
]);
|
|
366
|
-
|
|
367
|
-
const input = (action || '').toLowerCase().trim();
|
|
368
|
-
const actionMap = {
|
|
369
|
-
'1': 'projectx',
|
|
370
|
-
'2': 'rithmic',
|
|
371
|
-
'3': 'tradovate',
|
|
372
|
-
'x': null
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
return actionMap[input] || null;
|
|
376
270
|
};
|
|
377
271
|
|
|
378
|
-
module.exports = {
|
|
379
|
-
loginPrompt,
|
|
380
|
-
projectXMenu,
|
|
381
|
-
rithmicMenu,
|
|
382
|
-
tradovateMenu,
|
|
383
|
-
addPropAccountMenu
|
|
384
|
-
};
|
|
272
|
+
module.exports = { loginPrompt, projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu };
|
package/src/menus/dashboard.js
CHANGED
|
@@ -1,46 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard Menu - Main menu after login
|
|
3
|
-
* Shows connected PropFirms and navigation options
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const chalk = require('chalk');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
6
|
const ora = require('ora');
|
|
9
7
|
const { execSync, spawn } = require('child_process');
|
|
10
8
|
|
|
11
9
|
const { connections } = require('../services');
|
|
12
10
|
const { getLogoWidth, centerText, prepareStdin } = require('../ui');
|
|
13
11
|
const { getCachedStats } = require('../services/stats-cache');
|
|
12
|
+
const { prompts } = require('../utils');
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Dashboard menu after login
|
|
17
|
-
* @param {Object} service - Connected service
|
|
18
16
|
*/
|
|
19
17
|
const dashboardMenu = async (service) => {
|
|
20
|
-
// Ensure stdin is ready for prompts
|
|
21
18
|
prepareStdin();
|
|
22
19
|
|
|
23
20
|
const boxWidth = getLogoWidth();
|
|
24
|
-
const W = boxWidth - 2;
|
|
21
|
+
const W = boxWidth - 2;
|
|
25
22
|
|
|
26
|
-
// Helper to create a line that fits exactly in the box
|
|
27
23
|
const makeLine = (content, align = 'left') => {
|
|
28
24
|
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
29
25
|
const padding = W - plainLen;
|
|
30
26
|
if (align === 'center') {
|
|
31
27
|
const leftPad = Math.floor(padding / 2);
|
|
32
|
-
|
|
33
|
-
return chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║');
|
|
28
|
+
return chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(padding - leftPad) + chalk.cyan('║');
|
|
34
29
|
}
|
|
35
30
|
return chalk.cyan('║') + content + ' '.repeat(Math.max(0, padding)) + chalk.cyan('║');
|
|
36
31
|
};
|
|
37
32
|
|
|
38
|
-
// Dashboard box header
|
|
39
33
|
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
40
34
|
console.log(makeLine(chalk.yellow.bold('Welcome, HQX Trader!'), 'center'));
|
|
41
35
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
42
36
|
|
|
43
|
-
// Show connected propfirms
|
|
37
|
+
// Show connected propfirms
|
|
44
38
|
const allConns = connections.getAll();
|
|
45
39
|
if (allConns.length > 0) {
|
|
46
40
|
const propfirms = allConns.slice(0, 3).map(c => c.propfirm || c.type || 'Connected');
|
|
@@ -48,38 +42,30 @@ const dashboardMenu = async (service) => {
|
|
|
48
42
|
console.log(makeLine(propfirmText, 'center'));
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
//
|
|
45
|
+
// Stats bar
|
|
52
46
|
const statsInfo = getCachedStats();
|
|
53
|
-
|
|
54
47
|
if (statsInfo) {
|
|
55
48
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
56
49
|
|
|
57
|
-
const
|
|
58
|
-
const accStr = `Accounts: ${statsInfo.accounts}`;
|
|
59
|
-
|
|
60
|
-
const balStr = statsInfo.balance !== null
|
|
61
|
-
? `$${statsInfo.balance.toLocaleString()}`
|
|
62
|
-
: '--';
|
|
50
|
+
const balStr = statsInfo.balance !== null ? `$${statsInfo.balance.toLocaleString()}` : '--';
|
|
63
51
|
const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
|
|
64
52
|
|
|
65
53
|
let pnlDisplay, pnlColor;
|
|
66
54
|
if (statsInfo.pnl !== null) {
|
|
67
|
-
const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
|
|
68
55
|
pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
|
|
69
|
-
pnlDisplay = `${
|
|
56
|
+
pnlDisplay = `${statsInfo.pnl >= 0 ? '+' : ''}$${Math.abs(statsInfo.pnl).toLocaleString()}`;
|
|
70
57
|
} else {
|
|
71
58
|
pnlColor = chalk.gray;
|
|
72
59
|
pnlDisplay = '--';
|
|
73
60
|
}
|
|
74
61
|
|
|
75
|
-
const
|
|
76
|
-
const statsPlain = `${connStr} ${accStr} Balance: ${balStr} P&L: ${pnlDisplay}`;
|
|
62
|
+
const statsPlain = `Connections: ${statsInfo.connections} Accounts: ${statsInfo.accounts} Balance: ${balStr} P&L: ${pnlDisplay}`;
|
|
77
63
|
const statsLeftPad = Math.floor((W - statsPlain.length) / 2);
|
|
78
64
|
const statsRightPad = W - statsPlain.length - statsLeftPad;
|
|
79
65
|
|
|
80
66
|
console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
|
|
81
|
-
chalk.white(
|
|
82
|
-
chalk.white(
|
|
67
|
+
chalk.white(`Connections: ${statsInfo.connections}`) + ' ' +
|
|
68
|
+
chalk.white(`Accounts: ${statsInfo.accounts}`) + ' ' +
|
|
83
69
|
chalk.white('Balance: ') + balColor(balStr) + ' ' +
|
|
84
70
|
chalk.white('P&L: ') + pnlColor(pnlDisplay) +
|
|
85
71
|
' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║'));
|
|
@@ -87,9 +73,8 @@ const dashboardMenu = async (service) => {
|
|
|
87
73
|
|
|
88
74
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
89
75
|
|
|
90
|
-
// Menu
|
|
76
|
+
// Menu in 2 columns
|
|
91
77
|
const col1Width = Math.floor(W / 2);
|
|
92
|
-
|
|
93
78
|
const menuRow = (left, right) => {
|
|
94
79
|
const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
|
|
95
80
|
const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -98,161 +83,93 @@ const dashboardMenu = async (service) => {
|
|
|
98
83
|
console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
|
|
99
84
|
};
|
|
100
85
|
|
|
101
|
-
// Display menu items in 2 columns inside the box
|
|
102
86
|
menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
|
|
103
87
|
menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.magenta('[A] Algo-Trading'));
|
|
104
88
|
menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
|
|
105
89
|
|
|
106
90
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
107
91
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
92
|
+
const action = await prompts.selectOption('Select:', [
|
|
93
|
+
{ value: 'accounts', label: '[1] View Accounts' },
|
|
94
|
+
{ value: 'stats', label: '[2] View Stats' },
|
|
95
|
+
{ value: 'add_prop_account', label: '[+] Add Prop-Account' },
|
|
96
|
+
{ value: 'algotrading', label: '[A] Algo-Trading' },
|
|
97
|
+
{ value: 'update', label: '[U] Update HQX' },
|
|
98
|
+
{ value: 'disconnect', label: '[X] Disconnect' }
|
|
116
99
|
]);
|
|
117
100
|
|
|
118
|
-
|
|
119
|
-
const input = (choice || '').toString().toLowerCase().trim();
|
|
120
|
-
const actionMap = {
|
|
121
|
-
'1': 'accounts',
|
|
122
|
-
'2': 'stats',
|
|
123
|
-
'+': 'add_prop_account',
|
|
124
|
-
'a': 'algotrading',
|
|
125
|
-
'u': 'update',
|
|
126
|
-
'x': 'disconnect'
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
return actionMap[input] || null;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Wait for user to press Enter
|
|
134
|
-
*/
|
|
135
|
-
const waitForEnter = async () => {
|
|
136
|
-
prepareStdin();
|
|
137
|
-
try {
|
|
138
|
-
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
|
|
139
|
-
} catch (e) {
|
|
140
|
-
// Ignore prompt errors
|
|
141
|
-
}
|
|
101
|
+
return action || 'disconnect';
|
|
142
102
|
};
|
|
143
103
|
|
|
144
104
|
/**
|
|
145
|
-
*
|
|
146
|
-
* Robust version that handles all edge cases
|
|
105
|
+
* Handle update process
|
|
147
106
|
*/
|
|
148
107
|
const handleUpdate = async () => {
|
|
149
108
|
prepareStdin();
|
|
150
109
|
|
|
151
110
|
let spinner = null;
|
|
152
111
|
let currentVersion = 'unknown';
|
|
153
|
-
let latestVersion = null;
|
|
154
112
|
|
|
155
113
|
try {
|
|
156
|
-
// Get current version safely
|
|
157
114
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
} catch (e) {
|
|
161
|
-
currentVersion = 'unknown';
|
|
162
|
-
}
|
|
115
|
+
currentVersion = require('../../package.json').version || 'unknown';
|
|
116
|
+
} catch (e) {}
|
|
163
117
|
|
|
164
118
|
spinner = ora({ text: 'Checking for updates...', color: 'yellow' }).start();
|
|
165
119
|
|
|
166
|
-
|
|
167
|
-
spinner.text = 'Checking npm registry...';
|
|
120
|
+
let latestVersion;
|
|
168
121
|
try {
|
|
169
|
-
|
|
170
|
-
stdio: 'pipe',
|
|
171
|
-
|
|
172
|
-
encoding: 'utf8'
|
|
173
|
-
});
|
|
174
|
-
latestVersion = (result || '').toString().trim();
|
|
122
|
+
latestVersion = execSync('npm view hedgequantx version 2>/dev/null', {
|
|
123
|
+
stdio: 'pipe', timeout: 15000, encoding: 'utf8'
|
|
124
|
+
}).trim();
|
|
175
125
|
|
|
176
|
-
// Validate version format (x.y.z)
|
|
177
126
|
if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) {
|
|
178
|
-
throw new Error('Invalid version
|
|
127
|
+
throw new Error('Invalid version');
|
|
179
128
|
}
|
|
180
129
|
} catch (e) {
|
|
181
130
|
spinner.fail('Cannot reach npm registry');
|
|
182
|
-
|
|
183
|
-
console.log();
|
|
184
|
-
await waitForEnter();
|
|
131
|
+
await prompts.waitForEnter();
|
|
185
132
|
return;
|
|
186
133
|
}
|
|
187
134
|
|
|
188
|
-
// Compare versions
|
|
189
135
|
if (currentVersion === latestVersion) {
|
|
190
136
|
spinner.succeed(`Already up to date! (v${currentVersion})`);
|
|
191
|
-
console.log();
|
|
192
137
|
await new Promise(r => setTimeout(r, 2000));
|
|
193
138
|
return;
|
|
194
139
|
}
|
|
195
140
|
|
|
196
|
-
// Show version info and update automatically
|
|
197
141
|
spinner.text = `Updating v${currentVersion} → v${latestVersion}...`;
|
|
198
142
|
|
|
199
143
|
try {
|
|
200
144
|
execSync('npm install -g hedgequantx@latest 2>/dev/null', {
|
|
201
|
-
stdio: 'pipe',
|
|
202
|
-
timeout: 120000,
|
|
203
|
-
encoding: 'utf8'
|
|
145
|
+
stdio: 'pipe', timeout: 120000, encoding: 'utf8'
|
|
204
146
|
});
|
|
205
147
|
} catch (e) {
|
|
206
148
|
spinner.fail('Update failed');
|
|
207
|
-
console.log();
|
|
208
|
-
|
|
209
|
-
console.log(chalk.white(' npm install -g hedgequantx@latest'));
|
|
210
|
-
console.log();
|
|
211
|
-
await waitForEnter();
|
|
149
|
+
console.log(chalk.yellow(' Try: npm install -g hedgequantx@latest'));
|
|
150
|
+
await prompts.waitForEnter();
|
|
212
151
|
return;
|
|
213
152
|
}
|
|
214
153
|
|
|
215
154
|
spinner.succeed(`Updated: v${currentVersion} → v${latestVersion}`);
|
|
216
|
-
console.log();
|
|
217
|
-
console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
|
|
218
|
-
console.log();
|
|
155
|
+
console.log(chalk.cyan(' Restarting...'));
|
|
219
156
|
|
|
220
|
-
// Auto restart after 2 seconds
|
|
221
157
|
await new Promise(r => setTimeout(r, 2000));
|
|
222
158
|
|
|
223
|
-
// Restart the CLI
|
|
224
159
|
try {
|
|
225
|
-
const child = spawn('hedgequantx', [], {
|
|
226
|
-
stdio: 'inherit',
|
|
227
|
-
detached: true,
|
|
228
|
-
shell: true
|
|
229
|
-
});
|
|
160
|
+
const child = spawn('hedgequantx', [], { stdio: 'inherit', detached: true, shell: true });
|
|
230
161
|
child.unref();
|
|
231
162
|
process.exit(0);
|
|
232
163
|
} catch (e) {
|
|
233
|
-
console.log(chalk.yellow('
|
|
234
|
-
|
|
235
|
-
await waitForEnter();
|
|
164
|
+
console.log(chalk.yellow(' Please run: hedgequantx'));
|
|
165
|
+
await prompts.waitForEnter();
|
|
236
166
|
}
|
|
237
167
|
|
|
238
168
|
} catch (error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
console.log();
|
|
244
|
-
console.log(chalk.red(' An error occurred during update'));
|
|
245
|
-
if (error && error.message) {
|
|
246
|
-
console.log(chalk.gray(` ${error.message.substring(0, 100)}`));
|
|
247
|
-
}
|
|
248
|
-
console.log();
|
|
249
|
-
console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
|
|
250
|
-
console.log();
|
|
251
|
-
await waitForEnter();
|
|
169
|
+
if (spinner) spinner.fail('Update error');
|
|
170
|
+
console.log(chalk.yellow(' Try: npm install -g hedgequantx@latest'));
|
|
171
|
+
await prompts.waitForEnter();
|
|
252
172
|
}
|
|
253
173
|
};
|
|
254
174
|
|
|
255
|
-
module.exports = {
|
|
256
|
-
dashboardMenu,
|
|
257
|
-
handleUpdate
|
|
258
|
-
};
|
|
175
|
+
module.exports = { dashboardMenu, handleUpdate };
|