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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedgequantx",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.7",
|
|
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": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"src/"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@clack/prompts": "^0.11.0",
|
|
44
45
|
"asciichart": "^1.5.25",
|
|
45
46
|
"chalk": "^4.1.2",
|
|
46
47
|
"commander": "^11.1.0",
|
|
47
48
|
"figlet": "^1.7.0",
|
|
48
|
-
"inquirer": "^7.3.3",
|
|
49
49
|
"ora": "^5.4.1",
|
|
50
50
|
"protobufjs": "^8.0.0",
|
|
51
51
|
"ws": "^8.18.3"
|
package/src/app.js
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const chalk = require('chalk');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
7
|
const ora = require('ora');
|
|
9
8
|
|
|
10
9
|
const { connections } = require('./services');
|
|
11
10
|
const { getLogoWidth, centerText, prepareStdin } = require('./ui');
|
|
12
|
-
const { logger } = require('./utils');
|
|
11
|
+
const { logger, prompts } = require('./utils');
|
|
13
12
|
const { setCachedStats, clearCachedStats } = require('./services/stats-cache');
|
|
14
13
|
|
|
15
14
|
const log = logger.scope('App');
|
|
@@ -24,29 +23,21 @@ const { projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu, dashboardM
|
|
|
24
23
|
|
|
25
24
|
// Current service reference
|
|
26
25
|
let currentService = null;
|
|
27
|
-
let currentPlatform = null; // 'projectx' or 'rithmic'
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
* Global terminal restoration
|
|
28
|
+
* Global terminal restoration
|
|
31
29
|
*/
|
|
32
30
|
const restoreTerminal = () => {
|
|
33
31
|
try {
|
|
34
|
-
// Exit alternate screen buffer
|
|
35
32
|
process.stdout.write('\x1B[?1049l');
|
|
36
|
-
// Show cursor
|
|
37
33
|
process.stdout.write('\x1B[?25h');
|
|
38
|
-
// Disable raw mode
|
|
39
34
|
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
40
35
|
process.stdin.setRawMode(false);
|
|
41
36
|
}
|
|
42
|
-
// Remove all keypress listeners
|
|
43
37
|
process.stdin.removeAllListeners('keypress');
|
|
44
|
-
} catch (e) {
|
|
45
|
-
// Ignore errors during cleanup
|
|
46
|
-
}
|
|
38
|
+
} catch (e) {}
|
|
47
39
|
};
|
|
48
40
|
|
|
49
|
-
// Register global handlers to restore terminal on exit/crash
|
|
50
41
|
process.on('exit', restoreTerminal);
|
|
51
42
|
process.on('SIGINT', () => { restoreTerminal(); process.exit(0); });
|
|
52
43
|
process.on('SIGTERM', () => { restoreTerminal(); process.exit(0); });
|
|
@@ -62,7 +53,7 @@ process.on('unhandledRejection', (reason) => {
|
|
|
62
53
|
});
|
|
63
54
|
|
|
64
55
|
/**
|
|
65
|
-
* Refresh cached stats
|
|
56
|
+
* Refresh cached stats
|
|
66
57
|
*/
|
|
67
58
|
const refreshStats = async () => {
|
|
68
59
|
if (connections.count() > 0) {
|
|
@@ -70,20 +61,14 @@ const refreshStats = async () => {
|
|
|
70
61
|
const allAccounts = await connections.getAllAccounts();
|
|
71
62
|
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
72
63
|
|
|
73
|
-
|
|
74
|
-
let
|
|
75
|
-
let totalPnl = null;
|
|
76
|
-
let hasBalanceData = false;
|
|
77
|
-
let hasPnlData = false;
|
|
64
|
+
let totalBalance = null, totalPnl = null;
|
|
65
|
+
let hasBalanceData = false, hasPnlData = false;
|
|
78
66
|
|
|
79
67
|
activeAccounts.forEach(account => {
|
|
80
|
-
// Balance: only sum if API returned a value
|
|
81
68
|
if (account.balance !== null && account.balance !== undefined) {
|
|
82
69
|
totalBalance = (totalBalance || 0) + account.balance;
|
|
83
70
|
hasBalanceData = true;
|
|
84
71
|
}
|
|
85
|
-
|
|
86
|
-
// P&L: only sum if API returned a value
|
|
87
72
|
if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
|
|
88
73
|
totalPnl = (totalPnl || 0) + account.profitAndLoss;
|
|
89
74
|
hasPnlData = true;
|
|
@@ -97,59 +82,36 @@ const refreshStats = async () => {
|
|
|
97
82
|
pnl: hasPnlData ? totalPnl : null,
|
|
98
83
|
pnlPercent: null
|
|
99
84
|
});
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// Ignore errors
|
|
102
|
-
}
|
|
85
|
+
} catch (e) {}
|
|
103
86
|
} else {
|
|
104
87
|
clearCachedStats();
|
|
105
88
|
}
|
|
106
89
|
};
|
|
107
90
|
|
|
108
91
|
/**
|
|
109
|
-
*
|
|
92
|
+
* Display banner
|
|
110
93
|
*/
|
|
111
94
|
const banner = async () => {
|
|
112
95
|
console.clear();
|
|
113
96
|
const termWidth = process.stdout.columns || 100;
|
|
114
97
|
const isMobile = termWidth < 60;
|
|
115
|
-
// Logo HEDGEQUANTX + X = 94 chars, need 98 for box (94 + 2 borders + 2 padding)
|
|
116
98
|
const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
|
|
117
99
|
const innerWidth = boxWidth - 2;
|
|
118
100
|
const version = require('../package.json').version;
|
|
119
101
|
|
|
120
|
-
// Draw logo - compact for mobile, full for desktop
|
|
121
|
-
|
|
122
102
|
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
123
103
|
|
|
124
104
|
if (isMobile) {
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
'██╗ ██╗ ██████╗ ',
|
|
128
|
-
'██║ ██║██╔═══██╗',
|
|
129
|
-
'███████║██║ ██║',
|
|
130
|
-
'██╔══██║██║▄▄ ██║',
|
|
131
|
-
'██║ ██║╚██████╔╝',
|
|
132
|
-
'╚═╝ ╚═╝ ╚══▀▀═╝ '
|
|
133
|
-
];
|
|
134
|
-
const logoX = [
|
|
135
|
-
'██╗ ██╗',
|
|
136
|
-
'╚██╗██╔╝',
|
|
137
|
-
' ╚███╔╝ ',
|
|
138
|
-
' ██╔██╗ ',
|
|
139
|
-
'██╔╝ ██╗',
|
|
140
|
-
'╚═╝ ╚═╝'
|
|
141
|
-
];
|
|
142
|
-
|
|
105
|
+
const logoHQ = ['██╗ ██╗ ██████╗ ','██║ ██║██╔═══██╗','███████║██║ ██║','██╔══██║██║▄▄ ██║','██║ ██║╚██████╔╝','╚═╝ ╚═╝ ╚══▀▀═╝ '];
|
|
106
|
+
const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
|
|
143
107
|
logoHQ.forEach((line, i) => {
|
|
144
108
|
const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
|
|
145
109
|
const totalLen = line.length + logoX[i].length;
|
|
146
110
|
const padding = innerWidth - totalLen;
|
|
147
111
|
const leftPad = Math.floor(padding / 2);
|
|
148
|
-
|
|
149
|
-
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
112
|
+
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
|
|
150
113
|
});
|
|
151
114
|
} else {
|
|
152
|
-
// Full HEDGEQUANTX logo for desktop
|
|
153
115
|
const logo = [
|
|
154
116
|
'██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗',
|
|
155
117
|
'██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝',
|
|
@@ -158,80 +120,42 @@ const banner = async () => {
|
|
|
158
120
|
'██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ',
|
|
159
121
|
'╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ '
|
|
160
122
|
];
|
|
161
|
-
const logoX = [
|
|
162
|
-
'██╗ ██╗',
|
|
163
|
-
'╚██╗██╔╝',
|
|
164
|
-
' ╚███╔╝ ',
|
|
165
|
-
' ██╔██╗ ',
|
|
166
|
-
'██╔╝ ██╗',
|
|
167
|
-
'╚═╝ ╚═╝'
|
|
168
|
-
];
|
|
169
|
-
|
|
123
|
+
const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
|
|
170
124
|
logo.forEach((line, i) => {
|
|
171
|
-
const
|
|
172
|
-
const xPart = chalk.yellow(logoX[i]);
|
|
173
|
-
const fullLine = mainPart + xPart;
|
|
125
|
+
const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
|
|
174
126
|
const totalLen = line.length + logoX[i].length;
|
|
175
127
|
const padding = innerWidth - totalLen;
|
|
176
128
|
const leftPad = Math.floor(padding / 2);
|
|
177
|
-
|
|
178
|
-
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
129
|
+
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
|
|
179
130
|
});
|
|
180
131
|
}
|
|
181
132
|
|
|
182
|
-
// Tagline
|
|
183
133
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
184
134
|
const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
|
|
185
135
|
console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
|
|
186
|
-
|
|
187
136
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
188
137
|
};
|
|
189
138
|
|
|
190
139
|
/**
|
|
191
|
-
* Main
|
|
140
|
+
* Main menu
|
|
192
141
|
*/
|
|
193
142
|
const mainMenu = async () => {
|
|
194
143
|
const boxWidth = getLogoWidth();
|
|
195
144
|
const innerWidth = boxWidth - 2;
|
|
196
|
-
const col1Width = Math.floor(innerWidth / 2);
|
|
197
|
-
const col2Width = innerWidth - col1Width;
|
|
198
145
|
|
|
199
|
-
// Connection menu box
|
|
200
146
|
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
201
147
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
|
|
202
148
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
203
|
-
|
|
204
|
-
// Menu row helper (2 columns)
|
|
205
|
-
const menuRow = (left, right) => {
|
|
206
|
-
const leftText = ' ' + left;
|
|
207
|
-
const rightText = right ? ' ' + right : '';
|
|
208
|
-
const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
209
|
-
const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
210
|
-
const leftPad = col1Width - leftLen;
|
|
211
|
-
const rightPad = col2Width - rightLen;
|
|
212
|
-
console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
|
|
213
|
-
};
|
|
214
|
-
|
|
215
149
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
216
|
-
console.log();
|
|
217
150
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
message: chalk.cyan('Select platform:'),
|
|
224
|
-
choices: [
|
|
225
|
-
{ name: chalk.cyan('[1] ProjectX'), value: 'projectx' },
|
|
226
|
-
{ name: chalk.cyan('[2] Rithmic'), value: 'rithmic' },
|
|
227
|
-
{ name: chalk.cyan('[3] Tradovate'), value: 'tradovate' },
|
|
228
|
-
{ name: chalk.red('[X] Exit'), value: 'exit' }
|
|
229
|
-
],
|
|
230
|
-
loop: false
|
|
231
|
-
}
|
|
151
|
+
const action = await prompts.selectOption('Select platform:', [
|
|
152
|
+
{ value: 'projectx', label: '[1] ProjectX' },
|
|
153
|
+
{ value: 'rithmic', label: '[2] Rithmic' },
|
|
154
|
+
{ value: 'tradovate', label: '[3] Tradovate' },
|
|
155
|
+
{ value: 'exit', label: '[X] Exit' }
|
|
232
156
|
]);
|
|
233
157
|
|
|
234
|
-
return action;
|
|
158
|
+
return action || 'exit';
|
|
235
159
|
};
|
|
236
160
|
|
|
237
161
|
/**
|
|
@@ -242,121 +166,76 @@ const run = async () => {
|
|
|
242
166
|
log.info('Starting HQX CLI');
|
|
243
167
|
await banner();
|
|
244
168
|
|
|
245
|
-
// Try to restore session
|
|
246
|
-
log.debug('Attempting to restore session');
|
|
247
169
|
const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
|
|
248
170
|
const restored = await connections.restoreFromStorage();
|
|
249
171
|
|
|
250
172
|
if (restored) {
|
|
251
173
|
spinner.succeed('Session restored');
|
|
252
174
|
currentService = connections.getAll()[0].service;
|
|
253
|
-
log.info('Session restored', { connections: connections.count() });
|
|
254
|
-
// Refresh stats after session restore
|
|
255
175
|
await refreshStats();
|
|
256
176
|
} else {
|
|
257
177
|
spinner.info('No active session');
|
|
258
|
-
log.debug('No session to restore');
|
|
259
178
|
}
|
|
260
179
|
|
|
261
|
-
// Main loop
|
|
262
180
|
while (true) {
|
|
263
181
|
try {
|
|
264
|
-
// Ensure stdin is ready for prompts (fixes input leaking to bash)
|
|
265
182
|
prepareStdin();
|
|
266
|
-
|
|
267
|
-
// Display banner (uses cached stats, no refetch)
|
|
268
183
|
await banner();
|
|
269
184
|
|
|
270
185
|
if (!connections.isConnected()) {
|
|
271
186
|
const choice = await mainMenu();
|
|
272
|
-
log.debug('Main menu choice', { choice });
|
|
273
187
|
|
|
274
188
|
if (choice === 'exit') {
|
|
275
|
-
log.info('User exit');
|
|
276
189
|
console.log(chalk.gray('Goodbye!'));
|
|
277
190
|
process.exit(0);
|
|
278
191
|
}
|
|
279
192
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
await refreshStats(); // Refresh after new connection
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (choice === 'rithmic') {
|
|
289
|
-
const service = await rithmicMenu();
|
|
290
|
-
if (service) {
|
|
291
|
-
currentService = service;
|
|
292
|
-
await refreshStats(); // Refresh after new connection
|
|
293
|
-
}
|
|
294
|
-
}
|
|
193
|
+
let service = null;
|
|
194
|
+
if (choice === 'projectx') service = await projectXMenu();
|
|
195
|
+
else if (choice === 'rithmic') service = await rithmicMenu();
|
|
196
|
+
else if (choice === 'tradovate') service = await tradovateMenu();
|
|
295
197
|
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
currentService = service;
|
|
300
|
-
await refreshStats(); // Refresh after new connection
|
|
301
|
-
}
|
|
198
|
+
if (service) {
|
|
199
|
+
currentService = service;
|
|
200
|
+
await refreshStats();
|
|
302
201
|
}
|
|
303
202
|
} else {
|
|
304
203
|
const action = await dashboardMenu(currentService);
|
|
305
|
-
log.debug('Dashboard action', { action });
|
|
306
204
|
|
|
307
205
|
switch (action) {
|
|
308
206
|
case 'accounts':
|
|
309
207
|
await showAccounts(currentService);
|
|
310
208
|
break;
|
|
311
|
-
|
|
312
209
|
case 'stats':
|
|
313
210
|
await showStats(currentService);
|
|
314
211
|
break;
|
|
315
212
|
case 'add_prop_account':
|
|
316
|
-
// Show platform selection menu
|
|
317
213
|
const platformChoice = await addPropAccountMenu();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const newService = await rithmicMenu();
|
|
326
|
-
if (newService) {
|
|
327
|
-
currentService = newService;
|
|
328
|
-
await refreshStats(); // Refresh after adding account
|
|
329
|
-
}
|
|
330
|
-
} else if (platformChoice === 'tradovate') {
|
|
331
|
-
const newService = await tradovateMenu();
|
|
332
|
-
if (newService) {
|
|
333
|
-
currentService = newService;
|
|
334
|
-
await refreshStats(); // Refresh after adding account
|
|
335
|
-
}
|
|
214
|
+
let newService = null;
|
|
215
|
+
if (platformChoice === 'projectx') newService = await projectXMenu();
|
|
216
|
+
else if (platformChoice === 'rithmic') newService = await rithmicMenu();
|
|
217
|
+
else if (platformChoice === 'tradovate') newService = await tradovateMenu();
|
|
218
|
+
if (newService) {
|
|
219
|
+
currentService = newService;
|
|
220
|
+
await refreshStats();
|
|
336
221
|
}
|
|
337
222
|
break;
|
|
338
223
|
case 'algotrading':
|
|
339
224
|
await algoTradingMenu(currentService);
|
|
340
225
|
break;
|
|
341
226
|
case 'update':
|
|
342
|
-
|
|
343
|
-
if (updateResult === 'restart') return; // Stop loop, new process spawned
|
|
227
|
+
await handleUpdate();
|
|
344
228
|
break;
|
|
345
229
|
case 'disconnect':
|
|
346
|
-
const connCount = connections.count();
|
|
347
230
|
connections.disconnectAll();
|
|
348
231
|
currentService = null;
|
|
349
232
|
clearCachedStats();
|
|
350
|
-
console.log(chalk.yellow(
|
|
233
|
+
console.log(chalk.yellow('Disconnected'));
|
|
351
234
|
break;
|
|
352
|
-
case 'exit':
|
|
353
|
-
console.log(chalk.gray('Goodbye!'));
|
|
354
|
-
process.exit(0);
|
|
355
235
|
}
|
|
356
236
|
}
|
|
357
237
|
} catch (loopError) {
|
|
358
|
-
console.error(chalk.red('Error
|
|
359
|
-
// Continue the loop
|
|
238
|
+
console.error(chalk.red('Error:'), loopError.message);
|
|
360
239
|
}
|
|
361
240
|
}
|
|
362
241
|
} catch (error) {
|