hedgequantx 1.1.1 → 1.2.31

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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Positions page
3
+ * @module pages/positions
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const inquirer = require('inquirer');
9
+
10
+ const { connections } = require('../services');
11
+ const { ORDER_SIDE } = require('../config');
12
+ const { getLogoWidth, drawBoxHeader, drawBoxFooter, drawBoxRow, drawBoxSeparator, visibleLength, padText } = require('../ui');
13
+
14
+ /**
15
+ * Shows all open positions from all connections
16
+ * @param {Object} service - Current service
17
+ */
18
+ const showPositions = async (service) => {
19
+ const spinner = ora('Fetching positions...').start();
20
+ const boxWidth = getLogoWidth();
21
+ const innerWidth = boxWidth - 2;
22
+
23
+ // Get all accounts first
24
+ let allAccounts = [];
25
+
26
+ if (connections.count() > 0) {
27
+ for (const conn of connections.getAll()) {
28
+ try {
29
+ const result = await conn.service.getTradingAccounts();
30
+ if (result.success && result.accounts) {
31
+ result.accounts.forEach(account => {
32
+ allAccounts.push({
33
+ ...account,
34
+ propfirm: conn.propfirm || conn.type,
35
+ service: conn.service
36
+ });
37
+ });
38
+ }
39
+ } catch (e) { /* ignore */ }
40
+ }
41
+ } else if (service) {
42
+ const result = await service.getTradingAccounts();
43
+ if (result.success && result.accounts) {
44
+ allAccounts = result.accounts.map(a => ({ ...a, service, propfirm: service.propfirm.name }));
45
+ }
46
+ }
47
+
48
+ // Get positions for each account
49
+ let allPositions = [];
50
+
51
+ for (const account of allAccounts) {
52
+ try {
53
+ const result = await account.service.getPositions(account.accountId);
54
+ if (result.success && result.positions && result.positions.length > 0) {
55
+ result.positions.forEach(pos => {
56
+ allPositions.push({
57
+ ...pos,
58
+ accountName: account.accountName || account.name,
59
+ propfirm: account.propfirm
60
+ });
61
+ });
62
+ }
63
+ } catch (e) { /* ignore */ }
64
+ }
65
+
66
+ spinner.succeed(`Found ${allPositions.length} position(s)`);
67
+ console.log();
68
+
69
+ drawBoxHeader('OPEN POSITIONS', boxWidth);
70
+
71
+ if (allPositions.length === 0) {
72
+ drawBoxRow(chalk.gray(' No open positions'), boxWidth);
73
+ } else {
74
+ // Header row
75
+ const header = ' ' +
76
+ 'Symbol'.padEnd(15) +
77
+ 'Side'.padEnd(8) +
78
+ 'Size'.padEnd(8) +
79
+ 'Entry'.padEnd(12) +
80
+ 'P&L'.padEnd(12) +
81
+ 'Account';
82
+ drawBoxRow(chalk.white.bold(header), boxWidth);
83
+ drawBoxSeparator(boxWidth);
84
+
85
+ // Position rows
86
+ for (const pos of allPositions) {
87
+ const symbol = (pos.contractId || pos.symbol || 'Unknown').substring(0, 14);
88
+ const sideInfo = ORDER_SIDE[pos.side] || { text: 'Unknown', color: 'white' };
89
+ const size = Math.abs(pos.size || pos.quantity || 0);
90
+ const entry = pos.averagePrice || pos.entryPrice || 0;
91
+ const pnl = pos.profitAndLoss || pos.unrealizedPnl || 0;
92
+ const account = (pos.accountName || 'Unknown').substring(0, 15);
93
+
94
+ const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
95
+ const pnlStr = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
96
+
97
+ const row = ' ' +
98
+ chalk.white(symbol.padEnd(15)) +
99
+ chalk[sideInfo.color](sideInfo.text.padEnd(8)) +
100
+ chalk.white(size.toString().padEnd(8)) +
101
+ chalk.white(entry.toFixed(2).padEnd(12)) +
102
+ pnlColor(pnlStr.padEnd(12)) +
103
+ chalk.gray(account);
104
+
105
+ drawBoxRow(row, boxWidth);
106
+ }
107
+ }
108
+
109
+ drawBoxFooter(boxWidth);
110
+ console.log();
111
+
112
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
113
+ };
114
+
115
+ module.exports = { showPositions };
@@ -57,6 +57,23 @@ const showStats = async (service) => {
57
57
  return;
58
58
  }
59
59
 
60
+ // Remove duplicates by accountId
61
+ const seen = new Set();
62
+ allAccountsData = allAccountsData.filter(acc => {
63
+ if (seen.has(acc.accountId)) return false;
64
+ seen.add(acc.accountId);
65
+ return true;
66
+ });
67
+
68
+ // Filter only active accounts (status === 0)
69
+ const activeAccounts = allAccountsData.filter(acc => acc.status === 0);
70
+
71
+ if (activeAccounts.length === 0) {
72
+ spinner.fail('No active accounts found');
73
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
74
+ return;
75
+ }
76
+
60
77
  // Collect stats
61
78
  let totalBalance = 0;
62
79
  let totalStartingBalance = 0;
@@ -68,7 +85,7 @@ const showStats = async (service) => {
68
85
 
69
86
  spinner.text = 'Fetching detailed stats...';
70
87
 
71
- for (const account of allAccountsData) {
88
+ for (const account of activeAccounts) {
72
89
  const svc = account.service;
73
90
  const currentBalance = account.balance || 0;
74
91
  totalBalance += currentBalance;
@@ -137,7 +154,7 @@ const showStats = async (service) => {
137
154
  longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
138
155
  };
139
156
 
140
- for (const account of allAccountsData) {
157
+ for (const account of activeAccounts) {
141
158
  if (account.lifetimeStats) {
142
159
  const s = account.lifetimeStats;
143
160
  stats.totalTrades += s.totalTrades || 0;
@@ -210,6 +227,49 @@ const showStats = async (service) => {
210
227
  const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '0.0';
211
228
  const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '0.0';
212
229
 
230
+ // Advanced quantitative metrics
231
+ const tradePnLs = allTrades.map(t => t.profitAndLoss || t.pnl || 0);
232
+ const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
233
+
234
+ // Standard deviation
235
+ const variance = tradePnLs.length > 0
236
+ ? tradePnLs.reduce((sum, pnl) => sum + Math.pow(pnl - avgReturn, 2), 0) / tradePnLs.length
237
+ : 0;
238
+ const stdDev = Math.sqrt(variance);
239
+
240
+ // Downside deviation (for Sortino)
241
+ const downsideReturns = tradePnLs.filter(pnl => pnl < 0);
242
+ const downsideVariance = downsideReturns.length > 0
243
+ ? downsideReturns.reduce((sum, pnl) => sum + Math.pow(pnl, 2), 0) / downsideReturns.length
244
+ : 0;
245
+ const downsideDev = Math.sqrt(downsideVariance);
246
+
247
+ // Sharpe Ratio (simplified - assumes risk-free rate = 0)
248
+ const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : '0.00';
249
+
250
+ // Sortino Ratio
251
+ const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : '0.00';
252
+
253
+ // Max Drawdown
254
+ let maxDrawdown = 0;
255
+ let peak = totalStartingBalance;
256
+ let equity = totalStartingBalance;
257
+ tradePnLs.forEach(pnl => {
258
+ equity += pnl;
259
+ if (equity > peak) peak = equity;
260
+ const drawdown = (peak - equity) / peak * 100;
261
+ if (drawdown > maxDrawdown) maxDrawdown = drawdown;
262
+ });
263
+
264
+ // Expectancy (average profit per trade)
265
+ const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
266
+
267
+ // Risk/Reward Ratio
268
+ const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : '0.00';
269
+
270
+ // Calmar Ratio (return / max drawdown)
271
+ const calmarRatio = maxDrawdown > 0 ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : '0.00';
272
+
213
273
  const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
214
274
  const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
215
275
 
@@ -218,7 +278,7 @@ const showStats = async (service) => {
218
278
  draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
219
279
 
220
280
  console.log(chalk.cyan('\u2551') + fmtRow('Connections:', chalk.cyan(connections.count().toString() || '1'), col1) + chalk.cyan('\u2502') + fmtRow('Total Trades:', chalk.white(stats.totalTrades.toString()), col2) + chalk.cyan('\u2551'));
221
- console.log(chalk.cyan('\u2551') + fmtRow('Total Accounts:', chalk.cyan(allAccountsData.length.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Winning Trades:', chalk.green(stats.winningTrades.toString()), col2) + chalk.cyan('\u2551'));
281
+ console.log(chalk.cyan('\u2551') + fmtRow('Total Accounts:', chalk.cyan(activeAccounts.length.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Winning Trades:', chalk.green(stats.winningTrades.toString()), col2) + chalk.cyan('\u2551'));
222
282
  console.log(chalk.cyan('\u2551') + fmtRow('Total Balance:', totalBalanceColor('$' + totalBalance.toLocaleString()), col1) + chalk.cyan('\u2502') + fmtRow('Losing Trades:', chalk.red(stats.losingTrades.toString()), col2) + chalk.cyan('\u2551'));
223
283
  console.log(chalk.cyan('\u2551') + fmtRow('Starting Balance:', chalk.white('$' + totalStartingBalance.toLocaleString()), col1) + chalk.cyan('\u2502') + fmtRow('Win Rate:', parseFloat(winRate) >= 50 ? chalk.green(winRate + '%') : chalk.yellow(winRate + '%'), col2) + chalk.cyan('\u2551'));
224
284
  console.log(chalk.cyan('\u2551') + fmtRow('Total P&L:', pnlColor('$' + totalPnL.toLocaleString() + ' (' + returnPercent + '%)'), col1) + chalk.cyan('\u2502') + fmtRow('Long Trades:', chalk.white(stats.longTrades + ' (' + longWinRate + '%)'), col2) + chalk.cyan('\u2551'));
@@ -237,6 +297,20 @@ const showStats = async (service) => {
237
297
  console.log(chalk.cyan('\u2551') + fmtRow('Avg Win:', chalk.green('$' + avgWin), col1) + chalk.cyan('\u2502') + fmtRow('Best Trade:', chalk.green('$' + stats.bestTrade.toFixed(2)), col2) + chalk.cyan('\u2551'));
238
298
  console.log(chalk.cyan('\u2551') + fmtRow('Avg Loss:', chalk.red('-$' + avgLoss), col1) + chalk.cyan('\u2502') + fmtRow('Worst Trade:', chalk.red('$' + stats.worstTrade.toFixed(2)), col2) + chalk.cyan('\u2551'));
239
299
 
300
+ // Quantitative Metrics
301
+ draw2ColSeparator(boxWidth);
302
+ draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
303
+
304
+ const sharpeColor = parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
305
+ const sortinoColor = parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
306
+ const ddColor = maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
307
+ const rrColor = parseFloat(riskRewardRatio) >= 2 ? chalk.green : parseFloat(riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
308
+
309
+ console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(riskRewardRatio), col2) + chalk.cyan('\u2551'));
310
+ console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
311
+ console.log(chalk.cyan('\u2551') + fmtRow('Max Drawdown:', ddColor(maxDrawdown.toFixed(2) + '%'), col1) + chalk.cyan('\u2502') + fmtRow('Expectancy:', expectancy >= 0 ? chalk.green('$' + expectancy.toFixed(2)) : chalk.red('$' + expectancy.toFixed(2)), col2) + chalk.cyan('\u2551'));
312
+ console.log(chalk.cyan('\u2551') + fmtRow('Std Deviation:', chalk.white('$' + stdDev.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Avg Trade:', avgReturn >= 0 ? chalk.green('$' + avgReturn.toFixed(2)) : chalk.red('$' + avgReturn.toFixed(2)), col2) + chalk.cyan('\u2551'));
313
+
240
314
  drawBoxFooter(boxWidth);
241
315
 
242
316
  // Equity Curve
@@ -280,6 +354,141 @@ const showStats = async (service) => {
280
354
  console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(chartInnerWidth - msg.length) + chalk.cyan('\u2551'));
281
355
  }
282
356
 
357
+ drawBoxFooter(boxWidth);
358
+
359
+ // Trades History
360
+ console.log();
361
+ drawBoxHeader('TRADES HISTORY', boxWidth);
362
+
363
+ const innerWidth = boxWidth - 2;
364
+
365
+ if (allTrades.length > 0) {
366
+ // Column widths
367
+ const colTime = 12;
368
+ const colSymbol = 10;
369
+ const colEntry = 10;
370
+ const colExit = 10;
371
+ const colEntryP = 10;
372
+ const colExitP = 10;
373
+ const colPnL = 10;
374
+ const colDir = 6;
375
+ const colID = innerWidth - colTime - colSymbol - colEntry - colExit - colEntryP - colExitP - colPnL - colDir - 9;
376
+
377
+ // Header
378
+ const header =
379
+ chalk.white(' Time'.padEnd(colTime)) + chalk.gray('|') +
380
+ chalk.white('Symbol'.padEnd(colSymbol)) + chalk.gray('|') +
381
+ chalk.white('Entry'.padEnd(colEntry)) + chalk.gray('|') +
382
+ chalk.white('Exit'.padEnd(colExit)) + chalk.gray('|') +
383
+ chalk.white('Entry $'.padEnd(colEntryP)) + chalk.gray('|') +
384
+ chalk.white('Exit $'.padEnd(colExitP)) + chalk.gray('|') +
385
+ chalk.white('P&L'.padEnd(colPnL)) + chalk.gray('|') +
386
+ chalk.white('Dir'.padEnd(colDir)) + chalk.gray('|') +
387
+ chalk.white('ID'.padEnd(colID));
388
+
389
+ console.log(chalk.cyan('\u2551') + header + chalk.cyan('\u2551'));
390
+ console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
391
+
392
+ // Show last 10 trades
393
+ const recentTrades = allTrades.slice(-10).reverse();
394
+
395
+ for (const trade of recentTrades) {
396
+ const time = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
397
+ const symbol = (trade.contractName || trade.symbol || 'N/A').substring(0, colSymbol - 1);
398
+ const entryTime = trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
399
+ const exitTime = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
400
+ const entryPrice = trade.entryPrice ? trade.entryPrice.toFixed(2) : 'N/A';
401
+ const exitPrice = trade.exitPrice ? trade.exitPrice.toFixed(2) : 'N/A';
402
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
403
+ const pnlStr = pnl >= 0 ? chalk.green('+$' + pnl.toFixed(0)) : chalk.red('-$' + Math.abs(pnl).toFixed(0));
404
+ const direction = trade.side === 0 ? chalk.green('LONG') : trade.side === 1 ? chalk.red('SHORT') : chalk.gray('N/A');
405
+ const tradeId = (trade.id || trade.tradeId || 'N/A').toString().substring(0, colID - 1);
406
+
407
+ const row =
408
+ (' ' + time).padEnd(colTime) + chalk.gray('|') +
409
+ symbol.padEnd(colSymbol) + chalk.gray('|') +
410
+ entryTime.padEnd(colEntry) + chalk.gray('|') +
411
+ exitTime.padEnd(colExit) + chalk.gray('|') +
412
+ entryPrice.padEnd(colEntryP) + chalk.gray('|') +
413
+ exitPrice.padEnd(colExitP) + chalk.gray('|') +
414
+ pnlStr.padEnd(colPnL + 10) + chalk.gray('|') + // +10 for color codes
415
+ direction.padEnd(colDir + 10) + chalk.gray('|') + // +10 for color codes
416
+ tradeId.padEnd(colID);
417
+
418
+ // Calculate visible length and pad
419
+ const visLen = row.replace(/\x1b\[[0-9;]*m/g, '').length;
420
+ const padding = innerWidth - visLen;
421
+
422
+ console.log(chalk.cyan('\u2551') + row + ' '.repeat(Math.max(0, padding)) + chalk.cyan('\u2551'));
423
+ }
424
+
425
+ if (allTrades.length > 10) {
426
+ const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
427
+ console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg) + ' '.repeat(innerWidth - moreMsg.length) + chalk.cyan('\u2551'));
428
+ }
429
+ } else {
430
+ const msg = ' No trade history available';
431
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(innerWidth - msg.length) + chalk.cyan('\u2551'));
432
+ }
433
+
434
+ drawBoxFooter(boxWidth);
435
+
436
+ // HQX Score
437
+ console.log();
438
+ drawBoxHeader('HQX SCORE', boxWidth);
439
+
440
+ // Calculate scores (0-100)
441
+ const winRateScore = Math.min(100, parseFloat(winRate) * 1.5); // 66% win rate = 100
442
+ const profitFactorScore = Math.min(100, parseFloat(profitFactor) * 40); // 2.5 PF = 100
443
+ const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
444
+ const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
445
+ ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
446
+ : 50;
447
+ const volumeScore = Math.min(100, stats.totalTrades * 2); // 50 trades = 100
448
+ const returnScore = Math.min(100, Math.max(0, parseFloat(returnPercent) * 10 + 50)); // 5% return = 100
449
+
450
+ // Overall HQX Score
451
+ const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
452
+ const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
453
+ const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
454
+
455
+ // Progress bar helper
456
+ const makeBar = (score, width = 20) => {
457
+ const filled = Math.round((score / 100) * width);
458
+ const empty = width - filled;
459
+ const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
460
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
461
+ };
462
+
463
+ // Metrics with progress bars
464
+ const metrics = [
465
+ { name: 'Win Rate', score: winRateScore },
466
+ { name: 'Profit Factor', score: profitFactorScore },
467
+ { name: 'Consistency', score: consistencyScore },
468
+ { name: 'Risk Management', score: riskScore },
469
+ { name: 'Volume', score: volumeScore },
470
+ { name: 'Returns', score: returnScore }
471
+ ];
472
+
473
+ const barWidth = 30;
474
+ const labelWidth = 18;
475
+
476
+ // Display overall score
477
+ const overallLine = ` OVERALL SCORE: ${scoreColor(hqxScore.toString())} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
478
+ const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
479
+ console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
480
+ console.log(chalk.cyan('\u2551') + chalk.gray('─'.repeat(innerWidth)) + chalk.cyan('\u2551'));
481
+
482
+ // Display each metric
483
+ for (const metric of metrics) {
484
+ const label = (' ' + metric.name + ':').padEnd(labelWidth);
485
+ const bar = makeBar(metric.score, barWidth);
486
+ const pct = (metric.score.toFixed(0) + '%').padStart(5);
487
+ const line = label + bar + ' ' + pct;
488
+ const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
489
+ console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
490
+ }
491
+
283
492
  drawBoxFooter(boxWidth);
284
493
  console.log();
285
494
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview User info page
3
+ * @module pages/user
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const inquirer = require('inquirer');
9
+
10
+ const { connections } = require('../services');
11
+ const { getLogoWidth, getColWidths, drawBoxHeader, drawBoxFooter, draw2ColHeader, visibleLength, padText } = require('../ui');
12
+
13
+ /**
14
+ * Shows user information
15
+ * @param {Object} service - Current service
16
+ */
17
+ const showUserInfo = async (service) => {
18
+ const spinner = ora('Fetching user info...').start();
19
+ const boxWidth = getLogoWidth();
20
+ const { col1, col2 } = getColWidths(boxWidth);
21
+
22
+ // Helper for row formatting
23
+ const fmtRow = (label, value, colW) => {
24
+ const labelStr = ' ' + label.padEnd(14);
25
+ const valueVisible = visibleLength(value || '');
26
+ const totalVisible = labelStr.length + valueVisible;
27
+ const padding = Math.max(0, colW - totalVisible);
28
+ return chalk.white(labelStr) + value + ' '.repeat(padding);
29
+ };
30
+
31
+ // Get user info
32
+ let userInfo = null;
33
+ let accountCount = 0;
34
+
35
+ if (service && service.user) {
36
+ userInfo = service.user;
37
+ } else if (service) {
38
+ const result = await service.getUser();
39
+ if (result.success) {
40
+ userInfo = result.user;
41
+ }
42
+ }
43
+
44
+ // Get account count
45
+ if (connections.count() > 0) {
46
+ const accounts = await connections.getAllAccounts();
47
+ accountCount = accounts.length;
48
+ } else if (service) {
49
+ const result = await service.getTradingAccounts();
50
+ if (result.success) {
51
+ accountCount = result.accounts.length;
52
+ }
53
+ }
54
+
55
+ spinner.succeed('User info loaded');
56
+ console.log();
57
+
58
+ drawBoxHeader('USER INFO', boxWidth);
59
+
60
+ if (!userInfo) {
61
+ console.log(chalk.cyan('║') + padText(chalk.gray(' No user info available'), boxWidth - 2) + chalk.cyan('║'));
62
+ } else {
63
+ draw2ColHeader('PROFILE', 'CONNECTIONS', boxWidth);
64
+
65
+ // Username
66
+ const username = userInfo.userName || userInfo.username || 'Unknown';
67
+ const connCount = connections.count() || 1;
68
+ console.log(chalk.cyan('║') + fmtRow('Username:', chalk.cyan(username.toUpperCase()), col1) + chalk.cyan('│') + fmtRow('Connections:', chalk.cyan(connCount.toString()), col2) + chalk.cyan('║'));
69
+
70
+ // Email
71
+ const email = userInfo.email || 'N/A';
72
+ console.log(chalk.cyan('║') + fmtRow('Email:', chalk.white(email), col1) + chalk.cyan('│') + fmtRow('Accounts:', chalk.cyan(accountCount.toString()), col2) + chalk.cyan('║'));
73
+
74
+ // User ID
75
+ const userId = userInfo.userId || userInfo.id || 'N/A';
76
+ const platform = service.propfirm?.name || 'ProjectX';
77
+ console.log(chalk.cyan('║') + fmtRow('User ID:', chalk.gray(userId.toString()), col1) + chalk.cyan('│') + fmtRow('Platform:', chalk.magenta(platform), col2) + chalk.cyan('║'));
78
+
79
+ // First/Last Name
80
+ const firstName = userInfo.firstName || '';
81
+ const lastName = userInfo.lastName || '';
82
+ const fullName = (firstName + ' ' + lastName).trim() || 'N/A';
83
+ console.log(chalk.cyan('║') + fmtRow('Name:', chalk.white(fullName), col1) + chalk.cyan('│') + padText('', col2) + chalk.cyan('║'));
84
+ }
85
+
86
+ drawBoxFooter(boxWidth);
87
+ console.log();
88
+
89
+ await inquirer.prompt([{ type: 'input', name: 'c', message: 'Press Enter to continue...' }]);
90
+ };
91
+
92
+ module.exports = { showUserInfo };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @fileoverview Encryption utilities for secure data storage
3
+ * @module security/encryption
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const os = require('os');
8
+
9
+ // Algorithm configuration
10
+ const ALGORITHM = 'aes-256-gcm';
11
+ const IV_LENGTH = 16;
12
+ const AUTH_TAG_LENGTH = 16;
13
+ const SALT_LENGTH = 32;
14
+ const KEY_LENGTH = 32;
15
+ const ITERATIONS = 100000;
16
+
17
+ /**
18
+ * Derives a unique machine key from hardware identifiers
19
+ * @returns {string} Machine-specific key
20
+ * @private
21
+ */
22
+ const getMachineKey = () => {
23
+ const components = [
24
+ os.hostname(),
25
+ os.platform(),
26
+ os.arch(),
27
+ os.cpus()[0]?.model || 'unknown',
28
+ os.homedir(),
29
+ process.env.USER || process.env.USERNAME || 'user'
30
+ ];
31
+ return crypto.createHash('sha256').update(components.join('|')).digest('hex');
32
+ };
33
+
34
+ /**
35
+ * Derives encryption key from password and salt using PBKDF2
36
+ * @param {string} password - Password to derive key from
37
+ * @param {Buffer} salt - Salt for key derivation
38
+ * @returns {Buffer} Derived key
39
+ * @private
40
+ */
41
+ const deriveKey = (password, salt) => {
42
+ return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha512');
43
+ };
44
+
45
+ /**
46
+ * Encrypts data using AES-256-GCM
47
+ * @param {string} plaintext - Data to encrypt
48
+ * @param {string} [password] - Optional password (uses machine key if not provided)
49
+ * @returns {string} Encrypted data as hex string (salt:iv:authTag:ciphertext)
50
+ */
51
+ const encrypt = (plaintext, password = null) => {
52
+ if (!plaintext) return '';
53
+
54
+ const secret = password || getMachineKey();
55
+ const salt = crypto.randomBytes(SALT_LENGTH);
56
+ const key = deriveKey(secret, salt);
57
+ const iv = crypto.randomBytes(IV_LENGTH);
58
+
59
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
60
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
61
+ encrypted += cipher.final('hex');
62
+
63
+ const authTag = cipher.getAuthTag();
64
+
65
+ // Format: salt:iv:authTag:ciphertext (all in hex)
66
+ return [
67
+ salt.toString('hex'),
68
+ iv.toString('hex'),
69
+ authTag.toString('hex'),
70
+ encrypted
71
+ ].join(':');
72
+ };
73
+
74
+ /**
75
+ * Decrypts data encrypted with AES-256-GCM
76
+ * @param {string} encryptedData - Encrypted data as hex string
77
+ * @param {string} [password] - Optional password (uses machine key if not provided)
78
+ * @returns {string|null} Decrypted plaintext or null if decryption fails
79
+ */
80
+ const decrypt = (encryptedData, password = null) => {
81
+ if (!encryptedData) return null;
82
+
83
+ try {
84
+ const parts = encryptedData.split(':');
85
+ if (parts.length !== 4) return null;
86
+
87
+ const [saltHex, ivHex, authTagHex, ciphertext] = parts;
88
+
89
+ const salt = Buffer.from(saltHex, 'hex');
90
+ const iv = Buffer.from(ivHex, 'hex');
91
+ const authTag = Buffer.from(authTagHex, 'hex');
92
+
93
+ const secret = password || getMachineKey();
94
+ const key = deriveKey(secret, salt);
95
+
96
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
97
+ decipher.setAuthTag(authTag);
98
+
99
+ let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
100
+ decrypted += decipher.final('utf8');
101
+
102
+ return decrypted;
103
+ } catch (error) {
104
+ // Decryption failed (wrong key, tampered data, etc.)
105
+ return null;
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Hashes a password using SHA-512 with salt
111
+ * @param {string} password - Password to hash
112
+ * @returns {string} Hashed password (salt:hash in hex)
113
+ */
114
+ const hashPassword = (password) => {
115
+ const salt = crypto.randomBytes(SALT_LENGTH);
116
+ const hash = crypto.pbkdf2Sync(password, salt, ITERATIONS, 64, 'sha512');
117
+ return salt.toString('hex') + ':' + hash.toString('hex');
118
+ };
119
+
120
+ /**
121
+ * Verifies a password against a hash
122
+ * @param {string} password - Password to verify
123
+ * @param {string} storedHash - Stored hash (salt:hash)
124
+ * @returns {boolean} True if password matches
125
+ */
126
+ const verifyPassword = (password, storedHash) => {
127
+ try {
128
+ const [saltHex, hashHex] = storedHash.split(':');
129
+ const salt = Buffer.from(saltHex, 'hex');
130
+ const hash = crypto.pbkdf2Sync(password, salt, ITERATIONS, 64, 'sha512');
131
+ return hash.toString('hex') === hashHex;
132
+ } catch {
133
+ return false;
134
+ }
135
+ };
136
+
137
+ /**
138
+ * Generates a secure random token
139
+ * @param {number} [length=32] - Token length in bytes
140
+ * @returns {string} Random token as hex string
141
+ */
142
+ const generateToken = (length = 32) => {
143
+ return crypto.randomBytes(length).toString('hex');
144
+ };
145
+
146
+ /**
147
+ * Masks sensitive data for logging
148
+ * @param {string} data - Data to mask
149
+ * @param {number} [visibleChars=4] - Number of visible characters at start/end
150
+ * @returns {string} Masked data
151
+ */
152
+ const maskSensitive = (data, visibleChars = 4) => {
153
+ if (!data || data.length <= visibleChars * 2) {
154
+ return '****';
155
+ }
156
+ const start = data.substring(0, visibleChars);
157
+ const end = data.substring(data.length - visibleChars);
158
+ return `${start}****${end}`;
159
+ };
160
+
161
+ module.exports = {
162
+ encrypt,
163
+ decrypt,
164
+ hashPassword,
165
+ verifyPassword,
166
+ generateToken,
167
+ maskSensitive
168
+ };