hedgequantx 2.9.164 → 2.9.166

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.164",
3
+ "version": "2.9.166",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -83,12 +83,18 @@ const fetchAllFrontMonths = (service) => {
83
83
  throw new Error('TICKER_PLANT not connected');
84
84
  }
85
85
 
86
+ const tickerState = service.tickerConn.connectionState;
87
+ const tickerConnected = service.tickerConn.isConnected;
88
+ log.debug('fetchAllFrontMonths starting', { tickerState, tickerConnected });
89
+
86
90
  return new Promise((resolve) => {
87
91
  const contracts = new Map();
88
92
  const productsToCheck = new Map();
93
+ let msgCount = 0;
89
94
 
90
95
  // Handler for ProductCodes responses
91
96
  const productHandler = (msg) => {
97
+ msgCount++;
92
98
  if (msg.templateId !== 112) return;
93
99
 
94
100
  const decoded = decodeProductCodes(msg.data);
@@ -112,6 +118,7 @@ const fetchAllFrontMonths = (service) => {
112
118
 
113
119
  // Handler for FrontMonth responses
114
120
  const frontMonthHandler = (msg) => {
121
+ msgCount++;
115
122
  if (msg.templateId !== 114) return;
116
123
 
117
124
  const decoded = decodeFrontMonthContract(msg.data);
@@ -128,23 +135,36 @@ const fetchAllFrontMonths = (service) => {
128
135
  service.tickerConn.on('message', frontMonthHandler);
129
136
 
130
137
  // Request all product codes
131
- service.tickerConn.send('RequestProductCodes', {
132
- templateId: 111,
133
- userMsg: ['get-products'],
134
- });
138
+ log.debug('Sending RequestProductCodes');
139
+ try {
140
+ service.tickerConn.send('RequestProductCodes', {
141
+ templateId: 111,
142
+ userMsg: ['get-products'],
143
+ });
144
+ } catch (err) {
145
+ log.warn('Failed to send RequestProductCodes', { error: err.message });
146
+ }
135
147
 
136
148
  // After timeout, request front months
137
149
  setTimeout(() => {
138
150
  service.tickerConn.removeListener('message', productHandler);
139
- log.debug('Collected products', { count: productsToCheck.size });
151
+ log.debug('Collected products', { count: productsToCheck.size, totalMsgs: msgCount });
152
+
153
+ if (productsToCheck.size === 0) {
154
+ log.warn('No products collected - TICKER may not be working');
155
+ }
140
156
 
141
157
  for (const product of productsToCheck.values()) {
142
- service.tickerConn.send('RequestFrontMonthContract', {
143
- templateId: 113,
144
- userMsg: [product.productCode],
145
- symbol: product.productCode,
146
- exchange: product.exchange,
147
- });
158
+ try {
159
+ service.tickerConn.send('RequestFrontMonthContract', {
160
+ templateId: 113,
161
+ userMsg: [product.productCode],
162
+ symbol: product.productCode,
163
+ exchange: product.exchange,
164
+ });
165
+ } catch (err) {
166
+ log.warn('Failed to send RequestFrontMonthContract', { product: product.productCode, error: err.message });
167
+ }
148
168
  }
149
169
 
150
170
  // Collect results after timeout
@@ -171,7 +191,7 @@ const fetchAllFrontMonths = (service) => {
171
191
  // Sort alphabetically by base symbol
172
192
  results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
173
193
 
174
- log.debug('Got contracts from API', { count: results.length });
194
+ log.debug('Got contracts from API', { count: results.length, totalMsgs: msgCount });
175
195
  resolve(results);
176
196
  }, TIMEOUTS.RITHMIC_PRODUCTS);
177
197
  }, TIMEOUTS.RITHMIC_CONTRACTS);
@@ -342,22 +342,70 @@ class RithmicBrokerDaemon {
342
342
  // Log service state for debugging
343
343
  const hasCredentials = !!conn.service.credentials;
344
344
  const hasTickerConn = !!conn.service.tickerConn;
345
- log('DEBUG', 'getContracts request', { propfirm: payload.propfirmKey, hasCredentials, hasTickerConn });
345
+ const tickerState = conn.service.tickerConn?.connectionState;
346
+ log('DEBUG', 'getContracts request', { propfirm: payload.propfirmKey, hasCredentials, hasTickerConn, tickerState });
346
347
 
347
348
  try {
348
349
  const result = await conn.service.getContracts();
350
+
351
+ // Log detailed result
352
+ const tickerStateAfter = conn.service.tickerConn?.connectionState;
349
353
  log('DEBUG', 'getContracts result', {
350
354
  propfirm: payload.propfirmKey,
351
355
  success: result.success,
352
356
  count: result.contracts?.length || 0,
357
+ source: result.source,
358
+ tickerStateAfter,
353
359
  error: result.error
354
360
  });
361
+
362
+ // If no contracts found, return fallback list for common futures
363
+ if (!result.success || result.contracts?.length === 0) {
364
+ log('WARN', 'Using fallback contracts list');
365
+ const fallbackContracts = this._getFallbackContracts();
366
+ return { type: 'contracts', payload: { success: true, contracts: fallbackContracts, source: 'fallback' }, requestId };
367
+ }
368
+
355
369
  return { type: 'contracts', payload: result, requestId };
356
370
  } catch (err) {
357
371
  log('ERROR', 'getContracts exception', { propfirm: payload.propfirmKey, error: err.message, stack: err.stack?.split('\n')[1] });
358
- return { type: 'contracts', payload: { success: false, error: err.message, contracts: [] }, requestId };
372
+ // Return fallback on error
373
+ log('WARN', 'Using fallback contracts due to error');
374
+ const fallbackContracts = this._getFallbackContracts();
375
+ return { type: 'contracts', payload: { success: true, contracts: fallbackContracts, source: 'fallback' }, requestId };
359
376
  }
360
377
  }
378
+
379
+ /**
380
+ * Get fallback contracts list when TICKER_PLANT fails
381
+ * These are common futures that most prop firms support
382
+ */
383
+ _getFallbackContracts() {
384
+ const now = new Date();
385
+ const month = now.getMonth();
386
+ const year = now.getFullYear();
387
+
388
+ // Determine front month code (H=Mar, M=Jun, U=Sep, Z=Dec for indices)
389
+ const monthCodes = ['H', 'H', 'H', 'M', 'M', 'M', 'U', 'U', 'U', 'Z', 'Z', 'Z'];
390
+ const frontMonth = monthCodes[month];
391
+ const yearCode = String(year).slice(-1);
392
+ const suffix = frontMonth + yearCode;
393
+
394
+ return [
395
+ { symbol: `MNQ${suffix}`, baseSymbol: 'MNQ', name: 'Micro E-mini Nasdaq-100', exchange: 'CME', tickSize: 0.25 },
396
+ { symbol: `MES${suffix}`, baseSymbol: 'MES', name: 'Micro E-mini S&P 500', exchange: 'CME', tickSize: 0.25 },
397
+ { symbol: `NQ${suffix}`, baseSymbol: 'NQ', name: 'E-mini Nasdaq-100', exchange: 'CME', tickSize: 0.25 },
398
+ { symbol: `ES${suffix}`, baseSymbol: 'ES', name: 'E-mini S&P 500', exchange: 'CME', tickSize: 0.25 },
399
+ { symbol: `MCL${suffix}`, baseSymbol: 'MCL', name: 'Micro WTI Crude Oil', exchange: 'NYMEX', tickSize: 0.01 },
400
+ { symbol: `MGC${suffix}`, baseSymbol: 'MGC', name: 'Micro Gold', exchange: 'COMEX', tickSize: 0.10 },
401
+ { symbol: `M2K${suffix}`, baseSymbol: 'M2K', name: 'Micro E-mini Russell 2000', exchange: 'CME', tickSize: 0.10 },
402
+ { symbol: `MYM${suffix}`, baseSymbol: 'MYM', name: 'Micro E-mini Dow', exchange: 'CBOT', tickSize: 0.50 },
403
+ { symbol: `RTY${suffix}`, baseSymbol: 'RTY', name: 'E-mini Russell 2000', exchange: 'CME', tickSize: 0.10 },
404
+ { symbol: `YM${suffix}`, baseSymbol: 'YM', name: 'E-mini Dow', exchange: 'CBOT', tickSize: 1.00 },
405
+ { symbol: `CL${suffix}`, baseSymbol: 'CL', name: 'Crude Oil', exchange: 'NYMEX', tickSize: 0.01 },
406
+ { symbol: `GC${suffix}`, baseSymbol: 'GC', name: 'Gold', exchange: 'COMEX', tickSize: 0.10 },
407
+ ];
408
+ }
361
409
 
362
410
  async _handleSearchContracts(payload, requestId) {
363
411
  const conn = this.connections.get(payload.propfirmKey);
@@ -111,19 +111,38 @@ const connections = {
111
111
  log.info('Connection added', { type, propfirm: propfirm || type });
112
112
  },
113
113
 
114
+ /**
115
+ * Sanitize account data to prevent corrupted data from being saved
116
+ */
117
+ _sanitizeAccount(acc) {
118
+ if (!acc || typeof acc !== 'object' || !acc.accountId) return null;
119
+ return {
120
+ accountId: String(acc.accountId),
121
+ fcmId: acc.fcmId ? String(acc.fcmId) : undefined,
122
+ ibId: acc.ibId ? String(acc.ibId) : undefined,
123
+ accountName: acc.accountName ? String(acc.accountName) : undefined,
124
+ };
125
+ },
126
+
114
127
  saveToStorage() {
115
128
  // Load existing sessions to preserve AI agents
116
129
  const existingSessions = storage.load();
117
130
  const aiSessions = existingSessions.filter(s => s.type === 'ai');
118
131
 
119
132
  // Build Rithmic sessions - INCLUDE accounts to avoid Rithmic API limit on restore
120
- const rithmicSessions = this.services.map(conn => ({
121
- type: conn.type,
122
- propfirm: conn.propfirm,
123
- propfirmKey: conn.service.propfirmKey || conn.propfirmKey,
124
- credentials: conn.service.credentials,
125
- accounts: conn.service.accounts || [], // CRITICAL: Cache accounts to avoid 2000 GetAccounts limit
126
- }));
133
+ const rithmicSessions = this.services.map(conn => {
134
+ // Sanitize accounts to prevent corrupted data
135
+ const rawAccounts = conn.service.accounts || [];
136
+ const accounts = rawAccounts.map(a => this._sanitizeAccount(a)).filter(Boolean);
137
+
138
+ return {
139
+ type: conn.type,
140
+ propfirm: conn.propfirm,
141
+ propfirmKey: conn.service.propfirmKey || conn.propfirmKey,
142
+ credentials: conn.service.credentials,
143
+ accounts, // CRITICAL: Cache sanitized accounts to avoid 2000 GetAccounts limit
144
+ };
145
+ });
127
146
 
128
147
  // Merge: AI sessions + Rithmic sessions
129
148
  storage.save([...aiSessions, ...rithmicSessions]);
@@ -200,8 +219,17 @@ const connections = {
200
219
  if (type === 'rithmic' && session.credentials) {
201
220
  const client = new RithmicBrokerClient(propfirmKey || 'apex_rithmic');
202
221
 
203
- // CRITICAL: Pass cached accounts to avoid Rithmic's 2000 GetAccounts limit
204
- const loginOptions = session.accounts ? { cachedAccounts: session.accounts } : {};
222
+ // Validate cached accounts before using
223
+ let validAccounts = null;
224
+ if (session.accounts && Array.isArray(session.accounts)) {
225
+ validAccounts = session.accounts
226
+ .map(a => this._sanitizeAccount(a))
227
+ .filter(Boolean);
228
+ if (validAccounts.length === 0) validAccounts = null;
229
+ }
230
+
231
+ // CRITICAL: Pass validated cached accounts to avoid Rithmic's 2000 GetAccounts limit
232
+ const loginOptions = validAccounts ? { cachedAccounts: validAccounts } : {};
205
233
  const result = await client.login(session.credentials.username, session.credentials.password, loginOptions);
206
234
 
207
235
  if (result.success) {
@@ -212,7 +240,7 @@ const connections = {
212
240
  propfirmKey,
213
241
  connectedAt: new Date(),
214
242
  });
215
- log.debug('Rithmic session restored via broker', { hasCachedAccounts: !!session.accounts });
243
+ log.debug('Rithmic session restored via broker', { hasCachedAccounts: !!validAccounts, accountCount: validAccounts?.length || 0 });
216
244
  }
217
245
  }
218
246
  },