hedgequantx 2.9.162 → 2.9.164

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.162",
3
+ "version": "2.9.164",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -37,15 +37,20 @@ class RithmicBrokerClient extends EventEmitter {
37
37
  // Ensure daemon is running
38
38
  const daemonStatus = await manager.ensureRunning();
39
39
  if (!daemonStatus.success) {
40
- return { success: false, error: daemonStatus.error || 'Failed to start daemon' };
40
+ const errorMsg = daemonStatus.error || 'Failed to start daemon';
41
+ return { success: false, error: `Daemon error: ${errorMsg}` };
41
42
  }
42
43
 
43
44
  return new Promise((resolve) => {
44
- this.ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
45
+ try {
46
+ this.ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
47
+ } catch (err) {
48
+ return resolve({ success: false, error: `WebSocket create error: ${err.message}` });
49
+ }
45
50
 
46
51
  const timeout = setTimeout(() => {
47
52
  this.ws?.terminate();
48
- resolve({ success: false, error: 'Connection timeout' });
53
+ resolve({ success: false, error: `Connection timeout (port ${BROKER_PORT})` });
49
54
  }, 5000);
50
55
 
51
56
  this.ws.on('open', () => {
@@ -64,7 +69,7 @@ class RithmicBrokerClient extends EventEmitter {
64
69
  this.ws.on('error', (err) => {
65
70
  clearTimeout(timeout);
66
71
  this.connected = false;
67
- resolve({ success: false, error: err.message });
72
+ resolve({ success: false, error: `Daemon connection failed: ${err.message}` });
68
73
  });
69
74
  });
70
75
  }
@@ -240,6 +240,22 @@ class ReconnectManager {
240
240
  this.reconnectState.set(propfirmKey, state);
241
241
  }
242
242
 
243
+ /**
244
+ * Validate cached accounts - ensure all fields are proper strings
245
+ */
246
+ _validateAccounts(accounts) {
247
+ if (!Array.isArray(accounts)) return [];
248
+
249
+ return accounts.filter(acc => {
250
+ // Must have accountId as string
251
+ if (!acc || typeof acc.accountId !== 'string') return false;
252
+ // fcmId and ibId should be strings if present
253
+ if (acc.fcmId && typeof acc.fcmId !== 'string') return false;
254
+ if (acc.ibId && typeof acc.ibId !== 'string') return false;
255
+ return true;
256
+ });
257
+ }
258
+
243
259
  /**
244
260
  * Restore connections from state with retry logic
245
261
  */
@@ -254,28 +270,51 @@ class ReconnectManager {
254
270
 
255
271
  this.log('INFO', 'Restoring connection', { propfirm: saved.propfirmKey });
256
272
 
273
+ // Validate cached accounts to prevent crashes from corrupted data
274
+ const validAccounts = this._validateAccounts(saved.accounts);
275
+ if (saved.accounts?.length && validAccounts.length !== saved.accounts.length) {
276
+ this.log('WARN', 'Some cached accounts invalid, will re-fetch', {
277
+ propfirm: saved.propfirmKey,
278
+ original: saved.accounts?.length || 0,
279
+ valid: validAccounts.length
280
+ });
281
+ }
282
+
257
283
  let success = false;
258
284
  let attempts = 0;
259
285
 
260
286
  while (!success && attempts < RECONNECT_CONFIG.RESTORE_MAX_ATTEMPTS) {
261
287
  attempts++;
262
288
 
263
- const result = await this.daemon._handleLogin({
264
- ...saved.credentials,
265
- propfirmKey: saved.propfirmKey,
266
- cachedAccounts: saved.accounts // Use cached accounts!
267
- }, null);
268
-
269
- if (result.payload?.success) {
270
- success = true;
271
- this.log('INFO', 'Connection restored', { propfirm: saved.propfirmKey });
272
- } else {
273
- this.log('WARN', 'Restore attempt failed', {
274
- propfirm: saved.propfirmKey,
289
+ try {
290
+ const result = await this.daemon._handleLogin({
291
+ ...saved.credentials,
292
+ propfirmKey: saved.propfirmKey,
293
+ // Only use cached accounts if they are valid, otherwise re-fetch
294
+ cachedAccounts: validAccounts.length > 0 ? validAccounts : null
295
+ }, null);
296
+
297
+ if (result.payload?.success) {
298
+ success = true;
299
+ this.log('INFO', 'Connection restored', { propfirm: saved.propfirmKey });
300
+ } else {
301
+ this.log('WARN', 'Restore attempt failed', {
302
+ propfirm: saved.propfirmKey,
303
+ attempt: attempts,
304
+ error: result.payload?.error || result.error
305
+ });
306
+
307
+ if (attempts < RECONNECT_CONFIG.RESTORE_MAX_ATTEMPTS) {
308
+ await new Promise(r => setTimeout(r, RECONNECT_CONFIG.RESTORE_RETRY_DELAY));
309
+ }
310
+ }
311
+ } catch (e) {
312
+ this.log('ERROR', 'Restore attempt error', {
313
+ propfirm: saved.propfirmKey,
275
314
  attempt: attempts,
276
- error: result.payload?.error || result.error
315
+ error: e.message
277
316
  });
278
-
317
+
279
318
  if (attempts < RECONNECT_CONFIG.RESTORE_MAX_ATTEMPTS) {
280
319
  await new Promise(r => setTimeout(r, RECONNECT_CONFIG.RESTORE_RETRY_DELAY));
281
320
  }
@@ -288,13 +327,15 @@ class ReconnectManager {
288
327
  service: null,
289
328
  credentials: saved.credentials,
290
329
  connectedAt: null,
291
- accounts: saved.accounts || [],
330
+ accounts: validAccounts,
292
331
  status: 'disconnected',
293
332
  });
294
333
  }
295
334
  }
296
335
  } catch (e) {
297
336
  this.log('ERROR', 'Restore failed', { error: e.message });
337
+ // Delete corrupted state file
338
+ try { fs.unlinkSync(stateFile); } catch (e2) { /* ignore */ }
298
339
  }
299
340
  }
300
341
 
@@ -67,16 +67,35 @@ class RithmicBrokerDaemon {
67
67
 
68
68
  if (!fs.existsSync(BROKER_DIR)) fs.mkdirSync(BROKER_DIR, { recursive: true });
69
69
  fs.writeFileSync(PID_FILE, String(process.pid));
70
+ log('INFO', 'Starting daemon...', { pid: process.pid });
70
71
 
71
72
  // Restore connections from state (with cached accounts - no API spam)
72
- await this.reconnectManager.restoreConnections(STATE_FILE);
73
+ try {
74
+ await this.reconnectManager.restoreConnections(STATE_FILE);
75
+ } catch (e) {
76
+ log('WARN', 'Failed to restore connections', { error: e.message });
77
+ }
78
+
79
+ // Create WebSocket server with proper error handling
80
+ try {
81
+ this.wss = new WebSocket.Server({ port: BROKER_PORT, host: '127.0.0.1' });
82
+ } catch (e) {
83
+ log('ERROR', 'Failed to create WebSocket server', { error: e.message, port: BROKER_PORT });
84
+ throw e;
85
+ }
86
+
87
+ // Wait for server to be listening
88
+ await new Promise((resolve, reject) => {
89
+ const timeout = setTimeout(() => reject(new Error('WSS listen timeout')), 5000);
90
+ this.wss.on('listening', () => { clearTimeout(timeout); resolve(); });
91
+ this.wss.on('error', (err) => { clearTimeout(timeout); reject(err); });
92
+ });
73
93
 
74
- this.wss = new WebSocket.Server({ port: BROKER_PORT, host: '127.0.0.1' });
75
94
  this.wss.on('connection', (ws) => this._handleClient(ws));
76
95
  this.wss.on('error', (err) => log('ERROR', 'WSS error', { error: err.message }));
77
96
 
78
97
  this.running = true;
79
- log('INFO', 'Daemon started', { pid: process.pid, port: BROKER_PORT });
98
+ log('INFO', 'Daemon started successfully', { pid: process.pid, port: BROKER_PORT });
80
99
 
81
100
  // Save state on ANY termination signal
82
101
  const gracefulShutdown = (signal) => {
@@ -364,6 +383,22 @@ class RithmicBrokerDaemon {
364
383
  return { type: 'credentials', payload: creds, requestId };
365
384
  }
366
385
 
386
+ /**
387
+ * Sanitize account for safe serialization - ensure all fields are proper types
388
+ */
389
+ _sanitizeAccount(acc) {
390
+ if (!acc || typeof acc !== 'object') return null;
391
+ if (!acc.accountId) return null;
392
+
393
+ return {
394
+ accountId: String(acc.accountId),
395
+ fcmId: acc.fcmId ? String(acc.fcmId) : undefined,
396
+ ibId: acc.ibId ? String(acc.ibId) : undefined,
397
+ accountName: acc.accountName ? String(acc.accountName) : undefined,
398
+ currency: acc.currency ? String(acc.currency) : undefined,
399
+ };
400
+ }
401
+
367
402
  /**
368
403
  * Save state including accounts (for reconnection without API calls)
369
404
  * CRITICAL: This state allows reconnection without hitting Rithmic's 2000 GetAccounts limit
@@ -372,10 +407,15 @@ class RithmicBrokerDaemon {
372
407
  const state = { connections: [], savedAt: new Date().toISOString() };
373
408
  for (const [key, conn] of this.connections) {
374
409
  if (conn.credentials) {
410
+ // Sanitize accounts to prevent corrupted data
411
+ const accounts = (conn.accounts || [])
412
+ .map(a => this._sanitizeAccount(a))
413
+ .filter(Boolean);
414
+
375
415
  state.connections.push({
376
416
  propfirmKey: key,
377
417
  credentials: conn.credentials,
378
- accounts: conn.accounts || [], // Save accounts to avoid fetchAccounts on restore
418
+ accounts,
379
419
  connectedAt: conn.connectedAt,
380
420
  propfirm: conn.service?.propfirm?.name || key
381
421
  });
@@ -392,8 +432,31 @@ class RithmicBrokerDaemon {
392
432
 
393
433
  // Main entry point
394
434
  if (require.main === module) {
395
- const daemon = new RithmicBrokerDaemon();
396
- daemon.start().catch((e) => { console.error('Daemon failed:', e.message); process.exit(1); });
435
+ // Ensure log directory exists early
436
+ if (!fs.existsSync(BROKER_DIR)) {
437
+ fs.mkdirSync(BROKER_DIR, { recursive: true });
438
+ }
439
+
440
+ // Log startup attempt
441
+ const startupLog = (msg) => {
442
+ const ts = new Date().toISOString();
443
+ fs.appendFileSync(LOG_FILE, `[${ts}] [STARTUP] ${msg}\n`);
444
+ };
445
+
446
+ startupLog(`Daemon starting (pid=${process.pid}, node=${process.version})`);
447
+
448
+ try {
449
+ const daemon = new RithmicBrokerDaemon();
450
+ daemon.start().catch((e) => {
451
+ startupLog(`FATAL: start() failed - ${e.message}`);
452
+ console.error('Daemon failed:', e.message);
453
+ process.exit(1);
454
+ });
455
+ } catch (e) {
456
+ startupLog(`FATAL: constructor failed - ${e.message}`);
457
+ console.error('Daemon failed:', e.message);
458
+ process.exit(1);
459
+ }
397
460
  }
398
461
 
399
462
  module.exports = { RithmicBrokerDaemon, BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE, STATE_FILE };
@@ -86,21 +86,29 @@ const start = async () => {
86
86
  child.unref();
87
87
  fs.closeSync(logFd);
88
88
 
89
- // Wait for daemon to start
90
- await new Promise(r => setTimeout(r, 2000));
89
+ // Wait for daemon to start (poll every 500ms, max 5s)
90
+ let attempts = 0;
91
+ const maxAttempts = 10;
92
+ let runStatus = { running: false, pid: null };
91
93
 
92
- const runStatus = await isRunning();
93
- if (runStatus.running) {
94
- return { success: true, error: null, pid: runStatus.pid || child.pid };
95
- } else {
96
- // Read log for error details
97
- let errorDetail = 'Failed to start RithmicBroker daemon';
98
- if (fs.existsSync(LOG_FILE)) {
99
- const log = fs.readFileSync(LOG_FILE, 'utf8').slice(-500);
100
- if (log) errorDetail += `: ${log.split('\n').filter(l => l).pop()}`;
94
+ while (attempts < maxAttempts) {
95
+ await new Promise(r => setTimeout(r, 500));
96
+ runStatus = await isRunning();
97
+ if (runStatus.running) {
98
+ return { success: true, error: null, pid: runStatus.pid || child.pid };
101
99
  }
102
- return { success: false, error: errorDetail, pid: null };
100
+ attempts++;
103
101
  }
102
+
103
+ // Read log for error details
104
+ let errorDetail = 'Daemon failed to start';
105
+ if (fs.existsSync(LOG_FILE)) {
106
+ const logContent = fs.readFileSync(LOG_FILE, 'utf8');
107
+ const lines = logContent.split('\n').filter(l => l.trim());
108
+ const lastLines = lines.slice(-5).join(' | ');
109
+ if (lastLines) errorDetail += ` - Log: ${lastLines}`;
110
+ }
111
+ return { success: false, error: errorDetail, pid: null };
104
112
  } catch (error) {
105
113
  return { success: false, error: error.message, pid: null };
106
114
  }