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 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.5",
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,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) : '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';
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
- const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
276
- const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
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
- // Main Summary
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
- const pfColor = parseFloat(profitFactor) >= 1.5 ? chalk.green(profitFactor) : parseFloat(profitFactor) >= 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);
356
+
357
+ // Worst trade display
358
+ const worstTradeStr = stats.worstTrade < 0 ? '-$' + Math.abs(stats.worstTrade).toFixed(2) : '$' + stats.worstTrade.toFixed(2);
295
359
 
296
- 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'));
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
- // 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 ==========
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
- // Equity Curve
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 = ' No trade data available';
356
- 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'));
357
425
  }
358
426
 
359
427
  drawBoxFooter(boxWidth);
360
428
 
361
- // Trades History
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 colTime = 12;
369
- const colSymbol = 10;
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
- // Use API fields directly: creationTimestamp, contractId, price, profitAndLoss, side
396
- const timestamp = trade.creationTimestamp || trade.timestamp || trade.exitTime;
397
- const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : '--:--';
398
- const symbol = (trade.contractId || trade.contractName || trade.symbol || 'N/A').substring(0, colSymbol - 1);
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 pnlStr = pnl >= 0 ? chalk.green('+$' + pnl.toFixed(0)) : chalk.red('-$' + Math.abs(pnl).toFixed(0));
406
- const direction = trade.side === 0 ? chalk.green('LONG') : trade.side === 1 ? chalk.red('SHORT') : chalk.gray('N/A');
407
- const tradeId = String(trade.id || trade.tradeId || 'N/A').substring(0, colID - 1);
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
- (' ' + time).padEnd(colTime) + chalk.gray('|') +
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.replace(/\x1b\[[0-9;]*m/g, '').length;
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') + row + ' '.repeat(Math.max(0, padding)) + 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) + ' '.repeat(innerWidth - moreMsg.length) + chalk.cyan('\u2551'));
480
+ console.log(chalk.cyan('\u2551') + chalk.gray(moreMsg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
429
481
  }
430
482
  } else {
431
- const msg = ' No trade history available';
432
- console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(innerWidth - msg.length) + chalk.cyan('\u2551'));
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 Score
438
- console.log();
439
- drawBoxHeader('HQX SCORE', boxWidth);
440
-
441
- const winRateScore = Math.min(100, parseFloat(winRate) * 1.5);
442
- const profitFactorScore = profitFactor === '∞' ? 100 : Math.min(100, parseFloat(profitFactor) * 40);
443
- const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
444
- const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
445
- ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
446
- : 50;
447
- const volumeScore = Math.min(100, stats.totalTrades * 2);
448
- const returnScore = Math.min(100, Math.max(0, parseFloat(returnPercent) * 10 + 50));
449
-
450
- const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
451
- const scoreColor = hqxScore >= 70 ? chalk.green : hqxScore >= 50 ? chalk.yellow : chalk.red;
452
- const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
453
-
454
- const makeBar = (score, width = 20) => {
455
- const filled = Math.round((score / 100) * width);
456
- const empty = width - filled;
457
- const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
458
- return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
459
- };
460
-
461
- const metricsDisplay = [
462
- { name: 'Win Rate', score: winRateScore },
463
- { name: 'Profit Factor', score: profitFactorScore },
464
- { name: 'Consistency', score: consistencyScore },
465
- { name: 'Risk Management', score: riskScore },
466
- { name: 'Volume', score: volumeScore },
467
- { name: 'Returns', score: returnScore }
468
- ];
469
-
470
- const barWidth = 30;
471
- const labelWidth = 18;
472
-
473
- const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScore))} / 100 [Grade: ${scoreColor(scoreGrade)}]`;
474
- const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
475
- console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
476
- console.log(chalk.cyan('\u2551') + chalk.gray('─'.repeat(innerWidth)) + chalk.cyan('\u2551'));
477
-
478
- for (const metric of metricsDisplay) {
479
- const label = (' ' + metric.name + ':').padEnd(labelWidth);
480
- const bar = makeBar(metric.score, barWidth);
481
- const pct = (metric.score.toFixed(0) + '%').padStart(5);
482
- const line = label + bar + ' ' + pct;
483
- const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
484
- 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);
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
  }