hedgequantx 2.6.162 → 2.6.163

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.
@@ -13,6 +13,7 @@
13
13
 
14
14
  const EventEmitter = require('events');
15
15
  const { logger } = require('../../utils/logger');
16
+ const { decodeLastTrade, decodeBestBidOffer } = require('./market-data-decoders');
16
17
 
17
18
  const log = logger.scope('RithmicMD');
18
19
 
@@ -25,284 +26,6 @@ const TEMPLATE_IDS = {
25
26
  BEST_BID_OFFER: 151,
26
27
  };
27
28
 
28
- // Rithmic field IDs for LastTrade (from protobuf)
29
- const LAST_TRADE_FIELDS = {
30
- TEMPLATE_ID: 154467,
31
- SYMBOL: 110100,
32
- EXCHANGE: 110101,
33
- TRADE_PRICE: 100006,
34
- TRADE_SIZE: 100178,
35
- AGGRESSOR: 112003, // 1=BUY, 2=SELL
36
- SSBOE: 150100,
37
- USECS: 150101,
38
- };
39
-
40
- // Rithmic field IDs for BestBidOffer (from protobuf)
41
- const BBO_FIELDS = {
42
- TEMPLATE_ID: 154467,
43
- SYMBOL: 110100,
44
- EXCHANGE: 110101,
45
- BID_PRICE: 100022,
46
- BID_SIZE: 100030,
47
- ASK_PRICE: 100025,
48
- ASK_SIZE: 100031,
49
- SSBOE: 150100,
50
- USECS: 150101,
51
- };
52
-
53
- /**
54
- * Read a varint from buffer starting at offset
55
- * Uses BigInt internally to handle large field IDs correctly
56
- * @param {Buffer} buffer
57
- * @param {number} offset
58
- * @returns {[number, number]} [value, newOffset]
59
- */
60
- function readVarint(buffer, offset) {
61
- let result = BigInt(0);
62
- let shift = BigInt(0);
63
- let pos = offset;
64
-
65
- while (pos < buffer.length) {
66
- const byte = buffer[pos++];
67
- result |= BigInt(byte & 0x7f) << shift;
68
- if ((byte & 0x80) === 0) {
69
- return [Number(result), pos];
70
- }
71
- shift += BigInt(7);
72
- if (shift > BigInt(63)) {
73
- throw new Error('Varint too large');
74
- }
75
- }
76
-
77
- throw new Error('Incomplete varint');
78
- }
79
-
80
- /**
81
- * Read a length-delimited field (string/bytes)
82
- * @param {Buffer} buffer
83
- * @param {number} offset
84
- * @returns {[string, number]} [value, newOffset]
85
- */
86
- function readLengthDelimited(buffer, offset) {
87
- const [length, newOffset] = readVarint(buffer, offset);
88
- const value = buffer.slice(newOffset, newOffset + length).toString('utf8');
89
- return [value, newOffset + length];
90
- }
91
-
92
- /**
93
- * Skip a field based on wire type
94
- * @param {Buffer} buffer
95
- * @param {number} offset
96
- * @param {number} wireType
97
- * @returns {number} newOffset
98
- */
99
- function skipField(buffer, offset, wireType) {
100
- switch (wireType) {
101
- case 0: // Varint
102
- const [, newOffset] = readVarint(buffer, offset);
103
- return newOffset;
104
- case 1: // 64-bit
105
- return offset + 8;
106
- case 2: // Length-delimited
107
- const [length, lenOffset] = readVarint(buffer, offset);
108
- return lenOffset + length;
109
- case 5: // 32-bit
110
- return offset + 4;
111
- default:
112
- throw new Error(`Unknown wire type: ${wireType}`);
113
- }
114
- }
115
-
116
- /**
117
- * Manually decode LastTrade message from Rithmic
118
- * Required because protobufjs can't handle field IDs > 100000
119
- * @param {Buffer} buffer
120
- * @returns {Object}
121
- */
122
- function decodeLastTrade(buffer) {
123
- const result = {};
124
- let offset = 0;
125
-
126
- while (offset < buffer.length) {
127
- try {
128
- const [tag, newOffset] = readVarint(buffer, offset);
129
- const fieldNumber = tag >>> 3;
130
- const wireType = tag & 0x7;
131
- offset = newOffset;
132
-
133
- switch (fieldNumber) {
134
- case LAST_TRADE_FIELDS.SYMBOL:
135
- if (wireType === 2) {
136
- const [val, next] = readLengthDelimited(buffer, offset);
137
- result.symbol = val;
138
- offset = next;
139
- } else {
140
- offset = skipField(buffer, offset, wireType);
141
- }
142
- break;
143
- case LAST_TRADE_FIELDS.EXCHANGE:
144
- if (wireType === 2) {
145
- const [val, next] = readLengthDelimited(buffer, offset);
146
- result.exchange = val;
147
- offset = next;
148
- } else {
149
- offset = skipField(buffer, offset, wireType);
150
- }
151
- break;
152
- case LAST_TRADE_FIELDS.TRADE_PRICE:
153
- if (wireType === 1) {
154
- result.tradePrice = buffer.readDoubleLE(offset);
155
- offset += 8;
156
- } else {
157
- offset = skipField(buffer, offset, wireType);
158
- }
159
- break;
160
- case LAST_TRADE_FIELDS.TRADE_SIZE:
161
- if (wireType === 0) {
162
- const [val, next] = readVarint(buffer, offset);
163
- result.tradeSize = val;
164
- offset = next;
165
- } else {
166
- offset = skipField(buffer, offset, wireType);
167
- }
168
- break;
169
- case LAST_TRADE_FIELDS.AGGRESSOR:
170
- if (wireType === 0) {
171
- const [val, next] = readVarint(buffer, offset);
172
- result.aggressor = val;
173
- offset = next;
174
- } else {
175
- offset = skipField(buffer, offset, wireType);
176
- }
177
- break;
178
- case LAST_TRADE_FIELDS.SSBOE:
179
- if (wireType === 0) {
180
- const [val, next] = readVarint(buffer, offset);
181
- result.ssboe = val;
182
- offset = next;
183
- } else {
184
- offset = skipField(buffer, offset, wireType);
185
- }
186
- break;
187
- case LAST_TRADE_FIELDS.USECS:
188
- if (wireType === 0) {
189
- const [val, next] = readVarint(buffer, offset);
190
- result.usecs = val;
191
- offset = next;
192
- } else {
193
- offset = skipField(buffer, offset, wireType);
194
- }
195
- break;
196
- default:
197
- offset = skipField(buffer, offset, wireType);
198
- }
199
- } catch {
200
- break;
201
- }
202
- }
203
-
204
- return result;
205
- }
206
-
207
- /**
208
- * Manually decode BestBidOffer message from Rithmic
209
- * Required because protobufjs can't handle field IDs > 100000
210
- * @param {Buffer} buffer
211
- * @returns {Object}
212
- */
213
- function decodeBestBidOffer(buffer) {
214
- const result = {};
215
- let offset = 0;
216
-
217
- while (offset < buffer.length) {
218
- try {
219
- const [tag, newOffset] = readVarint(buffer, offset);
220
- const fieldNumber = tag >>> 3;
221
- const wireType = tag & 0x7;
222
- offset = newOffset;
223
-
224
- switch (fieldNumber) {
225
- case BBO_FIELDS.SYMBOL:
226
- if (wireType === 2) {
227
- const [val, next] = readLengthDelimited(buffer, offset);
228
- result.symbol = val;
229
- offset = next;
230
- } else {
231
- offset = skipField(buffer, offset, wireType);
232
- }
233
- break;
234
- case BBO_FIELDS.EXCHANGE:
235
- if (wireType === 2) {
236
- const [val, next] = readLengthDelimited(buffer, offset);
237
- result.exchange = val;
238
- offset = next;
239
- } else {
240
- offset = skipField(buffer, offset, wireType);
241
- }
242
- break;
243
- case BBO_FIELDS.BID_PRICE:
244
- if (wireType === 1) {
245
- result.bidPrice = buffer.readDoubleLE(offset);
246
- offset += 8;
247
- } else {
248
- offset = skipField(buffer, offset, wireType);
249
- }
250
- break;
251
- case BBO_FIELDS.BID_SIZE:
252
- if (wireType === 0) {
253
- const [val, next] = readVarint(buffer, offset);
254
- result.bidSize = val;
255
- offset = next;
256
- } else {
257
- offset = skipField(buffer, offset, wireType);
258
- }
259
- break;
260
- case BBO_FIELDS.ASK_PRICE:
261
- if (wireType === 1) {
262
- result.askPrice = buffer.readDoubleLE(offset);
263
- offset += 8;
264
- } else {
265
- offset = skipField(buffer, offset, wireType);
266
- }
267
- break;
268
- case BBO_FIELDS.ASK_SIZE:
269
- if (wireType === 0) {
270
- const [val, next] = readVarint(buffer, offset);
271
- result.askSize = val;
272
- offset = next;
273
- } else {
274
- offset = skipField(buffer, offset, wireType);
275
- }
276
- break;
277
- case BBO_FIELDS.SSBOE:
278
- if (wireType === 0) {
279
- const [val, next] = readVarint(buffer, offset);
280
- result.ssboe = val;
281
- offset = next;
282
- } else {
283
- offset = skipField(buffer, offset, wireType);
284
- }
285
- break;
286
- case BBO_FIELDS.USECS:
287
- if (wireType === 0) {
288
- const [val, next] = readVarint(buffer, offset);
289
- result.usecs = val;
290
- offset = next;
291
- } else {
292
- offset = skipField(buffer, offset, wireType);
293
- }
294
- break;
295
- default:
296
- offset = skipField(buffer, offset, wireType);
297
- }
298
- } catch {
299
- break;
300
- }
301
- }
302
-
303
- return result;
304
- }
305
-
306
29
  /**
307
30
  * Rithmic Market Data Feed
308
31
  * Provides real-time market data via Rithmic WebSocket connection
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Rithmic Fast Orders Module
3
+ * @module services/rithmic/orders-fast
4
+ *
5
+ * Ultra-low latency order entry/exit for scalping
6
+ * Target: < 5ms local processing (network latency separate)
7
+ *
8
+ * OPTIMIZATIONS:
9
+ * - Pre-allocated order template objects
10
+ * - Fast orderTag generation (no Date.now in hot path)
11
+ * - Direct proto encoding with cached types
12
+ * - Minimal object creation
13
+ */
14
+
15
+ const { REQ } = require('./constants');
16
+ const { proto } = require('./protobuf');
17
+ const { LatencyTracker } = require('./latency-tracker');
18
+ const { performance } = require('perf_hooks');
19
+
20
+ // Debug mode - use no-op function when disabled for zero overhead
21
+ const DEBUG = process.env.HQX_DEBUG === '1';
22
+ const debug = DEBUG ? (...args) => console.log('[Rithmic:Orders]', ...args) : () => {};
23
+
24
+ // ==================== FAST ORDER TAG ====================
25
+ // Pre-generate prefix once at module load (not per-order)
26
+ const ORDER_TAG_PREFIX = `HQX${process.pid}-`;
27
+ let orderIdCounter = 0;
28
+
29
+ /**
30
+ * Ultra-fast order tag generation
31
+ * Avoids Date.now() and string interpolation in hot path
32
+ * @returns {string}
33
+ */
34
+ const generateOrderTag = () => ORDER_TAG_PREFIX + (++orderIdCounter);
35
+
36
+ // ==================== PRE-ALLOCATED ORDER TEMPLATES ====================
37
+
38
+ /**
39
+ * Order object pool for zero-allocation hot path
40
+ */
41
+ const OrderPool = {
42
+ // Pre-allocated order template
43
+ _template: {
44
+ templateId: REQ.NEW_ORDER,
45
+ userMsg: [''],
46
+ userTag: '',
47
+ fcmId: '',
48
+ ibId: '',
49
+ accountId: '',
50
+ symbol: '',
51
+ exchange: 'CME',
52
+ quantity: 0,
53
+ transactionType: 1,
54
+ duration: 1,
55
+ priceType: 2, // priceType 2 = MARKET order
56
+ manualOrAuto: 2,
57
+ tradeRoute: '',
58
+ },
59
+
60
+ /**
61
+ * Get order object with values filled in
62
+ * @param {string} orderTag
63
+ * @param {Object} loginInfo - { fcmId, ibId }
64
+ * @param {Object} orderData - { accountId, symbol, exchange, size, side, tradeRoute }
65
+ */
66
+ fill(orderTag, loginInfo, orderData) {
67
+ const o = this._template;
68
+ o.userMsg[0] = orderTag;
69
+ o.userTag = orderTag;
70
+ o.fcmId = loginInfo.fcmId;
71
+ o.ibId = loginInfo.ibId;
72
+ o.accountId = orderData.accountId;
73
+ o.symbol = orderData.symbol;
74
+ o.exchange = orderData.exchange || 'CME';
75
+ o.quantity = orderData.size;
76
+ o.transactionType = orderData.side === 0 ? 1 : 2;
77
+ o.tradeRoute = orderData.tradeRoute || '';
78
+ return o;
79
+ }
80
+ };
81
+
82
+ /**
83
+ * Get effective login info for account
84
+ * @param {RithmicService} service
85
+ * @param {string} accountId
86
+ * @returns {{ fcmId: string, ibId: string }}
87
+ */
88
+ function getEffectiveLoginInfo(service, accountId) {
89
+ const account = service.accounts?.find(a =>
90
+ a.accountId === accountId || a.rithmicAccountId === accountId
91
+ );
92
+ return {
93
+ fcmId: account?.fcmId || service.loginInfo.fcmId,
94
+ ibId: account?.ibId || service.loginInfo.ibId,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Send order buffer via optimized path
100
+ * @param {Object} conn - Order connection
101
+ * @param {Buffer} buffer
102
+ */
103
+ function sendOrderBuffer(conn, buffer) {
104
+ const sent = conn.ultraSend
105
+ ? conn.ultraSend(buffer)
106
+ : (conn.fastSend(buffer), true);
107
+
108
+ if (!sent) {
109
+ conn.fastSend(buffer);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Ultra-fast market order entry - HOT PATH
115
+ * NO SL/TP, NO await confirmation, fire-and-forget
116
+ *
117
+ * @param {RithmicService} service
118
+ * @param {Object} orderData - { accountId, symbol, exchange, size, side }
119
+ * @returns {{ success: boolean, orderTag: string, entryTime: number, latencyMs: number }}
120
+ */
121
+ const fastEntry = (service, orderData) => {
122
+ const startTime = performance.now();
123
+ const orderTag = generateOrderTag();
124
+ const entryTime = Date.now();
125
+
126
+ if (!service.orderConn?.isConnected || !service.loginInfo) {
127
+ return {
128
+ success: false,
129
+ error: 'Not connected',
130
+ orderTag,
131
+ entryTime,
132
+ latencyMs: performance.now() - startTime,
133
+ };
134
+ }
135
+
136
+ try {
137
+ const effectiveLoginInfo = getEffectiveLoginInfo(service, orderData.accountId);
138
+
139
+ const exchange = orderData.exchange || 'CME';
140
+ const tradeRoute = service.getTradeRoute?.(exchange);
141
+ if (!tradeRoute) {
142
+ return {
143
+ success: false,
144
+ error: `No trade route for exchange ${exchange}`,
145
+ orderTag,
146
+ entryTime,
147
+ latencyMs: performance.now() - startTime,
148
+ };
149
+ }
150
+
151
+ const orderWithRoute = { ...orderData, tradeRoute };
152
+ const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderWithRoute);
153
+
154
+ debug('ORDER Sending:', orderTag, orderData.side === 0 ? 'BUY' : 'SELL', orderData.size, 'x', orderData.symbol);
155
+
156
+ const buffer = proto.fastEncode('RequestNewOrder', order);
157
+ sendOrderBuffer(service.orderConn, buffer);
158
+
159
+ debug('ORDER Sent to Rithmic:', orderTag, 'buffer:', buffer.length, 'bytes');
160
+
161
+ LatencyTracker.recordEntry(orderTag, entryTime);
162
+
163
+ return {
164
+ success: true,
165
+ orderTag,
166
+ entryTime,
167
+ latencyMs: performance.now() - startTime,
168
+ };
169
+ } catch (error) {
170
+ return {
171
+ success: false,
172
+ error: error.message,
173
+ orderTag,
174
+ entryTime,
175
+ latencyMs: performance.now() - startTime,
176
+ };
177
+ }
178
+ };
179
+
180
+ /**
181
+ * Ultra-fast market exit - for position closing
182
+ *
183
+ * @param {RithmicService} service
184
+ * @param {Object} orderData - { accountId, symbol, exchange, size, side }
185
+ * @returns {{ success: boolean, orderTag: string, exitTime: number, latencyMs: number }}
186
+ */
187
+ const fastExit = (service, orderData) => {
188
+ const startTime = performance.now();
189
+ const orderTag = generateOrderTag();
190
+ const exitTime = Date.now();
191
+
192
+ if (!service.orderConn?.isConnected || !service.loginInfo) {
193
+ return {
194
+ success: false,
195
+ error: 'Not connected',
196
+ orderTag,
197
+ exitTime,
198
+ latencyMs: performance.now() - startTime,
199
+ };
200
+ }
201
+
202
+ try {
203
+ const effectiveLoginInfo = getEffectiveLoginInfo(service, orderData.accountId);
204
+
205
+ const exchange = orderData.exchange || 'CME';
206
+ const tradeRoute = service.getTradeRoute?.(exchange);
207
+ if (!tradeRoute) {
208
+ return {
209
+ success: false,
210
+ error: `No trade route for exchange ${exchange}`,
211
+ orderTag,
212
+ exitTime,
213
+ latencyMs: performance.now() - startTime,
214
+ };
215
+ }
216
+
217
+ const orderWithRoute = { ...orderData, tradeRoute };
218
+ const order = OrderPool.fill(orderTag, effectiveLoginInfo, orderWithRoute);
219
+
220
+ const buffer = proto.fastEncode('RequestNewOrder', order);
221
+ sendOrderBuffer(service.orderConn, buffer);
222
+
223
+ return {
224
+ success: true,
225
+ orderTag,
226
+ exitTime,
227
+ latencyMs: performance.now() - startTime,
228
+ };
229
+ } catch (error) {
230
+ return {
231
+ success: false,
232
+ error: error.message,
233
+ orderTag,
234
+ exitTime,
235
+ latencyMs: performance.now() - startTime,
236
+ };
237
+ }
238
+ };
239
+
240
+ module.exports = {
241
+ generateOrderTag,
242
+ OrderPool,
243
+ getEffectiveLoginInfo,
244
+ fastEntry,
245
+ fastExit,
246
+ };