hedgequantx 2.7.14 → 2.7.16
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/package.json +1 -1
- package/src/lib/data.js +245 -471
- package/src/lib/m/s1-models.js +173 -0
- package/src/lib/m/s1.js +354 -735
- package/src/services/rithmic/accounts.js +22 -7
- package/src/lib/api.js +0 -198
- package/src/lib/api2.js +0 -353
- package/src/lib/core.js +0 -539
- package/src/lib/core2.js +0 -341
- package/src/lib/data2.js +0 -492
- package/src/lib/decoder.js +0 -599
- package/src/lib/m/s2.js +0 -34
- package/src/lib/n/r1.js +0 -454
- package/src/lib/n/r2.js +0 -514
- package/src/lib/n/r3.js +0 -631
- package/src/lib/n/r4.js +0 -401
- package/src/lib/n/r5.js +0 -335
- package/src/lib/n/r6.js +0 -425
- package/src/lib/n/r7.js +0 -530
- package/src/lib/o/l1.js +0 -44
- package/src/lib/o/l2.js +0 -427
- package/src/lib/python-bridge.js +0 -206
package/src/lib/n/r2.js
DELETED
|
@@ -1,514 +0,0 @@
|
|
|
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 };
|