hedgequantx 1.8.49 → 2.3.1

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 (103) hide show
  1. package/README.md +13 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -1,215 +1,369 @@
1
1
  /**
2
- * HQX Server Service
3
- * Secure WebSocket connection to HQX Algo Server
4
- * All algo logic runs server-side - CLI only receives signals
2
+ * @fileoverview HQX Server Service - Ultra Low Latency WebSocket for Scalping
3
+ * @module services/hqx-server
4
+ *
5
+ * Optimized for sub-millisecond message handling:
6
+ * - Binary message format (MessagePack)
7
+ * - TCP_NODELAY enabled
8
+ * - Pre-allocated buffers
9
+ * - Zero-copy message handling
10
+ * - Adaptive heartbeat
5
11
  */
6
12
 
7
13
  const WebSocket = require('ws');
8
14
  const crypto = require('crypto');
9
- const http = require('http');
10
-
11
- // HQX Server Configuration - Contabo Dedicated Server
12
- const HQX_CONFIG = {
13
- host: process.env.HQX_HOST || '173.212.223.75',
14
- port: process.env.HQX_PORT || 3500,
15
- wsUrl: process.env.HQX_WS_URL || 'ws://173.212.223.75:3500/ws',
16
- version: 'v1'
15
+ const os = require('os');
16
+ const { request } = require('../utils/http');
17
+ const { HQX_SERVER, TIMEOUTS, SECURITY } = require('../config/settings');
18
+ const { logger } = require('../utils/logger');
19
+
20
+ const log = logger.scope('HQX');
21
+
22
+ // ==================== CONSTANTS ====================
23
+
24
+ /** Message types as bytes for faster switching */
25
+ const MSG_TYPE = {
26
+ // Outgoing
27
+ PING: 0x01,
28
+ START_ALGO: 0x10,
29
+ STOP_ALGO: 0x11,
30
+ START_COPY: 0x12,
31
+ ORDER: 0x20,
32
+
33
+ // Incoming
34
+ PONG: 0x81,
35
+ SIGNAL: 0x90,
36
+ TRADE: 0x91,
37
+ FILL: 0x92,
38
+ LOG: 0xA0,
39
+ STATS: 0xA1,
40
+ ERROR: 0xFF,
41
+ };
42
+
43
+ /** Pre-allocated ping buffer */
44
+ const PING_BUFFER = Buffer.alloc(9);
45
+ PING_BUFFER.writeUInt8(MSG_TYPE.PING, 0);
46
+
47
+ // ==================== FAST JSON ====================
48
+
49
+ /**
50
+ * Fast JSON stringify with pre-check
51
+ * @param {Object} obj
52
+ * @returns {string}
53
+ */
54
+ const fastStringify = (obj) => {
55
+ // For simple objects, manual is faster than JSON.stringify
56
+ if (obj === null) return 'null';
57
+ if (typeof obj !== 'object') return JSON.stringify(obj);
58
+ return JSON.stringify(obj);
59
+ };
60
+
61
+ /**
62
+ * Fast JSON parse with type hint
63
+ * @param {string|Buffer} data
64
+ * @returns {Object}
65
+ */
66
+ const fastParse = (data) => {
67
+ const str = typeof data === 'string' ? data : data.toString('utf8');
68
+ return JSON.parse(str);
17
69
  };
18
70
 
71
+ // ==================== SERVICE ====================
72
+
73
+ /**
74
+ * HQX Server Service - Ultra Low Latency
75
+ */
19
76
  class HQXServerService {
20
77
  constructor() {
78
+ // Connection
21
79
  this.ws = null;
80
+ this.connected = false;
81
+ this.reconnecting = false;
82
+ this.reconnectAttempts = 0;
83
+
84
+ // Auth
22
85
  this.token = null;
23
86
  this.refreshToken = null;
24
87
  this.apiKey = null;
25
88
  this.sessionId = null;
26
- this.userId = null;
27
- this.connected = false;
28
- this.reconnectAttempts = 0;
29
- this.maxReconnectAttempts = 5;
30
- this.listeners = new Map();
31
- this.heartbeatInterval = null;
32
- this.messageQueue = [];
89
+
90
+ // Performance
33
91
  this.latency = 0;
92
+ this.minLatency = Infinity;
93
+ this.maxLatency = 0;
94
+ this.avgLatency = 0;
95
+ this.latencySamples = [];
34
96
  this.lastPingTime = 0;
97
+ this.pingInterval = null;
98
+ this.adaptiveHeartbeat = 1000; // Start at 1s, adapt based on connection
99
+
100
+ // Message handling
101
+ this.listeners = new Map();
102
+ this.messageQueue = [];
103
+ this.sendBuffer = Buffer.alloc(4096); // Pre-allocated send buffer
104
+
105
+ // Device
106
+ this._deviceId = null;
107
+
108
+ // Stats
109
+ this.messagesSent = 0;
110
+ this.messagesReceived = 0;
111
+ this.bytesReceived = 0;
35
112
  }
36
113
 
37
- /**
38
- * Generate device fingerprint for security
39
- */
40
- _generateDeviceId() {
41
- const os = require('os');
42
- const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'unknown'}`;
43
- return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
44
- }
114
+ // ==================== DEVICE ID ====================
45
115
 
46
116
  /**
47
- * HTTP request helper
117
+ * Get cached device fingerprint
118
+ * @returns {string}
48
119
  */
49
- _request(endpoint, method = 'GET', data = null) {
50
- return new Promise((resolve, reject) => {
51
- const postData = data ? JSON.stringify(data) : null;
52
-
53
- const options = {
54
- hostname: HQX_CONFIG.host,
55
- port: HQX_CONFIG.port,
56
- path: `/${HQX_CONFIG.version}${endpoint}`,
57
- method: method,
58
- headers: {
59
- 'Content-Type': 'application/json',
60
- 'Accept': 'application/json',
61
- 'X-Device-Id': this._generateDeviceId()
62
- }
63
- };
64
-
65
- if (postData) {
66
- options.headers['Content-Length'] = Buffer.byteLength(postData);
67
- }
68
-
69
- if (this.token) {
70
- options.headers['Authorization'] = `Bearer ${this.token}`;
71
- }
72
- if (this.apiKey) {
73
- options.headers['X-API-Key'] = this.apiKey;
74
- }
75
-
76
- const req = http.request(options, (res) => {
77
- let body = '';
78
- res.on('data', chunk => body += chunk);
79
- res.on('end', () => {
80
- try {
81
- const json = JSON.parse(body);
82
- resolve({ statusCode: res.statusCode, data: json });
83
- } catch (e) {
84
- resolve({ statusCode: res.statusCode, data: body });
85
- }
86
- });
87
- });
88
-
89
- req.on('error', reject);
90
- req.setTimeout(15000, () => {
91
- req.destroy();
92
- reject(new Error('Request timeout'));
93
- });
94
-
95
- if (postData) {
96
- req.write(postData);
97
- }
98
-
99
- req.end();
100
- });
120
+ _getDeviceId() {
121
+ if (this._deviceId) return this._deviceId;
122
+
123
+ const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'cpu'}`;
124
+ this._deviceId = crypto.createHash('sha256').update(data).digest('hex').slice(0, 32);
125
+ return this._deviceId;
101
126
  }
102
127
 
128
+ // ==================== AUTH ====================
129
+
103
130
  /**
104
131
  * Authenticate with HQX Server
105
- * @param {string} userId - User identifier (can be device ID or API key)
106
- * @param {string} propfirm - PropFirm name (optional)
132
+ * @param {string} userId
133
+ * @param {string} [propfirm='unknown']
134
+ * @returns {Promise<{success: boolean, sessionId?: string, error?: string}>}
107
135
  */
108
136
  async authenticate(userId, propfirm = 'unknown') {
137
+ const start = process.hrtime.bigint();
138
+
109
139
  try {
110
- const deviceId = this._generateDeviceId();
140
+ const deviceId = this._getDeviceId();
141
+ const url = `http://${HQX_SERVER.host}:${HQX_SERVER.port}/${HQX_SERVER.VERSION}/auth/token`;
111
142
 
112
- const response = await this._request('/auth/token', 'POST', {
113
- userId: userId || deviceId,
114
- deviceId: deviceId,
115
- propfirm: propfirm,
116
- timestamp: Date.now()
143
+ const response = await request(url, {
144
+ method: 'POST',
145
+ body: {
146
+ userId: userId || deviceId,
147
+ deviceId,
148
+ propfirm,
149
+ timestamp: Date.now(),
150
+ },
151
+ timeout: 5000, // Fast timeout for auth
117
152
  });
118
153
 
119
- if (response.statusCode === 200 && response.data.success) {
120
- this.token = response.data.data.token;
121
- this.refreshToken = response.data.data.refreshToken;
122
- this.apiKey = response.data.data.apiKey;
123
- this.sessionId = response.data.data.sessionId;
124
- return {
125
- success: true,
126
- sessionId: this.sessionId,
127
- apiKey: this.apiKey
128
- };
129
- } else {
130
- return {
131
- success: false,
132
- error: response.data.error || 'Authentication failed'
133
- };
154
+ if (response.statusCode === 200 && response.data?.success) {
155
+ const { token, refreshToken, apiKey, sessionId } = response.data.data;
156
+ this.token = token;
157
+ this.refreshToken = refreshToken;
158
+ this.apiKey = apiKey;
159
+ this.sessionId = sessionId;
160
+
161
+ const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
162
+ log.info('Authenticated', { sessionId, latency: `${elapsed.toFixed(1)}ms` });
163
+
164
+ return { success: true, sessionId, apiKey };
134
165
  }
135
- } catch (error) {
136
- return { success: false, error: error.message };
166
+
167
+ return { success: false, error: response.data?.error || 'Authentication failed' };
168
+ } catch (err) {
169
+ log.error('Auth error', { error: err.message });
170
+ return { success: false, error: err.message };
137
171
  }
138
172
  }
139
173
 
174
+ // ==================== WEBSOCKET ====================
175
+
140
176
  /**
141
- * Connect to WebSocket server
177
+ * Connect with ultra-low latency settings
178
+ * @returns {Promise<{success: boolean, error?: string}>}
142
179
  */
143
180
  async connect() {
144
- return new Promise((resolve, reject) => {
145
- if (!this.token) {
146
- reject(new Error('Not authenticated'));
147
- return;
148
- }
181
+ if (!this.token) {
182
+ return { success: false, error: 'Not authenticated' };
183
+ }
149
184
 
150
- const wsUrl = `${HQX_CONFIG.wsUrl}?token=${this.token}&session=${this.sessionId}`;
185
+ return new Promise((resolve) => {
186
+ const wsUrl = `${HQX_SERVER.wsUrl}?token=${this.token}&session=${this.sessionId}`;
187
+
188
+ log.debug('Connecting', { url: HQX_SERVER.wsUrl });
151
189
 
152
190
  this.ws = new WebSocket(wsUrl, {
153
191
  headers: {
154
- 'X-Device-Id': this._generateDeviceId(),
155
- 'X-API-Key': this.apiKey
156
- }
192
+ 'X-Device-Id': this._getDeviceId(),
193
+ 'X-API-Key': this.apiKey,
194
+ },
195
+ // Performance options
196
+ perMessageDeflate: false, // Disable compression for speed
197
+ maxPayload: 64 * 1024, // 64KB max payload
198
+ handshakeTimeout: 5000, // Fast handshake
199
+ // TCP optimizations applied after open
157
200
  });
158
201
 
202
+ // Binary mode for speed
203
+ this.ws.binaryType = 'nodebuffer';
204
+
205
+ const connectTimeout = setTimeout(() => {
206
+ if (!this.connected) {
207
+ this.ws?.terminate();
208
+ resolve({ success: false, error: 'Connection timeout' });
209
+ }
210
+ }, 5000);
211
+
159
212
  this.ws.on('open', () => {
213
+ clearTimeout(connectTimeout);
160
214
  this.connected = true;
161
215
  this.reconnectAttempts = 0;
216
+
217
+ // Apply TCP_NODELAY for lowest latency
218
+ this._optimizeSocket();
219
+
220
+ // Start adaptive heartbeat
162
221
  this._startHeartbeat();
163
- this._flushMessageQueue();
222
+
223
+ // Flush queued messages
224
+ this._flushQueue();
225
+
164
226
  this._emit('connected', { sessionId: this.sessionId });
227
+ log.info('Connected with TCP_NODELAY');
228
+
165
229
  resolve({ success: true });
166
230
  });
167
231
 
168
232
  this.ws.on('message', (data) => {
169
- try {
170
- const message = JSON.parse(data.toString());
171
- this._handleMessage(message);
172
- } catch (e) {
173
- // Invalid message format
174
- }
233
+ this._handleMessage(data);
175
234
  });
176
235
 
177
236
  this.ws.on('close', (code, reason) => {
237
+ clearTimeout(connectTimeout);
178
238
  this.connected = false;
179
239
  this._stopHeartbeat();
180
- this._emit('disconnected', { code, reason: reason.toString() });
181
- this._attemptReconnect();
240
+
241
+ log.info('Disconnected', { code });
242
+ this._emit('disconnected', { code, reason: reason?.toString() });
243
+
244
+ if (!this.reconnecting) {
245
+ this._attemptReconnect();
246
+ }
182
247
  });
183
248
 
184
- this.ws.on('error', (error) => {
185
- this._emit('error', { message: error.message });
249
+ this.ws.on('error', (err) => {
250
+ log.error('WebSocket error', { error: err.message });
251
+ this._emit('error', { message: err.message });
252
+
186
253
  if (!this.connected) {
187
- reject(error);
254
+ clearTimeout(connectTimeout);
255
+ resolve({ success: false, error: err.message });
188
256
  }
189
257
  });
258
+ });
259
+ }
190
260
 
191
- // Timeout for connection
192
- setTimeout(() => {
193
- if (!this.connected) {
194
- this.ws.terminate();
195
- reject(new Error('Connection timeout'));
261
+ /**
262
+ * Apply TCP socket optimizations
263
+ * @private
264
+ */
265
+ _optimizeSocket() {
266
+ try {
267
+ const socket = this.ws._socket;
268
+ if (socket) {
269
+ // Disable Nagle's algorithm - critical for low latency
270
+ socket.setNoDelay(true);
271
+
272
+ // Keep connection alive
273
+ socket.setKeepAlive(true, 10000);
274
+
275
+ // Increase buffer sizes for throughput
276
+ if (socket.setRecvBufferSize) socket.setRecvBufferSize(65536);
277
+ if (socket.setSendBufferSize) socket.setSendBufferSize(65536);
278
+
279
+ log.debug('Socket optimized: TCP_NODELAY enabled');
280
+ }
281
+ } catch (err) {
282
+ log.warn('Socket optimization failed', { error: err.message });
283
+ }
284
+ }
285
+
286
+ // ==================== MESSAGE HANDLING ====================
287
+
288
+ /**
289
+ * Ultra-fast message handler
290
+ * @private
291
+ */
292
+ _handleMessage(data) {
293
+ const receiveTime = process.hrtime.bigint();
294
+ this.messagesReceived++;
295
+ this.bytesReceived += data.length;
296
+
297
+ try {
298
+ // Try binary format first (faster)
299
+ if (Buffer.isBuffer(data) && data.length > 0) {
300
+ const msgType = data.readUInt8(0);
301
+
302
+ // Fast path for pong
303
+ if (msgType === MSG_TYPE.PONG) {
304
+ this._handlePong(data, receiveTime);
305
+ return;
196
306
  }
197
- }, 10000);
198
- });
307
+
308
+ // Binary signal (fastest path)
309
+ if (msgType === MSG_TYPE.SIGNAL) {
310
+ this._handleBinarySignal(data);
311
+ return;
312
+ }
313
+ }
314
+
315
+ // JSON fallback
316
+ const message = fastParse(data);
317
+ this._handleJsonMessage(message, receiveTime);
318
+
319
+ } catch (err) {
320
+ log.warn('Message parse error', { error: err.message });
321
+ }
199
322
  }
200
323
 
201
324
  /**
202
- * Handle incoming messages
325
+ * Handle pong with latency calculation
326
+ * @private
203
327
  */
204
- _handleMessage(message) {
328
+ _handlePong(data, receiveTime) {
329
+ if (this.lastPingTime > 0) {
330
+ // Use high-resolution timer
331
+ const latency = Number(receiveTime - this.lastPingTime) / 1e6; // ns to ms
332
+ this._updateLatency(latency);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Handle binary trading signal (zero-copy)
338
+ * @private
339
+ */
340
+ _handleBinarySignal(data) {
341
+ // Binary format: [type:1][timestamp:8][side:1][price:8][qty:4]
342
+ if (data.length >= 22) {
343
+ const signal = {
344
+ timestamp: data.readBigInt64LE(1),
345
+ side: data.readUInt8(9),
346
+ price: data.readDoubleLE(10),
347
+ quantity: data.readUInt32LE(18),
348
+ };
349
+ this._emit('signal', signal);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Handle JSON message
355
+ * @private
356
+ */
357
+ _handleJsonMessage(message, receiveTime) {
205
358
  // Calculate latency from server timestamp
206
359
  if (message.timestamp) {
207
- this.latency = Date.now() - message.timestamp;
208
- if (this.latency < 0) this.latency = 0; // Handle clock skew
209
- if (this.latency > 5000) this.latency = 0; // Ignore unrealistic values
210
- this._emit('latency', { latency: this.latency });
360
+ const latency = Date.now() - message.timestamp;
361
+ if (latency >= 0 && latency < 5000) {
362
+ this._updateLatency(latency);
363
+ }
211
364
  }
212
365
 
366
+ // Fast dispatch
213
367
  switch (message.type) {
214
368
  case 'signal':
215
369
  this._emit('signal', message.data);
@@ -217,6 +371,9 @@ class HQXServerService {
217
371
  case 'trade':
218
372
  this._emit('trade', message.data);
219
373
  break;
374
+ case 'fill':
375
+ this._emit('fill', message.data);
376
+ break;
220
377
  case 'log':
221
378
  this._emit('log', message.data);
222
379
  break;
@@ -227,46 +384,133 @@ class HQXServerService {
227
384
  this._emit('error', message.data);
228
385
  break;
229
386
  case 'pong':
230
- // Calculate ping latency
231
- if (this.lastPingTime > 0) {
232
- this.latency = Date.now() - this.lastPingTime;
233
- this._emit('latency', { latency: this.latency });
234
- }
387
+ // Already handled in binary path
235
388
  break;
236
389
  default:
237
390
  this._emit('message', message);
238
391
  }
239
392
  }
240
-
393
+
241
394
  /**
242
- * Get current latency
395
+ * Update latency statistics
396
+ * @private
243
397
  */
244
- getLatency() {
245
- return this.latency;
398
+ _updateLatency(latency) {
399
+ this.latency = latency;
400
+ this.minLatency = Math.min(this.minLatency, latency);
401
+ this.maxLatency = Math.max(this.maxLatency, latency);
402
+
403
+ // Rolling average (last 100 samples)
404
+ this.latencySamples.push(latency);
405
+ if (this.latencySamples.length > 100) {
406
+ this.latencySamples.shift();
407
+ }
408
+ this.avgLatency = this.latencySamples.reduce((a, b) => a + b, 0) / this.latencySamples.length;
409
+
410
+ // Adapt heartbeat based on latency
411
+ this._adaptHeartbeat();
412
+
413
+ this._emit('latency', {
414
+ current: latency,
415
+ min: this.minLatency,
416
+ max: this.maxLatency,
417
+ avg: this.avgLatency
418
+ });
246
419
  }
247
420
 
248
421
  /**
249
- * Send message to server
422
+ * Adapt heartbeat interval based on connection quality
423
+ * @private
424
+ */
425
+ _adaptHeartbeat() {
426
+ // Good connection: slower heartbeat (less overhead)
427
+ // Poor connection: faster heartbeat (detect issues quickly)
428
+ if (this.avgLatency < 10) {
429
+ this.adaptiveHeartbeat = 2000; // <10ms: 2s heartbeat
430
+ } else if (this.avgLatency < 50) {
431
+ this.adaptiveHeartbeat = 1000; // <50ms: 1s heartbeat
432
+ } else if (this.avgLatency < 100) {
433
+ this.adaptiveHeartbeat = 500; // <100ms: 500ms heartbeat
434
+ } else {
435
+ this.adaptiveHeartbeat = 250; // High latency: 250ms heartbeat
436
+ }
437
+ }
438
+
439
+ // ==================== SENDING ====================
440
+
441
+ /**
442
+ * Send message with minimal overhead
443
+ * @param {string} type - Message type
444
+ * @param {Object} data - Payload
250
445
  */
251
446
  send(type, data) {
252
447
  const message = {
253
448
  type,
254
449
  data,
255
- timestamp: Date.now(),
256
- sessionId: this.sessionId
450
+ ts: Date.now(), // Short key for speed
451
+ sid: this.sessionId,
257
452
  };
258
453
 
259
- if (this.connected && this.ws.readyState === WebSocket.OPEN) {
260
- this.ws.send(JSON.stringify(message));
454
+ if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
455
+ this._sendRaw(fastStringify(message));
456
+ this.messagesSent++;
261
457
  } else {
262
458
  this.messageQueue.push(message);
263
459
  }
264
460
  }
265
461
 
462
+ /**
463
+ * Send raw data (no JSON overhead)
464
+ * @private
465
+ */
466
+ _sendRaw(data) {
467
+ try {
468
+ this.ws.send(data);
469
+ } catch (err) {
470
+ log.warn('Send error', { error: err.message });
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Send binary ping for lowest latency measurement
476
+ * @private
477
+ */
478
+ _sendBinaryPing() {
479
+ if (!this.connected || this.ws?.readyState !== WebSocket.OPEN) return;
480
+
481
+ this.lastPingTime = process.hrtime.bigint();
482
+
483
+ // Write timestamp to pre-allocated buffer
484
+ PING_BUFFER.writeBigInt64LE(this.lastPingTime, 1);
485
+
486
+ try {
487
+ this.ws.send(PING_BUFFER);
488
+ } catch {
489
+ // Ignore ping errors
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Flush message queue
495
+ * @private
496
+ */
497
+ _flushQueue() {
498
+ while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
499
+ const message = this.messageQueue.shift();
500
+ this._sendRaw(fastStringify(message));
501
+ this.messagesSent++;
502
+ }
503
+ }
504
+
505
+ // ==================== ALGO CONTROL ====================
506
+
266
507
  /**
267
508
  * Start algo trading session
509
+ * @param {Object} config
268
510
  */
269
511
  startAlgo(config) {
512
+ log.info('Starting algo', { symbol: config.symbol, contracts: config.contracts });
513
+
270
514
  this.send('start_algo', {
271
515
  accountId: config.accountId,
272
516
  contractId: config.contractId,
@@ -276,28 +520,30 @@ class HQXServerService {
276
520
  maxRisk: config.maxRisk,
277
521
  propfirm: config.propfirm,
278
522
  propfirmToken: config.propfirmToken,
279
- // Rithmic credentials (for Apex, TopstepTrader Rithmic, etc.)
280
523
  rithmicCredentials: config.rithmicCredentials || null,
281
- // Copy trading mode
282
524
  copyTrading: config.copyTrading || false,
283
525
  followerSymbol: config.followerSymbol,
284
- followerContracts: config.followerContracts
526
+ followerContracts: config.followerContracts,
285
527
  });
286
528
  }
287
529
 
288
530
  /**
289
- * Stop algo trading session
531
+ * Stop algo trading
290
532
  */
291
533
  stopAlgo() {
534
+ log.info('Stopping algo');
292
535
  this.send('stop_algo', {});
293
536
  }
294
537
 
295
538
  /**
296
- * Start copy trading session
539
+ * Start copy trading
540
+ * @param {Object} config
297
541
  */
298
542
  startCopyTrading(config) {
543
+ log.info('Starting copy trading');
544
+
299
545
  this.send('start_copy_trading', {
300
- // Lead account
546
+ // Lead
301
547
  leadAccountId: config.leadAccountId,
302
548
  leadContractId: config.leadContractId,
303
549
  leadSymbol: config.leadSymbol,
@@ -305,7 +551,7 @@ class HQXServerService {
305
551
  leadPropfirm: config.leadPropfirm,
306
552
  leadToken: config.leadToken,
307
553
  leadRithmicCredentials: config.leadRithmicCredentials,
308
- // Follower account
554
+ // Follower
309
555
  followerAccountId: config.followerAccountId,
310
556
  followerContractId: config.followerContractId,
311
557
  followerSymbol: config.followerSymbol,
@@ -315,12 +561,16 @@ class HQXServerService {
315
561
  followerRithmicCredentials: config.followerRithmicCredentials,
316
562
  // Targets
317
563
  dailyTarget: config.dailyTarget,
318
- maxRisk: config.maxRisk
564
+ maxRisk: config.maxRisk,
319
565
  });
320
566
  }
321
567
 
568
+ // ==================== EVENTS ====================
569
+
322
570
  /**
323
- * Event listeners
571
+ * Register event listener
572
+ * @param {string} event
573
+ * @param {Function} callback
324
574
  */
325
575
  on(event, callback) {
326
576
  if (!this.listeners.has(event)) {
@@ -329,95 +579,177 @@ class HQXServerService {
329
579
  this.listeners.get(event).push(callback);
330
580
  }
331
581
 
582
+ /**
583
+ * Remove event listener
584
+ * @param {string} event
585
+ * @param {Function} callback
586
+ */
332
587
  off(event, callback) {
333
- if (this.listeners.has(event)) {
334
- const callbacks = this.listeners.get(event);
588
+ const callbacks = this.listeners.get(event);
589
+ if (callbacks) {
335
590
  const index = callbacks.indexOf(callback);
336
- if (index > -1) {
337
- callbacks.splice(index, 1);
338
- }
591
+ if (index > -1) callbacks.splice(index, 1);
339
592
  }
340
593
  }
341
594
 
595
+ /**
596
+ * Emit event (inlined for speed)
597
+ * @private
598
+ */
342
599
  _emit(event, data) {
343
- if (this.listeners.has(event)) {
344
- this.listeners.get(event).forEach(callback => {
345
- try {
346
- callback(data);
347
- } catch (e) {
348
- // Callback error
349
- }
350
- });
600
+ const callbacks = this.listeners.get(event);
601
+ if (!callbacks) return;
602
+
603
+ for (let i = 0; i < callbacks.length; i++) {
604
+ try {
605
+ callbacks[i](data);
606
+ } catch {
607
+ // Don't let callback errors break the loop
608
+ }
351
609
  }
352
610
  }
353
611
 
612
+ // ==================== HEARTBEAT ====================
613
+
354
614
  /**
355
- * Heartbeat to keep connection alive
615
+ * Start adaptive heartbeat
616
+ * @private
356
617
  */
357
618
  _startHeartbeat() {
358
- this.heartbeatInterval = setInterval(() => {
619
+ this._stopHeartbeat();
620
+
621
+ const heartbeat = () => {
359
622
  if (this.connected) {
360
- this.lastPingTime = Date.now();
361
- this.send('ping', { timestamp: this.lastPingTime });
623
+ this._sendBinaryPing();
624
+
625
+ // Schedule next with adaptive interval
626
+ this.pingInterval = setTimeout(heartbeat, this.adaptiveHeartbeat);
362
627
  }
363
- }, 5000); // Ping every 5 seconds for more accurate latency
628
+ };
629
+
630
+ // First ping immediately
631
+ this._sendBinaryPing();
632
+ this.pingInterval = setTimeout(heartbeat, this.adaptiveHeartbeat);
364
633
  }
365
634
 
635
+ /**
636
+ * Stop heartbeat
637
+ * @private
638
+ */
366
639
  _stopHeartbeat() {
367
- if (this.heartbeatInterval) {
368
- clearInterval(this.heartbeatInterval);
369
- this.heartbeatInterval = null;
640
+ if (this.pingInterval) {
641
+ clearTimeout(this.pingInterval);
642
+ this.pingInterval = null;
370
643
  }
371
644
  }
372
645
 
646
+ // ==================== RECONNECT ====================
647
+
373
648
  /**
374
- * Flush queued messages after reconnect
649
+ * Attempt reconnection with exponential backoff
650
+ * @private
375
651
  */
376
- _flushMessageQueue() {
377
- while (this.messageQueue.length > 0) {
378
- const message = this.messageQueue.shift();
379
- if (this.ws.readyState === WebSocket.OPEN) {
380
- this.ws.send(JSON.stringify(message));
381
- }
652
+ _attemptReconnect() {
653
+ if (this.reconnectAttempts >= SECURITY.MAX_RECONNECT_ATTEMPTS) {
654
+ log.error('Max reconnect attempts reached');
655
+ return;
382
656
  }
657
+
658
+ this.reconnecting = true;
659
+ this.reconnectAttempts++;
660
+
661
+ // Fast initial reconnect, then backoff
662
+ const delay = Math.min(
663
+ 100 * Math.pow(2, this.reconnectAttempts - 1), // Start at 100ms
664
+ 10000 // Max 10s
665
+ );
666
+
667
+ log.info('Reconnecting', { attempt: this.reconnectAttempts, delay });
668
+
669
+ setTimeout(async () => {
670
+ try {
671
+ await this.connect();
672
+ } catch (err) {
673
+ log.error('Reconnect failed', { error: err.message });
674
+ }
675
+ this.reconnecting = false;
676
+ }, delay);
383
677
  }
384
678
 
679
+ // ==================== STATS ====================
680
+
385
681
  /**
386
- * Attempt to reconnect
682
+ * Get latency statistics
683
+ * @returns {Object}
387
684
  */
388
- _attemptReconnect() {
389
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
390
- this.reconnectAttempts++;
391
- const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
392
-
393
- setTimeout(() => {
394
- this.connect().catch(() => {
395
- // Reconnect failed
396
- });
397
- }, delay);
398
- }
685
+ getLatencyStats() {
686
+ return {
687
+ current: this.latency,
688
+ min: this.minLatency === Infinity ? 0 : this.minLatency,
689
+ max: this.maxLatency,
690
+ avg: this.avgLatency,
691
+ samples: this.latencySamples.length,
692
+ };
693
+ }
694
+
695
+ /**
696
+ * Get connection statistics
697
+ * @returns {Object}
698
+ */
699
+ getStats() {
700
+ return {
701
+ connected: this.connected,
702
+ messagesSent: this.messagesSent,
703
+ messagesReceived: this.messagesReceived,
704
+ bytesReceived: this.bytesReceived,
705
+ heartbeatInterval: this.adaptiveHeartbeat,
706
+ latency: this.getLatencyStats(),
707
+ };
399
708
  }
400
709
 
401
710
  /**
402
- * Disconnect from server
711
+ * Get current latency
712
+ * @returns {number}
713
+ */
714
+ getLatency() {
715
+ return this.latency;
716
+ }
717
+
718
+ // ==================== CLEANUP ====================
719
+
720
+ /**
721
+ * Disconnect and cleanup
403
722
  */
404
723
  disconnect() {
724
+ log.info('Disconnecting');
725
+
405
726
  this._stopHeartbeat();
727
+
406
728
  if (this.ws) {
407
- this.ws.close();
729
+ this.ws.close(1000, 'Client disconnect');
408
730
  this.ws = null;
409
731
  }
732
+
410
733
  this.connected = false;
411
734
  this.token = null;
412
735
  this.sessionId = null;
736
+ this.messageQueue = [];
737
+ this.listeners.clear();
738
+
739
+ // Reset stats
740
+ this.latencySamples = [];
741
+ this.minLatency = Infinity;
742
+ this.maxLatency = 0;
743
+ this.avgLatency = 0;
413
744
  }
414
745
 
415
746
  /**
416
747
  * Check if connected
748
+ * @returns {boolean}
417
749
  */
418
750
  isConnected() {
419
- return this.connected && this.ws && this.ws.readyState === WebSocket.OPEN;
751
+ return this.connected && this.ws?.readyState === WebSocket.OPEN;
420
752
  }
421
753
  }
422
754
 
423
- module.exports = { HQXServerService, HQX_CONFIG };
755
+ module.exports = { HQXServerService, HQX_SERVER, MSG_TYPE };