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,213 @@
1
+ /**
2
+ * @fileoverview Stats metrics calculations
3
+ * @module pages/stats/metrics
4
+ *
5
+ * STRICT RULE: All calculations based on API data only
6
+ */
7
+
8
+ /**
9
+ * Calculate quantitative metrics from trade data
10
+ * @param {Array} trades - Trade array from API
11
+ * @param {number} totalStartingBalance - Starting balance
12
+ * @param {number} totalPnL - Total P&L from API
13
+ * @returns {Object} Calculated metrics
14
+ */
15
+ const calculateQuantMetrics = (trades, totalStartingBalance, totalPnL) => {
16
+ const tradePnLs = trades.map(t => t.profitAndLoss || t.pnl || 0);
17
+ const avgReturn = tradePnLs.length > 0 ? tradePnLs.reduce((a, b) => a + b, 0) / tradePnLs.length : 0;
18
+
19
+ // Standard deviation
20
+ const variance = tradePnLs.length > 0
21
+ ? tradePnLs.reduce((sum, pnl) => sum + Math.pow(pnl - avgReturn, 2), 0) / tradePnLs.length
22
+ : 0;
23
+ const stdDev = Math.sqrt(variance);
24
+
25
+ // Downside deviation
26
+ const downsideReturns = tradePnLs.filter(pnl => pnl < 0);
27
+ const downsideVariance = downsideReturns.length > 0
28
+ ? downsideReturns.reduce((sum, pnl) => sum + Math.pow(pnl, 2), 0) / downsideReturns.length
29
+ : 0;
30
+ const downsideDev = Math.sqrt(downsideVariance);
31
+
32
+ // Ratios
33
+ const sharpeRatio = stdDev > 0 ? (avgReturn / stdDev).toFixed(2) : 'N/A';
34
+ const sortinoRatio = downsideDev > 0 ? (avgReturn / downsideDev).toFixed(2) : 'N/A';
35
+
36
+ // Max Drawdown
37
+ let maxDrawdown = 0;
38
+ let peak = totalStartingBalance || 0;
39
+ let equity = peak;
40
+ if (peak > 0 && tradePnLs.length > 0) {
41
+ tradePnLs.forEach(pnl => {
42
+ equity += pnl;
43
+ if (equity > peak) peak = equity;
44
+ const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0;
45
+ if (drawdown > maxDrawdown) maxDrawdown = drawdown;
46
+ });
47
+ }
48
+
49
+ const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : 'N/A';
50
+
51
+ return {
52
+ avgReturn,
53
+ stdDev,
54
+ downsideDev,
55
+ sharpeRatio,
56
+ sortinoRatio,
57
+ maxDrawdown,
58
+ returnPercent,
59
+ };
60
+ };
61
+
62
+ /**
63
+ * Aggregate stats from lifetime stats or trades
64
+ * @param {Array} activeAccounts - Active accounts with lifetimeStats
65
+ * @param {Array} allTrades - All trades from API
66
+ * @returns {Object} Aggregated stats
67
+ */
68
+ const aggregateStats = (activeAccounts, allTrades) => {
69
+ let stats = {
70
+ totalTrades: 0, winningTrades: 0, losingTrades: 0,
71
+ totalWinAmount: 0, totalLossAmount: 0,
72
+ bestTrade: 0, worstTrade: 0, totalVolume: 0,
73
+ maxConsecutiveWins: 0, maxConsecutiveLosses: 0,
74
+ longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
75
+ };
76
+
77
+ // First: aggregate lifetimeStats from APIs
78
+ for (const account of activeAccounts) {
79
+ if (account.lifetimeStats) {
80
+ const s = account.lifetimeStats;
81
+ stats.totalTrades += s.totalTrades || 0;
82
+ stats.winningTrades += s.winningTrades || 0;
83
+ stats.losingTrades += s.losingTrades || 0;
84
+ stats.totalWinAmount += s.totalWinAmount || 0;
85
+ stats.totalLossAmount += s.totalLossAmount || 0;
86
+ stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
87
+ stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
88
+ stats.totalVolume += s.totalVolume || 0;
89
+ stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
90
+ stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
91
+ stats.longTrades += s.longTrades || 0;
92
+ stats.shortTrades += s.shortTrades || 0;
93
+ }
94
+ }
95
+
96
+ // If no lifetimeStats, calculate from trade history
97
+ if (stats.totalTrades === 0 && allTrades.length > 0) {
98
+ stats.totalTrades = allTrades.length;
99
+ let consecutiveWins = 0, consecutiveLosses = 0;
100
+
101
+ for (const trade of allTrades) {
102
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
103
+ const size = trade.size || trade.quantity || 1;
104
+ const side = trade.side;
105
+
106
+ stats.totalVolume += Math.abs(size);
107
+
108
+ if (side === 0) {
109
+ stats.longTrades++;
110
+ if (pnl > 0) stats.longWins++;
111
+ } else if (side === 1) {
112
+ stats.shortTrades++;
113
+ if (pnl > 0) stats.shortWins++;
114
+ }
115
+
116
+ if (pnl > 0) {
117
+ stats.winningTrades++;
118
+ stats.totalWinAmount += pnl;
119
+ consecutiveWins++;
120
+ consecutiveLosses = 0;
121
+ if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
122
+ if (pnl > stats.bestTrade) stats.bestTrade = pnl;
123
+ } else if (pnl < 0) {
124
+ stats.losingTrades++;
125
+ stats.totalLossAmount += Math.abs(pnl);
126
+ consecutiveLosses++;
127
+ consecutiveWins = 0;
128
+ if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
129
+ if (pnl < stats.worstTrade) stats.worstTrade = pnl;
130
+ }
131
+ }
132
+ }
133
+
134
+ return stats;
135
+ };
136
+
137
+ /**
138
+ * Calculate derived metrics
139
+ * @param {Object} stats - Aggregated stats
140
+ * @param {number} totalStartingBalance - Starting balance
141
+ * @param {number} totalPnL - Total P&L
142
+ * @returns {Object} Derived metrics
143
+ */
144
+ const calculateDerivedMetrics = (stats, totalStartingBalance, totalPnL) => {
145
+ const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : 'N/A';
146
+ const avgWin = stats.winningTrades > 0 ? (stats.totalWinAmount / stats.winningTrades).toFixed(2) : '0.00';
147
+ const avgLoss = stats.losingTrades > 0 ? (stats.totalLossAmount / stats.losingTrades).toFixed(2) : '0.00';
148
+ const profitFactor = stats.totalLossAmount > 0
149
+ ? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2)
150
+ : (stats.totalWinAmount > 0 ? '∞' : 'N/A');
151
+ const netPnL = stats.totalWinAmount - stats.totalLossAmount;
152
+ const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : 'N/A';
153
+ const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : 'N/A';
154
+ const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : 'N/A';
155
+ const expectancy = stats.totalTrades > 0 ? netPnL / stats.totalTrades : 0;
156
+ const riskRewardRatio = parseFloat(avgLoss) > 0 ? (parseFloat(avgWin) / parseFloat(avgLoss)).toFixed(2) : 'N/A';
157
+
158
+ return {
159
+ winRate,
160
+ avgWin,
161
+ avgLoss,
162
+ profitFactor,
163
+ netPnL,
164
+ returnPercent,
165
+ longWinRate,
166
+ shortWinRate,
167
+ expectancy,
168
+ riskRewardRatio,
169
+ };
170
+ };
171
+
172
+ /**
173
+ * Calculate HQX score
174
+ * @param {Object} stats - Stats object
175
+ * @param {Object} metrics - Derived metrics
176
+ * @param {number} totalStartingBalance - Starting balance
177
+ * @returns {Object} Score data
178
+ */
179
+ const calculateHQXScore = (stats, metrics, totalStartingBalance) => {
180
+ const winRateNum = metrics.winRate !== 'N/A' ? parseFloat(metrics.winRate) : 0;
181
+ const winRateScore = Math.min(100, winRateNum * 1.5);
182
+ const profitFactorScore = metrics.profitFactor === '∞' ? 100 : metrics.profitFactor === 'N/A' ? 0 : Math.min(100, parseFloat(metrics.profitFactor) * 40);
183
+ const consistencyScore = stats.maxConsecutiveLosses > 0 ? Math.max(0, 100 - (stats.maxConsecutiveLosses * 15)) : 100;
184
+ const riskScore = stats.worstTrade !== 0 && totalStartingBalance > 0
185
+ ? Math.max(0, 100 - (Math.abs(stats.worstTrade) / totalStartingBalance * 1000))
186
+ : 50;
187
+ const volumeScore = Math.min(100, stats.totalTrades * 2);
188
+ const returnNum = metrics.returnPercent !== 'N/A' ? parseFloat(metrics.returnPercent) : 0;
189
+ const returnScore = Math.min(100, Math.max(0, returnNum * 10 + 50));
190
+
191
+ const hqxScore = Math.round((winRateScore + profitFactorScore + consistencyScore + riskScore + volumeScore + returnScore) / 6);
192
+ const scoreGrade = hqxScore >= 90 ? 'S' : hqxScore >= 80 ? 'A' : hqxScore >= 70 ? 'B' : hqxScore >= 60 ? 'C' : hqxScore >= 50 ? 'D' : 'F';
193
+
194
+ return {
195
+ hqxScore,
196
+ scoreGrade,
197
+ breakdown: [
198
+ { name: 'Win Rate', score: winRateScore },
199
+ { name: 'Profit Factor', score: profitFactorScore },
200
+ { name: 'Consistency', score: consistencyScore },
201
+ { name: 'Risk Management', score: riskScore },
202
+ { name: 'Volume', score: volumeScore },
203
+ { name: 'Returns', score: returnScore }
204
+ ]
205
+ };
206
+ };
207
+
208
+ module.exports = {
209
+ calculateQuantMetrics,
210
+ aggregateStats,
211
+ calculateDerivedMetrics,
212
+ calculateHQXScore,
213
+ };
package/src/pages/user.js CHANGED
@@ -26,7 +26,7 @@ const showUserInfo = async (service) => {
26
26
 
27
27
  try {
28
28
  // Step 1: Get user info
29
- spinner = ora({ text: 'LOADING USER INFO...', color: 'yellow' }).start();
29
+ spinner = ora({ text: 'Loading user info...', color: 'yellow' }).start();
30
30
 
31
31
  let userInfo = null;
32
32
 
@@ -39,10 +39,10 @@ const showUserInfo = async (service) => {
39
39
  } catch (e) {}
40
40
  }
41
41
 
42
- spinner.succeed('USER INFO LOADED');
42
+ spinner.succeed('User info loaded');
43
43
 
44
44
  // Step 2: Get account count
45
- spinner = ora({ text: 'COUNTING ACCOUNTS...', color: 'yellow' }).start();
45
+ spinner = ora({ text: 'Counting accounts...', color: 'yellow' }).start();
46
46
 
47
47
  let accountCount = 0;
48
48
 
@@ -71,13 +71,13 @@ const showUserInfo = async (service) => {
71
71
 
72
72
  const username = userInfo.userName || userInfo.username || 'Unknown';
73
73
  const connCount = connections.count() || 1;
74
- console.log(chalk.cyan('║') + fmtRow('USERNAME:', chalk.cyan(username.toUpperCase()), col1) + chalk.cyan('│') + fmtRow('CONNECTIONS:', chalk.cyan(String(connCount)), col2) + chalk.cyan('║'));
74
+ console.log(chalk.cyan('║') + fmtRow('Username:', chalk.cyan(username.toUpperCase()), col1) + chalk.cyan('│') + fmtRow('Connections:', chalk.cyan(String(connCount)), col2) + chalk.cyan('║'));
75
75
 
76
76
  const email = userInfo.email || 'N/A';
77
- console.log(chalk.cyan('║') + fmtRow('EMAIL:', chalk.white(email), col1) + chalk.cyan('│') + fmtRow('ACCOUNTS:', chalk.cyan(String(accountCount)), col2) + chalk.cyan('║'));
77
+ console.log(chalk.cyan('║') + fmtRow('Email:', chalk.white(email), col1) + chalk.cyan('│') + fmtRow('Accounts:', chalk.cyan(String(accountCount)), col2) + chalk.cyan('║'));
78
78
 
79
79
  const userId = userInfo.userId || userInfo.id || 'N/A';
80
- const platform = service?.propfirm?.name || 'ProjectX';
80
+ const platform = service?.propfirm?.name || 'Rithmic';
81
81
  console.log(chalk.cyan('║') + fmtRow('User ID:', chalk.gray(String(userId)), col1) + chalk.cyan('│') + fmtRow('Platform:', chalk.magenta(platform), col2) + chalk.cyan('║'));
82
82
 
83
83
  const firstName = userInfo.firstName || '';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview HQX Server Constants
3
+ * @module services/hqx-server/constants
4
+ */
5
+
6
+ /** Message types as bytes for faster switching */
7
+ const MSG_TYPE = {
8
+ // Outgoing
9
+ PING: 0x01,
10
+ START_ALGO: 0x10,
11
+ STOP_ALGO: 0x11,
12
+ START_COPY: 0x12,
13
+ ORDER: 0x20,
14
+
15
+ // Incoming
16
+ PONG: 0x81,
17
+ SIGNAL: 0x90,
18
+ TRADE: 0x91,
19
+ FILL: 0x92,
20
+ LOG: 0xA0,
21
+ STATS: 0xA1,
22
+ ERROR: 0xFF,
23
+ };
24
+
25
+ /** Pre-allocated ping buffer */
26
+ const PING_BUFFER = Buffer.alloc(9);
27
+ PING_BUFFER.writeUInt8(MSG_TYPE.PING, 0);
28
+
29
+ /**
30
+ * Fast JSON stringify with pre-check
31
+ * @param {Object} obj
32
+ * @returns {string}
33
+ */
34
+ const fastStringify = (obj) => {
35
+ if (obj === null) return 'null';
36
+ if (typeof obj !== 'object') return JSON.stringify(obj);
37
+ return JSON.stringify(obj);
38
+ };
39
+
40
+ /**
41
+ * Fast JSON parse with type hint
42
+ * @param {string|Buffer} data
43
+ * @returns {Object}
44
+ */
45
+ const fastParse = (data) => {
46
+ const str = typeof data === 'string' ? data : data.toString('utf8');
47
+ return JSON.parse(str);
48
+ };
49
+
50
+ module.exports = {
51
+ MSG_TYPE,
52
+ PING_BUFFER,
53
+ fastStringify,
54
+ fastParse,
55
+ };
@@ -0,0 +1,401 @@
1
+ /**
2
+ * @fileoverview HQX Server Service - Ultra Low Latency WebSocket
3
+ * @module services/hqx-server
4
+ *
5
+ * STRICT RULE: No mock data, real API only
6
+ */
7
+
8
+ const WebSocket = require('ws');
9
+ const crypto = require('crypto');
10
+ const os = require('os');
11
+ const { request } = require('../../utils/http');
12
+ const { HQX_SERVER, SECURITY } = require('../../config/settings');
13
+ const { logger } = require('../../utils/logger');
14
+ const { MSG_TYPE, PING_BUFFER, fastStringify, fastParse } = require('./constants');
15
+ const { LatencyTracker } = require('./latency');
16
+
17
+ const log = logger.scope('HQX');
18
+
19
+ /**
20
+ * HQX Server Service - Ultra Low Latency
21
+ */
22
+ class HQXServerService {
23
+ constructor() {
24
+ this.ws = null;
25
+ this.connected = false;
26
+ this.reconnecting = false;
27
+ this.reconnectAttempts = 0;
28
+
29
+ this.token = null;
30
+ this.refreshToken = null;
31
+ this.apiKey = null;
32
+ this.sessionId = null;
33
+
34
+ this.latencyTracker = new LatencyTracker();
35
+ this.lastPingTime = 0;
36
+ this.pingInterval = null;
37
+
38
+ this.listeners = new Map();
39
+ this.messageQueue = [];
40
+ this._deviceId = null;
41
+
42
+ this.messagesSent = 0;
43
+ this.messagesReceived = 0;
44
+ this.bytesReceived = 0;
45
+ }
46
+
47
+ // ==================== DEVICE ID ====================
48
+
49
+ _getDeviceId() {
50
+ if (this._deviceId) return this._deviceId;
51
+ const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'cpu'}`;
52
+ this._deviceId = crypto.createHash('sha256').update(data).digest('hex').slice(0, 32);
53
+ return this._deviceId;
54
+ }
55
+
56
+ // ==================== AUTH ====================
57
+
58
+ async authenticate(userId, propfirm = 'unknown') {
59
+ const start = process.hrtime.bigint();
60
+
61
+ try {
62
+ const deviceId = this._getDeviceId();
63
+ const url = `http://${HQX_SERVER.host}:${HQX_SERVER.port}/${HQX_SERVER.VERSION}/auth/token`;
64
+
65
+ const response = await request(url, {
66
+ method: 'POST',
67
+ body: { userId: userId || deviceId, deviceId, propfirm, timestamp: Date.now() },
68
+ timeout: 5000,
69
+ });
70
+
71
+ if (response.statusCode === 200 && response.data?.success) {
72
+ const { token, refreshToken, apiKey, sessionId } = response.data.data;
73
+ this.token = token;
74
+ this.refreshToken = refreshToken;
75
+ this.apiKey = apiKey;
76
+ this.sessionId = sessionId;
77
+
78
+ const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
79
+ log.info('Authenticated', { sessionId, latency: `${elapsed.toFixed(1)}ms` });
80
+
81
+ return { success: true, sessionId, apiKey };
82
+ }
83
+
84
+ return { success: false, error: response.data?.error || 'Authentication failed' };
85
+ } catch (err) {
86
+ log.error('Auth error', { error: err.message });
87
+ return { success: false, error: err.message };
88
+ }
89
+ }
90
+
91
+ // ==================== WEBSOCKET ====================
92
+
93
+ async connect() {
94
+ if (!this.token) return { success: false, error: 'Not authenticated' };
95
+
96
+ return new Promise((resolve) => {
97
+ const wsUrl = `${HQX_SERVER.wsUrl}?token=${this.token}&session=${this.sessionId}`;
98
+
99
+ this.ws = new WebSocket(wsUrl, {
100
+ headers: { 'X-Device-Id': this._getDeviceId(), 'X-API-Key': this.apiKey },
101
+ perMessageDeflate: false,
102
+ maxPayload: 64 * 1024,
103
+ handshakeTimeout: 5000,
104
+ });
105
+
106
+ this.ws.binaryType = 'nodebuffer';
107
+
108
+ const connectTimeout = setTimeout(() => {
109
+ if (!this.connected) {
110
+ this.ws?.terminate();
111
+ resolve({ success: false, error: 'Connection timeout' });
112
+ }
113
+ }, 5000);
114
+
115
+ this.ws.on('open', () => {
116
+ clearTimeout(connectTimeout);
117
+ this.connected = true;
118
+ this.reconnectAttempts = 0;
119
+ this._optimizeSocket();
120
+ this._startHeartbeat();
121
+ this._flushQueue();
122
+ this._emit('connected', { sessionId: this.sessionId });
123
+ log.info('Connected with TCP_NODELAY');
124
+ resolve({ success: true });
125
+ });
126
+
127
+ this.ws.on('message', (data) => this._handleMessage(data));
128
+
129
+ this.ws.on('close', (code, reason) => {
130
+ clearTimeout(connectTimeout);
131
+ this.connected = false;
132
+ this._stopHeartbeat();
133
+ log.info('Disconnected', { code });
134
+ this._emit('disconnected', { code, reason: reason?.toString() });
135
+ if (!this.reconnecting) this._attemptReconnect();
136
+ });
137
+
138
+ this.ws.on('error', (err) => {
139
+ log.error('WebSocket error', { error: err.message });
140
+ this._emit('error', { message: err.message });
141
+ if (!this.connected) {
142
+ clearTimeout(connectTimeout);
143
+ resolve({ success: false, error: err.message });
144
+ }
145
+ });
146
+ });
147
+ }
148
+
149
+ _optimizeSocket() {
150
+ try {
151
+ const socket = this.ws._socket;
152
+ if (socket) {
153
+ socket.setNoDelay(true);
154
+ socket.setKeepAlive(true, 10000);
155
+ }
156
+ } catch (err) {
157
+ log.warn('Socket optimization failed', { error: err.message });
158
+ }
159
+ }
160
+
161
+ // ==================== MESSAGE HANDLING ====================
162
+
163
+ _handleMessage(data) {
164
+ const receiveTime = process.hrtime.bigint();
165
+ this.messagesReceived++;
166
+ this.bytesReceived += data.length;
167
+
168
+ try {
169
+ if (Buffer.isBuffer(data) && data.length > 0) {
170
+ const msgType = data.readUInt8(0);
171
+
172
+ if (msgType === MSG_TYPE.PONG) {
173
+ this._handlePong(receiveTime);
174
+ return;
175
+ }
176
+
177
+ if (msgType === MSG_TYPE.SIGNAL) {
178
+ this._handleBinarySignal(data);
179
+ return;
180
+ }
181
+ }
182
+
183
+ const message = fastParse(data);
184
+ this._handleJsonMessage(message);
185
+ } catch (err) {
186
+ log.warn('Message parse error', { error: err.message });
187
+ }
188
+ }
189
+
190
+ _handlePong(receiveTime) {
191
+ if (this.lastPingTime > 0) {
192
+ const latency = Number(receiveTime - this.lastPingTime) / 1e6;
193
+ this.latencyTracker.update(latency);
194
+ this._emit('latency', this.latencyTracker.getStats());
195
+ }
196
+ }
197
+
198
+ _handleBinarySignal(data) {
199
+ if (data.length >= 22) {
200
+ const signal = {
201
+ timestamp: data.readBigInt64LE(1),
202
+ side: data.readUInt8(9),
203
+ price: data.readDoubleLE(10),
204
+ quantity: data.readUInt32LE(18),
205
+ };
206
+ this._emit('signal', signal);
207
+ }
208
+ }
209
+
210
+ _handleJsonMessage(message) {
211
+ if (message.timestamp) {
212
+ const latency = Date.now() - message.timestamp;
213
+ if (latency >= 0 && latency < 5000) {
214
+ this.latencyTracker.update(latency);
215
+ }
216
+ }
217
+
218
+ switch (message.type) {
219
+ case 'signal': this._emit('signal', message.data); break;
220
+ case 'trade': this._emit('trade', message.data); break;
221
+ case 'fill': this._emit('fill', message.data); break;
222
+ case 'log': this._emit('log', message.data); break;
223
+ case 'stats': this._emit('stats', message.data); break;
224
+ case 'error': this._emit('error', message.data); break;
225
+ default: this._emit('message', message);
226
+ }
227
+ }
228
+
229
+ // ==================== SENDING ====================
230
+
231
+ send(type, data) {
232
+ const message = { type, data, ts: Date.now(), sid: this.sessionId };
233
+
234
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
235
+ try { this.ws.send(fastStringify(message)); } catch {}
236
+ this.messagesSent++;
237
+ } else {
238
+ this.messageQueue.push(message);
239
+ }
240
+ }
241
+
242
+ _sendBinaryPing() {
243
+ if (!this.connected || this.ws?.readyState !== WebSocket.OPEN) return;
244
+ this.lastPingTime = process.hrtime.bigint();
245
+ PING_BUFFER.writeBigInt64LE(this.lastPingTime, 1);
246
+ try { this.ws.send(PING_BUFFER); } catch {}
247
+ }
248
+
249
+ _flushQueue() {
250
+ while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
251
+ const message = this.messageQueue.shift();
252
+ try { this.ws.send(fastStringify(message)); } catch {}
253
+ this.messagesSent++;
254
+ }
255
+ }
256
+
257
+ // ==================== ALGO CONTROL ====================
258
+
259
+ startAlgo(config) {
260
+ log.info('Starting algo', { symbol: config.symbol, contracts: config.contracts });
261
+ this.send('start_algo', {
262
+ accountId: config.accountId,
263
+ contractId: config.contractId,
264
+ symbol: config.symbol,
265
+ contracts: config.contracts,
266
+ dailyTarget: config.dailyTarget,
267
+ maxRisk: config.maxRisk,
268
+ propfirm: config.propfirm,
269
+ propfirmToken: config.propfirmToken,
270
+ rithmicCredentials: config.rithmicCredentials || null,
271
+ copyTrading: config.copyTrading || false,
272
+ followerSymbol: config.followerSymbol,
273
+ followerContracts: config.followerContracts,
274
+ });
275
+ }
276
+
277
+ stopAlgo() {
278
+ log.info('Stopping algo');
279
+ this.send('stop_algo', {});
280
+ }
281
+
282
+ startCopyTrading(config) {
283
+ log.info('Starting copy trading');
284
+ this.send('start_copy_trading', {
285
+ leadAccountId: config.leadAccountId,
286
+ leadContractId: config.leadContractId,
287
+ leadSymbol: config.leadSymbol,
288
+ leadContracts: config.leadContracts,
289
+ leadPropfirm: config.leadPropfirm,
290
+ leadToken: config.leadToken,
291
+ leadRithmicCredentials: config.leadRithmicCredentials,
292
+ followerAccountId: config.followerAccountId,
293
+ followerContractId: config.followerContractId,
294
+ followerSymbol: config.followerSymbol,
295
+ followerContracts: config.followerContracts,
296
+ followerPropfirm: config.followerPropfirm,
297
+ followerToken: config.followerToken,
298
+ followerRithmicCredentials: config.followerRithmicCredentials,
299
+ dailyTarget: config.dailyTarget,
300
+ maxRisk: config.maxRisk,
301
+ });
302
+ }
303
+
304
+ // ==================== EVENTS ====================
305
+
306
+ on(event, callback) {
307
+ if (!this.listeners.has(event)) this.listeners.set(event, []);
308
+ this.listeners.get(event).push(callback);
309
+ }
310
+
311
+ off(event, callback) {
312
+ const callbacks = this.listeners.get(event);
313
+ if (callbacks) {
314
+ const index = callbacks.indexOf(callback);
315
+ if (index > -1) callbacks.splice(index, 1);
316
+ }
317
+ }
318
+
319
+ _emit(event, data) {
320
+ const callbacks = this.listeners.get(event);
321
+ if (!callbacks) return;
322
+ for (let i = 0; i < callbacks.length; i++) {
323
+ try { callbacks[i](data); } catch {}
324
+ }
325
+ }
326
+
327
+ // ==================== HEARTBEAT ====================
328
+
329
+ _startHeartbeat() {
330
+ this._stopHeartbeat();
331
+ const heartbeat = () => {
332
+ if (this.connected) {
333
+ this._sendBinaryPing();
334
+ this.pingInterval = setTimeout(heartbeat, this.latencyTracker.adaptiveHeartbeat);
335
+ }
336
+ };
337
+ this._sendBinaryPing();
338
+ this.pingInterval = setTimeout(heartbeat, this.latencyTracker.adaptiveHeartbeat);
339
+ }
340
+
341
+ _stopHeartbeat() {
342
+ if (this.pingInterval) {
343
+ clearTimeout(this.pingInterval);
344
+ this.pingInterval = null;
345
+ }
346
+ }
347
+
348
+ // ==================== RECONNECT ====================
349
+
350
+ _attemptReconnect() {
351
+ if (this.reconnectAttempts >= SECURITY.MAX_RECONNECT_ATTEMPTS) {
352
+ log.error('Max reconnect attempts reached');
353
+ return;
354
+ }
355
+
356
+ this.reconnecting = true;
357
+ this.reconnectAttempts++;
358
+ const delay = Math.min(100 * Math.pow(2, this.reconnectAttempts - 1), 10000);
359
+
360
+ log.info('Reconnecting', { attempt: this.reconnectAttempts, delay });
361
+
362
+ setTimeout(async () => {
363
+ try { await this.connect(); } catch {}
364
+ this.reconnecting = false;
365
+ }, delay);
366
+ }
367
+
368
+ // ==================== STATS & CLEANUP ====================
369
+
370
+ getLatencyStats() { return this.latencyTracker.getStats(); }
371
+ getLatency() { return this.latencyTracker.latency; }
372
+ isConnected() { return this.connected && this.ws?.readyState === WebSocket.OPEN; }
373
+
374
+ getStats() {
375
+ return {
376
+ connected: this.connected,
377
+ messagesSent: this.messagesSent,
378
+ messagesReceived: this.messagesReceived,
379
+ bytesReceived: this.bytesReceived,
380
+ heartbeatInterval: this.latencyTracker.adaptiveHeartbeat,
381
+ latency: this.getLatencyStats(),
382
+ };
383
+ }
384
+
385
+ disconnect() {
386
+ log.info('Disconnecting');
387
+ this._stopHeartbeat();
388
+ if (this.ws) {
389
+ this.ws.close(1000, 'Client disconnect');
390
+ this.ws = null;
391
+ }
392
+ this.connected = false;
393
+ this.token = null;
394
+ this.sessionId = null;
395
+ this.messageQueue = [];
396
+ this.listeners.clear();
397
+ this.latencyTracker.reset();
398
+ }
399
+ }
400
+
401
+ module.exports = { HQXServerService, HQX_SERVER, MSG_TYPE };