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,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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
stats.latency = Math.round(tickLatencies.
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 -
|
|
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
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
107
|
-
const orderTag = `HQX-${
|
|
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
|
-
//
|
|
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
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
142
|
+
status,
|
|
132
143
|
fillPrice: order.avgFillPrice || orderData.price,
|
|
133
144
|
filledQty: order.totalFillSize || orderData.size,
|
|
134
|
-
orderTag
|
|
145
|
+
orderTag,
|
|
135
146
|
});
|
|
136
|
-
} else if (
|
|
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 ${
|
|
150
|
+
error: `Order rejected: status ${status}`,
|
|
141
151
|
orderId: order.basketId,
|
|
142
|
-
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* @param {
|
|
77
|
-
* @returns {
|
|
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
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
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 <
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}
|
|
151
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
271
|
-
const data = buffer.length > 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 <
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
}
|
|
307
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
322
|
-
const data = buffer.length > 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 <
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
}
|
|
362
|
-
|
|
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
|
|