hedgequantx 2.9.181 → 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.
@@ -1,469 +0,0 @@
1
- /**
2
- * RithmicBroker Daemon
3
- *
4
- * Background process that maintains persistent Rithmic connections.
5
- * Survives CLI restarts/updates. Only stops on explicit logout or reboot.
6
- *
7
- * Communication: WebSocket server on port 18765
8
- *
9
- * Key features:
10
- * - Persistent connections (no disconnect on CLI restart)
11
- * - Smart reconnection with rate limiting (max 10/day)
12
- * - Cached accounts (no repeated API calls)
13
- */
14
-
15
- 'use strict';
16
-
17
- const WebSocket = require('ws');
18
- const fs = require('fs');
19
- const path = require('path');
20
- const os = require('os');
21
- const { ReconnectManager } = require('./daemon-reconnect');
22
-
23
- // Paths
24
- const BROKER_DIR = path.join(os.homedir(), '.hqx', 'rithmic-broker');
25
- const PID_FILE = path.join(BROKER_DIR, 'broker.pid');
26
- const LOG_FILE = path.join(BROKER_DIR, 'broker.log');
27
- const STATE_FILE = path.join(BROKER_DIR, 'state.json');
28
- const BROKER_PORT = 18765;
29
-
30
- // Lazy load RithmicService
31
- let RithmicService = null;
32
- const loadRithmicService = () => {
33
- if (!RithmicService) {
34
- ({ RithmicService } = require('../rithmic'));
35
- }
36
- return RithmicService;
37
- };
38
-
39
- // Logger
40
- const log = (level, msg, data = {}) => {
41
- const ts = new Date().toISOString();
42
- const line = `[${ts}] [${level}] ${msg} ${JSON.stringify(data)}\n`;
43
- try { fs.appendFileSync(LOG_FILE, line); } catch (e) { /* ignore */ }
44
- if (process.env.HQX_DEBUG === '1') console.log(`[Broker] [${level}] ${msg}`, data);
45
- };
46
-
47
- /**
48
- * RithmicBroker Daemon Class
49
- */
50
- class RithmicBrokerDaemon {
51
- constructor() {
52
- this.wss = null;
53
- this.clients = new Set();
54
- this.connections = new Map(); // propfirmKey -> { service, credentials, connectedAt, accounts, status }
55
- this.pnlCache = new Map(); // accountId -> { pnl, openPnl, closedPnl, balance, updatedAt }
56
- this.running = false;
57
-
58
- // Reconnection manager (handles health checks & reconnection with rate limiting)
59
- this.reconnectManager = new ReconnectManager(this, log);
60
-
61
- // Expose loadRithmicService for ReconnectManager
62
- this.loadRithmicService = loadRithmicService;
63
- }
64
-
65
- async start() {
66
- if (this.running) return;
67
-
68
- if (!fs.existsSync(BROKER_DIR)) fs.mkdirSync(BROKER_DIR, { recursive: true });
69
- fs.writeFileSync(PID_FILE, String(process.pid));
70
- log('INFO', 'Starting daemon...', { pid: process.pid });
71
-
72
- // Restore connections from state (with cached accounts - no API spam)
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
- });
93
-
94
- this.wss.on('connection', (ws) => this._handleClient(ws));
95
- this.wss.on('error', (err) => log('ERROR', 'WSS error', { error: err.message }));
96
-
97
- this.running = true;
98
- log('INFO', 'Daemon started successfully', { pid: process.pid, port: BROKER_PORT });
99
-
100
- // Save state on ANY termination signal
101
- const gracefulShutdown = (signal) => {
102
- log('WARN', `Received ${signal}, saving state...`);
103
- this._saveState();
104
- this.stop();
105
- };
106
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
107
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
108
- process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
109
- process.on('uncaughtException', (err) => {
110
- log('ERROR', 'Uncaught exception, saving state...', { error: err.message });
111
- this._saveState();
112
- process.exit(1);
113
- });
114
- process.on('unhandledRejection', (err) => {
115
- log('ERROR', 'Unhandled rejection', { error: err?.message || String(err) });
116
- this._saveState();
117
- });
118
-
119
- // Auto-save state every 5s (critical for surviving updates)
120
- setInterval(() => this._saveState(), 5000);
121
-
122
- // Start health check (monitoring + rate-limited reconnection)
123
- this.reconnectManager.startHealthCheck();
124
- }
125
-
126
- async stop() {
127
- log('INFO', 'Daemon stopping...');
128
- this.running = false;
129
-
130
- // Stop health check
131
- this.reconnectManager.stopHealthCheck();
132
-
133
- for (const [key, conn] of this.connections) {
134
- try { if (conn.service?.disconnect) await conn.service.disconnect(); }
135
- catch (e) { log('WARN', 'Disconnect error', { propfirm: key, error: e.message }); }
136
- }
137
- this.connections.clear();
138
-
139
- if (this.wss) {
140
- for (const client of this.clients) client.close(1000, 'Daemon shutting down');
141
- this.wss.close();
142
- }
143
-
144
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
145
- if (fs.existsSync(STATE_FILE)) fs.unlinkSync(STATE_FILE);
146
-
147
- log('INFO', 'Daemon stopped');
148
- process.exit(0);
149
- }
150
-
151
- _handleClient(ws) {
152
- this.clients.add(ws);
153
- log('DEBUG', 'Client connected', { total: this.clients.size });
154
-
155
- ws.on('message', async (data) => {
156
- try {
157
- const msg = JSON.parse(data.toString());
158
- const response = await this._handleMessage(msg);
159
- ws.send(JSON.stringify(response));
160
- } catch (e) {
161
- ws.send(JSON.stringify({ error: e.message, requestId: null }));
162
- }
163
- });
164
-
165
- ws.on('close', () => { this.clients.delete(ws); });
166
- ws.on('error', () => { this.clients.delete(ws); });
167
- }
168
-
169
- async _handleMessage(msg) {
170
- const { type, payload = {}, requestId } = msg;
171
-
172
- const handlers = {
173
- ping: () => ({ type: 'pong', requestId }),
174
- status: () => ({ type: 'status', payload: this._getStatus(), requestId }),
175
- login: () => this._handleLogin(payload, requestId),
176
- logout: () => this._handleLogout(payload, requestId),
177
- getAccounts: () => this._handleGetAccounts(requestId),
178
- getPnL: () => this._handleGetPnL(payload, requestId),
179
- getPositions: () => this._handleGetPositions(payload, requestId),
180
- placeOrder: () => this._handlePlaceOrder(payload, requestId),
181
- cancelOrder: () => this._handleCancelOrder(payload, requestId),
182
- getContracts: () => this._handleGetContracts(payload, requestId),
183
- searchContracts: () => this._handleSearchContracts(payload, requestId),
184
- getRithmicCredentials: () => this._handleGetCredentials(payload, requestId),
185
- };
186
-
187
- if (handlers[type]) {
188
- try { return await handlers[type](); }
189
- catch (e) { return { error: e.message, requestId }; }
190
- }
191
- return { error: `Unknown type: ${type}`, requestId };
192
- }
193
-
194
- _getStatus() {
195
- const conns = [];
196
- for (const [key, conn] of this.connections) {
197
- const isAlive = conn.service?.orderConn?.isConnected &&
198
- conn.service?.orderConn?.connectionState === 'LOGGED_IN';
199
- conns.push({
200
- propfirmKey: key,
201
- propfirm: conn.service?.propfirm?.name || key,
202
- connectedAt: conn.connectedAt,
203
- accountCount: conn.accounts?.length || 0,
204
- status: conn.status || (isAlive ? 'connected' : 'disconnected'),
205
- isAlive,
206
- });
207
- }
208
- return { running: this.running, pid: process.pid, uptime: process.uptime(), connections: conns };
209
- }
210
-
211
- async _handleLogin(payload, requestId) {
212
- const { propfirmKey, username, password, cachedAccounts } = payload;
213
- if (!propfirmKey || !username || !password) {
214
- return { error: 'Missing credentials', requestId };
215
- }
216
-
217
- // Already connected?
218
- if (this.connections.has(propfirmKey)) {
219
- const conn = this.connections.get(propfirmKey);
220
- if (conn.service?.loginInfo) {
221
- return { type: 'loginResult', payload: { success: true, accounts: conn.accounts, alreadyConnected: true }, requestId };
222
- }
223
- }
224
-
225
- const Service = loadRithmicService();
226
- const service = new Service(propfirmKey);
227
-
228
- log('INFO', 'Logging in...', { propfirm: propfirmKey, hasCachedAccounts: !!cachedAccounts });
229
-
230
- // Login with optional cached accounts (skips fetchAccounts API call)
231
- const loginOptions = cachedAccounts ? { skipFetchAccounts: true, cachedAccounts } : {};
232
- const result = await service.login(username, password, loginOptions);
233
-
234
- if (result.success) {
235
- // Use cached accounts if provided, otherwise use result from login
236
- const accounts = cachedAccounts || result.accounts || [];
237
-
238
- this.connections.set(propfirmKey, {
239
- service,
240
- credentials: { username, password },
241
- connectedAt: new Date().toISOString(),
242
- accounts,
243
- status: 'connected',
244
- });
245
-
246
- this._setupPnLUpdates(propfirmKey, service);
247
- this.reconnectManager.setupConnectionMonitoring(propfirmKey, service);
248
- this._saveState();
249
-
250
- log('INFO', 'Login successful', { propfirm: propfirmKey, accounts: accounts.length });
251
- return { type: 'loginResult', payload: { success: true, accounts }, requestId };
252
- }
253
-
254
- log('WARN', 'Login failed', { propfirm: propfirmKey, error: result.error });
255
- return { type: 'loginResult', payload: { success: false, error: result.error }, requestId };
256
- }
257
-
258
- _setupPnLUpdates(propfirmKey, service) {
259
- service.on('pnlUpdate', (pnl) => {
260
- if (pnl.accountId) {
261
- this.pnlCache.set(pnl.accountId, {
262
- pnl: pnl.dayPnl || ((pnl.openPositionPnl || 0) + (pnl.closedPositionPnl || 0)),
263
- openPnl: pnl.openPositionPnl || 0,
264
- closedPnl: pnl.closedPositionPnl || 0,
265
- balance: pnl.accountBalance || 0,
266
- updatedAt: Date.now(),
267
- });
268
- }
269
- this._broadcast({ type: 'pnlUpdate', payload: pnl });
270
- });
271
- service.on('positionUpdate', (pos) => this._broadcast({ type: 'positionUpdate', payload: pos }));
272
- service.on('trade', (trade) => this._broadcast({ type: 'trade', payload: trade }));
273
- }
274
-
275
- _broadcast(msg) {
276
- const data = JSON.stringify(msg);
277
- for (const client of this.clients) {
278
- if (client.readyState === WebSocket.OPEN) {
279
- try { client.send(data); } catch (e) { /* ignore */ }
280
- }
281
- }
282
- }
283
-
284
- async _handleLogout(payload, requestId) {
285
- const { propfirmKey } = payload;
286
- if (propfirmKey) {
287
- const conn = this.connections.get(propfirmKey);
288
- if (conn?.service) { await conn.service.disconnect(); this.connections.delete(propfirmKey); }
289
- } else {
290
- await this.stop();
291
- }
292
- this._saveState();
293
- return { type: 'logoutResult', payload: { success: true }, requestId };
294
- }
295
-
296
- async _handleGetAccounts(requestId) {
297
- const allAccounts = [];
298
- for (const [propfirmKey, conn] of this.connections) {
299
- // Include accounts even if service is temporarily disconnected (from cache)
300
- for (const acc of conn.accounts || []) {
301
- allAccounts.push({
302
- ...acc,
303
- propfirmKey,
304
- propfirm: conn.service?.propfirm?.name || propfirmKey,
305
- connectionStatus: conn.status
306
- });
307
- }
308
- }
309
- return { type: 'accounts', payload: { accounts: allAccounts }, requestId };
310
- }
311
-
312
- _handleGetPnL(payload, requestId) {
313
- const cached = this.pnlCache.get(payload.accountId);
314
- return { type: 'pnl', payload: cached || { pnl: null }, requestId };
315
- }
316
-
317
- async _handleGetPositions(payload, requestId) {
318
- const conn = this.connections.get(payload.propfirmKey);
319
- if (!conn?.service) return { error: 'Not connected', requestId };
320
- return { type: 'positions', payload: await conn.service.getPositions(), requestId };
321
- }
322
-
323
- async _handlePlaceOrder(payload, requestId) {
324
- const conn = this.connections.get(payload.propfirmKey);
325
- if (!conn?.service) return { error: 'Not connected', requestId };
326
- return { type: 'orderResult', payload: await conn.service.placeOrder(payload.orderData), requestId };
327
- }
328
-
329
- async _handleCancelOrder(payload, requestId) {
330
- const conn = this.connections.get(payload.propfirmKey);
331
- if (!conn?.service) return { error: 'Not connected', requestId };
332
- return { type: 'cancelResult', payload: await conn.service.cancelOrder(payload.orderId), requestId };
333
- }
334
-
335
- async _handleGetContracts(payload, requestId) {
336
- const conn = this.connections.get(payload.propfirmKey);
337
- if (!conn?.service) {
338
- log('WARN', 'getContracts: Not connected', { propfirm: payload.propfirmKey, hasConn: !!conn });
339
- return { error: 'Not connected to broker', requestId };
340
- }
341
-
342
- // Log service state for debugging
343
- const hasCredentials = !!conn.service.credentials;
344
- const hasTickerConn = !!conn.service.tickerConn;
345
- const tickerState = conn.service.tickerConn?.connectionState;
346
- log('DEBUG', 'getContracts request', { propfirm: payload.propfirmKey, hasCredentials, hasTickerConn, tickerState });
347
-
348
- try {
349
- const result = await conn.service.getContracts();
350
-
351
- // Log detailed result
352
- const tickerStateAfter = conn.service.tickerConn?.connectionState;
353
- log('DEBUG', 'getContracts result', {
354
- propfirm: payload.propfirmKey,
355
- success: result.success,
356
- count: result.contracts?.length || 0,
357
- source: result.source,
358
- tickerStateAfter,
359
- error: result.error
360
- });
361
-
362
- return { type: 'contracts', payload: result, requestId };
363
- } catch (err) {
364
- log('ERROR', 'getContracts exception', { propfirm: payload.propfirmKey, error: err.message, stack: err.stack?.split('\n')[1] });
365
- return { type: 'contracts', payload: { success: false, error: err.message, contracts: [] }, requestId };
366
- }
367
- }
368
-
369
- async _handleSearchContracts(payload, requestId) {
370
- const conn = this.connections.get(payload.propfirmKey);
371
- if (!conn?.service) return { error: 'Not connected', requestId };
372
- return { type: 'searchResults', payload: await conn.service.searchContracts(payload.searchText), requestId };
373
- }
374
-
375
- _handleGetCredentials(payload, requestId) {
376
- const conn = this.connections.get(payload.propfirmKey);
377
- if (!conn) {
378
- log('WARN', 'getCredentials: propfirm not found', { propfirm: payload.propfirmKey });
379
- return { error: `Propfirm "${payload.propfirmKey}" not connected - run "hqx login"`, requestId };
380
- }
381
- if (!conn.service) {
382
- log('WARN', 'getCredentials: service is null', { propfirm: payload.propfirmKey, status: conn.status });
383
- return { error: `Connection lost for "${payload.propfirmKey}" - run "hqx login"`, requestId };
384
- }
385
- const creds = conn.service.getRithmicCredentials?.();
386
- if (!creds) {
387
- log('WARN', 'getCredentials: credentials null', { propfirm: payload.propfirmKey });
388
- return { error: `Credentials not available for "${payload.propfirmKey}"`, requestId };
389
- }
390
- return { type: 'credentials', payload: creds, requestId };
391
- }
392
-
393
- /**
394
- * Sanitize account for safe serialization - ensure all fields are proper types
395
- */
396
- _sanitizeAccount(acc) {
397
- if (!acc || typeof acc !== 'object') return null;
398
- if (!acc.accountId) return null;
399
-
400
- return {
401
- accountId: String(acc.accountId),
402
- fcmId: acc.fcmId ? String(acc.fcmId) : undefined,
403
- ibId: acc.ibId ? String(acc.ibId) : undefined,
404
- accountName: acc.accountName ? String(acc.accountName) : undefined,
405
- currency: acc.currency ? String(acc.currency) : undefined,
406
- };
407
- }
408
-
409
- /**
410
- * Save state including accounts (for reconnection without API calls)
411
- * CRITICAL: This state allows reconnection without hitting Rithmic's 2000 GetAccounts limit
412
- */
413
- _saveState() {
414
- const state = { connections: [], savedAt: new Date().toISOString() };
415
- for (const [key, conn] of this.connections) {
416
- if (conn.credentials) {
417
- // Sanitize accounts to prevent corrupted data
418
- const accounts = (conn.accounts || [])
419
- .map(a => this._sanitizeAccount(a))
420
- .filter(Boolean);
421
-
422
- state.connections.push({
423
- propfirmKey: key,
424
- credentials: conn.credentials,
425
- accounts,
426
- connectedAt: conn.connectedAt,
427
- propfirm: conn.service?.propfirm?.name || key
428
- });
429
- }
430
- }
431
- try {
432
- fs.writeFileSync(STATE_FILE, JSON.stringify(state));
433
- log('DEBUG', 'State saved', { connections: state.connections.length });
434
- } catch (e) {
435
- log('ERROR', 'Failed to save state', { error: e.message });
436
- }
437
- }
438
- }
439
-
440
- // Main entry point
441
- if (require.main === module) {
442
- // Ensure log directory exists early
443
- if (!fs.existsSync(BROKER_DIR)) {
444
- fs.mkdirSync(BROKER_DIR, { recursive: true });
445
- }
446
-
447
- // Log startup attempt
448
- const startupLog = (msg) => {
449
- const ts = new Date().toISOString();
450
- fs.appendFileSync(LOG_FILE, `[${ts}] [STARTUP] ${msg}\n`);
451
- };
452
-
453
- startupLog(`Daemon starting (pid=${process.pid}, node=${process.version})`);
454
-
455
- try {
456
- const daemon = new RithmicBrokerDaemon();
457
- daemon.start().catch((e) => {
458
- startupLog(`FATAL: start() failed - ${e.message}`);
459
- console.error('Daemon failed:', e.message);
460
- process.exit(1);
461
- });
462
- } catch (e) {
463
- startupLog(`FATAL: constructor failed - ${e.message}`);
464
- console.error('Daemon failed:', e.message);
465
- process.exit(1);
466
- }
467
- }
468
-
469
- module.exports = { RithmicBrokerDaemon, BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE, STATE_FILE };
@@ -1,46 +0,0 @@
1
- /**
2
- * RithmicBroker Service
3
- *
4
- * Persistent Rithmic connection manager.
5
- * Daemon runs in background, survives CLI restarts.
6
- *
7
- * Usage:
8
- * const { RithmicBrokerClient, manager } = require('./rithmic-broker');
9
- *
10
- * // Start daemon if not running
11
- * await manager.ensureRunning();
12
- *
13
- * // Create client (same API as RithmicService)
14
- * const client = new RithmicBrokerClient('apex');
15
- * await client.login(username, password);
16
- *
17
- * // Use like RithmicService
18
- * const accounts = await client.getTradingAccounts();
19
- */
20
-
21
- 'use strict';
22
-
23
- const { RithmicBrokerClient } = require('./client');
24
- const manager = require('./manager');
25
- const { BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE, STATE_FILE } = require('./daemon');
26
-
27
- module.exports = {
28
- // Client class (use instead of RithmicService)
29
- RithmicBrokerClient,
30
-
31
- // Manager functions
32
- manager,
33
- isRunning: manager.isRunning,
34
- start: manager.start,
35
- stop: manager.stop,
36
- getStatus: manager.getStatus,
37
- ensureRunning: manager.ensureRunning,
38
- restart: manager.restart,
39
-
40
- // Constants
41
- BROKER_PORT,
42
- BROKER_DIR,
43
- PID_FILE,
44
- LOG_FILE,
45
- STATE_FILE,
46
- };