hedgequantx 2.9.19 → 2.9.21

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +42 -64
  3. package/src/lib/m/hqx-2b.js +7 -0
  4. package/src/lib/m/index.js +138 -0
  5. package/src/lib/m/ultra-scalping.js +7 -0
  6. package/src/menus/connect.js +14 -17
  7. package/src/menus/dashboard.js +58 -76
  8. package/src/pages/accounts.js +38 -49
  9. package/src/pages/algo/copy-trading.js +546 -178
  10. package/src/pages/algo/index.js +18 -75
  11. package/src/pages/algo/one-account.js +322 -57
  12. package/src/pages/algo/ui.js +15 -15
  13. package/src/pages/orders.js +19 -22
  14. package/src/pages/positions.js +19 -22
  15. package/src/pages/stats/index.js +15 -16
  16. package/src/pages/user.js +7 -11
  17. package/src/services/ai-supervision/health.js +35 -47
  18. package/src/services/index.js +1 -9
  19. package/src/services/rithmic/accounts.js +8 -6
  20. package/src/ui/box.js +9 -5
  21. package/src/ui/index.js +5 -18
  22. package/src/ui/menu.js +4 -4
  23. package/src/pages/ai-agents-ui.js +0 -388
  24. package/src/pages/ai-agents.js +0 -494
  25. package/src/pages/ai-models.js +0 -389
  26. package/src/pages/algo/algo-executor.js +0 -307
  27. package/src/pages/algo/copy-executor.js +0 -331
  28. package/src/pages/algo/custom-strategy.js +0 -313
  29. package/src/services/ai-supervision/consensus.js +0 -284
  30. package/src/services/ai-supervision/context.js +0 -275
  31. package/src/services/ai-supervision/directive.js +0 -167
  32. package/src/services/ai-supervision/index.js +0 -359
  33. package/src/services/ai-supervision/parser.js +0 -278
  34. package/src/services/ai-supervision/symbols.js +0 -259
  35. package/src/services/cliproxy/index.js +0 -256
  36. package/src/services/cliproxy/installer.js +0 -111
  37. package/src/services/cliproxy/manager.js +0 -387
  38. package/src/services/llmproxy/index.js +0 -166
  39. package/src/services/llmproxy/manager.js +0 -411
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.19",
3
+ "version": "2.9.21",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -7,7 +7,7 @@ const chalk = require('chalk');
7
7
  const ora = require('ora');
8
8
 
9
9
  const { connections } = require('./services');
10
- const { getLogoWidth, centerText, prepareStdin, clearScreen } = require('./ui');
10
+ const { getLogoWidth, centerText, prepareStdin } = require('./ui');
11
11
  const { logger, prompts } = require('./utils');
12
12
  const { setCachedStats, clearCachedStats } = require('./services/stats-cache');
13
13
 
@@ -17,7 +17,6 @@ const log = logger.scope('App');
17
17
  const { showStats } = require('./pages/stats');
18
18
  const { showAccounts } = require('./pages/accounts');
19
19
  const { algoTradingMenu } = require('./pages/algo');
20
- const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
21
20
 
22
21
  // Menus
23
22
  const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
@@ -30,7 +29,7 @@ let currentService = null;
30
29
 
31
30
  const restoreTerminal = () => {
32
31
  try {
33
- // Show cursor
32
+ process.stdout.write('\x1B[?1049l');
34
33
  process.stdout.write('\x1B[?25h');
35
34
  if (process.stdin.isTTY && process.stdin.isRaw) {
36
35
  process.stdin.setRawMode(false);
@@ -42,15 +41,7 @@ const restoreTerminal = () => {
42
41
  };
43
42
 
44
43
  process.on('exit', restoreTerminal);
45
- process.on('SIGINT', () => {
46
- restoreTerminal();
47
- // Draw bottom border before exit
48
- const termWidth = process.stdout.columns || 100;
49
- const boxWidth = termWidth < 60 ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
50
- console.log(chalk.cyan('\n╚' + '═'.repeat(boxWidth - 2) + '╝'));
51
- console.log(chalk.gray('GOODBYE!'));
52
- process.exit(0);
53
- });
44
+ process.on('SIGINT', () => { restoreTerminal(); process.exit(0); });
54
45
  process.on('SIGTERM', () => { restoreTerminal(); process.exit(0); });
55
46
 
56
47
  process.on('uncaughtException', (err) => {
@@ -101,7 +92,6 @@ const refreshStats = async () => {
101
92
  balance: hasBalanceData ? totalBalance : null,
102
93
  pnl: hasPnlData ? totalPnl : null,
103
94
  pnlPercent: null,
104
- agents: getActiveAgentCount(),
105
95
  });
106
96
  } catch (err) {
107
97
  log.warn('Failed to refresh stats', { error: err.message });
@@ -111,7 +101,7 @@ const refreshStats = async () => {
111
101
  // ==================== BANNER ====================
112
102
 
113
103
  const banner = async () => {
114
- clearScreen();
104
+ console.clear();
115
105
 
116
106
  const termWidth = process.stdout.columns || 100;
117
107
  const isMobile = termWidth < 60;
@@ -133,11 +123,8 @@ const banner = async () => {
133
123
 
134
124
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
135
125
 
136
- const tagline = isMobile ? `HQX V${version}` : `PROP FUTURES ALGO TRADING V${version}`;
137
- console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
138
-
139
- // ALWAYS close the banner
140
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
126
+ const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
127
+ console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
141
128
  };
142
129
 
143
130
  const getFullLogo = () => [
@@ -158,37 +145,39 @@ const getMobileLogo = () => [
158
145
  ['╚═╝ ╚═╝ ╚══▀▀═╝ ', '╚═╝ ╚═╝'],
159
146
  ];
160
147
 
148
+ const bannerClosed = async () => {
149
+ await banner();
150
+ const termWidth = process.stdout.columns || 100;
151
+ const boxWidth = termWidth < 60 ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
152
+ console.log(chalk.cyan('╚' + '═'.repeat(boxWidth - 2) + '╝'));
153
+ };
154
+
161
155
  // ==================== MAIN LOOP ====================
162
156
 
163
157
  const run = async () => {
164
158
  try {
165
159
  log.info('Starting HQX CLI');
166
-
167
- // First launch - show banner then try restore session
168
- await banner();
169
-
170
- const spinner = ora({ text: 'LOADING DASHBOARD...', color: 'yellow' }).start();
171
-
160
+ await bannerClosed();
161
+
162
+ // Restore session
163
+ const spinner = ora({ text: 'Testing connections...', color: 'yellow' }).start();
172
164
  const restored = await connections.restoreFromStorage();
173
165
 
174
166
  if (restored) {
167
+ spinner.succeed('Connected');
175
168
  currentService = connections.getAll()[0].service;
176
169
  await refreshStats();
177
- // Store spinner globally - dashboard will stop it when ready to display
178
- global.__hqxSpinner = spinner;
179
170
  } else {
180
- spinner.stop(); // Stop spinner - no session to restore
181
- global.__hqxSpinner = null;
171
+ spinner.info('No saved connections');
182
172
  }
183
173
 
184
174
  // Main loop
185
175
  while (true) {
186
176
  try {
187
177
  prepareStdin();
178
+ await banner();
188
179
 
189
180
  if (!connections.isConnected()) {
190
- // Not connected - show banner + propfirm selection
191
- await banner();
192
181
  // Not connected - show propfirm selection directly
193
182
  const boxWidth = getLogoWidth();
194
183
  const innerWidth = boxWidth - 2;
@@ -199,14 +188,13 @@ const run = async () => {
199
188
 
200
189
  // Find max name length for alignment
201
190
  const maxNameLen = Math.max(...numbered.map(n => n.name.length));
202
- const itemWidth = 4 + 1 + maxNameLen; // [##] + space + name
203
- const gap = 3; // gap between columns
204
- const totalContentWidth = (itemWidth * numCols) + (gap * (numCols - 1));
191
+ const colWidth = 4 + 1 + maxNameLen + 2; // [##] + space + name + gap
192
+ const totalContentWidth = numCols * colWidth;
193
+ const leftMargin = Math.max(2, Math.floor((innerWidth - totalContentWidth) / 2));
205
194
 
206
- // New rectangle (banner is always closed)
207
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
208
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
209
195
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
196
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
197
+ console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
210
198
 
211
199
  const rows = Math.ceil(numbered.length / numCols);
212
200
  for (let row = 0; row < rows; row++) {
@@ -223,32 +211,29 @@ const run = async () => {
223
211
  }
224
212
  }
225
213
 
226
- // Build line content
227
- let content = '';
214
+ let line = ' '.repeat(leftMargin);
228
215
  for (let i = 0; i < lineParts.length; i++) {
229
216
  if (lineParts[i]) {
230
- content += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
217
+ line += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
231
218
  } else {
232
- content += ' '.repeat(itemWidth);
219
+ line += ' '.repeat(4 + 1 + maxNameLen);
233
220
  }
234
- if (i < lineParts.length - 1) content += ' '.repeat(gap);
221
+ if (i < lineParts.length - 1) line += ' ';
235
222
  }
236
223
 
237
- // Center the content
238
- const contentLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
239
- const leftPad = Math.floor((innerWidth - contentLen) / 2);
240
- const rightPad = innerWidth - contentLen - leftPad;
241
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║'));
224
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
225
+ const rightPad = Math.max(0, innerWidth - lineLen);
226
+ console.log(chalk.cyan('║') + line + ' '.repeat(rightPad) + chalk.cyan('║'));
242
227
  }
243
228
 
244
229
  console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
245
- console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
230
+ console.log(chalk.cyan('║') + chalk.red(centerText('[X] Exit', innerWidth)) + chalk.cyan('║'));
246
231
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
247
232
 
248
- const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
233
+ const input = await prompts.textInput(chalk.cyan('Select number (or X):'));
249
234
 
250
235
  if (!input || input.toLowerCase() === 'x') {
251
- console.log(chalk.gray('GOODBYE!'));
236
+ console.log(chalk.gray('Goodbye!'));
252
237
  process.exit(0);
253
238
  }
254
239
 
@@ -259,26 +244,26 @@ const run = async () => {
259
244
  const credentials = await loginPrompt(selectedPropfirm.name);
260
245
 
261
246
  if (credentials) {
262
- const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
247
+ const spinner = ora({ text: 'Connecting to Rithmic...', color: 'yellow' }).start();
263
248
  try {
264
249
  const { RithmicService } = require('./services/rithmic');
265
250
  const service = new RithmicService(selectedPropfirm.key);
266
251
  const result = await service.login(credentials.username, credentials.password);
267
252
 
268
253
  if (result.success) {
269
- spinner.text = 'FETCHING ACCOUNTS...';
254
+ spinner.text = 'Fetching accounts...';
270
255
  const accResult = await service.getTradingAccounts();
271
256
  connections.add('rithmic', service, service.propfirm.name);
272
- spinner.succeed(`CONNECTED TO ${service.propfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
257
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
273
258
  currentService = service;
274
259
  await refreshStats();
275
260
  await new Promise(r => setTimeout(r, 1500));
276
261
  } else {
277
- spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
262
+ spinner.fail(result.error || 'Authentication failed');
278
263
  await new Promise(r => setTimeout(r, 2000));
279
264
  }
280
265
  } catch (error) {
281
- spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
266
+ spinner.fail(`Connection error: ${error.message}`);
282
267
  await new Promise(r => setTimeout(r, 2000));
283
268
  }
284
269
  }
@@ -311,20 +296,13 @@ const run = async () => {
311
296
  try {
312
297
  await algoTradingMenu(currentService);
313
298
  } catch (err) {
314
- console.log(chalk.red(` ALGO ERROR: ${err.message.toUpperCase()}`));
299
+ console.log(chalk.red(` Algo error: ${err.message}`));
315
300
  prepareStdin();
316
301
  }
317
302
  break;
318
303
 
319
- case 'aiagents':
320
- await aiAgentsMenu();
321
- break;
322
-
323
304
  case 'update':
324
- const updateResult = await handleUpdate();
325
- if (updateResult === 'exit') {
326
- running = false;
327
- }
305
+ await handleUpdate();
328
306
  break;
329
307
 
330
308
  case 'disconnect':
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ // Load from private sources (dev mode) or compiled (prod mode)
3
+ try {
4
+ module.exports = require('../../../dist/lib/m/hqx-2b.jsc');
5
+ } catch (e) {
6
+ module.exports = require('../../../private/strategies/hqx-2b-liquidity-sweep');
7
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Strategy Registry - Available Trading Strategies
3
+ *
4
+ * All strategies are compiled to bytecode (.jsc) for protection.
5
+ * This module provides a unified interface to load strategies.
6
+ */
7
+
8
+ const path = require('path');
9
+
10
+ // =============================================================================
11
+ // AVAILABLE STRATEGIES
12
+ // =============================================================================
13
+
14
+ const STRATEGIES = {
15
+ 'ultra-scalping': {
16
+ id: 'ultra-scalping',
17
+ name: 'HQX Ultra Scalping',
18
+ description: '6 Mathematical Models (Z-Score, VPIN, Kyle, Kalman, Vol, OFI)',
19
+ version: '2.0',
20
+ backtest: {
21
+ pnl: '$2,012,373',
22
+ winRate: '71.1%',
23
+ trades: '146,685',
24
+ period: 'Jan 2020 - Nov 2025'
25
+ },
26
+ params: {
27
+ stopTicks: 8,
28
+ targetTicks: 16,
29
+ riskReward: '1:2'
30
+ },
31
+ loader: () => require('./ultra-scalping')
32
+ },
33
+
34
+ 'hqx-2b': {
35
+ id: 'hqx-2b',
36
+ name: 'HQX-2B Liquidity Sweep',
37
+ description: '2B Pattern with Liquidity Zone Sweeps (Optimized)',
38
+ version: '1.0',
39
+ backtest: {
40
+ pnl: '$6,601,305',
41
+ winRate: '82.8%',
42
+ trades: '100,220',
43
+ period: 'Dec 2020 - Nov 2025'
44
+ },
45
+ params: {
46
+ stopTicks: 10,
47
+ targetTicks: 40,
48
+ riskReward: '1:4'
49
+ },
50
+ loader: () => require('./hqx-2b')
51
+ }
52
+ };
53
+
54
+ // =============================================================================
55
+ // EXPORTS
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Get list of available strategies
60
+ * @returns {Array} List of strategy info objects
61
+ */
62
+ function getAvailableStrategies() {
63
+ return Object.values(STRATEGIES).map(s => ({
64
+ id: s.id,
65
+ name: s.name,
66
+ description: s.description,
67
+ version: s.version,
68
+ backtest: s.backtest,
69
+ params: s.params
70
+ }));
71
+ }
72
+
73
+ /**
74
+ * Get strategy by ID
75
+ * @param {string} strategyId - Strategy identifier
76
+ * @returns {Object|null} Strategy info or null if not found
77
+ */
78
+ function getStrategy(strategyId) {
79
+ return STRATEGIES[strategyId] || null;
80
+ }
81
+
82
+ /**
83
+ * Load strategy module by ID
84
+ * Returns normalized module with M1 as the strategy class
85
+ * @param {string} strategyId - Strategy identifier
86
+ * @returns {Object} Strategy module with M1 class
87
+ */
88
+ function loadStrategy(strategyId) {
89
+ const strategy = STRATEGIES[strategyId];
90
+ if (!strategy) {
91
+ throw new Error(`Strategy not found: ${strategyId}`);
92
+ }
93
+
94
+ let module;
95
+ try {
96
+ module = strategy.loader();
97
+ } catch (e) {
98
+ // If compiled bytecode not found, try to load from private sources (dev mode)
99
+ if (e.code === 'MODULE_NOT_FOUND') {
100
+ try {
101
+ const devPath = path.join(__dirname, '../../../private/strategies',
102
+ strategyId === 'ultra-scalping' ? 'ultra-scalping.js' : 'hqx-2b-liquidity-sweep.js'
103
+ );
104
+ module = require(devPath);
105
+ } catch (devErr) {
106
+ throw new Error(`Failed to load strategy ${strategyId}: ${e.message}`);
107
+ }
108
+ } else {
109
+ throw e;
110
+ }
111
+ }
112
+
113
+ // Normalize: always return { M1: StrategyClass }
114
+ // Ultra Scalping exports M1, HQX-2B exports M2
115
+ if (module.M1) {
116
+ return module;
117
+ } else if (module.M2) {
118
+ return { M1: module.M2, ...module };
119
+ } else {
120
+ throw new Error(`Strategy ${strategyId} has no valid strategy class (M1 or M2)`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get default strategy ID
126
+ * @returns {string} Default strategy ID
127
+ */
128
+ function getDefaultStrategy() {
129
+ return 'ultra-scalping';
130
+ }
131
+
132
+ module.exports = {
133
+ STRATEGIES,
134
+ getAvailableStrategies,
135
+ getStrategy,
136
+ loadStrategy,
137
+ getDefaultStrategy
138
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ // Load from private sources (dev mode) or compiled (prod mode)
3
+ try {
4
+ module.exports = require('../../../dist/lib/m/ultra-scalping.jsc');
5
+ } catch (e) {
6
+ module.exports = require('../../../private/strategies/ultra-scalping');
7
+ }
@@ -8,7 +8,7 @@ const ora = require('ora');
8
8
  const { connections } = require('../services');
9
9
  const { RithmicService } = require('../services/rithmic');
10
10
  const { PROPFIRM_CHOICES } = require('../config');
11
- const { getLogoWidth, centerText, prepareStdin, displayBanner , clearScreen } = require('../ui');
11
+ const { getLogoWidth, centerText, prepareStdin } = require('../ui');
12
12
  const { validateUsername, validatePassword } = require('../security');
13
13
  const { prompts } = require('../utils');
14
14
 
@@ -18,16 +18,16 @@ const { prompts } = require('../utils');
18
18
  const loginPrompt = async (propfirmName) => {
19
19
  prepareStdin();
20
20
  console.log();
21
- console.log(chalk.cyan(`CONNECTING TO ${propfirmName.toUpperCase()}...`));
21
+ console.log(chalk.cyan(`Connecting to ${propfirmName}...`));
22
22
  console.log();
23
23
 
24
- const username = await prompts.textInput('USERNAME:', '', (input) => {
25
- try { validateUsername(input); return undefined; } catch (e) { return e.message.toUpperCase(); }
24
+ const username = await prompts.textInput('Username:', '', (input) => {
25
+ try { validateUsername(input); return undefined; } catch (e) { return e.message; }
26
26
  });
27
27
  if (!username) return null;
28
28
 
29
- const pwd = await prompts.passwordInput('PASSWORD:', (input) => {
30
- try { validatePassword(input); return undefined; } catch (e) { return e.message.toUpperCase(); }
29
+ const pwd = await prompts.passwordInput('Password:', (input) => {
30
+ try { validatePassword(input); return undefined; } catch (e) { return e.message; }
31
31
  });
32
32
  if (!pwd) return null;
33
33
 
@@ -38,10 +38,6 @@ const loginPrompt = async (propfirmName) => {
38
38
  * Rithmic menu - Main connection menu
39
39
  */
40
40
  const rithmicMenu = async () => {
41
- // Clear screen and show banner
42
- clearScreen();
43
- displayBanner();
44
-
45
41
  const propfirms = PROPFIRM_CHOICES;
46
42
  const boxWidth = getLogoWidth();
47
43
  const innerWidth = boxWidth - 2;
@@ -50,6 +46,7 @@ const rithmicMenu = async () => {
50
46
 
51
47
  const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.value, name: pf.name }));
52
48
 
49
+ console.log();
53
50
  console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
54
51
  console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
55
52
  console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
@@ -74,10 +71,10 @@ const rithmicMenu = async () => {
74
71
  }
75
72
 
76
73
  console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
77
- console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
74
+ console.log(chalk.cyan('║') + chalk.red(centerText('[X] Exit', innerWidth)) + chalk.cyan('║'));
78
75
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
79
76
 
80
- const input = await prompts.textInput(chalk.cyan('SELECT NUMBER (OR X):'));
77
+ const input = await prompts.textInput(chalk.cyan('Select number (or X):'));
81
78
  if (!input || input.toLowerCase() === 'x') return null;
82
79
 
83
80
  const action = parseInt(input);
@@ -87,26 +84,26 @@ const rithmicMenu = async () => {
87
84
  const credentials = await loginPrompt(selectedPropfirm.name);
88
85
  if (!credentials) return null;
89
86
 
90
- const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
87
+ const spinner = ora({ text: 'Connecting to Rithmic...', color: 'yellow' }).start();
91
88
 
92
89
  try {
93
90
  const service = new RithmicService(selectedPropfirm.key);
94
91
  const result = await service.login(credentials.username, credentials.password);
95
92
 
96
93
  if (result.success) {
97
- spinner.text = 'FETCHING ACCOUNTS...';
94
+ spinner.text = 'Fetching accounts...';
98
95
  const accResult = await service.getTradingAccounts();
99
96
  connections.add('rithmic', service, service.propfirm.name);
100
- spinner.succeed(`CONNECTED TO ${service.propfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
97
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
101
98
  await new Promise(r => setTimeout(r, 1500));
102
99
  return service;
103
100
  } else {
104
- spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
101
+ spinner.fail(result.error || 'Authentication failed');
105
102
  await new Promise(r => setTimeout(r, 2000));
106
103
  return null;
107
104
  }
108
105
  } catch (error) {
109
- spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
106
+ spinner.fail(`Connection error: ${error.message}`);
110
107
  await new Promise(r => setTimeout(r, 2000));
111
108
  return null;
112
109
  }