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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.240",
3
+ "version": "2.9.242",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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, disconnecting');
149
- this.disconnect();
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: `Order rejected: status ${status}`,
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 5: // 32-bit
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
- throw new Error(`Unknown wire type: ${wireType}`);
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
- while (offset < data.length) {
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
  }