hedgequantx 1.7.6 → 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 +43 -118
- 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,153 +83,93 @@ const dashboardMenu = async (service) => {
|
|
|
98
83
|
console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
|
|
99
84
|
};
|
|
100
85
|
|
|
86
|
+
menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
|
|
87
|
+
menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.magenta('[A] Algo-Trading'));
|
|
88
|
+
menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
|
|
89
|
+
|
|
101
90
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
102
91
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{ name: chalk.cyan('[1] View Accounts'), value: 'accounts' },
|
|
111
|
-
{ name: chalk.cyan('[2] View Stats'), value: 'stats' },
|
|
112
|
-
{ name: chalk.cyan('[+] Add Prop-Account'), value: 'add_prop_account' },
|
|
113
|
-
{ name: chalk.magenta('[A] Algo-Trading'), value: 'algotrading' },
|
|
114
|
-
{ name: chalk.yellow('[U] Update HQX'), value: 'update' },
|
|
115
|
-
{ name: chalk.red('[X] Disconnect'), value: 'disconnect' }
|
|
116
|
-
],
|
|
117
|
-
loop: false
|
|
118
|
-
}
|
|
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' }
|
|
119
99
|
]);
|
|
120
100
|
|
|
121
|
-
return action;
|
|
101
|
+
return action || 'disconnect';
|
|
122
102
|
};
|
|
123
103
|
|
|
124
104
|
/**
|
|
125
|
-
*
|
|
126
|
-
*/
|
|
127
|
-
const waitForEnter = async () => {
|
|
128
|
-
prepareStdin();
|
|
129
|
-
try {
|
|
130
|
-
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
|
|
131
|
-
} catch (e) {
|
|
132
|
-
// Ignore prompt errors
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Handles the update process with auto-restart
|
|
138
|
-
* Robust version that handles all edge cases
|
|
105
|
+
* Handle update process
|
|
139
106
|
*/
|
|
140
107
|
const handleUpdate = async () => {
|
|
141
108
|
prepareStdin();
|
|
142
109
|
|
|
143
110
|
let spinner = null;
|
|
144
111
|
let currentVersion = 'unknown';
|
|
145
|
-
let latestVersion = null;
|
|
146
112
|
|
|
147
113
|
try {
|
|
148
|
-
// Get current version safely
|
|
149
114
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch (e) {
|
|
153
|
-
currentVersion = 'unknown';
|
|
154
|
-
}
|
|
115
|
+
currentVersion = require('../../package.json').version || 'unknown';
|
|
116
|
+
} catch (e) {}
|
|
155
117
|
|
|
156
118
|
spinner = ora({ text: 'Checking for updates...', color: 'yellow' }).start();
|
|
157
119
|
|
|
158
|
-
|
|
159
|
-
spinner.text = 'Checking npm registry...';
|
|
120
|
+
let latestVersion;
|
|
160
121
|
try {
|
|
161
|
-
|
|
162
|
-
stdio: 'pipe',
|
|
163
|
-
|
|
164
|
-
encoding: 'utf8'
|
|
165
|
-
});
|
|
166
|
-
latestVersion = (result || '').toString().trim();
|
|
122
|
+
latestVersion = execSync('npm view hedgequantx version 2>/dev/null', {
|
|
123
|
+
stdio: 'pipe', timeout: 15000, encoding: 'utf8'
|
|
124
|
+
}).trim();
|
|
167
125
|
|
|
168
|
-
// Validate version format (x.y.z)
|
|
169
126
|
if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) {
|
|
170
|
-
throw new Error('Invalid version
|
|
127
|
+
throw new Error('Invalid version');
|
|
171
128
|
}
|
|
172
129
|
} catch (e) {
|
|
173
130
|
spinner.fail('Cannot reach npm registry');
|
|
174
|
-
|
|
175
|
-
console.log();
|
|
176
|
-
await waitForEnter();
|
|
131
|
+
await prompts.waitForEnter();
|
|
177
132
|
return;
|
|
178
133
|
}
|
|
179
134
|
|
|
180
|
-
// Compare versions
|
|
181
135
|
if (currentVersion === latestVersion) {
|
|
182
136
|
spinner.succeed(`Already up to date! (v${currentVersion})`);
|
|
183
|
-
console.log();
|
|
184
137
|
await new Promise(r => setTimeout(r, 2000));
|
|
185
138
|
return;
|
|
186
139
|
}
|
|
187
140
|
|
|
188
|
-
// Show version info and update automatically
|
|
189
141
|
spinner.text = `Updating v${currentVersion} → v${latestVersion}...`;
|
|
190
142
|
|
|
191
143
|
try {
|
|
192
144
|
execSync('npm install -g hedgequantx@latest 2>/dev/null', {
|
|
193
|
-
stdio: 'pipe',
|
|
194
|
-
timeout: 120000,
|
|
195
|
-
encoding: 'utf8'
|
|
145
|
+
stdio: 'pipe', timeout: 120000, encoding: 'utf8'
|
|
196
146
|
});
|
|
197
147
|
} catch (e) {
|
|
198
148
|
spinner.fail('Update failed');
|
|
199
|
-
console.log();
|
|
200
|
-
|
|
201
|
-
console.log(chalk.white(' npm install -g hedgequantx@latest'));
|
|
202
|
-
console.log();
|
|
203
|
-
await waitForEnter();
|
|
149
|
+
console.log(chalk.yellow(' Try: npm install -g hedgequantx@latest'));
|
|
150
|
+
await prompts.waitForEnter();
|
|
204
151
|
return;
|
|
205
152
|
}
|
|
206
153
|
|
|
207
154
|
spinner.succeed(`Updated: v${currentVersion} → v${latestVersion}`);
|
|
208
|
-
console.log();
|
|
209
|
-
console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
|
|
210
|
-
console.log();
|
|
155
|
+
console.log(chalk.cyan(' Restarting...'));
|
|
211
156
|
|
|
212
|
-
// Auto restart after 2 seconds
|
|
213
157
|
await new Promise(r => setTimeout(r, 2000));
|
|
214
158
|
|
|
215
|
-
// Restart the CLI
|
|
216
159
|
try {
|
|
217
|
-
const child = spawn('hedgequantx', [], {
|
|
218
|
-
stdio: 'inherit',
|
|
219
|
-
detached: true,
|
|
220
|
-
shell: true
|
|
221
|
-
});
|
|
160
|
+
const child = spawn('hedgequantx', [], { stdio: 'inherit', detached: true, shell: true });
|
|
222
161
|
child.unref();
|
|
223
162
|
process.exit(0);
|
|
224
163
|
} catch (e) {
|
|
225
|
-
console.log(chalk.yellow('
|
|
226
|
-
|
|
227
|
-
await waitForEnter();
|
|
164
|
+
console.log(chalk.yellow(' Please run: hedgequantx'));
|
|
165
|
+
await prompts.waitForEnter();
|
|
228
166
|
}
|
|
229
167
|
|
|
230
168
|
} catch (error) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
console.log();
|
|
236
|
-
console.log(chalk.red(' An error occurred during update'));
|
|
237
|
-
if (error && error.message) {
|
|
238
|
-
console.log(chalk.gray(` ${error.message.substring(0, 100)}`));
|
|
239
|
-
}
|
|
240
|
-
console.log();
|
|
241
|
-
console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
|
|
242
|
-
console.log();
|
|
243
|
-
await waitForEnter();
|
|
169
|
+
if (spinner) spinner.fail('Update error');
|
|
170
|
+
console.log(chalk.yellow(' Try: npm install -g hedgequantx@latest'));
|
|
171
|
+
await prompts.waitForEnter();
|
|
244
172
|
}
|
|
245
173
|
};
|
|
246
174
|
|
|
247
|
-
module.exports = {
|
|
248
|
-
dashboardMenu,
|
|
249
|
-
handleUpdate
|
|
250
|
-
};
|
|
175
|
+
module.exports = { dashboardMenu, handleUpdate };
|