hedgequantx 2.9.163 → 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.163",
3
+ "version": "2.9.164",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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
 
@@ -383,6 +383,22 @@ class RithmicBrokerDaemon {
383
383
  return { type: 'credentials', payload: creds, requestId };
384
384
  }
385
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
+
386
402
  /**
387
403
  * Save state including accounts (for reconnection without API calls)
388
404
  * CRITICAL: This state allows reconnection without hitting Rithmic's 2000 GetAccounts limit
@@ -391,10 +407,15 @@ class RithmicBrokerDaemon {
391
407
  const state = { connections: [], savedAt: new Date().toISOString() };
392
408
  for (const [key, conn] of this.connections) {
393
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
+
394
415
  state.connections.push({
395
416
  propfirmKey: key,
396
417
  credentials: conn.credentials,
397
- accounts: conn.accounts || [], // Save accounts to avoid fetchAccounts on restore
418
+ accounts,
398
419
  connectedAt: conn.connectedAt,
399
420
  propfirm: conn.service?.propfirm?.name || key
400
421
  });