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,514 @@
1
+ /**
2
+ * Rithmic Connection Manager
3
+ * Handles WebSocket connection, protobuf encoding/decoding, and heartbeat
4
+ */
5
+
6
+ const EventEmitter = require('events');
7
+ const WebSocket = require('ws');
8
+ const protobuf = require('protobufjs');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+
12
+ const { TEMPLATE_IDS, INFRA_TYPES, RITHMIC_GATEWAYS } = require('./r7');
13
+
14
+ // Proto path
15
+ const PROTO_PATH = path.join(__dirname, '../../../protos/rithmic');
16
+
17
+ class RithmicConnection extends EventEmitter {
18
+ constructor(options = {}) {
19
+ super();
20
+
21
+ this.ws = null;
22
+ this.isConnected = false;
23
+ this.isLoggedIn = false;
24
+ this.infraType = null;
25
+ this.config = null;
26
+
27
+ // Login info from server
28
+ this.fcmId = null;
29
+ this.ibId = null;
30
+ this.uniqueUserId = null;
31
+ this.heartbeatInterval = 30; // seconds
32
+
33
+ // Protobuf message types (loaded dynamically)
34
+ this.protoRoot = null;
35
+ this.messageTypes = {};
36
+
37
+ // Heartbeat
38
+ this._heartbeatTimer = null;
39
+ this._lastHeartbeat = 0;
40
+
41
+ // Reconnection
42
+ this.reconnectAttempts = 0;
43
+ this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
44
+ this.reconnectDelay = options.reconnectDelay || 2000;
45
+ this._intentionalDisconnect = false;
46
+
47
+ // Message handling
48
+ this._messageHandlers = new Map();
49
+ this._pendingRequests = new Map();
50
+ this._requestId = 0;
51
+
52
+ // Debug
53
+ this.debug = options.debug || false;
54
+ }
55
+
56
+ /**
57
+ * Load all protobuf definitions
58
+ */
59
+ async _loadProtos() {
60
+ if (this.protoRoot) return;
61
+
62
+ try {
63
+ // Get all proto files
64
+ const protoFiles = fs.readdirSync(PROTO_PATH)
65
+ .filter(f => f.endsWith('.proto'))
66
+ .map(f => path.join(PROTO_PATH, f));
67
+
68
+ if (protoFiles.length === 0) {
69
+ throw new Error('No proto files found in ' + PROTO_PATH);
70
+ }
71
+
72
+ this.protoRoot = await protobuf.load(protoFiles);
73
+
74
+ // Load all message types
75
+ const messageNames = [
76
+ 'RequestLogin', 'ResponseLogin',
77
+ 'RequestLogout', 'ResponseLogout',
78
+ 'RequestHeartbeat', 'ResponseHeartbeat',
79
+ 'RequestMarketDataUpdate', 'ResponseMarketDataUpdate',
80
+ 'LastTrade', 'BestBidOffer',
81
+ 'RequestNewOrder', 'ResponseNewOrder',
82
+ 'RequestCancelAllOrders',
83
+ 'RequestSubscribeForOrderUpdates', 'ResponseSubscribeForOrderUpdates',
84
+ 'ExchangeOrderNotification', 'RithmicOrderNotification',
85
+ 'RequestTradeRoutes', 'ResponseTradeRoutes',
86
+ 'RequestAccountList', 'ResponseAccountList',
87
+ 'RequestPnLPositionSnapshot', 'ResponsePnLPositionSnapshot',
88
+ 'RequestPnLPositionUpdates', 'ResponsePnLPositionUpdates',
89
+ 'AccountPnLPositionUpdate', 'InstrumentPnLPositionUpdate',
90
+ 'RequestShowOrders', 'ResponseShowOrders',
91
+ 'RequestLoginInfo', 'ResponseLoginInfo'
92
+ ];
93
+
94
+ for (const name of messageNames) {
95
+ try {
96
+ this.messageTypes[name] = this.protoRoot.lookupType(`rti.${name}`);
97
+ } catch (e) {
98
+ // Some message types may not exist
99
+ if (this.debug) {
100
+ // Proto type not found - skip silently
101
+ }
102
+ }
103
+ }
104
+
105
+
106
+
107
+ } catch (error) {
108
+ console.error('[RITHMIC] Failed to load protos:', error.message);
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Connect to Rithmic gateway
115
+ * @param {Object} config - Connection config
116
+ * @param {string} config.userId - Rithmic username
117
+ * @param {string} config.password - Rithmic password
118
+ * @param {string} config.systemName - PropFirm system name
119
+ * @param {string} config.gateway - Gateway URL
120
+ * @param {number} config.infraType - Plant type (TICKER_PLANT, ORDER_PLANT, etc.)
121
+ */
122
+ async connect(config) {
123
+ await this._loadProtos();
124
+
125
+ this.config = config;
126
+ this.infraType = config.infraType || INFRA_TYPES.TICKER_PLANT;
127
+
128
+ const gatewayUrl = config.gateway || RITHMIC_GATEWAYS.CHICAGO;
129
+ const infraName = this._getInfraName(this.infraType);
130
+
131
+ return new Promise((resolve, reject) => {
132
+ this.ws = new WebSocket(gatewayUrl, {
133
+ rejectUnauthorized: false,
134
+ handshakeTimeout: 15000
135
+ });
136
+
137
+ const timeout = setTimeout(() => {
138
+ this.ws.close();
139
+ reject(new Error('Connection timeout'));
140
+ }, 20000);
141
+
142
+ this.ws.on('open', () => {
143
+ this.isConnected = true;
144
+ this._sendLogin();
145
+ });
146
+
147
+ this.ws.on('message', (data) => {
148
+ this._handleMessage(data, (success, error, loginData) => {
149
+ if (success !== undefined) {
150
+ clearTimeout(timeout);
151
+ if (success) {
152
+ this.isLoggedIn = true;
153
+ this.reconnectAttempts = 0;
154
+ this._startHeartbeat();
155
+ this.emit('loggedIn', loginData);
156
+ resolve(loginData);
157
+ } else {
158
+ this.emit('loginFailed', error);
159
+ reject(new Error(error || 'Login failed'));
160
+ }
161
+ }
162
+ });
163
+ });
164
+
165
+ this.ws.on('error', (error) => {
166
+ console.error(`[RITHMIC:${infraName}] WebSocket error:`, error.message);
167
+ this.emit('error', error);
168
+ clearTimeout(timeout);
169
+ if (!this.isLoggedIn) {
170
+ reject(error);
171
+ }
172
+ });
173
+
174
+ this.ws.on('close', (code, reason) => {
175
+ const reasonStr = reason ? reason.toString() : 'unknown';
176
+ this.isConnected = false;
177
+ this.isLoggedIn = false;
178
+ this._stopHeartbeat();
179
+ this.emit('disconnected', { code, reason: reasonStr });
180
+
181
+ // Auto-reconnect if not intentional
182
+ if (!this._intentionalDisconnect) {
183
+ this._attemptReconnect();
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Send login request
191
+ */
192
+ _sendLogin() {
193
+ const RequestLogin = this.messageTypes.RequestLogin;
194
+ if (!RequestLogin) {
195
+ throw new Error('RequestLogin proto not loaded');
196
+ }
197
+
198
+ const loginRequest = RequestLogin.create({
199
+ templateId: TEMPLATE_IDS.REQUEST_LOGIN,
200
+ templateVersion: '3.9',
201
+ user: this.config.userId,
202
+ password: this.config.password,
203
+ appName: 'App',
204
+ appVersion: '1.0.0',
205
+ systemName: this.config.systemName,
206
+ infraType: this.infraType
207
+ });
208
+
209
+ const buffer = RequestLogin.encode(loginRequest).finish();
210
+ this.ws.send(buffer);
211
+
212
+ if (this.debug) {
213
+ console.log(`[RITHMIC] Login sent for ${this.config.userId} on ${this.config.systemName}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Handle incoming message
219
+ */
220
+ _handleMessage(data, loginCallback) {
221
+ try {
222
+ const buffer = new Uint8Array(data);
223
+
224
+ // Try to decode as ResponseLogin first
225
+ if (loginCallback && !this.isLoggedIn) {
226
+ try {
227
+ const ResponseLogin = this.messageTypes.ResponseLogin;
228
+ if (ResponseLogin) {
229
+ const response = ResponseLogin.decode(buffer);
230
+
231
+ if (response.templateId === TEMPLATE_IDS.RESPONSE_LOGIN) {
232
+ const rpCode = Array.isArray(response.rpCode) ? response.rpCode[0] : response.rpCode;
233
+
234
+ if (rpCode === '0') {
235
+ this.fcmId = response.fcmId;
236
+ this.ibId = response.ibId;
237
+ this.uniqueUserId = response.uniqueUserId;
238
+ if (response.heartbeatInterval) {
239
+ this.heartbeatInterval = response.heartbeatInterval;
240
+ }
241
+
242
+ loginCallback(true, null, { fcmId: this.fcmId, ibId: this.ibId });
243
+ return;
244
+ } else {
245
+ const errorMsg = response.textMsg || `Error code: ${rpCode}`;
246
+ loginCallback(false, errorMsg);
247
+ return;
248
+ }
249
+ }
250
+ }
251
+ } catch (e) {
252
+ // Not a login response
253
+ }
254
+ }
255
+
256
+ // Decode based on template ID (first few bytes contain it)
257
+ const decoded = this._decodeMessage(buffer);
258
+ if (decoded) {
259
+ this._dispatchMessage(decoded);
260
+ }
261
+
262
+ } catch (error) {
263
+ if (this.debug) {
264
+ console.error('[RITHMIC] Message decode error:', error.message);
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Decode a protobuf message
271
+ */
272
+ _decodeMessage(buffer) {
273
+ // First check template ID for messages that need manual decoding (large field IDs)
274
+ const { getTemplateId, decodeAccountPnL, decodeInstrumentPnL } = require('../decoder');
275
+ const templateId = getTemplateId(Buffer.from(buffer));
276
+
277
+ // Use manual decoder for P&L messages (protobufjs can't handle field IDs > 100000)
278
+ if (templateId === 450) {
279
+ const decoded = decodeAccountPnL(Buffer.from(buffer));
280
+ return { type: 'AccountPnLPositionUpdate', data: decoded };
281
+ }
282
+ if (templateId === 451) {
283
+ const decoded = decodeInstrumentPnL(Buffer.from(buffer));
284
+ return { type: 'InstrumentPnLPositionUpdate', data: decoded };
285
+ }
286
+
287
+ // Try different message types based on context
288
+ const decoders = [
289
+ { type: 'ResponseHeartbeat', id: TEMPLATE_IDS.RESPONSE_HEARTBEAT },
290
+ { type: 'LastTrade', id: TEMPLATE_IDS.LAST_TRADE },
291
+ { type: 'BestBidOffer', id: TEMPLATE_IDS.BEST_BID_OFFER },
292
+ { type: 'ResponseNewOrder', id: TEMPLATE_IDS.RESPONSE_NEW_ORDER },
293
+ // RithmicOrderNotification (351) - internal order status updates
294
+ { type: 'RithmicOrderNotification', id: TEMPLATE_IDS.RITHMIC_ORDER_NOTIFICATION },
295
+ // ExchangeOrderNotification (352) - exchange order fills/updates
296
+ { type: 'ExchangeOrderNotification', id: TEMPLATE_IDS.EXCHANGE_ORDER_NOTIFICATION },
297
+ { type: 'ResponseTradeRoutes', id: TEMPLATE_IDS.RESPONSE_TRADE_ROUTES },
298
+ { type: 'ResponseAccountList', id: TEMPLATE_IDS.RESPONSE_ACCOUNT_LIST },
299
+ { type: 'ResponsePnLPositionUpdates', id: TEMPLATE_IDS.RESPONSE_PNL_POSITION_UPDATES },
300
+ { type: 'ResponseSubscribeForOrderUpdates', id: TEMPLATE_IDS.RESPONSE_SUBSCRIBE_FOR_ORDER_UPDATES },
301
+ { type: 'ResponseLoginInfo', id: TEMPLATE_IDS.RESPONSE_LOGIN_INFO },
302
+ { type: 'ResponseShowOrders', id: TEMPLATE_IDS.RESPONSE_SHOW_ORDERS }
303
+ ];
304
+
305
+ for (const decoder of decoders) {
306
+ try {
307
+ const MessageType = this.messageTypes[decoder.type];
308
+ if (MessageType) {
309
+ const decoded = MessageType.decode(buffer);
310
+ if (decoded.templateId === decoder.id) {
311
+ return { type: decoder.type, data: decoded };
312
+ }
313
+ }
314
+ } catch (e) {
315
+ // Not this message type
316
+ }
317
+ }
318
+
319
+ return null;
320
+ }
321
+
322
+ /**
323
+ * Dispatch decoded message to handlers
324
+ */
325
+ _dispatchMessage(decoded) {
326
+ const { type, data } = decoded;
327
+
328
+ // Handle heartbeat silently
329
+ if (type === 'ResponseHeartbeat') {
330
+ this._lastHeartbeat = Date.now();
331
+ return;
332
+ }
333
+
334
+ // Emit specific event
335
+ this.emit(type, data);
336
+
337
+ // Also emit generic message event
338
+ this.emit('message', decoded);
339
+
340
+ // Handle pending requests
341
+ if (data.userMsg && Array.isArray(data.userMsg)) {
342
+ for (const msg of data.userMsg) {
343
+ if (this._pendingRequests.has(msg)) {
344
+ const { resolve } = this._pendingRequests.get(msg);
345
+ this._pendingRequests.delete(msg);
346
+ resolve(data);
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Send a protobuf message
354
+ */
355
+ send(messageType, messageData) {
356
+ if (!this.isConnected || !this.ws) {
357
+ throw new Error('Not connected');
358
+ }
359
+
360
+ const MessageType = this.messageTypes[messageType];
361
+ if (!MessageType) {
362
+ throw new Error(`Unknown message type: ${messageType}`);
363
+ }
364
+
365
+ const message = MessageType.create(messageData);
366
+ const buffer = MessageType.encode(message).finish();
367
+ this.ws.send(buffer);
368
+
369
+ return true;
370
+ }
371
+
372
+ /**
373
+ * Send a request and wait for response
374
+ */
375
+ async sendRequest(messageType, messageData, timeoutMs = 10000) {
376
+ const requestId = `req_${++this._requestId}_${Date.now()}`;
377
+
378
+ // Add request ID to user_msg for tracking
379
+ messageData.userMsg = [requestId];
380
+
381
+ return new Promise((resolve, reject) => {
382
+ const timeout = setTimeout(() => {
383
+ this._pendingRequests.delete(requestId);
384
+ reject(new Error('Request timeout'));
385
+ }, timeoutMs);
386
+
387
+ this._pendingRequests.set(requestId, {
388
+ resolve: (data) => {
389
+ clearTimeout(timeout);
390
+ resolve(data);
391
+ },
392
+ reject: (error) => {
393
+ clearTimeout(timeout);
394
+ reject(error);
395
+ }
396
+ });
397
+
398
+ try {
399
+ this.send(messageType, messageData);
400
+ } catch (error) {
401
+ this._pendingRequests.delete(requestId);
402
+ clearTimeout(timeout);
403
+ reject(error);
404
+ }
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Start heartbeat
410
+ */
411
+ _startHeartbeat() {
412
+ this._stopHeartbeat();
413
+
414
+ const intervalMs = (this.heartbeatInterval || 30) * 1000;
415
+
416
+ this._heartbeatTimer = setInterval(() => {
417
+ if (this.isConnected && this.ws) {
418
+ try {
419
+ this.send('RequestHeartbeat', {
420
+ templateId: TEMPLATE_IDS.REQUEST_HEARTBEAT
421
+ });
422
+ } catch (e) {
423
+ console.error('[RITHMIC] Heartbeat failed:', e.message);
424
+ }
425
+ }
426
+ }, intervalMs);
427
+ }
428
+
429
+ /**
430
+ * Stop heartbeat
431
+ */
432
+ _stopHeartbeat() {
433
+ if (this._heartbeatTimer) {
434
+ clearInterval(this._heartbeatTimer);
435
+ this._heartbeatTimer = null;
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Attempt reconnection
441
+ */
442
+ _attemptReconnect() {
443
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
444
+ console.error('[RITHMIC] Max reconnect attempts reached');
445
+ this.emit('maxReconnectAttempts');
446
+ return;
447
+ }
448
+
449
+ this.reconnectAttempts++;
450
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
451
+ const maxDelay = 30000;
452
+ const actualDelay = Math.min(delay, maxDelay);
453
+
454
+ setTimeout(() => {
455
+ if (!this._intentionalDisconnect) {
456
+ this.connect(this.config).catch(err => {
457
+ console.error('[RITHMIC] Reconnect failed:', err.message);
458
+ });
459
+ }
460
+ }, actualDelay);
461
+ }
462
+
463
+ /**
464
+ * Disconnect
465
+ */
466
+ async disconnect() {
467
+ this._intentionalDisconnect = true;
468
+ this._stopHeartbeat();
469
+
470
+ if (this.ws) {
471
+ // Send logout first
472
+ if (this.isLoggedIn) {
473
+ try {
474
+ this.send('RequestLogout', {
475
+ templateId: TEMPLATE_IDS.REQUEST_LOGOUT
476
+ });
477
+ // Give it a moment
478
+ await new Promise(resolve => setTimeout(resolve, 200));
479
+ } catch (e) {
480
+ // Ignore logout errors
481
+ }
482
+ }
483
+
484
+ this.ws.close();
485
+ this.ws = null;
486
+ }
487
+
488
+ this.isConnected = false;
489
+ this.isLoggedIn = false;
490
+ }
491
+
492
+ /**
493
+ * Get infra type name for logging
494
+ */
495
+ _getInfraName(infraType) {
496
+ const names = {
497
+ [INFRA_TYPES.TICKER_PLANT]: 'TICKER',
498
+ [INFRA_TYPES.ORDER_PLANT]: 'ORDER',
499
+ [INFRA_TYPES.HISTORY_PLANT]: 'HISTORY',
500
+ [INFRA_TYPES.PNL_PLANT]: 'PNL',
501
+ [INFRA_TYPES.REPOSITORY_PLANT]: 'REPO'
502
+ };
503
+ return names[infraType] || 'UNKNOWN';
504
+ }
505
+
506
+ /**
507
+ * Check if connected and logged in
508
+ */
509
+ get isReady() {
510
+ return this.isConnected && this.isLoggedIn;
511
+ }
512
+ }
513
+
514
+ module.exports = { RithmicConnection };