hedgequantx 2.9.246 → 2.9.248

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.246",
3
+ "version": "2.9.248",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -77,47 +77,37 @@ const center = (text, width) => {
77
77
 
78
78
  /**
79
79
  * Fit text to exact width (truncate or pad)
80
- * Ensures output is EXACTLY width characters (visible)
80
+ * Ensures output is EXACTLY width visible characters
81
81
  */
82
82
  const fitToWidth = (text, width) => {
83
83
  const plain = stripAnsi(text);
84
84
 
85
- if (plain.length > width) {
86
- // Need to truncate - find cut point accounting for ANSI codes
87
- let visibleCount = 0;
88
- let cutIndex = 0;
89
- let inAnsi = false;
90
-
91
- for (let i = 0; i < text.length; i++) {
92
- if (text[i] === '\x1B') {
93
- inAnsi = true;
94
- } else if (inAnsi && text[i] === 'm') {
95
- inAnsi = false;
96
- } else if (!inAnsi) {
97
- visibleCount++;
98
- if (visibleCount >= width - 2) {
99
- cutIndex = i + 1;
100
- break;
101
- }
85
+ if (plain.length <= width) {
86
+ // Pad to exact width
87
+ return text + ' '.repeat(width - plain.length);
88
+ }
89
+
90
+ // Need to truncate to (width - 2) visible chars, then add ".."
91
+ const targetLen = width - 2;
92
+ let visibleCount = 0;
93
+ let cutIndex = 0;
94
+ let i = 0;
95
+
96
+ while (i < text.length && visibleCount < targetLen) {
97
+ if (text[i] === '\x1B') {
98
+ // Skip entire ANSI sequence
99
+ while (i < text.length && text[i] !== 'm') {
100
+ i++;
102
101
  }
103
- cutIndex = i + 1;
104
- }
105
-
106
- // Truncate and add ".." (2 chars to stay within width)
107
- const truncated = text.substring(0, cutIndex);
108
- const truncatedPlain = stripAnsi(truncated);
109
- const remaining = width - truncatedPlain.length;
110
-
111
- if (remaining >= 2) {
112
- return truncated + '..' + ' '.repeat(remaining - 2);
113
- } else if (remaining > 0) {
114
- return truncated + ' '.repeat(remaining);
102
+ i++; // skip 'm'
103
+ } else {
104
+ visibleCount++;
105
+ i++;
115
106
  }
116
- return truncated;
107
+ cutIndex = i;
117
108
  }
118
109
 
119
- // Pad to exact width
120
- return text + ' '.repeat(width - plain.length);
110
+ return text.substring(0, cutIndex) + '..';
121
111
  };
122
112
 
123
113
  /**
@@ -215,32 +215,34 @@ const handleShowOrdersResponse = (service, data) => {
215
215
 
216
216
  /**
217
217
  * Handle new order response (template 313)
218
+ * rpCode[0] = '0' means accepted, anything else = rejected by gateway
218
219
  */
219
220
  const handleNewOrderResponse = (service, data) => {
220
221
  try {
221
222
  const res = proto.decode('ResponseNewOrder', data);
222
- debug('New order response:', JSON.stringify(res));
223
223
 
224
- // Emit as orderNotification for the placeOrder listener
225
- // Even if no basketId, emit for rejection handling
226
- const isRejected = res.rpCode?.[0] !== '0';
224
+ const isAccepted = res.rpCode?.[0] === '0';
225
+ const rejectReason = res.rpCode?.[1] || res.rqHandlerRpCode?.[1] || 'Unknown';
226
+
227
+ // Log rejection details for debugging
228
+ if (!isAccepted) {
229
+ console.log('[Rithmic] Order rejected:', rejectReason, '| rpCode:', res.rpCode, '| rqHandlerRpCode:', res.rqHandlerRpCode);
230
+ }
231
+
227
232
  const order = {
228
- basketId: res.basketId || res.orderId || res.userMsg?.[0] || 'unknown',
229
- accountId: res.accountId,
233
+ basketId: res.basketId || null,
230
234
  symbol: res.symbol,
231
235
  exchange: res.exchange || 'CME',
232
- status: isRejected ? 5 : 2, // 2=Working, 5=Rejected
233
- notifyType: isRejected ? 0 : 1, // 1=Accepted
236
+ notifyType: isAccepted ? 1 : 6, // 1=STATUS (accepted), 6=REJECT
237
+ status: isAccepted ? 2 : 6,
238
+ text: isAccepted ? null : rejectReason,
234
239
  rpCode: res.rpCode,
235
- rqHandlerRpCode: res.rqHandlerRpCode, // Also include request handler response code
236
240
  userMsg: res.userMsg,
237
241
  };
238
242
  service.emit('orderNotification', order);
239
-
240
- // Also emit specific event
241
243
  service.emit('newOrderResponse', res);
242
244
  } catch (e) {
243
- debug('Error decoding ResponseNewOrder:', e.message);
245
+ console.error('Error decoding ResponseNewOrder:', e.message);
244
246
  }
245
247
  };
246
248
 
@@ -369,12 +371,31 @@ const handleAccountRmsInfo = (service, data) => {
369
371
 
370
372
  /**
371
373
  * Handle exchange order notification (fills/trades)
372
- * NotifyType: 5 = FILL
374
+ * NotifyType: 1=STATUS, 2=MODIFY, 3=CANCEL, 4=TRIGGER, 5=FILL, 6=REJECT
373
375
  */
374
376
  const handleExchangeNotification = (service, data) => {
375
377
  try {
376
378
  const res = proto.decode('ExchangeOrderNotification', data);
377
- debug('Exchange notification:', res.notifyType, res.symbol);
379
+ debug('Exchange notification:', res.notifyType, res.symbol, res.status);
380
+
381
+ // Emit orderNotification for placeOrder listener
382
+ // This is critical for order tracking
383
+ const orderNotif = {
384
+ basketId: res.basketId,
385
+ accountId: res.accountId,
386
+ symbol: res.symbol,
387
+ exchange: res.exchange || 'CME',
388
+ notifyType: res.notifyType, // 1=STATUS, 5=FILL, 6=REJECT
389
+ status: res.notifyType === 5 ? 3 : res.notifyType === 6 ? 6 : 2,
390
+ fillPrice: res.fillPrice ? parseFloat(res.fillPrice) : null,
391
+ fillSize: res.fillSize ? parseInt(res.fillSize) : null,
392
+ avgFillPrice: res.avgFillPrice ? parseFloat(res.avgFillPrice) : null,
393
+ totalFillSize: res.totalFillSize ? parseInt(res.totalFillSize) : null,
394
+ confirmedSize: res.confirmedSize ? parseInt(res.confirmedSize) : null,
395
+ text: res.text || res.reportText,
396
+ userMsg: res.userTag ? [res.userTag] : null,
397
+ };
398
+ service.emit('orderNotification', orderNotif);
378
399
 
379
400
  // notifyType 5 = FILL (trade executed)
380
401
  if (res.notifyType === 5 && res.fillPrice && res.fillSize) {
@@ -125,72 +125,83 @@ const placeOrder = async (service, orderData) => {
125
125
  }, ORDER_TIMEOUTS.PLACE);
126
126
 
127
127
  const onNotification = (order) => {
128
- // HFT: Fast match by orderTag first (most specific)
129
- const orderMatches = (order.userMsg && order.userMsg.includes(orderTag)) ||
130
- order.symbol === orderData.symbol;
128
+ // Match by symbol
129
+ if (order.symbol !== orderData.symbol) return;
131
130
 
132
- if (orderMatches) {
131
+ const notifyType = order.notifyType;
132
+
133
+ // FILL (notifyType 5) - Order executed
134
+ if (notifyType === 5) {
133
135
  clearTimeout(timeout);
134
136
  service.removeListener('orderNotification', onNotification);
135
-
136
- // HFT: Combined status check (2=Working, 3=Filled, 15=Complete)
137
- const status = order.status;
138
- if (status === 2 || status === 3 || order.notifyType === 15) {
139
- resolve({
140
- success: true,
141
- orderId: order.basketId,
142
- status,
143
- fillPrice: order.avgFillPrice || orderData.price,
144
- filledQty: order.totalFillSize || orderData.size,
145
- orderTag,
146
- });
147
- } else if (status === 5 || status === 6) {
148
- // Extract rejection reason from rpCode[1] or rqHandlerRpCode[1]
149
- const rpCode = order.rpCode;
150
- const rqCode = order.rqHandlerRpCode;
151
- let errorMsg = `Order rejected: status ${status}`;
152
-
153
- // Try rpCode first, then rqHandlerRpCode
154
- if (rpCode && Array.isArray(rpCode) && rpCode.length > 1 && rpCode[1]) {
155
- errorMsg = `Rejected: ${rpCode[1]}`;
156
- } else if (rqCode && Array.isArray(rqCode) && rqCode.length > 1 && rqCode[1]) {
157
- errorMsg = `Rejected: ${rqCode[1]}`;
158
- } else if (rpCode && Array.isArray(rpCode) && rpCode[0] && rpCode[0] !== '0') {
159
- errorMsg = `Rejected: code ${rpCode[0]}`;
160
- }
161
-
162
- resolve({
163
- success: false,
164
- error: errorMsg,
165
- orderId: order.basketId,
166
- orderTag,
167
- rpCode,
168
- rqHandlerRpCode: rqCode,
169
- });
137
+ resolve({
138
+ success: true,
139
+ orderId: order.basketId,
140
+ status: 3,
141
+ fillPrice: order.avgFillPrice || order.fillPrice || orderData.price,
142
+ filledQty: order.totalFillSize || order.fillSize || orderData.size,
143
+ orderTag,
144
+ });
145
+ return;
146
+ }
147
+
148
+ // REJECT (notifyType 6) - Order rejected
149
+ if (notifyType === 6) {
150
+ clearTimeout(timeout);
151
+ service.removeListener('orderNotification', onNotification);
152
+ resolve({
153
+ success: false,
154
+ error: order.text || 'Order rejected',
155
+ orderId: order.basketId,
156
+ orderTag,
157
+ });
158
+ return;
159
+ }
160
+
161
+ // STATUS (notifyType 1) with basketId - Order accepted by gateway
162
+ if (notifyType === 1 && order.basketId) {
163
+ // For market orders, wait a bit more for fill notification
164
+ // For limit orders, this is success (working)
165
+ if (orderData.type === 2) {
166
+ // Market order - don't resolve yet, wait for fill
167
+ return;
170
168
  }
169
+ clearTimeout(timeout);
170
+ service.removeListener('orderNotification', onNotification);
171
+ resolve({
172
+ success: true,
173
+ orderId: order.basketId,
174
+ status: 2,
175
+ fillPrice: orderData.price,
176
+ filledQty: 0,
177
+ orderTag,
178
+ });
179
+ return;
171
180
  }
172
181
  };
173
182
 
174
183
  service.on('orderNotification', onNotification);
175
184
 
176
- // HFT: Inline order request construction (no try-catch in hot path)
177
185
  const exchange = orderData.exchange || 'CME';
178
186
 
179
- // Get trade route from cache
187
+ // CRITICAL: Get trade route - orders WILL BE REJECTED without it
180
188
  let tradeRoute = null;
181
189
  const routes = service.tradeRoutes;
182
190
  if (routes && routes.size > 0) {
183
191
  const routeInfo = routes.get(exchange);
184
- tradeRoute = routeInfo ? routeInfo.tradeRoute :
185
- routes.values().next().value?.tradeRoute || null;
192
+ tradeRoute = routeInfo?.tradeRoute || routes.values().next().value?.tradeRoute;
186
193
  }
187
194
 
188
- // Warn if no trade route - Rithmic will reject the order
195
+ // FAIL FAST if no trade route
189
196
  if (!tradeRoute) {
190
- DEBUG && console.log('[Orders] WARNING: No trade route for', exchange, '- order may be rejected');
197
+ clearTimeout(timeout);
198
+ service.removeListener('orderNotification', onNotification);
199
+ console.log('[Orders] ERROR: No trade route for', exchange);
200
+ resolve({ success: false, error: `No trade route for ${exchange}. Login may be incomplete.`, orderTag });
201
+ return;
191
202
  }
192
203
 
193
- // HFT: Reuse template and mutate (faster than object spread)
204
+ // Build order request
194
205
  ORDER_REQUEST_TEMPLATE.userMsg[0] = orderTag;
195
206
  ORDER_REQUEST_TEMPLATE.fcmId = service.loginInfo.fcmId;
196
207
  ORDER_REQUEST_TEMPLATE.ibId = service.loginInfo.ibId;