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.
- package/package.json +1 -1
- package/src/pages/accounts.js +118 -63
- package/src/pages/orders.js +104 -58
- package/src/pages/positions.js +105 -59
- package/src/pages/stats.js +450 -424
- package/src/pages/user.js +68 -47
package/src/pages/stats.js
CHANGED
|
@@ -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
|
-
|
|
17
|
+
let spinner;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
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) {
|
|
55
|
+
} catch (e) {
|
|
56
|
+
spinner.fail(`${propfirmName}: Failed`);
|
|
57
|
+
}
|
|
37
58
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
170
|
+
// Aggregate stats
|
|
171
|
+
spinner = ora({ text: 'Calculating metrics...', color: 'yellow' }).start();
|
|
81
172
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
//
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
99
|
-
const
|
|
100
|
-
|
|
240
|
+
// Display
|
|
241
|
+
const boxWidth = getLogoWidth();
|
|
242
|
+
const { col1, col2 } = getColWidths(boxWidth);
|
|
101
243
|
|
|
102
|
-
|
|
103
|
-
if (
|
|
244
|
+
// Use 0 if null
|
|
245
|
+
if (!hasBalanceData) totalBalance = 0;
|
|
246
|
+
if (!hasPnLData) totalPnL = 0;
|
|
104
247
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
stats.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
console.log(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|