hedgequantx 1.8.34 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.8.34",
3
+ "version": "1.8.35",
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": {
@@ -180,7 +180,7 @@ const getContractsFromAPI = async () => {
180
180
  };
181
181
 
182
182
  /**
183
- * Symbol selection helper - uses API contracts for all services
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
- const options = contracts.map(c => ({ label: c.name || c.symbol, value: c }));
222
- options.push({ label: '< Cancel', value: null });
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 - same as copy-trading
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
- // Normalize contract structure - API returns { name: "ESH6", description: "E-mini S&P 500..." }
87
- const contracts = contractsResult.contracts.map(c => ({
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
- const options = contracts.map(c => ({ label: c.name, value: c }));
109
- options.push({ label: '< Back', value: 'back' });
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,7 +5,7 @@
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');
@@ -425,19 +425,34 @@ class RithmicService extends EventEmitter {
425
425
  setTimeout(() => {
426
426
  this.tickerConn.removeListener('message', frontMonthHandler);
427
427
 
428
- // Merge with product names
428
+ // Merge with product names and add category info
429
429
  const results = [];
430
430
  for (const [baseSymbol, contract] of contracts) {
431
431
  const productKey = `${baseSymbol}:${contract.exchange}`;
432
432
  const product = productsToCheck.get(productKey);
433
+ const symbolInfo = getSymbolInfo(baseSymbol);
434
+
433
435
  results.push({
434
436
  symbol: contract.symbol,
435
437
  baseSymbol: baseSymbol,
436
- name: product ? product.productName : baseSymbol,
438
+ name: symbolInfo.name || product?.productName || baseSymbol,
437
439
  exchange: contract.exchange,
440
+ category: symbolInfo.category,
441
+ categoryName: symbolInfo.categoryName,
442
+ categoryOrder: symbolInfo.categoryOrder,
443
+ tickSize: symbolInfo.tickSize,
444
+ tickValue: symbolInfo.tickValue,
438
445
  });
439
446
  }
440
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
+
441
456
  debug(`Got ${results.length} tradeable contracts from API`);
442
457
  resolve(results);
443
458
  }, 8000);
@@ -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
- name: opt.label,
140
- value: opt.value
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: 15
157
+ pageSize: 20 // Increased to show more symbols
151
158
  }]);
152
159
 
153
160
  return value;