hedgequantx 2.3.5 → 2.3.7
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/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +1 -1
- package/src/pages/stats.js +236 -169
package/dist/lib/api.jsc
CHANGED
|
Binary file
|
package/dist/lib/api2.jsc
CHANGED
|
Binary file
|
package/dist/lib/core.jsc
CHANGED
|
Binary file
|
package/dist/lib/core2.jsc
CHANGED
|
Binary file
|
package/dist/lib/data.jsc
CHANGED
|
Binary file
|
package/dist/lib/data2.jsc
CHANGED
|
Binary file
|
package/dist/lib/decoder.jsc
CHANGED
|
Binary file
|
package/dist/lib/m/mod1.jsc
CHANGED
|
Binary file
|
package/dist/lib/m/mod2.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r1.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r2.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r3.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r4.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r5.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r6.jsc
CHANGED
|
Binary file
|
package/dist/lib/n/r7.jsc
CHANGED
|
Binary file
|
package/dist/lib/o/util1.jsc
CHANGED
|
Binary file
|
package/dist/lib/o/util2.jsc
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/pages/stats.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stats Page
|
|
3
|
+
*
|
|
4
|
+
* STRICT RULE: Display ONLY values returned by API
|
|
5
|
+
* - ProjectX: Uses /api/Trade/search, /Position, /TradingAccount APIs
|
|
6
|
+
* - Rithmic: Uses PNL_PLANT for balance/P&L, ORDER_PLANT for accounts
|
|
7
|
+
* - NO estimation, NO simulation, NO mock data
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
const chalk = require('chalk');
|
|
@@ -12,14 +17,15 @@ const { prompts } = require('../utils');
|
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Show Stats Page
|
|
20
|
+
* Aggregates data from all connections (ProjectX, Rithmic, Tradovate)
|
|
15
21
|
*/
|
|
16
22
|
const showStats = async (service) => {
|
|
17
23
|
let spinner;
|
|
18
24
|
|
|
19
25
|
try {
|
|
20
|
-
// Single spinner for loading
|
|
21
26
|
spinner = ora({ text: 'Loading stats...', color: 'yellow' }).start();
|
|
22
27
|
|
|
28
|
+
// Get all connections
|
|
23
29
|
const allConns = connections.count() > 0
|
|
24
30
|
? connections.getAll()
|
|
25
31
|
: (service ? [{ service, propfirm: service.propfirm?.name || 'Unknown', type: 'single' }] : []);
|
|
@@ -30,12 +36,25 @@ const showStats = async (service) => {
|
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
//
|
|
39
|
+
// Track connection types for display
|
|
40
|
+
const connectionTypes = {
|
|
41
|
+
projectx: 0,
|
|
42
|
+
rithmic: 0,
|
|
43
|
+
tradovate: 0
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Fetch accounts from each connection with type detection
|
|
34
47
|
let allAccountsData = [];
|
|
35
48
|
|
|
36
49
|
for (const conn of allConns) {
|
|
50
|
+
const connType = conn.type || 'projectx';
|
|
37
51
|
const propfirmName = conn.propfirm || conn.type || 'Unknown';
|
|
38
52
|
|
|
53
|
+
// Count connection types
|
|
54
|
+
if (connType === 'projectx') connectionTypes.projectx++;
|
|
55
|
+
else if (connType === 'rithmic') connectionTypes.rithmic++;
|
|
56
|
+
else if (connType === 'tradovate') connectionTypes.tradovate++;
|
|
57
|
+
|
|
39
58
|
try {
|
|
40
59
|
const result = await conn.service.getTradingAccounts();
|
|
41
60
|
if (result.success && result.accounts && result.accounts.length > 0) {
|
|
@@ -43,11 +62,14 @@ const showStats = async (service) => {
|
|
|
43
62
|
allAccountsData.push({
|
|
44
63
|
...account,
|
|
45
64
|
propfirm: propfirmName,
|
|
65
|
+
connectionType: connType,
|
|
46
66
|
service: conn.service
|
|
47
67
|
});
|
|
48
68
|
});
|
|
49
69
|
}
|
|
50
|
-
} catch (e) {
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// Silently skip failed connections
|
|
72
|
+
}
|
|
51
73
|
}
|
|
52
74
|
|
|
53
75
|
if (allAccountsData.length === 0) {
|
|
@@ -74,40 +96,51 @@ const showStats = async (service) => {
|
|
|
74
96
|
return;
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
//
|
|
99
|
+
// ========== AGGREGATE DATA FROM APIs ==========
|
|
100
|
+
// All values come from APIs - NO local calculation for P&L
|
|
101
|
+
|
|
78
102
|
let totalBalance = 0;
|
|
79
103
|
let totalPnL = 0;
|
|
80
104
|
let totalStartingBalance = 0;
|
|
81
105
|
let allTrades = [];
|
|
82
106
|
let totalOpenPositions = 0;
|
|
83
107
|
let totalOpenOrders = 0;
|
|
108
|
+
|
|
109
|
+
// Track data availability (null means no data from API)
|
|
84
110
|
let hasBalanceData = false;
|
|
85
111
|
let hasPnLData = false;
|
|
112
|
+
let hasTradeData = false;
|
|
86
113
|
|
|
87
114
|
for (let i = 0; i < activeAccounts.length; i++) {
|
|
88
115
|
const account = activeAccounts[i];
|
|
89
116
|
const svc = account.service;
|
|
117
|
+
const connType = account.connectionType || 'projectx';
|
|
90
118
|
|
|
91
119
|
try {
|
|
92
|
-
//
|
|
120
|
+
// ===== BALANCE (from API) =====
|
|
93
121
|
if (account.balance !== null && account.balance !== undefined) {
|
|
94
122
|
totalBalance += account.balance;
|
|
95
123
|
hasBalanceData = true;
|
|
96
124
|
}
|
|
97
125
|
|
|
98
|
-
// P&L
|
|
126
|
+
// ===== P&L (from API - NEVER calculated locally) =====
|
|
127
|
+
// ProjectX: profitAndLoss comes from /Position API (unrealized) + /Trade API (realized)
|
|
128
|
+
// Rithmic: profitAndLoss comes from PNL_PLANT (dayPnl or openPnl + closedPnl)
|
|
99
129
|
if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
|
|
100
130
|
totalPnL += account.profitAndLoss;
|
|
101
131
|
hasPnLData = true;
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
//
|
|
105
|
-
|
|
134
|
+
// ===== STARTING BALANCE =====
|
|
135
|
+
// Derived: startingBalance from API or calculated as balance - P&L
|
|
136
|
+
if (account.startingBalance !== null && account.startingBalance !== undefined) {
|
|
137
|
+
totalStartingBalance += account.startingBalance;
|
|
138
|
+
} else if (account.balance !== null && account.balance !== undefined) {
|
|
106
139
|
const pnl = account.profitAndLoss || 0;
|
|
107
140
|
totalStartingBalance += (account.balance - pnl);
|
|
108
141
|
}
|
|
109
142
|
|
|
110
|
-
//
|
|
143
|
+
// ===== POSITIONS (from API) =====
|
|
111
144
|
try {
|
|
112
145
|
const posResult = await svc.getPositions(account.accountId);
|
|
113
146
|
if (posResult.success && posResult.positions) {
|
|
@@ -115,15 +148,16 @@ const showStats = async (service) => {
|
|
|
115
148
|
}
|
|
116
149
|
} catch (e) {}
|
|
117
150
|
|
|
118
|
-
//
|
|
151
|
+
// ===== ORDERS (from API) =====
|
|
119
152
|
try {
|
|
120
153
|
const ordResult = await svc.getOrders(account.accountId);
|
|
121
154
|
if (ordResult.success && ordResult.orders) {
|
|
122
|
-
totalOpenOrders += ordResult.orders.filter(o => o.status === 1).length;
|
|
155
|
+
totalOpenOrders += ordResult.orders.filter(o => o.status === 1 || o.status === 'Working').length;
|
|
123
156
|
}
|
|
124
157
|
} catch (e) {}
|
|
125
158
|
|
|
126
|
-
//
|
|
159
|
+
// ===== LIFETIME STATS (from API - ProjectX only) =====
|
|
160
|
+
// Rithmic doesn't have getLifetimeStats - returns null
|
|
127
161
|
if (typeof svc.getLifetimeStats === 'function') {
|
|
128
162
|
try {
|
|
129
163
|
const lifetimeResult = await svc.getLifetimeStats(account.accountId);
|
|
@@ -133,15 +167,18 @@ const showStats = async (service) => {
|
|
|
133
167
|
} catch (e) {}
|
|
134
168
|
}
|
|
135
169
|
|
|
136
|
-
//
|
|
170
|
+
// ===== TRADE HISTORY (from API - ProjectX only) =====
|
|
171
|
+
// Rithmic doesn't have getTradeHistory - returns empty array
|
|
137
172
|
if (typeof svc.getTradeHistory === 'function') {
|
|
138
173
|
try {
|
|
139
174
|
const tradesResult = await svc.getTradeHistory(account.accountId, 30);
|
|
140
175
|
if (tradesResult.success && tradesResult.trades && tradesResult.trades.length > 0) {
|
|
176
|
+
hasTradeData = true;
|
|
141
177
|
allTrades = allTrades.concat(tradesResult.trades.map(t => ({
|
|
142
178
|
...t,
|
|
143
179
|
accountName: account.accountName,
|
|
144
|
-
propfirm: account.propfirm
|
|
180
|
+
propfirm: account.propfirm,
|
|
181
|
+
connectionType: connType
|
|
145
182
|
})));
|
|
146
183
|
}
|
|
147
184
|
} catch (e) {}
|
|
@@ -149,7 +186,8 @@ const showStats = async (service) => {
|
|
|
149
186
|
} catch (e) {}
|
|
150
187
|
}
|
|
151
188
|
|
|
152
|
-
//
|
|
189
|
+
// ========== AGGREGATE STATS FROM API DATA ==========
|
|
190
|
+
// Stats come from API (lifetimeStats) or calculated from API trade data
|
|
153
191
|
|
|
154
192
|
let stats = {
|
|
155
193
|
totalTrades: 0, winningTrades: 0, losingTrades: 0,
|
|
@@ -159,6 +197,7 @@ const showStats = async (service) => {
|
|
|
159
197
|
longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
|
|
160
198
|
};
|
|
161
199
|
|
|
200
|
+
// First: aggregate lifetimeStats from APIs (ProjectX)
|
|
162
201
|
for (const account of activeAccounts) {
|
|
163
202
|
if (account.lifetimeStats) {
|
|
164
203
|
const s = account.lifetimeStats;
|
|
@@ -177,18 +216,20 @@ const showStats = async (service) => {
|
|
|
177
216
|
}
|
|
178
217
|
}
|
|
179
218
|
|
|
180
|
-
// If no
|
|
219
|
+
// If no lifetimeStats, calculate from trade history (still 100% API data)
|
|
181
220
|
if (stats.totalTrades === 0 && allTrades.length > 0) {
|
|
182
221
|
stats.totalTrades = allTrades.length;
|
|
183
222
|
let consecutiveWins = 0, consecutiveLosses = 0;
|
|
184
223
|
|
|
185
224
|
for (const trade of allTrades) {
|
|
225
|
+
// P&L comes directly from API response
|
|
186
226
|
const pnl = trade.profitAndLoss || trade.pnl || 0;
|
|
187
227
|
const size = trade.size || trade.quantity || 1;
|
|
188
228
|
const side = trade.side;
|
|
189
229
|
|
|
190
230
|
stats.totalVolume += Math.abs(size);
|
|
191
231
|
|
|
232
|
+
// Side: 0 = Buy/Long, 1 = Sell/Short (ProjectX API format)
|
|
192
233
|
if (side === 0) {
|
|
193
234
|
stats.longTrades++;
|
|
194
235
|
if (pnl > 0) stats.longWins++;
|
|
@@ -212,31 +253,30 @@ const showStats = async (service) => {
|
|
|
212
253
|
if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
|
|
213
254
|
if (pnl < stats.worstTrade) stats.worstTrade = pnl;
|
|
214
255
|
}
|
|
256
|
+
// pnl === 0 trades are neither win nor loss
|
|
215
257
|
}
|
|
216
258
|
}
|
|
217
259
|
|
|
218
|
-
spinner.succeed('
|
|
260
|
+
spinner.succeed('Stats loaded');
|
|
219
261
|
console.log();
|
|
220
262
|
|
|
221
|
-
//
|
|
263
|
+
// ========== DISPLAY ==========
|
|
222
264
|
const boxWidth = getLogoWidth();
|
|
223
265
|
const { col1, col2 } = getColWidths(boxWidth);
|
|
224
266
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
if (!hasPnLData) totalPnL = 0;
|
|
228
|
-
|
|
229
|
-
// Calculated metrics
|
|
230
|
-
const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : '0.0';
|
|
267
|
+
// Calculate metrics (using API data only)
|
|
268
|
+
const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : 'N/A';
|
|
231
269
|
const avgWin = stats.winningTrades > 0 ? (stats.totalWinAmount / stats.winningTrades).toFixed(2) : '0.00';
|
|
232
270
|
const avgLoss = stats.losingTrades > 0 ? (stats.totalLossAmount / stats.losingTrades).toFixed(2) : '0.00';
|
|
233
|
-
const profitFactor = stats.totalLossAmount > 0
|
|
271
|
+
const profitFactor = stats.totalLossAmount > 0
|
|
272
|
+
? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2)
|
|
273
|
+
: (stats.totalWinAmount > 0 ? '∞' : 'N/A');
|
|
234
274
|
const netPnL = stats.totalWinAmount - stats.totalLossAmount;
|
|
235
|
-
const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : '
|
|
236
|
-
const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '
|
|
237
|
-
const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '
|
|
275
|
+
const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : 'N/A';
|
|
276
|
+
const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : 'N/A';
|
|
277
|
+
const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : 'N/A';
|
|
238
278
|
|
|
239
|
-
//
|
|
279
|
+
// Quantitative metrics (calculated from API trade data)
|
|
240
280
|
const tradePnLs = allTrades.map(t => t.profitAndLoss || t.pnl || 0);
|
|
241
281
|
const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
|
|
242
282
|
|
|
@@ -254,68 +294,94 @@ const showStats = async (service) => {
|
|
|
254
294
|
const downsideDev = Math.sqrt(downsideVariance);
|
|
255
295
|
|
|
256
296
|
// Ratios
|
|
257
|
-
const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : '
|
|
258
|
-
const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : '
|
|
297
|
+
const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : 'N/A';
|
|
298
|
+
const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : 'N/A';
|
|
259
299
|
|
|
260
300
|
// Max Drawdown
|
|
261
301
|
let maxDrawdown = 0;
|
|
262
|
-
let peak = totalStartingBalance ||
|
|
302
|
+
let peak = totalStartingBalance || 0;
|
|
263
303
|
let equity = peak;
|
|
264
|
-
tradePnLs.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
if (peak > 0 && tradePnLs.length > 0) {
|
|
305
|
+
tradePnLs.forEach(pnl => {
|
|
306
|
+
equity += pnl;
|
|
307
|
+
if (equity > peak) peak = equity;
|
|
308
|
+
const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
|
|
309
|
+
if (drawdown > maxDrawdown) maxDrawdown = drawdown;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
270
312
|
|
|
271
313
|
const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
|
|
272
|
-
const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : '
|
|
273
|
-
const calmarRatio = maxDrawdown > 0 ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : '
|
|
314
|
+
const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : 'N/A';
|
|
315
|
+
const calmarRatio = maxDrawdown > 0 && returnPercent !== 'N/A' ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : 'N/A';
|
|
316
|
+
|
|
317
|
+
// Colors
|
|
318
|
+
const totalBalanceColor = hasBalanceData ? (totalBalance >= 0 ? chalk.green : chalk.red) : chalk.gray;
|
|
319
|
+
const pnlColor = hasPnLData ? (totalPnL >= 0 ? chalk.green : chalk.red) : chalk.gray;
|
|
274
320
|
|
|
275
|
-
|
|
276
|
-
const
|
|
321
|
+
// Connection type string
|
|
322
|
+
const connTypeStr = [];
|
|
323
|
+
if (connectionTypes.projectx > 0) connTypeStr.push(`ProjectX(${connectionTypes.projectx})`);
|
|
324
|
+
if (connectionTypes.rithmic > 0) connTypeStr.push(`Rithmic(${connectionTypes.rithmic})`);
|
|
325
|
+
if (connectionTypes.tradovate > 0) connTypeStr.push(`Tradovate(${connectionTypes.tradovate})`);
|
|
277
326
|
|
|
278
|
-
//
|
|
327
|
+
// ========== MAIN SUMMARY ==========
|
|
279
328
|
drawBoxHeader('HQX STATS', boxWidth);
|
|
280
329
|
draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
|
|
281
330
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
331
|
+
// Format balance/P&L - show "N/A" if no data from API
|
|
332
|
+
const balanceStr = hasBalanceData ? '$' + totalBalance.toLocaleString(undefined, {minimumFractionDigits: 2}) : 'N/A';
|
|
333
|
+
const pnlStr = hasPnLData
|
|
334
|
+
? (totalPnL >= 0 ? '+' : '') + '$' + totalPnL.toLocaleString(undefined, {minimumFractionDigits: 2}) + (returnPercent !== 'N/A' ? ' (' + returnPercent + '%)' : '')
|
|
335
|
+
: 'N/A';
|
|
336
|
+
const startBalStr = totalStartingBalance > 0 ? '$' + totalStartingBalance.toLocaleString(undefined, {minimumFractionDigits: 2}) : 'N/A';
|
|
337
|
+
|
|
338
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Connections:', chalk.cyan(connTypeStr.join(', ') || String(connections.count() || 1)), col1) + chalk.cyan('\u2502') + fmtRow('Total Trades:', hasTradeData || stats.totalTrades > 0 ? chalk.white(String(stats.totalTrades)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
339
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Total Accounts:', chalk.cyan(String(activeAccounts.length)), col1) + chalk.cyan('\u2502') + fmtRow('Winning Trades:', hasTradeData || stats.winningTrades > 0 ? chalk.green(String(stats.winningTrades)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
340
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Total Balance:', totalBalanceColor(balanceStr), col1) + chalk.cyan('\u2502') + fmtRow('Losing Trades:', hasTradeData || stats.losingTrades > 0 ? chalk.red(String(stats.losingTrades)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
341
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Starting Balance:', chalk.white(startBalStr), col1) + chalk.cyan('\u2502') + fmtRow('Win Rate:', winRate !== 'N/A' ? (parseFloat(winRate) >= 50 ? chalk.green(winRate + '%') : chalk.yellow(winRate + '%')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
342
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Total P&L:', pnlColor(pnlStr), col1) + chalk.cyan('\u2502') + fmtRow('Long Trades:', hasTradeData ? chalk.white(stats.longTrades + (longWinRate !== 'N/A' ? ' (' + longWinRate + '%)' : '')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
343
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Open Positions:', chalk.white(String(totalOpenPositions)), col1) + chalk.cyan('\u2502') + fmtRow('Short Trades:', hasTradeData ? chalk.white(stats.shortTrades + (shortWinRate !== 'N/A' ? ' (' + shortWinRate + '%)' : '')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
344
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Open Orders:', chalk.white(String(totalOpenOrders)), col1) + chalk.cyan('\u2502') + fmtRow('Volume:', hasTradeData ? chalk.white(stats.totalVolume + ' contracts') : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
345
|
+
|
|
346
|
+
// ========== P&L METRICS ==========
|
|
291
347
|
draw2ColSeparator(boxWidth);
|
|
292
348
|
draw2ColHeader('P&L METRICS', 'RISK METRICS', boxWidth);
|
|
293
349
|
|
|
294
|
-
|
|
350
|
+
// Profit Factor coloring
|
|
351
|
+
const pfColor = profitFactor === '∞' ? chalk.green(profitFactor)
|
|
352
|
+
: profitFactor === 'N/A' ? chalk.gray(profitFactor)
|
|
353
|
+
: parseFloat(profitFactor) >= 1.5 ? chalk.green(profitFactor)
|
|
354
|
+
: parseFloat(profitFactor) >= 1 ? chalk.yellow(profitFactor)
|
|
355
|
+
: chalk.red(profitFactor);
|
|
356
|
+
|
|
357
|
+
// Worst trade display
|
|
358
|
+
const worstTradeStr = stats.worstTrade < 0 ? '-$' + Math.abs(stats.worstTrade).toFixed(2) : '$' + stats.worstTrade.toFixed(2);
|
|
295
359
|
|
|
296
|
-
|
|
297
|
-
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'));
|
|
298
|
-
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'));
|
|
299
|
-
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'));
|
|
300
|
-
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'));
|
|
360
|
+
const netPnLStr = hasTradeData ? (netPnL >= 0 ? chalk.green('$' + netPnL.toFixed(2)) : chalk.red('-$' + Math.abs(netPnL).toFixed(2))) : chalk.gray('N/A');
|
|
301
361
|
|
|
302
|
-
|
|
362
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnLStr, col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
|
|
363
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Gross Profit:', hasTradeData ? chalk.green('$' + stats.totalWinAmount.toFixed(2)) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Wins:', hasTradeData ? chalk.green(String(stats.maxConsecutiveWins)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
364
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Gross Loss:', hasTradeData ? chalk.red('-$' + stats.totalLossAmount.toFixed(2)) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Loss:', hasTradeData ? (stats.maxConsecutiveLosses > 0 ? chalk.red(String(stats.maxConsecutiveLosses)) : chalk.green('0')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
365
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Avg Win:', hasTradeData ? chalk.green('$' + avgWin) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Best Trade:', hasTradeData ? chalk.green('$' + stats.bestTrade.toFixed(2)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
366
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Avg Loss:', hasTradeData ? (stats.losingTrades > 0 ? chalk.red('-$' + avgLoss) : chalk.green('$0.00')) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Worst Trade:', hasTradeData ? (stats.worstTrade < 0 ? chalk.red(worstTradeStr) : chalk.green(worstTradeStr)) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
367
|
+
|
|
368
|
+
// ========== QUANTITATIVE METRICS ==========
|
|
303
369
|
draw2ColSeparator(boxWidth);
|
|
304
370
|
draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
|
|
305
371
|
|
|
306
|
-
const sharpeColor = parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
|
|
307
|
-
const sortinoColor = parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
|
|
308
|
-
const ddColor = maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
|
|
309
|
-
const rrColor = parseFloat(riskRewardRatio) >= 2 ? chalk.green : parseFloat(riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
|
|
372
|
+
const sharpeColor = sharpeRatio === 'N/A' ? chalk.gray : parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
|
|
373
|
+
const sortinoColor = sortinoRatio === 'N/A' ? chalk.gray : parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
|
|
374
|
+
const ddColor = maxDrawdown === 0 ? chalk.gray : maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
|
|
375
|
+
const rrColor = riskRewardRatio === 'N/A' ? chalk.gray : parseFloat(riskRewardRatio) >= 2 ? chalk.green : parseFloat(riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
|
|
310
376
|
|
|
311
377
|
console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(riskRewardRatio), col2) + chalk.cyan('\u2551'));
|
|
312
|
-
console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
|
|
313
|
-
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'));
|
|
314
|
-
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'));
|
|
378
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', calmarRatio === 'N/A' ? chalk.gray(calmarRatio) : chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
|
|
379
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Max Drawdown:', hasTradeData && maxDrawdown > 0 ? ddColor(maxDrawdown.toFixed(2) + '%') : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Expectancy:', hasTradeData ? (expectancy >= 0 ? chalk.green('$' + expectancy.toFixed(2)) : chalk.red('$' + expectancy.toFixed(2))) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
380
|
+
console.log(chalk.cyan('\u2551') + fmtRow('Std Deviation:', hasTradeData ? chalk.white('$' + stdDev.toFixed(2)) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Avg Trade:', hasTradeData ? (avgReturn >= 0 ? chalk.green('$' + avgReturn.toFixed(2)) : chalk.red('$' + avgReturn.toFixed(2))) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
|
|
315
381
|
|
|
316
382
|
drawBoxFooter(boxWidth);
|
|
317
383
|
|
|
318
|
-
//
|
|
384
|
+
// ========== EQUITY CURVE ==========
|
|
319
385
|
console.log();
|
|
320
386
|
drawBoxHeader('EQUITY CURVE', boxWidth);
|
|
321
387
|
|
|
@@ -352,141 +418,142 @@ const showStats = async (service) => {
|
|
|
352
418
|
console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
|
|
353
419
|
});
|
|
354
420
|
} else {
|
|
355
|
-
const msg =
|
|
356
|
-
|
|
421
|
+
const msg = connectionTypes.rithmic > 0
|
|
422
|
+
? ' No trade history (Rithmic does not provide trade history API)'
|
|
423
|
+
: ' No trade data available';
|
|
424
|
+
console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(Math.max(0, chartInnerWidth - msg.length)) + chalk.cyan('\u2551'));
|
|
357
425
|
}
|
|
358
426
|
|
|
359
427
|
drawBoxFooter(boxWidth);
|
|
360
428
|
|
|
361
|
-
//
|
|
429
|
+
// ========== TRADES HISTORY ==========
|
|
362
430
|
console.log();
|
|
363
431
|
drawBoxHeader('TRADES HISTORY', boxWidth);
|
|
364
432
|
|
|
365
433
|
const innerWidth = boxWidth - 2;
|
|
366
434
|
|
|
435
|
+
// Helper to extract symbol from contractId (e.g., "CON.F.US.EP.H25" -> "ES H25")
|
|
436
|
+
const extractSymbol = (contractId) => {
|
|
437
|
+
if (!contractId) return 'N/A';
|
|
438
|
+
// ProjectX format: CON.F.US.{SYMBOL}.{MONTH}
|
|
439
|
+
const parts = contractId.split('.');
|
|
440
|
+
if (parts.length >= 5) {
|
|
441
|
+
const sym = parts[3];
|
|
442
|
+
const month = parts[4];
|
|
443
|
+
const symbolMap = { 'EP': 'ES', 'ENQ': 'NQ', 'MES': 'MES', 'MNQ': 'MNQ', 'YM': 'YM', 'NKD': 'NKD', 'RTY': 'RTY' };
|
|
444
|
+
return (symbolMap[sym] || sym) + ' ' + month;
|
|
445
|
+
}
|
|
446
|
+
// Rithmic format: already clean symbol
|
|
447
|
+
if (contractId.length <= 10) return contractId;
|
|
448
|
+
return contractId.substring(0, 10);
|
|
449
|
+
};
|
|
450
|
+
|
|
367
451
|
if (allTrades.length > 0) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
const colEntry = 10;
|
|
371
|
-
const colExit = 10;
|
|
372
|
-
const colEntryP = 10;
|
|
373
|
-
const colExitP = 10;
|
|
374
|
-
const colPnL = 10;
|
|
375
|
-
const colDir = 6;
|
|
376
|
-
const colID = innerWidth - colTime - colSymbol - colEntry - colExit - colEntryP - colExitP - colPnL - colDir - 9;
|
|
377
|
-
|
|
378
|
-
const header =
|
|
379
|
-
chalk.white(' Time'.padEnd(colTime)) + chalk.gray('|') +
|
|
380
|
-
chalk.white('Symbol'.padEnd(colSymbol)) + chalk.gray('|') +
|
|
381
|
-
chalk.white('Entry'.padEnd(colEntry)) + chalk.gray('|') +
|
|
382
|
-
chalk.white('Exit'.padEnd(colExit)) + chalk.gray('|') +
|
|
383
|
-
chalk.white('Entry $'.padEnd(colEntryP)) + chalk.gray('|') +
|
|
384
|
-
chalk.white('Exit $'.padEnd(colExitP)) + chalk.gray('|') +
|
|
385
|
-
chalk.white('P&L'.padEnd(colPnL)) + chalk.gray('|') +
|
|
386
|
-
chalk.white('Dir'.padEnd(colDir)) + chalk.gray('|') +
|
|
387
|
-
chalk.white('ID'.padEnd(colID));
|
|
388
|
-
|
|
389
|
-
console.log(chalk.cyan('\u2551') + header + chalk.cyan('\u2551'));
|
|
452
|
+
const header = ' Time | Symbol | Price | P&L | Side ';
|
|
453
|
+
console.log(chalk.cyan('\u2551') + chalk.white(header.padEnd(innerWidth)) + chalk.cyan('\u2551'));
|
|
390
454
|
console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
|
|
391
455
|
|
|
392
456
|
const recentTrades = allTrades.slice(-10).reverse();
|
|
393
457
|
|
|
394
458
|
for (const trade of recentTrades) {
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const entryTime = trade.entryTime ? new Date(trade.entryTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : time;
|
|
400
|
-
const exitTime = trade.exitTime ? new Date(trade.exitTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : time;
|
|
401
|
-
const price = trade.price || trade.entryPrice || trade.exitPrice || 0;
|
|
402
|
-
const entryPrice = trade.entryPrice ? trade.entryPrice.toFixed(2) : (price ? price.toFixed(2) : 'N/A');
|
|
403
|
-
const exitPrice = trade.exitPrice ? trade.exitPrice.toFixed(2) : (price ? price.toFixed(2) : 'N/A');
|
|
459
|
+
const timestamp = trade.creationTimestamp || trade.timestamp;
|
|
460
|
+
const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
|
|
461
|
+
const symbol = extractSymbol(trade.contractId || trade.symbol);
|
|
462
|
+
const price = (trade.price || 0).toFixed(2);
|
|
404
463
|
const pnl = trade.profitAndLoss || trade.pnl || 0;
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const
|
|
464
|
+
const pnlText = pnl >= 0 ? `+$${pnl.toFixed(0)}` : `-$${Math.abs(pnl).toFixed(0)}`;
|
|
465
|
+
const pnlColored = pnl >= 0 ? chalk.green(pnlText.padEnd(10)) : chalk.red(pnlText.padEnd(10));
|
|
466
|
+
const side = trade.side === 0 ? 'BUY' : trade.side === 1 ? 'SELL' : 'N/A';
|
|
467
|
+
const sideColored = trade.side === 0 ? chalk.green(side.padEnd(6)) : chalk.red(side.padEnd(6));
|
|
408
468
|
|
|
409
|
-
const row =
|
|
410
|
-
|
|
411
|
-
symbol.padEnd(colSymbol) + chalk.gray('|') +
|
|
412
|
-
entryTime.padEnd(colEntry) + chalk.gray('|') +
|
|
413
|
-
exitTime.padEnd(colExit) + chalk.gray('|') +
|
|
414
|
-
entryPrice.padEnd(colEntryP) + chalk.gray('|') +
|
|
415
|
-
exitPrice.padEnd(colExitP) + chalk.gray('|') +
|
|
416
|
-
pnlStr.padEnd(colPnL + 10) + chalk.gray('|') +
|
|
417
|
-
direction.padEnd(colDir + 10) + chalk.gray('|') +
|
|
418
|
-
tradeId.padEnd(colID);
|
|
469
|
+
const row = ` ${time.padEnd(9)} | ${symbol.padEnd(9)} | ${price.padEnd(10)} | ${pnlText.padEnd(10)} | ${side.padEnd(6)}`;
|
|
470
|
+
const coloredRow = ` ${time.padEnd(9)} | ${symbol.padEnd(9)} | ${price.padEnd(10)} | ${pnlColored} | ${sideColored}`;
|
|
419
471
|
|
|
420
|
-
const visLen = row.
|
|
421
|
-
const padding = innerWidth - visLen;
|
|
472
|
+
const visLen = row.length;
|
|
473
|
+
const padding = Math.max(0, innerWidth - visLen);
|
|
422
474
|
|
|
423
|
-
console.log(chalk.cyan('\u2551') +
|
|
475
|
+
console.log(chalk.cyan('\u2551') + coloredRow + ' '.repeat(padding) + chalk.cyan('\u2551'));
|
|
424
476
|
}
|
|
425
477
|
|
|
426
478
|
if (allTrades.length > 10) {
|
|
427
479
|
const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
|
|
428
|
-
console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg
|
|
480
|
+
console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
|
|
429
481
|
}
|
|
430
482
|
} else {
|
|
431
|
-
const msg =
|
|
432
|
-
|
|
483
|
+
const msg = connectionTypes.rithmic > 0
|
|
484
|
+
? ' No trade history (Rithmic API limitation)'
|
|
485
|
+
: ' No trade history available';
|
|
486
|
+
console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
|
|
433
487
|
}
|
|
434
488
|
|
|
435
489
|
drawBoxFooter(boxWidth);
|
|
436
490
|
|
|
437
|
-
// HQX
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
? Math.
|
|
446
|
-
:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
491
|
+
// ========== HQX SCORE ==========
|
|
492
|
+
// Only show if we have trade data to score
|
|
493
|
+
if (hasTradeData || stats.totalTrades > 0) {
|
|
494
|
+
console.log();
|
|
495
|
+
drawBoxHeader('HQX SCORE', boxWidth);
|
|
496
|
+
|
|
497
|
+
const winRateNum = winRate !== 'N/A' ? parseFloat(winRate) : 0;
|
|
498
|
+
const winRateScore = Math.min(100, winRateNum * 1.5);
|
|
499
|
+
const profitFactorScore = profitFactor === '∞' ? 100 : profitFactor === 'N/A' ? 0 : Math.min(100, parseFloat(profitFactor) * 40);
|
|
500
|
+
const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
|
|
501
|
+
const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
|
|
502
|
+
? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
|
|
503
|
+
: 50;
|
|
504
|
+
const volumeScore = Math.min(100, stats.totalTrades * 2);
|
|
505
|
+
const returnNum = returnPercent !== 'N/A' ? parseFloat(returnPercent) : 0;
|
|
506
|
+
const returnScore = Math.min(100, Math.max(0, returnNum * 10 + 50));
|
|
507
|
+
|
|
508
|
+
const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
|
|
509
|
+
const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
|
|
510
|
+
const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
|
|
511
|
+
|
|
512
|
+
const makeBar = (score, width = 20) => {
|
|
513
|
+
const filled = Math.round((score / 100) * width);
|
|
514
|
+
const empty = width - filled;
|
|
515
|
+
const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
516
|
+
return color('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(empty));
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const metricsDisplay = [
|
|
520
|
+
{ name: 'Win Rate', score: winRateScore },
|
|
521
|
+
{ name: 'Profit Factor', score: profitFactorScore },
|
|
522
|
+
{ name: 'Consistency', score: consistencyScore },
|
|
523
|
+
{ name: 'Risk Management', score: riskScore },
|
|
524
|
+
{ name: 'Volume', score: volumeScore },
|
|
525
|
+
{ name: 'Returns', score: returnScore }
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const barWidth = 30;
|
|
529
|
+
const labelWidth = 18;
|
|
530
|
+
|
|
531
|
+
const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScore))} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
|
|
532
|
+
const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
533
|
+
console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
|
|
534
|
+
console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
|
|
535
|
+
|
|
536
|
+
for (const metric of metricsDisplay) {
|
|
537
|
+
const label = (' ' + metric.name + ':').padEnd(labelWidth);
|
|
538
|
+
const bar = makeBar(metric.score, barWidth);
|
|
539
|
+
const pct = (metric.score.toFixed(0) + '%').padStart(5);
|
|
540
|
+
const line = label + bar + ' ' + pct;
|
|
541
|
+
const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
542
|
+
console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
drawBoxFooter(boxWidth);
|
|
485
546
|
}
|
|
486
547
|
|
|
487
|
-
drawBoxFooter(boxWidth);
|
|
488
548
|
console.log();
|
|
489
549
|
|
|
550
|
+
// Show data source notice
|
|
551
|
+
if (connectionTypes.rithmic > 0 && connectionTypes.projectx === 0) {
|
|
552
|
+
console.log(chalk.gray(' Note: Rithmic API provides balance/P&L only. Trade history not available.'));
|
|
553
|
+
} else if (connectionTypes.rithmic > 0 && connectionTypes.projectx > 0) {
|
|
554
|
+
console.log(chalk.gray(' Note: Trade history shown from ProjectX accounts only.'));
|
|
555
|
+
}
|
|
556
|
+
|
|
490
557
|
} catch (error) {
|
|
491
558
|
if (spinner) spinner.fail('Error: ' + error.message);
|
|
492
559
|
}
|