hedgequantx 2.9.240 → 2.9.242
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
CHANGED
|
@@ -43,6 +43,21 @@ class DaemonClient extends EventEmitter {
|
|
|
43
43
|
|
|
44
44
|
/** @type {Object|null} Cached daemon info */
|
|
45
45
|
this.daemonInfo = null;
|
|
46
|
+
|
|
47
|
+
/** @type {boolean} Auto-reconnect enabled */
|
|
48
|
+
this.autoReconnect = true;
|
|
49
|
+
|
|
50
|
+
/** @type {number} Reconnect attempts */
|
|
51
|
+
this.reconnectAttempts = 0;
|
|
52
|
+
|
|
53
|
+
/** @type {number} Max reconnect attempts */
|
|
54
|
+
this.maxReconnectAttempts = 10;
|
|
55
|
+
|
|
56
|
+
/** @type {NodeJS.Timeout|null} Reconnect timer */
|
|
57
|
+
this.reconnectTimer = null;
|
|
58
|
+
|
|
59
|
+
/** @type {boolean} Intentionally disconnecting */
|
|
60
|
+
this._disconnecting = false;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/**
|
|
@@ -64,6 +79,9 @@ class DaemonClient extends EventEmitter {
|
|
|
64
79
|
this.daemonInfo = await this._request(MSG_TYPE.HANDSHAKE, null, TIMEOUTS.HANDSHAKE);
|
|
65
80
|
log.debug('Handshake complete', this.daemonInfo);
|
|
66
81
|
|
|
82
|
+
// Reset reconnect attempts on successful connection
|
|
83
|
+
this.reconnectAttempts = 0;
|
|
84
|
+
|
|
67
85
|
// Start ping interval
|
|
68
86
|
this._startPing();
|
|
69
87
|
|
|
@@ -84,8 +102,14 @@ class DaemonClient extends EventEmitter {
|
|
|
84
102
|
|
|
85
103
|
this.socket.on('close', () => {
|
|
86
104
|
log.debug('Disconnected from daemon');
|
|
105
|
+
const wasConnected = this.connected;
|
|
87
106
|
this._cleanup();
|
|
88
107
|
this.emit('disconnected');
|
|
108
|
+
|
|
109
|
+
// Auto-reconnect if enabled and not intentionally disconnecting
|
|
110
|
+
if (wasConnected && this.autoReconnect && !this._disconnecting) {
|
|
111
|
+
this._scheduleReconnect();
|
|
112
|
+
}
|
|
89
113
|
});
|
|
90
114
|
|
|
91
115
|
this.socket.on('error', (err) => {
|
|
@@ -113,16 +137,28 @@ class DaemonClient extends EventEmitter {
|
|
|
113
137
|
|
|
114
138
|
/**
|
|
115
139
|
* Disconnect from daemon
|
|
140
|
+
* @param {boolean} [permanent=false] - If true, disable auto-reconnect
|
|
116
141
|
*/
|
|
117
|
-
disconnect() {
|
|
142
|
+
disconnect(permanent = false) {
|
|
143
|
+
this._disconnecting = true;
|
|
144
|
+
if (permanent) {
|
|
145
|
+
this.autoReconnect = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.reconnectTimer) {
|
|
149
|
+
clearTimeout(this.reconnectTimer);
|
|
150
|
+
this.reconnectTimer = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
if (this.socket) {
|
|
119
154
|
this.socket.destroy();
|
|
120
155
|
}
|
|
121
156
|
this._cleanup();
|
|
157
|
+
this._disconnecting = false;
|
|
122
158
|
}
|
|
123
159
|
|
|
124
160
|
/**
|
|
125
|
-
* Cleanup state
|
|
161
|
+
* Cleanup state (does NOT clear reconnect state)
|
|
126
162
|
*/
|
|
127
163
|
_cleanup() {
|
|
128
164
|
this.connected = false;
|
|
@@ -137,6 +173,18 @@ class DaemonClient extends EventEmitter {
|
|
|
137
173
|
this.socket = null;
|
|
138
174
|
}
|
|
139
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Enable/disable auto-reconnect
|
|
178
|
+
* @param {boolean} enable
|
|
179
|
+
*/
|
|
180
|
+
setAutoReconnect(enable) {
|
|
181
|
+
this.autoReconnect = enable;
|
|
182
|
+
if (!enable && this.reconnectTimer) {
|
|
183
|
+
clearTimeout(this.reconnectTimer);
|
|
184
|
+
this.reconnectTimer = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
140
188
|
/**
|
|
141
189
|
* Start ping interval
|
|
142
190
|
*/
|
|
@@ -145,12 +193,49 @@ class DaemonClient extends EventEmitter {
|
|
|
145
193
|
try {
|
|
146
194
|
await this._request(MSG_TYPE.PING, null, TIMEOUTS.PING_TIMEOUT);
|
|
147
195
|
} catch (err) {
|
|
148
|
-
log.warn('Ping failed,
|
|
149
|
-
|
|
196
|
+
log.warn('Ping failed, will auto-reconnect');
|
|
197
|
+
// Don't call disconnect() - let socket close trigger reconnect
|
|
198
|
+
if (this.socket) {
|
|
199
|
+
this.socket.destroy();
|
|
200
|
+
}
|
|
150
201
|
}
|
|
151
202
|
}, TIMEOUTS.PING_INTERVAL);
|
|
152
203
|
}
|
|
153
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Schedule auto-reconnect with exponential backoff
|
|
207
|
+
*/
|
|
208
|
+
_scheduleReconnect() {
|
|
209
|
+
if (this.reconnectTimer) {
|
|
210
|
+
clearTimeout(this.reconnectTimer);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
214
|
+
log.error('Max reconnect attempts reached, giving up');
|
|
215
|
+
this.emit('reconnectFailed');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Exponential backoff: 1s, 2s, 4s, 8s... max 30s
|
|
220
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
221
|
+
this.reconnectAttempts++;
|
|
222
|
+
|
|
223
|
+
log.debug(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
224
|
+
|
|
225
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
226
|
+
this.reconnectTimer = null;
|
|
227
|
+
log.debug('Attempting reconnect...');
|
|
228
|
+
|
|
229
|
+
const success = await this.connect();
|
|
230
|
+
if (success) {
|
|
231
|
+
log.debug('Reconnected successfully');
|
|
232
|
+
this.reconnectAttempts = 0;
|
|
233
|
+
this.emit('reconnected');
|
|
234
|
+
}
|
|
235
|
+
// If failed, the connect() will trigger another close -> scheduleReconnect
|
|
236
|
+
}, delay);
|
|
237
|
+
}
|
|
238
|
+
|
|
154
239
|
/**
|
|
155
240
|
* Handle incoming message
|
|
156
241
|
* @param {Object} msg
|
|
@@ -145,11 +145,18 @@ const placeOrder = async (service, orderData) => {
|
|
|
145
145
|
orderTag,
|
|
146
146
|
});
|
|
147
147
|
} else if (status === 5 || status === 6) {
|
|
148
|
+
// Extract rejection reason from rpCode[1] if available
|
|
149
|
+
const rpCode = order.rpCode;
|
|
150
|
+
let errorMsg = `Order rejected: status ${status}`;
|
|
151
|
+
if (rpCode && Array.isArray(rpCode) && rpCode.length > 1 && rpCode[1]) {
|
|
152
|
+
errorMsg = `Order rejected: ${rpCode[1]}`;
|
|
153
|
+
}
|
|
148
154
|
resolve({
|
|
149
155
|
success: false,
|
|
150
|
-
error:
|
|
156
|
+
error: errorMsg,
|
|
151
157
|
orderId: order.basketId,
|
|
152
158
|
orderTag,
|
|
159
|
+
rpCode,
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
162
|
}
|
|
@@ -169,6 +176,11 @@ const placeOrder = async (service, orderData) => {
|
|
|
169
176
|
routes.values().next().value?.tradeRoute || null;
|
|
170
177
|
}
|
|
171
178
|
|
|
179
|
+
// Warn if no trade route - Rithmic will reject the order
|
|
180
|
+
if (!tradeRoute) {
|
|
181
|
+
DEBUG && console.log('[Orders] WARNING: No trade route for', exchange, '- order may be rejected');
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
// HFT: Reuse template and mutate (faster than object spread)
|
|
173
185
|
ORDER_REQUEST_TEMPLATE.userMsg[0] = orderTag;
|
|
174
186
|
ORDER_REQUEST_TEMPLATE.fcmId = service.loginInfo.fcmId;
|
|
@@ -99,15 +99,24 @@ function skipField(buffer, offset, wireType) {
|
|
|
99
99
|
case 0: // Varint
|
|
100
100
|
const [, newOffset] = readVarint(buffer, offset);
|
|
101
101
|
return newOffset;
|
|
102
|
-
case 1: // 64-bit
|
|
102
|
+
case 1: // 64-bit (fixed64, sfixed64, double)
|
|
103
103
|
return offset + 8;
|
|
104
|
-
case 2: // Length-delimited
|
|
104
|
+
case 2: // Length-delimited (string, bytes, embedded messages, packed repeated)
|
|
105
105
|
const [length, lenOffset] = readVarint(buffer, offset);
|
|
106
106
|
return lenOffset + length;
|
|
107
|
-
case
|
|
107
|
+
case 3: // Start group (deprecated)
|
|
108
|
+
case 4: // End group (deprecated)
|
|
109
|
+
// Groups are deprecated, skip to end of buffer
|
|
110
|
+
return buffer.length;
|
|
111
|
+
case 5: // 32-bit (fixed32, sfixed32, float)
|
|
108
112
|
return offset + 4;
|
|
113
|
+
case 6: // Reserved (unused)
|
|
114
|
+
case 7: // Reserved (unused) - indicates corrupted data
|
|
115
|
+
// Skip to end to prevent infinite loops on corrupted data
|
|
116
|
+
return buffer.length;
|
|
109
117
|
default:
|
|
110
|
-
|
|
118
|
+
// Unknown wire type - skip to end
|
|
119
|
+
return buffer.length;
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -93,8 +93,15 @@ class ProtobufHandler {
|
|
|
93
93
|
// Skip 4-byte length prefix
|
|
94
94
|
const data = buffer.length > 4 ? buffer.slice(4) : buffer;
|
|
95
95
|
|
|
96
|
+
// Sanity check: buffer must be at least a few bytes
|
|
97
|
+
if (data.length < 2) return -1;
|
|
98
|
+
|
|
96
99
|
let offset = 0;
|
|
97
|
-
|
|
100
|
+
let iterations = 0;
|
|
101
|
+
const maxIterations = 100; // Prevent infinite loops on corrupted data
|
|
102
|
+
|
|
103
|
+
while (offset < data.length && iterations < maxIterations) {
|
|
104
|
+
iterations++;
|
|
98
105
|
try {
|
|
99
106
|
const [tag, newOffset] = readVarint(data, offset);
|
|
100
107
|
const fieldNumber = tag >>> 3;
|
|
@@ -106,7 +113,11 @@ class ProtobufHandler {
|
|
|
106
113
|
return templateId;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
const prevOffset = offset;
|
|
109
117
|
offset = skipField(data, offset, wireType);
|
|
118
|
+
|
|
119
|
+
// Ensure we're making progress (prevent infinite loop)
|
|
120
|
+
if (offset <= prevOffset) break;
|
|
110
121
|
} catch (e) {
|
|
111
122
|
break;
|
|
112
123
|
}
|