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.
- package/bin/cli.js +56 -7
- package/package.json +1 -1
- package/src/menus/dashboard.js +11 -4
- package/src/services/daemon/client.js +413 -0
- package/src/services/daemon/constants.js +104 -0
- package/src/services/daemon/handlers.js +381 -0
- package/src/services/daemon/index.js +258 -0
- package/src/services/daemon/protocol.js +197 -0
- package/src/services/daemon/proxy.js +408 -0
- package/src/services/daemon/server.js +340 -0
|
@@ -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
|
+
};
|