hedgequantx 2.6.162 → 2.6.163

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.
@@ -11,13 +11,13 @@ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ } = require('./constants');
11
11
  const { createOrderHandler, createPnLHandler, LatencyTracker } = require('./handlers');
12
12
  const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions } = require('./accounts');
13
13
  const { placeOrder, cancelOrder, cancelAllOrders, getOrders, getOrderHistory, closePosition, flattenAll, emergencyStop, fastEntry, fastExit } = require('./orders');
14
- const { decodeFrontMonthContract } = require('./protobuf');
15
- const { TIMEOUTS, CACHE } = require('../../config/settings');
14
+ const { TIMEOUTS } = require('../../config/settings');
16
15
  const { logger } = require('../../utils/logger');
17
16
 
18
17
  // Extracted modules
19
- const { CME_CONTRACT_SPECS, PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
18
+ const { PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
20
19
  const { getTradeHistory, setupOrderFillListener } = require('./trade-history');
20
+ const { getContracts, searchContracts } = require('./contracts');
21
21
 
22
22
  const log = logger.scope('Rithmic');
23
23
 
@@ -336,182 +336,8 @@ class RithmicService extends EventEmitter {
336
336
 
337
337
  // ==================== CONTRACTS ====================
338
338
 
339
- async getContracts() {
340
- if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
341
- return { success: true, contracts: this._contractsCache, source: 'cache' };
342
- }
343
-
344
- if (!this.credentials) return { success: false, error: 'Not logged in' };
345
-
346
- try {
347
- if (!this.tickerConn) {
348
- const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
349
- if (!connected) return { success: false, error: 'Failed to connect to TICKER_PLANT' };
350
- }
351
-
352
- this.tickerConn.setMaxListeners(5000);
353
- log.debug('Fetching contracts from Rithmic API');
354
- const contracts = await this._fetchAllFrontMonths();
355
-
356
- if (!contracts.length) return { success: false, error: 'No tradeable contracts found' };
357
-
358
- this._contractsCache = contracts;
359
- this._contractsCacheTime = Date.now();
360
-
361
- return { success: true, contracts, source: 'api' };
362
- } catch (err) {
363
- log.error('getContracts error', { error: err.message });
364
- return { success: false, error: err.message };
365
- }
366
- }
367
-
368
- async searchContracts(searchText) {
369
- const result = await this.getContracts();
370
- if (!searchText || !result.success) return result.contracts || [];
371
-
372
- const search = searchText.toUpperCase();
373
- return result.contracts.filter(c =>
374
- c.symbol.toUpperCase().includes(search) ||
375
- c.name.toUpperCase().includes(search)
376
- );
377
- }
378
-
379
- async _fetchAllFrontMonths() {
380
- if (!this.tickerConn) throw new Error('TICKER_PLANT not connected');
381
-
382
- return new Promise((resolve) => {
383
- const contracts = new Map();
384
- const productsToCheck = new Map();
385
-
386
- const productHandler = (msg) => {
387
- if (msg.templateId !== 112) return;
388
-
389
- const decoded = this._decodeProductCodes(msg.data);
390
- if (!decoded.productCode || !decoded.exchange) return;
391
-
392
- const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
393
- if (!validExchanges.includes(decoded.exchange)) return;
394
-
395
- const name = (decoded.productName || '').toLowerCase();
396
- if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
397
-
398
- const key = `${decoded.productCode}:${decoded.exchange}`;
399
- if (!productsToCheck.has(key)) {
400
- productsToCheck.set(key, {
401
- productCode: decoded.productCode,
402
- productName: decoded.productName || decoded.productCode,
403
- exchange: decoded.exchange,
404
- });
405
- }
406
- };
407
-
408
- const frontMonthHandler = (msg) => {
409
- if (msg.templateId !== 114) return;
410
-
411
- const decoded = decodeFrontMonthContract(msg.data);
412
- if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
413
- contracts.set(decoded.userMsg, {
414
- symbol: decoded.tradingSymbol,
415
- baseSymbol: decoded.userMsg,
416
- exchange: decoded.exchange,
417
- });
418
- }
419
- };
420
-
421
- this.tickerConn.on('message', productHandler);
422
- this.tickerConn.on('message', frontMonthHandler);
423
-
424
- this.tickerConn.send('RequestProductCodes', {
425
- templateId: 111,
426
- userMsg: ['get-products'],
427
- });
428
-
429
- setTimeout(() => {
430
- this.tickerConn.removeListener('message', productHandler);
431
- log.debug('Collected products', { count: productsToCheck.size });
432
-
433
- for (const product of productsToCheck.values()) {
434
- this.tickerConn.send('RequestFrontMonthContract', {
435
- templateId: 113,
436
- userMsg: [product.productCode],
437
- symbol: product.productCode,
438
- exchange: product.exchange,
439
- });
440
- }
441
-
442
- setTimeout(() => {
443
- this.tickerConn.removeListener('message', frontMonthHandler);
444
-
445
- const results = [];
446
- for (const [baseSymbol, contract] of contracts) {
447
- const productKey = `${baseSymbol}:${contract.exchange}`;
448
- const product = productsToCheck.get(productKey);
449
- const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
450
- const productName = specs?.name || product?.productName || baseSymbol;
451
-
452
- results.push({
453
- symbol: contract.symbol,
454
- baseSymbol,
455
- name: contract.symbol,
456
- description: productName,
457
- exchange: contract.exchange,
458
- tickSize: specs?.tickSize ?? null,
459
- tickValue: specs?.tickValue ?? null,
460
- });
461
- }
462
-
463
- results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
464
- log.debug('Got contracts from API', { count: results.length });
465
- resolve(results);
466
- }, TIMEOUTS.RITHMIC_PRODUCTS);
467
- }, TIMEOUTS.RITHMIC_CONTRACTS);
468
- });
469
- }
470
-
471
- _decodeProductCodes(buffer) {
472
- const result = {};
473
- let offset = 0;
474
-
475
- const readVarint = (buf, off) => {
476
- let value = 0, shift = 0;
477
- while (off < buf.length) {
478
- const byte = buf[off++];
479
- value |= (byte & 0x7F) << shift;
480
- if (!(byte & 0x80)) break;
481
- shift += 7;
482
- }
483
- return [value, off];
484
- };
485
-
486
- const readString = (buf, off) => {
487
- const [len, newOff] = readVarint(buf, off);
488
- return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
489
- };
490
-
491
- while (offset < buffer.length) {
492
- try {
493
- const [tag, tagOff] = readVarint(buffer, offset);
494
- const wireType = tag & 0x7;
495
- const fieldNumber = tag >>> 3;
496
- offset = tagOff;
497
-
498
- if (wireType === 0) {
499
- const [, newOff] = readVarint(buffer, offset);
500
- offset = newOff;
501
- } else if (wireType === 2) {
502
- const [val, newOff] = readString(buffer, offset);
503
- offset = newOff;
504
- if (fieldNumber === 110101) result.exchange = val;
505
- if (fieldNumber === 100749) result.productCode = val;
506
- if (fieldNumber === 100003) result.productName = val;
507
- } else {
508
- break;
509
- }
510
- } catch { break; }
511
- }
512
-
513
- return result;
514
- }
339
+ async getContracts() { return getContracts(this); }
340
+ async searchContracts(searchText) { return searchContracts(this, searchText); }
515
341
 
516
342
  // ==================== CLEANUP ====================
517
343
 
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Rithmic Market Data Decoders
3
+ * @module services/rithmic/market-data-decoders
4
+ *
5
+ * Manual decoders for LastTrade and BestBidOffer messages.
6
+ * Required because protobufjs can't handle field IDs > 100000
7
+ */
8
+
9
+ const { readVarint, readLengthDelimited, skipField } = require('./proto-decoders');
10
+
11
+ // Rithmic field IDs for LastTrade (from protobuf)
12
+ const LAST_TRADE_FIELDS = {
13
+ TEMPLATE_ID: 154467,
14
+ SYMBOL: 110100,
15
+ EXCHANGE: 110101,
16
+ TRADE_PRICE: 100006,
17
+ TRADE_SIZE: 100178,
18
+ AGGRESSOR: 112003, // 1=BUY, 2=SELL
19
+ SSBOE: 150100,
20
+ USECS: 150101,
21
+ };
22
+
23
+ // Rithmic field IDs for BestBidOffer (from protobuf)
24
+ const BBO_FIELDS = {
25
+ TEMPLATE_ID: 154467,
26
+ SYMBOL: 110100,
27
+ EXCHANGE: 110101,
28
+ BID_PRICE: 100022,
29
+ BID_SIZE: 100030,
30
+ ASK_PRICE: 100025,
31
+ ASK_SIZE: 100031,
32
+ SSBOE: 150100,
33
+ USECS: 150101,
34
+ };
35
+
36
+ /**
37
+ * Manually decode LastTrade message from Rithmic
38
+ * @param {Buffer} buffer
39
+ * @returns {Object}
40
+ */
41
+ function decodeLastTrade(buffer) {
42
+ const result = {};
43
+ let offset = 0;
44
+
45
+ while (offset < buffer.length) {
46
+ try {
47
+ const [tag, newOffset] = readVarint(buffer, offset);
48
+ const fieldNumber = tag >>> 3;
49
+ const wireType = tag & 0x7;
50
+ offset = newOffset;
51
+
52
+ switch (fieldNumber) {
53
+ case LAST_TRADE_FIELDS.SYMBOL:
54
+ if (wireType === 2) {
55
+ const [val, next] = readLengthDelimited(buffer, offset);
56
+ result.symbol = val;
57
+ offset = next;
58
+ } else {
59
+ offset = skipField(buffer, offset, wireType);
60
+ }
61
+ break;
62
+ case LAST_TRADE_FIELDS.EXCHANGE:
63
+ if (wireType === 2) {
64
+ const [val, next] = readLengthDelimited(buffer, offset);
65
+ result.exchange = val;
66
+ offset = next;
67
+ } else {
68
+ offset = skipField(buffer, offset, wireType);
69
+ }
70
+ break;
71
+ case LAST_TRADE_FIELDS.TRADE_PRICE:
72
+ if (wireType === 1) {
73
+ result.tradePrice = buffer.readDoubleLE(offset);
74
+ offset += 8;
75
+ } else {
76
+ offset = skipField(buffer, offset, wireType);
77
+ }
78
+ break;
79
+ case LAST_TRADE_FIELDS.TRADE_SIZE:
80
+ if (wireType === 0) {
81
+ const [val, next] = readVarint(buffer, offset);
82
+ result.tradeSize = val;
83
+ offset = next;
84
+ } else {
85
+ offset = skipField(buffer, offset, wireType);
86
+ }
87
+ break;
88
+ case LAST_TRADE_FIELDS.AGGRESSOR:
89
+ if (wireType === 0) {
90
+ const [val, next] = readVarint(buffer, offset);
91
+ result.aggressor = val;
92
+ offset = next;
93
+ } else {
94
+ offset = skipField(buffer, offset, wireType);
95
+ }
96
+ break;
97
+ case LAST_TRADE_FIELDS.SSBOE:
98
+ if (wireType === 0) {
99
+ const [val, next] = readVarint(buffer, offset);
100
+ result.ssboe = val;
101
+ offset = next;
102
+ } else {
103
+ offset = skipField(buffer, offset, wireType);
104
+ }
105
+ break;
106
+ case LAST_TRADE_FIELDS.USECS:
107
+ if (wireType === 0) {
108
+ const [val, next] = readVarint(buffer, offset);
109
+ result.usecs = val;
110
+ offset = next;
111
+ } else {
112
+ offset = skipField(buffer, offset, wireType);
113
+ }
114
+ break;
115
+ default:
116
+ offset = skipField(buffer, offset, wireType);
117
+ }
118
+ } catch {
119
+ break;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Manually decode BestBidOffer message from Rithmic
128
+ * @param {Buffer} buffer
129
+ * @returns {Object}
130
+ */
131
+ function decodeBestBidOffer(buffer) {
132
+ const result = {};
133
+ let offset = 0;
134
+
135
+ while (offset < buffer.length) {
136
+ try {
137
+ const [tag, newOffset] = readVarint(buffer, offset);
138
+ const fieldNumber = tag >>> 3;
139
+ const wireType = tag & 0x7;
140
+ offset = newOffset;
141
+
142
+ switch (fieldNumber) {
143
+ case BBO_FIELDS.SYMBOL:
144
+ if (wireType === 2) {
145
+ const [val, next] = readLengthDelimited(buffer, offset);
146
+ result.symbol = val;
147
+ offset = next;
148
+ } else {
149
+ offset = skipField(buffer, offset, wireType);
150
+ }
151
+ break;
152
+ case BBO_FIELDS.EXCHANGE:
153
+ if (wireType === 2) {
154
+ const [val, next] = readLengthDelimited(buffer, offset);
155
+ result.exchange = val;
156
+ offset = next;
157
+ } else {
158
+ offset = skipField(buffer, offset, wireType);
159
+ }
160
+ break;
161
+ case BBO_FIELDS.BID_PRICE:
162
+ if (wireType === 1) {
163
+ result.bidPrice = buffer.readDoubleLE(offset);
164
+ offset += 8;
165
+ } else {
166
+ offset = skipField(buffer, offset, wireType);
167
+ }
168
+ break;
169
+ case BBO_FIELDS.BID_SIZE:
170
+ if (wireType === 0) {
171
+ const [val, next] = readVarint(buffer, offset);
172
+ result.bidSize = val;
173
+ offset = next;
174
+ } else {
175
+ offset = skipField(buffer, offset, wireType);
176
+ }
177
+ break;
178
+ case BBO_FIELDS.ASK_PRICE:
179
+ if (wireType === 1) {
180
+ result.askPrice = buffer.readDoubleLE(offset);
181
+ offset += 8;
182
+ } else {
183
+ offset = skipField(buffer, offset, wireType);
184
+ }
185
+ break;
186
+ case BBO_FIELDS.ASK_SIZE:
187
+ if (wireType === 0) {
188
+ const [val, next] = readVarint(buffer, offset);
189
+ result.askSize = val;
190
+ offset = next;
191
+ } else {
192
+ offset = skipField(buffer, offset, wireType);
193
+ }
194
+ break;
195
+ case BBO_FIELDS.SSBOE:
196
+ if (wireType === 0) {
197
+ const [val, next] = readVarint(buffer, offset);
198
+ result.ssboe = val;
199
+ offset = next;
200
+ } else {
201
+ offset = skipField(buffer, offset, wireType);
202
+ }
203
+ break;
204
+ case BBO_FIELDS.USECS:
205
+ if (wireType === 0) {
206
+ const [val, next] = readVarint(buffer, offset);
207
+ result.usecs = val;
208
+ offset = next;
209
+ } else {
210
+ offset = skipField(buffer, offset, wireType);
211
+ }
212
+ break;
213
+ default:
214
+ offset = skipField(buffer, offset, wireType);
215
+ }
216
+ } catch {
217
+ break;
218
+ }
219
+ }
220
+
221
+ return result;
222
+ }
223
+
224
+ module.exports = {
225
+ LAST_TRADE_FIELDS,
226
+ BBO_FIELDS,
227
+ decodeLastTrade,
228
+ decodeBestBidOffer,
229
+ };