hedgequantx 2.9.219 → 2.9.220

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.219",
3
+ "version": "2.9.220",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
- * @fileoverview Main application router - Rithmic Only
2
+ * @fileoverview Main application router - Rithmic Only (Daemon Mode)
3
3
  * @module app
4
+ *
5
+ * The TUI always uses the daemon for Rithmic connections.
6
+ * Daemon is auto-started in background if not running.
7
+ * This allows TUI updates without losing connection.
4
8
  */
5
9
 
6
10
  const chalk = require('chalk');
@@ -10,6 +14,7 @@ const { connections } = require('./services');
10
14
  const { getLogoWidth, centerText, prepareStdin, clearScreen } = require('./ui');
11
15
  const { logger, prompts } = require('./utils');
12
16
  const { setCachedStats, clearCachedStats } = require('./services/stats-cache');
17
+ const { startDaemonBackground, isDaemonRunning, getDaemonClient } = require('./services/daemon');
13
18
 
14
19
  const log = logger.scope('App');
15
20
 
@@ -22,10 +27,55 @@ const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
22
27
  // Menus
23
28
  const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
24
29
  const { PROPFIRM_CHOICES } = require('./config');
30
+ const { showPropfirmSelection } = require('./menus/connect');
25
31
 
26
32
  /** @type {Object|null} */
27
33
  let currentService = null;
28
34
 
35
+ /** @type {Object|null} Daemon client for IPC */
36
+ let daemonClient = null;
37
+
38
+ /**
39
+ * Create a proxy service that uses daemon for all operations
40
+ * @param {Object} client - DaemonClient instance
41
+ * @param {Object} propfirm - Propfirm info
42
+ * @returns {Object} Service-like object
43
+ */
44
+ function createDaemonProxyService(client, propfirm) {
45
+ const checkMarketHours = () => {
46
+ const now = new Date(), utcDay = now.getUTCDay(), utcHour = now.getUTCHours();
47
+ const isDST = now.getTimezoneOffset() < Math.max(
48
+ new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
49
+ new Date(now.getFullYear(), 6, 1).getTimezoneOffset());
50
+ const ctOffset = isDST ? 5 : 6, ctHour = (utcHour - ctOffset + 24) % 24;
51
+ const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
52
+ if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
53
+ if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5PM CT' };
54
+ if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday 4PM CT)' };
55
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
56
+ return { isOpen: true, message: 'Market is open' };
57
+ };
58
+
59
+ return {
60
+ propfirm, propfirmKey: propfirm?.key, accounts: [], credentials: null,
61
+ async getTradingAccounts() { return client.getTradingAccounts(); },
62
+ async getPositions() { return client.getPositions(); },
63
+ async getOrders() { return client.getOrders(); },
64
+ async placeOrder(data) { return client.placeOrder(data); },
65
+ async cancelOrder(orderId) { return client.cancelOrder(orderId); },
66
+ async cancelAllOrders(accountId) { return client.cancelAllOrders(accountId); },
67
+ async closePosition(accountId, symbol) { return client.closePosition(accountId, symbol); },
68
+ async getContracts() { return client.getContracts(); },
69
+ async searchContracts(search) { return client.searchContracts(search); },
70
+ getAccountPnL() { return { pnl: null, openPnl: null, closedPnl: null, balance: null }; },
71
+ getToken() { return 'daemon-connected'; },
72
+ getPropfirm() { return propfirm?.key || 'apex'; },
73
+ getRithmicCredentials() { return null; },
74
+ checkMarketHours,
75
+ async disconnect() { return { success: true }; },
76
+ };
77
+ }
78
+
29
79
  // ==================== TERMINAL ====================
30
80
 
31
81
  const restoreTerminal = () => {
@@ -176,20 +226,92 @@ const run = async () => {
176
226
  try {
177
227
  log.info('Starting HQX CLI');
178
228
 
179
- // First launch - show banner then try restore session
229
+ // First launch - show banner
180
230
  await banner();
181
231
 
182
- const spinner = ora({ text: 'Restoring session...', color: 'cyan' }).start();
232
+ // ==================== DAEMON AUTO-START ====================
233
+ // Always ensure daemon is running for persistent connections
234
+ let spinner = ora({ text: 'Starting daemon...', color: 'cyan' }).start();
235
+
236
+ if (!isDaemonRunning()) {
237
+ const daemonStarted = await startDaemonBackground();
238
+ if (!daemonStarted) {
239
+ spinner.warn('Daemon failed to start - using direct mode');
240
+ await new Promise(r => setTimeout(r, 500));
241
+ } else {
242
+ spinner.succeed('Daemon started');
243
+ }
244
+ } else {
245
+ spinner.succeed('Daemon running');
246
+ }
247
+
248
+ // Connect to daemon
249
+ daemonClient = getDaemonClient();
250
+ const daemonConnected = await daemonClient.connect();
251
+
252
+ if (!daemonConnected) {
253
+ log.warn('Could not connect to daemon, falling back to direct mode');
254
+ }
183
255
 
184
- const restored = await connections.restoreFromStorage();
256
+ // ==================== SESSION RESTORE ====================
257
+ spinner = ora({ text: 'Restoring session...', color: 'cyan' }).start();
258
+
259
+ // Try to restore via daemon first
260
+ let restored = false;
261
+ if (daemonConnected) {
262
+ try {
263
+ const status = await daemonClient.getStatus();
264
+
265
+ if (status.connected) {
266
+ // Daemon already has a connection, use it
267
+ const accountsResult = await daemonClient.getTradingAccounts();
268
+ if (accountsResult.success && accountsResult.accounts?.length > 0) {
269
+ // Create a proxy service that uses daemon
270
+ currentService = createDaemonProxyService(daemonClient, status.propfirm);
271
+ connections.services.push({
272
+ type: 'rithmic',
273
+ service: currentService,
274
+ propfirm: status.propfirm?.name,
275
+ propfirmKey: status.propfirm?.key,
276
+ connectedAt: new Date(),
277
+ });
278
+ restored = true;
279
+ spinner.succeed(`Session active: ${status.propfirm?.name} (${accountsResult.accounts.length} accounts)`);
280
+ }
281
+ } else {
282
+ // Daemon not connected, try to restore session via daemon
283
+ const restoreResult = await daemonClient.restoreSession();
284
+ if (restoreResult.success) {
285
+ currentService = createDaemonProxyService(daemonClient, restoreResult.propfirm);
286
+ connections.services.push({
287
+ type: 'rithmic',
288
+ service: currentService,
289
+ propfirm: restoreResult.propfirm?.name,
290
+ propfirmKey: restoreResult.propfirm?.key,
291
+ connectedAt: new Date(),
292
+ });
293
+ restored = true;
294
+ spinner.succeed(`Session restored: ${restoreResult.propfirm?.name} (${restoreResult.accounts?.length || 0} accounts)`);
295
+ }
296
+ }
297
+ } catch (err) {
298
+ log.warn('Daemon restore failed', { error: err.message });
299
+ }
300
+ }
301
+
302
+ // Fallback to direct restore if daemon failed
303
+ if (!restored) {
304
+ restored = await connections.restoreFromStorage();
305
+ if (restored) {
306
+ const conn = connections.getAll()[0];
307
+ currentService = conn.service;
308
+ const accountCount = currentService.accounts?.length || 0;
309
+ spinner.succeed(`Session restored: ${conn.propfirm} (${accountCount} accounts)`);
310
+ }
311
+ }
185
312
 
186
313
  if (restored) {
187
- const conn = connections.getAll()[0];
188
- currentService = conn.service;
189
- const accountCount = currentService.accounts?.length || 0;
190
- spinner.succeed(`Session restored: ${conn.propfirm} (${accountCount} accounts)`);
191
314
  await new Promise(r => setTimeout(r, 500));
192
-
193
315
  const spinner2 = ora({ text: 'Loading dashboard...', color: 'yellow' }).start();
194
316
  await refreshStats();
195
317
  global.__hqxSpinner = spinner2;
@@ -207,84 +329,42 @@ const run = async () => {
207
329
  if (!connections.isConnected()) {
208
330
  // Not connected - show banner + propfirm selection
209
331
  await banner();
210
- // Not connected - show propfirm selection directly
211
- const boxWidth = getLogoWidth();
212
- const innerWidth = boxWidth - 2;
213
- const numCols = 3;
214
332
 
215
- const propfirms = PROPFIRM_CHOICES;
216
- const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.value, name: pf.name }));
217
-
218
- // Find max name length for alignment
219
- const maxNameLen = Math.max(...numbered.map(n => n.name.length));
220
- const itemWidth = 4 + 1 + maxNameLen; // [##] + space + name
221
- const gap = 3; // gap between columns
222
- const totalContentWidth = (itemWidth * numCols) + (gap * (numCols - 1));
223
-
224
- // New rectangle (banner is always closed)
225
- console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
226
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
227
- console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
228
-
229
- const rows = Math.ceil(numbered.length / numCols);
230
- for (let row = 0; row < rows; row++) {
231
- let lineParts = [];
232
- for (let col = 0; col < numCols; col++) {
233
- const idx = row + col * rows;
234
- if (idx < numbered.length) {
235
- const item = numbered[idx];
236
- const numStr = item.num.toString().padStart(2, ' ');
237
- const namePadded = item.name.padEnd(maxNameLen);
238
- lineParts.push({ num: `[${numStr}]`, name: namePadded });
239
- } else {
240
- lineParts.push(null);
241
- }
242
- }
243
-
244
- // Build line content
245
- let content = '';
246
- for (let i = 0; i < lineParts.length; i++) {
247
- if (lineParts[i]) {
248
- content += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
249
- } else {
250
- content += ' '.repeat(itemWidth);
251
- }
252
- if (i < lineParts.length - 1) content += ' '.repeat(gap);
253
- }
254
-
255
- // Center the content
256
- const contentLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
257
- const leftPad = Math.floor((innerWidth - contentLen) / 2);
258
- const rightPad = innerWidth - contentLen - leftPad;
259
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║'));
260
- }
261
-
262
- console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
263
- console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
264
- console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
265
-
266
- const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
267
-
268
- if (!input || input.toLowerCase() === 'x') {
333
+ const selectedPropfirm = await showPropfirmSelection();
334
+ if (!selectedPropfirm) {
269
335
  console.log(chalk.gray('GOODBYE!'));
270
336
  process.exit(0);
271
337
  }
272
338
 
273
- const action = parseInt(input);
274
- if (!isNaN(action) && action >= 1 && action <= numbered.length) {
275
- const selectedPropfirm = numbered[action - 1];
276
- const { loginPrompt } = require('./menus/connect');
277
- const credentials = await loginPrompt(selectedPropfirm.name);
278
-
279
- if (credentials) {
280
- const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
281
- try {
282
- // Direct connection to Rithmic (no daemon)
339
+ const { loginPrompt } = require('./menus/connect');
340
+ const credentials = await loginPrompt(selectedPropfirm.name);
341
+
342
+ if (credentials) {
343
+ const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
344
+ try {
345
+ let result;
346
+
347
+ // Try daemon connection first (persistent)
348
+ if (daemonClient?.connected) {
349
+ result = await daemonClient.login(selectedPropfirm.key, credentials.username, credentials.password);
350
+ if (result.success) {
351
+ currentService = createDaemonProxyService(daemonClient, result.propfirm);
352
+ connections.services.push({
353
+ type: 'rithmic', service: currentService,
354
+ propfirm: selectedPropfirm.name, propfirmKey: selectedPropfirm.key, connectedAt: new Date(),
355
+ });
356
+ spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS) [DAEMON]`);
357
+ await refreshStats();
358
+ await new Promise(r => setTimeout(r, 1500));
359
+ } else {
360
+ spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
361
+ await new Promise(r => setTimeout(r, 2000));
362
+ }
363
+ } else {
364
+ // Fallback to direct connection
283
365
  const { RithmicService } = require('./services/rithmic');
284
-
285
366
  const service = new RithmicService(selectedPropfirm.key);
286
- const result = await service.login(credentials.username, credentials.password);
287
-
367
+ result = await service.login(credentials.username, credentials.password);
288
368
  if (result.success) {
289
369
  connections.add('rithmic', service, selectedPropfirm.name);
290
370
  spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
@@ -295,10 +375,10 @@ const run = async () => {
295
375
  spinner.fail((result.error || 'AUTHENTICATION FAILED').toUpperCase());
296
376
  await new Promise(r => setTimeout(r, 2000));
297
377
  }
298
- } catch (error) {
299
- spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
300
- await new Promise(r => setTimeout(r, 2000));
301
378
  }
379
+ } catch (error) {
380
+ spinner.fail(`CONNECTION ERROR: ${error.message.toUpperCase()}`);
381
+ await new Promise(r => setTimeout(r, 2000));
302
382
  }
303
383
  }
304
384
  } else {
@@ -147,4 +147,68 @@ const rithmicMenu = async () => {
147
147
  }
148
148
  };
149
149
 
150
- module.exports = { loginPrompt, rithmicMenu };
150
+ /**
151
+ * Show propfirm selection menu and return selected propfirm
152
+ * @returns {Promise<{key: string, name: string}|null>}
153
+ */
154
+ const showPropfirmSelection = async () => {
155
+ const boxWidth = getLogoWidth();
156
+ const innerWidth = boxWidth - 2;
157
+ const numCols = 3;
158
+
159
+ const propfirms = PROPFIRM_CHOICES;
160
+ const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.value, name: pf.name }));
161
+ const maxNameLen = Math.max(...numbered.map(n => n.name.length));
162
+ const itemWidth = 4 + 1 + maxNameLen;
163
+ const gap = 3;
164
+
165
+ console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
166
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
167
+ console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
168
+
169
+ const rows = Math.ceil(numbered.length / numCols);
170
+ for (let row = 0; row < rows; row++) {
171
+ let lineParts = [];
172
+ for (let col = 0; col < numCols; col++) {
173
+ const idx = row + col * rows;
174
+ if (idx < numbered.length) {
175
+ const item = numbered[idx];
176
+ const numStr = item.num.toString().padStart(2, ' ');
177
+ const namePadded = item.name.padEnd(maxNameLen);
178
+ lineParts.push({ num: `[${numStr}]`, name: namePadded });
179
+ } else {
180
+ lineParts.push(null);
181
+ }
182
+ }
183
+
184
+ let content = '';
185
+ for (let i = 0; i < lineParts.length; i++) {
186
+ if (lineParts[i]) {
187
+ content += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
188
+ } else {
189
+ content += ' '.repeat(itemWidth);
190
+ }
191
+ if (i < lineParts.length - 1) content += ' '.repeat(gap);
192
+ }
193
+
194
+ const contentLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
195
+ const leftPad = Math.floor((innerWidth - contentLen) / 2);
196
+ const rightPad = innerWidth - contentLen - leftPad;
197
+ console.log(chalk.cyan('║') + ' '.repeat(leftPad) + content + ' '.repeat(rightPad) + chalk.cyan('║'));
198
+ }
199
+
200
+ console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
201
+ console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
202
+ console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
203
+
204
+ const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
205
+
206
+ if (!input || input.toLowerCase() === 'x') return null;
207
+
208
+ const action = parseInt(input);
209
+ if (isNaN(action) || action < 1 || action > numbered.length) return null;
210
+
211
+ return numbered[action - 1];
212
+ };
213
+
214
+ module.exports = { loginPrompt, rithmicMenu, showPropfirmSelection };