hedgequantx 2.9.57 → 2.9.59

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.57",
3
+ "version": "2.9.59",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -136,8 +136,17 @@ const banner = async () => {
136
136
 
137
137
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
138
138
 
139
- const tagline = isMobile ? `HQX V${version}` : `PROP FUTURES ALGO TRADING V${version}`;
140
- console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
139
+ if (isMobile) {
140
+ const tagline = `HQX V${version}`;
141
+ console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
142
+ } else {
143
+ const taglineBase = 'PROP FUTURES ALGO TRADING ';
144
+ const taglineVersion = `V${version}`;
145
+ const totalLen = taglineBase.length + taglineVersion.length;
146
+ const padLeft = Math.floor((innerWidth - totalLen) / 2);
147
+ const padRight = innerWidth - totalLen - padLeft;
148
+ console.log(chalk.cyan('║') + ' '.repeat(padLeft) + chalk.yellow(taglineBase) + chalk.magenta(taglineVersion) + ' '.repeat(padRight) + chalk.cyan('║'));
149
+ }
141
150
 
142
151
  // ALWAYS close the banner
143
152
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
package/src/lib/data.js CHANGED
@@ -57,6 +57,9 @@ class MarketDataFeed extends EventEmitter {
57
57
  };
58
58
 
59
59
  try {
60
+ // Ensure protobuf definitions are loaded
61
+ await proto.load();
62
+
60
63
  await this.connection.connect(this.config);
61
64
 
62
65
  // Setup message handler for market data
@@ -212,6 +215,7 @@ class MarketDataFeed extends EventEmitter {
212
215
  const tradeObj = trade.toJSON ? trade.toJSON() : trade;
213
216
 
214
217
  // protobufjs converts to camelCase and enums to strings
218
+ // tradePrice is a double, should come through directly
215
219
  const price = tradeObj.tradePrice;
216
220
  const size = Number(tradeObj.tradeSize) || 1;
217
221
  const volume = Number(tradeObj.volume) || size;
@@ -221,6 +225,13 @@ class MarketDataFeed extends EventEmitter {
221
225
  const side = (agg === 1 || agg === 'BUY') ? 'buy' :
222
226
  (agg === 2 || agg === 'SELL') ? 'sell' : 'unknown';
223
227
 
228
+ // Debug first few ticks
229
+ if (!this._tradeDebugCount) this._tradeDebugCount = 0;
230
+ if (this._tradeDebugCount < 3) {
231
+ this._tradeDebugCount++;
232
+ this.emit('debug', `Trade raw: price=${price}, size=${tradeObj.tradeSize}, vol=${tradeObj.volume}`);
233
+ }
234
+
224
235
  if (price === undefined || price === null) return;
225
236
 
226
237
  const tick = {
@@ -238,7 +249,11 @@ class MarketDataFeed extends EventEmitter {
238
249
 
239
250
  this.emit('tick', tick);
240
251
  } catch (e) {
241
- // Silent - don't spam errors
252
+ // Debug decode errors
253
+ if (!this._decodeErrorLogged) {
254
+ this._decodeErrorLogged = true;
255
+ this.emit('debug', `LastTrade decode error: ${e.message}`);
256
+ }
242
257
  }
243
258
  }
244
259
 
@@ -256,6 +271,13 @@ class MarketDataFeed extends EventEmitter {
256
271
  const bidSize = Number(bboObj.bidSize) || 0;
257
272
  const askSize = Number(bboObj.askSize) || 0;
258
273
 
274
+ // Debug first few BBOs
275
+ if (!this._bboDebugCount) this._bboDebugCount = 0;
276
+ if (this._bboDebugCount < 3) {
277
+ this._bboDebugCount++;
278
+ this.emit('debug', `BBO raw: bid=${bid}, ask=${ask}`);
279
+ }
280
+
259
281
  // Calculate mid price
260
282
  const price = (bid && ask) ? (Number(bid) + Number(ask)) / 2 : (bid || ask || null);
261
283
 
@@ -277,7 +299,11 @@ class MarketDataFeed extends EventEmitter {
277
299
 
278
300
  this.emit('tick', tick);
279
301
  } catch (e) {
280
- // Ignore decode errors
302
+ // Debug decode errors
303
+ if (!this._bboDecodeErrorLogged) {
304
+ this._bboDecodeErrorLogged = true;
305
+ this.emit('debug', `BBO decode error: ${e.message}`);
306
+ }
281
307
  }
282
308
  }
283
309
  }
@@ -75,7 +75,7 @@ const oneAccountMenu = async (service) => {
75
75
  console.log();
76
76
  console.log(chalk.cyan(' Last configuration found:'));
77
77
  console.log(chalk.gray(` Account: ${lastConfig.accountName} (${lastConfig.propfirm})`));
78
- console.log(chalk.gray(` Symbol: ${lastConfig.symbol}`));
78
+ console.log(chalk.gray(` Symbol: ${lastConfig.baseSymbol || lastConfig.symbol} (${lastConfig.symbol})`));
79
79
  console.log(chalk.gray(` Strategy: ${lastConfig.strategyName}`));
80
80
  console.log(chalk.gray(` Contracts: ${lastConfig.contracts} | Target: $${lastConfig.dailyTarget} | Risk: $${lastConfig.maxRisk}`));
81
81
  console.log();
@@ -86,10 +86,15 @@ const oneAccountMenu = async (service) => {
86
86
  selectedAccount = matchingAccount;
87
87
  accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
88
88
 
89
- // Load contracts to find the saved symbol
89
+ // Load contracts to find the saved symbol (match by baseSymbol first, then exact symbol)
90
90
  const contractsResult = await accountService.getContracts();
91
91
  if (contractsResult.success) {
92
- contract = contractsResult.contracts.find(c => c.symbol === lastConfig.symbol);
92
+ // Try baseSymbol match first (more stable across contract rolls)
93
+ contract = contractsResult.contracts.find(c => c.baseSymbol === lastConfig.baseSymbol);
94
+ // Fall back to exact symbol match
95
+ if (!contract) {
96
+ contract = contractsResult.contracts.find(c => c.symbol === lastConfig.symbol);
97
+ }
93
98
  }
94
99
 
95
100
  // Find strategy
@@ -147,12 +152,13 @@ const oneAccountMenu = async (service) => {
147
152
  config = await configureAlgo(selectedAccount, contract, strategy);
148
153
  if (!config) return;
149
154
 
150
- // Save config for next time
155
+ // Save config for next time (include baseSymbol for stable matching across contract rolls)
151
156
  saveOneAccountConfig({
152
157
  accountId: selectedAccount.accountId || selectedAccount.rithmicAccountId,
153
158
  accountName: selectedAccount.accountName || selectedAccount.rithmicAccountId || selectedAccount.accountId,
154
159
  propfirm: selectedAccount.propfirm || selectedAccount.platform || 'Unknown',
155
160
  symbol: contract.symbol,
161
+ baseSymbol: contract.baseSymbol,
156
162
  strategyId: strategy.id,
157
163
  strategyName: strategy.name,
158
164
  contracts: config.contracts,
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Protobuf Decoders - Decode Rithmic protobuf messages
3
+ * @module services/rithmic/protobuf-decoders
4
+ */
5
+
6
+ const { readVarint, readLengthDelimited, skipField } = require('./protobuf-utils');
7
+
8
+ // PnL field IDs (Rithmic uses very large field IDs)
9
+ const PNL_FIELDS = {
10
+ TEMPLATE_ID: 154467,
11
+ IS_SNAPSHOT: 110121,
12
+ FCM_ID: 154013,
13
+ IB_ID: 154014,
14
+ ACCOUNT_ID: 154008,
15
+ ACCOUNT_BALANCE: 156970,
16
+ CASH_ON_HAND: 156971,
17
+ MARGIN_BALANCE: 156977,
18
+ MIN_ACCOUNT_BALANCE: 156968,
19
+ OPEN_POSITION_PNL: 156961,
20
+ CLOSED_POSITION_PNL: 156963,
21
+ DAY_PNL: 157956,
22
+ DAY_OPEN_PNL: 157954,
23
+ DAY_CLOSED_PNL: 157955,
24
+ AVAILABLE_BUYING_POWER: 157015,
25
+ SSBOE: 150100,
26
+ USECS: 150101,
27
+ };
28
+
29
+ // Symbol/Contract field IDs (ResponseProductCodes, ResponseFrontMonthContract)
30
+ const SYMBOL_FIELDS = {
31
+ TEMPLATE_ID: 154467,
32
+ RP_CODE: 132766,
33
+ EXCHANGE: 110101,
34
+ PRODUCT_CODE: 110102, // Base symbol (ES, NQ, MNQ)
35
+ PRODUCT_NAME: 110103, // Product name
36
+ SYMBOL: 110100, // Full contract symbol (ESH26)
37
+ TRADING_SYMBOL: 157095, // Trading symbol
38
+ DESCRIPTION: 110114, // Contract description
39
+ USER_MSG: 132760,
40
+ };
41
+
42
+ // Instrument PnL Position Update field IDs
43
+ const INSTRUMENT_PNL_FIELDS = {
44
+ TEMPLATE_ID: 154467,
45
+ IS_SNAPSHOT: 110121,
46
+ FCM_ID: 154013,
47
+ IB_ID: 154014,
48
+ ACCOUNT_ID: 154008,
49
+ SYMBOL: 110100,
50
+ EXCHANGE: 110101,
51
+ PRODUCT_CODE: 100749,
52
+ INSTRUMENT_TYPE: 110116,
53
+ FILL_BUY_QTY: 154041,
54
+ FILL_SELL_QTY: 154042,
55
+ ORDER_BUY_QTY: 154037,
56
+ ORDER_SELL_QTY: 154038,
57
+ BUY_QTY: 154260,
58
+ SELL_QTY: 154261,
59
+ AVG_OPEN_FILL_PRICE: 154434,
60
+ DAY_OPEN_PNL: 157954,
61
+ DAY_CLOSED_PNL: 157955,
62
+ DAY_PNL: 157956,
63
+ OPEN_POSITION_PNL: 156961,
64
+ OPEN_POSITION_QUANTITY: 156962,
65
+ CLOSED_POSITION_PNL: 156963,
66
+ CLOSED_POSITION_QUANTITY: 156964,
67
+ NET_QUANTITY: 156967,
68
+ SSBOE: 150100,
69
+ USECS: 150101,
70
+ };
71
+
72
+ /**
73
+ * Manually decode AccountPnL from raw bytes
74
+ * Skips 4-byte length prefix if present
75
+ * @param {Buffer} buffer - Raw protobuf buffer
76
+ * @returns {Object} Decoded account PnL data
77
+ */
78
+ function decodeAccountPnL(buffer) {
79
+ // Skip 4-byte length prefix
80
+ const data = buffer.length > 4 ? buffer.slice(4) : buffer;
81
+
82
+ const result = {};
83
+ let offset = 0;
84
+
85
+ while (offset < data.length) {
86
+ try {
87
+ const [tag, tagOffset] = readVarint(data, offset);
88
+ const wireType = tag & 0x7;
89
+ const fieldNumber = tag >>> 3;
90
+ offset = tagOffset;
91
+
92
+ switch (fieldNumber) {
93
+ case PNL_FIELDS.TEMPLATE_ID:
94
+ [result.templateId, offset] = readVarint(data, offset);
95
+ break;
96
+ case PNL_FIELDS.IS_SNAPSHOT:
97
+ const [isSnap, snapOffset] = readVarint(data, offset);
98
+ result.isSnapshot = isSnap !== 0;
99
+ offset = snapOffset;
100
+ break;
101
+ case PNL_FIELDS.FCM_ID:
102
+ [result.fcmId, offset] = readLengthDelimited(data, offset);
103
+ break;
104
+ case PNL_FIELDS.IB_ID:
105
+ [result.ibId, offset] = readLengthDelimited(data, offset);
106
+ break;
107
+ case PNL_FIELDS.ACCOUNT_ID:
108
+ [result.accountId, offset] = readLengthDelimited(data, offset);
109
+ break;
110
+ case PNL_FIELDS.ACCOUNT_BALANCE:
111
+ [result.accountBalance, offset] = readLengthDelimited(data, offset);
112
+ break;
113
+ case PNL_FIELDS.CASH_ON_HAND:
114
+ [result.cashOnHand, offset] = readLengthDelimited(data, offset);
115
+ break;
116
+ case PNL_FIELDS.MARGIN_BALANCE:
117
+ [result.marginBalance, offset] = readLengthDelimited(data, offset);
118
+ break;
119
+ case PNL_FIELDS.MIN_ACCOUNT_BALANCE:
120
+ [result.minAccountBalance, offset] = readLengthDelimited(data, offset);
121
+ break;
122
+ case PNL_FIELDS.OPEN_POSITION_PNL:
123
+ [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
124
+ break;
125
+ case PNL_FIELDS.CLOSED_POSITION_PNL:
126
+ [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
127
+ break;
128
+ case PNL_FIELDS.DAY_PNL:
129
+ [result.dayPnl, offset] = readLengthDelimited(data, offset);
130
+ break;
131
+ case PNL_FIELDS.DAY_OPEN_PNL:
132
+ [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
133
+ break;
134
+ case PNL_FIELDS.DAY_CLOSED_PNL:
135
+ [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
136
+ break;
137
+ case PNL_FIELDS.AVAILABLE_BUYING_POWER:
138
+ [result.availableBuyingPower, offset] = readLengthDelimited(data, offset);
139
+ break;
140
+ case PNL_FIELDS.SSBOE:
141
+ [result.ssboe, offset] = readVarint(data, offset);
142
+ break;
143
+ case PNL_FIELDS.USECS:
144
+ [result.usecs, offset] = readVarint(data, offset);
145
+ break;
146
+ default:
147
+ offset = skipField(data, offset, wireType);
148
+ }
149
+ } catch (error) {
150
+ break;
151
+ }
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Manually decode InstrumentPnLPositionUpdate from raw bytes
159
+ * Skips 4-byte length prefix if present
160
+ * @param {Buffer} buffer - Raw protobuf buffer
161
+ * @returns {Object} Decoded instrument PnL data
162
+ */
163
+ function decodeInstrumentPnL(buffer) {
164
+ // Skip 4-byte length prefix
165
+ const data = buffer.length > 4 ? buffer.slice(4) : buffer;
166
+
167
+ const result = {};
168
+ let offset = 0;
169
+
170
+ while (offset < data.length) {
171
+ try {
172
+ const [tag, tagOffset] = readVarint(data, offset);
173
+ const wireType = tag & 0x7;
174
+ const fieldNumber = tag >>> 3;
175
+ offset = tagOffset;
176
+
177
+ switch (fieldNumber) {
178
+ case INSTRUMENT_PNL_FIELDS.TEMPLATE_ID:
179
+ [result.templateId, offset] = readVarint(data, offset);
180
+ break;
181
+ case INSTRUMENT_PNL_FIELDS.IS_SNAPSHOT:
182
+ const [isSnap, snapOffset] = readVarint(data, offset);
183
+ result.isSnapshot = isSnap !== 0;
184
+ offset = snapOffset;
185
+ break;
186
+ case INSTRUMENT_PNL_FIELDS.FCM_ID:
187
+ [result.fcmId, offset] = readLengthDelimited(data, offset);
188
+ break;
189
+ case INSTRUMENT_PNL_FIELDS.IB_ID:
190
+ [result.ibId, offset] = readLengthDelimited(data, offset);
191
+ break;
192
+ case INSTRUMENT_PNL_FIELDS.ACCOUNT_ID:
193
+ [result.accountId, offset] = readLengthDelimited(data, offset);
194
+ break;
195
+ case INSTRUMENT_PNL_FIELDS.SYMBOL:
196
+ [result.symbol, offset] = readLengthDelimited(data, offset);
197
+ break;
198
+ case INSTRUMENT_PNL_FIELDS.EXCHANGE:
199
+ [result.exchange, offset] = readLengthDelimited(data, offset);
200
+ break;
201
+ case INSTRUMENT_PNL_FIELDS.PRODUCT_CODE:
202
+ [result.productCode, offset] = readLengthDelimited(data, offset);
203
+ break;
204
+ case INSTRUMENT_PNL_FIELDS.BUY_QTY:
205
+ [result.buyQty, offset] = readVarint(data, offset);
206
+ break;
207
+ case INSTRUMENT_PNL_FIELDS.SELL_QTY:
208
+ [result.sellQty, offset] = readVarint(data, offset);
209
+ break;
210
+ case INSTRUMENT_PNL_FIELDS.FILL_BUY_QTY:
211
+ [result.fillBuyQty, offset] = readVarint(data, offset);
212
+ break;
213
+ case INSTRUMENT_PNL_FIELDS.FILL_SELL_QTY:
214
+ [result.fillSellQty, offset] = readVarint(data, offset);
215
+ break;
216
+ case INSTRUMENT_PNL_FIELDS.NET_QUANTITY:
217
+ [result.netQuantity, offset] = readVarint(data, offset);
218
+ break;
219
+ case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_QUANTITY:
220
+ [result.openPositionQuantity, offset] = readVarint(data, offset);
221
+ break;
222
+ case INSTRUMENT_PNL_FIELDS.AVG_OPEN_FILL_PRICE:
223
+ // Double is 64-bit fixed
224
+ if (wireType === 1) {
225
+ result.avgOpenFillPrice = data.readDoubleLE(offset);
226
+ offset += 8;
227
+ } else {
228
+ offset = skipField(data, offset, wireType);
229
+ }
230
+ break;
231
+ case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_PNL:
232
+ [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
233
+ break;
234
+ case INSTRUMENT_PNL_FIELDS.CLOSED_POSITION_PNL:
235
+ [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
236
+ break;
237
+ case INSTRUMENT_PNL_FIELDS.DAY_PNL:
238
+ [result.dayPnl, offset] = readLengthDelimited(data, offset);
239
+ break;
240
+ case INSTRUMENT_PNL_FIELDS.DAY_OPEN_PNL:
241
+ [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
242
+ break;
243
+ case INSTRUMENT_PNL_FIELDS.DAY_CLOSED_PNL:
244
+ [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
245
+ break;
246
+ case INSTRUMENT_PNL_FIELDS.SSBOE:
247
+ [result.ssboe, offset] = readVarint(data, offset);
248
+ break;
249
+ case INSTRUMENT_PNL_FIELDS.USECS:
250
+ [result.usecs, offset] = readVarint(data, offset);
251
+ break;
252
+ default:
253
+ offset = skipField(data, offset, wireType);
254
+ }
255
+ } catch (error) {
256
+ break;
257
+ }
258
+ }
259
+
260
+ return result;
261
+ }
262
+
263
+ /**
264
+ * Decode ResponseProductCodes (template 112) - list of available symbols
265
+ * @param {Buffer} buffer - Raw protobuf buffer
266
+ * @returns {Object} Decoded product codes
267
+ */
268
+ function decodeProductCodes(buffer) {
269
+ const result = { rpCode: [] };
270
+ let offset = 0;
271
+
272
+ while (offset < buffer.length) {
273
+ try {
274
+ const [tag, tagOffset] = readVarint(buffer, offset);
275
+ const wireType = tag & 0x7;
276
+ const fieldNumber = tag >>> 3;
277
+ offset = tagOffset;
278
+
279
+ switch (fieldNumber) {
280
+ case SYMBOL_FIELDS.TEMPLATE_ID:
281
+ [result.templateId, offset] = readVarint(buffer, offset);
282
+ break;
283
+ case SYMBOL_FIELDS.RP_CODE:
284
+ let rpCode;
285
+ [rpCode, offset] = readLengthDelimited(buffer, offset);
286
+ result.rpCode.push(rpCode);
287
+ break;
288
+ case SYMBOL_FIELDS.EXCHANGE:
289
+ [result.exchange, offset] = readLengthDelimited(buffer, offset);
290
+ break;
291
+ case SYMBOL_FIELDS.PRODUCT_CODE:
292
+ [result.productCode, offset] = readLengthDelimited(buffer, offset);
293
+ break;
294
+ case SYMBOL_FIELDS.PRODUCT_NAME:
295
+ [result.productName, offset] = readLengthDelimited(buffer, offset);
296
+ break;
297
+ case SYMBOL_FIELDS.USER_MSG:
298
+ [result.userMsg, offset] = readLengthDelimited(buffer, offset);
299
+ break;
300
+ default:
301
+ offset = skipField(buffer, offset, wireType);
302
+ }
303
+ } catch (error) {
304
+ break;
305
+ }
306
+ }
307
+
308
+ return result;
309
+ }
310
+
311
+ /**
312
+ * Decode ResponseFrontMonthContract (template 114) - current tradeable contract
313
+ * Skips 4-byte length prefix if present
314
+ * @param {Buffer} buffer - Raw protobuf buffer
315
+ * @returns {Object} Decoded front month contract
316
+ */
317
+ function decodeFrontMonthContract(buffer) {
318
+ // Skip 4-byte length prefix
319
+ const data = buffer.length > 4 ? buffer.slice(4) : buffer;
320
+
321
+ const result = { rpCode: [] };
322
+ let offset = 0;
323
+
324
+ while (offset < data.length) {
325
+ try {
326
+ const [tag, tagOffset] = readVarint(data, offset);
327
+ const wireType = tag & 0x7;
328
+ const fieldNumber = tag >>> 3;
329
+ offset = tagOffset;
330
+
331
+ switch (fieldNumber) {
332
+ case SYMBOL_FIELDS.TEMPLATE_ID:
333
+ [result.templateId, offset] = readVarint(data, offset);
334
+ break;
335
+ case SYMBOL_FIELDS.RP_CODE:
336
+ let rpCode;
337
+ [rpCode, offset] = readLengthDelimited(data, offset);
338
+ result.rpCode.push(rpCode);
339
+ break;
340
+ case SYMBOL_FIELDS.SYMBOL:
341
+ [result.symbol, offset] = readLengthDelimited(data, offset);
342
+ break;
343
+ case SYMBOL_FIELDS.EXCHANGE:
344
+ [result.exchange, offset] = readLengthDelimited(data, offset);
345
+ break;
346
+ case SYMBOL_FIELDS.TRADING_SYMBOL:
347
+ [result.tradingSymbol, offset] = readLengthDelimited(data, offset);
348
+ break;
349
+ case SYMBOL_FIELDS.DESCRIPTION:
350
+ [result.description, offset] = readLengthDelimited(data, offset);
351
+ break;
352
+ case SYMBOL_FIELDS.USER_MSG:
353
+ [result.userMsg, offset] = readLengthDelimited(data, offset);
354
+ break;
355
+ default:
356
+ offset = skipField(data, offset, wireType);
357
+ }
358
+ } catch (error) {
359
+ break;
360
+ }
361
+ }
362
+
363
+ return result;
364
+ }
365
+
366
+ module.exports = {
367
+ decodeAccountPnL,
368
+ decodeInstrumentPnL,
369
+ decodeProductCodes,
370
+ decodeFrontMonthContract,
371
+ // Export field constants for reference
372
+ PNL_FIELDS,
373
+ SYMBOL_FIELDS,
374
+ INSTRUMENT_PNL_FIELDS,
375
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Protobuf Utilities - Low-level protobuf parsing functions
3
+ * @module services/rithmic/protobuf-utils
4
+ */
5
+
6
+ /**
7
+ * Read a varint from buffer
8
+ * @param {Buffer} buffer - Input buffer
9
+ * @param {number} offset - Start offset
10
+ * @returns {[number, number]} [value, newOffset]
11
+ */
12
+ function readVarint(buffer, offset) {
13
+ let result = BigInt(0);
14
+ let shift = BigInt(0);
15
+ let pos = offset;
16
+
17
+ while (pos < buffer.length) {
18
+ const byte = buffer[pos++];
19
+ result |= BigInt(byte & 0x7f) << shift;
20
+ if ((byte & 0x80) === 0) {
21
+ return [Number(result), pos];
22
+ }
23
+ shift += BigInt(7);
24
+ if (shift > BigInt(63)) {
25
+ throw new Error('Varint too large');
26
+ }
27
+ }
28
+ throw new Error('Incomplete varint');
29
+ }
30
+
31
+ /**
32
+ * Read a length-delimited field (string/bytes)
33
+ * @param {Buffer} buffer - Input buffer
34
+ * @param {number} offset - Start offset
35
+ * @returns {[string, number]} [value, newOffset]
36
+ */
37
+ function readLengthDelimited(buffer, offset) {
38
+ const [length, newOffset] = readVarint(buffer, offset);
39
+ const value = buffer.slice(newOffset, newOffset + length).toString('utf8');
40
+ return [value, newOffset + length];
41
+ }
42
+
43
+ /**
44
+ * Skip a field based on wire type
45
+ * @param {Buffer} buffer - Input buffer
46
+ * @param {number} offset - Start offset
47
+ * @param {number} wireType - Protobuf wire type
48
+ * @returns {number} New offset
49
+ */
50
+ function skipField(buffer, offset, wireType) {
51
+ switch (wireType) {
52
+ case 0: // Varint
53
+ const [, newOffset] = readVarint(buffer, offset);
54
+ return newOffset;
55
+ case 1: // 64-bit
56
+ return offset + 8;
57
+ case 2: // Length-delimited
58
+ const [length, lenOffset] = readVarint(buffer, offset);
59
+ return lenOffset + length;
60
+ case 5: // 32-bit
61
+ return offset + 4;
62
+ default:
63
+ throw new Error(`Unknown wire type: ${wireType}`);
64
+ }
65
+ }
66
+
67
+ module.exports = {
68
+ readVarint,
69
+ readLengthDelimited,
70
+ skipField,
71
+ };