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.
- package/README.md +15 -88
- package/bin/cli.js +0 -11
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +1 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +6 -3
- package/src/app.js +40 -162
- package/src/config/constants.js +31 -33
- package/src/config/propfirms.js +13 -217
- package/src/config/settings.js +0 -43
- package/src/lib/api.js +198 -0
- package/src/lib/api2.js +353 -0
- package/src/lib/core.js +539 -0
- package/src/lib/core2.js +341 -0
- package/src/lib/data.js +555 -0
- package/src/lib/data2.js +492 -0
- package/src/lib/decoder.js +599 -0
- package/src/lib/m/s1.js +804 -0
- package/src/lib/m/s2.js +34 -0
- package/src/lib/n/r1.js +454 -0
- package/src/lib/n/r2.js +514 -0
- package/src/lib/n/r3.js +631 -0
- package/src/lib/n/r4.js +401 -0
- package/src/lib/n/r5.js +335 -0
- package/src/lib/n/r6.js +425 -0
- package/src/lib/n/r7.js +530 -0
- package/src/lib/o/l1.js +44 -0
- package/src/lib/o/l2.js +427 -0
- package/src/lib/python-bridge.js +206 -0
- package/src/menus/connect.js +14 -176
- package/src/menus/dashboard.js +65 -110
- package/src/pages/accounts.js +18 -18
- package/src/pages/algo/copy-trading.js +210 -240
- package/src/pages/algo/index.js +41 -104
- package/src/pages/algo/one-account.js +386 -33
- package/src/pages/algo/ui.js +312 -151
- package/src/pages/orders.js +3 -3
- package/src/pages/positions.js +3 -3
- package/src/pages/stats/chart.js +74 -0
- package/src/pages/stats/display.js +228 -0
- package/src/pages/stats/index.js +236 -0
- package/src/pages/stats/metrics.js +213 -0
- package/src/pages/user.js +6 -6
- package/src/services/hqx-server/constants.js +55 -0
- package/src/services/hqx-server/index.js +401 -0
- package/src/services/hqx-server/latency.js +81 -0
- package/src/services/index.js +12 -3
- package/src/services/rithmic/accounts.js +7 -32
- package/src/services/rithmic/connection.js +1 -204
- package/src/services/rithmic/contracts.js +116 -99
- package/src/services/rithmic/handlers.js +21 -196
- package/src/services/rithmic/index.js +63 -120
- package/src/services/rithmic/market.js +31 -0
- package/src/services/rithmic/orders.js +5 -111
- package/src/services/rithmic/protobuf.js +384 -138
- package/src/services/session.js +22 -173
- package/src/ui/box.js +10 -18
- package/src/ui/index.js +1 -3
- package/src/ui/menu.js +1 -1
- package/src/utils/prompts.js +2 -2
- package/dist/lib/m/s1.js +0 -1
- package/src/menus/ai-agent-connect.js +0 -181
- package/src/menus/ai-agent-models.js +0 -219
- package/src/menus/ai-agent-oauth.js +0 -292
- package/src/menus/ai-agent-ui.js +0 -141
- package/src/menus/ai-agent.js +0 -484
- package/src/pages/algo/algo-config.js +0 -195
- package/src/pages/algo/algo-multi.js +0 -801
- package/src/pages/algo/algo-utils.js +0 -58
- package/src/pages/algo/copy-engine.js +0 -449
- package/src/pages/algo/custom-strategy.js +0 -459
- package/src/pages/algo/logger.js +0 -245
- package/src/pages/algo/smart-logs-data.js +0 -218
- package/src/pages/algo/smart-logs.js +0 -387
- package/src/pages/algo/ui-constants.js +0 -144
- package/src/pages/algo/ui-summary.js +0 -184
- package/src/pages/stats-calculations.js +0 -191
- package/src/pages/stats-ui.js +0 -381
- package/src/pages/stats.js +0 -339
- package/src/services/ai/client-analysis.js +0 -194
- package/src/services/ai/client-models.js +0 -333
- package/src/services/ai/client.js +0 -343
- package/src/services/ai/index.js +0 -384
- package/src/services/ai/oauth-anthropic.js +0 -265
- package/src/services/ai/oauth-gemini.js +0 -223
- package/src/services/ai/oauth-iflow.js +0 -269
- package/src/services/ai/oauth-openai.js +0 -233
- package/src/services/ai/oauth-qwen.js +0 -279
- package/src/services/ai/providers/direct-providers.js +0 -323
- package/src/services/ai/providers/index.js +0 -62
- package/src/services/ai/providers/other-providers.js +0 -104
- package/src/services/ai/proxy-install.js +0 -249
- package/src/services/ai/proxy-manager.js +0 -494
- package/src/services/ai/proxy-remote.js +0 -161
- package/src/services/ai/strategy-supervisor.js +0 -1312
- package/src/services/ai/supervisor-data.js +0 -195
- package/src/services/ai/supervisor-optimize.js +0 -215
- package/src/services/ai/supervisor-sync.js +0 -178
- package/src/services/ai/supervisor-utils.js +0 -158
- package/src/services/ai/supervisor.js +0 -484
- package/src/services/ai/validation.js +0 -250
- package/src/services/hqx-server-events.js +0 -110
- package/src/services/hqx-server-handlers.js +0 -217
- package/src/services/hqx-server-latency.js +0 -136
- package/src/services/hqx-server.js +0 -403
- package/src/services/position-constants.js +0 -28
- package/src/services/position-exit-logic.js +0 -174
- package/src/services/position-manager.js +0 -438
- package/src/services/position-momentum.js +0 -206
- package/src/services/projectx/accounts.js +0 -142
- package/src/services/projectx/index.js +0 -443
- package/src/services/projectx/market.js +0 -172
- package/src/services/projectx/stats.js +0 -110
- package/src/services/projectx/trading.js +0 -180
- package/src/services/rithmic/latency-tracker.js +0 -182
- package/src/services/rithmic/market-data-decoders.js +0 -229
- package/src/services/rithmic/market-data.js +0 -272
- package/src/services/rithmic/orders-fast.js +0 -246
- package/src/services/rithmic/proto-decoders.js +0 -403
- package/src/services/rithmic/specs.js +0 -146
- package/src/services/rithmic/trade-history.js +0 -254
- package/src/services/session-history.js +0 -475
- package/src/services/strategy/hft-signal-calc.js +0 -147
- package/src/services/strategy/hft-tick.js +0 -407
- package/src/services/strategy/recovery-math.js +0 -402
- package/src/services/tradovate/constants.js +0 -109
- package/src/services/tradovate/index.js +0 -392
- package/src/services/tradovate/market.js +0 -47
- package/src/services/tradovate/orders.js +0 -145
- 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: '
|
|
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('
|
|
42
|
+
spinner.succeed('User info loaded');
|
|
43
43
|
|
|
44
44
|
// Step 2: Get account count
|
|
45
|
-
spinner = ora({ text: '
|
|
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('
|
|
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('
|
|
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 || '
|
|
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 };
|