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
|
@@ -449,20 +449,21 @@ const FillInfoPool = {
|
|
|
449
449
|
*/
|
|
450
450
|
fill(notif, receiveTime, latency) {
|
|
451
451
|
const o = this._template;
|
|
452
|
-
o.orderTag = notif.
|
|
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
|
-
|
|
460
|
-
o.
|
|
461
|
-
o.
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
const
|
|
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
|
-
|
|
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:
|
|
532
|
-
avgPrice:
|
|
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, //
|
|
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
|
|
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} |
|
|
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
|
|
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:
|
|
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, //
|
|
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' };
|