hedgequantx 1.8.33 → 1.8.35
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/pages/algo/copy-trading.js +30 -16
- package/src/pages/algo/one-account.js +26 -23
- package/src/services/rithmic/constants.js +152 -0
- package/src/services/rithmic/index.js +191 -96
- package/src/utils/prompts.js +12 -5
package/package.json
CHANGED
|
@@ -180,7 +180,7 @@ const getContractsFromAPI = async () => {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Symbol selection helper -
|
|
183
|
+
* Symbol selection helper - grouped by category with indices first
|
|
184
184
|
*/
|
|
185
185
|
const selectSymbol = async (service, label) => {
|
|
186
186
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
@@ -193,6 +193,7 @@ const selectSymbol = async (service, label) => {
|
|
|
193
193
|
if (!contracts && typeof service.getContracts === 'function') {
|
|
194
194
|
const result = await service.getContracts();
|
|
195
195
|
if (result.success && result.contracts?.length > 0) {
|
|
196
|
+
// Contracts from Rithmic are already categorized and sorted
|
|
196
197
|
contracts = result.contracts;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -203,23 +204,36 @@ const selectSymbol = async (service, label) => {
|
|
|
203
204
|
return null;
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
// Sort: Popular indices first (ES, NQ, YM, RTY, MES, MNQ)
|
|
207
|
-
const prioritySymbols = ['ES', 'NQ', 'YM', 'RTY', 'MES', 'MNQ', 'MYM', 'M2K', 'NKD', 'CL', 'GC'];
|
|
208
|
-
contracts.sort((a, b) => {
|
|
209
|
-
const aSymbol = (a.symbol || a.name || '').toUpperCase();
|
|
210
|
-
const bSymbol = (b.symbol || b.name || '').toUpperCase();
|
|
211
|
-
const aIdx = prioritySymbols.findIndex(p => aSymbol.startsWith(p));
|
|
212
|
-
const bIdx = prioritySymbols.findIndex(p => bSymbol.startsWith(p));
|
|
213
|
-
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
|
|
214
|
-
if (aIdx !== -1) return -1;
|
|
215
|
-
if (bIdx !== -1) return 1;
|
|
216
|
-
return (a.name || '').localeCompare(b.name || '');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
207
|
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
220
208
|
|
|
221
|
-
|
|
222
|
-
options
|
|
209
|
+
// Build options with category headers (if contracts have category info)
|
|
210
|
+
const options = [];
|
|
211
|
+
let currentCategory = null;
|
|
212
|
+
|
|
213
|
+
for (const c of contracts) {
|
|
214
|
+
// Add category header when category changes (if available)
|
|
215
|
+
if (c.categoryName && c.categoryName !== currentCategory) {
|
|
216
|
+
currentCategory = c.categoryName;
|
|
217
|
+
options.push({
|
|
218
|
+
label: chalk.cyan.bold(`── ${currentCategory} ──`),
|
|
219
|
+
value: null,
|
|
220
|
+
disabled: true
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Format label based on available data
|
|
225
|
+
const symbolDisplay = c.symbol || c.name;
|
|
226
|
+
const nameDisplay = c.name || c.symbol;
|
|
227
|
+
const exchangeDisplay = c.exchange ? ` (${c.exchange})` : '';
|
|
228
|
+
const label = c.categoryName
|
|
229
|
+
? ` ${symbolDisplay} - ${nameDisplay}${exchangeDisplay}`
|
|
230
|
+
: `${nameDisplay}${exchangeDisplay}`;
|
|
231
|
+
|
|
232
|
+
options.push({ label, value: c });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
options.push({ label: '', value: null, disabled: true }); // Spacer
|
|
236
|
+
options.push({ label: chalk.gray('< Cancel'), value: null });
|
|
223
237
|
|
|
224
238
|
return await prompts.selectOption(`${label} Symbol:`, options);
|
|
225
239
|
} catch (e) {
|
|
@@ -72,7 +72,7 @@ const oneAccountMenu = async (service) => {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Symbol selection -
|
|
75
|
+
* Symbol selection - grouped by category with indices first
|
|
76
76
|
*/
|
|
77
77
|
const selectSymbol = async (service, account) => {
|
|
78
78
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
@@ -83,33 +83,36 @@ const selectSymbol = async (service, account) => {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
const contracts = contractsResult.contracts
|
|
88
|
-
...c,
|
|
89
|
-
symbol: c.name || c.symbol,
|
|
90
|
-
name: c.description || c.name || c.symbol
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
// Sort: Popular indices first (ES, NQ, YM, RTY, MES, MNQ)
|
|
94
|
-
const prioritySymbols = ['ES', 'NQ', 'YM', 'RTY', 'MES', 'MNQ', 'MYM', 'M2K', 'NKD', 'CL', 'GC'];
|
|
95
|
-
contracts.sort((a, b) => {
|
|
96
|
-
const aSymbol = (a.symbol || '').toUpperCase();
|
|
97
|
-
const bSymbol = (b.symbol || '').toUpperCase();
|
|
98
|
-
const aIdx = prioritySymbols.findIndex(p => aSymbol.startsWith(p));
|
|
99
|
-
const bIdx = prioritySymbols.findIndex(p => bSymbol.startsWith(p));
|
|
100
|
-
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
|
|
101
|
-
if (aIdx !== -1) return -1;
|
|
102
|
-
if (bIdx !== -1) return 1;
|
|
103
|
-
return (a.name || '').localeCompare(b.name || '');
|
|
104
|
-
});
|
|
86
|
+
// Contracts are already sorted by category from getContracts()
|
|
87
|
+
const contracts = contractsResult.contracts;
|
|
105
88
|
|
|
106
89
|
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
options
|
|
91
|
+
// Build options with category headers
|
|
92
|
+
const options = [];
|
|
93
|
+
let currentCategory = null;
|
|
94
|
+
|
|
95
|
+
for (const c of contracts) {
|
|
96
|
+
// Add category header when category changes
|
|
97
|
+
if (c.categoryName !== currentCategory) {
|
|
98
|
+
currentCategory = c.categoryName;
|
|
99
|
+
options.push({
|
|
100
|
+
label: chalk.cyan.bold(`── ${currentCategory} ──`),
|
|
101
|
+
value: null,
|
|
102
|
+
disabled: true
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Format: "ESH6 - E-mini S&P 500 (CME)"
|
|
107
|
+
const label = ` ${c.symbol} - ${c.name} (${c.exchange})`;
|
|
108
|
+
options.push({ label, value: c });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
options.push({ label: '', value: null, disabled: true }); // Spacer
|
|
112
|
+
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
110
113
|
|
|
111
114
|
const contract = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
112
|
-
return contract === 'back' ? null : contract;
|
|
115
|
+
return contract === 'back' || contract === null ? null : contract;
|
|
113
116
|
};
|
|
114
117
|
|
|
115
118
|
/**
|
|
@@ -165,6 +165,155 @@ const PROTO_FILES = [
|
|
|
165
165
|
'response_front_month_contract.proto',
|
|
166
166
|
];
|
|
167
167
|
|
|
168
|
+
// Symbol Categories - Order matters for display (indices first)
|
|
169
|
+
const SYMBOL_CATEGORIES = {
|
|
170
|
+
INDICES: {
|
|
171
|
+
name: 'Indices',
|
|
172
|
+
order: 1,
|
|
173
|
+
symbols: {
|
|
174
|
+
// E-mini Indices
|
|
175
|
+
'ES': { name: 'E-mini S&P 500', tickSize: 0.25, tickValue: 12.50 },
|
|
176
|
+
'NQ': { name: 'E-mini Nasdaq-100', tickSize: 0.25, tickValue: 5.00 },
|
|
177
|
+
'YM': { name: 'E-mini Dow Jones', tickSize: 1.00, tickValue: 5.00 },
|
|
178
|
+
'RTY': { name: 'E-mini Russell 2000', tickSize: 0.10, tickValue: 5.00 },
|
|
179
|
+
'EMD': { name: 'E-mini S&P MidCap 400', tickSize: 0.10, tickValue: 10.00 },
|
|
180
|
+
// Micro Indices
|
|
181
|
+
'MES': { name: 'Micro E-mini S&P 500', tickSize: 0.25, tickValue: 1.25 },
|
|
182
|
+
'MNQ': { name: 'Micro E-mini Nasdaq-100', tickSize: 0.25, tickValue: 0.50 },
|
|
183
|
+
'MYM': { name: 'Micro E-mini Dow Jones', tickSize: 1.00, tickValue: 0.50 },
|
|
184
|
+
'M2K': { name: 'Micro E-mini Russell 2000', tickSize: 0.10, tickValue: 0.50 },
|
|
185
|
+
// International Indices
|
|
186
|
+
'NKD': { name: 'Nikkei 225 (USD)', tickSize: 5.00, tickValue: 25.00 },
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
ENERGY: {
|
|
190
|
+
name: 'Energy',
|
|
191
|
+
order: 2,
|
|
192
|
+
symbols: {
|
|
193
|
+
'CL': { name: 'Crude Oil WTI', tickSize: 0.01, tickValue: 10.00 },
|
|
194
|
+
'MCL': { name: 'Micro Crude Oil WTI', tickSize: 0.01, tickValue: 1.00 },
|
|
195
|
+
'BZ': { name: 'Brent Crude Oil', tickSize: 0.01, tickValue: 10.00 },
|
|
196
|
+
'NG': { name: 'Natural Gas', tickSize: 0.001, tickValue: 10.00 },
|
|
197
|
+
'HO': { name: 'Heating Oil', tickSize: 0.0001, tickValue: 4.20 },
|
|
198
|
+
'RB': { name: 'RBOB Gasoline', tickSize: 0.0001, tickValue: 4.20 },
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
METALS: {
|
|
202
|
+
name: 'Metals',
|
|
203
|
+
order: 3,
|
|
204
|
+
symbols: {
|
|
205
|
+
'GC': { name: 'Gold', tickSize: 0.10, tickValue: 10.00 },
|
|
206
|
+
'MGC': { name: 'Micro Gold', tickSize: 0.10, tickValue: 1.00 },
|
|
207
|
+
'1OZ': { name: '1 Ounce Gold', tickSize: 0.25, tickValue: 0.25 },
|
|
208
|
+
'SI': { name: 'Silver', tickSize: 0.005, tickValue: 25.00 },
|
|
209
|
+
'SIL': { name: 'Silver 1000oz', tickSize: 0.005, tickValue: 5.00 },
|
|
210
|
+
'HG': { name: 'Copper', tickSize: 0.0005, tickValue: 12.50 },
|
|
211
|
+
'MHG': { name: 'Micro Copper', tickSize: 0.0005, tickValue: 1.25 },
|
|
212
|
+
'PL': { name: 'Platinum', tickSize: 0.10, tickValue: 5.00 },
|
|
213
|
+
'PA': { name: 'Palladium', tickSize: 0.10, tickValue: 10.00 },
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
CURRENCIES: {
|
|
217
|
+
name: 'Currencies (FX)',
|
|
218
|
+
order: 4,
|
|
219
|
+
symbols: {
|
|
220
|
+
'6E': { name: 'Euro FX', tickSize: 0.00005, tickValue: 6.25 },
|
|
221
|
+
'M6E': { name: 'Micro Euro FX', tickSize: 0.0001, tickValue: 1.25 },
|
|
222
|
+
'6B': { name: 'British Pound', tickSize: 0.0001, tickValue: 6.25 },
|
|
223
|
+
'M6B': { name: 'Micro British Pound', tickSize: 0.0001, tickValue: 0.625 },
|
|
224
|
+
'6J': { name: 'Japanese Yen', tickSize: 0.0000005, tickValue: 6.25 },
|
|
225
|
+
'6A': { name: 'Australian Dollar', tickSize: 0.0001, tickValue: 10.00 },
|
|
226
|
+
'M6A': { name: 'Micro Australian Dollar', tickSize: 0.0001, tickValue: 1.00 },
|
|
227
|
+
'6C': { name: 'Canadian Dollar', tickSize: 0.00005, tickValue: 5.00 },
|
|
228
|
+
'6S': { name: 'Swiss Franc', tickSize: 0.0001, tickValue: 12.50 },
|
|
229
|
+
'6N': { name: 'New Zealand Dollar', tickSize: 0.0001, tickValue: 10.00 },
|
|
230
|
+
'6M': { name: 'Mexican Peso', tickSize: 0.00001, tickValue: 5.00 },
|
|
231
|
+
'E7': { name: 'E-mini Euro FX', tickSize: 0.0001, tickValue: 6.25 },
|
|
232
|
+
'RF': { name: 'Euro FX/Swiss Franc', tickSize: 0.0001, tickValue: 12.50 },
|
|
233
|
+
'RP': { name: 'Euro FX/British Pound', tickSize: 0.00005, tickValue: 6.25 },
|
|
234
|
+
'RY': { name: 'Euro FX/Japanese Yen', tickSize: 0.01, tickValue: 6.25 },
|
|
235
|
+
'SEK': { name: 'Swedish Krona', tickSize: 0.00001, tickValue: 12.50 },
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
CRYPTO: {
|
|
239
|
+
name: 'Crypto',
|
|
240
|
+
order: 5,
|
|
241
|
+
symbols: {
|
|
242
|
+
'BTC': { name: 'Bitcoin', tickSize: 5.00, tickValue: 25.00 },
|
|
243
|
+
'MBT': { name: 'Micro Bitcoin', tickSize: 5.00, tickValue: 0.50 },
|
|
244
|
+
'ETH': { name: 'Ether', tickSize: 0.25, tickValue: 12.50 },
|
|
245
|
+
'MET': { name: 'Micro Ether', tickSize: 0.05, tickValue: 0.25 },
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
RATES: {
|
|
249
|
+
name: 'Interest Rates',
|
|
250
|
+
order: 6,
|
|
251
|
+
symbols: {
|
|
252
|
+
'ZB': { name: '30-Year T-Bond', tickSize: 0.03125, tickValue: 31.25 },
|
|
253
|
+
'ZN': { name: '10-Year T-Note', tickSize: 0.015625, tickValue: 15.625 },
|
|
254
|
+
'ZF': { name: '5-Year T-Note', tickSize: 0.0078125, tickValue: 7.8125 },
|
|
255
|
+
'ZT': { name: '2-Year T-Note', tickSize: 0.0078125, tickValue: 15.625 },
|
|
256
|
+
'TN': { name: 'Ultra 10-Year T-Note', tickSize: 0.015625, tickValue: 15.625 },
|
|
257
|
+
'ZQ': { name: '30-Day Fed Funds', tickSize: 0.0025, tickValue: 10.4167 },
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
AGRICULTURE: {
|
|
261
|
+
name: 'Agriculture',
|
|
262
|
+
order: 7,
|
|
263
|
+
symbols: {
|
|
264
|
+
'ZC': { name: 'Corn', tickSize: 0.25, tickValue: 12.50 },
|
|
265
|
+
'ZS': { name: 'Soybeans', tickSize: 0.25, tickValue: 12.50 },
|
|
266
|
+
'ZW': { name: 'Wheat', tickSize: 0.25, tickValue: 12.50 },
|
|
267
|
+
'ZL': { name: 'Soybean Oil', tickSize: 0.01, tickValue: 6.00 },
|
|
268
|
+
'ZM': { name: 'Soybean Meal', tickSize: 0.10, tickValue: 10.00 },
|
|
269
|
+
'ZO': { name: 'Oats', tickSize: 0.25, tickValue: 12.50 },
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
MEATS: {
|
|
273
|
+
name: 'Meats',
|
|
274
|
+
order: 8,
|
|
275
|
+
symbols: {
|
|
276
|
+
'LE': { name: 'Live Cattle', tickSize: 0.025, tickValue: 10.00 },
|
|
277
|
+
'HE': { name: 'Lean Hogs', tickSize: 0.025, tickValue: 10.00 },
|
|
278
|
+
'GF': { name: 'Feeder Cattle', tickSize: 0.025, tickValue: 12.50 },
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get symbol info (category, name, tick info)
|
|
285
|
+
*/
|
|
286
|
+
function getSymbolInfo(baseSymbol) {
|
|
287
|
+
for (const [catKey, category] of Object.entries(SYMBOL_CATEGORIES)) {
|
|
288
|
+
if (category.symbols[baseSymbol]) {
|
|
289
|
+
return {
|
|
290
|
+
category: catKey,
|
|
291
|
+
categoryName: category.name,
|
|
292
|
+
categoryOrder: category.order,
|
|
293
|
+
...category.symbols[baseSymbol]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Unknown symbol
|
|
298
|
+
return {
|
|
299
|
+
category: 'OTHER',
|
|
300
|
+
categoryName: 'Other',
|
|
301
|
+
categoryOrder: 99,
|
|
302
|
+
name: baseSymbol,
|
|
303
|
+
tickSize: null,
|
|
304
|
+
tickValue: null
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get all categories in display order
|
|
310
|
+
*/
|
|
311
|
+
function getCategoryOrder() {
|
|
312
|
+
return Object.entries(SYMBOL_CATEGORIES)
|
|
313
|
+
.sort((a, b) => a[1].order - b[1].order)
|
|
314
|
+
.map(([key, val]) => ({ key, name: val.name, order: val.order }));
|
|
315
|
+
}
|
|
316
|
+
|
|
168
317
|
module.exports = {
|
|
169
318
|
RITHMIC_ENDPOINTS,
|
|
170
319
|
RITHMIC_SYSTEMS,
|
|
@@ -173,4 +322,7 @@ module.exports = {
|
|
|
173
322
|
RES,
|
|
174
323
|
STREAM,
|
|
175
324
|
PROTO_FILES,
|
|
325
|
+
SYMBOL_CATEGORIES,
|
|
326
|
+
getSymbolInfo,
|
|
327
|
+
getCategoryOrder,
|
|
176
328
|
};
|
|
@@ -5,53 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
const EventEmitter = require('events');
|
|
7
7
|
const { RithmicConnection } = require('./connection');
|
|
8
|
-
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
|
|
8
|
+
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, getSymbolInfo, getCategoryOrder } = 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
|
|
259
|
+
* Get front month contracts for multiple symbols at once - batch request
|
|
297
260
|
*/
|
|
298
|
-
async
|
|
261
|
+
async getFrontMonthsBatch(products) {
|
|
299
262
|
if (!this.tickerConn) {
|
|
300
|
-
|
|
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
|
|
310
|
-
const
|
|
311
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
273
|
+
const baseSymbol = decoded.userMsg;
|
|
274
|
+
|
|
275
|
+
if (pending.has(baseSymbol)) {
|
|
276
|
+
pending.delete(baseSymbol);
|
|
320
277
|
|
|
321
|
-
if (decoded.rpCode[0] === '0') {
|
|
322
|
-
|
|
278
|
+
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
279
|
+
results.set(baseSymbol, {
|
|
280
|
+
symbol: decoded.tradingSymbol,
|
|
323
281
|
baseSymbol: baseSymbol,
|
|
324
|
-
|
|
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,181 @@ class RithmicService extends EventEmitter {
|
|
|
333
288
|
|
|
334
289
|
this.tickerConn.on('message', handler);
|
|
335
290
|
|
|
336
|
-
// Send
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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 and add category info
|
|
429
|
+
const results = [];
|
|
430
|
+
for (const [baseSymbol, contract] of contracts) {
|
|
431
|
+
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
432
|
+
const product = productsToCheck.get(productKey);
|
|
433
|
+
const symbolInfo = getSymbolInfo(baseSymbol);
|
|
434
|
+
|
|
435
|
+
results.push({
|
|
436
|
+
symbol: contract.symbol,
|
|
437
|
+
baseSymbol: baseSymbol,
|
|
438
|
+
name: symbolInfo.name || product?.productName || baseSymbol,
|
|
439
|
+
exchange: contract.exchange,
|
|
440
|
+
category: symbolInfo.category,
|
|
441
|
+
categoryName: symbolInfo.categoryName,
|
|
442
|
+
categoryOrder: symbolInfo.categoryOrder,
|
|
443
|
+
tickSize: symbolInfo.tickSize,
|
|
444
|
+
tickValue: symbolInfo.tickValue,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Sort by category order, then by symbol name within category
|
|
449
|
+
results.sort((a, b) => {
|
|
450
|
+
if (a.categoryOrder !== b.categoryOrder) {
|
|
451
|
+
return a.categoryOrder - b.categoryOrder;
|
|
452
|
+
}
|
|
453
|
+
return a.baseSymbol.localeCompare(b.baseSymbol);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
debug(`Got ${results.length} tradeable contracts from API`);
|
|
457
|
+
resolve(results);
|
|
458
|
+
}, 8000);
|
|
459
|
+
}, 5000);
|
|
343
460
|
});
|
|
344
461
|
}
|
|
345
462
|
|
|
346
463
|
/**
|
|
347
|
-
* Get all available contracts - REAL DATA from Rithmic API
|
|
464
|
+
* Get all available contracts - 100% REAL DATA from Rithmic API
|
|
465
|
+
* Fetches product codes and front months directly from the API
|
|
348
466
|
*/
|
|
349
467
|
async getContracts() {
|
|
350
468
|
// Return cached if available and fresh (5 min cache)
|
|
@@ -366,37 +484,14 @@ class RithmicService extends EventEmitter {
|
|
|
366
484
|
}
|
|
367
485
|
}
|
|
368
486
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
debug(`Fetching front months for ${allSymbols.length} symbols...`);
|
|
487
|
+
// Increase max listeners to avoid warnings
|
|
488
|
+
this.tickerConn.setMaxListeners(5000);
|
|
373
489
|
|
|
374
|
-
|
|
375
|
-
const
|
|
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
|
-
});
|
|
393
|
-
|
|
394
|
-
const results = await Promise.all(promises);
|
|
395
|
-
contracts.push(...results.filter(r => r !== null));
|
|
396
|
-
}
|
|
490
|
+
debug('Fetching all tradeable contracts from Rithmic API...');
|
|
491
|
+
const contracts = await this.fetchAllFrontMonths();
|
|
397
492
|
|
|
398
493
|
if (contracts.length === 0) {
|
|
399
|
-
return { success: false, error: 'No contracts
|
|
494
|
+
return { success: false, error: 'No tradeable contracts found' };
|
|
400
495
|
}
|
|
401
496
|
|
|
402
497
|
// Cache the results
|
package/src/utils/prompts.js
CHANGED
|
@@ -126,6 +126,7 @@ const numberInput = async (message, defaultVal = 1, min = 1, max = 1000) => {
|
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
128
|
* Select - arrow keys navigation
|
|
129
|
+
* Supports disabled options (separators) via opt.disabled
|
|
129
130
|
*/
|
|
130
131
|
const selectOption = async (message, options) => {
|
|
131
132
|
// Close shared readline before inquirer to avoid conflicts
|
|
@@ -135,10 +136,16 @@ const selectOption = async (message, options) => {
|
|
|
135
136
|
}
|
|
136
137
|
prepareStdin();
|
|
137
138
|
|
|
138
|
-
const choices = options.map(opt =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const choices = options.map(opt => {
|
|
140
|
+
if (opt.disabled) {
|
|
141
|
+
// Use inquirer Separator for disabled items (category headers)
|
|
142
|
+
return new inquirer.Separator(opt.label);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
name: opt.label,
|
|
146
|
+
value: opt.value
|
|
147
|
+
};
|
|
148
|
+
});
|
|
142
149
|
|
|
143
150
|
const { value } = await inquirer.prompt([{
|
|
144
151
|
type: 'list',
|
|
@@ -147,7 +154,7 @@ const selectOption = async (message, options) => {
|
|
|
147
154
|
choices,
|
|
148
155
|
prefix: '',
|
|
149
156
|
loop: false,
|
|
150
|
-
pageSize:
|
|
157
|
+
pageSize: 20 // Increased to show more symbols
|
|
151
158
|
}]);
|
|
152
159
|
|
|
153
160
|
return value;
|