hedgequantx 2.9.237 → 2.9.238

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.237",
3
+ "version": "2.9.238",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * Algo Executor - Execution engine for algo modes with AI supervision
3
+ *
4
+ * HFT-GRADE OPTIMIZATIONS:
5
+ * - CircularBuffer for O(1) tick storage (replaces O(n) shift())
6
+ * - Pre-allocated tick structure pool
7
+ * - Cached timestamp operations
3
8
  */
4
9
  const readline = require('readline');
5
10
  const { AlgoUI, renderSessionSummary } = require('./ui');
@@ -9,6 +14,7 @@ const { SupervisionEngine } = require('../../services/ai-supervision');
9
14
  const smartLogs = require('../../lib/smart-logs');
10
15
  const { createEngine: createLogsEngine } = require('../../lib/smart-logs-engine');
11
16
  const { sessionLogger } = require('../../services/session-logger');
17
+ const { CircularBuffer, Float64CircularBuffer, timestampCache } = require('../../lib/hft');
12
18
 
13
19
  /**
14
20
  * Execute algo strategy with market data
@@ -54,7 +60,16 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
54
60
  let running = true, stopReason = null, startingPnL = null;
55
61
  let currentPosition = 0, pendingOrder = false, tickCount = 0, lastBias = 'FLAT';
56
62
 
57
- const aiContext = { recentTicks: [], recentSignals: [], recentTrades: [], maxTicks: 100 };
63
+ // HFT: Use CircularBuffer for O(1) tick storage (replaces O(n) shift())
64
+ const recentTicksBuffer = new CircularBuffer(100);
65
+ const recentSignalsBuffer = new CircularBuffer(10);
66
+ const recentTradesBuffer = new CircularBuffer(20);
67
+ const aiContext = {
68
+ recentTicks: recentTicksBuffer, // CircularBuffer - use .recent(n) to get array
69
+ recentSignals: recentSignalsBuffer, // CircularBuffer
70
+ recentTrades: recentTradesBuffer, // CircularBuffer
71
+ maxTicks: 100
72
+ };
58
73
 
59
74
  const strategy = new StrategyClass({ tickSize });
60
75
  strategy.initialize(contractId, tickSize);
@@ -129,8 +144,8 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
129
144
  let { direction, entry, stopLoss, takeProfit, confidence } = signal;
130
145
  let orderSize = contracts;
131
146
 
132
- aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
133
- if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
147
+ // HFT: O(1) push with automatic overflow handling
148
+ aiContext.recentSignals.push({ ...signal, timestamp: timestampCache.now() });
134
149
 
135
150
  const riskLog = smartLogs.getRiskCheckLog(true, `${direction.toUpperCase()} @ ${entry.toFixed(2)}`);
136
151
  ui.addLog('risk', `${riskLog.message} - ${riskLog.details}`);
@@ -139,12 +154,13 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
139
154
  if (supervisionEnabled && supervisionEngine) {
140
155
  ui.addLog('analysis', 'AI analyzing signal...');
141
156
 
157
+ // HFT: Extract arrays from CircularBuffers for AI supervision
142
158
  const supervisionResult = await supervisionEngine.supervise({
143
159
  symbolId: symbolName,
144
160
  signal: { direction, entry, stopLoss, takeProfit, confidence, size: contracts },
145
- recentTicks: aiContext.recentTicks,
146
- recentSignals: aiContext.recentSignals,
147
- recentTrades: aiContext.recentTrades,
161
+ recentTicks: aiContext.recentTicks.recent(aiContext.maxTicks),
162
+ recentSignals: aiContext.recentSignals.recent(10),
163
+ recentTrades: aiContext.recentTrades.recent(20),
148
164
  stats,
149
165
  config: { dailyTarget, maxRisk }
150
166
  });
@@ -228,7 +244,9 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
228
244
  let lastPrice = null, lastBid = null, lastAsk = null;
229
245
  let ticksPerSecond = 0, lastTickSecond = Math.floor(Date.now() / 1000);
230
246
  let lastBiasLogSecond = 0, lastStateLogSecond = 0;
231
- let buyVolume = 0, sellVolume = 0, lastTickTime = 0, tickLatencies = [];
247
+ // HFT: Float64CircularBuffer for O(1) latency statistics
248
+ let buyVolume = 0, sellVolume = 0, lastTickTime = 0;
249
+ const tickLatencies = new Float64CircularBuffer(20);
232
250
  let runningDelta = 0, runningBuyPct = 50; // For live logs
233
251
 
234
252
  marketFeed.on('tick', (tick) => {
@@ -250,8 +268,8 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
250
268
  lastTickSecond = currentSecond;
251
269
  }
252
270
 
271
+ // HFT: O(1) push with automatic overflow handling (no shift needed)
253
272
  aiContext.recentTicks.push(tick);
254
- if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
255
273
 
256
274
  const price = Number(tick.price) || Number(tick.tradePrice) || null;
257
275
  const bid = Number(tick.bid) || Number(tick.bidPrice) || null;
@@ -314,6 +332,7 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
314
332
  }
315
333
 
316
334
  // Calculate latency from Rithmic ssboe/usecs or inter-tick timing
335
+ // HFT: Use cached timestamp and Float64CircularBuffer for O(1) mean calculation
317
336
  if (tick.ssboe && tick.usecs !== undefined) {
318
337
  const tickTimeMs = (tick.ssboe * 1000) + Math.floor(tick.usecs / 1000);
319
338
  const latency = now - tickTimeMs;
@@ -322,8 +341,8 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
322
341
  const timeSinceLastTick = now - lastTickTime;
323
342
  if (timeSinceLastTick < 100) {
324
343
  tickLatencies.push(timeSinceLastTick);
325
- if (tickLatencies.length > 20) tickLatencies.shift();
326
- stats.latency = Math.round(tickLatencies.reduce((a, b) => a + b, 0) / tickLatencies.length);
344
+ // HFT: O(1) mean calculation (pre-computed running sum)
345
+ stats.latency = Math.round(tickLatencies.mean());
327
346
  }
328
347
  }
329
348
  lastTickTime = now;
@@ -1,16 +1,42 @@
1
1
  /**
2
2
  * Rithmic Message Handlers
3
3
  * Handles ORDER_PLANT and PNL_PLANT messages
4
+ *
5
+ * HFT-GRADE OPTIMIZATIONS:
6
+ * - Pre-allocated result objects
7
+ * - Minimal object creation in hot paths
8
+ * - No debug logging in production
4
9
  */
5
10
 
6
11
  const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
7
12
  const { RES, STREAM } = require('./constants');
8
13
  const { sanitizeQuantity } = require('./protobuf-utils');
9
14
 
10
- // Debug mode - DISABLED to avoid polluting interactive UI
11
- // Use session logs instead for debugging
15
+ // HFT: Debug completely disabled - no function call overhead
12
16
  const DEBUG = false;
13
- const debug = (...args) => {};
17
+ const debug = DEBUG ? (...args) => {} : () => {};
18
+
19
+ // HFT: Pre-allocated objects for hot path handlers
20
+ const _pnlDataTemplate = {
21
+ accountBalance: 0,
22
+ cashOnHand: 0,
23
+ marginBalance: 0,
24
+ openPositionPnl: 0,
25
+ closedPositionPnl: 0,
26
+ dayPnl: 0,
27
+ };
28
+
29
+ const _positionTemplate = {
30
+ accountId: '',
31
+ symbol: '',
32
+ exchange: 'CME',
33
+ quantity: 0,
34
+ averagePrice: 0,
35
+ openPnl: 0,
36
+ closedPnl: 0,
37
+ dayPnl: 0,
38
+ isSnapshot: false,
39
+ };
14
40
 
15
41
  /**
16
42
  * Create ORDER_PLANT message handler
@@ -219,71 +245,84 @@ const handleNewOrderResponse = (service, data) => {
219
245
 
220
246
  /**
221
247
  * Handle account PnL update
248
+ * HFT: Optimized for minimal allocations
222
249
  */
223
250
  const handleAccountPnLUpdate = (service, data) => {
224
- try {
225
- const pnl = decodeAccountPnL(data);
226
- debug('Decoded Account PNL:', JSON.stringify(pnl));
227
-
228
- if (pnl.accountId) {
229
- const pnlData = {
230
- accountBalance: parseFloat(pnl.accountBalance || 0),
231
- cashOnHand: parseFloat(pnl.cashOnHand || 0),
232
- marginBalance: parseFloat(pnl.marginBalance || 0),
233
- openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
234
- closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
235
- dayPnl: parseFloat(pnl.dayPnl || 0),
251
+ const pnl = decodeAccountPnL(data);
252
+
253
+ if (pnl.accountId) {
254
+ // HFT: Reuse cached object from Map or create once
255
+ let pnlData = service.accountPnL.get(pnl.accountId);
256
+ if (!pnlData) {
257
+ pnlData = {
258
+ accountBalance: 0,
259
+ cashOnHand: 0,
260
+ marginBalance: 0,
261
+ openPositionPnl: 0,
262
+ closedPositionPnl: 0,
263
+ dayPnl: 0,
236
264
  };
237
- debug('Storing PNL for account:', pnl.accountId, pnlData);
238
265
  service.accountPnL.set(pnl.accountId, pnlData);
239
- service.emit('pnlUpdate', pnl);
240
- } else {
241
- debug('No accountId in PNL response');
242
266
  }
243
- } catch (e) {
244
- debug('Error decoding Account PNL:', e.message);
267
+
268
+ // HFT: Mutate existing object instead of creating new
269
+ pnlData.accountBalance = parseFloat(pnl.accountBalance || 0);
270
+ pnlData.cashOnHand = parseFloat(pnl.cashOnHand || 0);
271
+ pnlData.marginBalance = parseFloat(pnl.marginBalance || 0);
272
+ pnlData.openPositionPnl = parseFloat(pnl.openPositionPnl || 0);
273
+ pnlData.closedPositionPnl = parseFloat(pnl.closedPositionPnl || 0);
274
+ pnlData.dayPnl = parseFloat(pnl.dayPnl || 0);
275
+
276
+ service.emit('pnlUpdate', pnl);
245
277
  }
246
278
  };
247
279
 
248
280
  /**
249
281
  * Handle instrument PnL update (positions)
282
+ * HFT: Optimized for minimal allocations - reuses position objects
250
283
  */
251
284
  const handleInstrumentPnLUpdate = (service, data) => {
252
- try {
253
- const pos = decodeInstrumentPnL(data);
254
- if (pos.symbol && pos.accountId) {
255
- const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
256
-
257
- // CRITICAL: Sanitize quantity to prevent overflow (18446744073709552000 bug)
258
- const rawQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
259
- const netQty = sanitizeQuantity(rawQty);
260
-
261
- if (netQty !== 0) {
262
- service.positions.set(key, {
263
- accountId: pos.accountId,
264
- symbol: pos.symbol,
265
- exchange: pos.exchange || 'CME',
266
- quantity: netQty,
267
- averagePrice: pos.avgOpenFillPrice || 0,
268
- openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
269
- closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
270
- dayPnl: parseFloat(pos.dayPnl || 0),
271
- isSnapshot: pos.isSnapshot || false,
272
- });
273
- } else {
274
- service.positions.delete(key);
285
+ const pos = decodeInstrumentPnL(data);
286
+
287
+ if (pos.symbol && pos.accountId) {
288
+ const key = `${pos.accountId}:${pos.symbol}:${pos.exchange || 'CME'}`;
289
+
290
+ // CRITICAL: Sanitize quantity to prevent overflow (18446744073709552000 bug)
291
+ const rawQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
292
+ const netQty = sanitizeQuantity(rawQty);
293
+
294
+ if (netQty !== 0) {
295
+ // HFT: Reuse existing position object or create once
296
+ let position = service.positions.get(key);
297
+ if (!position) {
298
+ position = {
299
+ accountId: '',
300
+ symbol: '',
301
+ exchange: 'CME',
302
+ quantity: 0,
303
+ averagePrice: 0,
304
+ openPnl: 0,
305
+ closedPnl: 0,
306
+ dayPnl: 0,
307
+ isSnapshot: false,
308
+ };
309
+ service.positions.set(key, position);
275
310
  }
276
311
 
277
- // Only emit if position exists (not deleted)
278
- const currentPos = service.positions.get(key);
279
- if (currentPos) {
280
- service.emit('positionUpdate', currentPos);
281
- }
282
- }
283
- } catch (e) {
284
- // Log decode errors for debugging (but don't pollute UI)
285
- if (process.env.HQX_DEBUG === '1') {
286
- console.error('[Handler] Position decode error:', e.message);
312
+ // HFT: Mutate existing object
313
+ position.accountId = pos.accountId;
314
+ position.symbol = pos.symbol;
315
+ position.exchange = pos.exchange || 'CME';
316
+ position.quantity = netQty;
317
+ position.averagePrice = pos.avgOpenFillPrice || 0;
318
+ position.openPnl = parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0);
319
+ position.closedPnl = parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0);
320
+ position.dayPnl = parseFloat(pos.dayPnl || 0);
321
+ position.isSnapshot = pos.isSnapshot || false;
322
+
323
+ service.emit('positionUpdate', position);
324
+ } else {
325
+ service.positions.delete(key);
287
326
  }
288
327
  }
289
328
  };
@@ -2,13 +2,18 @@
2
2
  * Rithmic Orders Module
3
3
  * Order placement, cancellation, and history
4
4
  *
5
+ * HFT-GRADE OPTIMIZATIONS:
6
+ * - Pre-allocated order request template
7
+ * - Cached timestamp for order tags
8
+ * - Zero console.log in production path
9
+ *
5
10
  * @module services/rithmic/orders
6
11
  */
7
12
 
8
13
  const { REQ } = require('./constants');
9
14
  const { sanitizeQuantity, MAX_SAFE_QUANTITY } = require('./protobuf-utils');
10
15
 
11
- // Debug mode - DISABLED to avoid polluting interactive UI
16
+ // HFT: Debug mode completely disabled - no conditional checks in hot path
12
17
  const DEBUG = false;
13
18
 
14
19
  // Order status constants
@@ -64,37 +69,44 @@ function validateOrderData(orderData) {
64
69
  return { valid: true };
65
70
  }
66
71
 
72
+ // HFT: Pre-allocated order request template to avoid object creation in hot path
73
+ const ORDER_REQUEST_TEMPLATE = {
74
+ templateId: REQ.NEW_ORDER,
75
+ userMsg: [''],
76
+ fcmId: '',
77
+ ibId: '',
78
+ accountId: '',
79
+ symbol: '',
80
+ exchange: 'CME',
81
+ quantity: 0,
82
+ transactionType: 1,
83
+ duration: 1,
84
+ priceType: 1,
85
+ price: 0,
86
+ tradeRoute: null,
87
+ manualOrAuto: 2,
88
+ };
89
+
90
+ // HFT: Monotonic counter for order tags (faster than Date.now())
91
+ let orderTagCounter = Date.now();
92
+
67
93
  /**
68
94
  * Place order via ORDER_PLANT and wait for confirmation
95
+ * HFT: Optimized for minimal latency
69
96
  * @param {RithmicService} service - The Rithmic service instance
70
97
  * @param {Object} orderData - Order parameters
71
98
  * @returns {Promise<{success: boolean, orderId?: string, error?: string}>}
72
99
  */
73
100
  const placeOrder = async (service, orderData) => {
74
- // Check connection state
75
- const connState = service.orderConn?.connectionState;
76
- const wsState = service.orderConn?.ws?.readyState;
77
-
78
- if (DEBUG) {
79
- console.log('[ORDER] Connection check:', {
80
- hasOrderConn: !!service.orderConn,
81
- connState,
82
- wsState,
83
- hasLoginInfo: !!service.loginInfo
84
- });
85
- }
86
-
87
- // Connection validation
88
- if (!service.orderConn) {
89
- return { success: false, error: 'ORDER_PLANT not connected' };
90
- }
91
-
92
- if (!service.loginInfo) {
93
- return { success: false, error: 'Not logged in - missing loginInfo' };
94
- }
95
-
96
- if (connState !== 'LOGGED_IN') {
97
- return { success: false, error: `ORDER_PLANT not logged in (state: ${connState})` };
101
+ // HFT: Fast connection validation (no intermediate variables)
102
+ if (!service.orderConn || !service.loginInfo ||
103
+ service.orderConn.connectionState !== 'LOGGED_IN') {
104
+ return {
105
+ success: false,
106
+ error: !service.orderConn ? 'ORDER_PLANT not connected' :
107
+ !service.loginInfo ? 'Not logged in' :
108
+ `ORDER_PLANT not logged in (state: ${service.orderConn.connectionState})`
109
+ };
98
110
  }
99
111
 
100
112
  // Validate order data
@@ -103,8 +115,8 @@ const placeOrder = async (service, orderData) => {
103
115
  return { success: false, error: validation.error };
104
116
  }
105
117
 
106
- // Generate unique user message for tracking
107
- const orderTag = `HQX-${Date.now()}`;
118
+ // HFT: Use monotonic counter for order tag (faster than Date.now())
119
+ const orderTag = `HQX-${++orderTagCounter}`;
108
120
 
109
121
  return new Promise((resolve) => {
110
122
  const timeout = setTimeout(() => {
@@ -113,8 +125,7 @@ const placeOrder = async (service, orderData) => {
113
125
  }, ORDER_TIMEOUTS.PLACE);
114
126
 
115
127
  const onNotification = (order) => {
116
- // CRITICAL FIX: Match by orderTag (userMsg) first, then symbol as fallback
117
- // This prevents race conditions when multiple orders for same symbol
128
+ // HFT: Fast match by orderTag first (most specific)
118
129
  const orderMatches = (order.userMsg && order.userMsg.includes(orderTag)) ||
119
130
  order.symbol === orderData.symbol;
120
131
 
@@ -122,89 +133,56 @@ const placeOrder = async (service, orderData) => {
122
133
  clearTimeout(timeout);
123
134
  service.removeListener('orderNotification', onNotification);
124
135
 
125
- // Check if order was accepted/filled
126
- if (order.status === 2 || order.status === 3 || order.notifyType === 15) {
127
- // Status 2 = Working, 3 = Filled, notifyType 15 = Complete
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) {
128
139
  resolve({
129
140
  success: true,
130
141
  orderId: order.basketId,
131
- status: order.status,
142
+ status,
132
143
  fillPrice: order.avgFillPrice || orderData.price,
133
144
  filledQty: order.totalFillSize || orderData.size,
134
- orderTag: orderTag,
145
+ orderTag,
135
146
  });
136
- } else if (order.status === 5 || order.status === 6) {
137
- // Status 5 = Rejected, 6 = Cancelled
147
+ } else if (status === 5 || status === 6) {
138
148
  resolve({
139
149
  success: false,
140
- error: `Order rejected: status ${order.status}`,
150
+ error: `Order rejected: status ${status}`,
141
151
  orderId: order.basketId,
142
- orderTag: orderTag,
152
+ orderTag,
143
153
  });
144
154
  }
145
- // Keep listening for other statuses
146
155
  }
147
156
  };
148
157
 
149
158
  service.on('orderNotification', onNotification);
150
159
 
151
- try {
152
- const exchange = orderData.exchange || 'CME';
153
-
154
- // Get trade route from cache (fetched during login)
155
- // Trade route is REQUIRED by Rithmic - orders rejected without it
156
- let tradeRoute = null;
157
- if (service.tradeRoutes && service.tradeRoutes.size > 0) {
158
- const routeInfo = service.tradeRoutes.get(exchange);
159
- if (routeInfo) {
160
- tradeRoute = routeInfo.tradeRoute;
161
- } else {
162
- // Fallback: use first available route
163
- const firstRoute = service.tradeRoutes.values().next().value;
164
- if (firstRoute) {
165
- tradeRoute = firstRoute.tradeRoute;
166
- }
167
- }
168
- }
169
-
170
- if (DEBUG) {
171
- console.log('[ORDER] Trade route for', exchange, ':', tradeRoute);
172
- }
173
-
174
- if (!tradeRoute) {
175
- // No trade route available - order will likely fail
176
- if (DEBUG) {
177
- console.log('[ORDER] WARNING: No trade route available, order may be rejected');
178
- }
179
- }
180
-
181
- const orderRequest = {
182
- templateId: REQ.NEW_ORDER,
183
- userMsg: [orderTag],
184
- fcmId: service.loginInfo.fcmId,
185
- ibId: service.loginInfo.ibId,
186
- accountId: orderData.accountId,
187
- symbol: orderData.symbol,
188
- exchange: exchange,
189
- quantity: sanitizeQuantity(orderData.size),
190
- transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
191
- duration: 1, // DAY
192
- priceType: orderData.type === 2 ? 2 : 1, // 2=Market, 1=Limit
193
- price: orderData.price || 0,
194
- tradeRoute: tradeRoute, // REQUIRED by Rithmic
195
- manualOrAuto: 2, // AUTO
196
- };
197
-
198
- if (DEBUG) {
199
- console.log('[ORDER] Sending RequestNewOrder:', JSON.stringify(orderRequest));
200
- }
201
-
202
- service.orderConn.send('RequestNewOrder', orderRequest);
203
- } catch (error) {
204
- clearTimeout(timeout);
205
- service.removeListener('orderNotification', onNotification);
206
- resolve({ success: false, error: error.message });
160
+ // HFT: Inline order request construction (no try-catch in hot path)
161
+ const exchange = orderData.exchange || 'CME';
162
+
163
+ // Get trade route from cache
164
+ let tradeRoute = null;
165
+ const routes = service.tradeRoutes;
166
+ if (routes && routes.size > 0) {
167
+ const routeInfo = routes.get(exchange);
168
+ tradeRoute = routeInfo ? routeInfo.tradeRoute :
169
+ routes.values().next().value?.tradeRoute || null;
207
170
  }
171
+
172
+ // HFT: Reuse template and mutate (faster than object spread)
173
+ ORDER_REQUEST_TEMPLATE.userMsg[0] = orderTag;
174
+ ORDER_REQUEST_TEMPLATE.fcmId = service.loginInfo.fcmId;
175
+ ORDER_REQUEST_TEMPLATE.ibId = service.loginInfo.ibId;
176
+ ORDER_REQUEST_TEMPLATE.accountId = orderData.accountId;
177
+ ORDER_REQUEST_TEMPLATE.symbol = orderData.symbol;
178
+ ORDER_REQUEST_TEMPLATE.exchange = exchange;
179
+ ORDER_REQUEST_TEMPLATE.quantity = sanitizeQuantity(orderData.size);
180
+ ORDER_REQUEST_TEMPLATE.transactionType = orderData.side === 0 ? 1 : 2;
181
+ ORDER_REQUEST_TEMPLATE.priceType = orderData.type === 2 ? 2 : 1;
182
+ ORDER_REQUEST_TEMPLATE.price = orderData.price || 0;
183
+ ORDER_REQUEST_TEMPLATE.tradeRoute = tradeRoute;
184
+
185
+ service.orderConn.send('RequestNewOrder', ORDER_REQUEST_TEMPLATE);
208
186
  });
209
187
  };
210
188
 
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Protobuf Decoders - Decode Rithmic protobuf messages
3
3
  * @module services/rithmic/protobuf-decoders
4
+ *
5
+ * HFT-GRADE OPTIMIZATIONS:
6
+ * - Lookup tables for O(1) field dispatch (replaces O(n) switch)
7
+ * - Pre-built decoder maps at module load time
8
+ * - Zero branch misprediction in hot path
4
9
  */
5
10
 
6
11
  const { readVarint, readLengthDelimited, skipField } = require('./protobuf-utils');
@@ -70,241 +75,243 @@ const INSTRUMENT_PNL_FIELDS = {
70
75
  USECS: 150101,
71
76
  };
72
77
 
78
+ // =============================================================================
79
+ // HFT: PRE-BUILT LOOKUP TABLES FOR O(1) FIELD DISPATCH
80
+ // =============================================================================
81
+
82
+ // Decoder types: 0=skip, 1=varint, 2=string, 3=bool, 4=double
83
+ const DECODE_SKIP = 0;
84
+ const DECODE_VARINT = 1;
85
+ const DECODE_STRING = 2;
86
+ const DECODE_BOOL = 3;
87
+ const DECODE_DOUBLE = 4;
88
+
73
89
  /**
74
- * Manually decode AccountPnL from raw bytes
75
- * Skips 4-byte length prefix if present
76
- * @param {Buffer} buffer - Raw protobuf buffer
77
- * @returns {Object} Decoded account PnL data
90
+ * Build lookup table from field definitions
91
+ * @param {Object} fields - Field ID mapping
92
+ * @param {Object} fieldTypes - Map of field name to [type, resultKey]
93
+ * @returns {Map} Lookup table: fieldNumber -> [type, resultKey]
78
94
  */
79
- function decodeAccountPnL(buffer) {
80
- // Skip 4-byte length prefix
81
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
82
-
83
- const result = {};
95
+ function buildLookupTable(fields, fieldTypes) {
96
+ const table = new Map();
97
+ for (const [fieldName, fieldNumber] of Object.entries(fields)) {
98
+ if (fieldTypes[fieldName]) {
99
+ table.set(fieldNumber, fieldTypes[fieldName]);
100
+ }
101
+ }
102
+ return table;
103
+ }
104
+
105
+ // PnL field types: [decoderType, resultKey]
106
+ const PNL_FIELD_TYPES = {
107
+ TEMPLATE_ID: [DECODE_VARINT, 'templateId'],
108
+ IS_SNAPSHOT: [DECODE_BOOL, 'isSnapshot'],
109
+ FCM_ID: [DECODE_STRING, 'fcmId'],
110
+ IB_ID: [DECODE_STRING, 'ibId'],
111
+ ACCOUNT_ID: [DECODE_STRING, 'accountId'],
112
+ ACCOUNT_BALANCE: [DECODE_STRING, 'accountBalance'],
113
+ CASH_ON_HAND: [DECODE_STRING, 'cashOnHand'],
114
+ MARGIN_BALANCE: [DECODE_STRING, 'marginBalance'],
115
+ MIN_ACCOUNT_BALANCE: [DECODE_STRING, 'minAccountBalance'],
116
+ OPEN_POSITION_PNL: [DECODE_STRING, 'openPositionPnl'],
117
+ CLOSED_POSITION_PNL: [DECODE_STRING, 'closedPositionPnl'],
118
+ DAY_PNL: [DECODE_STRING, 'dayPnl'],
119
+ DAY_OPEN_PNL: [DECODE_STRING, 'dayOpenPnl'],
120
+ DAY_CLOSED_PNL: [DECODE_STRING, 'dayClosedPnl'],
121
+ AVAILABLE_BUYING_POWER: [DECODE_STRING, 'availableBuyingPower'],
122
+ SSBOE: [DECODE_VARINT, 'ssboe'],
123
+ USECS: [DECODE_VARINT, 'usecs'],
124
+ };
125
+
126
+ // Instrument PnL field types
127
+ const INSTRUMENT_PNL_FIELD_TYPES = {
128
+ TEMPLATE_ID: [DECODE_VARINT, 'templateId'],
129
+ IS_SNAPSHOT: [DECODE_BOOL, 'isSnapshot'],
130
+ FCM_ID: [DECODE_STRING, 'fcmId'],
131
+ IB_ID: [DECODE_STRING, 'ibId'],
132
+ ACCOUNT_ID: [DECODE_STRING, 'accountId'],
133
+ SYMBOL: [DECODE_STRING, 'symbol'],
134
+ EXCHANGE: [DECODE_STRING, 'exchange'],
135
+ PRODUCT_CODE: [DECODE_STRING, 'productCode'],
136
+ BUY_QTY: [DECODE_VARINT, 'buyQty'],
137
+ SELL_QTY: [DECODE_VARINT, 'sellQty'],
138
+ FILL_BUY_QTY: [DECODE_VARINT, 'fillBuyQty'],
139
+ FILL_SELL_QTY: [DECODE_VARINT, 'fillSellQty'],
140
+ NET_QUANTITY: [DECODE_VARINT, 'netQuantity'],
141
+ OPEN_POSITION_QUANTITY: [DECODE_VARINT, 'openPositionQuantity'],
142
+ AVG_OPEN_FILL_PRICE: [DECODE_DOUBLE, 'avgOpenFillPrice'],
143
+ OPEN_POSITION_PNL: [DECODE_STRING, 'openPositionPnl'],
144
+ CLOSED_POSITION_PNL: [DECODE_STRING, 'closedPositionPnl'],
145
+ DAY_PNL: [DECODE_STRING, 'dayPnl'],
146
+ DAY_OPEN_PNL: [DECODE_STRING, 'dayOpenPnl'],
147
+ DAY_CLOSED_PNL: [DECODE_STRING, 'dayClosedPnl'],
148
+ SSBOE: [DECODE_VARINT, 'ssboe'],
149
+ USECS: [DECODE_VARINT, 'usecs'],
150
+ };
151
+
152
+ // Symbol/Product codes field types
153
+ const SYMBOL_FIELD_TYPES = {
154
+ TEMPLATE_ID: [DECODE_VARINT, 'templateId'],
155
+ RP_CODE: [DECODE_STRING, 'rpCode'], // Array field - handled specially
156
+ EXCHANGE: [DECODE_STRING, 'exchange'],
157
+ PRODUCT_CODE: [DECODE_STRING, 'productCode'],
158
+ PRODUCT_NAME: [DECODE_STRING, 'productName'],
159
+ SYMBOL: [DECODE_STRING, 'symbol'],
160
+ TRADING_SYMBOL: [DECODE_STRING, 'tradingSymbol'],
161
+ DESCRIPTION: [DECODE_STRING, 'description'],
162
+ USER_MSG: [DECODE_STRING, 'userMsg'],
163
+ };
164
+
165
+ // Pre-build lookup tables at module load time (O(1) access in hot path)
166
+ const PNL_LOOKUP = buildLookupTable(PNL_FIELDS, PNL_FIELD_TYPES);
167
+ const INSTRUMENT_PNL_LOOKUP = buildLookupTable(INSTRUMENT_PNL_FIELDS, INSTRUMENT_PNL_FIELD_TYPES);
168
+ const SYMBOL_LOOKUP = buildLookupTable(SYMBOL_FIELDS, SYMBOL_FIELD_TYPES);
169
+
170
+ // =============================================================================
171
+ // HFT: GENERIC DECODER WITH LOOKUP TABLE - O(1) FIELD DISPATCH
172
+ // =============================================================================
173
+
174
+ /**
175
+ * HFT-optimized generic decoder using lookup table
176
+ * Replaces O(n) switch statements with O(1) Map lookup
177
+ * @param {Buffer} data - Raw protobuf data (without length prefix)
178
+ * @param {Map} lookup - Pre-built field lookup table
179
+ * @param {Object} result - Pre-allocated result object (optional)
180
+ * @returns {Object} Decoded data
181
+ */
182
+ function decodeWithLookup(data, lookup, result = {}) {
84
183
  let offset = 0;
184
+ const len = data.length;
85
185
 
86
- while (offset < data.length) {
87
- try {
88
- const [tag, tagOffset] = readVarint(data, offset);
89
- const wireType = tag & 0x7;
90
- const fieldNumber = tag >>> 3;
91
- offset = tagOffset;
186
+ while (offset < len) {
187
+ // Read tag (varint) - inlined for performance
188
+ let tag = 0;
189
+ let shift = 0;
190
+ let byte;
191
+ do {
192
+ byte = data[offset++];
193
+ tag |= (byte & 0x7f) << shift;
194
+ shift += 7;
195
+ } while (byte & 0x80);
92
196
 
93
- switch (fieldNumber) {
94
- case PNL_FIELDS.TEMPLATE_ID:
95
- [result.templateId, offset] = readVarint(data, offset);
96
- break;
97
- case PNL_FIELDS.IS_SNAPSHOT:
98
- const [isSnap, snapOffset] = readVarint(data, offset);
99
- result.isSnapshot = isSnap !== 0;
100
- offset = snapOffset;
101
- break;
102
- case PNL_FIELDS.FCM_ID:
103
- [result.fcmId, offset] = readLengthDelimited(data, offset);
104
- break;
105
- case PNL_FIELDS.IB_ID:
106
- [result.ibId, offset] = readLengthDelimited(data, offset);
107
- break;
108
- case PNL_FIELDS.ACCOUNT_ID:
109
- [result.accountId, offset] = readLengthDelimited(data, offset);
110
- break;
111
- case PNL_FIELDS.ACCOUNT_BALANCE:
112
- [result.accountBalance, offset] = readLengthDelimited(data, offset);
113
- break;
114
- case PNL_FIELDS.CASH_ON_HAND:
115
- [result.cashOnHand, offset] = readLengthDelimited(data, offset);
116
- break;
117
- case PNL_FIELDS.MARGIN_BALANCE:
118
- [result.marginBalance, offset] = readLengthDelimited(data, offset);
119
- break;
120
- case PNL_FIELDS.MIN_ACCOUNT_BALANCE:
121
- [result.minAccountBalance, offset] = readLengthDelimited(data, offset);
122
- break;
123
- case PNL_FIELDS.OPEN_POSITION_PNL:
124
- [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
125
- break;
126
- case PNL_FIELDS.CLOSED_POSITION_PNL:
127
- [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
128
- break;
129
- case PNL_FIELDS.DAY_PNL:
130
- [result.dayPnl, offset] = readLengthDelimited(data, offset);
131
- break;
132
- case PNL_FIELDS.DAY_OPEN_PNL:
133
- [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
134
- break;
135
- case PNL_FIELDS.DAY_CLOSED_PNL:
136
- [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
137
- break;
138
- case PNL_FIELDS.AVAILABLE_BUYING_POWER:
139
- [result.availableBuyingPower, offset] = readLengthDelimited(data, offset);
140
- break;
141
- case PNL_FIELDS.SSBOE:
142
- [result.ssboe, offset] = readVarint(data, offset);
143
- break;
144
- case PNL_FIELDS.USECS:
145
- [result.usecs, offset] = readVarint(data, offset);
146
- break;
147
- default:
197
+ const wireType = tag & 0x7;
198
+ const fieldNumber = tag >>> 3;
199
+
200
+ // O(1) lookup instead of O(n) switch
201
+ const fieldInfo = lookup.get(fieldNumber);
202
+
203
+ if (fieldInfo) {
204
+ const [decodeType, key] = fieldInfo;
205
+
206
+ // Decode based on type (minimal branching)
207
+ if (decodeType === DECODE_VARINT) {
208
+ let value = 0;
209
+ shift = 0;
210
+ do {
211
+ byte = data[offset++];
212
+ value |= (byte & 0x7f) << shift;
213
+ shift += 7;
214
+ } while (byte & 0x80);
215
+ result[key] = value;
216
+ } else if (decodeType === DECODE_STRING) {
217
+ // Read length
218
+ let strLen = 0;
219
+ shift = 0;
220
+ do {
221
+ byte = data[offset++];
222
+ strLen |= (byte & 0x7f) << shift;
223
+ shift += 7;
224
+ } while (byte & 0x80);
225
+ result[key] = data.toString('utf8', offset, offset + strLen);
226
+ offset += strLen;
227
+ } else if (decodeType === DECODE_BOOL) {
228
+ let value = 0;
229
+ shift = 0;
230
+ do {
231
+ byte = data[offset++];
232
+ value |= (byte & 0x7f) << shift;
233
+ shift += 7;
234
+ } while (byte & 0x80);
235
+ result[key] = value !== 0;
236
+ } else if (decodeType === DECODE_DOUBLE) {
237
+ if (wireType === 1) {
238
+ result[key] = data.readDoubleLE(offset);
239
+ offset += 8;
240
+ } else {
148
241
  offset = skipField(data, offset, wireType);
242
+ }
149
243
  }
150
- } catch (error) {
151
- break;
244
+ } else {
245
+ // Unknown field - skip efficiently
246
+ offset = skipField(data, offset, wireType);
152
247
  }
153
248
  }
154
249
 
155
250
  return result;
156
251
  }
157
252
 
253
+ /**
254
+ * Manually decode AccountPnL from raw bytes
255
+ * HFT: Uses O(1) lookup table instead of O(n) switch
256
+ * @param {Buffer} buffer - Raw protobuf buffer
257
+ * @returns {Object} Decoded account PnL data
258
+ */
259
+ function decodeAccountPnL(buffer) {
260
+ // Skip 4-byte length prefix
261
+ const data = buffer.length > 4 ? buffer.subarray(4) : buffer;
262
+ return decodeWithLookup(data, PNL_LOOKUP);
263
+ }
264
+
158
265
  /**
159
266
  * Manually decode InstrumentPnLPositionUpdate from raw bytes
160
- * Skips 4-byte length prefix if present
267
+ * HFT: Uses O(1) lookup table instead of O(n) switch
161
268
  * @param {Buffer} buffer - Raw protobuf buffer
162
269
  * @returns {Object} Decoded instrument PnL data
163
270
  */
164
271
  function decodeInstrumentPnL(buffer) {
165
- // Skip 4-byte length prefix
166
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
167
-
168
- const result = {};
169
- let offset = 0;
170
-
171
- while (offset < data.length) {
172
- try {
173
- const [tag, tagOffset] = readVarint(data, offset);
174
- const wireType = tag & 0x7;
175
- const fieldNumber = tag >>> 3;
176
- offset = tagOffset;
177
-
178
- switch (fieldNumber) {
179
- case INSTRUMENT_PNL_FIELDS.TEMPLATE_ID:
180
- [result.templateId, offset] = readVarint(data, offset);
181
- break;
182
- case INSTRUMENT_PNL_FIELDS.IS_SNAPSHOT:
183
- const [isSnap, snapOffset] = readVarint(data, offset);
184
- result.isSnapshot = isSnap !== 0;
185
- offset = snapOffset;
186
- break;
187
- case INSTRUMENT_PNL_FIELDS.FCM_ID:
188
- [result.fcmId, offset] = readLengthDelimited(data, offset);
189
- break;
190
- case INSTRUMENT_PNL_FIELDS.IB_ID:
191
- [result.ibId, offset] = readLengthDelimited(data, offset);
192
- break;
193
- case INSTRUMENT_PNL_FIELDS.ACCOUNT_ID:
194
- [result.accountId, offset] = readLengthDelimited(data, offset);
195
- break;
196
- case INSTRUMENT_PNL_FIELDS.SYMBOL:
197
- [result.symbol, offset] = readLengthDelimited(data, offset);
198
- break;
199
- case INSTRUMENT_PNL_FIELDS.EXCHANGE:
200
- [result.exchange, offset] = readLengthDelimited(data, offset);
201
- break;
202
- case INSTRUMENT_PNL_FIELDS.PRODUCT_CODE:
203
- [result.productCode, offset] = readLengthDelimited(data, offset);
204
- break;
205
- case INSTRUMENT_PNL_FIELDS.BUY_QTY:
206
- [result.buyQty, offset] = readVarint(data, offset);
207
- break;
208
- case INSTRUMENT_PNL_FIELDS.SELL_QTY:
209
- [result.sellQty, offset] = readVarint(data, offset);
210
- break;
211
- case INSTRUMENT_PNL_FIELDS.FILL_BUY_QTY:
212
- [result.fillBuyQty, offset] = readVarint(data, offset);
213
- break;
214
- case INSTRUMENT_PNL_FIELDS.FILL_SELL_QTY:
215
- [result.fillSellQty, offset] = readVarint(data, offset);
216
- break;
217
- case INSTRUMENT_PNL_FIELDS.NET_QUANTITY:
218
- [result.netQuantity, offset] = readVarint(data, offset);
219
- break;
220
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_QUANTITY:
221
- [result.openPositionQuantity, offset] = readVarint(data, offset);
222
- break;
223
- case INSTRUMENT_PNL_FIELDS.AVG_OPEN_FILL_PRICE:
224
- // Double is 64-bit fixed
225
- if (wireType === 1) {
226
- result.avgOpenFillPrice = data.readDoubleLE(offset);
227
- offset += 8;
228
- } else {
229
- offset = skipField(data, offset, wireType);
230
- }
231
- break;
232
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_PNL:
233
- [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
234
- break;
235
- case INSTRUMENT_PNL_FIELDS.CLOSED_POSITION_PNL:
236
- [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
237
- break;
238
- case INSTRUMENT_PNL_FIELDS.DAY_PNL:
239
- [result.dayPnl, offset] = readLengthDelimited(data, offset);
240
- break;
241
- case INSTRUMENT_PNL_FIELDS.DAY_OPEN_PNL:
242
- [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
243
- break;
244
- case INSTRUMENT_PNL_FIELDS.DAY_CLOSED_PNL:
245
- [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
246
- break;
247
- case INSTRUMENT_PNL_FIELDS.SSBOE:
248
- [result.ssboe, offset] = readVarint(data, offset);
249
- break;
250
- case INSTRUMENT_PNL_FIELDS.USECS:
251
- [result.usecs, offset] = readVarint(data, offset);
252
- break;
253
- default:
254
- offset = skipField(data, offset, wireType);
255
- }
256
- } catch (error) {
257
- break;
258
- }
259
- }
260
-
261
- return result;
272
+ // Skip 4-byte length prefix - use subarray (no copy) instead of slice
273
+ const data = buffer.length > 4 ? buffer.subarray(4) : buffer;
274
+ return decodeWithLookup(data, INSTRUMENT_PNL_LOOKUP);
262
275
  }
263
276
 
264
277
  /**
265
278
  * Decode ResponseProductCodes (template 112) - list of available symbols
279
+ * Note: Not hot-path (initialization only), uses switch for rpCode array handling
266
280
  * @param {Buffer} buffer - Raw protobuf buffer (with 4-byte length prefix)
267
281
  * @returns {Object} Decoded product codes
268
282
  */
269
283
  function decodeProductCodes(buffer) {
270
- // Skip 4-byte length prefix
271
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
284
+ // HFT: Use subarray (zero-copy) instead of slice
285
+ const data = buffer.length > 4 ? buffer.subarray(4) : buffer;
272
286
  const result = { rpCode: [] };
273
287
  let offset = 0;
288
+ const len = data.length;
274
289
 
275
- while (offset < data.length) {
276
- try {
277
- const [tag, tagOffset] = readVarint(data, offset);
278
- const wireType = tag & 0x7;
279
- const fieldNumber = tag >>> 3;
280
- offset = tagOffset;
290
+ while (offset < len) {
291
+ const [tag, tagOffset] = readVarint(data, offset);
292
+ const wireType = tag & 0x7;
293
+ const fieldNumber = tag >>> 3;
294
+ offset = tagOffset;
281
295
 
282
- switch (fieldNumber) {
283
- case SYMBOL_FIELDS.TEMPLATE_ID:
284
- [result.templateId, offset] = readVarint(data, offset);
285
- break;
286
- case SYMBOL_FIELDS.RP_CODE:
287
- let rpCode;
288
- [rpCode, offset] = readLengthDelimited(data, offset);
289
- result.rpCode.push(rpCode);
290
- break;
291
- case SYMBOL_FIELDS.EXCHANGE:
292
- [result.exchange, offset] = readLengthDelimited(data, offset);
293
- break;
294
- case SYMBOL_FIELDS.PRODUCT_CODE:
295
- [result.productCode, offset] = readLengthDelimited(data, offset);
296
- break;
297
- case SYMBOL_FIELDS.PRODUCT_NAME:
298
- [result.productName, offset] = readLengthDelimited(data, offset);
299
- break;
300
- case SYMBOL_FIELDS.USER_MSG:
301
- [result.userMsg, offset] = readLengthDelimited(data, offset);
302
- break;
303
- default:
304
- offset = skipField(data, offset, wireType);
296
+ // Use lookup for most fields, special case for rpCode array
297
+ const fieldInfo = SYMBOL_LOOKUP.get(fieldNumber);
298
+ if (fieldInfo) {
299
+ const [, key] = fieldInfo;
300
+ if (key === 'rpCode') {
301
+ // Array field - push to existing array
302
+ let rpCode;
303
+ [rpCode, offset] = readLengthDelimited(data, offset);
304
+ result.rpCode.push(rpCode);
305
+ } else {
306
+ // Regular field
307
+ let value;
308
+ [value, offset] = readLengthDelimited(data, offset);
309
+ result[key] = value;
305
310
  }
306
- } catch (error) {
307
- break;
311
+ } else if (fieldNumber === SYMBOL_FIELDS.TEMPLATE_ID) {
312
+ [result.templateId, offset] = readVarint(data, offset);
313
+ } else {
314
+ offset = skipField(data, offset, wireType);
308
315
  }
309
316
  }
310
317
 
@@ -313,53 +320,42 @@ function decodeProductCodes(buffer) {
313
320
 
314
321
  /**
315
322
  * Decode ResponseFrontMonthContract (template 114) - current tradeable contract
316
- * Skips 4-byte length prefix if present
323
+ * Note: Not hot-path (initialization only), uses switch for rpCode array handling
317
324
  * @param {Buffer} buffer - Raw protobuf buffer
318
325
  * @returns {Object} Decoded front month contract
319
326
  */
320
327
  function decodeFrontMonthContract(buffer) {
321
- // Skip 4-byte length prefix
322
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
323
-
328
+ // HFT: Use subarray (zero-copy) instead of slice
329
+ const data = buffer.length > 4 ? buffer.subarray(4) : buffer;
324
330
  const result = { rpCode: [] };
325
331
  let offset = 0;
332
+ const len = data.length;
326
333
 
327
- while (offset < data.length) {
328
- try {
329
- const [tag, tagOffset] = readVarint(data, offset);
330
- const wireType = tag & 0x7;
331
- const fieldNumber = tag >>> 3;
332
- offset = tagOffset;
334
+ while (offset < len) {
335
+ const [tag, tagOffset] = readVarint(data, offset);
336
+ const wireType = tag & 0x7;
337
+ const fieldNumber = tag >>> 3;
338
+ offset = tagOffset;
333
339
 
334
- switch (fieldNumber) {
335
- case SYMBOL_FIELDS.TEMPLATE_ID:
336
- [result.templateId, offset] = readVarint(data, offset);
337
- break;
338
- case SYMBOL_FIELDS.RP_CODE:
339
- let rpCode;
340
- [rpCode, offset] = readLengthDelimited(data, offset);
341
- result.rpCode.push(rpCode);
342
- break;
343
- case SYMBOL_FIELDS.SYMBOL:
344
- [result.symbol, offset] = readLengthDelimited(data, offset);
345
- break;
346
- case SYMBOL_FIELDS.EXCHANGE:
347
- [result.exchange, offset] = readLengthDelimited(data, offset);
348
- break;
349
- case SYMBOL_FIELDS.TRADING_SYMBOL:
350
- [result.tradingSymbol, offset] = readLengthDelimited(data, offset);
351
- break;
352
- case SYMBOL_FIELDS.DESCRIPTION:
353
- [result.description, offset] = readLengthDelimited(data, offset);
354
- break;
355
- case SYMBOL_FIELDS.USER_MSG:
356
- [result.userMsg, offset] = readLengthDelimited(data, offset);
357
- break;
358
- default:
359
- offset = skipField(data, offset, wireType);
340
+ // Use lookup for most fields, special case for rpCode array
341
+ const fieldInfo = SYMBOL_LOOKUP.get(fieldNumber);
342
+ if (fieldInfo) {
343
+ const [, key] = fieldInfo;
344
+ if (key === 'rpCode') {
345
+ // Array field - push to existing array
346
+ let rpCode;
347
+ [rpCode, offset] = readLengthDelimited(data, offset);
348
+ result.rpCode.push(rpCode);
349
+ } else {
350
+ // Regular field
351
+ let value;
352
+ [value, offset] = readLengthDelimited(data, offset);
353
+ result[key] = value;
360
354
  }
361
- } catch (error) {
362
- break;
355
+ } else if (fieldNumber === SYMBOL_FIELDS.TEMPLATE_ID) {
356
+ [result.templateId, offset] = readVarint(data, offset);
357
+ } else {
358
+ offset = skipField(data, offset, wireType);
363
359
  }
364
360
  }
365
361