hedgequantx 2.9.182 → 2.9.183

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.182",
3
+ "version": "2.9.183",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -271,31 +271,18 @@ const run = async () => {
271
271
  const credentials = await loginPrompt(selectedPropfirm.name);
272
272
 
273
273
  if (credentials) {
274
- const spinner = ora({ text: 'STARTING BROKER DAEMON...', color: 'yellow' }).start();
274
+ const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
275
275
  try {
276
- // Use BrokerClient to go through daemon (persists connections)
277
- const { RithmicBrokerClient, manager: brokerManager } = require('./services/rithmic-broker');
276
+ // Direct connection to Rithmic (no daemon)
277
+ const { RithmicService } = require('./services/rithmic');
278
278
 
279
- // Ensure daemon is running
280
- const daemonResult = await brokerManager.ensureRunning();
281
- if (!daemonResult.success) {
282
- spinner.fail('FAILED TO START BROKER DAEMON');
283
- console.log(chalk.yellow(` → ${daemonResult.error}`));
284
- await new Promise(r => setTimeout(r, 3000));
285
- continue;
286
- }
287
-
288
- spinner.text = 'CONNECTING TO RITHMIC...';
289
- const client = new RithmicBrokerClient(selectedPropfirm.key);
290
- const result = await client.login(credentials.username, credentials.password);
279
+ const service = new RithmicService(selectedPropfirm.key);
280
+ const result = await service.login(credentials.username, credentials.password);
291
281
 
292
282
  if (result.success) {
293
- spinner.text = 'FETCHING ACCOUNTS...';
294
- const accResult = await client.getTradingAccounts();
295
- client.accounts = accResult.accounts || [];
296
- connections.add('rithmic', client, selectedPropfirm.name);
297
- spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
298
- currentService = client;
283
+ connections.add('rithmic', service, selectedPropfirm.name);
284
+ spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
285
+ currentService = service;
299
286
  await refreshStats();
300
287
  await new Promise(r => setTimeout(r, 1500));
301
288
  } else {
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Connection Menus - Rithmic Only
2
+ * Connection Menus - Direct Rithmic Connection
3
3
  */
4
4
 
5
5
  const chalk = require('chalk');
6
6
  const ora = require('ora');
7
7
 
8
8
  const { connections } = require('../services');
9
- const { RithmicBrokerClient, manager: brokerManager } = require('../services/rithmic-broker');
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, displayBanner, clearScreen } = require('../ui');
12
12
  const { validateUsername, validatePassword } = require('../security');
13
13
  const { prompts } = require('../utils');
14
14
 
@@ -38,7 +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
41
  clearScreen();
43
42
  displayBanner();
44
43
 
@@ -87,29 +86,18 @@ const rithmicMenu = async () => {
87
86
  const credentials = await loginPrompt(selectedPropfirm.name);
88
87
  if (!credentials) return null;
89
88
 
90
- const spinner = ora({ text: 'STARTING BROKER DAEMON...', color: 'yellow' }).start();
89
+ const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
91
90
 
92
91
  try {
93
- // Ensure broker daemon is running
94
- const daemonResult = await brokerManager.ensureRunning();
95
- if (!daemonResult.success) {
96
- spinner.fail('FAILED TO START BROKER DAEMON');
97
- console.log(chalk.yellow(` → ${daemonResult.error}`));
98
- await new Promise(r => setTimeout(r, 3000));
99
- return null;
100
- }
101
-
102
- spinner.text = 'CONNECTING TO RITHMIC...';
103
- const client = new RithmicBrokerClient(selectedPropfirm.key);
104
- const result = await client.login(credentials.username, credentials.password);
92
+ // Direct connection to Rithmic (no daemon)
93
+ const service = new RithmicService(selectedPropfirm.key);
94
+ const result = await service.login(credentials.username, credentials.password);
105
95
 
106
96
  if (result.success) {
107
- spinner.text = 'FETCHING ACCOUNTS...';
108
- const accResult = await client.getTradingAccounts();
109
- connections.add('rithmic', client, client.propfirm.name || selectedPropfirm.name);
110
- spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
97
+ connections.add('rithmic', service, selectedPropfirm.name);
98
+ spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
111
99
  await new Promise(r => setTimeout(r, 1500));
112
- return client;
100
+ return service;
113
101
  } else {
114
102
  // Detailed error messages for common Rithmic issues
115
103
  const err = (result.error || '').toLowerCase();
@@ -1,13 +1,15 @@
1
1
  /**
2
- * @fileoverview Secure session management - Rithmic Only
2
+ * @fileoverview Secure session management - Direct Rithmic Connection
3
3
  * @module services/session
4
+ *
5
+ * NO DAEMON - Direct connection to Rithmic API
4
6
  */
5
7
 
6
8
  const fs = require('fs');
7
9
  const path = require('path');
8
10
  const os = require('os');
9
11
  const crypto = require('crypto');
10
- const { encrypt, decrypt, maskSensitive } = require('../security/encryption');
12
+ const { encrypt, decrypt } = require('../security/encryption');
11
13
  const { SECURITY } = require('../config/settings');
12
14
  const { logger } = require('../utils/logger');
13
15
 
@@ -23,7 +25,6 @@ const storage = {
23
25
  _ensureDir() {
24
26
  if (!fs.existsSync(SESSION_DIR)) {
25
27
  fs.mkdirSync(SESSION_DIR, { recursive: true, mode: SECURITY.DIR_PERMISSIONS });
26
- log.debug('Created session directory');
27
28
  }
28
29
  },
29
30
 
@@ -33,7 +34,6 @@ const storage = {
33
34
  const data = JSON.stringify(sessions);
34
35
  const encrypted = encrypt(data);
35
36
  fs.writeFileSync(SESSION_FILE, encrypted, { mode: SECURITY.FILE_PERMISSIONS });
36
- log.debug('Session saved', { count: sessions.length });
37
37
  return true;
38
38
  } catch (err) {
39
39
  log.error('Failed to save session', { error: err.message });
@@ -51,14 +51,11 @@ const storage = {
51
51
  const decrypted = decrypt(encrypted);
52
52
 
53
53
  if (!decrypted) {
54
- log.warn('Session decryption failed - clearing');
55
54
  this.clear();
56
55
  return [];
57
56
  }
58
57
 
59
- const sessions = JSON.parse(decrypted);
60
- log.debug('Session loaded', { count: sessions.length });
61
- return sessions;
58
+ return JSON.parse(decrypted);
62
59
  } catch (err) {
63
60
  log.error('Failed to load session', { error: err.message });
64
61
  this.clear();
@@ -74,7 +71,6 @@ const storage = {
74
71
  fs.writeFileSync(SESSION_FILE, crypto.randomBytes(size));
75
72
  }
76
73
  fs.unlinkSync(SESSION_FILE);
77
- log.debug('Session cleared securely');
78
74
  }
79
75
  return true;
80
76
  } catch (err) {
@@ -84,27 +80,25 @@ const storage = {
84
80
  },
85
81
  };
86
82
 
87
- // Lazy load services to avoid circular dependencies
88
- let RithmicBrokerClient, brokerManager, RithmicService;
89
- const loadServices = () => {
90
- if (!RithmicBrokerClient) {
91
- ({ RithmicBrokerClient, manager: brokerManager } = require('./rithmic-broker'));
92
- }
83
+ // Lazy load RithmicService
84
+ let RithmicService;
85
+ const loadRithmicService = () => {
93
86
  if (!RithmicService) {
94
87
  ({ RithmicService } = require('./rithmic'));
95
88
  }
89
+ return RithmicService;
96
90
  };
97
91
 
98
- // Direct mode flag - bypass daemon completely
99
- const DIRECT_MODE = process.env.HQX_DIRECT === '1' || true; // Default to direct for now
100
-
101
92
  /**
102
- * Multi-connection manager (Rithmic only)
93
+ * Multi-connection manager - Direct Rithmic connections
103
94
  */
104
95
  const connections = {
105
96
  /** @type {Array<{type: string, service: Object, propfirm: string, propfirmKey: string, connectedAt: Date}>} */
106
97
  services: [],
107
98
 
99
+ /**
100
+ * Add a new connection
101
+ */
108
102
  add(type, service, propfirm = null) {
109
103
  this.services.push({
110
104
  type,
@@ -118,7 +112,7 @@ const connections = {
118
112
  },
119
113
 
120
114
  /**
121
- * Sanitize account data to prevent corrupted data from being saved
115
+ * Sanitize account data
122
116
  */
123
117
  _sanitizeAccount(acc) {
124
118
  if (!acc || typeof acc !== 'object' || !acc.accountId) return null;
@@ -130,14 +124,14 @@ const connections = {
130
124
  };
131
125
  },
132
126
 
127
+ /**
128
+ * Save sessions to encrypted storage
129
+ */
133
130
  saveToStorage() {
134
- // Load existing sessions to preserve AI agents
135
131
  const existingSessions = storage.load();
136
132
  const aiSessions = existingSessions.filter(s => s.type === 'ai');
137
133
 
138
- // Build Rithmic sessions - INCLUDE accounts to avoid Rithmic API limit on restore
139
134
  const rithmicSessions = this.services.map(conn => {
140
- // Sanitize accounts to prevent corrupted data
141
135
  const rawAccounts = conn.service.accounts || [];
142
136
  const accounts = rawAccounts.map(a => this._sanitizeAccount(a)).filter(Boolean);
143
137
 
@@ -146,80 +140,17 @@ const connections = {
146
140
  propfirm: conn.propfirm,
147
141
  propfirmKey: conn.service.propfirmKey || conn.propfirmKey,
148
142
  credentials: conn.service.credentials,
149
- accounts, // CRITICAL: Cache sanitized accounts to avoid 2000 GetAccounts limit
143
+ accounts,
150
144
  };
151
145
  });
152
146
 
153
- // Merge: AI sessions + Rithmic sessions
154
147
  storage.save([...aiSessions, ...rithmicSessions]);
155
- log.debug('Session saved', { rithmicCount: rithmicSessions.length, hasAccounts: rithmicSessions.some(s => s.accounts?.length > 0) });
156
148
  },
157
149
 
150
+ /**
151
+ * Restore sessions from storage - Direct connection to Rithmic
152
+ */
158
153
  async restoreFromStorage() {
159
- loadServices();
160
-
161
- // DIRECT MODE: Bypass daemon, connect directly to Rithmic
162
- if (DIRECT_MODE) {
163
- log.info('Direct mode enabled - bypassing daemon');
164
-
165
- const sessions = storage.load();
166
- const rithmicSessions = sessions.filter(s => s.type === 'rithmic');
167
-
168
- if (!rithmicSessions.length) {
169
- return false;
170
- }
171
-
172
- for (const session of rithmicSessions) {
173
- try {
174
- await this._restoreSessionDirect(session);
175
- } catch (err) {
176
- log.warn('Failed to restore session direct', { error: err.message });
177
- }
178
- }
179
-
180
- return this.services.length > 0;
181
- }
182
-
183
- // Check if daemon is already running with active connections
184
- const daemonStatus = await brokerManager.getStatus();
185
-
186
- if (daemonStatus.running && daemonStatus.connections?.length > 0) {
187
- // Daemon has active connections - just create clients (NO API calls)
188
- log.info('Daemon active, restoring from broker', { connections: daemonStatus.connections.length });
189
-
190
- for (const conn of daemonStatus.connections) {
191
- const client = new RithmicBrokerClient(conn.propfirmKey);
192
- await client.connect();
193
-
194
- // Get accounts from daemon cache
195
- const accountsResult = await client.getTradingAccounts();
196
- client.accounts = accountsResult.accounts || [];
197
-
198
- // Cache credentials locally for sync access (fetch from daemon)
199
- try {
200
- const creds = await client.getRithmicCredentialsAsync();
201
- if (creds && creds.userId && creds.password) {
202
- client.credentials = { username: creds.userId, password: creds.password };
203
- client.propfirm = { name: conn.propfirmKey, systemName: creds.systemName, gateway: creds.gateway };
204
- }
205
- } catch (e) {
206
- log.warn('Failed to cache credentials', { propfirm: conn.propfirmKey, error: e.message });
207
- }
208
-
209
- this.services.push({
210
- type: 'rithmic',
211
- service: client,
212
- propfirm: conn.propfirm,
213
- propfirmKey: conn.propfirmKey,
214
- connectedAt: new Date(conn.connectedAt),
215
- });
216
- log.debug('Restored from broker', { propfirm: conn.propfirmKey, hasCreds: !!client.credentials });
217
- }
218
-
219
- return this.services.length > 0;
220
- }
221
-
222
- // Daemon not running or no connections - check local storage
223
154
  const sessions = storage.load();
224
155
  const rithmicSessions = sessions.filter(s => s.type === 'rithmic');
225
156
 
@@ -227,87 +158,69 @@ const connections = {
227
158
  return false;
228
159
  }
229
160
 
230
- log.info('Restoring sessions via broker', { count: rithmicSessions.length });
161
+ log.info('Restoring sessions', { count: rithmicSessions.length });
231
162
 
232
163
  for (const session of rithmicSessions) {
233
164
  try {
234
165
  await this._restoreSession(session);
235
166
  } catch (err) {
236
- log.warn('Failed to restore session', { type: session.type, error: err.message });
167
+ log.warn('Failed to restore session', { error: err.message });
237
168
  }
238
169
  }
239
170
 
240
171
  return this.services.length > 0;
241
172
  },
242
-
173
+
243
174
  /**
244
- * Restore session using direct RithmicService (no daemon)
175
+ * Restore a single session using direct RithmicService
245
176
  */
246
- async _restoreSessionDirect(session) {
177
+ async _restoreSession(session) {
247
178
  const { type, propfirm, propfirmKey } = session;
248
179
 
249
- if (type === 'rithmic' && session.credentials) {
250
- const service = new RithmicService(propfirmKey || 'apex_rithmic');
251
-
252
- // Validate cached accounts
253
- let validAccounts = null;
254
- if (session.accounts && Array.isArray(session.accounts)) {
255
- validAccounts = session.accounts
256
- .map(a => this._sanitizeAccount(a))
257
- .filter(Boolean);
258
- if (validAccounts.length === 0) validAccounts = null;
259
- }
260
-
261
- // Login with cached accounts to avoid API limit
262
- const loginOptions = validAccounts ? { skipFetchAccounts: true, cachedAccounts: validAccounts } : {};
263
- const result = await service.login(session.credentials.username, session.credentials.password, loginOptions);
264
-
265
- if (result.success) {
266
- this.services.push({
267
- type,
268
- service,
269
- propfirm,
270
- propfirmKey,
271
- connectedAt: new Date(),
272
- });
273
- log.info('Direct session restored', { propfirm, accounts: service.accounts?.length || 0 });
274
- }
180
+ if (type !== 'rithmic' || !session.credentials) {
181
+ return;
275
182
  }
276
- },
277
-
278
- async _restoreSession(session) {
279
- const { type, propfirm, propfirmKey } = session;
280
183
 
281
- // Use broker client (daemon handles persistence)
282
- if (type === 'rithmic' && session.credentials) {
283
- const client = new RithmicBrokerClient(propfirmKey || 'apex_rithmic');
284
-
285
- // Validate cached accounts before using
286
- let validAccounts = null;
287
- if (session.accounts && Array.isArray(session.accounts)) {
288
- validAccounts = session.accounts
289
- .map(a => this._sanitizeAccount(a))
290
- .filter(Boolean);
291
- if (validAccounts.length === 0) validAccounts = null;
292
- }
293
-
294
- // CRITICAL: Pass validated cached accounts to avoid Rithmic's 2000 GetAccounts limit
295
- const loginOptions = validAccounts ? { cachedAccounts: validAccounts } : {};
296
- const result = await client.login(session.credentials.username, session.credentials.password, loginOptions);
297
-
298
- if (result.success) {
299
- this.services.push({
300
- type,
301
- service: client,
302
- propfirm,
303
- propfirmKey,
304
- connectedAt: new Date(),
305
- });
306
- log.debug('Rithmic session restored via broker', { hasCachedAccounts: !!validAccounts, accountCount: validAccounts?.length || 0 });
307
- }
184
+ const Service = loadRithmicService();
185
+ const service = new Service(propfirmKey || 'apex_rithmic');
186
+
187
+ // Validate cached accounts
188
+ let validAccounts = null;
189
+ if (session.accounts && Array.isArray(session.accounts)) {
190
+ validAccounts = session.accounts
191
+ .map(a => this._sanitizeAccount(a))
192
+ .filter(Boolean);
193
+ if (validAccounts.length === 0) validAccounts = null;
194
+ }
195
+
196
+ // Login with cached accounts to avoid Rithmic API limit
197
+ const loginOptions = validAccounts
198
+ ? { skipFetchAccounts: true, cachedAccounts: validAccounts }
199
+ : {};
200
+
201
+ const result = await service.login(
202
+ session.credentials.username,
203
+ session.credentials.password,
204
+ loginOptions
205
+ );
206
+
207
+ if (result.success) {
208
+ this.services.push({
209
+ type,
210
+ service,
211
+ propfirm,
212
+ propfirmKey,
213
+ connectedAt: new Date(),
214
+ });
215
+ log.info('Session restored', { propfirm, accounts: service.accounts?.length || 0 });
216
+ } else {
217
+ log.warn('Session restore failed', { propfirm, error: result.error });
308
218
  }
309
219
  },
310
220
 
221
+ /**
222
+ * Remove a connection by index
223
+ */
311
224
  remove(index) {
312
225
  if (index < 0 || index >= this.services.length) return;
313
226
 
@@ -342,6 +255,9 @@ const connections = {
342
255
  return this.services.length;
343
256
  },
344
257
 
258
+ /**
259
+ * Get all accounts from all connections
260
+ */
345
261
  async getAllAccounts() {
346
262
  const allAccounts = [];
347
263
 
@@ -367,6 +283,9 @@ const connections = {
367
283
  return allAccounts;
368
284
  },
369
285
 
286
+ /**
287
+ * Get service for a specific account
288
+ */
370
289
  getServiceForAccount(accountId) {
371
290
  for (const conn of this.services) {
372
291
  if (!conn.service?.accounts) continue;
@@ -386,22 +305,14 @@ const connections = {
386
305
  return this.services.length > 0;
387
306
  },
388
307
 
308
+ /**
309
+ * Disconnect all connections
310
+ */
389
311
  async disconnectAll() {
390
- loadServices();
391
-
392
- // Stop the broker daemon (closes all Rithmic connections)
393
- try {
394
- await brokerManager.stop();
395
- log.info('Broker daemon stopped');
396
- } catch (err) {
397
- log.warn('Broker stop failed', { error: err.message });
398
- }
399
-
400
- // Disconnect local clients
401
312
  for (const conn of this.services) {
402
313
  try {
403
314
  if (conn.service?.disconnect) {
404
- conn.service.disconnect();
315
+ await conn.service.disconnect();
405
316
  }
406
317
  } catch (err) {
407
318
  log.warn('Disconnect failed', { type: conn.type, error: err.message });