hedgequantx 2.9.216 → 2.9.218

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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * @fileoverview Daemon Message Handlers
3
+ * @module services/daemon/handlers
4
+ *
5
+ * Handlers for all daemon IPC messages.
6
+ * Extracted from server.js to keep files under 500 lines.
7
+ *
8
+ * NO MOCK DATA - All data from real Rithmic API
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const { MSG_TYPE } = require('./constants');
14
+ const { createMessage } = require('./protocol');
15
+ const { logger } = require('../../utils/logger');
16
+
17
+ const log = logger.scope('DaemonHandlers');
18
+
19
+ /**
20
+ * Create handlers bound to a daemon server instance
21
+ * @param {Object} daemon - DaemonServer instance
22
+ * @returns {Object} Handler functions
23
+ */
24
+ function createHandlers(daemon) {
25
+
26
+ // ==================== AUTH HANDLERS ====================
27
+
28
+ async function handleLogin(socket, id, data) {
29
+ const { propfirmKey, username, password } = data;
30
+
31
+ if (!propfirmKey || !username || !password) {
32
+ daemon._send(socket, createMessage(MSG_TYPE.LOGIN_RESULT, {
33
+ success: false,
34
+ error: 'Missing credentials',
35
+ }, id));
36
+ return;
37
+ }
38
+
39
+ // Lazy load RithmicService
40
+ const { RithmicService } = require('../rithmic');
41
+
42
+ // Disconnect existing connection if any
43
+ if (daemon.rithmic) {
44
+ try {
45
+ await daemon.rithmic.disconnect();
46
+ } catch (_) {}
47
+ }
48
+
49
+ daemon.rithmic = new RithmicService(propfirmKey);
50
+
51
+ // Set up event forwarding to all clients
52
+ setupRithmicEvents(daemon);
53
+
54
+ const result = await daemon.rithmic.login(username, password);
55
+
56
+ if (result.success) {
57
+ daemon.propfirm = {
58
+ key: propfirmKey,
59
+ name: daemon.rithmic.propfirm.name,
60
+ };
61
+
62
+ // Save session for restore
63
+ const { storage } = require('../session');
64
+ storage.save([{
65
+ type: 'rithmic',
66
+ propfirm: daemon.propfirm.name,
67
+ propfirmKey,
68
+ credentials: { username, password },
69
+ accounts: daemon.rithmic.accounts,
70
+ }]);
71
+
72
+ log.info('Login successful', { propfirm: daemon.propfirm.name, accounts: result.accounts?.length });
73
+ } else {
74
+ daemon.rithmic = null;
75
+ daemon.propfirm = null;
76
+ }
77
+
78
+ daemon._send(socket, createMessage(MSG_TYPE.LOGIN_RESULT, {
79
+ success: result.success,
80
+ error: result.error || null,
81
+ propfirm: daemon.propfirm,
82
+ accounts: result.accounts || [],
83
+ }, id));
84
+ }
85
+
86
+ async function handleRestoreSession(socket, id) {
87
+ const { storage } = require('../session');
88
+ const sessions = storage.load();
89
+ const rithmicSession = sessions.find(s => s.type === 'rithmic' && s.credentials);
90
+
91
+ if (!rithmicSession) {
92
+ daemon._send(socket, createMessage(MSG_TYPE.LOGIN_RESULT, {
93
+ success: false,
94
+ error: 'No saved session',
95
+ }, id));
96
+ return;
97
+ }
98
+
99
+ const { propfirmKey, credentials, accounts } = rithmicSession;
100
+ const { RithmicService } = require('../rithmic');
101
+
102
+ daemon.rithmic = new RithmicService(propfirmKey);
103
+ setupRithmicEvents(daemon);
104
+
105
+ const result = await daemon.rithmic.login(
106
+ credentials.username,
107
+ credentials.password,
108
+ { skipFetchAccounts: !!accounts, cachedAccounts: accounts }
109
+ );
110
+
111
+ if (result.success) {
112
+ daemon.propfirm = {
113
+ key: propfirmKey,
114
+ name: daemon.rithmic.propfirm.name,
115
+ };
116
+ log.info('Session restored', { propfirm: daemon.propfirm.name });
117
+ } else {
118
+ daemon.rithmic = null;
119
+ daemon.propfirm = null;
120
+ }
121
+
122
+ daemon._send(socket, createMessage(MSG_TYPE.LOGIN_RESULT, {
123
+ success: result.success,
124
+ error: result.error || null,
125
+ propfirm: daemon.propfirm,
126
+ accounts: result.accounts || [],
127
+ restored: true,
128
+ }, id));
129
+ }
130
+
131
+ async function handleLogout(socket, id) {
132
+ if (daemon.rithmic) {
133
+ await daemon.rithmic.disconnect();
134
+ daemon.rithmic = null;
135
+ daemon.propfirm = null;
136
+
137
+ const { storage } = require('../session');
138
+ storage.clear();
139
+ }
140
+
141
+ daemon._send(socket, createMessage(MSG_TYPE.STATUS, {
142
+ connected: false,
143
+ logout: true,
144
+ }, id));
145
+ }
146
+
147
+ // ==================== DATA HANDLERS ====================
148
+
149
+ async function handleGetAccounts(socket, id) {
150
+ if (!daemon.rithmic) {
151
+ daemon._send(socket, createMessage(MSG_TYPE.ACCOUNTS, {
152
+ success: false,
153
+ error: 'Not connected',
154
+ accounts: [],
155
+ }, id));
156
+ return;
157
+ }
158
+
159
+ const result = await daemon.rithmic.getTradingAccounts();
160
+ daemon._send(socket, createMessage(MSG_TYPE.ACCOUNTS, result, id));
161
+ }
162
+
163
+ async function handleGetPositions(socket, id) {
164
+ if (!daemon.rithmic) {
165
+ daemon._send(socket, createMessage(MSG_TYPE.POSITIONS, {
166
+ success: false,
167
+ error: 'Not connected',
168
+ positions: [],
169
+ }, id));
170
+ return;
171
+ }
172
+
173
+ const result = await daemon.rithmic.getPositions();
174
+ daemon._send(socket, createMessage(MSG_TYPE.POSITIONS, result, id));
175
+ }
176
+
177
+ async function handleGetOrders(socket, id) {
178
+ if (!daemon.rithmic) {
179
+ daemon._send(socket, createMessage(MSG_TYPE.ORDERS, {
180
+ success: false,
181
+ error: 'Not connected',
182
+ orders: [],
183
+ }, id));
184
+ return;
185
+ }
186
+
187
+ const result = await daemon.rithmic.getOrders();
188
+ daemon._send(socket, createMessage(MSG_TYPE.ORDERS, result, id));
189
+ }
190
+
191
+ async function handleGetPnL(socket, id, data) {
192
+ if (!daemon.rithmic) {
193
+ daemon._send(socket, createMessage(MSG_TYPE.PNL, {
194
+ success: false,
195
+ error: 'Not connected',
196
+ }, id));
197
+ return;
198
+ }
199
+
200
+ const { accountId } = data || {};
201
+ const pnl = accountId
202
+ ? daemon.rithmic.getAccountPnL(accountId)
203
+ : null;
204
+
205
+ daemon._send(socket, createMessage(MSG_TYPE.PNL, {
206
+ success: true,
207
+ pnl,
208
+ }, id));
209
+ }
210
+
211
+ // ==================== TRADING HANDLERS ====================
212
+
213
+ async function handlePlaceOrder(socket, id, data) {
214
+ if (!daemon.rithmic) {
215
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, {
216
+ success: false,
217
+ error: 'Not connected',
218
+ }, id));
219
+ return;
220
+ }
221
+
222
+ const result = await daemon.rithmic.placeOrder(data);
223
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, result, id));
224
+ }
225
+
226
+ async function handleCancelOrder(socket, id, data) {
227
+ if (!daemon.rithmic) {
228
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, {
229
+ success: false,
230
+ error: 'Not connected',
231
+ }, id));
232
+ return;
233
+ }
234
+
235
+ const result = await daemon.rithmic.cancelOrder(data.orderId);
236
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, result, id));
237
+ }
238
+
239
+ async function handleCancelAll(socket, id, data) {
240
+ if (!daemon.rithmic) {
241
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, {
242
+ success: false,
243
+ error: 'Not connected',
244
+ }, id));
245
+ return;
246
+ }
247
+
248
+ const result = await daemon.rithmic.cancelAllOrders(data.accountId);
249
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, result, id));
250
+ }
251
+
252
+ async function handleClosePosition(socket, id, data) {
253
+ if (!daemon.rithmic) {
254
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, {
255
+ success: false,
256
+ error: 'Not connected',
257
+ }, id));
258
+ return;
259
+ }
260
+
261
+ const result = await daemon.rithmic.closePosition(data.accountId, data.symbol);
262
+ daemon._send(socket, createMessage(MSG_TYPE.ORDER_RESULT, result, id));
263
+ }
264
+
265
+ // ==================== CONTRACT HANDLERS ====================
266
+
267
+ async function handleGetContracts(socket, id) {
268
+ if (!daemon.rithmic) {
269
+ daemon._send(socket, createMessage(MSG_TYPE.CONTRACTS, {
270
+ success: false,
271
+ error: 'Not connected',
272
+ contracts: [],
273
+ }, id));
274
+ return;
275
+ }
276
+
277
+ const result = await daemon.rithmic.getContracts();
278
+ daemon._send(socket, createMessage(MSG_TYPE.CONTRACTS, result, id));
279
+ }
280
+
281
+ async function handleSearchContracts(socket, id, data) {
282
+ if (!daemon.rithmic) {
283
+ daemon._send(socket, createMessage(MSG_TYPE.CONTRACTS, {
284
+ success: false,
285
+ error: 'Not connected',
286
+ contracts: [],
287
+ }, id));
288
+ return;
289
+ }
290
+
291
+ const result = await daemon.rithmic.searchContracts(data.search);
292
+ daemon._send(socket, createMessage(MSG_TYPE.CONTRACTS, result, id));
293
+ }
294
+
295
+ // ==================== MARKET DATA HANDLERS ====================
296
+
297
+ async function handleSubscribeMarket(socket, id, data) {
298
+ daemon._send(socket, createMessage(MSG_TYPE.STATUS, {
299
+ success: true,
300
+ subscribed: data.symbol,
301
+ }, id));
302
+ }
303
+
304
+ async function handleUnsubscribeMarket(socket, id, data) {
305
+ daemon._send(socket, createMessage(MSG_TYPE.STATUS, {
306
+ success: true,
307
+ unsubscribed: data.symbol,
308
+ }, id));
309
+ }
310
+
311
+ // ==================== ALGO HANDLERS ====================
312
+
313
+ async function handleStartAlgo(socket, id, data) {
314
+ daemon._send(socket, createMessage(MSG_TYPE.ALGO_STATUS, {
315
+ success: false,
316
+ error: 'Algo trading in daemon not yet implemented',
317
+ }, id));
318
+ }
319
+
320
+ async function handleStopAlgo(socket, id, data) {
321
+ daemon._send(socket, createMessage(MSG_TYPE.ALGO_STATUS, {
322
+ success: false,
323
+ error: 'Algo trading in daemon not yet implemented',
324
+ }, id));
325
+ }
326
+
327
+ return {
328
+ handleLogin,
329
+ handleRestoreSession,
330
+ handleLogout,
331
+ handleGetAccounts,
332
+ handleGetPositions,
333
+ handleGetOrders,
334
+ handleGetPnL,
335
+ handlePlaceOrder,
336
+ handleCancelOrder,
337
+ handleCancelAll,
338
+ handleClosePosition,
339
+ handleGetContracts,
340
+ handleSearchContracts,
341
+ handleSubscribeMarket,
342
+ handleUnsubscribeMarket,
343
+ handleStartAlgo,
344
+ handleStopAlgo,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Setup Rithmic event forwarding to all clients
350
+ * @param {Object} daemon - DaemonServer instance
351
+ */
352
+ function setupRithmicEvents(daemon) {
353
+ if (!daemon.rithmic) return;
354
+
355
+ // Forward order updates to all clients
356
+ daemon.rithmic.on('orderUpdate', (order) => {
357
+ daemon._broadcast(createMessage(MSG_TYPE.EVENT_ORDER_UPDATE, order));
358
+ });
359
+
360
+ // Forward position updates
361
+ daemon.rithmic.on('positionUpdate', (position) => {
362
+ daemon._broadcast(createMessage(MSG_TYPE.EVENT_POSITION_UPDATE, position));
363
+ });
364
+
365
+ // Forward P&L updates
366
+ daemon.rithmic.on('pnlUpdate', (pnl) => {
367
+ daemon._broadcast(createMessage(MSG_TYPE.EVENT_PNL_UPDATE, pnl));
368
+ });
369
+
370
+ // Forward fills
371
+ daemon.rithmic.on('fill', (fill) => {
372
+ daemon._broadcast(createMessage(MSG_TYPE.EVENT_FILL, fill));
373
+ });
374
+
375
+ // Forward disconnect events
376
+ daemon.rithmic.on('disconnected', (info) => {
377
+ daemon._broadcast(createMessage(MSG_TYPE.EVENT_DISCONNECTED, info));
378
+ });
379
+ }
380
+
381
+ module.exports = { createHandlers, setupRithmicEvents };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @fileoverview HQX Daemon Module
3
+ * @module services/daemon
4
+ *
5
+ * Provides persistent Rithmic connection via background daemon.
6
+ * TUI can restart/update without losing connection.
7
+ *
8
+ * Architecture:
9
+ *
10
+ * ┌─────────────────────────────────────────────────────┐
11
+ * │ HQX DAEMON (hqx --daemon) │
12
+ * │ ───────────────────────────────────────────────── │
13
+ * │ • Persistent process (survives TUI restarts) │
14
+ * │ • Maintains Rithmic WebSocket connections │
15
+ * │ • ORDER_PLANT, PNL_PLANT, TICKER_PLANT │
16
+ * │ • Handles reconnection automatically │
17
+ * │ • Runs algo strategies │
18
+ * └──────────────────────┬──────────────────────────────┘
19
+ * │ Unix Socket IPC
20
+ * ┌──────────────────────▼──────────────────────────────┐
21
+ * │ HQX TUI (hqx) │
22
+ * │ ───────────────────────────────────────────────── │
23
+ * │ • User interface │
24
+ * │ • Can restart/update without connection loss │
25
+ * │ • Sends commands to daemon │
26
+ * │ • Receives events/data from daemon │
27
+ * └─────────────────────────────────────────────────────┘
28
+ *
29
+ * Usage:
30
+ * hqx --daemon # Start daemon in background
31
+ * hqx # Start TUI (connects to daemon if available)
32
+ * hqx --stop # Stop daemon
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const { spawn } = require('child_process');
39
+ const { SOCKET_PATH, PID_FILE, SOCKET_DIR } = require('./constants');
40
+ const { DaemonServer } = require('./server');
41
+ const { DaemonClient, getDaemonClient } = require('./client');
42
+ const { logger } = require('../../utils/logger');
43
+
44
+ const log = logger.scope('Daemon');
45
+
46
+ /**
47
+ * Check if daemon is running
48
+ * @returns {boolean}
49
+ */
50
+ function isDaemonRunning() {
51
+ if (!fs.existsSync(PID_FILE)) {
52
+ return false;
53
+ }
54
+
55
+ try {
56
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
57
+
58
+ // Check if process is alive
59
+ process.kill(pid, 0);
60
+ return true;
61
+ } catch (err) {
62
+ // Process not running, clean up stale files
63
+ if (fs.existsSync(PID_FILE)) {
64
+ fs.unlinkSync(PID_FILE);
65
+ }
66
+ if (fs.existsSync(SOCKET_PATH)) {
67
+ fs.unlinkSync(SOCKET_PATH);
68
+ }
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get daemon PID
75
+ * @returns {number|null}
76
+ */
77
+ function getDaemonPid() {
78
+ if (!fs.existsSync(PID_FILE)) {
79
+ return null;
80
+ }
81
+
82
+ try {
83
+ return parseInt(fs.readFileSync(PID_FILE, 'utf8'));
84
+ } catch (_) {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Start daemon in foreground (blocking)
91
+ * Used when running `hqx --daemon`
92
+ * @returns {Promise<void>}
93
+ */
94
+ async function startDaemonForeground() {
95
+ if (isDaemonRunning()) {
96
+ console.log('Daemon already running (PID:', getDaemonPid() + ')');
97
+ process.exit(1);
98
+ }
99
+
100
+ const daemon = new DaemonServer();
101
+
102
+ // Handle shutdown signals
103
+ process.on('SIGINT', () => daemon.stop());
104
+ process.on('SIGTERM', () => daemon.stop());
105
+
106
+ const started = await daemon.start();
107
+
108
+ if (!started) {
109
+ console.error('Failed to start daemon');
110
+ process.exit(1);
111
+ }
112
+
113
+ console.log('Daemon started (PID:', process.pid + ')');
114
+ console.log('Socket:', SOCKET_PATH);
115
+
116
+ // Restore session if available
117
+ const { storage } = require('../session');
118
+ const sessions = storage.load();
119
+ const rithmicSession = sessions.find(s => s.type === 'rithmic' && s.credentials);
120
+
121
+ if (rithmicSession) {
122
+ console.log('Restoring session...');
123
+ const { RithmicService } = require('../rithmic');
124
+
125
+ daemon.rithmic = new RithmicService(rithmicSession.propfirmKey);
126
+ daemon._setupRithmicEvents();
127
+
128
+ const result = await daemon.rithmic.login(
129
+ rithmicSession.credentials.username,
130
+ rithmicSession.credentials.password,
131
+ { skipFetchAccounts: true, cachedAccounts: rithmicSession.accounts }
132
+ );
133
+
134
+ if (result.success) {
135
+ daemon.propfirm = {
136
+ key: rithmicSession.propfirmKey,
137
+ name: daemon.rithmic.propfirm.name,
138
+ };
139
+ console.log('Session restored:', daemon.propfirm.name);
140
+ console.log('Accounts:', daemon.rithmic.accounts?.length || 0);
141
+ } else {
142
+ console.log('Session restore failed:', result.error);
143
+ }
144
+ }
145
+
146
+ console.log('Daemon ready. Press Ctrl+C to stop.');
147
+ }
148
+
149
+ /**
150
+ * Start daemon in background
151
+ * @returns {Promise<boolean>}
152
+ */
153
+ async function startDaemonBackground() {
154
+ if (isDaemonRunning()) {
155
+ log.debug('Daemon already running');
156
+ return true;
157
+ }
158
+
159
+ return new Promise((resolve) => {
160
+ // Spawn daemon process
161
+ const daemon = spawn(process.execPath, [
162
+ require.resolve('../../cli-daemon'),
163
+ ], {
164
+ detached: true,
165
+ stdio: 'ignore',
166
+ });
167
+
168
+ daemon.unref();
169
+
170
+ // Wait for daemon to start
171
+ let attempts = 0;
172
+ const maxAttempts = 20;
173
+
174
+ const check = setInterval(() => {
175
+ attempts++;
176
+
177
+ if (isDaemonRunning()) {
178
+ clearInterval(check);
179
+ log.debug('Daemon started');
180
+ resolve(true);
181
+ } else if (attempts >= maxAttempts) {
182
+ clearInterval(check);
183
+ log.error('Daemon failed to start');
184
+ resolve(false);
185
+ }
186
+ }, 100);
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Stop daemon
192
+ * @returns {boolean}
193
+ */
194
+ function stopDaemon() {
195
+ const pid = getDaemonPid();
196
+
197
+ if (!pid) {
198
+ console.log('Daemon not running');
199
+ return true;
200
+ }
201
+
202
+ try {
203
+ process.kill(pid, 'SIGTERM');
204
+ console.log('Daemon stopped (PID:', pid + ')');
205
+ return true;
206
+ } catch (err) {
207
+ console.error('Failed to stop daemon:', err.message);
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Ensure daemon is running, start if not
214
+ * @returns {Promise<DaemonClient|null>}
215
+ */
216
+ async function ensureDaemon() {
217
+ // Check if daemon is running
218
+ if (!isDaemonRunning()) {
219
+ log.debug('Daemon not running, starting...');
220
+ const started = await startDaemonBackground();
221
+
222
+ if (!started) {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ // Connect client
228
+ const client = getDaemonClient();
229
+ const connected = await client.connect();
230
+
231
+ if (!connected) {
232
+ log.error('Failed to connect to daemon');
233
+ return null;
234
+ }
235
+
236
+ return client;
237
+ }
238
+
239
+ module.exports = {
240
+ // Server
241
+ DaemonServer,
242
+ startDaemonForeground,
243
+ startDaemonBackground,
244
+ stopDaemon,
245
+
246
+ // Client
247
+ DaemonClient,
248
+ getDaemonClient,
249
+ ensureDaemon,
250
+
251
+ // Utilities
252
+ isDaemonRunning,
253
+ getDaemonPid,
254
+
255
+ // Constants
256
+ SOCKET_PATH,
257
+ PID_FILE,
258
+ };