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