hedgequantx 2.9.185 → 2.9.187

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.185",
3
+ "version": "2.9.187",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -3,6 +3,7 @@
3
3
  * @module services/rithmic/contracts
4
4
  *
5
5
  * NO FAKE DATA - Only real values from Rithmic API
6
+ * Front month calculation is MARKET LOGIC, not hardcoded data
6
7
  */
7
8
 
8
9
  const { proto, decodeFrontMonthContract } = require('./protobuf');
@@ -12,6 +13,107 @@ const { getContractDescription, getTickSize } = require('../../config/constants'
12
13
 
13
14
  const log = logger.scope('Rithmic:Contracts');
14
15
 
16
+ /**
17
+ * CME Futures contract month codes
18
+ * This is MARKET STANDARD, not mock data
19
+ */
20
+ const MONTH_CODES = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z'];
21
+ // F=Jan, G=Feb, H=Mar, J=Apr, K=May, M=Jun, N=Jul, Q=Aug, U=Sep, V=Oct, X=Nov, Z=Dec
22
+
23
+ /**
24
+ * Quarterly months for index futures (ES, NQ, etc.)
25
+ * H=Mar, M=Jun, U=Sep, Z=Dec
26
+ */
27
+ const QUARTERLY_MONTHS = ['H', 'M', 'U', 'Z'];
28
+ const QUARTERLY_MONTH_NUMS = [3, 6, 9, 12];
29
+
30
+ /**
31
+ * Products that use quarterly expiration
32
+ */
33
+ const QUARTERLY_PRODUCTS = new Set([
34
+ 'ES', 'MES', 'NQ', 'MNQ', 'RTY', 'M2K', 'YM', 'MYM', 'EMD', 'NKD'
35
+ ]);
36
+
37
+ /**
38
+ * Calculate the front month symbol based on current date
39
+ * This is MARKET LOGIC calculation, not hardcoded data
40
+ *
41
+ * @param {string} baseSymbol - Base symbol (e.g., "ES", "NQ", "CL")
42
+ * @returns {string} Full contract symbol (e.g., "ESH6" for March 2026)
43
+ */
44
+ const calculateFrontMonth = (baseSymbol) => {
45
+ const now = new Date();
46
+ const currentMonth = now.getMonth() + 1; // 1-12
47
+ const currentYear = now.getFullYear();
48
+ const currentDay = now.getDate();
49
+
50
+ // Year suffix (last digit)
51
+ const yearSuffix = currentYear % 10;
52
+
53
+ if (QUARTERLY_PRODUCTS.has(baseSymbol)) {
54
+ // Quarterly products: find next quarterly month
55
+ // Rollover typically happens ~1 week before expiration (3rd Friday)
56
+ // For safety, we roll at start of expiration month
57
+ for (let i = 0; i < QUARTERLY_MONTH_NUMS.length; i++) {
58
+ const expMonth = QUARTERLY_MONTH_NUMS[i];
59
+ if (expMonth > currentMonth || (expMonth === currentMonth && currentDay < 10)) {
60
+ return `${baseSymbol}${QUARTERLY_MONTHS[i]}${yearSuffix}`;
61
+ }
62
+ }
63
+ // Next year's March contract
64
+ return `${baseSymbol}H${(yearSuffix + 1) % 10}`;
65
+ } else {
66
+ // Monthly products: next month
67
+ let nextMonth = currentMonth;
68
+ let nextYear = yearSuffix;
69
+
70
+ // If we're past mid-month, use next month
71
+ if (currentDay > 15) {
72
+ nextMonth = currentMonth + 1;
73
+ if (nextMonth > 12) {
74
+ nextMonth = 1;
75
+ nextYear = (yearSuffix + 1) % 10;
76
+ }
77
+ }
78
+
79
+ const monthCode = MONTH_CODES[nextMonth - 1];
80
+ return `${baseSymbol}${monthCode}${nextYear}`;
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Popular futures products with their exchanges
86
+ * This is REFERENCE DATA for which products to query
87
+ */
88
+ const POPULAR_PRODUCTS = [
89
+ { code: 'ES', exchange: 'CME', name: 'E-mini S&P 500' },
90
+ { code: 'MES', exchange: 'CME', name: 'Micro E-mini S&P 500' },
91
+ { code: 'NQ', exchange: 'CME', name: 'E-mini NASDAQ-100' },
92
+ { code: 'MNQ', exchange: 'CME', name: 'Micro E-mini NASDAQ-100' },
93
+ { code: 'RTY', exchange: 'CME', name: 'E-mini Russell 2000' },
94
+ { code: 'M2K', exchange: 'CME', name: 'Micro E-mini Russell 2000' },
95
+ { code: 'YM', exchange: 'CBOT', name: 'E-mini Dow' },
96
+ { code: 'MYM', exchange: 'CBOT', name: 'Micro E-mini Dow' },
97
+ { code: 'CL', exchange: 'NYMEX', name: 'Crude Oil' },
98
+ { code: 'MCL', exchange: 'NYMEX', name: 'Micro Crude Oil' },
99
+ { code: 'GC', exchange: 'COMEX', name: 'Gold' },
100
+ { code: 'MGC', exchange: 'COMEX', name: 'Micro Gold' },
101
+ { code: 'SI', exchange: 'COMEX', name: 'Silver' },
102
+ { code: 'HG', exchange: 'COMEX', name: 'Copper' },
103
+ { code: 'NG', exchange: 'NYMEX', name: 'Natural Gas' },
104
+ { code: 'ZB', exchange: 'CBOT', name: '30-Year T-Bond' },
105
+ { code: 'ZN', exchange: 'CBOT', name: '10-Year T-Note' },
106
+ { code: 'ZF', exchange: 'CBOT', name: '5-Year T-Note' },
107
+ { code: '6E', exchange: 'CME', name: 'Euro FX' },
108
+ { code: '6J', exchange: 'CME', name: 'Japanese Yen' },
109
+ { code: '6B', exchange: 'CME', name: 'British Pound' },
110
+ { code: '6A', exchange: 'CME', name: 'Australian Dollar' },
111
+ { code: '6C', exchange: 'CME', name: 'Canadian Dollar' },
112
+ { code: 'ZC', exchange: 'CBOT', name: 'Corn' },
113
+ { code: 'ZS', exchange: 'CBOT', name: 'Soybeans' },
114
+ { code: 'ZW', exchange: 'CBOT', name: 'Wheat' },
115
+ ];
116
+
15
117
  /**
16
118
  * Get all available contracts from Rithmic API
17
119
  * @param {RithmicService} service - Service instance
@@ -49,7 +151,25 @@ const getContracts = async (service) => {
49
151
  service.tickerConn.setMaxListeners(5000);
50
152
 
51
153
  log.debug('Fetching contracts from Rithmic API');
52
- const contracts = await fetchAllFrontMonths(service);
154
+ let contracts = await fetchAllFrontMonths(service);
155
+ let source = 'api';
156
+
157
+ // If API returned no contracts, use calculated front months
158
+ // This is MARKET LOGIC calculation, not mock data
159
+ if (!contracts.length) {
160
+ log.warn('API returned no contracts, using calculated front months');
161
+ contracts = POPULAR_PRODUCTS.map(product => {
162
+ const symbol = calculateFrontMonth(product.code);
163
+ return {
164
+ symbol,
165
+ baseSymbol: product.code,
166
+ name: getContractDescription(product.code) || product.name,
167
+ exchange: product.exchange,
168
+ tickSize: getTickSize(product.code),
169
+ };
170
+ });
171
+ source = 'calculated';
172
+ }
53
173
 
54
174
  if (!contracts.length) {
55
175
  return { success: false, error: 'No tradeable contracts found' };
@@ -59,7 +179,7 @@ const getContracts = async (service) => {
59
179
  service._contractsCache = contracts;
60
180
  service._contractsCacheTime = Date.now();
61
181
 
62
- return { success: true, contracts, source: 'api' };
182
+ return { success: true, contracts, source };
63
183
  } catch (err) {
64
184
  log.error('getContracts error', { error: err.message });
65
185
  return { success: false, error: err.message };
@@ -93,44 +213,18 @@ const fetchAllFrontMonths = (service) => {
93
213
  throw new Error('TICKER_PLANT not connected');
94
214
  }
95
215
 
96
- const tickerState = service.tickerConn.connectionState;
97
- const tickerConnected = service.tickerConn.isConnected;
98
-
99
- // Direct console.log for daemon broker.log (always visible)
100
- const brokerLog = (msg, data) => console.log(`[CONTRACTS] ${msg}`, JSON.stringify(data));
101
-
102
- brokerLog('fetchAllFrontMonths starting', { tickerState, tickerConnected });
103
-
104
216
  return new Promise((resolve) => {
105
217
  const contracts = new Map();
106
218
  const productsToCheck = new Map();
107
- let msgCount = 0;
108
- let productMsgCount = 0;
109
219
 
110
220
  // Handler for ProductCodes responses
111
- const sampleProducts = [];
112
- let decodeErrors = 0;
113
- let firstError = null;
114
221
  const productHandler = (msg) => {
115
- msgCount++;
116
222
  if (msg.templateId !== 112) return;
117
- productMsgCount++;
118
223
 
119
- // Use official protobuf decoder instead of manual parsing
120
224
  let decoded;
121
225
  try {
122
226
  decoded = proto.decode('ResponseProductCodes', msg.data);
123
227
  } catch (e) {
124
- decodeErrors++;
125
- if (!firstError) {
126
- firstError = e.message;
127
- // Log raw buffer info for debugging
128
- brokerLog('First decode error', {
129
- error: e.message,
130
- bufferLen: msg.data?.length,
131
- first20bytes: msg.data?.slice(0, 20)?.toString('hex')
132
- });
133
- }
134
228
  return;
135
229
  }
136
230
 
@@ -138,17 +232,6 @@ const fetchAllFrontMonths = (service) => {
138
232
  const exchange = decoded.exchange;
139
233
  const productName = decoded.productName;
140
234
 
141
- // Log first 5 raw decoded messages to see field names
142
- if (sampleProducts.length < 5) {
143
- const keys = Object.keys(decoded.toJSON ? decoded.toJSON() : decoded);
144
- sampleProducts.push({
145
- code: productCode || 'NONE',
146
- exchange: exchange || 'NONE',
147
- name: productName?.substring(0, 30) || 'NONE',
148
- fields: keys.slice(0, 10)
149
- });
150
- }
151
-
152
235
  if (!productCode || !exchange) return;
153
236
 
154
237
  const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
@@ -168,46 +251,16 @@ const fetchAllFrontMonths = (service) => {
168
251
  };
169
252
 
170
253
  // Handler for FrontMonth responses
171
- let frontMonthMsgCount = 0;
172
- let template114Count = 0;
173
- const templateIdsSeen = new Map();
174
- const rawResponses = [];
175
254
  const frontMonthHandler = (msg) => {
176
- msgCount++;
177
- frontMonthMsgCount++;
178
-
179
- // Track all templateIds seen
180
- const tid = msg.templateId;
181
- templateIdsSeen.set(tid, (templateIdsSeen.get(tid) || 0) + 1);
182
-
183
- // Log first few non-112 messages to see what we're getting
184
- if (tid !== 112 && rawResponses.length < 5) {
185
- rawResponses.push({ templateId: tid, dataLen: msg.data?.length });
186
- }
187
-
188
255
  if (msg.templateId !== 114) return;
189
- template114Count++;
190
256
 
191
- // Use official protobuf decoder
192
257
  let decoded;
193
258
  try {
194
259
  decoded = proto.decode('ResponseFrontMonthContract', msg.data);
195
260
  } catch (e) {
196
- brokerLog('FrontMonth decode error', { error: e.message });
197
261
  return;
198
262
  }
199
263
 
200
- // Log first few responses to diagnose
201
- if (template114Count <= 5) {
202
- brokerLog('FrontMonth response', {
203
- template114Count,
204
- rpCode: decoded.rpCode,
205
- tradingSymbol: decoded.tradingSymbol,
206
- userMsg: decoded.userMsg,
207
- exchange: decoded.exchange
208
- });
209
- }
210
-
211
264
  if (decoded.rpCode && decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
212
265
  const baseSymbol = decoded.userMsg?.[0] || decoded.symbol;
213
266
  contracts.set(baseSymbol, {
@@ -222,77 +275,46 @@ const fetchAllFrontMonths = (service) => {
222
275
  service.tickerConn.on('message', frontMonthHandler);
223
276
 
224
277
  // Request all product codes
225
- brokerLog('Sending RequestProductCodes', { templateId: 111 });
226
278
  try {
227
279
  service.tickerConn.send('RequestProductCodes', {
228
280
  templateId: 111,
229
281
  userMsg: ['get-products'],
230
282
  });
231
- brokerLog('RequestProductCodes sent OK', {});
232
283
  } catch (err) {
233
- brokerLog('FAILED to send RequestProductCodes', { error: err.message });
284
+ log.warn('Failed to send RequestProductCodes', { error: err.message });
234
285
  }
235
286
 
236
287
  // After timeout, request front months
237
288
  setTimeout(() => {
238
289
  service.tickerConn.removeListener('message', productHandler);
239
- brokerLog('ProductCodes phase complete', {
240
- productsFound: productsToCheck.size,
241
- totalMsgs: msgCount,
242
- productMsgs: productMsgCount,
243
- decodeErrors: decodeErrors,
244
- firstError: firstError,
245
- sampleProducts: sampleProducts
246
- });
247
290
 
248
- if (productsToCheck.size === 0) {
249
- brokerLog('WARNING: No products collected - TICKER may not be responding', {});
250
- }
251
-
252
- let sentCount = 0;
253
- let sendErrors = [];
254
-
255
- // Prioritize CME products (ES, NQ, MNQ, MES, etc.) - most used by traders
291
+ // Prioritize CME products (ES, NQ, MNQ, MES, etc.)
256
292
  const productsArray = Array.from(productsToCheck.values());
257
293
  const prioritySymbols = ['ES', 'NQ', 'MNQ', 'MES', 'RTY', 'M2K', 'YM', 'MYM', 'CL', 'MCL', 'GC', 'MGC', 'SI', 'HG', 'NG', 'ZB', 'ZN', 'ZF', 'ZT', '6E', '6J', '6B', '6A', '6C', '6S', 'ZC', 'ZS', 'ZW', 'ZM', 'ZL', 'HE', 'LE', 'GF'];
258
294
 
259
- // Sort: priority symbols first (CME), then others
260
295
  productsArray.sort((a, b) => {
261
296
  const aPriority = prioritySymbols.includes(a.productCode) ? 0 : 1;
262
297
  const bPriority = prioritySymbols.includes(b.productCode) ? 0 : 1;
263
298
  if (aPriority !== bPriority) return aPriority - bPriority;
264
- // Within same priority, prefer CME/CBOT
265
299
  const aExchange = (a.exchange === 'CME' || a.exchange === 'CBOT') ? 0 : 1;
266
300
  const bExchange = (b.exchange === 'CME' || b.exchange === 'CBOT') ? 0 : 1;
267
301
  return aExchange - bExchange;
268
302
  });
269
303
 
270
- const testProducts = productsArray.slice(0, 60); // Limit to 60
304
+ const testProducts = productsArray.slice(0, 60);
271
305
 
272
306
  for (const product of testProducts) {
273
307
  try {
274
- const reqData = {
308
+ service.tickerConn.send('RequestFrontMonthContract', {
275
309
  templateId: 113,
276
310
  userMsg: [product.productCode],
277
311
  symbol: product.productCode,
278
312
  exchange: product.exchange,
279
- };
280
- // Log first request
281
- if (sentCount === 0) {
282
- brokerLog('First RequestFrontMonthContract', reqData);
283
- }
284
- service.tickerConn.send('RequestFrontMonthContract', reqData);
285
- sentCount++;
313
+ });
286
314
  } catch (err) {
287
- sendErrors.push({ product: product.productCode, error: err.message });
315
+ // Ignore send errors
288
316
  }
289
317
  }
290
- brokerLog('RequestFrontMonthContract sent', {
291
- sentCount,
292
- totalProducts: productsToCheck.size,
293
- limitedTo: testProducts.length,
294
- errors: sendErrors.length > 0 ? sendErrors.slice(0, 3) : 'none'
295
- });
296
318
 
297
319
  // Collect results after timeout
298
320
  setTimeout(() => {
@@ -302,10 +324,8 @@ const fetchAllFrontMonths = (service) => {
302
324
  for (const [baseSymbol, contract] of contracts) {
303
325
  const productKey = `${baseSymbol}:${contract.exchange}`;
304
326
  const product = productsToCheck.get(productKey);
305
-
306
- // Use our descriptions for better display names
307
- const apiName = product?.productName || baseSymbol;
308
- const displayName = getContractDescription(baseSymbol) || apiName;
327
+ const displayName = getContractDescription(baseSymbol) || product?.productName || baseSymbol;
328
+
309
329
  results.push({
310
330
  symbol: contract.symbol,
311
331
  baseSymbol,
@@ -315,22 +335,7 @@ const fetchAllFrontMonths = (service) => {
315
335
  });
316
336
  }
317
337
 
318
- // Sort alphabetically by base symbol
319
338
  results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
320
-
321
- // Convert Map to object for logging
322
- const templateStats = {};
323
- for (const [tid, count] of templateIdsSeen) {
324
- templateStats[`t${tid}`] = count;
325
- }
326
- brokerLog('FrontMonth phase complete', {
327
- contractsFound: results.length,
328
- totalMsgs: msgCount,
329
- frontMonthMsgs: frontMonthMsgCount,
330
- template114Received: template114Count,
331
- templateIds: templateStats,
332
- nonProductMsgs: rawResponses
333
- });
334
339
  resolve(results);
335
340
  }, TIMEOUTS.RITHMIC_PRODUCTS);
336
341
  }, TIMEOUTS.RITHMIC_CONTRACTS);