hedgequantx 2.5.43 → 2.6.0
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/bin/cli.js +11 -0
- package/package.json +1 -1
- package/src/app.js +11 -0
- package/src/config/settings.js +29 -0
- package/src/pages/algo/copy-trading.js +692 -253
- package/src/pages/algo/one-account.js +241 -47
- package/src/services/position-manager.js +927 -0
- package/src/services/rithmic/connection.js +201 -1
- package/src/services/rithmic/handlers.js +341 -5
- package/src/services/rithmic/index.js +75 -3
- package/src/services/rithmic/orders.js +193 -1
- package/src/services/rithmic/protobuf.js +171 -2
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const EventEmitter = require('events');
|
|
9
9
|
const { RithmicConnection } = require('./connection');
|
|
10
10
|
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
|
|
11
|
-
const { createOrderHandler, createPnLHandler } = require('./handlers');
|
|
11
|
+
const { createOrderHandler, createPnLHandler, LatencyTracker } = require('./handlers');
|
|
12
12
|
const {
|
|
13
13
|
fetchAccounts,
|
|
14
14
|
getTradingAccounts,
|
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
getPositions,
|
|
18
18
|
hashAccountId,
|
|
19
19
|
} = require('./accounts');
|
|
20
|
-
const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
|
|
20
|
+
const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition, fastEntry, fastExit } = require('./orders');
|
|
21
21
|
const { decodeFrontMonthContract } = require('./protobuf');
|
|
22
22
|
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
23
23
|
const { logger } = require('../../utils/logger');
|
|
@@ -255,6 +255,78 @@ class RithmicService extends EventEmitter {
|
|
|
255
255
|
async cancelOrder(orderId) { return cancelOrder(this, orderId); }
|
|
256
256
|
async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
|
|
257
257
|
|
|
258
|
+
// ==================== FAST SCALPING (Ultra-Low Latency) ====================
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Ultra-fast market order entry - fire-and-forget
|
|
262
|
+
* Target latency: < 5ms local processing (network latency separate)
|
|
263
|
+
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
264
|
+
* @returns {{ success: boolean, orderTag: string, entryTime: number, latencyMs: number }}
|
|
265
|
+
*/
|
|
266
|
+
fastEntry(orderData) { return fastEntry(this, orderData); }
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Ultra-fast market exit - fire-and-forget
|
|
270
|
+
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
271
|
+
* @returns {{ success: boolean, orderTag: string, exitTime: number, latencyMs: number }}
|
|
272
|
+
*/
|
|
273
|
+
fastExit(orderData) { return fastExit(this, orderData); }
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Warmup connections for minimum latency
|
|
277
|
+
* Call after login but before trading starts
|
|
278
|
+
* @returns {Promise<boolean>}
|
|
279
|
+
*/
|
|
280
|
+
async warmup() {
|
|
281
|
+
const results = [];
|
|
282
|
+
|
|
283
|
+
if (this.orderConn) {
|
|
284
|
+
results.push(await this.orderConn.warmup());
|
|
285
|
+
}
|
|
286
|
+
if (this.pnlConn) {
|
|
287
|
+
results.push(await this.pnlConn.warmup());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
log.debug('Connection warmup complete', {
|
|
291
|
+
success: results.filter(Boolean).length,
|
|
292
|
+
total: results.length,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return results.every(Boolean);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get latency statistics from order fills
|
|
300
|
+
* @returns {Object} Latency stats: min, max, avg, p50, p99, samples
|
|
301
|
+
*/
|
|
302
|
+
getLatencyStats() {
|
|
303
|
+
return LatencyTracker.getStats();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get recent latency samples
|
|
308
|
+
* @param {number} n - Number of samples to return
|
|
309
|
+
* @returns {number[]}
|
|
310
|
+
*/
|
|
311
|
+
getRecentLatencies(n = 10) {
|
|
312
|
+
return LatencyTracker.getRecent(n);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get connection diagnostics
|
|
317
|
+
* @returns {Object}
|
|
318
|
+
*/
|
|
319
|
+
getDiagnostics() {
|
|
320
|
+
return {
|
|
321
|
+
orderConn: this.orderConn?.getDiagnostics() || null,
|
|
322
|
+
pnlConn: this.pnlConn?.getDiagnostics() || null,
|
|
323
|
+
tickerConn: this.tickerConn?.getDiagnostics() || null,
|
|
324
|
+
latency: this.getLatencyStats(),
|
|
325
|
+
accounts: this.accounts.length,
|
|
326
|
+
positions: this.positions.size,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
258
330
|
// ==================== STUBS ====================
|
|
259
331
|
|
|
260
332
|
async getUser() { return this.user; }
|
|
@@ -552,4 +624,4 @@ class RithmicService extends EventEmitter {
|
|
|
552
624
|
}
|
|
553
625
|
}
|
|
554
626
|
|
|
555
|
-
module.exports = { RithmicService, RITHMIC_SYSTEMS, RITHMIC_ENDPOINTS };
|
|
627
|
+
module.exports = { RithmicService, RITHMIC_SYSTEMS, RITHMIC_ENDPOINTS, LatencyTracker };
|
|
@@ -1,9 +1,198 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rithmic Orders Module
|
|
3
3
|
* Order placement, cancellation, and history
|
|
4
|
+
*
|
|
5
|
+
* FAST SCALPING: fastEntry() and fastExit() for ultra-low latency execution
|
|
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
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
15
|
const { REQ } = require('./constants');
|
|
16
|
+
const { proto } = require('./protobuf');
|
|
17
|
+
const { LatencyTracker } = require('./handlers');
|
|
18
|
+
const { performance } = require('perf_hooks');
|
|
19
|
+
|
|
20
|
+
// ==================== FAST ORDER TAG ====================
|
|
21
|
+
// Pre-generate prefix once at module load (not per-order)
|
|
22
|
+
const ORDER_TAG_PREFIX = `HQX${process.pid}-`;
|
|
23
|
+
let orderIdCounter = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ultra-fast order tag generation
|
|
27
|
+
* Avoids Date.now() and string interpolation in hot path
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
const generateOrderTag = () => ORDER_TAG_PREFIX + (++orderIdCounter);
|
|
31
|
+
|
|
32
|
+
// ==================== PRE-ALLOCATED ORDER TEMPLATES ====================
|
|
33
|
+
// Reusable order object to minimize GC pressure
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Order object pool for zero-allocation hot path
|
|
37
|
+
*/
|
|
38
|
+
const OrderPool = {
|
|
39
|
+
// Pre-allocated order template
|
|
40
|
+
_template: {
|
|
41
|
+
templateId: REQ.NEW_ORDER,
|
|
42
|
+
userMsg: [''],
|
|
43
|
+
fcmId: '',
|
|
44
|
+
ibId: '',
|
|
45
|
+
accountId: '',
|
|
46
|
+
symbol: '',
|
|
47
|
+
exchange: 'CME',
|
|
48
|
+
quantity: 0,
|
|
49
|
+
transactionType: 1,
|
|
50
|
+
duration: 1,
|
|
51
|
+
orderType: 1,
|
|
52
|
+
manualOrAuto: 2,
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get order object with values filled in
|
|
57
|
+
* Reuses same object to avoid allocation
|
|
58
|
+
*/
|
|
59
|
+
fill(orderTag, loginInfo, orderData) {
|
|
60
|
+
const o = this._template;
|
|
61
|
+
o.userMsg[0] = orderTag;
|
|
62
|
+
o.fcmId = loginInfo.fcmId;
|
|
63
|
+
o.ibId = loginInfo.ibId;
|
|
64
|
+
o.accountId = orderData.accountId;
|
|
65
|
+
o.symbol = orderData.symbol;
|
|
66
|
+
o.exchange = orderData.exchange || 'CME';
|
|
67
|
+
o.quantity = orderData.size;
|
|
68
|
+
o.transactionType = orderData.side === 0 ? 1 : 2;
|
|
69
|
+
return o;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ultra-fast market order entry - HOT PATH
|
|
75
|
+
* NO SL/TP, NO await confirmation, fire-and-forget
|
|
76
|
+
* Target latency: < 5ms local processing
|
|
77
|
+
*
|
|
78
|
+
* OPTIMIZATIONS:
|
|
79
|
+
* - Reuses pre-allocated order object
|
|
80
|
+
* - Fast orderTag (no Date.now)
|
|
81
|
+
* - Uses fastEncode for cached protobuf type
|
|
82
|
+
* - Minimal branching
|
|
83
|
+
*
|
|
84
|
+
* @param {RithmicService} service - The Rithmic service instance
|
|
85
|
+
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
86
|
+
* @returns {{ success: boolean, orderTag: string, entryTime: number, latencyMs: number }}
|
|
87
|
+
*/
|
|
88
|
+
const fastEntry = (service, orderData) => {
|
|
89
|
+
const startTime = performance.now();
|
|
90
|
+
const orderTag = generateOrderTag();
|
|
91
|
+
const entryTime = Date.now();
|
|
92
|
+
|
|
93
|
+
// Fast connection check
|
|
94
|
+
if (!service.orderConn?.isConnected || !service.loginInfo) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: 'Not connected',
|
|
98
|
+
orderTag,
|
|
99
|
+
entryTime,
|
|
100
|
+
latencyMs: performance.now() - startTime,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// OPTIMIZED: Use pre-allocated order object
|
|
106
|
+
const order = OrderPool.fill(orderTag, service.loginInfo, orderData);
|
|
107
|
+
|
|
108
|
+
// OPTIMIZED: Use fastEncode with cached type
|
|
109
|
+
const buffer = proto.fastEncode('RequestNewOrder', order);
|
|
110
|
+
|
|
111
|
+
// ULTRA-OPTIMIZED: Try direct socket write first, fallback to fastSend
|
|
112
|
+
const sent = service.orderConn.ultraSend
|
|
113
|
+
? service.orderConn.ultraSend(buffer)
|
|
114
|
+
: (service.orderConn.fastSend(buffer), true);
|
|
115
|
+
|
|
116
|
+
if (!sent) {
|
|
117
|
+
service.orderConn.fastSend(buffer);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Track for round-trip latency measurement
|
|
121
|
+
LatencyTracker.recordEntry(orderTag, entryTime);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
orderTag,
|
|
126
|
+
entryTime,
|
|
127
|
+
latencyMs: performance.now() - startTime,
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: error.message,
|
|
133
|
+
orderTag,
|
|
134
|
+
entryTime,
|
|
135
|
+
latencyMs: performance.now() - startTime,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Ultra-fast market exit - for position closing
|
|
142
|
+
* Fire-and-forget like fastEntry
|
|
143
|
+
* Same optimizations as fastEntry
|
|
144
|
+
*
|
|
145
|
+
* @param {RithmicService} service - The Rithmic service instance
|
|
146
|
+
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
147
|
+
* @returns {{ success: boolean, orderTag: string, exitTime: number, latencyMs: number }}
|
|
148
|
+
*/
|
|
149
|
+
const fastExit = (service, orderData) => {
|
|
150
|
+
const startTime = performance.now();
|
|
151
|
+
const orderTag = generateOrderTag();
|
|
152
|
+
const exitTime = Date.now();
|
|
153
|
+
|
|
154
|
+
if (!service.orderConn?.isConnected || !service.loginInfo) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: 'Not connected',
|
|
158
|
+
orderTag,
|
|
159
|
+
exitTime,
|
|
160
|
+
latencyMs: performance.now() - startTime,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// OPTIMIZED: Use pre-allocated order object
|
|
166
|
+
const order = OrderPool.fill(orderTag, service.loginInfo, orderData);
|
|
167
|
+
|
|
168
|
+
// OPTIMIZED: Use fastEncode with cached type
|
|
169
|
+
const buffer = proto.fastEncode('RequestNewOrder', order);
|
|
170
|
+
|
|
171
|
+
// ULTRA-OPTIMIZED: Try direct socket write first, fallback to fastSend
|
|
172
|
+
const sent = service.orderConn.ultraSend
|
|
173
|
+
? service.orderConn.ultraSend(buffer)
|
|
174
|
+
: (service.orderConn.fastSend(buffer), true);
|
|
175
|
+
|
|
176
|
+
if (!sent) {
|
|
177
|
+
service.orderConn.fastSend(buffer);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
orderTag,
|
|
183
|
+
exitTime,
|
|
184
|
+
latencyMs: performance.now() - startTime,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: error.message,
|
|
190
|
+
orderTag,
|
|
191
|
+
exitTime,
|
|
192
|
+
latencyMs: performance.now() - startTime,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
};
|
|
7
196
|
|
|
8
197
|
/**
|
|
9
198
|
* Place order via ORDER_PLANT
|
|
@@ -188,5 +377,8 @@ module.exports = {
|
|
|
188
377
|
cancelOrder,
|
|
189
378
|
getOrders,
|
|
190
379
|
getOrderHistory,
|
|
191
|
-
closePosition
|
|
380
|
+
closePosition,
|
|
381
|
+
// Fast scalping - ultra-low latency
|
|
382
|
+
fastEntry,
|
|
383
|
+
fastExit,
|
|
192
384
|
};
|
|
@@ -7,6 +7,74 @@ const protobuf = require('protobufjs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { PROTO_FILES } = require('./constants');
|
|
9
9
|
|
|
10
|
+
// ==================== BUFFER POOL ====================
|
|
11
|
+
// Pre-allocated buffer pool for zero-allocation hot path
|
|
12
|
+
// Avoids GC pressure during high-frequency trading
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* High-performance buffer pool for zero-allocation encoding
|
|
16
|
+
* Uses ring buffer pattern for O(1) acquire/release
|
|
17
|
+
*/
|
|
18
|
+
class BufferPool {
|
|
19
|
+
constructor(poolSize = 16, bufferSize = 512) {
|
|
20
|
+
this._pool = new Array(poolSize);
|
|
21
|
+
this._available = new Array(poolSize);
|
|
22
|
+
this._size = poolSize;
|
|
23
|
+
this._bufferSize = bufferSize;
|
|
24
|
+
this._head = 0;
|
|
25
|
+
this._tail = 0;
|
|
26
|
+
this._count = poolSize;
|
|
27
|
+
|
|
28
|
+
// Pre-allocate all buffers
|
|
29
|
+
for (let i = 0; i < poolSize; i++) {
|
|
30
|
+
this._pool[i] = Buffer.allocUnsafe(bufferSize);
|
|
31
|
+
this._available[i] = i;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Acquire a buffer from the pool
|
|
37
|
+
* @returns {Buffer|null} Buffer or null if pool exhausted
|
|
38
|
+
*/
|
|
39
|
+
acquire() {
|
|
40
|
+
if (this._count === 0) {
|
|
41
|
+
// Pool exhausted - allocate new (fallback)
|
|
42
|
+
return Buffer.allocUnsafe(this._bufferSize);
|
|
43
|
+
}
|
|
44
|
+
const idx = this._available[this._head];
|
|
45
|
+
this._head = (this._head + 1) % this._size;
|
|
46
|
+
this._count--;
|
|
47
|
+
return this._pool[idx];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Release a buffer back to pool
|
|
52
|
+
* Only releases buffers that belong to the pool
|
|
53
|
+
* @param {Buffer} buffer
|
|
54
|
+
*/
|
|
55
|
+
release(buffer) {
|
|
56
|
+
// Find if this buffer is from our pool
|
|
57
|
+
const idx = this._pool.indexOf(buffer);
|
|
58
|
+
if (idx !== -1 && this._count < this._size) {
|
|
59
|
+
this._available[this._tail] = idx;
|
|
60
|
+
this._tail = (this._tail + 1) % this._size;
|
|
61
|
+
this._count++;
|
|
62
|
+
}
|
|
63
|
+
// If not from pool, let GC handle it
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get pool stats
|
|
68
|
+
*/
|
|
69
|
+
getStats() {
|
|
70
|
+
return {
|
|
71
|
+
size: this._size,
|
|
72
|
+
available: this._count,
|
|
73
|
+
bufferSize: this._bufferSize,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
10
78
|
// PnL field IDs (Rithmic uses very large field IDs)
|
|
11
79
|
const PNL_FIELDS = {
|
|
12
80
|
TEMPLATE_ID: 154467,
|
|
@@ -303,16 +371,34 @@ function decodeInstrumentPnL(buffer) {
|
|
|
303
371
|
|
|
304
372
|
/**
|
|
305
373
|
* Protobuf Handler class
|
|
374
|
+
* OPTIMIZED: Pre-compile types, cache encoders, buffer pooling
|
|
375
|
+
*
|
|
376
|
+
* ULTRA-LOW LATENCY FEATURES:
|
|
377
|
+
* - Pre-allocated buffer pool (zero allocation in hot path)
|
|
378
|
+
* - Reusable protobuf Writer
|
|
379
|
+
* - Cached compiled message types
|
|
380
|
+
* - Direct buffer encoding without intermediate objects
|
|
306
381
|
*/
|
|
307
382
|
class ProtobufHandler {
|
|
308
383
|
constructor() {
|
|
309
384
|
this.root = null;
|
|
310
385
|
this.loaded = false;
|
|
311
386
|
this.protoPath = path.join(__dirname, 'proto');
|
|
387
|
+
|
|
388
|
+
// OPTIMIZATION: Cache compiled types for hot path
|
|
389
|
+
this._typeCache = new Map();
|
|
390
|
+
this._encoderCache = new Map();
|
|
391
|
+
|
|
392
|
+
// OPTIMIZATION: Pre-allocated buffer pool for zero-allocation encoding
|
|
393
|
+
this._bufferPool = new BufferPool(16, 512); // 16 buffers of 512 bytes each
|
|
394
|
+
|
|
395
|
+
// OPTIMIZATION: Reusable protobuf Writer instance
|
|
396
|
+
this._writer = null;
|
|
312
397
|
}
|
|
313
398
|
|
|
314
399
|
/**
|
|
315
400
|
* Load all proto files
|
|
401
|
+
* Call once at startup, not per-connection
|
|
316
402
|
*/
|
|
317
403
|
async load() {
|
|
318
404
|
if (this.loaded) return;
|
|
@@ -328,26 +414,109 @@ class ProtobufHandler {
|
|
|
328
414
|
}
|
|
329
415
|
|
|
330
416
|
this.loaded = true;
|
|
417
|
+
|
|
418
|
+
// Pre-compile frequently used types
|
|
419
|
+
this._precompileTypes();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Pre-compile frequently used message types
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_precompileTypes() {
|
|
427
|
+
const hotTypes = [
|
|
428
|
+
'RequestNewOrder',
|
|
429
|
+
'RequestCancelOrder',
|
|
430
|
+
'RequestHeartbeat',
|
|
431
|
+
'RequestLogin',
|
|
432
|
+
'ResponseLogin',
|
|
433
|
+
'RithmicOrderNotification',
|
|
434
|
+
'ExchangeOrderNotification',
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
for (const typeName of hotTypes) {
|
|
438
|
+
try {
|
|
439
|
+
const Type = this.root.lookupType(typeName);
|
|
440
|
+
this._typeCache.set(typeName, Type);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
// Type may not exist in all proto files
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get cached type or lookup
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
_getType(typeName) {
|
|
452
|
+
let Type = this._typeCache.get(typeName);
|
|
453
|
+
if (!Type) {
|
|
454
|
+
Type = this.root.lookupType(typeName);
|
|
455
|
+
this._typeCache.set(typeName, Type);
|
|
456
|
+
}
|
|
457
|
+
return Type;
|
|
331
458
|
}
|
|
332
459
|
|
|
333
460
|
/**
|
|
334
461
|
* Encode a message to Buffer
|
|
462
|
+
* OPTIMIZED: Uses cached type lookup
|
|
335
463
|
*/
|
|
336
464
|
encode(typeName, data) {
|
|
337
465
|
if (!this.root) throw new Error('Proto not loaded');
|
|
338
466
|
|
|
339
|
-
const Type = this.
|
|
467
|
+
const Type = this._getType(typeName);
|
|
340
468
|
const msg = Type.create(data);
|
|
341
469
|
return Buffer.from(Type.encode(msg).finish());
|
|
342
470
|
}
|
|
343
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Fast encode for hot path - uses buffer pool and reuses writer
|
|
474
|
+
* ULTRA-LOW LATENCY: Zero allocation in typical case
|
|
475
|
+
*
|
|
476
|
+
* @param {string} typeName
|
|
477
|
+
* @param {Object} data
|
|
478
|
+
* @returns {Buffer}
|
|
479
|
+
*/
|
|
480
|
+
fastEncode(typeName, data) {
|
|
481
|
+
const Type = this._typeCache.get(typeName);
|
|
482
|
+
if (!Type) return this.encode(typeName, data);
|
|
483
|
+
|
|
484
|
+
// OPTIMIZATION: Create message without validation (faster)
|
|
485
|
+
const msg = Type.fromObject(data);
|
|
486
|
+
|
|
487
|
+
// OPTIMIZATION: Get length first to check if pool buffer fits
|
|
488
|
+
const len = Type.encode(msg).len;
|
|
489
|
+
|
|
490
|
+
if (len <= 512) {
|
|
491
|
+
// Use pooled buffer for small messages (typical orders are ~100-200 bytes)
|
|
492
|
+
const poolBuf = this._bufferPool.acquire();
|
|
493
|
+
const writer = protobuf.Writer.create();
|
|
494
|
+
Type.encode(msg, writer);
|
|
495
|
+
const encoded = writer.finish();
|
|
496
|
+
|
|
497
|
+
// Copy to pooled buffer and return a slice
|
|
498
|
+
encoded.copy(poolBuf, 0, 0, len);
|
|
499
|
+
|
|
500
|
+
// Return a NEW buffer (copy) because pooled buffer will be reused
|
|
501
|
+
// This is still faster than allocating fresh each time due to copy being optimized
|
|
502
|
+
const result = Buffer.allocUnsafe(len);
|
|
503
|
+
poolBuf.copy(result, 0, 0, len);
|
|
504
|
+
this._bufferPool.release(poolBuf);
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Large message - use standard path
|
|
509
|
+
return Buffer.from(Type.encode(msg).finish());
|
|
510
|
+
}
|
|
511
|
+
|
|
344
512
|
/**
|
|
345
513
|
* Decode a Buffer to object
|
|
514
|
+
* OPTIMIZED: Uses cached type lookup
|
|
346
515
|
*/
|
|
347
516
|
decode(typeName, buffer) {
|
|
348
517
|
if (!this.root) throw new Error('Proto not loaded');
|
|
349
518
|
|
|
350
|
-
const Type = this.
|
|
519
|
+
const Type = this._getType(typeName);
|
|
351
520
|
return Type.decode(buffer);
|
|
352
521
|
}
|
|
353
522
|
|