hedgequantx 1.8.43 → 1.8.45

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.
@@ -7,7 +7,6 @@ const ora = require('ora');
7
7
  const asciichart = require('asciichart');
8
8
 
9
9
  const { connections } = require('../services');
10
- const { ACCOUNT_STATUS, ACCOUNT_TYPE } = require('../config');
11
10
  const { getLogoWidth, visibleLength, drawBoxHeader, drawBoxFooter, getColWidths, draw2ColHeader, draw2ColSeparator, fmtRow } = require('../ui');
12
11
  const { prompts } = require('../utils');
13
12
 
@@ -15,465 +14,499 @@ const { prompts } = require('../utils');
15
14
  * Show Stats Page
16
15
  */
17
16
  const showStats = async (service) => {
18
- const spinner = ora({ text: 'Fetching stats for all accounts...', color: 'yellow' }).start();
17
+ let spinner;
19
18
 
20
- let allAccountsData = [];
21
-
22
- // Get accounts from all connections
23
- if (connections.count() > 0) {
24
- for (const conn of connections.getAll()) {
19
+ try {
20
+ // Step 1: Get connections
21
+ spinner = ora({ text: 'Loading connections...', color: 'yellow' }).start();
22
+
23
+ const allConns = connections.count() > 0
24
+ ? connections.getAll()
25
+ : (service ? [{ service, propfirm: service.propfirm?.name || 'Unknown', type: 'single' }] : []);
26
+
27
+ if (allConns.length === 0) {
28
+ spinner.fail('No connections found');
29
+ await prompts.waitForEnter();
30
+ return;
31
+ }
32
+ spinner.succeed(`Found ${allConns.length} connection(s)`);
33
+
34
+ // Step 2: Fetch accounts from each connection
35
+ let allAccountsData = [];
36
+
37
+ for (const conn of allConns) {
38
+ const propfirmName = conn.propfirm || conn.type || 'Unknown';
39
+ spinner = ora({ text: `Fetching accounts from ${propfirmName}...`, color: 'yellow' }).start();
40
+
25
41
  try {
26
42
  const result = await conn.service.getTradingAccounts();
27
- if (result.success && result.accounts) {
43
+ if (result.success && result.accounts && result.accounts.length > 0) {
28
44
  result.accounts.forEach(account => {
29
45
  allAccountsData.push({
30
46
  ...account,
31
- propfirm: conn.propfirm || conn.type,
47
+ propfirm: propfirmName,
32
48
  service: conn.service
33
49
  });
34
50
  });
51
+ spinner.succeed(`${propfirmName}: ${result.accounts.length} account(s)`);
52
+ } else {
53
+ spinner.warn(`${propfirmName}: No accounts`);
35
54
  }
36
- } catch (e) { /* ignore */ }
55
+ } catch (e) {
56
+ spinner.fail(`${propfirmName}: Failed`);
57
+ }
37
58
  }
38
- } else if (service) {
39
- const result = await service.getTradingAccounts();
40
- if (result.success && result.accounts) {
41
- allAccountsData = result.accounts.map(a => ({ ...a, service }));
59
+
60
+ if (allAccountsData.length === 0) {
61
+ console.log(chalk.yellow('\n No accounts found.'));
62
+ await prompts.waitForEnter();
63
+ return;
42
64
  }
43
- }
44
-
45
- if (allAccountsData.length === 0) {
46
- spinner.fail('No accounts found');
47
- await prompts.waitForEnter();
48
- return;
49
- }
50
65
 
51
- // Remove duplicates by accountId
52
- const seen = new Set();
53
- allAccountsData = allAccountsData.filter(acc => {
54
- if (seen.has(acc.accountId)) return false;
55
- seen.add(acc.accountId);
56
- return true;
57
- });
66
+ // Remove duplicates by accountId
67
+ const seen = new Set();
68
+ allAccountsData = allAccountsData.filter(acc => {
69
+ const id = String(acc.accountId);
70
+ if (seen.has(id)) return false;
71
+ seen.add(id);
72
+ return true;
73
+ });
58
74
 
59
- // Filter only active accounts (status === 0)
60
- const activeAccounts = allAccountsData.filter(acc => acc.status === 0);
61
-
62
- if (activeAccounts.length === 0) {
63
- spinner.fail('No active accounts found');
64
- await prompts.waitForEnter();
65
- return;
66
- }
75
+ // Filter only active accounts (status === 0)
76
+ const activeAccounts = allAccountsData.filter(acc => acc.status === 0);
77
+
78
+ if (activeAccounts.length === 0) {
79
+ console.log(chalk.yellow('\n No active accounts found.'));
80
+ await prompts.waitForEnter();
81
+ return;
82
+ }
67
83
 
68
- // Collect REAL stats only - no estimation
69
- let totalBalance = null;
70
- let totalPnL = null;
71
- let allTrades = [];
72
- let totalOpenPositions = 0;
73
- let totalOpenOrders = 0;
74
- let allDailyStats = [];
84
+ // Step 3: Collect stats for each account
85
+ let totalBalance = 0;
86
+ let totalPnL = 0;
87
+ let totalStartingBalance = 0;
88
+ let allTrades = [];
89
+ let totalOpenPositions = 0;
90
+ let totalOpenOrders = 0;
91
+ let hasBalanceData = false;
92
+ let hasPnLData = false;
75
93
 
76
- spinner.text = 'Fetching detailed stats...';
94
+ for (let i = 0; i < activeAccounts.length; i++) {
95
+ const account = activeAccounts[i];
96
+ const svc = account.service;
97
+ const accName = String(account.accountId || account.accountName || `Account ${i+1}`).substring(0, 20);
98
+
99
+ spinner = ora({ text: `Loading stats for ${accName}...`, color: 'yellow' }).start();
100
+
101
+ try {
102
+ // Balance
103
+ if (account.balance !== null && account.balance !== undefined) {
104
+ totalBalance += account.balance;
105
+ hasBalanceData = true;
106
+ }
107
+
108
+ // P&L
109
+ if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
110
+ totalPnL += account.profitAndLoss;
111
+ hasPnLData = true;
112
+ }
113
+
114
+ // Starting balance
115
+ if (account.balance !== null && account.balance !== undefined) {
116
+ const pnl = account.profitAndLoss || 0;
117
+ totalStartingBalance += (account.balance - pnl);
118
+ }
119
+
120
+ // Positions
121
+ spinner.text = `Loading positions for ${accName}...`;
122
+ try {
123
+ const posResult = await svc.getPositions(account.accountId);
124
+ if (posResult.success && posResult.positions) {
125
+ totalOpenPositions += posResult.positions.length;
126
+ }
127
+ } catch (e) {}
128
+
129
+ // Orders
130
+ spinner.text = `Loading orders for ${accName}...`;
131
+ try {
132
+ const ordResult = await svc.getOrders(account.accountId);
133
+ if (ordResult.success && ordResult.orders) {
134
+ totalOpenOrders += ordResult.orders.filter(o => o.status === 1).length;
135
+ }
136
+ } catch (e) {}
137
+
138
+ // Lifetime stats
139
+ spinner.text = `Loading lifetime stats for ${accName}...`;
140
+ if (typeof svc.getLifetimeStats === 'function') {
141
+ try {
142
+ const lifetimeResult = await svc.getLifetimeStats(account.accountId);
143
+ if (lifetimeResult.success && lifetimeResult.stats) {
144
+ account.lifetimeStats = lifetimeResult.stats;
145
+ }
146
+ } catch (e) {}
147
+ }
148
+
149
+ // Trade history
150
+ spinner.text = `Loading trade history for ${accName}...`;
151
+ if (typeof svc.getTradeHistory === 'function') {
152
+ try {
153
+ const tradesResult = await svc.getTradeHistory(account.accountId, 30);
154
+ if (tradesResult.success && tradesResult.trades && tradesResult.trades.length > 0) {
155
+ allTrades = allTrades.concat(tradesResult.trades.map(t => ({
156
+ ...t,
157
+ accountName: account.accountName,
158
+ propfirm: account.propfirm
159
+ })));
160
+ }
161
+ } catch (e) {}
162
+ }
163
+
164
+ spinner.succeed(`${accName}: Stats loaded`);
165
+ } catch (e) {
166
+ spinner.fail(`${accName}: Failed to load stats`);
167
+ }
168
+ }
77
169
 
78
- for (const account of activeAccounts) {
79
- const svc = account.service;
170
+ // Aggregate stats
171
+ spinner = ora({ text: 'Calculating metrics...', color: 'yellow' }).start();
172
+
173
+ let stats = {
174
+ totalTrades: 0, winningTrades: 0, losingTrades: 0,
175
+ totalWinAmount: 0, totalLossAmount: 0,
176
+ bestTrade: 0, worstTrade: 0, totalVolume: 0,
177
+ maxConsecutiveWins: 0, maxConsecutiveLosses: 0,
178
+ longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
179
+ };
80
180
 
81
- // Only add REAL balance from API
82
- if (account.balance !== null && account.balance !== undefined) {
83
- totalBalance = (totalBalance || 0) + account.balance;
181
+ for (const account of activeAccounts) {
182
+ if (account.lifetimeStats) {
183
+ const s = account.lifetimeStats;
184
+ stats.totalTrades += s.totalTrades || 0;
185
+ stats.winningTrades += s.winningTrades || 0;
186
+ stats.losingTrades += s.losingTrades || 0;
187
+ stats.totalWinAmount += s.totalWinAmount || 0;
188
+ stats.totalLossAmount += s.totalLossAmount || 0;
189
+ stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
190
+ stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
191
+ stats.totalVolume += s.totalVolume || 0;
192
+ stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
193
+ stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
194
+ stats.longTrades += s.longTrades || 0;
195
+ stats.shortTrades += s.shortTrades || 0;
196
+ }
84
197
  }
85
198
 
86
- // Only add REAL P&L from API
87
- if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
88
- totalPnL = (totalPnL || 0) + account.profitAndLoss;
199
+ // If no stats from API, calculate from trades
200
+ if (stats.totalTrades === 0 && allTrades.length > 0) {
201
+ stats.totalTrades = allTrades.length;
202
+ let consecutiveWins = 0, consecutiveLosses = 0;
203
+
204
+ for (const trade of allTrades) {
205
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
206
+ const size = trade.size || trade.quantity || 1;
207
+ const side = trade.side;
208
+
209
+ stats.totalVolume += Math.abs(size);
210
+
211
+ if (side === 0) {
212
+ stats.longTrades++;
213
+ if (pnl > 0) stats.longWins++;
214
+ } else if (side === 1) {
215
+ stats.shortTrades++;
216
+ if (pnl > 0) stats.shortWins++;
217
+ }
218
+
219
+ if (pnl > 0) {
220
+ stats.winningTrades++;
221
+ stats.totalWinAmount += pnl;
222
+ consecutiveWins++;
223
+ consecutiveLosses = 0;
224
+ if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
225
+ if (pnl > stats.bestTrade) stats.bestTrade = pnl;
226
+ } else if (pnl < 0) {
227
+ stats.losingTrades++;
228
+ stats.totalLossAmount += Math.abs(pnl);
229
+ consecutiveLosses++;
230
+ consecutiveWins = 0;
231
+ if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
232
+ if (pnl < stats.worstTrade) stats.worstTrade = pnl;
233
+ }
234
+ }
89
235
  }
236
+
237
+ spinner.succeed('All stats loaded');
238
+ console.log();
90
239
 
91
- // Positions & Orders
92
- const posResult = await svc.getPositions(account.accountId);
93
- if (posResult.success) totalOpenPositions += posResult.positions.length;
240
+ // Display
241
+ const boxWidth = getLogoWidth();
242
+ const { col1, col2 } = getColWidths(boxWidth);
94
243
 
95
- const ordResult = await svc.getOrders(account.accountId);
96
- if (ordResult.success) totalOpenOrders += ordResult.orders.filter(o => o.status === 1).length;
244
+ // Use 0 if null
245
+ if (!hasBalanceData) totalBalance = 0;
246
+ if (!hasPnLData) totalPnL = 0;
97
247
 
98
- // Lifetime stats (if available)
99
- if (typeof svc.getLifetimeStats === 'function') {
100
- const lifetimeResult = await svc.getLifetimeStats(account.accountId);
101
- if (lifetimeResult.success && lifetimeResult.stats) {
102
- account.lifetimeStats = lifetimeResult.stats;
103
- }
104
- }
248
+ // Calculated metrics
249
+ const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : '0.0';
250
+ const avgWin = stats.winningTrades > 0 ? (stats.totalWinAmount / stats.winningTrades).toFixed(2) : '0.00';
251
+ const avgLoss = stats.losingTrades > 0 ? (stats.totalLossAmount / stats.losingTrades).toFixed(2) : '0.00';
252
+ const profitFactor = stats.totalLossAmount > 0 ? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2) : '0.00';
253
+ const netPnL = stats.totalWinAmount - stats.totalLossAmount;
254
+ const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : '0.00';
255
+ const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '0.0';
256
+ const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '0.0';
105
257
 
106
- // Daily stats (if available)
107
- if (typeof svc.getDailyStats === 'function') {
108
- const dailyResult = await svc.getDailyStats(account.accountId);
109
- if (dailyResult.success && dailyResult.stats) {
110
- account.dailyStats = dailyResult.stats;
111
- allDailyStats = allDailyStats.concat(dailyResult.stats);
112
- }
113
- }
258
+ // Advanced quantitative metrics
259
+ const tradePnLs = allTrades.map(t => t.profitAndLoss || t.pnl || 0);
260
+ const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
114
261
 
115
- // Trade history (if available)
116
- if (typeof svc.getTradeHistory === 'function') {
117
- const tradesResult = await svc.getTradeHistory(account.accountId, 30);
118
- if (tradesResult.success && tradesResult.trades.length > 0) {
119
- allTrades = allTrades.concat(tradesResult.trades.map(t => ({
120
- ...t,
121
- accountName: account.accountName,
122
- propfirm: account.propfirm
123
- })));
124
- }
125
- }
126
- }
127
-
128
- // NO estimation - only real data from API
129
-
130
- // Aggregate stats
131
- let stats = {
132
- totalTrades: 0, winningTrades: 0, losingTrades: 0,
133
- totalWinAmount: 0, totalLossAmount: 0,
134
- bestTrade: 0, worstTrade: 0, totalVolume: 0,
135
- maxConsecutiveWins: 0, maxConsecutiveLosses: 0,
136
- longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
137
- };
138
-
139
- for (const account of activeAccounts) {
140
- if (account.lifetimeStats) {
141
- const s = account.lifetimeStats;
142
- stats.totalTrades += s.totalTrades || 0;
143
- stats.winningTrades += s.winningTrades || 0;
144
- stats.losingTrades += s.losingTrades || 0;
145
- stats.totalWinAmount += s.totalWinAmount || 0;
146
- stats.totalLossAmount += s.totalLossAmount || 0;
147
- stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
148
- stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
149
- stats.totalVolume += s.totalVolume || 0;
150
- stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
151
- stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
152
- stats.longTrades += s.longTrades || 0;
153
- stats.shortTrades += s.shortTrades || 0;
154
- }
155
- }
156
-
157
- // If no stats from API, calculate from trades
158
- if (stats.totalTrades === 0 && allTrades.length > 0) {
159
- stats.totalTrades = allTrades.length;
160
- let consecutiveWins = 0, consecutiveLosses = 0;
161
-
162
- for (const trade of allTrades) {
163
- const pnl = trade.profitAndLoss || trade.pnl || 0;
164
- const size = trade.size || trade.quantity || 1;
165
- const side = trade.side;
262
+ // Standard deviation
263
+ const variance = tradePnLs.length > 0
264
+ ? tradePnLs.reduce((sum, pnl) => sum + Math.pow(pnl - avgReturn, 2), 0) / tradePnLs.length
265
+ : 0;
266
+ const stdDev = Math.sqrt(variance);
267
+
268
+ // Downside deviation
269
+ const downsideReturns = tradePnLs.filter(pnl => pnl < 0);
270
+ const downsideVariance = downsideReturns.length > 0
271
+ ? downsideReturns.reduce((sum, pnl) => sum + Math.pow(pnl, 2), 0) / downsideReturns.length
272
+ : 0;
273
+ const downsideDev = Math.sqrt(downsideVariance);
274
+
275
+ // Ratios
276
+ const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : '0.00';
277
+ const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : '0.00';
278
+
279
+ // Max Drawdown
280
+ let maxDrawdown = 0;
281
+ let peak = totalStartingBalance || 100000;
282
+ let equity = peak;
283
+ tradePnLs.forEach(pnl => {
284
+ equity += pnl;
285
+ if (equity > peak) peak = equity;
286
+ const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
287
+ if (drawdown > maxDrawdown) maxDrawdown = drawdown;
288
+ });
289
+
290
+ const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
291
+ const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : '0.00';
292
+ const calmarRatio = maxDrawdown > 0 ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : '0.00';
293
+
294
+ const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
295
+ const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
296
+
297
+ // Main Summary
298
+ drawBoxHeader('HQX STATS', boxWidth);
299
+ draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
300
+
301
+ console.log(chalk.cyan('\u2551') + fmtRow('Connections:', chalk.cyan(String(connections.count() || 1)), col1) + chalk.cyan('\u2502') + fmtRow('Total Trades:', chalk.white(String(stats.totalTrades)), col2) + chalk.cyan('\u2551'));
302
+ console.log(chalk.cyan('\u2551') + fmtRow('Total Accounts:', chalk.cyan(String(activeAccounts.length)), col1) + chalk.cyan('\u2502') + fmtRow('Winning Trades:', chalk.green(String(stats.winningTrades)), col2) + chalk.cyan('\u2551'));
303
+ console.log(chalk.cyan('\u2551') + fmtRow('Total Balance:', totalBalanceColor('$' + totalBalance.toLocaleString(undefined, {minimumFractionDigits: 2})), col1) + chalk.cyan('\u2502') + fmtRow('Losing Trades:', chalk.red(String(stats.losingTrades)), col2) + chalk.cyan('\u2551'));
304
+ console.log(chalk.cyan('\u2551') + fmtRow('Starting Balance:', chalk.white('$' + totalStartingBalance.toLocaleString(undefined, {minimumFractionDigits: 2})), col1) + chalk.cyan('\u2502') + fmtRow('Win Rate:', parseFloat(winRate) >= 50 ? chalk.green(winRate + '%') : chalk.yellow(winRate + '%'), col2) + chalk.cyan('\u2551'));
305
+ console.log(chalk.cyan('\u2551') + fmtRow('Total P&L:', pnlColor((totalPnL >= 0 ? '+' : '') + '$' + totalPnL.toLocaleString(undefined, {minimumFractionDigits: 2}) + ' (' + returnPercent + '%)'), col1) + chalk.cyan('\u2502') + fmtRow('Long Trades:', chalk.white(stats.longTrades + ' (' + longWinRate + '%)'), col2) + chalk.cyan('\u2551'));
306
+ console.log(chalk.cyan('\u2551') + fmtRow('Open Positions:', chalk.white(String(totalOpenPositions)), col1) + chalk.cyan('\u2502') + fmtRow('Short Trades:', chalk.white(stats.shortTrades + ' (' + shortWinRate + '%)'), col2) + chalk.cyan('\u2551'));
307
+ console.log(chalk.cyan('\u2551') + fmtRow('Open Orders:', chalk.white(String(totalOpenOrders)), col1) + chalk.cyan('\u2502') + fmtRow('Volume:', chalk.white(stats.totalVolume + ' contracts'), col2) + chalk.cyan('\u2551'));
308
+
309
+ // P&L Metrics
310
+ draw2ColSeparator(boxWidth);
311
+ draw2ColHeader('P&L METRICS', 'RISK METRICS', boxWidth);
312
+
313
+ const pfColor = parseFloat(profitFactor) >= 1.5 ? chalk.green(profitFactor) : parseFloat(profitFactor) >= 1 ? chalk.yellow(profitFactor) : chalk.red(profitFactor);
314
+
315
+ console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnL >= 0 ? chalk.green('$' + netPnL.toFixed(2)) : chalk.red('$' + netPnL.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
316
+ console.log(chalk.cyan('\u2551') + fmtRow('Gross Profit:', chalk.green('$' + stats.totalWinAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Wins:', chalk.green(String(stats.maxConsecutiveWins)), col2) + chalk.cyan('\u2551'));
317
+ console.log(chalk.cyan('\u2551') + fmtRow('Gross Loss:', chalk.red('-$' + stats.totalLossAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Loss:', chalk.red(String(stats.maxConsecutiveLosses)), col2) + chalk.cyan('\u2551'));
318
+ 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'));
319
+ 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'));
320
+
321
+ // Quantitative Metrics
322
+ draw2ColSeparator(boxWidth);
323
+ draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
324
+
325
+ const sharpeColor = parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
326
+ const sortinoColor = parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
327
+ const ddColor = maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
328
+ const rrColor = parseFloat(riskRewardRatio) >= 2 ? chalk.green : parseFloat(riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
329
+
330
+ console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(riskRewardRatio), col2) + chalk.cyan('\u2551'));
331
+ console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
332
+ 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'));
333
+ 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'));
334
+
335
+ drawBoxFooter(boxWidth);
336
+
337
+ // Equity Curve
338
+ console.log();
339
+ drawBoxHeader('EQUITY CURVE', boxWidth);
340
+
341
+ const chartInnerWidth = boxWidth - 2;
342
+
343
+ if (allTrades.length > 0) {
344
+ const yAxisWidth = 10;
345
+ const chartAreaWidth = chartInnerWidth - yAxisWidth - 4;
166
346
 
167
- stats.totalVolume += Math.abs(size);
347
+ let equityData = [totalStartingBalance || 100000];
348
+ let eqVal = equityData[0];
349
+ allTrades.forEach(trade => {
350
+ eqVal += (trade.profitAndLoss || trade.pnl || 0);
351
+ equityData.push(eqVal);
352
+ });
168
353
 
169
- if (side === 0) {
170
- stats.longTrades++;
171
- if (pnl > 0) stats.longWins++;
172
- } else if (side === 1) {
173
- stats.shortTrades++;
174
- if (pnl > 0) stats.shortWins++;
354
+ const maxDataPoints = chartAreaWidth - 5;
355
+ if (equityData.length > maxDataPoints) {
356
+ const step = Math.ceil(equityData.length / maxDataPoints);
357
+ equityData = equityData.filter((_, i) => i % step === 0);
175
358
  }
176
359
 
177
- if (pnl > 0) {
178
- stats.winningTrades++;
179
- stats.totalWinAmount += pnl;
180
- consecutiveWins++;
181
- consecutiveLosses = 0;
182
- if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
183
- if (pnl > stats.bestTrade) stats.bestTrade = pnl;
184
- } else if (pnl < 0) {
185
- stats.losingTrades++;
186
- stats.totalLossAmount += Math.abs(pnl);
187
- consecutiveLosses++;
188
- consecutiveWins = 0;
189
- if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
190
- if (pnl < stats.worstTrade) stats.worstTrade = pnl;
191
- }
360
+ const chartConfig = {
361
+ height: 10,
362
+ colors: [equityData[equityData.length - 1] < equityData[0] ? asciichart.red : asciichart.green],
363
+ format: (x) => ('$' + (x / 1000).toFixed(0) + 'K').padStart(yAxisWidth)
364
+ };
365
+
366
+ const chart = asciichart.plot(equityData, chartConfig);
367
+ chart.split('\n').forEach(line => {
368
+ let chartLine = ' ' + line;
369
+ const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
370
+ if (len < chartInnerWidth) chartLine += ' '.repeat(chartInnerWidth - len);
371
+ console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
372
+ });
373
+ } else {
374
+ const msg = ' No trade data available';
375
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(chartInnerWidth - msg.length) + chalk.cyan('\u2551'));
192
376
  }
193
- }
194
-
195
- spinner.succeed('Stats loaded');
196
- console.log();
197
-
198
- // Display
199
- const boxWidth = getLogoWidth();
200
- const { col1, col2 } = getColWidths(boxWidth);
201
-
202
- // Calculated metrics
203
- const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : '0.0';
204
- const avgWin = stats.winningTrades > 0 ? (stats.totalWinAmount / stats.winningTrades).toFixed(2) : '0.00';
205
- const avgLoss = stats.losingTrades > 0 ? (stats.totalLossAmount / stats.losingTrades).toFixed(2) : '0.00';
206
- const profitFactor = stats.totalLossAmount > 0 ? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2) : '0.00';
207
- const netPnL = stats.totalWinAmount - stats.totalLossAmount;
208
- const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : '0.00';
209
- const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '0.0';
210
- const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '0.0';
211
-
212
- // Advanced quantitative metrics
213
- const tradePnLs = allTrades.map(t => t.profitAndLoss || t.pnl || 0);
214
- const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
215
-
216
- // Standard deviation
217
- const variance = tradePnLs.length > 0
218
- ? tradePnLs.reduce((sum, pnl) => sum + Math.pow(pnl - avgReturn, 2), 0) / tradePnLs.length
219
- : 0;
220
- const stdDev = Math.sqrt(variance);
221
-
222
- // Downside deviation (for Sortino)
223
- const downsideReturns = tradePnLs.filter(pnl => pnl < 0);
224
- const downsideVariance = downsideReturns.length > 0
225
- ? downsideReturns.reduce((sum, pnl) => sum + Math.pow(pnl, 2), 0) / downsideReturns.length
226
- : 0;
227
- const downsideDev = Math.sqrt(downsideVariance);
228
-
229
- // Sharpe Ratio (simplified - assumes risk-free rate = 0)
230
- const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : '0.00';
231
-
232
- // Sortino Ratio
233
- const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : '0.00';
234
-
235
- // Max Drawdown
236
- let maxDrawdown = 0;
237
- let peak = totalStartingBalance;
238
- let equity = totalStartingBalance;
239
- tradePnLs.forEach(pnl => {
240
- equity += pnl;
241
- if (equity > peak) peak = equity;
242
- const drawdown = (peak - equity) / peak * 100;
243
- if (drawdown > maxDrawdown) maxDrawdown = drawdown;
244
- });
245
-
246
- // Expectancy (average profit per trade)
247
- const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
248
-
249
- // Risk/Reward Ratio
250
- const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : '0.00';
251
-
252
- // Calmar Ratio (return / max drawdown)
253
- const calmarRatio = maxDrawdown > 0 ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : '0.00';
254
-
255
- const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
256
- const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
257
-
258
- // Main Summary
259
- drawBoxHeader('HQX STATS', boxWidth);
260
- draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
261
-
262
- 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'));
263
- 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'));
264
- 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'));
265
- 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'));
266
- 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'));
267
- console.log(chalk.cyan('\u2551') + fmtRow('Open Positions:', chalk.white(totalOpenPositions.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Short Trades:', chalk.white(stats.shortTrades + ' (' + shortWinRate + '%)'), col2) + chalk.cyan('\u2551'));
268
- console.log(chalk.cyan('\u2551') + fmtRow('Open Orders:', chalk.white(totalOpenOrders.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Volume:', chalk.white(stats.totalVolume + ' contracts'), col2) + chalk.cyan('\u2551'));
269
-
270
- // P&L Metrics
271
- draw2ColSeparator(boxWidth);
272
- draw2ColHeader('P&L METRICS', 'RISK METRICS', boxWidth);
273
-
274
- const pfColor = parseFloat(profitFactor) >= 1.5 ? chalk.green(profitFactor) : parseFloat(profitFactor) >= 1 ? chalk.yellow(profitFactor) : chalk.red(profitFactor);
275
-
276
- console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnL >= 0 ? chalk.green('$' + netPnL.toFixed(2)) : chalk.red('$' + netPnL.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
277
- console.log(chalk.cyan('\u2551') + fmtRow('Gross Profit:', chalk.green('$' + stats.totalWinAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Wins:', chalk.green(stats.maxConsecutiveWins.toString()), col2) + chalk.cyan('\u2551'));
278
- console.log(chalk.cyan('\u2551') + fmtRow('Gross Loss:', chalk.red('-$' + stats.totalLossAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Loss:', chalk.red(stats.maxConsecutiveLosses.toString()), col2) + chalk.cyan('\u2551'));
279
- 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'));
280
- 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'));
281
-
282
- // Quantitative Metrics
283
- draw2ColSeparator(boxWidth);
284
- draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
285
-
286
- const sharpeColor = parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
287
- const sortinoColor = parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
288
- const ddColor = maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
289
- const rrColor = parseFloat(riskRewardRatio) >= 2 ? chalk.green : parseFloat(riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
290
-
291
- console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(riskRewardRatio), col2) + chalk.cyan('\u2551'));
292
- console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
293
- 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'));
294
- 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'));
295
-
296
- drawBoxFooter(boxWidth);
297
-
298
- // Equity Curve
299
- console.log();
300
- drawBoxHeader('EQUITY CURVE', boxWidth);
301
-
302
- const chartInnerWidth = boxWidth - 2;
303
-
304
- if (allTrades.length > 0) {
305
- const yAxisWidth = 10;
306
- const chartAreaWidth = chartInnerWidth - yAxisWidth - 4;
307
-
308
- let equityData = [totalStartingBalance];
309
- let equity = totalStartingBalance;
310
- allTrades.forEach(trade => {
311
- equity += (trade.profitAndLoss || trade.pnl || 0);
312
- equityData.push(equity);
313
- });
314
377
 
315
- const maxDataPoints = chartAreaWidth - 5;
316
- if (equityData.length > maxDataPoints) {
317
- const step = Math.ceil(equityData.length / maxDataPoints);
318
- equityData = equityData.filter((_, i) => i % step === 0);
319
- }
378
+ drawBoxFooter(boxWidth);
320
379
 
321
- const chartConfig = {
322
- height: 10,
323
- colors: [equityData[equityData.length - 1] < equityData[0] ? asciichart.red : asciichart.green],
324
- format: (x) => ('$' + (x / 1000).toFixed(0) + 'K').padStart(yAxisWidth)
325
- };
380
+ // Trades History
381
+ console.log();
382
+ drawBoxHeader('TRADES HISTORY', boxWidth);
326
383
 
327
- const chart = asciichart.plot(equityData, chartConfig);
328
- chart.split('\n').forEach(line => {
329
- let chartLine = ' ' + line;
330
- const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
331
- if (len < chartInnerWidth) chartLine += ' '.repeat(chartInnerWidth - len);
332
- console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
333
- });
334
- } else {
335
- const msg = ' No trade data available';
336
- console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(chartInnerWidth - msg.length) + chalk.cyan('\u2551'));
337
- }
338
-
339
- drawBoxFooter(boxWidth);
340
-
341
- // Trades History
342
- console.log();
343
- drawBoxHeader('TRADES HISTORY', boxWidth);
344
-
345
- const innerWidth = boxWidth - 2;
346
-
347
- if (allTrades.length > 0) {
348
- // Column widths
349
- const colTime = 12;
350
- const colSymbol = 10;
351
- const colEntry = 10;
352
- const colExit = 10;
353
- const colEntryP = 10;
354
- const colExitP = 10;
355
- const colPnL = 10;
356
- const colDir = 6;
357
- const colID = innerWidth - colTime - colSymbol - colEntry - colExit - colEntryP - colExitP - colPnL - colDir - 9;
358
-
359
- // Header
360
- const header =
361
- chalk.white(' Time'.padEnd(colTime)) + chalk.gray('|') +
362
- chalk.white('Symbol'.padEnd(colSymbol)) + chalk.gray('|') +
363
- chalk.white('Entry'.padEnd(colEntry)) + chalk.gray('|') +
364
- chalk.white('Exit'.padEnd(colExit)) + chalk.gray('|') +
365
- chalk.white('Entry $'.padEnd(colEntryP)) + chalk.gray('|') +
366
- chalk.white('Exit $'.padEnd(colExitP)) + chalk.gray('|') +
367
- chalk.white('P&L'.padEnd(colPnL)) + chalk.gray('|') +
368
- chalk.white('Dir'.padEnd(colDir)) + chalk.gray('|') +
369
- chalk.white('ID'.padEnd(colID));
370
-
371
- console.log(chalk.cyan('\u2551') + header + chalk.cyan('\u2551'));
372
- console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
373
-
374
- // Show last 10 trades
375
- const recentTrades = allTrades.slice(-10).reverse();
376
-
377
- for (const trade of recentTrades) {
378
- const time = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
379
- const symbol = (trade.contractName || trade.symbol || 'N/A').substring(0, colSymbol - 1);
380
- const entryTime = trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
381
- const exitTime = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
382
- const entryPrice = trade.entryPrice ? trade.entryPrice.toFixed(2) : 'N/A';
383
- const exitPrice = trade.exitPrice ? trade.exitPrice.toFixed(2) : 'N/A';
384
- const pnl = trade.profitAndLoss || trade.pnl || 0;
385
- const pnlStr = pnl >= 0 ? chalk.green('+$' + pnl.toFixed(0)) : chalk.red('-$' + Math.abs(pnl).toFixed(0));
386
- const direction = trade.side === 0 ? chalk.green('LONG') : trade.side === 1 ? chalk.red('SHORT') : chalk.gray('N/A');
387
- const tradeId = (trade.id || trade.tradeId || 'N/A').toString().substring(0, colID - 1);
384
+ const innerWidth = boxWidth - 2;
385
+
386
+ if (allTrades.length > 0) {
387
+ const colTime = 12;
388
+ const colSymbol = 10;
389
+ const colEntry = 10;
390
+ const colExit = 10;
391
+ const colEntryP = 10;
392
+ const colExitP = 10;
393
+ const colPnL = 10;
394
+ const colDir = 6;
395
+ const colID = innerWidth - colTime - colSymbol - colEntry - colExit - colEntryP - colExitP - colPnL - colDir - 9;
396
+
397
+ const header =
398
+ chalk.white(' Time'.padEnd(colTime)) + chalk.gray('|') +
399
+ chalk.white('Symbol'.padEnd(colSymbol)) + chalk.gray('|') +
400
+ chalk.white('Entry'.padEnd(colEntry)) + chalk.gray('|') +
401
+ chalk.white('Exit'.padEnd(colExit)) + chalk.gray('|') +
402
+ chalk.white('Entry $'.padEnd(colEntryP)) + chalk.gray('|') +
403
+ chalk.white('Exit $'.padEnd(colExitP)) + chalk.gray('|') +
404
+ chalk.white('P&L'.padEnd(colPnL)) + chalk.gray('|') +
405
+ chalk.white('Dir'.padEnd(colDir)) + chalk.gray('|') +
406
+ chalk.white('ID'.padEnd(colID));
407
+
408
+ console.log(chalk.cyan('\u2551') + header + chalk.cyan('\u2551'));
409
+ console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
388
410
 
389
- const row =
390
- (' ' + time).padEnd(colTime) + chalk.gray('|') +
391
- symbol.padEnd(colSymbol) + chalk.gray('|') +
392
- entryTime.padEnd(colEntry) + chalk.gray('|') +
393
- exitTime.padEnd(colExit) + chalk.gray('|') +
394
- entryPrice.padEnd(colEntryP) + chalk.gray('|') +
395
- exitPrice.padEnd(colExitP) + chalk.gray('|') +
396
- pnlStr.padEnd(colPnL + 10) + chalk.gray('|') + // +10 for color codes
397
- direction.padEnd(colDir + 10) + chalk.gray('|') + // +10 for color codes
398
- tradeId.padEnd(colID);
411
+ const recentTrades = allTrades.slice(-10).reverse();
399
412
 
400
- // Calculate visible length and pad
401
- const visLen = row.replace(/\x1b\[[0-9;]*m/g, '').length;
402
- const padding = innerWidth - visLen;
413
+ for (const trade of recentTrades) {
414
+ const time = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
415
+ const symbol = (trade.contractName || trade.symbol || 'N/A').substring(0, colSymbol - 1);
416
+ const entryTime = trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
417
+ const exitTime = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
418
+ const entryPrice = trade.entryPrice ? trade.entryPrice.toFixed(2) : 'N/A';
419
+ const exitPrice = trade.exitPrice ? trade.exitPrice.toFixed(2) : 'N/A';
420
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
421
+ const pnlStr = pnl >= 0 ? chalk.green('+$' + pnl.toFixed(0)) : chalk.red('-$' + Math.abs(pnl).toFixed(0));
422
+ const direction = trade.side === 0 ? chalk.green('LONG') : trade.side === 1 ? chalk.red('SHORT') : chalk.gray('N/A');
423
+ const tradeId = String(trade.id || trade.tradeId || 'N/A').substring(0, colID - 1);
424
+
425
+ const row =
426
+ (' ' + time).padEnd(colTime) + chalk.gray('|') +
427
+ symbol.padEnd(colSymbol) + chalk.gray('|') +
428
+ entryTime.padEnd(colEntry) + chalk.gray('|') +
429
+ exitTime.padEnd(colExit) + chalk.gray('|') +
430
+ entryPrice.padEnd(colEntryP) + chalk.gray('|') +
431
+ exitPrice.padEnd(colExitP) + chalk.gray('|') +
432
+ pnlStr.padEnd(colPnL + 10) + chalk.gray('|') +
433
+ direction.padEnd(colDir + 10) + chalk.gray('|') +
434
+ tradeId.padEnd(colID);
435
+
436
+ const visLen = row.replace(/\x1b\[[0-9;]*m/g, '').length;
437
+ const padding = innerWidth - visLen;
438
+
439
+ console.log(chalk.cyan('\u2551') + row + ' '.repeat(Math.max(0, padding)) + chalk.cyan('\u2551'));
440
+ }
403
441
 
404
- console.log(chalk.cyan('\u2551') + row + ' '.repeat(Math.max(0, padding)) + chalk.cyan('\u2551'));
442
+ if (allTrades.length > 10) {
443
+ const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
444
+ console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg) + ' '.repeat(innerWidth - moreMsg.length) + chalk.cyan('\u2551'));
445
+ }
446
+ } else {
447
+ const msg = ' No trade history available';
448
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(innerWidth - msg.length) + chalk.cyan('\u2551'));
405
449
  }
406
450
 
407
- if (allTrades.length > 10) {
408
- const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
409
- console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg) + ' '.repeat(innerWidth - moreMsg.length) + chalk.cyan('\u2551'));
451
+ drawBoxFooter(boxWidth);
452
+
453
+ // HQX Score
454
+ console.log();
455
+ drawBoxHeader('HQX SCORE', boxWidth);
456
+
457
+ const winRateScore = Math.min(100, parseFloat(winRate) * 1.5);
458
+ const profitFactorScore = Math.min(100, parseFloat(profitFactor) * 40);
459
+ const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
460
+ const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
461
+ ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
462
+ : 50;
463
+ const volumeScore = Math.min(100, stats.totalTrades * 2);
464
+ const returnScore = Math.min(100, Math.max(0, parseFloat(returnPercent) * 10 + 50));
465
+
466
+ const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
467
+ const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
468
+ const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
469
+
470
+ const makeBar = (score, width = 20) => {
471
+ const filled = Math.round((score / 100) * width);
472
+ const empty = width - filled;
473
+ const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
474
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
475
+ };
476
+
477
+ const metricsDisplay = [
478
+ { name: 'Win Rate', score: winRateScore },
479
+ { name: 'Profit Factor', score: profitFactorScore },
480
+ { name: 'Consistency', score: consistencyScore },
481
+ { name: 'Risk Management', score: riskScore },
482
+ { name: 'Volume', score: volumeScore },
483
+ { name: 'Returns', score: returnScore }
484
+ ];
485
+
486
+ const barWidth = 30;
487
+ const labelWidth = 18;
488
+
489
+ const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScore))} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
490
+ const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
491
+ console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
492
+ console.log(chalk.cyan('\u2551') + chalk.gray('─'.repeat(innerWidth)) + chalk.cyan('\u2551'));
493
+
494
+ for (const metric of metricsDisplay) {
495
+ const label = (' ' + metric.name + ':').padEnd(labelWidth);
496
+ const bar = makeBar(metric.score, barWidth);
497
+ const pct = (metric.score.toFixed(0) + '%').padStart(5);
498
+ const line = label + bar + ' ' + pct;
499
+ const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
500
+ console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
410
501
  }
411
- } else {
412
- const msg = ' No trade history available';
413
- console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(innerWidth - msg.length) + chalk.cyan('\u2551'));
414
- }
415
-
416
- drawBoxFooter(boxWidth);
417
-
418
- // HQX Score
419
- console.log();
420
- drawBoxHeader('HQX SCORE', boxWidth);
421
-
422
- // Calculate scores (0-100)
423
- const winRateScore = Math.min(100, parseFloat(winRate) * 1.5); // 66% win rate = 100
424
- const profitFactorScore = Math.min(100, parseFloat(profitFactor) * 40); // 2.5 PF = 100
425
- const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
426
- const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
427
- ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
428
- : 50;
429
- const volumeScore = Math.min(100, stats.totalTrades * 2); // 50 trades = 100
430
- const returnScore = Math.min(100, Math.max(0, parseFloat(returnPercent) * 10 + 50)); // 5% return = 100
431
-
432
- // Overall HQX Score
433
- const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
434
- const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
435
- const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
436
-
437
- // Progress bar helper
438
- const makeBar = (score, width = 20) => {
439
- const filled = Math.round((score / 100) * width);
440
- const empty = width - filled;
441
- const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
442
- return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
443
- };
444
-
445
- // Metrics with progress bars
446
- const metrics = [
447
- { name: 'Win Rate', score: winRateScore },
448
- { name: 'Profit Factor', score: profitFactorScore },
449
- { name: 'Consistency', score: consistencyScore },
450
- { name: 'Risk Management', score: riskScore },
451
- { name: 'Volume', score: volumeScore },
452
- { name: 'Returns', score: returnScore }
453
- ];
454
-
455
- const barWidth = 30;
456
- const labelWidth = 18;
457
-
458
- // Display overall score
459
- const overallLine = ` OVERALL SCORE: ${scoreColor(hqxScore.toString())} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
460
- const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
461
- console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
462
- console.log(chalk.cyan('\u2551') + chalk.gray('─'.repeat(innerWidth)) + chalk.cyan('\u2551'));
463
-
464
- // Display each metric
465
- for (const metric of metrics) {
466
- const label = (' ' + metric.name + ':').padEnd(labelWidth);
467
- const bar = makeBar(metric.score, barWidth);
468
- const pct = (metric.score.toFixed(0) + '%').padStart(5);
469
- const line = label + bar + ' ' + pct;
470
- const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
471
- console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
502
+
503
+ drawBoxFooter(boxWidth);
504
+ console.log();
505
+
506
+ } catch (error) {
507
+ if (spinner) spinner.fail('Error: ' + error.message);
472
508
  }
473
509
 
474
- drawBoxFooter(boxWidth);
475
- console.log();
476
-
477
510
  await prompts.waitForEnter();
478
511
  };
479
512