hedgequantx 1.7.6 → 1.7.8
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 -2
- package/src/app.js +54 -159
- package/src/menus/connect.js +54 -166
- package/src/menus/dashboard.js +49 -120
- 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 +195 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedgequantx",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
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": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"chalk": "^4.1.2",
|
|
46
46
|
"commander": "^11.1.0",
|
|
47
47
|
"figlet": "^1.7.0",
|
|
48
|
-
"inquirer": "^7.3.3",
|
|
49
48
|
"ora": "^5.4.1",
|
|
50
49
|
"protobufjs": "^8.0.0",
|
|
51
50
|
"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,58 @@ 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
|
-
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
136
|
+
// No closing line - dashboard will continue the box
|
|
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
145
|
const col1Width = Math.floor(innerWidth / 2);
|
|
197
|
-
const col2Width = innerWidth - col1Width;
|
|
198
146
|
|
|
199
|
-
|
|
200
|
-
|
|
147
|
+
const menuRow = (left, right) => {
|
|
148
|
+
const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
|
|
149
|
+
const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
|
|
150
|
+
const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
|
|
151
|
+
const rightPadded = (right || '') + ' '.repeat(Math.max(0, innerWidth - col1Width - rightPlain.length));
|
|
152
|
+
console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Continue from banner
|
|
156
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
201
157
|
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
|
|
202
158
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
203
159
|
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
};
|
|
160
|
+
menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
|
|
161
|
+
menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Exit'));
|
|
214
162
|
|
|
215
163
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
216
|
-
console.log();
|
|
217
164
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
}
|
|
232
|
-
]);
|
|
165
|
+
const input = await prompts.textInput('Select (1/2/3/X)');
|
|
166
|
+
|
|
167
|
+
const actionMap = {
|
|
168
|
+
'1': 'projectx',
|
|
169
|
+
'2': 'rithmic',
|
|
170
|
+
'3': 'tradovate',
|
|
171
|
+
'x': 'exit'
|
|
172
|
+
};
|
|
233
173
|
|
|
234
|
-
return
|
|
174
|
+
return actionMap[(input || '').toLowerCase()] || 'exit';
|
|
235
175
|
};
|
|
236
176
|
|
|
237
177
|
/**
|
|
@@ -242,121 +182,76 @@ const run = async () => {
|
|
|
242
182
|
log.info('Starting HQX CLI');
|
|
243
183
|
await banner();
|
|
244
184
|
|
|
245
|
-
// Try to restore session
|
|
246
|
-
log.debug('Attempting to restore session');
|
|
247
185
|
const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
|
|
248
186
|
const restored = await connections.restoreFromStorage();
|
|
249
187
|
|
|
250
188
|
if (restored) {
|
|
251
189
|
spinner.succeed('Session restored');
|
|
252
190
|
currentService = connections.getAll()[0].service;
|
|
253
|
-
log.info('Session restored', { connections: connections.count() });
|
|
254
|
-
// Refresh stats after session restore
|
|
255
191
|
await refreshStats();
|
|
256
192
|
} else {
|
|
257
193
|
spinner.info('No active session');
|
|
258
|
-
log.debug('No session to restore');
|
|
259
194
|
}
|
|
260
195
|
|
|
261
|
-
// Main loop
|
|
262
196
|
while (true) {
|
|
263
197
|
try {
|
|
264
|
-
// Ensure stdin is ready for prompts (fixes input leaking to bash)
|
|
265
198
|
prepareStdin();
|
|
266
|
-
|
|
267
|
-
// Display banner (uses cached stats, no refetch)
|
|
268
199
|
await banner();
|
|
269
200
|
|
|
270
201
|
if (!connections.isConnected()) {
|
|
271
202
|
const choice = await mainMenu();
|
|
272
|
-
log.debug('Main menu choice', { choice });
|
|
273
203
|
|
|
274
204
|
if (choice === 'exit') {
|
|
275
|
-
log.info('User exit');
|
|
276
205
|
console.log(chalk.gray('Goodbye!'));
|
|
277
206
|
process.exit(0);
|
|
278
207
|
}
|
|
279
208
|
|
|
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
|
-
}
|
|
209
|
+
let service = null;
|
|
210
|
+
if (choice === 'projectx') service = await projectXMenu();
|
|
211
|
+
else if (choice === 'rithmic') service = await rithmicMenu();
|
|
212
|
+
else if (choice === 'tradovate') service = await tradovateMenu();
|
|
295
213
|
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
currentService = service;
|
|
300
|
-
await refreshStats(); // Refresh after new connection
|
|
301
|
-
}
|
|
214
|
+
if (service) {
|
|
215
|
+
currentService = service;
|
|
216
|
+
await refreshStats();
|
|
302
217
|
}
|
|
303
218
|
} else {
|
|
304
219
|
const action = await dashboardMenu(currentService);
|
|
305
|
-
log.debug('Dashboard action', { action });
|
|
306
220
|
|
|
307
221
|
switch (action) {
|
|
308
222
|
case 'accounts':
|
|
309
223
|
await showAccounts(currentService);
|
|
310
224
|
break;
|
|
311
|
-
|
|
312
225
|
case 'stats':
|
|
313
226
|
await showStats(currentService);
|
|
314
227
|
break;
|
|
315
228
|
case 'add_prop_account':
|
|
316
|
-
// Show platform selection menu
|
|
317
229
|
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
|
-
}
|
|
230
|
+
let newService = null;
|
|
231
|
+
if (platformChoice === 'projectx') newService = await projectXMenu();
|
|
232
|
+
else if (platformChoice === 'rithmic') newService = await rithmicMenu();
|
|
233
|
+
else if (platformChoice === 'tradovate') newService = await tradovateMenu();
|
|
234
|
+
if (newService) {
|
|
235
|
+
currentService = newService;
|
|
236
|
+
await refreshStats();
|
|
336
237
|
}
|
|
337
238
|
break;
|
|
338
239
|
case 'algotrading':
|
|
339
240
|
await algoTradingMenu(currentService);
|
|
340
241
|
break;
|
|
341
242
|
case 'update':
|
|
342
|
-
|
|
343
|
-
if (updateResult === 'restart') return; // Stop loop, new process spawned
|
|
243
|
+
await handleUpdate();
|
|
344
244
|
break;
|
|
345
245
|
case 'disconnect':
|
|
346
|
-
const connCount = connections.count();
|
|
347
246
|
connections.disconnectAll();
|
|
348
247
|
currentService = null;
|
|
349
248
|
clearCachedStats();
|
|
350
|
-
console.log(chalk.yellow(
|
|
249
|
+
console.log(chalk.yellow('Disconnected'));
|
|
351
250
|
break;
|
|
352
|
-
case 'exit':
|
|
353
|
-
console.log(chalk.gray('Goodbye!'));
|
|
354
|
-
process.exit(0);
|
|
355
251
|
}
|
|
356
252
|
}
|
|
357
253
|
} catch (loopError) {
|
|
358
|
-
console.error(chalk.red('Error
|
|
359
|
-
// Continue the loop
|
|
254
|
+
console.error(chalk.red('Error:'), loopError.message);
|
|
360
255
|
}
|
|
361
256
|
}
|
|
362
257
|
} catch (error) {
|