hedgequantx 2.9.57 → 2.9.58

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.58",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
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
+ };
@@ -1,316 +1,23 @@
1
1
  /**
2
2
  * Rithmic Protobuf Handler
3
- * Handles encoding/decoding of Rithmic protobuf messages
3
+ * Main interface for encoding/decoding Rithmic protobuf messages
4
+ * @module services/rithmic/protobuf
4
5
  */
5
6
 
6
7
  const protobuf = require('protobufjs');
7
8
  const path = require('path');
8
9
  const { PROTO_FILES } = require('./constants');
9
-
10
- // PnL field IDs (Rithmic uses very large field IDs)
11
- const PNL_FIELDS = {
12
- TEMPLATE_ID: 154467,
13
- IS_SNAPSHOT: 110121,
14
- FCM_ID: 154013,
15
- IB_ID: 154014,
16
- ACCOUNT_ID: 154008,
17
- ACCOUNT_BALANCE: 156970,
18
- CASH_ON_HAND: 156971,
19
- MARGIN_BALANCE: 156977,
20
- MIN_ACCOUNT_BALANCE: 156968,
21
- OPEN_POSITION_PNL: 156961,
22
- CLOSED_POSITION_PNL: 156963,
23
- DAY_PNL: 157956,
24
- DAY_OPEN_PNL: 157954,
25
- DAY_CLOSED_PNL: 157955,
26
- AVAILABLE_BUYING_POWER: 157015,
27
- SSBOE: 150100,
28
- USECS: 150101,
29
- };
30
-
31
- // Symbol/Contract field IDs (ResponseProductCodes, ResponseFrontMonthContract)
32
- const SYMBOL_FIELDS = {
33
- TEMPLATE_ID: 154467,
34
- RP_CODE: 132766,
35
- EXCHANGE: 110101,
36
- PRODUCT_CODE: 110102, // Base symbol (ES, NQ, MNQ)
37
- PRODUCT_NAME: 110103, // Product name
38
- SYMBOL: 110100, // Full contract symbol (ESH26)
39
- TRADING_SYMBOL: 157095, // Trading symbol
40
- DESCRIPTION: 110114, // Contract description
41
- USER_MSG: 132760,
42
- };
43
-
44
- // Instrument PnL Position Update field IDs
45
- const INSTRUMENT_PNL_FIELDS = {
46
- TEMPLATE_ID: 154467,
47
- IS_SNAPSHOT: 110121,
48
- FCM_ID: 154013,
49
- IB_ID: 154014,
50
- ACCOUNT_ID: 154008,
51
- SYMBOL: 110100,
52
- EXCHANGE: 110101,
53
- PRODUCT_CODE: 100749,
54
- INSTRUMENT_TYPE: 110116,
55
- FILL_BUY_QTY: 154041,
56
- FILL_SELL_QTY: 154042,
57
- ORDER_BUY_QTY: 154037,
58
- ORDER_SELL_QTY: 154038,
59
- BUY_QTY: 154260,
60
- SELL_QTY: 154261,
61
- AVG_OPEN_FILL_PRICE: 154434,
62
- DAY_OPEN_PNL: 157954,
63
- DAY_CLOSED_PNL: 157955,
64
- DAY_PNL: 157956,
65
- OPEN_POSITION_PNL: 156961,
66
- OPEN_POSITION_QUANTITY: 156962,
67
- CLOSED_POSITION_PNL: 156963,
68
- CLOSED_POSITION_QUANTITY: 156964,
69
- NET_QUANTITY: 156967,
70
- SSBOE: 150100,
71
- USECS: 150101,
72
- };
73
-
74
- /**
75
- * Read a varint from buffer
76
- */
77
- function readVarint(buffer, offset) {
78
- let result = BigInt(0);
79
- let shift = BigInt(0);
80
- let pos = offset;
81
-
82
- while (pos < buffer.length) {
83
- const byte = buffer[pos++];
84
- result |= BigInt(byte & 0x7f) << shift;
85
- if ((byte & 0x80) === 0) {
86
- return [Number(result), pos];
87
- }
88
- shift += BigInt(7);
89
- if (shift > BigInt(63)) {
90
- throw new Error('Varint too large');
91
- }
92
- }
93
- throw new Error('Incomplete varint');
94
- }
95
-
96
- /**
97
- * Read a length-delimited field (string/bytes)
98
- */
99
- function readLengthDelimited(buffer, offset) {
100
- const [length, newOffset] = readVarint(buffer, offset);
101
- const value = buffer.slice(newOffset, newOffset + length).toString('utf8');
102
- return [value, newOffset + length];
103
- }
104
-
105
- /**
106
- * Skip a field based on wire type
107
- */
108
- function skipField(buffer, offset, wireType) {
109
- switch (wireType) {
110
- case 0: // Varint
111
- const [, newOffset] = readVarint(buffer, offset);
112
- return newOffset;
113
- case 1: // 64-bit
114
- return offset + 8;
115
- case 2: // Length-delimited
116
- const [length, lenOffset] = readVarint(buffer, offset);
117
- return lenOffset + length;
118
- case 5: // 32-bit
119
- return offset + 4;
120
- default:
121
- throw new Error(`Unknown wire type: ${wireType}`);
122
- }
123
- }
124
-
125
- /**
126
- * Manually decode AccountPnL from raw bytes
127
- * Skips 4-byte length prefix if present
128
- */
129
- function decodeAccountPnL(buffer) {
130
- // Skip 4-byte length prefix
131
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
132
-
133
- const result = {};
134
- let offset = 0;
135
-
136
- while (offset < data.length) {
137
- try {
138
- const [tag, tagOffset] = readVarint(data, offset);
139
- const wireType = tag & 0x7;
140
- const fieldNumber = tag >>> 3;
141
- offset = tagOffset;
142
-
143
- switch (fieldNumber) {
144
- case PNL_FIELDS.TEMPLATE_ID:
145
- [result.templateId, offset] = readVarint(data, offset);
146
- break;
147
- case PNL_FIELDS.IS_SNAPSHOT:
148
- const [isSnap, snapOffset] = readVarint(data, offset);
149
- result.isSnapshot = isSnap !== 0;
150
- offset = snapOffset;
151
- break;
152
- case PNL_FIELDS.FCM_ID:
153
- [result.fcmId, offset] = readLengthDelimited(data, offset);
154
- break;
155
- case PNL_FIELDS.IB_ID:
156
- [result.ibId, offset] = readLengthDelimited(data, offset);
157
- break;
158
- case PNL_FIELDS.ACCOUNT_ID:
159
- [result.accountId, offset] = readLengthDelimited(data, offset);
160
- break;
161
- case PNL_FIELDS.ACCOUNT_BALANCE:
162
- [result.accountBalance, offset] = readLengthDelimited(data, offset);
163
- break;
164
- case PNL_FIELDS.CASH_ON_HAND:
165
- [result.cashOnHand, offset] = readLengthDelimited(data, offset);
166
- break;
167
- case PNL_FIELDS.MARGIN_BALANCE:
168
- [result.marginBalance, offset] = readLengthDelimited(data, offset);
169
- break;
170
- case PNL_FIELDS.MIN_ACCOUNT_BALANCE:
171
- [result.minAccountBalance, offset] = readLengthDelimited(data, offset);
172
- break;
173
- case PNL_FIELDS.OPEN_POSITION_PNL:
174
- [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
175
- break;
176
- case PNL_FIELDS.CLOSED_POSITION_PNL:
177
- [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
178
- break;
179
- case PNL_FIELDS.DAY_PNL:
180
- [result.dayPnl, offset] = readLengthDelimited(data, offset);
181
- break;
182
- case PNL_FIELDS.DAY_OPEN_PNL:
183
- [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
184
- break;
185
- case PNL_FIELDS.DAY_CLOSED_PNL:
186
- [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
187
- break;
188
- case PNL_FIELDS.AVAILABLE_BUYING_POWER:
189
- [result.availableBuyingPower, offset] = readLengthDelimited(data, offset);
190
- break;
191
- case PNL_FIELDS.SSBOE:
192
- [result.ssboe, offset] = readVarint(data, offset);
193
- break;
194
- case PNL_FIELDS.USECS:
195
- [result.usecs, offset] = readVarint(data, offset);
196
- break;
197
- default:
198
- offset = skipField(data, offset, wireType);
199
- }
200
- } catch (error) {
201
- break;
202
- }
203
- }
204
-
205
- return result;
206
- }
207
-
208
- /**
209
- * Manually decode InstrumentPnLPositionUpdate from raw bytes
210
- * Skips 4-byte length prefix if present
211
- */
212
- function decodeInstrumentPnL(buffer) {
213
- // Skip 4-byte length prefix
214
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
215
-
216
- const result = {};
217
- let offset = 0;
218
-
219
- while (offset < data.length) {
220
- try {
221
- const [tag, tagOffset] = readVarint(data, offset);
222
- const wireType = tag & 0x7;
223
- const fieldNumber = tag >>> 3;
224
- offset = tagOffset;
225
-
226
- switch (fieldNumber) {
227
- case INSTRUMENT_PNL_FIELDS.TEMPLATE_ID:
228
- [result.templateId, offset] = readVarint(data, offset);
229
- break;
230
- case INSTRUMENT_PNL_FIELDS.IS_SNAPSHOT:
231
- const [isSnap, snapOffset] = readVarint(data, offset);
232
- result.isSnapshot = isSnap !== 0;
233
- offset = snapOffset;
234
- break;
235
- case INSTRUMENT_PNL_FIELDS.FCM_ID:
236
- [result.fcmId, offset] = readLengthDelimited(data, offset);
237
- break;
238
- case INSTRUMENT_PNL_FIELDS.IB_ID:
239
- [result.ibId, offset] = readLengthDelimited(data, offset);
240
- break;
241
- case INSTRUMENT_PNL_FIELDS.ACCOUNT_ID:
242
- [result.accountId, offset] = readLengthDelimited(data, offset);
243
- break;
244
- case INSTRUMENT_PNL_FIELDS.SYMBOL:
245
- [result.symbol, offset] = readLengthDelimited(data, offset);
246
- break;
247
- case INSTRUMENT_PNL_FIELDS.EXCHANGE:
248
- [result.exchange, offset] = readLengthDelimited(data, offset);
249
- break;
250
- case INSTRUMENT_PNL_FIELDS.PRODUCT_CODE:
251
- [result.productCode, offset] = readLengthDelimited(data, offset);
252
- break;
253
- case INSTRUMENT_PNL_FIELDS.BUY_QTY:
254
- [result.buyQty, offset] = readVarint(data, offset);
255
- break;
256
- case INSTRUMENT_PNL_FIELDS.SELL_QTY:
257
- [result.sellQty, offset] = readVarint(data, offset);
258
- break;
259
- case INSTRUMENT_PNL_FIELDS.FILL_BUY_QTY:
260
- [result.fillBuyQty, offset] = readVarint(data, offset);
261
- break;
262
- case INSTRUMENT_PNL_FIELDS.FILL_SELL_QTY:
263
- [result.fillSellQty, offset] = readVarint(data, offset);
264
- break;
265
- case INSTRUMENT_PNL_FIELDS.NET_QUANTITY:
266
- [result.netQuantity, offset] = readVarint(data, offset);
267
- break;
268
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_QUANTITY:
269
- [result.openPositionQuantity, offset] = readVarint(data, offset);
270
- break;
271
- case INSTRUMENT_PNL_FIELDS.AVG_OPEN_FILL_PRICE:
272
- // Double is 64-bit fixed
273
- if (wireType === 1) {
274
- result.avgOpenFillPrice = data.readDoubleLE(offset);
275
- offset += 8;
276
- } else {
277
- offset = skipField(data, offset, wireType);
278
- }
279
- break;
280
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_PNL:
281
- [result.openPositionPnl, offset] = readLengthDelimited(data, offset);
282
- break;
283
- case INSTRUMENT_PNL_FIELDS.CLOSED_POSITION_PNL:
284
- [result.closedPositionPnl, offset] = readLengthDelimited(data, offset);
285
- break;
286
- case INSTRUMENT_PNL_FIELDS.DAY_PNL:
287
- [result.dayPnl, offset] = readLengthDelimited(data, offset);
288
- break;
289
- case INSTRUMENT_PNL_FIELDS.DAY_OPEN_PNL:
290
- [result.dayOpenPnl, offset] = readLengthDelimited(data, offset);
291
- break;
292
- case INSTRUMENT_PNL_FIELDS.DAY_CLOSED_PNL:
293
- [result.dayClosedPnl, offset] = readLengthDelimited(data, offset);
294
- break;
295
- case INSTRUMENT_PNL_FIELDS.SSBOE:
296
- [result.ssboe, offset] = readVarint(data, offset);
297
- break;
298
- case INSTRUMENT_PNL_FIELDS.USECS:
299
- [result.usecs, offset] = readVarint(data, offset);
300
- break;
301
- default:
302
- offset = skipField(data, offset, wireType);
303
- }
304
- } catch (error) {
305
- break;
306
- }
307
- }
308
-
309
- return result;
310
- }
10
+ const { readVarint, readLengthDelimited, skipField } = require('./protobuf-utils');
11
+ const {
12
+ decodeAccountPnL,
13
+ decodeInstrumentPnL,
14
+ decodeProductCodes,
15
+ decodeFrontMonthContract,
16
+ } = require('./protobuf-decoders');
311
17
 
312
18
  /**
313
19
  * Protobuf Handler class
20
+ * Handles loading proto files and encoding/decoding messages
314
21
  */
315
22
  class ProtobufHandler {
316
23
  constructor() {
@@ -340,6 +47,9 @@ class ProtobufHandler {
340
47
 
341
48
  /**
342
49
  * Encode a message to Buffer with 4-byte length prefix
50
+ * @param {string} typeName - Protobuf type name
51
+ * @param {Object} data - Data to encode
52
+ * @returns {Buffer} Encoded buffer with length prefix
343
53
  */
344
54
  encode(typeName, data) {
345
55
  if (!this.root) throw new Error('Proto not loaded');
@@ -357,6 +67,9 @@ class ProtobufHandler {
357
67
 
358
68
  /**
359
69
  * Decode a Buffer to object (skip 4-byte length prefix)
70
+ * @param {string} typeName - Protobuf type name
71
+ * @param {Buffer} buffer - Buffer to decode
72
+ * @returns {Object} Decoded message
360
73
  */
361
74
  decode(typeName, buffer) {
362
75
  if (!this.root) throw new Error('Proto not loaded');
@@ -371,6 +84,8 @@ class ProtobufHandler {
371
84
  /**
372
85
  * Get template ID from buffer (manual decode for large field IDs)
373
86
  * Skips 4-byte length prefix if present
87
+ * @param {Buffer} buffer - Buffer to parse
88
+ * @returns {number} Template ID or -1 if not found
374
89
  */
375
90
  getTemplateId(buffer) {
376
91
  const TEMPLATE_ID_FIELD = 154467;
@@ -411,106 +126,7 @@ class ProtobufHandler {
411
126
  }
412
127
  }
413
128
 
414
- /**
415
- * Decode ResponseProductCodes (template 112) - list of available symbols
416
- */
417
- function decodeProductCodes(buffer) {
418
- const result = { rpCode: [] };
419
- let offset = 0;
420
-
421
- while (offset < buffer.length) {
422
- try {
423
- const [tag, tagOffset] = readVarint(buffer, offset);
424
- const wireType = tag & 0x7;
425
- const fieldNumber = tag >>> 3;
426
- offset = tagOffset;
427
-
428
- switch (fieldNumber) {
429
- case SYMBOL_FIELDS.TEMPLATE_ID:
430
- [result.templateId, offset] = readVarint(buffer, offset);
431
- break;
432
- case SYMBOL_FIELDS.RP_CODE:
433
- let rpCode;
434
- [rpCode, offset] = readLengthDelimited(buffer, offset);
435
- result.rpCode.push(rpCode);
436
- break;
437
- case SYMBOL_FIELDS.EXCHANGE:
438
- [result.exchange, offset] = readLengthDelimited(buffer, offset);
439
- break;
440
- case SYMBOL_FIELDS.PRODUCT_CODE:
441
- [result.productCode, offset] = readLengthDelimited(buffer, offset);
442
- break;
443
- case SYMBOL_FIELDS.PRODUCT_NAME:
444
- [result.productName, offset] = readLengthDelimited(buffer, offset);
445
- break;
446
- case SYMBOL_FIELDS.USER_MSG:
447
- [result.userMsg, offset] = readLengthDelimited(buffer, offset);
448
- break;
449
- default:
450
- offset = skipField(buffer, offset, wireType);
451
- }
452
- } catch (error) {
453
- break;
454
- }
455
- }
456
-
457
- return result;
458
- }
459
-
460
- /**
461
- * Decode ResponseFrontMonthContract (template 114) - current tradeable contract
462
- * Skips 4-byte length prefix if present
463
- */
464
- function decodeFrontMonthContract(buffer) {
465
- // Skip 4-byte length prefix
466
- const data = buffer.length > 4 ? buffer.slice(4) : buffer;
467
-
468
- const result = { rpCode: [] };
469
- let offset = 0;
470
-
471
- while (offset < data.length) {
472
- try {
473
- const [tag, tagOffset] = readVarint(data, offset);
474
- const wireType = tag & 0x7;
475
- const fieldNumber = tag >>> 3;
476
- offset = tagOffset;
477
-
478
- switch (fieldNumber) {
479
- case SYMBOL_FIELDS.TEMPLATE_ID:
480
- [result.templateId, offset] = readVarint(data, offset);
481
- break;
482
- case SYMBOL_FIELDS.RP_CODE:
483
- let rpCode;
484
- [rpCode, offset] = readLengthDelimited(data, offset);
485
- result.rpCode.push(rpCode);
486
- break;
487
- case SYMBOL_FIELDS.SYMBOL:
488
- [result.symbol, offset] = readLengthDelimited(data, offset);
489
- break;
490
- case SYMBOL_FIELDS.EXCHANGE:
491
- [result.exchange, offset] = readLengthDelimited(data, offset);
492
- break;
493
- case SYMBOL_FIELDS.TRADING_SYMBOL:
494
- [result.tradingSymbol, offset] = readLengthDelimited(data, offset);
495
- break;
496
- case SYMBOL_FIELDS.DESCRIPTION:
497
- [result.description, offset] = readLengthDelimited(data, offset);
498
- break;
499
- case SYMBOL_FIELDS.USER_MSG:
500
- [result.userMsg, offset] = readLengthDelimited(data, offset);
501
- break;
502
- default:
503
- offset = skipField(data, offset, wireType);
504
- }
505
- } catch (error) {
506
- break;
507
- }
508
- }
509
-
510
- return result;
511
- }
512
-
513
- // Singleton
129
+ // Singleton instance
514
130
  const proto = new ProtobufHandler();
515
131
 
516
132
  module.exports = {