hedgequantx 2.6.163 → 2.7.0

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.
Files changed (146) hide show
  1. package/README.md +15 -88
  2. package/bin/cli.js +0 -11
  3. package/dist/lib/api.jsc +0 -0
  4. package/dist/lib/api2.jsc +0 -0
  5. package/dist/lib/core.jsc +0 -0
  6. package/dist/lib/core2.jsc +0 -0
  7. package/dist/lib/data.js +1 -1
  8. package/dist/lib/data.jsc +0 -0
  9. package/dist/lib/data2.jsc +0 -0
  10. package/dist/lib/decoder.jsc +0 -0
  11. package/dist/lib/m/mod1.jsc +0 -0
  12. package/dist/lib/m/mod2.jsc +0 -0
  13. package/dist/lib/n/r1.jsc +0 -0
  14. package/dist/lib/n/r2.jsc +0 -0
  15. package/dist/lib/n/r3.jsc +0 -0
  16. package/dist/lib/n/r4.jsc +0 -0
  17. package/dist/lib/n/r5.jsc +0 -0
  18. package/dist/lib/n/r6.jsc +0 -0
  19. package/dist/lib/n/r7.jsc +0 -0
  20. package/dist/lib/o/util1.jsc +0 -0
  21. package/dist/lib/o/util2.jsc +0 -0
  22. package/package.json +6 -3
  23. package/src/app.js +40 -162
  24. package/src/config/constants.js +31 -33
  25. package/src/config/propfirms.js +13 -217
  26. package/src/config/settings.js +0 -43
  27. package/src/lib/api.js +198 -0
  28. package/src/lib/api2.js +353 -0
  29. package/src/lib/core.js +539 -0
  30. package/src/lib/core2.js +341 -0
  31. package/src/lib/data.js +555 -0
  32. package/src/lib/data2.js +492 -0
  33. package/src/lib/decoder.js +599 -0
  34. package/src/lib/m/s1.js +804 -0
  35. package/src/lib/m/s2.js +34 -0
  36. package/src/lib/n/r1.js +454 -0
  37. package/src/lib/n/r2.js +514 -0
  38. package/src/lib/n/r3.js +631 -0
  39. package/src/lib/n/r4.js +401 -0
  40. package/src/lib/n/r5.js +335 -0
  41. package/src/lib/n/r6.js +425 -0
  42. package/src/lib/n/r7.js +530 -0
  43. package/src/lib/o/l1.js +44 -0
  44. package/src/lib/o/l2.js +427 -0
  45. package/src/lib/python-bridge.js +206 -0
  46. package/src/menus/connect.js +14 -176
  47. package/src/menus/dashboard.js +65 -110
  48. package/src/pages/accounts.js +18 -18
  49. package/src/pages/algo/copy-trading.js +210 -240
  50. package/src/pages/algo/index.js +41 -104
  51. package/src/pages/algo/one-account.js +386 -33
  52. package/src/pages/algo/ui.js +312 -151
  53. package/src/pages/orders.js +3 -3
  54. package/src/pages/positions.js +3 -3
  55. package/src/pages/stats/chart.js +74 -0
  56. package/src/pages/stats/display.js +228 -0
  57. package/src/pages/stats/index.js +236 -0
  58. package/src/pages/stats/metrics.js +213 -0
  59. package/src/pages/user.js +6 -6
  60. package/src/services/hqx-server/constants.js +55 -0
  61. package/src/services/hqx-server/index.js +401 -0
  62. package/src/services/hqx-server/latency.js +81 -0
  63. package/src/services/index.js +12 -3
  64. package/src/services/rithmic/accounts.js +7 -32
  65. package/src/services/rithmic/connection.js +1 -204
  66. package/src/services/rithmic/contracts.js +116 -99
  67. package/src/services/rithmic/handlers.js +21 -196
  68. package/src/services/rithmic/index.js +63 -120
  69. package/src/services/rithmic/market.js +31 -0
  70. package/src/services/rithmic/orders.js +5 -111
  71. package/src/services/rithmic/protobuf.js +384 -138
  72. package/src/services/session.js +22 -173
  73. package/src/ui/box.js +10 -18
  74. package/src/ui/index.js +1 -3
  75. package/src/ui/menu.js +1 -1
  76. package/src/utils/prompts.js +2 -2
  77. package/dist/lib/m/s1.js +0 -1
  78. package/src/menus/ai-agent-connect.js +0 -181
  79. package/src/menus/ai-agent-models.js +0 -219
  80. package/src/menus/ai-agent-oauth.js +0 -292
  81. package/src/menus/ai-agent-ui.js +0 -141
  82. package/src/menus/ai-agent.js +0 -484
  83. package/src/pages/algo/algo-config.js +0 -195
  84. package/src/pages/algo/algo-multi.js +0 -801
  85. package/src/pages/algo/algo-utils.js +0 -58
  86. package/src/pages/algo/copy-engine.js +0 -449
  87. package/src/pages/algo/custom-strategy.js +0 -459
  88. package/src/pages/algo/logger.js +0 -245
  89. package/src/pages/algo/smart-logs-data.js +0 -218
  90. package/src/pages/algo/smart-logs.js +0 -387
  91. package/src/pages/algo/ui-constants.js +0 -144
  92. package/src/pages/algo/ui-summary.js +0 -184
  93. package/src/pages/stats-calculations.js +0 -191
  94. package/src/pages/stats-ui.js +0 -381
  95. package/src/pages/stats.js +0 -339
  96. package/src/services/ai/client-analysis.js +0 -194
  97. package/src/services/ai/client-models.js +0 -333
  98. package/src/services/ai/client.js +0 -343
  99. package/src/services/ai/index.js +0 -384
  100. package/src/services/ai/oauth-anthropic.js +0 -265
  101. package/src/services/ai/oauth-gemini.js +0 -223
  102. package/src/services/ai/oauth-iflow.js +0 -269
  103. package/src/services/ai/oauth-openai.js +0 -233
  104. package/src/services/ai/oauth-qwen.js +0 -279
  105. package/src/services/ai/providers/direct-providers.js +0 -323
  106. package/src/services/ai/providers/index.js +0 -62
  107. package/src/services/ai/providers/other-providers.js +0 -104
  108. package/src/services/ai/proxy-install.js +0 -249
  109. package/src/services/ai/proxy-manager.js +0 -494
  110. package/src/services/ai/proxy-remote.js +0 -161
  111. package/src/services/ai/strategy-supervisor.js +0 -1312
  112. package/src/services/ai/supervisor-data.js +0 -195
  113. package/src/services/ai/supervisor-optimize.js +0 -215
  114. package/src/services/ai/supervisor-sync.js +0 -178
  115. package/src/services/ai/supervisor-utils.js +0 -158
  116. package/src/services/ai/supervisor.js +0 -484
  117. package/src/services/ai/validation.js +0 -250
  118. package/src/services/hqx-server-events.js +0 -110
  119. package/src/services/hqx-server-handlers.js +0 -217
  120. package/src/services/hqx-server-latency.js +0 -136
  121. package/src/services/hqx-server.js +0 -403
  122. package/src/services/position-constants.js +0 -28
  123. package/src/services/position-exit-logic.js +0 -174
  124. package/src/services/position-manager.js +0 -438
  125. package/src/services/position-momentum.js +0 -206
  126. package/src/services/projectx/accounts.js +0 -142
  127. package/src/services/projectx/index.js +0 -443
  128. package/src/services/projectx/market.js +0 -172
  129. package/src/services/projectx/stats.js +0 -110
  130. package/src/services/projectx/trading.js +0 -180
  131. package/src/services/rithmic/latency-tracker.js +0 -182
  132. package/src/services/rithmic/market-data-decoders.js +0 -229
  133. package/src/services/rithmic/market-data.js +0 -272
  134. package/src/services/rithmic/orders-fast.js +0 -246
  135. package/src/services/rithmic/proto-decoders.js +0 -403
  136. package/src/services/rithmic/specs.js +0 -146
  137. package/src/services/rithmic/trade-history.js +0 -254
  138. package/src/services/session-history.js +0 -475
  139. package/src/services/strategy/hft-signal-calc.js +0 -147
  140. package/src/services/strategy/hft-tick.js +0 -407
  141. package/src/services/strategy/recovery-math.js +0 -402
  142. package/src/services/tradovate/constants.js +0 -109
  143. package/src/services/tradovate/index.js +0 -392
  144. package/src/services/tradovate/market.js +0 -47
  145. package/src/services/tradovate/orders.js +0 -145
  146. package/src/services/tradovate/websocket.js +0 -97
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @fileoverview Stats equity curve chart
3
+ * @module pages/stats/chart
4
+ *
5
+ * STRICT RULE: Display ONLY values from API - NO estimation/simulation
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const asciichart = require('asciichart');
10
+ const { getLogoWidth, drawBoxHeader, drawBoxFooter } = require('../../ui');
11
+
12
+ /**
13
+ * Render equity curve chart
14
+ * @param {Object} data - Chart data
15
+ * @param {Array} data.allTrades - All trades from API
16
+ * @param {number} data.totalStartingBalance - Starting balance
17
+ * @param {Object} data.connectionTypes - Connection type counts
18
+ */
19
+ const renderEquityCurve = (data) => {
20
+ const { allTrades, totalStartingBalance, connectionTypes } = data;
21
+ const boxWidth = getLogoWidth();
22
+ const chartInnerWidth = boxWidth - 2;
23
+
24
+ console.log();
25
+ drawBoxHeader('EQUITY CURVE', boxWidth);
26
+
27
+ if (allTrades.length > 0) {
28
+ const yAxisWidth = 10;
29
+ const chartAreaWidth = chartInnerWidth - yAxisWidth - 4;
30
+
31
+ // Build equity curve from trades P&L (100% API data)
32
+ let equityData = [totalStartingBalance || 100000];
33
+ let eqVal = equityData[0];
34
+ allTrades.forEach(trade => {
35
+ eqVal += (trade.profitAndLoss || trade.pnl || 0);
36
+ equityData.push(eqVal);
37
+ });
38
+
39
+ // Downsample if too many data points
40
+ const maxDataPoints = chartAreaWidth - 5;
41
+ if (equityData.length > maxDataPoints) {
42
+ const step = Math.ceil(equityData.length / maxDataPoints);
43
+ equityData = equityData.filter((_, i) => i % step === 0);
44
+ }
45
+
46
+ // Chart configuration
47
+ const chartConfig = {
48
+ height: 10,
49
+ colors: [equityData[equityData.length - 1] < equityData[0] ? asciichart.red : asciichart.green],
50
+ format: (x) => ('$' + (x / 1000).toFixed(0) + 'K').padStart(yAxisWidth)
51
+ };
52
+
53
+ // Render chart
54
+ const chart = asciichart.plot(equityData, chartConfig);
55
+ chart.split('\n').forEach(line => {
56
+ let chartLine = ' ' + line;
57
+ const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
58
+ if (len < chartInnerWidth) chartLine += ' '.repeat(chartInnerWidth - len);
59
+ console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
60
+ });
61
+ } else {
62
+ // No trade data message
63
+ const msg = connectionTypes.rithmic > 0
64
+ ? ' No trade history (Rithmic does not provide trade history API)'
65
+ : ' No trade data available';
66
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(Math.max(0, chartInnerWidth - msg.length)) + chalk.cyan('\u2551'));
67
+ }
68
+
69
+ drawBoxFooter(boxWidth);
70
+ };
71
+
72
+ module.exports = {
73
+ renderEquityCurve,
74
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @fileoverview Stats display/rendering functions - Rithmic Only
3
+ * @module pages/stats/display
4
+ *
5
+ * STRICT RULE: Display ONLY values from API - NO estimation/simulation
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const { getLogoWidth, drawBoxHeader, drawBoxFooter, getColWidths, draw2ColHeader, draw2ColSeparator, fmtRow } = require('../../ui');
10
+
11
+ /**
12
+ * Render account overview and trading performance section
13
+ */
14
+ const renderOverview = (data) => {
15
+ const { connectionTypes, activeAccounts, totalBalance, totalPnL, totalStartingBalance,
16
+ totalOpenPositions, totalOpenOrders, stats, metrics,
17
+ hasBalanceData, hasPnLData, hasTradeData, connections } = data;
18
+
19
+ const boxWidth = getLogoWidth();
20
+ const { col1, col2 } = getColWidths(boxWidth);
21
+
22
+ // Connection type string (Rithmic only)
23
+ const connTypeStr = connectionTypes.rithmic > 0 ? `Rithmic(${connectionTypes.rithmic})` : '';
24
+
25
+ // Format balance/P&L
26
+ const balanceStr = hasBalanceData ? '$' + totalBalance.toLocaleString(undefined, {minimumFractionDigits: 2}) : 'N/A';
27
+ const pnlStr = hasPnLData
28
+ ? (totalPnL >= 0 ? '+' : '') + '$' + totalPnL.toLocaleString(undefined, {minimumFractionDigits: 2}) + (metrics.returnPercent !== 'N/A' ? ' (' + metrics.returnPercent + '%)' : '')
29
+ : 'N/A';
30
+ const startBalStr = totalStartingBalance > 0 ? '$' + totalStartingBalance.toLocaleString(undefined, {minimumFractionDigits: 2}) : 'N/A';
31
+
32
+ // Colors
33
+ const totalBalanceColor = hasBalanceData ? (totalBalance >= 0 ? chalk.green : chalk.red) : chalk.gray;
34
+ const pnlColor = hasPnLData ? (totalPnL >= 0 ? chalk.green : chalk.red) : chalk.gray;
35
+
36
+ drawBoxHeader('HQX STATS', boxWidth);
37
+ draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
38
+
39
+ console.log(chalk.cyan('\u2551') + fmtRow('Connections:', chalk.cyan(connTypeStr || String(connections || 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'));
40
+ 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'));
41
+ 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'));
42
+ console.log(chalk.cyan('\u2551') + fmtRow('Starting Balance:', chalk.white(startBalStr), col1) + chalk.cyan('\u2502') + fmtRow('Win Rate:', metrics.winRate !== 'N/A' ? (parseFloat(metrics.winRate) >= 50 ? chalk.green(metrics.winRate + '%') : chalk.yellow(metrics.winRate + '%')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
43
+ console.log(chalk.cyan('\u2551') + fmtRow('Total P&L:', pnlColor(pnlStr), col1) + chalk.cyan('\u2502') + fmtRow('Long Trades:', hasTradeData ? chalk.white(stats.longTrades + (metrics.longWinRate !== 'N/A' ? ' (' + metrics.longWinRate + '%)' : '')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
44
+ console.log(chalk.cyan('\u2551') + fmtRow('Open Positions:', chalk.white(String(totalOpenPositions)), col1) + chalk.cyan('\u2502') + fmtRow('Short Trades:', hasTradeData ? chalk.white(stats.shortTrades + (metrics.shortWinRate !== 'N/A' ? ' (' + metrics.shortWinRate + '%)' : '')) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
45
+ 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'));
46
+ };
47
+
48
+ /**
49
+ * Render P&L metrics section
50
+ */
51
+ const renderPnLMetrics = (data) => {
52
+ const { stats, metrics, hasTradeData } = data;
53
+ const boxWidth = getLogoWidth();
54
+ const { col1, col2 } = getColWidths(boxWidth);
55
+
56
+ draw2ColSeparator(boxWidth);
57
+ draw2ColHeader('P&L METRICS', 'RISK METRICS', boxWidth);
58
+
59
+ const pfColor = metrics.profitFactor === '∞' ? chalk.green(metrics.profitFactor)
60
+ : metrics.profitFactor === 'N/A' ? chalk.gray(metrics.profitFactor)
61
+ : parseFloat(metrics.profitFactor) >= 1.5 ? chalk.green(metrics.profitFactor)
62
+ : parseFloat(metrics.profitFactor) >= 1 ? chalk.yellow(metrics.profitFactor)
63
+ : chalk.red(metrics.profitFactor);
64
+
65
+ const worstTradeStr = stats.worstTrade < 0 ? '-$' + Math.abs(stats.worstTrade).toFixed(2) : '$' + stats.worstTrade.toFixed(2);
66
+ const netPnLStr = hasTradeData ? (metrics.netPnL >= 0 ? chalk.green('$' + metrics.netPnL.toFixed(2)) : chalk.red('-$' + Math.abs(metrics.netPnL).toFixed(2))) : chalk.gray('N/A');
67
+
68
+ console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnLStr, col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
69
+ 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'));
70
+ 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'));
71
+ console.log(chalk.cyan('\u2551') + fmtRow('Avg Win:', hasTradeData ? chalk.green('$' + metrics.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'));
72
+ console.log(chalk.cyan('\u2551') + fmtRow('Avg Loss:', hasTradeData ? (stats.losingTrades > 0 ? chalk.red('-$' + metrics.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'));
73
+ };
74
+
75
+ /**
76
+ * Render quantitative metrics section
77
+ */
78
+ const renderQuantMetrics = (data) => {
79
+ const { quantMetrics, metrics, hasTradeData } = data;
80
+ const boxWidth = getLogoWidth();
81
+ const { col1, col2 } = getColWidths(boxWidth);
82
+
83
+ draw2ColSeparator(boxWidth);
84
+ draw2ColHeader('QUANTITATIVE METRICS', 'ADVANCED RATIOS', boxWidth);
85
+
86
+ const calmarRatio = quantMetrics.maxDrawdown > 0 && quantMetrics.returnPercent !== 'N/A'
87
+ ? (parseFloat(quantMetrics.returnPercent) / quantMetrics.maxDrawdown).toFixed(2) : 'N/A';
88
+
89
+ const sharpeColor = quantMetrics.sharpeRatio === 'N/A' ? chalk.gray : parseFloat(quantMetrics.sharpeRatio) >= 1 ? chalk.green : parseFloat(quantMetrics.sharpeRatio) >= 0.5 ? chalk.yellow : chalk.red;
90
+ const sortinoColor = quantMetrics.sortinoRatio === 'N/A' ? chalk.gray : parseFloat(quantMetrics.sortinoRatio) >= 1.5 ? chalk.green : parseFloat(quantMetrics.sortinoRatio) >= 0.5 ? chalk.yellow : chalk.red;
91
+ const ddColor = quantMetrics.maxDrawdown === 0 ? chalk.gray : quantMetrics.maxDrawdown <= 5 ? chalk.green : quantMetrics.maxDrawdown <= 15 ? chalk.yellow : chalk.red;
92
+ const rrColor = metrics.riskRewardRatio === 'N/A' ? chalk.gray : parseFloat(metrics.riskRewardRatio) >= 2 ? chalk.green : parseFloat(metrics.riskRewardRatio) >= 1 ? chalk.yellow : chalk.red;
93
+
94
+ console.log(chalk.cyan('\u2551') + fmtRow('Sharpe Ratio:', sharpeColor(quantMetrics.sharpeRatio), col1) + chalk.cyan('\u2502') + fmtRow('Risk/Reward:', rrColor(metrics.riskRewardRatio), col2) + chalk.cyan('\u2551'));
95
+ console.log(chalk.cyan('\u2551') + fmtRow('Sortino Ratio:', sortinoColor(quantMetrics.sortinoRatio), col1) + chalk.cyan('\u2502') + fmtRow('Calmar Ratio:', calmarRatio === 'N/A' ? chalk.gray(calmarRatio) : chalk.white(calmarRatio), col2) + chalk.cyan('\u2551'));
96
+ console.log(chalk.cyan('\u2551') + fmtRow('Max Drawdown:', hasTradeData && quantMetrics.maxDrawdown > 0 ? ddColor(quantMetrics.maxDrawdown.toFixed(2) + '%') : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Expectancy:', hasTradeData ? (metrics.expectancy >= 0 ? chalk.green('$' + metrics.expectancy.toFixed(2)) : chalk.red('$' + metrics.expectancy.toFixed(2))) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
97
+ console.log(chalk.cyan('\u2551') + fmtRow('Std Deviation:', hasTradeData ? chalk.white('$' + quantMetrics.stdDev.toFixed(2)) : chalk.gray('N/A'), col1) + chalk.cyan('\u2502') + fmtRow('Avg Trade:', hasTradeData ? (quantMetrics.avgReturn >= 0 ? chalk.green('$' + quantMetrics.avgReturn.toFixed(2)) : chalk.red('$' + quantMetrics.avgReturn.toFixed(2))) : chalk.gray('N/A'), col2) + chalk.cyan('\u2551'));
98
+
99
+ drawBoxFooter(boxWidth);
100
+ };
101
+
102
+ /**
103
+ * Render trades history section
104
+ */
105
+ const renderTradesHistory = (data) => {
106
+ const { allTrades } = data;
107
+ const boxWidth = getLogoWidth();
108
+ const innerWidth = boxWidth - 2;
109
+
110
+ console.log();
111
+ drawBoxHeader('TRADES HISTORY', boxWidth);
112
+
113
+ const extractSymbol = (contractId) => {
114
+ if (!contractId) return 'N/A';
115
+ if (contractId.length <= 10) return contractId;
116
+ return contractId.substring(0, 10);
117
+ };
118
+
119
+ if (allTrades.length > 0) {
120
+ const colTime = 10;
121
+ const colSymbol = 12;
122
+ const colPrice = 12;
123
+ const colPnl = 12;
124
+ const colSide = 6;
125
+ const separators = 15;
126
+ const fixedWidth = colTime + colSymbol + colPrice + colPnl + colSide + separators;
127
+ const colAccount = Math.max(10, innerWidth - fixedWidth);
128
+
129
+ const header = ` ${'Time'.padEnd(colTime)}| ${'Symbol'.padEnd(colSymbol)}| ${'Price'.padEnd(colPrice)}| ${'P&L'.padEnd(colPnl)}| ${'Side'.padEnd(colSide)}| ${'Account'.padEnd(colAccount - 2)}`;
130
+ console.log(chalk.cyan('\u2551') + chalk.white(header) + chalk.cyan('\u2551'));
131
+ console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
132
+
133
+ const recentTrades = allTrades.slice(-10).reverse();
134
+
135
+ for (const trade of recentTrades) {
136
+ const timestamp = trade.creationTimestamp || trade.timestamp;
137
+ const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
138
+ const symbol = extractSymbol(trade.contractId || trade.symbol);
139
+ const price = (trade.price || 0).toFixed(2);
140
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
141
+ const pnlText = pnl >= 0 ? `+$${pnl.toFixed(0)}` : `-$${Math.abs(pnl).toFixed(0)}`;
142
+ const side = trade.side === 0 ? 'BUY' : trade.side === 1 ? 'SELL' : 'N/A';
143
+ const accountName = (trade.accountName || 'N/A').substring(0, colAccount - 3);
144
+
145
+ const timeStr = time.padEnd(colTime);
146
+ const symbolStr = symbol.padEnd(colSymbol);
147
+ const priceStr = price.padEnd(colPrice);
148
+ const pnlStr = pnlText.padEnd(colPnl);
149
+ const sideStr = side.padEnd(colSide);
150
+ const accountStr = accountName.padEnd(colAccount - 2);
151
+
152
+ const pnlColored = pnl >= 0 ? chalk.green(pnlStr) : chalk.red(pnlStr);
153
+ const sideColored = trade.side === 0 ? chalk.green(sideStr) : chalk.red(sideStr);
154
+
155
+ const row = ` ${timeStr}| ${symbolStr}| ${priceStr}| ${pnlColored}| ${sideColored}| ${accountStr}`;
156
+ console.log(chalk.cyan('\u2551') + row + chalk.cyan('\u2551'));
157
+ }
158
+
159
+ if (allTrades.length > 10) {
160
+ const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
161
+ console.log(chalk.cyan('\u2551') + moreMsg.padEnd(innerWidth) + chalk.cyan('\u2551'));
162
+ }
163
+ } else {
164
+ const msg = ' No trade history (Rithmic API limitation)';
165
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
166
+ }
167
+
168
+ drawBoxFooter(boxWidth);
169
+ };
170
+
171
+ /**
172
+ * Render HQX score section
173
+ */
174
+ const renderHQXScore = (data) => {
175
+ const { hqxScoreData, hasTradeData, stats } = data;
176
+
177
+ if (!hasTradeData && stats.totalTrades === 0) return;
178
+
179
+ const boxWidth = getLogoWidth();
180
+ const innerWidth = boxWidth - 2;
181
+
182
+ console.log();
183
+ drawBoxHeader('HQX SCORE', boxWidth);
184
+
185
+ const scoreColor = hqxScoreData.hqxScore >= 70 ? chalk.green : hqxScoreData.hqxScore >= 50 ? chalk.yellow : chalk.red;
186
+
187
+ const makeBar = (score, width = 30) => {
188
+ const filled = Math.round((score / 100) * width);
189
+ const empty = width - filled;
190
+ const color = score >= 70 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
191
+ return color('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(empty));
192
+ };
193
+
194
+ const labelWidth = 18;
195
+
196
+ const overallLine = ` OVERALL SCORE: ${scoreColor(String(hqxScoreData.hqxScore))} / 100 [Grade: ${scoreColor(hqxScoreData.scoreGrade)}]`;
197
+ const overallVisLen = overallLine.replace(/\x1b\[[0-9;]*m/g, '').length;
198
+ console.log(chalk.cyan('\u2551') + overallLine + ' '.repeat(innerWidth - overallVisLen) + chalk.cyan('\u2551'));
199
+ console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
200
+
201
+ for (const metric of hqxScoreData.breakdown) {
202
+ const label = (' ' + metric.name + ':').padEnd(labelWidth);
203
+ const bar = makeBar(metric.score);
204
+ const pct = (metric.score.toFixed(0) + '%').padStart(5);
205
+ const line = label + bar + ' ' + pct;
206
+ const visLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
207
+ console.log(chalk.cyan('\u2551') + chalk.white(label) + bar + ' ' + chalk.white(pct) + ' '.repeat(innerWidth - visLen) + chalk.cyan('\u2551'));
208
+ }
209
+
210
+ drawBoxFooter(boxWidth);
211
+ };
212
+
213
+ /**
214
+ * Render data source notice
215
+ */
216
+ const renderNotice = () => {
217
+ console.log();
218
+ console.log(chalk.gray(' Note: Rithmic API provides balance/P&L only. Trade history not available.'));
219
+ };
220
+
221
+ module.exports = {
222
+ renderOverview,
223
+ renderPnLMetrics,
224
+ renderQuantMetrics,
225
+ renderTradesHistory,
226
+ renderHQXScore,
227
+ renderNotice,
228
+ };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @fileoverview Stats Page - Rithmic Only
3
+ * @module pages/stats
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API
6
+ * - Rithmic: Uses PNL_PLANT for balance/P&L, ORDER_PLANT for accounts
7
+ * - NO estimation, NO simulation, NO mock data
8
+ */
9
+
10
+ const ora = require('ora');
11
+
12
+ const { connections } = require('../../services');
13
+ const { prompts } = require('../../utils');
14
+ const { aggregateStats, calculateDerivedMetrics, calculateQuantMetrics, calculateHQXScore } = require('./metrics');
15
+ const { renderOverview, renderPnLMetrics, renderQuantMetrics, renderTradesHistory, renderHQXScore, renderNotice } = require('./display');
16
+ const { renderEquityCurve } = require('./chart');
17
+
18
+ /**
19
+ * Fetch account data from all connections
20
+ */
21
+ const fetchAccountsData = async (allConns) => {
22
+ const connectionTypes = { rithmic: 0 };
23
+ let allAccountsData = [];
24
+
25
+ for (const conn of allConns) {
26
+ const connType = conn.type || 'rithmic';
27
+ const propfirmName = conn.propfirm || conn.type || 'Unknown';
28
+
29
+ if (connType === 'rithmic') connectionTypes.rithmic++;
30
+
31
+ try {
32
+ const result = await conn.service.getTradingAccounts();
33
+ if (result.success && result.accounts && result.accounts.length > 0) {
34
+ result.accounts.forEach(account => {
35
+ allAccountsData.push({
36
+ ...account,
37
+ propfirm: propfirmName,
38
+ connectionType: connType,
39
+ service: conn.service
40
+ });
41
+ });
42
+ }
43
+ } catch (e) {
44
+ // Silently skip failed connections
45
+ }
46
+ }
47
+
48
+ // Remove duplicates by accountId
49
+ const seen = new Set();
50
+ allAccountsData = allAccountsData.filter(acc => {
51
+ const id = String(acc.accountId);
52
+ if (seen.has(id)) return false;
53
+ seen.add(id);
54
+ return true;
55
+ });
56
+
57
+ return { connectionTypes, allAccountsData };
58
+ };
59
+
60
+ /**
61
+ * Aggregate data from active accounts
62
+ */
63
+ const aggregateAccountData = async (activeAccounts) => {
64
+ let totalBalance = 0;
65
+ let totalPnL = 0;
66
+ let totalStartingBalance = 0;
67
+ let allTrades = [];
68
+ let totalOpenPositions = 0;
69
+ let totalOpenOrders = 0;
70
+ let hasBalanceData = false;
71
+ let hasPnLData = false;
72
+ let hasTradeData = false;
73
+
74
+ for (let i = 0; i < activeAccounts.length; i++) {
75
+ const account = activeAccounts[i];
76
+ const svc = account.service;
77
+ const connType = account.connectionType || 'rithmic';
78
+
79
+ try {
80
+ // Balance from API
81
+ if (account.balance !== null && account.balance !== undefined) {
82
+ totalBalance += account.balance;
83
+ hasBalanceData = true;
84
+ }
85
+
86
+ // P&L from API (NEVER calculated locally)
87
+ if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
88
+ totalPnL += account.profitAndLoss;
89
+ hasPnLData = true;
90
+ }
91
+
92
+ // Starting balance
93
+ if (account.startingBalance !== null && account.startingBalance !== undefined) {
94
+ totalStartingBalance += account.startingBalance;
95
+ } else if (account.balance !== null && account.balance !== undefined) {
96
+ const pnl = account.profitAndLoss || 0;
97
+ totalStartingBalance += (account.balance - pnl);
98
+ }
99
+
100
+ // Positions from API
101
+ try {
102
+ const posResult = await svc.getPositions(account.accountId);
103
+ if (posResult.success && posResult.positions) {
104
+ totalOpenPositions += posResult.positions.length;
105
+ }
106
+ } catch (e) {}
107
+
108
+ // Orders from API
109
+ try {
110
+ const ordResult = await svc.getOrders(account.accountId);
111
+ if (ordResult.success && ordResult.orders) {
112
+ totalOpenOrders += ordResult.orders.filter(o => o.status === 1 || o.status === 'Working').length;
113
+ }
114
+ } catch (e) {}
115
+
116
+ // Lifetime stats (Rithmic returns null)
117
+ if (typeof svc.getLifetimeStats === 'function') {
118
+ try {
119
+ const lifetimeResult = await svc.getLifetimeStats(account.accountId);
120
+ if (lifetimeResult.success && lifetimeResult.stats) {
121
+ account.lifetimeStats = lifetimeResult.stats;
122
+ }
123
+ } catch (e) {}
124
+ }
125
+
126
+ // Trade history (Rithmic doesn't provide this)
127
+ if (typeof svc.getTradeHistory === 'function') {
128
+ try {
129
+ const tradesResult = await svc.getTradeHistory(account.accountId, 30);
130
+ if (tradesResult.success && tradesResult.trades && tradesResult.trades.length > 0) {
131
+ hasTradeData = true;
132
+ allTrades = allTrades.concat(tradesResult.trades.map(t => ({
133
+ ...t,
134
+ accountName: account.accountName,
135
+ propfirm: account.propfirm,
136
+ connectionType: connType
137
+ })));
138
+ }
139
+ } catch (e) {}
140
+ }
141
+ } catch (e) {}
142
+ }
143
+
144
+ return {
145
+ totalBalance,
146
+ totalPnL,
147
+ totalStartingBalance,
148
+ allTrades,
149
+ totalOpenPositions,
150
+ totalOpenOrders,
151
+ hasBalanceData,
152
+ hasPnLData,
153
+ hasTradeData,
154
+ };
155
+ };
156
+
157
+ /**
158
+ * Show Stats Page
159
+ */
160
+ const showStats = async (service) => {
161
+ let spinner;
162
+
163
+ try {
164
+ spinner = ora({ text: 'Loading stats...', color: 'yellow' }).start();
165
+
166
+ // Get all connections
167
+ const allConns = connections.count() > 0
168
+ ? connections.getAll()
169
+ : (service ? [{ service, propfirm: service.propfirm?.name || 'Unknown', type: 'rithmic' }] : []);
170
+
171
+ if (allConns.length === 0) {
172
+ spinner.fail('No connections found');
173
+ await prompts.waitForEnter();
174
+ return;
175
+ }
176
+
177
+ // Fetch accounts data
178
+ const { connectionTypes, allAccountsData } = await fetchAccountsData(allConns);
179
+
180
+ if (allAccountsData.length === 0) {
181
+ spinner.fail('No accounts found');
182
+ await prompts.waitForEnter();
183
+ return;
184
+ }
185
+
186
+ // Filter active accounts (status === 0)
187
+ const activeAccounts = allAccountsData.filter(acc => acc.status === 0);
188
+
189
+ if (activeAccounts.length === 0) {
190
+ spinner.fail('No active accounts found');
191
+ await prompts.waitForEnter();
192
+ return;
193
+ }
194
+
195
+ // Aggregate account data from APIs
196
+ const accountData = await aggregateAccountData(activeAccounts);
197
+
198
+ spinner.succeed('Stats loaded');
199
+ console.log();
200
+
201
+ // Calculate stats from API data
202
+ const stats = aggregateStats(activeAccounts, accountData.allTrades);
203
+ const metrics = calculateDerivedMetrics(stats, accountData.totalStartingBalance, accountData.totalPnL);
204
+ const quantMetrics = calculateQuantMetrics(accountData.allTrades, accountData.totalStartingBalance, accountData.totalPnL);
205
+ const hqxScoreData = calculateHQXScore(stats, metrics, accountData.totalStartingBalance);
206
+
207
+ // Prepare display data
208
+ const displayData = {
209
+ connectionTypes,
210
+ activeAccounts,
211
+ connections: connections.count() || 1,
212
+ stats,
213
+ metrics,
214
+ quantMetrics,
215
+ hqxScoreData,
216
+ allTrades: accountData.allTrades,
217
+ ...accountData,
218
+ };
219
+
220
+ // Render all sections
221
+ renderOverview(displayData);
222
+ renderPnLMetrics(displayData);
223
+ renderQuantMetrics(displayData);
224
+ renderEquityCurve(displayData);
225
+ renderTradesHistory(displayData);
226
+ renderHQXScore(displayData);
227
+ renderNotice();
228
+
229
+ } catch (error) {
230
+ if (spinner) spinner.fail('Error: ' + error.message);
231
+ }
232
+
233
+ await prompts.waitForEnter();
234
+ };
235
+
236
+ module.exports = { showStats };