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 +1 -1
- package/src/app.js +11 -2
- package/src/lib/data.js +28 -2
- package/src/pages/algo/one-account.js +10 -4
- package/src/services/rithmic/protobuf-decoders.js +375 -0
- package/src/services/rithmic/protobuf-utils.js +71 -0
- package/src/services/rithmic/protobuf.js +19 -403
- package/src/ui/menu.js +6 -1
- package/src/utils/prompts.js +302 -116
package/package.json
CHANGED
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
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
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
|
+
};
|