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 CHANGED
@@ -3,7 +3,13 @@
3
3
  /**
4
4
  * HedgeQuantX CLI - Entry Point
5
5
  * Prop Futures Algo Trading with Protected Strategy
6
- * @version 2.1.0
6
+ *
7
+ * Modes:
8
+ * hqx - Start TUI (connects to daemon if available, or standalone)
9
+ * hqx --daemon - Start daemon in foreground (persistent Rithmic connection)
10
+ * hqx --stop - Stop running daemon
11
+ * hqx --status - Check daemon status
12
+ * hqx -u - Update HQX to latest version
7
13
  */
8
14
 
9
15
  'use strict';
@@ -34,7 +40,10 @@ program
34
40
  .name('hqx')
35
41
  .description('HedgeQuantX - Prop Futures Algo Trading CLI')
36
42
  .version(pkg.version)
37
- .option('-u, --update', 'Update HQX to latest version');
43
+ .option('-u, --update', 'Update HQX to latest version')
44
+ .option('-d, --daemon', 'Start daemon (persistent Rithmic connection)')
45
+ .option('--stop', 'Stop running daemon')
46
+ .option('--status', 'Check daemon status');
38
47
 
39
48
  program
40
49
  .command('start', { isDefault: true })
@@ -51,12 +60,23 @@ program
51
60
  console.log(`HedgeQuantX CLI v${pkg.version}`);
52
61
  });
53
62
 
54
- // Handle -u flag before parsing commands
55
- if (process.argv.includes('-u') || process.argv.includes('--update')) {
63
+ program
64
+ .command('daemon')
65
+ .description('Start daemon in foreground')
66
+ .action(async () => {
67
+ const { startDaemonForeground } = require('../src/services/daemon');
68
+ await startDaemonForeground();
69
+ });
70
+
71
+ // Handle special flags before parsing
72
+ const args = process.argv;
73
+
74
+ // Handle -u flag
75
+ if (args.includes('-u') || args.includes('--update')) {
56
76
  const { execSync } = require('child_process');
57
77
  console.log('Updating HedgeQuantX...');
58
78
  try {
59
- execSync('npm install -g @hedgequantx/cli@latest', { stdio: 'inherit' });
79
+ execSync('npm update -g hedgequantx', { stdio: 'inherit' });
60
80
  console.log('Update complete! Run "hqx" to start.');
61
81
  } catch (e) {
62
82
  console.error('Update failed:', e.message);
@@ -64,5 +84,34 @@ if (process.argv.includes('-u') || process.argv.includes('--update')) {
64
84
  process.exit(0);
65
85
  }
66
86
 
67
- // Parse and run
68
- program.parse(process.argv);
87
+ // Handle --daemon flag
88
+ if (args.includes('-d') || args.includes('--daemon')) {
89
+ const { startDaemonForeground } = require('../src/services/daemon');
90
+ startDaemonForeground().catch((err) => {
91
+ console.error('Daemon error:', err.message);
92
+ process.exit(1);
93
+ });
94
+ }
95
+ // Handle --stop flag
96
+ else if (args.includes('--stop')) {
97
+ const { stopDaemon } = require('../src/services/daemon');
98
+ stopDaemon();
99
+ process.exit(0);
100
+ }
101
+ // Handle --status flag
102
+ else if (args.includes('--status')) {
103
+ const { isDaemonRunning, getDaemonPid, SOCKET_PATH } = require('../src/services/daemon');
104
+
105
+ if (isDaemonRunning()) {
106
+ console.log('Daemon Status: RUNNING');
107
+ console.log(' PID:', getDaemonPid());
108
+ console.log(' Socket:', SOCKET_PATH);
109
+ } else {
110
+ console.log('Daemon Status: NOT RUNNING');
111
+ }
112
+ process.exit(0);
113
+ }
114
+ // Normal TUI startup
115
+ else {
116
+ program.parse(process.argv);
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.216",
3
+ "version": "2.9.218",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const { getLogoWidth, centerText, prepareStdin, displayBanner, clearScreen } = r
11
11
  const { getCachedStats } = require('../services/stats-cache');
12
12
  const { prompts } = require('../utils');
13
13
  const { getActiveAgentCount } = require('../pages/ai-agents');
14
+ const { isDaemonRunning } = require('../services/daemon');
14
15
 
15
16
  /**
16
17
  * Dashboard menu after login
@@ -67,9 +68,14 @@ const dashboardMenu = async (service) => {
67
68
  const agentDisplay = agentCount > 0 ? 'ON' : 'OFF';
68
69
  const agentColor = agentCount > 0 ? chalk.green : chalk.red;
69
70
 
70
- // Fixed width columns for alignment (3 columns)
71
+ // Daemon status
72
+ const daemonOn = isDaemonRunning();
73
+ const daemonDisplay = daemonOn ? 'ON' : 'OFF';
74
+ const daemonColor = daemonOn ? chalk.green : chalk.gray;
75
+
76
+ // Fixed width columns for alignment (4 columns)
71
77
  const icon = chalk.yellow('✔ ');
72
- const colWidth = Math.floor(W / 3);
78
+ const colWidth = Math.floor(W / 4);
73
79
 
74
80
  const formatCol = (label, value, valueColor = chalk.white) => {
75
81
  const text = `✔ ${label}: ${value}`;
@@ -81,9 +87,10 @@ const dashboardMenu = async (service) => {
81
87
 
82
88
  const col1 = formatCol('Accounts', String(statsInfo.accounts));
83
89
  const col2 = formatCol('Balance', balStr, balColor);
84
- const col3 = formatCol('AI Agents', agentDisplay, agentColor);
90
+ const col3 = formatCol('Daemon', daemonDisplay, daemonColor);
91
+ const col4 = formatCol('AI', agentDisplay, agentColor);
85
92
 
86
- const statsLine = col1 + col2 + col3;
93
+ const statsLine = col1 + col2 + col3 + col4;
87
94
  const statsPlainLen = statsLine.replace(/\x1b\[[0-9;]*m/g, '').length;
88
95
  const extraPad = W - statsPlainLen;
89
96
 
@@ -0,0 +1,413 @@
1
+ /**
2
+ * @fileoverview Daemon Client - TUI connection to daemon
3
+ * @module services/daemon/client
4
+ *
5
+ * Connects to the HQX daemon via Unix socket.
6
+ * Provides async request/response API for TUI.
7
+ *
8
+ * NO MOCK DATA - All data comes from daemon (which gets it from Rithmic)
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const net = require('net');
14
+ const EventEmitter = require('events');
15
+ const { SOCKET_PATH, MSG_TYPE, TIMEOUTS } = require('./constants');
16
+ const { createMessage, encode, MessageParser, RequestHandler } = require('./protocol');
17
+ const { logger } = require('../../utils/logger');
18
+
19
+ const log = logger.scope('DaemonClient');
20
+
21
+ /**
22
+ * Daemon Client for TUI
23
+ * Connects to daemon and provides async API
24
+ */
25
+ class DaemonClient extends EventEmitter {
26
+ constructor() {
27
+ super();
28
+
29
+ /** @type {net.Socket|null} */
30
+ this.socket = null;
31
+
32
+ /** @type {MessageParser} */
33
+ this.parser = new MessageParser();
34
+
35
+ /** @type {RequestHandler} */
36
+ this.requests = new RequestHandler();
37
+
38
+ /** @type {boolean} */
39
+ this.connected = false;
40
+
41
+ /** @type {NodeJS.Timeout|null} */
42
+ this.pingInterval = null;
43
+
44
+ /** @type {Object|null} Cached daemon info */
45
+ this.daemonInfo = null;
46
+ }
47
+
48
+ /**
49
+ * Connect to daemon
50
+ * @returns {Promise<boolean>}
51
+ */
52
+ async connect() {
53
+ if (this.connected) return true;
54
+
55
+ return new Promise((resolve) => {
56
+ this.socket = net.createConnection(SOCKET_PATH);
57
+
58
+ this.socket.on('connect', async () => {
59
+ log.debug('Connected to daemon');
60
+ this.connected = true;
61
+
62
+ // Perform handshake
63
+ try {
64
+ this.daemonInfo = await this._request(MSG_TYPE.HANDSHAKE, null, TIMEOUTS.HANDSHAKE);
65
+ log.debug('Handshake complete', this.daemonInfo);
66
+
67
+ // Start ping interval
68
+ this._startPing();
69
+
70
+ resolve(true);
71
+ } catch (err) {
72
+ log.error('Handshake failed', { error: err.message });
73
+ this.disconnect();
74
+ resolve(false);
75
+ }
76
+ });
77
+
78
+ this.socket.on('data', (data) => {
79
+ const messages = this.parser.feed(data);
80
+ for (const msg of messages) {
81
+ this._handleMessage(msg);
82
+ }
83
+ });
84
+
85
+ this.socket.on('close', () => {
86
+ log.debug('Disconnected from daemon');
87
+ this._cleanup();
88
+ this.emit('disconnected');
89
+ });
90
+
91
+ this.socket.on('error', (err) => {
92
+ if (err.code === 'ENOENT') {
93
+ log.debug('Daemon not running');
94
+ } else if (err.code === 'ECONNREFUSED') {
95
+ log.debug('Daemon connection refused');
96
+ } else {
97
+ log.warn('Socket error', { error: err.message });
98
+ }
99
+ this._cleanup();
100
+ resolve(false);
101
+ });
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Check if daemon is available
107
+ * @returns {Promise<boolean>}
108
+ */
109
+ async isAvailable() {
110
+ const connected = await this.connect();
111
+ return connected;
112
+ }
113
+
114
+ /**
115
+ * Disconnect from daemon
116
+ */
117
+ disconnect() {
118
+ if (this.socket) {
119
+ this.socket.destroy();
120
+ }
121
+ this._cleanup();
122
+ }
123
+
124
+ /**
125
+ * Cleanup state
126
+ */
127
+ _cleanup() {
128
+ this.connected = false;
129
+ this.requests.clear();
130
+ this.parser.reset();
131
+
132
+ if (this.pingInterval) {
133
+ clearInterval(this.pingInterval);
134
+ this.pingInterval = null;
135
+ }
136
+
137
+ this.socket = null;
138
+ }
139
+
140
+ /**
141
+ * Start ping interval
142
+ */
143
+ _startPing() {
144
+ this.pingInterval = setInterval(async () => {
145
+ try {
146
+ await this._request(MSG_TYPE.PING, null, TIMEOUTS.PING_TIMEOUT);
147
+ } catch (err) {
148
+ log.warn('Ping failed, disconnecting');
149
+ this.disconnect();
150
+ }
151
+ }, TIMEOUTS.PING_INTERVAL);
152
+ }
153
+
154
+ /**
155
+ * Handle incoming message
156
+ * @param {Object} msg
157
+ */
158
+ _handleMessage(msg) {
159
+ const { type, data, replyTo } = msg;
160
+
161
+ // Check if this is a response to a pending request
162
+ if (replyTo && this.requests.resolve(replyTo, data)) {
163
+ return;
164
+ }
165
+
166
+ // Handle push events from daemon
167
+ switch (type) {
168
+ case MSG_TYPE.EVENT_ORDER_UPDATE:
169
+ this.emit('orderUpdate', data);
170
+ break;
171
+
172
+ case MSG_TYPE.EVENT_POSITION_UPDATE:
173
+ this.emit('positionUpdate', data);
174
+ break;
175
+
176
+ case MSG_TYPE.EVENT_PNL_UPDATE:
177
+ this.emit('pnlUpdate', data);
178
+ break;
179
+
180
+ case MSG_TYPE.EVENT_FILL:
181
+ this.emit('fill', data);
182
+ break;
183
+
184
+ case MSG_TYPE.EVENT_DISCONNECTED:
185
+ this.emit('rithmicDisconnected', data);
186
+ break;
187
+
188
+ case MSG_TYPE.EVENT_RECONNECTED:
189
+ this.emit('rithmicReconnected', data);
190
+ break;
191
+
192
+ case MSG_TYPE.MARKET_DATA:
193
+ case MSG_TYPE.TICK:
194
+ this.emit('marketData', data);
195
+ break;
196
+
197
+ case MSG_TYPE.ALGO_LOG:
198
+ this.emit('algoLog', data);
199
+ break;
200
+
201
+ case MSG_TYPE.PONG:
202
+ // Handled by request handler
203
+ break;
204
+
205
+ default:
206
+ log.debug('Unhandled message', { type });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Send request and wait for response
212
+ * @param {string} type - Message type
213
+ * @param {any} data - Request data
214
+ * @param {number} [timeout] - Timeout in ms
215
+ * @returns {Promise<any>} Response data
216
+ */
217
+ async _request(type, data, timeout = TIMEOUTS.REQUEST) {
218
+ if (!this.connected || !this.socket) {
219
+ throw new Error('Not connected to daemon');
220
+ }
221
+
222
+ const msg = createMessage(type, data);
223
+ const promise = this.requests.createRequest(msg.id, timeout);
224
+
225
+ this.socket.write(encode(msg));
226
+
227
+ return promise;
228
+ }
229
+
230
+ // ==================== PUBLIC API ====================
231
+
232
+ /**
233
+ * Get daemon status
234
+ * @returns {Promise<Object>}
235
+ */
236
+ async getStatus() {
237
+ return this._request(MSG_TYPE.GET_STATUS);
238
+ }
239
+
240
+ /**
241
+ * Login to Rithmic via daemon
242
+ * @param {string} propfirmKey
243
+ * @param {string} username
244
+ * @param {string} password
245
+ * @returns {Promise<Object>}
246
+ */
247
+ async login(propfirmKey, username, password) {
248
+ return this._request(MSG_TYPE.LOGIN, { propfirmKey, username, password }, TIMEOUTS.LOGIN);
249
+ }
250
+
251
+ /**
252
+ * Restore session from storage
253
+ * @returns {Promise<Object>}
254
+ */
255
+ async restoreSession() {
256
+ return this._request(MSG_TYPE.RESTORE_SESSION, null, TIMEOUTS.LOGIN);
257
+ }
258
+
259
+ /**
260
+ * Logout
261
+ * @returns {Promise<Object>}
262
+ */
263
+ async logout() {
264
+ return this._request(MSG_TYPE.LOGOUT);
265
+ }
266
+
267
+ /**
268
+ * Get trading accounts
269
+ * @returns {Promise<Object>}
270
+ */
271
+ async getTradingAccounts() {
272
+ return this._request(MSG_TYPE.GET_ACCOUNTS);
273
+ }
274
+
275
+ /**
276
+ * Get positions
277
+ * @returns {Promise<Object>}
278
+ */
279
+ async getPositions() {
280
+ return this._request(MSG_TYPE.GET_POSITIONS);
281
+ }
282
+
283
+ /**
284
+ * Get orders
285
+ * @returns {Promise<Object>}
286
+ */
287
+ async getOrders() {
288
+ return this._request(MSG_TYPE.GET_ORDERS);
289
+ }
290
+
291
+ /**
292
+ * Get P&L for account
293
+ * @param {string} accountId
294
+ * @returns {Promise<Object>}
295
+ */
296
+ async getPnL(accountId) {
297
+ return this._request(MSG_TYPE.GET_PNL, { accountId });
298
+ }
299
+
300
+ /**
301
+ * Place order
302
+ * @param {Object} orderData
303
+ * @returns {Promise<Object>}
304
+ */
305
+ async placeOrder(orderData) {
306
+ return this._request(MSG_TYPE.PLACE_ORDER, orderData);
307
+ }
308
+
309
+ /**
310
+ * Cancel order
311
+ * @param {string} orderId
312
+ * @returns {Promise<Object>}
313
+ */
314
+ async cancelOrder(orderId) {
315
+ return this._request(MSG_TYPE.CANCEL_ORDER, { orderId });
316
+ }
317
+
318
+ /**
319
+ * Cancel all orders for account
320
+ * @param {string} accountId
321
+ * @returns {Promise<Object>}
322
+ */
323
+ async cancelAllOrders(accountId) {
324
+ return this._request(MSG_TYPE.CANCEL_ALL, { accountId });
325
+ }
326
+
327
+ /**
328
+ * Close position
329
+ * @param {string} accountId
330
+ * @param {string} symbol
331
+ * @returns {Promise<Object>}
332
+ */
333
+ async closePosition(accountId, symbol) {
334
+ return this._request(MSG_TYPE.CLOSE_POSITION, { accountId, symbol });
335
+ }
336
+
337
+ /**
338
+ * Get contracts
339
+ * @returns {Promise<Object>}
340
+ */
341
+ async getContracts() {
342
+ return this._request(MSG_TYPE.GET_CONTRACTS);
343
+ }
344
+
345
+ /**
346
+ * Search contracts
347
+ * @param {string} search
348
+ * @returns {Promise<Object>}
349
+ */
350
+ async searchContracts(search) {
351
+ return this._request(MSG_TYPE.SEARCH_CONTRACTS, { search });
352
+ }
353
+
354
+ /**
355
+ * Subscribe to market data
356
+ * @param {string} symbol
357
+ * @returns {Promise<Object>}
358
+ */
359
+ async subscribeMarket(symbol) {
360
+ return this._request(MSG_TYPE.SUBSCRIBE_MARKET, { symbol });
361
+ }
362
+
363
+ /**
364
+ * Unsubscribe from market data
365
+ * @param {string} symbol
366
+ * @returns {Promise<Object>}
367
+ */
368
+ async unsubscribeMarket(symbol) {
369
+ return this._request(MSG_TYPE.UNSUBSCRIBE_MARKET, { symbol });
370
+ }
371
+
372
+ /**
373
+ * Start algo trading
374
+ * @param {Object} config
375
+ * @returns {Promise<Object>}
376
+ */
377
+ async startAlgo(config) {
378
+ return this._request(MSG_TYPE.START_ALGO, config);
379
+ }
380
+
381
+ /**
382
+ * Stop algo trading
383
+ * @param {string} algoId
384
+ * @returns {Promise<Object>}
385
+ */
386
+ async stopAlgo(algoId) {
387
+ return this._request(MSG_TYPE.STOP_ALGO, { algoId });
388
+ }
389
+
390
+ /**
391
+ * Shutdown daemon
392
+ * @returns {Promise<Object>}
393
+ */
394
+ async shutdown() {
395
+ return this._request(MSG_TYPE.SHUTDOWN);
396
+ }
397
+ }
398
+
399
+ // Singleton instance
400
+ let instance = null;
401
+
402
+ /**
403
+ * Get daemon client instance
404
+ * @returns {DaemonClient}
405
+ */
406
+ function getDaemonClient() {
407
+ if (!instance) {
408
+ instance = new DaemonClient();
409
+ }
410
+ return instance;
411
+ }
412
+
413
+ module.exports = { DaemonClient, getDaemonClient };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @fileoverview Daemon Constants
3
+ * @module services/daemon/constants
4
+ *
5
+ * Configuration for HQX Daemon - persistent Rithmic connection service
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const os = require('os');
11
+ const path = require('path');
12
+
13
+ /** Daemon socket path */
14
+ const SOCKET_DIR = path.join(os.homedir(), '.hedgequantx');
15
+ const SOCKET_PATH = path.join(SOCKET_DIR, 'hqx.sock');
16
+
17
+ /** Daemon PID file */
18
+ const PID_FILE = path.join(SOCKET_DIR, 'daemon.pid');
19
+
20
+ /** IPC Protocol version */
21
+ const PROTOCOL_VERSION = 1;
22
+
23
+ /** Message types for IPC communication */
24
+ const MSG_TYPE = {
25
+ // Connection
26
+ PING: 'ping',
27
+ PONG: 'pong',
28
+ HANDSHAKE: 'handshake',
29
+ HANDSHAKE_ACK: 'handshake_ack',
30
+
31
+ // Auth
32
+ LOGIN: 'login',
33
+ LOGIN_RESULT: 'login_result',
34
+ LOGOUT: 'logout',
35
+ RESTORE_SESSION: 'restore_session',
36
+
37
+ // Data requests
38
+ GET_ACCOUNTS: 'get_accounts',
39
+ GET_POSITIONS: 'get_positions',
40
+ GET_ORDERS: 'get_orders',
41
+ GET_PNL: 'get_pnl',
42
+ GET_STATUS: 'get_status',
43
+ GET_CONTRACTS: 'get_contracts',
44
+ SEARCH_CONTRACTS: 'search_contracts',
45
+
46
+ // Data responses
47
+ ACCOUNTS: 'accounts',
48
+ POSITIONS: 'positions',
49
+ ORDERS: 'orders',
50
+ PNL: 'pnl',
51
+ STATUS: 'status',
52
+ CONTRACTS: 'contracts',
53
+
54
+ // Trading
55
+ PLACE_ORDER: 'place_order',
56
+ CANCEL_ORDER: 'cancel_order',
57
+ CANCEL_ALL: 'cancel_all',
58
+ CLOSE_POSITION: 'close_position',
59
+ ORDER_RESULT: 'order_result',
60
+
61
+ // Market data
62
+ SUBSCRIBE_MARKET: 'subscribe_market',
63
+ UNSUBSCRIBE_MARKET: 'unsubscribe_market',
64
+ MARKET_DATA: 'market_data',
65
+ TICK: 'tick',
66
+
67
+ // Algo trading
68
+ START_ALGO: 'start_algo',
69
+ STOP_ALGO: 'stop_algo',
70
+ ALGO_STATUS: 'algo_status',
71
+ ALGO_LOG: 'algo_log',
72
+
73
+ // Events (daemon → TUI push)
74
+ EVENT_ORDER_UPDATE: 'event_order_update',
75
+ EVENT_POSITION_UPDATE: 'event_position_update',
76
+ EVENT_PNL_UPDATE: 'event_pnl_update',
77
+ EVENT_FILL: 'event_fill',
78
+ EVENT_DISCONNECTED: 'event_disconnected',
79
+ EVENT_RECONNECTED: 'event_reconnected',
80
+
81
+ // Errors
82
+ ERROR: 'error',
83
+
84
+ // Daemon control
85
+ SHUTDOWN: 'shutdown',
86
+ };
87
+
88
+ /** Timeouts */
89
+ const TIMEOUTS = {
90
+ HANDSHAKE: 5000,
91
+ REQUEST: 30000,
92
+ LOGIN: 60000,
93
+ PING_INTERVAL: 10000,
94
+ PING_TIMEOUT: 5000,
95
+ };
96
+
97
+ module.exports = {
98
+ SOCKET_DIR,
99
+ SOCKET_PATH,
100
+ PID_FILE,
101
+ PROTOCOL_VERSION,
102
+ MSG_TYPE,
103
+ TIMEOUTS,
104
+ };