hedgequantx 2.6.37 → 2.6.39

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.6.37",
3
+ "version": "2.6.39",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -449,20 +449,21 @@ const FillInfoPool = {
449
449
  */
450
450
  fill(notif, receiveTime, latency) {
451
451
  const o = this._template;
452
- o.orderTag = notif.userMsg?.[0] || null;
452
+ o.orderTag = notif.userTag || null; // userTag contains our order tag
453
453
  o.basketId = notif.basketId;
454
- o.orderId = notif.orderId;
454
+ o.orderId = notif.exchangeOrderId || notif.orderId;
455
455
  o.status = notif.status;
456
456
  o.symbol = notif.symbol;
457
457
  o.exchange = notif.exchange;
458
458
  o.accountId = notif.accountId;
459
- o.fillQuantity = notif.fillQuantity || 0;
460
- o.totalFillQuantity = notif.totalFillQuantity || 0;
461
- o.remainingQuantity = notif.remainingQuantity || 0;
459
+ // Proto uses totalFillSize, not fillQuantity
460
+ o.fillQuantity = notif.totalFillSize || notif.fillQuantity || 0;
461
+ o.totalFillQuantity = notif.totalFillSize || notif.totalFillQuantity || 0;
462
+ o.remainingQuantity = notif.totalUnfilledSize || notif.remainingQuantity || 0;
462
463
  o.avgFillPrice = parseFloat(notif.avgFillPrice || 0);
463
- o.lastFillPrice = parseFloat(notif.fillPrice || 0);
464
+ o.lastFillPrice = parseFloat(notif.price || notif.fillPrice || 0);
464
465
  o.transactionType = notif.transactionType;
465
- o.orderType = notif.orderType;
466
+ o.orderType = notif.priceType || notif.orderType;
466
467
  o.quantity = notif.quantity;
467
468
  o.ssboe = notif.ssboe;
468
469
  o.usecs = notif.usecs;
@@ -495,11 +496,13 @@ const handleOrderNotification = (service, data) => {
495
496
 
496
497
  try {
497
498
  const notif = proto.decode('RithmicOrderNotification', data);
498
- const orderTag = notif.userMsg?.[0] || null;
499
+ // userTag contains our order tag (userMsg is not in this proto)
500
+ const orderTag = notif.userTag || null;
499
501
 
500
502
  // FAST PATH: Check for fill immediately
501
- const fillQty = notif.fillQuantity || notif.totalFillQuantity || 0;
502
- const isFill = fillQty > 0;
503
+ // Proto uses total_fill_size (camelCase: totalFillSize), not fillQuantity
504
+ const fillQty = notif.totalFillSize || notif.totalFillQuantity || notif.fillQuantity || 0;
505
+ const isFill = fillQty > 0 || notif.status === 'complete';
503
506
 
504
507
  // Calculate round-trip latency if this is a fill we're tracking
505
508
  let roundTripLatency = null;
@@ -521,15 +524,18 @@ const handleOrderNotification = (service, data) => {
521
524
  // Emit raw notification
522
525
  service.emit('orderNotification', fillInfo);
523
526
 
524
- // Emit fill event if this is a fill
527
+ // Emit fill event if this is a fill (status=complete means order is fully filled)
525
528
  if (isFill) {
526
- console.log(`[FILL] Received: ${orderTag} | ${fillInfo.transactionType === 1 ? 'BUY' : 'SELL'} ${fillQty}x @ ${fillInfo.avgFillPrice} | latency=${roundTripLatency}ms`);
529
+ const actualFillQty = fillInfo.totalFillQuantity || fillInfo.fillQuantity || notif.quantity || 0;
530
+ const fillPrice = fillInfo.avgFillPrice || fillInfo.lastFillPrice || 0;
531
+
532
+ console.log(`[FILL] Received: ${orderTag} | ${fillInfo.transactionType === 1 ? 'BUY' : 'SELL'} ${actualFillQty}x @ ${fillPrice} | latency=${roundTripLatency}ms`);
527
533
 
528
534
  debug('ORDER FILLED:', {
529
535
  orderTag,
530
536
  side: fillInfo.transactionType === 1 ? 'BUY' : 'SELL',
531
- qty: fillQty,
532
- avgPrice: fillInfo.avgFillPrice,
537
+ qty: actualFillQty,
538
+ avgPrice: fillPrice,
533
539
  latencyMs: roundTripLatency,
534
540
  });
535
541
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  const EventEmitter = require('events');
9
9
  const { RithmicConnection } = require('./connection');
10
- const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
10
+ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ } = require('./constants');
11
11
  const { createOrderHandler, createPnLHandler, LatencyTracker } = require('./handlers');
12
12
  const {
13
13
  fetchAccounts,
@@ -135,6 +135,7 @@ class RithmicService extends EventEmitter {
135
135
  this.completedTrades = []; // Store filled orders for trade history
136
136
  this.user = null;
137
137
  this.credentials = null;
138
+ this.tradeRoutes = new Map(); // exchange -> tradeRoute (from API)
138
139
 
139
140
  // Cache
140
141
  this._contractsCache = null;
@@ -175,6 +176,14 @@ class RithmicService extends EventEmitter {
175
176
  this.loginInfo = data;
176
177
  this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
177
178
 
179
+ // Fetch trade routes first (needed for orders)
180
+ try {
181
+ await this._fetchTradeRoutes();
182
+ log.debug('Fetched trade routes', { count: this.tradeRoutes.size });
183
+ } catch (err) {
184
+ log.warn('Failed to fetch trade routes', { error: err.message });
185
+ }
186
+
178
187
  // Fetch accounts
179
188
  try {
180
189
  await fetchAccounts(this);
@@ -183,6 +192,22 @@ class RithmicService extends EventEmitter {
183
192
  log.warn('Failed to fetch accounts', { error: err.message });
184
193
  }
185
194
 
195
+ // Subscribe to order updates for each account (required to receive fill notifications)
196
+ try {
197
+ for (const acc of this.accounts) {
198
+ this.orderConn.send('RequestSubscribeForOrderUpdates', {
199
+ templateId: REQ.ORDER_UPDATES,
200
+ userMsg: ['HQX'],
201
+ fcmId: acc.fcmId || data.fcmId,
202
+ ibId: acc.ibId || data.ibId,
203
+ accountId: acc.accountId,
204
+ });
205
+ log.debug('Subscribed to order updates for account', { accountId: acc.accountId });
206
+ }
207
+ } catch (err) {
208
+ log.warn('Failed to subscribe to order updates', { error: err.message });
209
+ }
210
+
186
211
  // Track open entries for P&L calculation
187
212
  this._openEntries = new Map(); // key: accountId:symbol, value: {side, qty, price}
188
213
 
@@ -381,6 +406,64 @@ class RithmicService extends EventEmitter {
381
406
  }
382
407
  }
383
408
 
409
+ // ==================== TRADE ROUTES ====================
410
+
411
+ /**
412
+ * Fetch trade routes from Rithmic API
413
+ * Trade routes are required for order submission
414
+ * @private
415
+ * @returns {Promise<void>}
416
+ */
417
+ async _fetchTradeRoutes() {
418
+ if (!this.orderConn || !this.loginInfo) {
419
+ return;
420
+ }
421
+
422
+ return new Promise((resolve) => {
423
+ const timeout = setTimeout(() => {
424
+ log.debug('Trade routes fetch timeout');
425
+ resolve();
426
+ }, 5000);
427
+
428
+ const onTradeRoute = (res) => {
429
+ if (res.tradeRoute && res.exchange) {
430
+ // Store trade route by exchange (from API)
431
+ this.tradeRoutes.set(res.exchange, res.tradeRoute);
432
+ log.debug('Trade route received', { exchange: res.exchange, route: res.tradeRoute });
433
+ }
434
+ if (res.rpCode?.[0] === '0' && !res.tradeRoute) {
435
+ // End of trade routes
436
+ clearTimeout(timeout);
437
+ this.removeListener('tradeRoutes', onTradeRoute);
438
+ resolve();
439
+ }
440
+ };
441
+
442
+ this.on('tradeRoutes', onTradeRoute);
443
+
444
+ try {
445
+ this.orderConn.send('RequestTradeRoutes', {
446
+ templateId: REQ.TRADE_ROUTES,
447
+ userMsg: ['HQX'],
448
+ subscribeForUpdates: true, // Required by Rithmic API
449
+ });
450
+ } catch (e) {
451
+ clearTimeout(timeout);
452
+ this.removeListener('tradeRoutes', onTradeRoute);
453
+ resolve();
454
+ }
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Get trade route for an exchange
460
+ * @param {string} exchange - Exchange name (e.g., 'CME', 'NYMEX')
461
+ * @returns {string|null} Trade route from API, or null if not available
462
+ */
463
+ getTradeRoute(exchange) {
464
+ return this.tradeRoutes.get(exchange) || null;
465
+ }
466
+
384
467
  // ==================== DELEGATED METHODS ====================
385
468
 
386
469
  async getTradingAccounts() { return getTradingAccounts(this); }
@@ -40,6 +40,7 @@ const OrderPool = {
40
40
  _template: {
41
41
  templateId: REQ.NEW_ORDER,
42
42
  userMsg: [''],
43
+ userTag: '', // Our order tag - returned in RithmicOrderNotification
43
44
  fcmId: '',
44
45
  ibId: '',
45
46
  accountId: '',
@@ -48,17 +49,22 @@ const OrderPool = {
48
49
  quantity: 0,
49
50
  transactionType: 1,
50
51
  duration: 1,
51
- priceType: 2, // FIXED: was 'orderType: 1' - priceType 2 = MARKET order
52
+ priceType: 2, // priceType 2 = MARKET order
52
53
  manualOrAuto: 2,
54
+ tradeRoute: '', // Required by Rithmic API - fetched from RequestTradeRoutes
53
55
  },
54
56
 
55
57
  /**
56
58
  * Get order object with values filled in
57
59
  * Reuses same object to avoid allocation
60
+ * @param {string} orderTag - Unique order tag
61
+ * @param {Object} loginInfo - { fcmId, ibId }
62
+ * @param {Object} orderData - { accountId, symbol, exchange, size, side, tradeRoute }
58
63
  */
59
64
  fill(orderTag, loginInfo, orderData) {
60
65
  const o = this._template;
61
66
  o.userMsg[0] = orderTag;
67
+ o.userTag = orderTag; // Set userTag for notification tracking
62
68
  o.fcmId = loginInfo.fcmId;
63
69
  o.ibId = loginInfo.ibId;
64
70
  o.accountId = orderData.accountId;
@@ -66,6 +72,7 @@ const OrderPool = {
66
72
  o.exchange = orderData.exchange || 'CME';
67
73
  o.quantity = orderData.size;
68
74
  o.transactionType = orderData.side === 0 ? 1 : 2;
75
+ o.tradeRoute = orderData.tradeRoute || ''; // From API via service.getTradeRoute()
69
76
  return o;
70
77
  }
71
78
  };
@@ -111,11 +118,25 @@ const fastEntry = (service, orderData) => {
111
118
  ibId: account?.ibId || service.loginInfo.ibId,
112
119
  };
113
120
 
121
+ // Get trade route from API (required by Rithmic)
122
+ const exchange = orderData.exchange || 'CME';
123
+ const tradeRoute = service.getTradeRoute?.(exchange);
124
+ if (!tradeRoute) {
125
+ return {
126
+ success: false,
127
+ error: `No trade route for exchange ${exchange}`,
128
+ orderTag,
129
+ entryTime,
130
+ latencyMs: performance.now() - startTime,
131
+ };
132
+ }
133
+
114
134
  // OPTIMIZED: Use pre-allocated order object
115
- const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderData);
135
+ const orderWithRoute = { ...orderData, tradeRoute };
136
+ const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderWithRoute);
116
137
 
117
138
  // DEBUG: Log order details
118
- console.log(`[ORDER] Sending: ${orderTag} | ${orderData.side === 0 ? 'BUY' : 'SELL'} ${orderData.size}x ${orderData.symbol} | acct=${orderData.accountId} | fcm=${effectiveLoginInfo.fcmId} ib=${effectiveLoginInfo.ibId}`);
139
+ console.log(`[ORDER] Sending: ${orderTag} | ${orderData.side === 0 ? 'BUY' : 'SELL'} ${orderData.size}x ${orderData.symbol} | acct=${orderData.accountId} | route=${tradeRoute}`);
119
140
 
120
141
  // OPTIMIZED: Use fastEncode with cached type
121
142
  const buffer = proto.fastEncode('RequestNewOrder', order);
@@ -185,8 +206,22 @@ const fastExit = (service, orderData) => {
185
206
  ibId: account?.ibId || service.loginInfo.ibId,
186
207
  };
187
208
 
209
+ // Get trade route from API (required by Rithmic)
210
+ const exchange = orderData.exchange || 'CME';
211
+ const tradeRoute = service.getTradeRoute?.(exchange);
212
+ if (!tradeRoute) {
213
+ return {
214
+ success: false,
215
+ error: `No trade route for exchange ${exchange}`,
216
+ orderTag,
217
+ exitTime,
218
+ latencyMs: performance.now() - startTime,
219
+ };
220
+ }
221
+
188
222
  // OPTIMIZED: Use pre-allocated order object
189
- const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderData);
223
+ const orderWithRoute = { ...orderData, tradeRoute };
224
+ const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderWithRoute);
190
225
 
191
226
  // OPTIMIZED: Use fastEncode with cached type
192
227
  const buffer = proto.fastEncode('RequestNewOrder', order);
@@ -233,6 +268,13 @@ const placeOrder = async (service, orderData) => {
233
268
  a.accountId === orderData.accountId || a.rithmicAccountId === orderData.accountId
234
269
  );
235
270
 
271
+ // Get trade route from API (required by Rithmic)
272
+ const exchange = orderData.exchange || 'CME';
273
+ const tradeRoute = service.getTradeRoute?.(exchange);
274
+ if (!tradeRoute) {
275
+ return { success: false, error: `No trade route for exchange ${exchange}` };
276
+ }
277
+
236
278
  service.orderConn.send('RequestNewOrder', {
237
279
  templateId: REQ.NEW_ORDER,
238
280
  userMsg: ['HQX'],
@@ -240,12 +282,13 @@ const placeOrder = async (service, orderData) => {
240
282
  ibId: account?.ibId || service.loginInfo.ibId,
241
283
  accountId: orderData.accountId,
242
284
  symbol: orderData.symbol,
243
- exchange: orderData.exchange || 'CME',
285
+ exchange: exchange,
244
286
  quantity: orderData.size,
245
287
  transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
246
288
  duration: 1, // DAY
247
- priceType: orderData.type === 2 ? 2 : 1, // FIXED: was 'orderType' with inverted logic. priceType: 1=Limit, 2=Market
289
+ priceType: orderData.type === 2 ? 2 : 1, // priceType: 1=Limit, 2=Market
248
290
  price: orderData.price || 0,
291
+ tradeRoute: tradeRoute, // Required by Rithmic API - fetched from RequestTradeRoutes
249
292
  });
250
293
 
251
294
  return { success: true, message: 'Order submitted' };