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.
- package/package.json +1 -1
- package/src/app.js +10 -618
- package/src/menus/connect.js +403 -0
- package/src/menus/dashboard.js +318 -0
- package/src/menus/index.js +8 -0
- package/src/pages/algo/ui.js +8 -5
- package/src/ui/index.js +23 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Menu - Main menu after login
|
|
3
|
+
* Shows connected PropFirms and navigation options
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const { execSync, spawn } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const { connections } = require('../services');
|
|
12
|
+
const { getLogoWidth, centerText, prepareStdin } = require('../ui');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dashboard menu after login
|
|
16
|
+
* @param {Object} service - Connected service
|
|
17
|
+
*/
|
|
18
|
+
const dashboardMenu = async (service) => {
|
|
19
|
+
// Ensure stdin is ready for prompts
|
|
20
|
+
prepareStdin();
|
|
21
|
+
|
|
22
|
+
const user = service.user;
|
|
23
|
+
const boxWidth = getLogoWidth();
|
|
24
|
+
const W = boxWidth - 2; // Same width as logo (inner width)
|
|
25
|
+
|
|
26
|
+
// Helper to center text
|
|
27
|
+
const centerLine = (text, width) => {
|
|
28
|
+
const pad = Math.floor((width - text.length) / 2);
|
|
29
|
+
return ' '.repeat(Math.max(0, pad)) + text + ' '.repeat(Math.max(0, width - pad - text.length));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Helper to pad text left
|
|
33
|
+
const padLine = (text, width) => {
|
|
34
|
+
return ' ' + text + ' '.repeat(Math.max(0, width - text.length - 1));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Dashboard box header
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
40
|
+
console.log(chalk.cyan('║') + chalk.yellow.bold(centerLine('Welcome, HQX Trader!', W)) + chalk.cyan('║'));
|
|
41
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
42
|
+
|
|
43
|
+
// Connection info - show all active connections in boxes (max 3 per row)
|
|
44
|
+
const allConns = connections.getAll();
|
|
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
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
96
|
+
|
|
97
|
+
// Menu options in 2 columns
|
|
98
|
+
const col1Width = Math.floor(W / 2);
|
|
99
|
+
const col2Width = W - col1Width;
|
|
100
|
+
|
|
101
|
+
const menuRow = (left, right) => {
|
|
102
|
+
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('║'));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
|
|
110
|
+
menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
|
|
111
|
+
menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
|
|
112
|
+
|
|
113
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
114
|
+
console.log();
|
|
115
|
+
|
|
116
|
+
const { action } = await inquirer.prompt([
|
|
117
|
+
{
|
|
118
|
+
type: 'input',
|
|
119
|
+
name: 'action',
|
|
120
|
+
message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
|
|
121
|
+
validate: (input) => {
|
|
122
|
+
const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
|
|
123
|
+
if (valid.includes(input)) return true;
|
|
124
|
+
return 'Please enter a valid option';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// Map input to action
|
|
130
|
+
const actionMap = {
|
|
131
|
+
'1': 'accounts',
|
|
132
|
+
'2': 'stats',
|
|
133
|
+
'+': 'add_prop_account',
|
|
134
|
+
'a': 'algotrading',
|
|
135
|
+
'A': 'algotrading',
|
|
136
|
+
'u': 'update',
|
|
137
|
+
'U': 'update',
|
|
138
|
+
'x': 'disconnect',
|
|
139
|
+
'X': 'disconnect'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return actionMap[action] || 'accounts';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Wait for user to press Enter
|
|
147
|
+
*/
|
|
148
|
+
const waitForEnter = async () => {
|
|
149
|
+
prepareStdin();
|
|
150
|
+
try {
|
|
151
|
+
await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// Ignore prompt errors
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handles the update process with auto-restart
|
|
159
|
+
* Robust version that handles all edge cases
|
|
160
|
+
*/
|
|
161
|
+
const handleUpdate = async () => {
|
|
162
|
+
prepareStdin();
|
|
163
|
+
|
|
164
|
+
let spinner = null;
|
|
165
|
+
let currentVersion = 'unknown';
|
|
166
|
+
let latestVersion = null;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// Get current version safely
|
|
170
|
+
try {
|
|
171
|
+
const pkg = require('../../package.json');
|
|
172
|
+
currentVersion = pkg.version || 'unknown';
|
|
173
|
+
} catch (e) {
|
|
174
|
+
currentVersion = 'unknown';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
spinner = ora('Checking for updates...').start();
|
|
178
|
+
|
|
179
|
+
// Check latest version on npm with timeout
|
|
180
|
+
spinner.text = 'Checking npm registry...';
|
|
181
|
+
try {
|
|
182
|
+
const result = execSync('npm view hedgequantx version 2>/dev/null', {
|
|
183
|
+
stdio: 'pipe',
|
|
184
|
+
timeout: 15000, // 15 second timeout
|
|
185
|
+
encoding: 'utf8'
|
|
186
|
+
});
|
|
187
|
+
latestVersion = (result || '').toString().trim();
|
|
188
|
+
|
|
189
|
+
// Validate version format (x.y.z)
|
|
190
|
+
if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) {
|
|
191
|
+
throw new Error('Invalid version format received');
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
spinner.fail('Cannot reach npm registry');
|
|
195
|
+
console.log(chalk.gray(' Check your internet connection'));
|
|
196
|
+
console.log();
|
|
197
|
+
await waitForEnter();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Compare versions
|
|
202
|
+
if (currentVersion === latestVersion) {
|
|
203
|
+
spinner.succeed('Already up to date!');
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.green(` You have the latest version: v${currentVersion}`));
|
|
206
|
+
console.log();
|
|
207
|
+
await waitForEnter();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Ask user before updating
|
|
212
|
+
spinner.stop();
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(chalk.cyan(` Current version: v${currentVersion}`));
|
|
215
|
+
console.log(chalk.green(` Latest version: v${latestVersion}`));
|
|
216
|
+
console.log();
|
|
217
|
+
|
|
218
|
+
prepareStdin();
|
|
219
|
+
const { confirm } = await inquirer.prompt([{
|
|
220
|
+
type: 'confirm',
|
|
221
|
+
name: 'confirm',
|
|
222
|
+
message: 'Do you want to update now?',
|
|
223
|
+
default: true
|
|
224
|
+
}]);
|
|
225
|
+
|
|
226
|
+
if (!confirm) {
|
|
227
|
+
console.log(chalk.gray(' Update cancelled'));
|
|
228
|
+
console.log();
|
|
229
|
+
await waitForEnter();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Update via npm
|
|
234
|
+
spinner = ora(`Updating v${currentVersion} -> v${latestVersion}...`).start();
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
execSync('npm install -g hedgequantx@latest 2>/dev/null', {
|
|
238
|
+
stdio: 'pipe',
|
|
239
|
+
timeout: 120000, // 2 minute timeout for install
|
|
240
|
+
encoding: 'utf8'
|
|
241
|
+
});
|
|
242
|
+
} catch (e) {
|
|
243
|
+
spinner.fail('Update failed');
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(chalk.yellow(' Try manually:'));
|
|
246
|
+
console.log(chalk.white(' npm install -g hedgequantx@latest'));
|
|
247
|
+
console.log();
|
|
248
|
+
if (e.message) {
|
|
249
|
+
console.log(chalk.gray(` Error: ${e.message.substring(0, 100)}`));
|
|
250
|
+
console.log();
|
|
251
|
+
}
|
|
252
|
+
await waitForEnter();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
spinner.succeed('Update complete!');
|
|
257
|
+
console.log();
|
|
258
|
+
console.log(chalk.green(` Updated: v${currentVersion} -> v${latestVersion}`));
|
|
259
|
+
console.log();
|
|
260
|
+
|
|
261
|
+
// Ask if user wants to restart
|
|
262
|
+
prepareStdin();
|
|
263
|
+
const { restart } = await inquirer.prompt([{
|
|
264
|
+
type: 'confirm',
|
|
265
|
+
name: 'restart',
|
|
266
|
+
message: 'Restart HQX now?',
|
|
267
|
+
default: true
|
|
268
|
+
}]);
|
|
269
|
+
|
|
270
|
+
if (restart) {
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(chalk.cyan(' Restarting HedgeQuantX CLI...'));
|
|
273
|
+
console.log();
|
|
274
|
+
|
|
275
|
+
// Small delay so user can see the message
|
|
276
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
277
|
+
|
|
278
|
+
// Restart the CLI
|
|
279
|
+
try {
|
|
280
|
+
const child = spawn('hedgequantx', [], {
|
|
281
|
+
stdio: 'inherit',
|
|
282
|
+
detached: true,
|
|
283
|
+
shell: true
|
|
284
|
+
});
|
|
285
|
+
child.unref();
|
|
286
|
+
process.exit(0);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.log(chalk.yellow(' Could not auto-restart. Please run: hedgequantx'));
|
|
289
|
+
console.log();
|
|
290
|
+
await waitForEnter();
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
console.log(chalk.gray(' Run "hedgequantx" to use the new version'));
|
|
294
|
+
console.log();
|
|
295
|
+
await waitForEnter();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// Catch-all for any unexpected errors
|
|
300
|
+
if (spinner) {
|
|
301
|
+
try { spinner.fail('Update error'); } catch (e) {}
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
console.log(chalk.red(' An error occurred during update'));
|
|
305
|
+
if (error && error.message) {
|
|
306
|
+
console.log(chalk.gray(` ${error.message.substring(0, 100)}`));
|
|
307
|
+
}
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(chalk.yellow(' Try manually: npm install -g hedgequantx@latest'));
|
|
310
|
+
console.log();
|
|
311
|
+
await waitForEnter();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
dashboardMenu,
|
|
317
|
+
handleUpdate
|
|
318
|
+
};
|
package/src/pages/algo/ui.js
CHANGED
|
@@ -193,8 +193,9 @@ class AlgoUI {
|
|
|
193
193
|
this._line(chalk.cyan(BOX.V) + chalk.white(left) + ' '.repeat(midPad) + chalk.cyan(mid) + ' '.repeat(space - midPad - mid.length) + chalk.yellow(right) + chalk.cyan(BOX.V));
|
|
194
194
|
this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
|
|
195
195
|
|
|
196
|
-
// Logs (newest
|
|
197
|
-
|
|
196
|
+
// Logs (oldest at top, newest at bottom - like a terminal)
|
|
197
|
+
// Take the last maxLogs entries (most recent), keep chronological order
|
|
198
|
+
const visible = logs.slice(-maxLogs);
|
|
198
199
|
|
|
199
200
|
if (visible.length === 0) {
|
|
200
201
|
this._line(chalk.cyan(BOX.V) + chalk.gray(fitToWidth(' Waiting for activity...', W)) + chalk.cyan(BOX.V));
|
|
@@ -202,15 +203,17 @@ class AlgoUI {
|
|
|
202
203
|
this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
|
|
203
204
|
}
|
|
204
205
|
} else {
|
|
206
|
+
// First draw empty lines for padding (so logs stick to bottom)
|
|
207
|
+
for (let i = visible.length; i < maxLogs; i++) {
|
|
208
|
+
this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
|
|
209
|
+
}
|
|
210
|
+
// Then draw logs (oldest first, newest last/at bottom)
|
|
205
211
|
visible.forEach(log => {
|
|
206
212
|
const color = LOG_COLORS[log.type] || chalk.white;
|
|
207
213
|
const icon = LOG_ICONS[log.type] || LOG_ICONS.info;
|
|
208
214
|
const line = ` [${log.timestamp}] ${icon} ${log.message}`;
|
|
209
215
|
this._line(chalk.cyan(BOX.V) + color(fitToWidth(line, W)) + chalk.cyan(BOX.V));
|
|
210
216
|
});
|
|
211
|
-
for (let i = visible.length; i < maxLogs; i++) {
|
|
212
|
-
this._line(chalk.cyan(BOX.V) + ' '.repeat(W) + chalk.cyan(BOX.V));
|
|
213
|
-
}
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
// Bottom border
|
package/src/ui/index.js
CHANGED
|
@@ -24,6 +24,26 @@ const {
|
|
|
24
24
|
} = require('./table');
|
|
25
25
|
const { createBoxMenu } = require('./menu');
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Ensure stdin is ready for inquirer prompts
|
|
29
|
+
* This fixes input leaking to bash after session restore or algo trading
|
|
30
|
+
*/
|
|
31
|
+
const prepareStdin = () => {
|
|
32
|
+
try {
|
|
33
|
+
// Remove any raw mode that might be left from previous operations
|
|
34
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
35
|
+
process.stdin.setRawMode(false);
|
|
36
|
+
}
|
|
37
|
+
// Remove any lingering keypress listeners
|
|
38
|
+
process.stdin.removeAllListeners('keypress');
|
|
39
|
+
process.stdin.removeAllListeners('data');
|
|
40
|
+
// Pause stdin so inquirer can take control
|
|
41
|
+
process.stdin.pause();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Ignore errors
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
27
47
|
module.exports = {
|
|
28
48
|
// Device
|
|
29
49
|
detectDevice,
|
|
@@ -47,5 +67,7 @@ module.exports = {
|
|
|
47
67
|
draw2ColSeparator,
|
|
48
68
|
fmtRow,
|
|
49
69
|
// Menu
|
|
50
|
-
createBoxMenu
|
|
70
|
+
createBoxMenu,
|
|
71
|
+
// Stdin
|
|
72
|
+
prepareStdin
|
|
51
73
|
};
|