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.
- package/README.md +128 -136
- package/bin/cli.js +28 -2076
- package/package.json +3 -3
- package/src/app.js +550 -0
- package/src/config/index.js +16 -2
- package/src/config/propfirms.js +324 -12
- package/src/pages/accounts.js +115 -0
- package/src/pages/algo.js +538 -0
- package/src/pages/index.js +13 -2
- package/src/pages/orders.js +114 -0
- package/src/pages/positions.js +115 -0
- package/src/pages/stats.js +212 -3
- package/src/pages/user.js +92 -0
- package/src/security/encryption.js +168 -0
- package/src/security/index.js +61 -0
- package/src/security/rateLimit.js +155 -0
- package/src/security/validation.js +253 -0
- package/src/services/hqx-server.js +34 -17
- package/src/services/index.js +2 -1
- package/src/services/projectx.js +383 -35
- package/src/services/session.js +150 -38
- package/src/ui/index.js +4 -1
- package/src/ui/menu.js +154 -0
- package/src/services/local-storage.js +0 -309
|
@@ -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 };
|
package/src/pages/stats.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
+
};
|