hedgequantx 1.4.0 → 1.5.0

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": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -9,6 +9,9 @@ const ora = require('ora');
9
9
 
10
10
  const { connections } = require('./services');
11
11
  const { getLogoWidth, centerText, prepareStdin } = require('./ui');
12
+ const { logger } = require('./utils');
13
+
14
+ const log = logger.scope('App');
12
15
 
13
16
  // Pages
14
17
  const { showStats } = require('./pages/stats');
@@ -292,19 +295,23 @@ const mainMenu = async () => {
292
295
  */
293
296
  const run = async () => {
294
297
  try {
298
+ log.info('Starting HQX CLI');
295
299
  await banner();
296
300
 
297
301
  // Try to restore session
302
+ log.debug('Attempting to restore session');
298
303
  const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
299
304
  const restored = await connections.restoreFromStorage();
300
305
 
301
306
  if (restored) {
302
307
  spinner.succeed('Session restored');
303
308
  currentService = connections.getAll()[0].service;
309
+ log.info('Session restored', { connections: connections.count() });
304
310
  // Refresh stats after session restore
305
311
  await refreshStats();
306
312
  } else {
307
313
  spinner.info('No active session');
314
+ log.debug('No session to restore');
308
315
  }
309
316
 
310
317
  // Main loop
@@ -318,8 +325,10 @@ const run = async () => {
318
325
 
319
326
  if (!connections.isConnected()) {
320
327
  const choice = await mainMenu();
328
+ log.debug('Main menu choice', { choice });
321
329
 
322
330
  if (choice === 'exit') {
331
+ log.info('User exit');
323
332
  console.log(chalk.gray('Goodbye!'));
324
333
  process.exit(0);
325
334
  }
@@ -349,6 +358,7 @@ const run = async () => {
349
358
  }
350
359
  } else {
351
360
  const action = await dashboardMenu(currentService);
361
+ log.debug('Dashboard action', { action });
352
362
 
353
363
  switch (action) {
354
364
  case 'accounts':
@@ -12,12 +12,17 @@ const { connections } = require('../../services');
12
12
  const { HQXServerService } = require('../../services/hqx-server');
13
13
  const { FUTURES_SYMBOLS } = require('../../config');
14
14
  const { AlgoUI } = require('./ui');
15
+ const { logger } = require('../../utils');
16
+
17
+ const log = logger.scope('CopyTrading');
15
18
 
16
19
  /**
17
20
  * Copy Trading Menu
18
21
  */
19
22
  const copyTradingMenu = async () => {
23
+ log.info('Copy Trading menu opened');
20
24
  const allConns = connections.getAll();
25
+ log.debug('Connections found', { count: allConns.length });
21
26
 
22
27
  if (allConns.length < 2) {
23
28
  console.log();
@@ -60,6 +65,7 @@ const copyTradingMenu = async () => {
60
65
  }
61
66
 
62
67
  spinner.succeed(`Found ${allAccounts.length} active accounts`);
68
+ log.debug('Active accounts loaded', { count: allAccounts.length, accounts: allAccounts.map(a => ({ propfirm: a.propfirm, name: a.account.accountName })) });
63
69
 
64
70
  // Step 1: Select Lead Account
65
71
  console.log(chalk.cyan(' Step 1: Select LEAD Account (source of trades)'));
@@ -76,8 +82,12 @@ const copyTradingMenu = async () => {
76
82
  choices: leadChoices
77
83
  }]);
78
84
 
79
- if (leadIdx === -1) return;
85
+ if (leadIdx === -1) {
86
+ log.debug('User cancelled at lead selection');
87
+ return;
88
+ }
80
89
  const lead = allAccounts[leadIdx];
90
+ log.debug('Lead account selected', { propfirm: lead.propfirm, account: lead.account.accountName });
81
91
 
82
92
  // Step 2: Select Follower Account
83
93
  console.log();
@@ -98,14 +108,23 @@ const copyTradingMenu = async () => {
98
108
  choices: followerChoices
99
109
  }]);
100
110
 
101
- if (followerIdx === -1) return;
111
+ if (followerIdx === -1) {
112
+ log.debug('User cancelled at follower selection');
113
+ return;
114
+ }
102
115
  const follower = allAccounts[followerIdx];
116
+ log.debug('Follower account selected', { propfirm: follower.propfirm, account: follower.account.accountName });
103
117
 
104
118
  // Step 3: Select Symbol for Lead
105
119
  console.log();
106
120
  console.log(chalk.cyan(' Step 3: Select Symbol for LEAD'));
121
+ log.debug('Selecting symbol for lead', { serviceType: lead.type });
107
122
  const leadSymbol = await selectSymbol(lead.service, 'Lead');
108
- if (!leadSymbol) return;
123
+ if (!leadSymbol) {
124
+ log.debug('Lead symbol selection failed or cancelled');
125
+ return;
126
+ }
127
+ log.debug('Lead symbol selected', { symbol: leadSymbol.name || leadSymbol.symbol });
109
128
 
110
129
  // Step 4: Select Symbol for Follower
111
130
  console.log();
@@ -184,8 +203,35 @@ const copyTradingMenu = async () => {
184
203
  * Symbol selection helper
185
204
  */
186
205
  const selectSymbol = async (service, label) => {
206
+ log.debug('selectSymbol called', { label, hasGetContracts: typeof service.getContracts === 'function' });
187
207
  try {
208
+ // Check if service has getContracts method
209
+ if (typeof service.getContracts !== 'function') {
210
+ log.warn('Service does not have getContracts method, using searchContracts fallback');
211
+ // Fallback: use searchContracts or return predefined list
212
+ if (typeof service.searchContracts === 'function') {
213
+ const contracts = await service.searchContracts('');
214
+ log.debug('searchContracts result', { count: contracts?.length });
215
+ if (contracts && contracts.length > 0) {
216
+ const choices = contracts.map(c => ({ name: c.name || c.symbol, value: c }));
217
+ choices.push({ name: chalk.yellow('< Cancel'), value: null });
218
+
219
+ const { symbol } = await inquirer.prompt([{
220
+ type: 'list',
221
+ name: 'symbol',
222
+ message: `${label} Symbol:`,
223
+ choices,
224
+ pageSize: 15
225
+ }]);
226
+ return symbol;
227
+ }
228
+ }
229
+ log.error('No contract fetching method available');
230
+ return null;
231
+ }
232
+
188
233
  const result = await service.getContracts();
234
+ log.debug('getContracts result', { success: result?.success, count: result?.contracts?.length });
189
235
  if (!result.success) return null;
190
236
 
191
237
  const choices = [];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Utils module exports
3
+ */
4
+
5
+ const { logger, LEVELS } = require('./logger');
6
+
7
+ module.exports = { logger, LEVELS };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * HQX Logger - Centralized logging for debugging
3
+ *
4
+ * Usage:
5
+ * HQX_DEBUG=1 hedgequantx - Enable all debug logs
6
+ * HQX_LOG_FILE=1 hedgequantx - Also write to ~/.hedgequantx/debug.log
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ // Log levels
14
+ const LEVELS = {
15
+ ERROR: 0,
16
+ WARN: 1,
17
+ INFO: 2,
18
+ DEBUG: 3,
19
+ TRACE: 4
20
+ };
21
+
22
+ // Colors for console output
23
+ const COLORS = {
24
+ ERROR: '\x1b[31m', // Red
25
+ WARN: '\x1b[33m', // Yellow
26
+ INFO: '\x1b[36m', // Cyan
27
+ DEBUG: '\x1b[90m', // Gray
28
+ TRACE: '\x1b[90m', // Gray
29
+ RESET: '\x1b[0m'
30
+ };
31
+
32
+ class Logger {
33
+ constructor() {
34
+ this.enabled = process.env.HQX_DEBUG === '1';
35
+ this.fileEnabled = process.env.HQX_LOG_FILE === '1';
36
+ this.level = LEVELS.DEBUG;
37
+ this.logFile = path.join(os.homedir(), '.hedgequantx', 'debug.log');
38
+
39
+ // Create log directory if file logging enabled
40
+ if (this.fileEnabled) {
41
+ const dir = path.dirname(this.logFile);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ // Clear log file on start
46
+ fs.writeFileSync(this.logFile, `=== HQX Debug Log Started ${new Date().toISOString()} ===\n`);
47
+ }
48
+ }
49
+
50
+ _format(level, module, message, data) {
51
+ const timestamp = new Date().toISOString().substr(11, 12); // HH:MM:SS.mmm
52
+ const dataStr = data !== undefined ? ' ' + JSON.stringify(data) : '';
53
+ return `[${timestamp}] [${level}] [${module}]${dataStr ? ' ' + message + dataStr : ' ' + message}`;
54
+ }
55
+
56
+ _log(level, levelName, module, message, data) {
57
+ if (!this.enabled || level > this.level) return;
58
+
59
+ const formatted = this._format(levelName, module, message, data);
60
+ const color = COLORS[levelName] || COLORS.RESET;
61
+
62
+ // Console output (stderr to not interfere with CLI UI)
63
+ console.error(`${color}${formatted}${COLORS.RESET}`);
64
+
65
+ // File output
66
+ if (this.fileEnabled) {
67
+ try {
68
+ fs.appendFileSync(this.logFile, formatted + '\n');
69
+ } catch (e) {
70
+ // Ignore file write errors
71
+ }
72
+ }
73
+ }
74
+
75
+ error(module, message, data) {
76
+ this._log(LEVELS.ERROR, 'ERROR', module, message, data);
77
+ }
78
+
79
+ warn(module, message, data) {
80
+ this._log(LEVELS.WARN, 'WARN', module, message, data);
81
+ }
82
+
83
+ info(module, message, data) {
84
+ this._log(LEVELS.INFO, 'INFO', module, message, data);
85
+ }
86
+
87
+ debug(module, message, data) {
88
+ this._log(LEVELS.DEBUG, 'DEBUG', module, message, data);
89
+ }
90
+
91
+ trace(module, message, data) {
92
+ this._log(LEVELS.TRACE, 'TRACE', module, message, data);
93
+ }
94
+
95
+ // Create a scoped logger for a specific module
96
+ scope(moduleName) {
97
+ return {
98
+ error: (msg, data) => this.error(moduleName, msg, data),
99
+ warn: (msg, data) => this.warn(moduleName, msg, data),
100
+ info: (msg, data) => this.info(moduleName, msg, data),
101
+ debug: (msg, data) => this.debug(moduleName, msg, data),
102
+ trace: (msg, data) => this.trace(moduleName, msg, data),
103
+ };
104
+ }
105
+ }
106
+
107
+ // Singleton instance
108
+ const logger = new Logger();
109
+
110
+ module.exports = { logger, LEVELS };