hedgequantx 1.1.1 → 1.2.32
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/README.md +128 -136
- package/bin/cli.js +28 -2076
- package/package.json +3 -3
- package/src/app.js +550 -0
- package/src/config/index.js +16 -2
- package/src/config/propfirms.js +324 -12
- package/src/pages/accounts.js +115 -0
- package/src/pages/algo.js +538 -0
- package/src/pages/index.js +13 -2
- package/src/pages/orders.js +114 -0
- package/src/pages/positions.js +115 -0
- package/src/pages/stats.js +212 -3
- package/src/pages/user.js +92 -0
- package/src/security/encryption.js +168 -0
- package/src/security/index.js +61 -0
- package/src/security/rateLimit.js +155 -0
- package/src/security/validation.js +253 -0
- package/src/services/hqx-server.js +34 -17
- package/src/services/index.js +2 -1
- package/src/services/projectx.js +383 -35
- package/src/services/session.js +150 -38
- package/src/ui/index.js +4 -1
- package/src/ui/menu.js +154 -0
- package/src/services/local-storage.js +0 -309
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedgequantx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.32",
|
|
4
4
|
"description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
|
|
5
|
-
"main": "src/
|
|
5
|
+
"main": "src/app.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hedgequantx": "./bin/cli.js",
|
|
8
8
|
"hqx": "./bin/cli.js"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"chalk": "^4.1.2",
|
|
46
46
|
"commander": "^11.1.0",
|
|
47
47
|
"figlet": "^1.7.0",
|
|
48
|
-
"inquirer": "^
|
|
48
|
+
"inquirer": "^7.3.3",
|
|
49
49
|
"ora": "^5.4.1",
|
|
50
50
|
"ws": "^8.18.3"
|
|
51
51
|
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main application router
|
|
3
|
+
* @module app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const figlet = require('figlet');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { ProjectXService, connections } = require('./services');
|
|
14
|
+
const { PROPFIRM_CHOICES, getPropFirmsByPlatform } = require('./config');
|
|
15
|
+
const { getDevice, getSeparator, printLogo, getLogoWidth, drawBoxHeader, drawBoxFooter, centerText, createBoxMenu } = require('./ui');
|
|
16
|
+
const { validateUsername, validatePassword, maskSensitive } = require('./security');
|
|
17
|
+
|
|
18
|
+
// Pages
|
|
19
|
+
const { showStats } = require('./pages/stats');
|
|
20
|
+
const { showAccounts } = require('./pages/accounts');
|
|
21
|
+
const { algoTradingMenu } = require('./pages/algo');
|
|
22
|
+
|
|
23
|
+
// Current service reference
|
|
24
|
+
let currentService = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Displays the application banner with stats if connected
|
|
28
|
+
*/
|
|
29
|
+
const banner = async () => {
|
|
30
|
+
console.clear();
|
|
31
|
+
const boxWidth = getLogoWidth();
|
|
32
|
+
const innerWidth = boxWidth - 2;
|
|
33
|
+
const version = require('../package.json').version;
|
|
34
|
+
|
|
35
|
+
// Get stats if connected (only active accounts: status === 0)
|
|
36
|
+
let statsInfo = null;
|
|
37
|
+
if (connections.count() > 0) {
|
|
38
|
+
try {
|
|
39
|
+
const allAccounts = await connections.getAllAccounts();
|
|
40
|
+
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
41
|
+
let totalBalance = 0;
|
|
42
|
+
let totalStartingBalance = 0;
|
|
43
|
+
let totalPnl = 0;
|
|
44
|
+
|
|
45
|
+
activeAccounts.forEach(account => {
|
|
46
|
+
totalBalance += account.balance || 0;
|
|
47
|
+
totalStartingBalance += account.startingBalance || 0;
|
|
48
|
+
totalPnl += account.profitAndLoss || 0;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const pnl = totalPnl !== 0 ? totalPnl : (totalBalance - totalStartingBalance);
|
|
52
|
+
const pnlPercent = totalStartingBalance > 0 ? ((pnl / totalStartingBalance) * 100).toFixed(1) : '0.0';
|
|
53
|
+
|
|
54
|
+
statsInfo = {
|
|
55
|
+
connections: connections.count(),
|
|
56
|
+
accounts: activeAccounts.length,
|
|
57
|
+
balance: totalBalance,
|
|
58
|
+
pnl: pnl,
|
|
59
|
+
pnlPercent: pnlPercent
|
|
60
|
+
};
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Ignore errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Draw logo HQX
|
|
67
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
68
|
+
|
|
69
|
+
const logo = [
|
|
70
|
+
'██╗ ██╗ ██████╗ ',
|
|
71
|
+
'██║ ██║██╔═══██╗',
|
|
72
|
+
'███████║██║ ██║',
|
|
73
|
+
'██╔══██║██║▄▄ ██║',
|
|
74
|
+
'██║ ██║╚██████╔╝',
|
|
75
|
+
'╚═╝ ╚═╝ ╚══▀▀═╝ '
|
|
76
|
+
];
|
|
77
|
+
const logoX = [
|
|
78
|
+
'██╗ ██╗',
|
|
79
|
+
'╚██╗██╔╝',
|
|
80
|
+
' ╚███╔╝ ',
|
|
81
|
+
' ██╔██╗ ',
|
|
82
|
+
'██╔╝ ██╗',
|
|
83
|
+
'╚═╝ ╚═╝'
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
logo.forEach((line, i) => {
|
|
87
|
+
const mainPart = chalk.cyan(line);
|
|
88
|
+
const xPart = chalk.yellow(logoX[i]);
|
|
89
|
+
const fullLine = mainPart + xPart;
|
|
90
|
+
const totalLen = line.length + logoX[i].length;
|
|
91
|
+
const padding = innerWidth - totalLen;
|
|
92
|
+
const leftPad = Math.floor(padding / 2);
|
|
93
|
+
const rightPad = padding - leftPad;
|
|
94
|
+
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Tagline
|
|
98
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
99
|
+
console.log(chalk.cyan('║') + chalk.white(centerText(`Prop Futures Algo Trading v${version}`, innerWidth)) + chalk.cyan('║'));
|
|
100
|
+
|
|
101
|
+
// Stats bar if connected
|
|
102
|
+
if (statsInfo) {
|
|
103
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
104
|
+
|
|
105
|
+
const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
|
|
106
|
+
const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
|
|
107
|
+
|
|
108
|
+
const connStr = `Connections: ${statsInfo.connections}`;
|
|
109
|
+
const accStr = `Accounts: ${statsInfo.accounts}`;
|
|
110
|
+
const balVal = `$${statsInfo.balance.toLocaleString()}`;
|
|
111
|
+
const pnlVal = `$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnlPercent}%)`;
|
|
112
|
+
|
|
113
|
+
const statsLen = connStr.length + 4 + accStr.length + 4 + 8 + balVal.length + 4 + 5 + pnlVal.length;
|
|
114
|
+
const statsLeftPad = Math.floor((innerWidth - statsLen) / 2);
|
|
115
|
+
const statsRightPad = innerWidth - statsLen - statsLeftPad;
|
|
116
|
+
|
|
117
|
+
console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
|
|
118
|
+
chalk.white(connStr) + ' ' +
|
|
119
|
+
chalk.white(accStr) + ' ' +
|
|
120
|
+
chalk.white('Balance: ') + chalk.green(balVal) + ' ' +
|
|
121
|
+
chalk.white('P&L: ') + pnlColor(pnlVal) + ' '.repeat(statsRightPad) + chalk.cyan('║')
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
126
|
+
console.log();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Login prompt with validation
|
|
131
|
+
* @param {string} propfirmName - PropFirm display name
|
|
132
|
+
* @returns {Promise<{username: string, password: string}>}
|
|
133
|
+
*/
|
|
134
|
+
const loginPrompt = async (propfirmName) => {
|
|
135
|
+
const device = getDevice();
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
|
|
138
|
+
console.log();
|
|
139
|
+
|
|
140
|
+
const credentials = await inquirer.prompt([
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'username',
|
|
144
|
+
message: chalk.white.bold('Username:'),
|
|
145
|
+
validate: (input) => {
|
|
146
|
+
try {
|
|
147
|
+
validateUsername(input);
|
|
148
|
+
return true;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return e.message;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'password',
|
|
156
|
+
name: 'password',
|
|
157
|
+
message: chalk.white.bold('Password:'),
|
|
158
|
+
mask: '*',
|
|
159
|
+
validate: (input) => {
|
|
160
|
+
try {
|
|
161
|
+
validatePassword(input);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return e.message;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
return credentials;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ProjectX platform connection menu
|
|
175
|
+
*/
|
|
176
|
+
const projectXMenu = async () => {
|
|
177
|
+
const propfirms = getPropFirmsByPlatform('ProjectX');
|
|
178
|
+
const boxWidth = getLogoWidth();
|
|
179
|
+
const innerWidth = boxWidth - 2;
|
|
180
|
+
const numCols = 3;
|
|
181
|
+
const colWidth = Math.floor(innerWidth / numCols);
|
|
182
|
+
|
|
183
|
+
// Build numbered list
|
|
184
|
+
const numbered = propfirms.map((pf, i) => ({
|
|
185
|
+
num: i + 1,
|
|
186
|
+
key: pf.key,
|
|
187
|
+
name: pf.displayName
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
// PropFirm selection box
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
193
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
|
|
194
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
195
|
+
|
|
196
|
+
// Display in 3 columns
|
|
197
|
+
const rows = Math.ceil(numbered.length / numCols);
|
|
198
|
+
for (let row = 0; row < rows; row++) {
|
|
199
|
+
let line = '';
|
|
200
|
+
for (let col = 0; col < numCols; col++) {
|
|
201
|
+
const idx = row + col * rows;
|
|
202
|
+
if (idx < numbered.length) {
|
|
203
|
+
const item = numbered[idx];
|
|
204
|
+
const text = `[${item.num}] ${item.name}`;
|
|
205
|
+
const coloredText = chalk.cyan(`[${item.num}]`) + ' ' + chalk.white(item.name);
|
|
206
|
+
const textLen = text.length;
|
|
207
|
+
const padding = colWidth - textLen - 2;
|
|
208
|
+
line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
|
|
209
|
+
} else {
|
|
210
|
+
line += ' '.repeat(colWidth);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Adjust for exact width
|
|
214
|
+
const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
215
|
+
const adjust = innerWidth - lineLen;
|
|
216
|
+
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
220
|
+
const backText = ' ' + chalk.red('[X] Back');
|
|
221
|
+
const backLen = '[X] Back'.length + 2;
|
|
222
|
+
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
223
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
224
|
+
console.log();
|
|
225
|
+
|
|
226
|
+
const validInputs = numbered.map(n => n.num.toString());
|
|
227
|
+
validInputs.push('x', 'X');
|
|
228
|
+
|
|
229
|
+
const { action } = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'input',
|
|
232
|
+
name: 'action',
|
|
233
|
+
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
234
|
+
validate: (input) => {
|
|
235
|
+
if (validInputs.includes(input)) return true;
|
|
236
|
+
return `Please enter 1-${numbered.length} or X`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
if (action.toLowerCase() === 'x') return null;
|
|
242
|
+
|
|
243
|
+
const selectedIdx = parseInt(action) - 1;
|
|
244
|
+
const selectedPropfirm = numbered[selectedIdx];
|
|
245
|
+
|
|
246
|
+
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
247
|
+
const spinner = ora('Authenticating...').start();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const service = new ProjectXService(selectedPropfirm.key);
|
|
251
|
+
const result = await service.login(credentials.username, credentials.password);
|
|
252
|
+
|
|
253
|
+
if (result.success) {
|
|
254
|
+
await service.getUser();
|
|
255
|
+
connections.add('projectx', service, service.propfirm.name);
|
|
256
|
+
currentService = service;
|
|
257
|
+
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
258
|
+
return service;
|
|
259
|
+
} else {
|
|
260
|
+
spinner.fail(result.error || 'Authentication failed');
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
spinner.fail(error.message);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Main connection menu
|
|
271
|
+
*/
|
|
272
|
+
const mainMenu = async () => {
|
|
273
|
+
const boxWidth = getLogoWidth();
|
|
274
|
+
const innerWidth = boxWidth - 2;
|
|
275
|
+
const col1Width = Math.floor(innerWidth / 2);
|
|
276
|
+
const col2Width = innerWidth - col1Width;
|
|
277
|
+
|
|
278
|
+
// Connection menu box
|
|
279
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
280
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
|
|
281
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
282
|
+
|
|
283
|
+
// Menu row helper (2 columns)
|
|
284
|
+
const menuRow = (left, right) => {
|
|
285
|
+
const leftText = ' ' + left;
|
|
286
|
+
const rightText = right ? ' ' + right : '';
|
|
287
|
+
const leftLen = leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
288
|
+
const rightLen = rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
289
|
+
const leftPad = col1Width - leftLen;
|
|
290
|
+
const rightPad = col2Width - rightLen;
|
|
291
|
+
console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
menuRow(chalk.cyan('[1] ProjectX'), chalk.gray('[2] Rithmic (Coming Soon)'));
|
|
295
|
+
menuRow(chalk.gray('[3] Tradovate (Coming Soon)'), chalk.red('[X] Exit'));
|
|
296
|
+
|
|
297
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
298
|
+
console.log();
|
|
299
|
+
|
|
300
|
+
const { action } = await inquirer.prompt([
|
|
301
|
+
{
|
|
302
|
+
type: 'input',
|
|
303
|
+
name: 'action',
|
|
304
|
+
message: chalk.cyan('Enter choice (1/X):'),
|
|
305
|
+
validate: (input) => {
|
|
306
|
+
const valid = ['1', 'x', 'X'];
|
|
307
|
+
if (valid.includes(input)) return true;
|
|
308
|
+
return 'Please enter 1 or X';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
// Map input to action
|
|
314
|
+
const actionMap = {
|
|
315
|
+
'1': 'projectx',
|
|
316
|
+
'x': 'exit',
|
|
317
|
+
'X': 'exit'
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return actionMap[action] || 'exit';
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Dashboard menu after login
|
|
325
|
+
* @param {Object} service - Connected service
|
|
326
|
+
*/
|
|
327
|
+
const dashboardMenu = async (service) => {
|
|
328
|
+
const user = service.user;
|
|
329
|
+
const boxWidth = getLogoWidth();
|
|
330
|
+
const innerWidth = boxWidth - 2;
|
|
331
|
+
|
|
332
|
+
// Dashboard box header
|
|
333
|
+
console.log();
|
|
334
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
335
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('DASHBOARD', innerWidth)) + chalk.cyan('║'));
|
|
336
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
337
|
+
|
|
338
|
+
// Connection info
|
|
339
|
+
const connInfo = chalk.green('Connected to ' + service.propfirm.name);
|
|
340
|
+
const connLen = ('Connected to ' + service.propfirm.name).length;
|
|
341
|
+
console.log(chalk.cyan('║') + ' ' + connInfo + ' '.repeat(innerWidth - connLen - 2) + chalk.cyan('║'));
|
|
342
|
+
|
|
343
|
+
if (user) {
|
|
344
|
+
const userInfo = 'Welcome, ' + user.userName.toUpperCase() + '!';
|
|
345
|
+
console.log(chalk.cyan('║') + ' ' + chalk.white(userInfo) + ' '.repeat(innerWidth - userInfo.length - 2) + chalk.cyan('║'));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
349
|
+
|
|
350
|
+
// Menu options in 2 columns
|
|
351
|
+
const col1Width = Math.floor(innerWidth / 2);
|
|
352
|
+
const col2Width = innerWidth - col1Width;
|
|
353
|
+
|
|
354
|
+
const menuRow = (left, right) => {
|
|
355
|
+
const leftText = ' ' + left;
|
|
356
|
+
const rightText = right ? ' ' + right : '';
|
|
357
|
+
const leftPad = col1Width - leftText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
358
|
+
const rightPad = col2Width - rightText.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
359
|
+
console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
menuRow(chalk.cyan('[1] View Accounts'), chalk.cyan('[2] View Stats'));
|
|
363
|
+
menuRow(chalk.cyan('[+] Add Prop-Account'), chalk.cyan('[A] Algo-Trading'));
|
|
364
|
+
menuRow(chalk.yellow('[U] Update HQX'), chalk.red('[X] Disconnect'));
|
|
365
|
+
|
|
366
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
367
|
+
console.log();
|
|
368
|
+
|
|
369
|
+
const { action } = await inquirer.prompt([
|
|
370
|
+
{
|
|
371
|
+
type: 'input',
|
|
372
|
+
name: 'action',
|
|
373
|
+
message: chalk.cyan('Enter choice (1/2/+/A/U/X):'),
|
|
374
|
+
validate: (input) => {
|
|
375
|
+
const valid = ['1', '2', '+', 'a', 'A', 'u', 'U', 'x', 'X'];
|
|
376
|
+
if (valid.includes(input)) return true;
|
|
377
|
+
return 'Please enter a valid option';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
// Map input to action
|
|
383
|
+
const actionMap = {
|
|
384
|
+
'1': 'accounts',
|
|
385
|
+
'2': 'stats',
|
|
386
|
+
'+': 'add_prop_account',
|
|
387
|
+
'a': 'algotrading',
|
|
388
|
+
'A': 'algotrading',
|
|
389
|
+
'u': 'update',
|
|
390
|
+
'U': 'update',
|
|
391
|
+
'x': 'disconnect',
|
|
392
|
+
'X': 'disconnect'
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
return actionMap[action] || 'accounts';
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Handles the update process with auto-restart
|
|
400
|
+
*/
|
|
401
|
+
const handleUpdate = async () => {
|
|
402
|
+
const { spawn } = require('child_process');
|
|
403
|
+
const pkg = require('../package.json');
|
|
404
|
+
const currentVersion = pkg.version;
|
|
405
|
+
const spinner = ora('Checking for updates...').start();
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const cliPath = path.resolve(__dirname, '..');
|
|
409
|
+
|
|
410
|
+
// Get current commit
|
|
411
|
+
const beforeCommit = execSync('git rev-parse --short HEAD', { cwd: cliPath, stdio: 'pipe' }).toString().trim();
|
|
412
|
+
|
|
413
|
+
// Fetch to check for updates
|
|
414
|
+
execSync('git fetch origin main', { cwd: cliPath, stdio: 'pipe' });
|
|
415
|
+
|
|
416
|
+
// Check if behind
|
|
417
|
+
const behindCount = execSync('git rev-list HEAD..origin/main --count', { cwd: cliPath, stdio: 'pipe' }).toString().trim();
|
|
418
|
+
|
|
419
|
+
if (parseInt(behindCount) === 0) {
|
|
420
|
+
spinner.succeed('Already up to date!');
|
|
421
|
+
console.log(chalk.cyan(` Version: v${currentVersion}`));
|
|
422
|
+
console.log(chalk.gray(` Commit: ${beforeCommit}`));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Stash local changes
|
|
427
|
+
spinner.text = 'Stashing local changes...';
|
|
428
|
+
try {
|
|
429
|
+
execSync('git stash --include-untracked', { cwd: cliPath, stdio: 'pipe' });
|
|
430
|
+
} catch (e) {
|
|
431
|
+
// If stash fails, reset
|
|
432
|
+
execSync('git checkout -- .', { cwd: cliPath, stdio: 'pipe' });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Pull latest
|
|
436
|
+
spinner.text = 'Downloading updates...';
|
|
437
|
+
execSync('git pull origin main', { cwd: cliPath, stdio: 'pipe' });
|
|
438
|
+
const afterCommit = execSync('git rev-parse --short HEAD', { cwd: cliPath, stdio: 'pipe' }).toString().trim();
|
|
439
|
+
|
|
440
|
+
// Install dependencies
|
|
441
|
+
spinner.text = 'Installing dependencies...';
|
|
442
|
+
try {
|
|
443
|
+
execSync('npm install --silent', { cwd: cliPath, stdio: 'pipe' });
|
|
444
|
+
} catch (e) { /* ignore */ }
|
|
445
|
+
|
|
446
|
+
// Get new version
|
|
447
|
+
delete require.cache[require.resolve('../package.json')];
|
|
448
|
+
const newPkg = require('../package.json');
|
|
449
|
+
const newVersion = newPkg.version;
|
|
450
|
+
|
|
451
|
+
spinner.succeed('CLI updated!');
|
|
452
|
+
console.log();
|
|
453
|
+
console.log(chalk.green(` Version: v${currentVersion} -> v${newVersion}`));
|
|
454
|
+
console.log(chalk.gray(` Commits: ${beforeCommit} -> ${afterCommit} (${behindCount} new)`));
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(chalk.cyan(' Restarting...'));
|
|
457
|
+
console.log();
|
|
458
|
+
|
|
459
|
+
// Restart CLI
|
|
460
|
+
const child = spawn(process.argv[0], [path.join(cliPath, 'bin', 'cli.js')], {
|
|
461
|
+
cwd: cliPath,
|
|
462
|
+
stdio: 'inherit',
|
|
463
|
+
shell: true
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
child.on('exit', (code) => {
|
|
467
|
+
process.exit(code);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Stop current process loop
|
|
471
|
+
return 'restart';
|
|
472
|
+
|
|
473
|
+
} catch (error) {
|
|
474
|
+
spinner.fail('Update failed: ' + error.message);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Main application loop
|
|
480
|
+
*/
|
|
481
|
+
const run = async () => {
|
|
482
|
+
await banner();
|
|
483
|
+
|
|
484
|
+
// Try to restore session
|
|
485
|
+
const spinner = ora('Restoring session...').start();
|
|
486
|
+
const restored = await connections.restoreFromStorage();
|
|
487
|
+
|
|
488
|
+
if (restored) {
|
|
489
|
+
spinner.succeed('Session restored');
|
|
490
|
+
currentService = connections.getAll()[0].service;
|
|
491
|
+
} else {
|
|
492
|
+
spinner.info('No active session');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Main loop
|
|
496
|
+
while (true) {
|
|
497
|
+
// Refresh banner with stats
|
|
498
|
+
await banner();
|
|
499
|
+
|
|
500
|
+
if (!connections.isConnected()) {
|
|
501
|
+
const choice = await mainMenu();
|
|
502
|
+
|
|
503
|
+
if (choice === 'exit') {
|
|
504
|
+
console.log(chalk.gray('Goodbye!'));
|
|
505
|
+
process.exit(0);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (choice === 'projectx') {
|
|
509
|
+
const service = await projectXMenu();
|
|
510
|
+
if (service) currentService = service;
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
const action = await dashboardMenu(currentService);
|
|
514
|
+
|
|
515
|
+
switch (action) {
|
|
516
|
+
case 'accounts':
|
|
517
|
+
await showAccounts(currentService);
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case 'stats':
|
|
521
|
+
await showStats(currentService);
|
|
522
|
+
break;
|
|
523
|
+
case 'add_prop_account':
|
|
524
|
+
const newService = await projectXMenu();
|
|
525
|
+
if (newService) {
|
|
526
|
+
currentService = newService;
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
case 'algotrading':
|
|
530
|
+
await algoTradingMenu(currentService);
|
|
531
|
+
break;
|
|
532
|
+
case 'update':
|
|
533
|
+
const updateResult = await handleUpdate();
|
|
534
|
+
if (updateResult === 'restart') return; // Stop loop, new process spawned
|
|
535
|
+
break;
|
|
536
|
+
case 'disconnect':
|
|
537
|
+
const connCount = connections.count();
|
|
538
|
+
connections.disconnectAll();
|
|
539
|
+
currentService = null;
|
|
540
|
+
console.log(chalk.yellow(`Disconnected ${connCount} connection${connCount > 1 ? 's' : ''}`));
|
|
541
|
+
break;
|
|
542
|
+
case 'exit':
|
|
543
|
+
console.log(chalk.gray('Goodbye!'));
|
|
544
|
+
process.exit(0);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
module.exports = { run, banner, loginPrompt, mainMenu, dashboardMenu };
|
package/src/config/index.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration
|
|
2
|
+
* @fileoverview Configuration module exports
|
|
3
|
+
* @module config
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
PROPFIRMS,
|
|
8
|
+
PROPFIRM_CHOICES,
|
|
9
|
+
getPropFirm,
|
|
10
|
+
getPropFirmById,
|
|
11
|
+
getPropFirmsByPlatform
|
|
12
|
+
} = require('./propfirms');
|
|
13
|
+
|
|
6
14
|
const {
|
|
7
15
|
ACCOUNT_STATUS,
|
|
8
16
|
ACCOUNT_TYPE,
|
|
@@ -13,8 +21,14 @@ const {
|
|
|
13
21
|
} = require('./constants');
|
|
14
22
|
|
|
15
23
|
module.exports = {
|
|
24
|
+
// PropFirms
|
|
16
25
|
PROPFIRMS,
|
|
17
26
|
PROPFIRM_CHOICES,
|
|
27
|
+
getPropFirm,
|
|
28
|
+
getPropFirmById,
|
|
29
|
+
getPropFirmsByPlatform,
|
|
30
|
+
|
|
31
|
+
// Constants
|
|
18
32
|
ACCOUNT_STATUS,
|
|
19
33
|
ACCOUNT_TYPE,
|
|
20
34
|
ORDER_STATUS,
|