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
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 = {
|