hedgequantx 2.3.6 → 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 CHANGED
Binary file
package/dist/lib/api2.jsc CHANGED
Binary file
package/dist/lib/core.jsc CHANGED
Binary file
Binary file
package/dist/lib/data.jsc CHANGED
Binary file
Binary file
Binary file
Binary file
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
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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
- // Fetch accounts from each connection
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
- // Collect stats for each account
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
- // Balance
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
- // Starting balance
105
- if (account.balance !== null && account.balance !== undefined) {
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
- // Positions
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
- // Orders
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
- // Lifetime stats
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
- // Trade history
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
- // Aggregate stats
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 stats from API, calculate from trades
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('All stats loaded');
260
+ spinner.succeed('Stats loaded');
219
261
  console.log();
220
262
 
221
- // Display
263
+ // ========== DISPLAY ==========
222
264
  const boxWidth = getLogoWidth();
223
265
  const { col1, col2 } = getColWidths(boxWidth);
224
266
 
225
- // Use 0 if null
226
- if (!hasBalanceData) totalBalance = 0;
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 ? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2) : (stats.totalWinAmount > 0 ? '∞' : '0.00');
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) : '0.00';
236
- const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '0.0';
237
- const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '0.0';
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
- // Advanced quantitative metrics
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,73 +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) : '0.00';
258
- const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : '0.00';
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 || 100000;
302
+ let peak = totalStartingBalance || 0;
263
303
  let equity = peak;
264
- tradePnLs.forEach(pnl => {
265
- equity += pnl;
266
- if (equity > peak) peak = equity;
267
- const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
268
- if (drawdown > maxDrawdown) maxDrawdown = drawdown;
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) : '0.00';
273
- const calmarRatio = maxDrawdown > 0 ? (parseFloat(returnPercent) / maxDrawdown).toFixed(2) : '0.00';
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';
274
316
 
275
- const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
276
- const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
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;
277
320
 
278
- // Main Summary
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})`);
326
+
327
+ // ========== MAIN SUMMARY ==========
279
328
  drawBoxHeader('HQX STATS', boxWidth);
280
329
  draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
281
330
 
282
- 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'));
283
- 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'));
284
- 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'));
285
- 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'));
286
- 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'));
287
- 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'));
288
- 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'));
289
-
290
- // P&L Metrics
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
- // Profit Factor coloring - ∞ is best (green)
295
- const pfNum = profitFactor === '∞' ? Infinity : parseFloat(profitFactor);
296
- const pfColor = pfNum === Infinity ? chalk.green(profitFactor) : pfNum >= 1.5 ? chalk.green(profitFactor) : pfNum >= 1 ? chalk.yellow(profitFactor) : chalk.red(profitFactor);
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);
297
356
 
298
- // Worst trade display - show as negative if it's a loss
357
+ // Worst trade display
299
358
  const worstTradeStr = stats.worstTrade < 0 ? '-$' + Math.abs(stats.worstTrade).toFixed(2) : '$' + stats.worstTrade.toFixed(2);
300
359
 
301
- console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnL >= 0 ? chalk.green('$' + netPnL.toFixed(2)) : chalk.red('-$' + Math.abs(netPnL).toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
302
- 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'));
303
- console.log(chalk.cyan('\u2551') + fmtRow('Gross Loss:', chalk.red('-$' + stats.totalLossAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Loss:', stats.maxConsecutiveLosses > 0 ? chalk.red(String(stats.maxConsecutiveLosses)) : chalk.green('0'), col2) + chalk.cyan('\u2551'));
304
- 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'));
305
- console.log(chalk.cyan('\u2551') + fmtRow('Avg Loss:', stats.losingTrades > 0 ? chalk.red('-$' + avgLoss) : chalk.green('$0.00'), col1) + chalk.cyan('\u2502') + fmtRow('Worst Trade:', stats.worstTrade < 0 ? chalk.red(worstTradeStr) : chalk.green(worstTradeStr), 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');
306
361
 
307
- // Quantitative Metrics
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 ==========
308
369
  draw2ColSeparator(boxWidth);
309
370
  draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
310
371
 
311
- const sharpeColor = parseFloat(sharpeRatio) >= 1 ? chalk.green : parseFloat(sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
312
- const sortinoColor = parseFloat(sortinoRatio) >= 1.5 ? chalk.green : parseFloat(sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
313
- const ddColor = maxDrawdown <= 5 ? chalk.green : maxDrawdown <= 15 ? chalk.yellow : chalk.red;
314
- 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;
315
376
 
316
377
  console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(riskRewardRatio), col2) + chalk.cyan('\u2551'));
317
- console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
318
- 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'));
319
- 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'));
320
381
 
321
382
  drawBoxFooter(boxWidth);
322
383
 
323
- // Equity Curve
384
+ // ========== EQUITY CURVE ==========
324
385
  console.log();
325
386
  drawBoxHeader('EQUITY CURVE', boxWidth);
326
387
 
@@ -357,13 +418,15 @@ const showStats = async (service) => {
357
418
  console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
358
419
  });
359
420
  } else {
360
- const msg = ' No trade data available';
361
- console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(chartInnerWidth - msg.length) + chalk.cyan('\u2551'));
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'));
362
425
  }
363
426
 
364
427
  drawBoxFooter(boxWidth);
365
428
 
366
- // Trades History - Simplified table
429
+ // ========== TRADES HISTORY ==========
367
430
  console.log();
368
431
  drawBoxHeader('TRADES HISTORY', boxWidth);
369
432
 
@@ -372,20 +435,20 @@ const showStats = async (service) => {
372
435
  // Helper to extract symbol from contractId (e.g., "CON.F.US.EP.H25" -> "ES H25")
373
436
  const extractSymbol = (contractId) => {
374
437
  if (!contractId) return 'N/A';
375
- // Format: CON.F.US.{SYMBOL}.{MONTH} -> extract symbol and month
438
+ // ProjectX format: CON.F.US.{SYMBOL}.{MONTH}
376
439
  const parts = contractId.split('.');
377
440
  if (parts.length >= 5) {
378
- const sym = parts[3]; // EP, ENQ, etc.
379
- const month = parts[4]; // H25, M25, etc.
380
- // Map common symbols
441
+ const sym = parts[3];
442
+ const month = parts[4];
381
443
  const symbolMap = { 'EP': 'ES', 'ENQ': 'NQ', 'MES': 'MES', 'MNQ': 'MNQ', 'YM': 'YM', 'NKD': 'NKD', 'RTY': 'RTY' };
382
444
  return (symbolMap[sym] || sym) + ' ' + month;
383
445
  }
446
+ // Rithmic format: already clean symbol
447
+ if (contractId.length <= 10) return contractId;
384
448
  return contractId.substring(0, 10);
385
449
  };
386
450
 
387
451
  if (allTrades.length > 0) {
388
- // Simple format: Time | Symbol | Price | P&L | Side
389
452
  const header = ' Time | Symbol | Price | P&L | Side ';
390
453
  console.log(chalk.cyan('\u2551') + chalk.white(header.padEnd(innerWidth)) + chalk.cyan('\u2551'));
391
454
  console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
@@ -406,7 +469,6 @@ const showStats = async (service) => {
406
469
  const row = ` ${time.padEnd(9)} | ${symbol.padEnd(9)} | ${price.padEnd(10)} | ${pnlText.padEnd(10)} | ${side.padEnd(6)}`;
407
470
  const coloredRow = ` ${time.padEnd(9)} | ${symbol.padEnd(9)} | ${price.padEnd(10)} | ${pnlColored} | ${sideColored}`;
408
471
 
409
- // Calculate visible length without ANSI codes
410
472
  const visLen = row.length;
411
473
  const padding = Math.max(0, innerWidth - visLen);
412
474
 
@@ -418,65 +480,80 @@ const showStats = async (service) => {
418
480
  console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
419
481
  }
420
482
  } else {
421
- const msg = ' No trade history available';
483
+ const msg = connectionTypes.rithmic > 0
484
+ ? ' No trade history (Rithmic API limitation)'
485
+ : ' No trade history available';
422
486
  console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
423
487
  }
424
488
 
425
489
  drawBoxFooter(boxWidth);
426
490
 
427
- // HQX Score
428
- console.log();
429
- drawBoxHeader('HQX SCORE', boxWidth);
430
-
431
- const winRateScore = Math.min(100, parseFloat(winRate) * 1.5);
432
- const profitFactorScore = profitFactor === '∞' ? 100 : Math.min(100, parseFloat(profitFactor) * 40);
433
- const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
434
- const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
435
- ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
436
- : 50;
437
- const volumeScore = Math.min(100, stats.totalTrades * 2);
438
- const returnScore = Math.min(100, Math.max(0, parseFloat(returnPercent) * 10 + 50));
439
-
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
- const makeBar = (score, width = 20) => {
445
- const filled = Math.round((score / 100) * width);
446
- const empty = width - filled;
447
- const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
448
- return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
449
- };
450
-
451
- const metricsDisplay = [
452
- { name: 'Win Rate', score: winRateScore },
453
- { name: 'Profit Factor', score: profitFactorScore },
454
- { name: 'Consistency', score: consistencyScore },
455
- { name: 'Risk Management', score: riskScore },
456
- { name: 'Volume', score: volumeScore },
457
- { name: 'Returns', score: returnScore }
458
- ];
459
-
460
- const barWidth = 30;
461
- const labelWidth = 18;
462
-
463
- const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScore))} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
464
- const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
465
- console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
466
- console.log(chalk.cyan('\u2551') + chalk.gray('─'.repeat(innerWidth)) + chalk.cyan('\u2551'));
467
-
468
- for (const metric of metricsDisplay) {
469
- const label = (' ' + metric.name + ':').padEnd(labelWidth);
470
- const bar = makeBar(metric.score, barWidth);
471
- const pct = (metric.score.toFixed(0) + '%').padStart(5);
472
- const line = label + bar + ' ' + pct;
473
- const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
474
- console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
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);
475
546
  }
476
547
 
477
- drawBoxFooter(boxWidth);
478
548
  console.log();
479
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
+
480
557
  } catch (error) {
481
558
  if (spinner) spinner.fail('Error: ' + error.message);
482
559
  }