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 +1 -1
- package/src/services/rithmic/contracts.js +130 -125
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
304
|
+
const testProducts = productsArray.slice(0, 60);
|
|
271
305
|
|
|
272
306
|
for (const product of testProducts) {
|
|
273
307
|
try {
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|