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
|
@@ -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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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:
|
|
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:
|
|
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
|
|
418
|
+
accounts,
|
|
398
419
|
connectedAt: conn.connectedAt,
|
|
399
420
|
propfirm: conn.service?.propfirm?.name || key
|
|
400
421
|
});
|