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
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
}
|