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 +1 -1
- package/src/app.js +10 -0
- package/src/pages/algo/copy-trading.js +49 -3
- package/src/utils/index.js +7 -0
- package/src/utils/logger.js +110 -0
package/package.json
CHANGED
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)
|
|
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)
|
|
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)
|
|
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,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 };
|