hedgequantx 1.8.33 → 1.8.34

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": "1.8.33",
3
+ "version": "1.8.34",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -9,49 +9,12 @@ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
9
9
  const { createOrderHandler, createPnLHandler } = require('./handlers');
10
10
  const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
11
11
  const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
12
- const { decodeFrontMonthContract } = require('./protobuf');
12
+ const { decodeFrontMonthContract, readVarint, readLengthDelimited, skipField } = require('./protobuf');
13
13
 
14
14
  // Debug mode
15
15
  const DEBUG = process.env.HQX_DEBUG === '1';
16
16
  const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
17
17
 
18
- // Base symbols for futures contracts
19
- const BASE_SYMBOLS = {
20
- // Equity Index (Quarterly: H, M, U, Z)
21
- quarterly: [
22
- { base: 'ES', name: 'E-mini S&P 500', exchange: 'CME', category: 'Index' },
23
- { base: 'NQ', name: 'E-mini NASDAQ-100', exchange: 'CME', category: 'Index' },
24
- { base: 'YM', name: 'E-mini Dow Jones', exchange: 'CBOT', category: 'Index' },
25
- { base: 'RTY', name: 'E-mini Russell 2000', exchange: 'CME', category: 'Index' },
26
- { base: 'MES', name: 'Micro E-mini S&P 500', exchange: 'CME', category: 'Micro Index' },
27
- { base: 'MNQ', name: 'Micro E-mini NASDAQ-100', exchange: 'CME', category: 'Micro Index' },
28
- { base: 'MYM', name: 'Micro E-mini Dow Jones', exchange: 'CBOT', category: 'Micro Index' },
29
- { base: 'M2K', name: 'Micro E-mini Russell 2000', exchange: 'CME', category: 'Micro Index' },
30
- // Currencies (Quarterly)
31
- { base: '6E', name: 'Euro FX', exchange: 'CME', category: 'Currency' },
32
- { base: 'M6E', name: 'Micro Euro FX', exchange: 'CME', category: 'Currency' },
33
- { base: '6B', name: 'British Pound', exchange: 'CME', category: 'Currency' },
34
- { base: '6J', name: 'Japanese Yen', exchange: 'CME', category: 'Currency' },
35
- { base: '6A', name: 'Australian Dollar', exchange: 'CME', category: 'Currency' },
36
- { base: '6C', name: 'Canadian Dollar', exchange: 'CME', category: 'Currency' },
37
- // Bonds (Quarterly)
38
- { base: 'ZB', name: '30-Year T-Bond', exchange: 'CBOT', category: 'Bonds' },
39
- { base: 'ZN', name: '10-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
40
- { base: 'ZF', name: '5-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
41
- { base: 'ZT', name: '2-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
42
- ],
43
- // Energy & Metals (Monthly)
44
- monthly: [
45
- { base: 'CL', name: 'Crude Oil WTI', exchange: 'NYMEX', category: 'Energy' },
46
- { base: 'MCL', name: 'Micro Crude Oil', exchange: 'NYMEX', category: 'Energy' },
47
- { base: 'NG', name: 'Natural Gas', exchange: 'NYMEX', category: 'Energy' },
48
- { base: 'GC', name: 'Gold', exchange: 'COMEX', category: 'Metals' },
49
- { base: 'MGC', name: 'Micro Gold', exchange: 'COMEX', category: 'Metals' },
50
- { base: 'SI', name: 'Silver', exchange: 'COMEX', category: 'Metals' },
51
- { base: 'HG', name: 'Copper', exchange: 'COMEX', category: 'Metals' },
52
- ],
53
- };
54
-
55
18
  // PropFirm configurations - NO FAKE DATA
56
19
  const PROPFIRM_CONFIGS = {
57
20
  'apex': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
@@ -293,39 +256,31 @@ class RithmicService extends EventEmitter {
293
256
  }
294
257
 
295
258
  /**
296
- * Get front month contract from Rithmic API
259
+ * Get front month contracts for multiple symbols at once - batch request
297
260
  */
298
- async getFrontMonth(baseSymbol, exchange) {
261
+ async getFrontMonthsBatch(products) {
299
262
  if (!this.tickerConn) {
300
- if (!this.credentials) {
301
- throw new Error('Not logged in - cannot fetch front month');
302
- }
303
- const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
304
- if (!connected) {
305
- throw new Error('Failed to connect to TICKER_PLANT');
306
- }
263
+ throw new Error('TICKER_PLANT not connected');
307
264
  }
308
265
 
309
- return new Promise((resolve, reject) => {
310
- const timeout = setTimeout(() => {
311
- reject(new Error(`Timeout getting front month for ${baseSymbol}`));
312
- }, 10000);
266
+ return new Promise((resolve) => {
267
+ const results = new Map();
268
+ const pending = new Set(products.map(p => p.productCode));
313
269
 
314
270
  const handler = (msg) => {
315
271
  if (msg.templateId === 114) { // ResponseFrontMonthContract
316
272
  const decoded = decodeFrontMonthContract(msg.data);
317
- if (decoded.userMsg === baseSymbol) {
318
- clearTimeout(timeout);
319
- this.tickerConn.removeListener('message', handler);
273
+ const baseSymbol = decoded.userMsg;
274
+
275
+ if (pending.has(baseSymbol)) {
276
+ pending.delete(baseSymbol);
320
277
 
321
- if (decoded.rpCode[0] === '0') {
322
- resolve({
278
+ if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
279
+ results.set(baseSymbol, {
280
+ symbol: decoded.tradingSymbol,
323
281
  baseSymbol: baseSymbol,
324
- symbol: decoded.tradingSymbol || decoded.symbol,
325
- exchange: decoded.exchange || exchange,
282
+ exchange: decoded.exchange,
326
283
  });
327
- } else {
328
- reject(new Error(`API error for ${baseSymbol}: ${decoded.rpCode.join(' ')}`));
329
284
  }
330
285
  }
331
286
  }
@@ -333,18 +288,166 @@ class RithmicService extends EventEmitter {
333
288
 
334
289
  this.tickerConn.on('message', handler);
335
290
 
336
- // Send RequestFrontMonthContract (template 113)
337
- this.tickerConn.send('RequestFrontMonthContract', {
338
- templateId: 113,
339
- userMsg: [baseSymbol],
340
- symbol: baseSymbol,
341
- exchange: exchange,
291
+ // Send all requests
292
+ for (const product of products) {
293
+ this.tickerConn.send('RequestFrontMonthContract', {
294
+ templateId: 113,
295
+ userMsg: [product.productCode],
296
+ symbol: product.productCode,
297
+ exchange: product.exchange,
298
+ });
299
+ }
300
+
301
+ // Wait for responses (with timeout)
302
+ setTimeout(() => {
303
+ this.tickerConn.removeListener('message', handler);
304
+ resolve(Array.from(results.values()));
305
+ }, 5000);
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Decode ProductCodes response (template 112) - field IDs from Rithmic API
311
+ */
312
+ decodeProductCodesResponse(buffer) {
313
+ const result = {};
314
+ let offset = 0;
315
+
316
+ while (offset < buffer.length) {
317
+ try {
318
+ const [tag, tagOffset] = readVarint(buffer, offset);
319
+ const wireType = tag & 0x7;
320
+ const fieldNumber = tag >>> 3;
321
+ offset = tagOffset;
322
+
323
+ if (wireType === 0) {
324
+ const [val, newOff] = readVarint(buffer, offset);
325
+ offset = newOff;
326
+ if (fieldNumber === 154467) result.templateId = val;
327
+ } else if (wireType === 2) {
328
+ const [val, newOff] = readLengthDelimited(buffer, offset);
329
+ offset = newOff;
330
+ if (fieldNumber === 110101) result.exchange = val; // exchange
331
+ if (fieldNumber === 100749) result.productCode = val; // product_code (base symbol)
332
+ if (fieldNumber === 100003) result.productName = val; // symbol_name
333
+ if (fieldNumber === 132760) result.userMsg = val; // user_msg
334
+ if (fieldNumber === 132764) result.rpCode = val; // rp_code
335
+ } else {
336
+ offset = skipField(buffer, offset, wireType);
337
+ }
338
+ } catch (e) { break; }
339
+ }
340
+
341
+ return result;
342
+ }
343
+
344
+ /**
345
+ * Get all tradeable futures from Rithmic API - 100% REAL DATA
346
+ * Sends RequestFrontMonthContract for known futures and gets real trading symbols
347
+ */
348
+ async fetchAllFrontMonths() {
349
+ if (!this.tickerConn) {
350
+ throw new Error('TICKER_PLANT not connected');
351
+ }
352
+
353
+ // Request front months for ALL products that might be futures
354
+ // The API will return the current trading symbol for each
355
+ return new Promise((resolve) => {
356
+ const contracts = new Map();
357
+ const productsToCheck = new Map(); // Will collect from ProductCodes
358
+
359
+ // Handler for ProductCodes responses
360
+ const productHandler = (msg) => {
361
+ if (msg.templateId === 112) {
362
+ const decoded = this.decodeProductCodesResponse(msg.data);
363
+ if (!decoded.productCode || !decoded.exchange) return;
364
+
365
+ // Only main futures exchanges
366
+ const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
367
+ if (!validExchanges.includes(decoded.exchange)) return;
368
+
369
+ // Skip obvious non-futures
370
+ const name = (decoded.productName || '').toLowerCase();
371
+ if (name.includes('option')) return;
372
+ if (name.includes('swap')) return;
373
+ if (name.includes('spread')) return;
374
+
375
+ const key = `${decoded.productCode}:${decoded.exchange}`;
376
+ if (!productsToCheck.has(key)) {
377
+ productsToCheck.set(key, {
378
+ productCode: decoded.productCode,
379
+ productName: decoded.productName || decoded.productCode,
380
+ exchange: decoded.exchange,
381
+ });
382
+ }
383
+ }
384
+ };
385
+
386
+ // Handler for FrontMonth responses
387
+ const frontMonthHandler = (msg) => {
388
+ if (msg.templateId === 114) {
389
+ const decoded = decodeFrontMonthContract(msg.data);
390
+ if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
391
+ contracts.set(decoded.userMsg, {
392
+ symbol: decoded.tradingSymbol,
393
+ baseSymbol: decoded.userMsg,
394
+ exchange: decoded.exchange,
395
+ });
396
+ }
397
+ }
398
+ };
399
+
400
+ this.tickerConn.on('message', productHandler);
401
+ this.tickerConn.on('message', frontMonthHandler);
402
+
403
+ // Step 1: Get all product codes
404
+ this.tickerConn.send('RequestProductCodes', {
405
+ templateId: 111,
406
+ userMsg: ['get-products'],
342
407
  });
408
+
409
+ // After 5 seconds, request front months for all collected products
410
+ setTimeout(() => {
411
+ this.tickerConn.removeListener('message', productHandler);
412
+ debug(`Collected ${productsToCheck.size} products, requesting front months...`);
413
+
414
+ // Send front month requests for all products
415
+ for (const [key, product] of productsToCheck) {
416
+ this.tickerConn.send('RequestFrontMonthContract', {
417
+ templateId: 113,
418
+ userMsg: [product.productCode],
419
+ symbol: product.productCode,
420
+ exchange: product.exchange,
421
+ });
422
+ }
423
+
424
+ // After another 5 seconds, collect results
425
+ setTimeout(() => {
426
+ this.tickerConn.removeListener('message', frontMonthHandler);
427
+
428
+ // Merge with product names
429
+ const results = [];
430
+ for (const [baseSymbol, contract] of contracts) {
431
+ const productKey = `${baseSymbol}:${contract.exchange}`;
432
+ const product = productsToCheck.get(productKey);
433
+ results.push({
434
+ symbol: contract.symbol,
435
+ baseSymbol: baseSymbol,
436
+ name: product ? product.productName : baseSymbol,
437
+ exchange: contract.exchange,
438
+ });
439
+ }
440
+
441
+ debug(`Got ${results.length} tradeable contracts from API`);
442
+ resolve(results);
443
+ }, 8000);
444
+ }, 5000);
343
445
  });
344
446
  }
345
447
 
346
448
  /**
347
- * Get all available contracts - REAL DATA from Rithmic API
449
+ * Get all available contracts - 100% REAL DATA from Rithmic API
450
+ * Fetches product codes and front months directly from the API
348
451
  */
349
452
  async getContracts() {
350
453
  // Return cached if available and fresh (5 min cache)
@@ -366,37 +469,14 @@ class RithmicService extends EventEmitter {
366
469
  }
367
470
  }
368
471
 
369
- const contracts = [];
370
- const allSymbols = [...BASE_SYMBOLS.quarterly, ...BASE_SYMBOLS.monthly];
371
-
372
- debug(`Fetching front months for ${allSymbols.length} symbols...`);
373
-
374
- // Fetch front months in parallel batches
375
- const batchSize = 10;
376
- for (let i = 0; i < allSymbols.length; i += batchSize) {
377
- const batch = allSymbols.slice(i, i + batchSize);
378
- const promises = batch.map(async (sym) => {
379
- try {
380
- const result = await this.getFrontMonth(sym.base, sym.exchange);
381
- return {
382
- symbol: result.symbol,
383
- name: `${sym.name} (${result.symbol})`,
384
- exchange: result.exchange,
385
- category: sym.category,
386
- baseSymbol: sym.base,
387
- };
388
- } catch (e) {
389
- debug(`Failed to get front month for ${sym.base}: ${e.message}`);
390
- return null;
391
- }
392
- });
472
+ // Increase max listeners to avoid warnings
473
+ this.tickerConn.setMaxListeners(5000);
393
474
 
394
- const results = await Promise.all(promises);
395
- contracts.push(...results.filter(r => r !== null));
396
- }
475
+ debug('Fetching all tradeable contracts from Rithmic API...');
476
+ const contracts = await this.fetchAllFrontMonths();
397
477
 
398
478
  if (contracts.length === 0) {
399
- return { success: false, error: 'No contracts returned from API' };
479
+ return { success: false, error: 'No tradeable contracts found' };
400
480
  }
401
481
 
402
482
  // Cache the results