hedgequantx 2.9.112 → 2.9.113

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.112",
3
+ "version": "2.9.113",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/lib/data.js CHANGED
@@ -171,11 +171,12 @@ class MarketDataFeed extends EventEmitter {
171
171
  }
172
172
 
173
173
  /**
174
- * Get historical tick bars from HISTORY_PLANT
174
+ * Get historical 1-minute bars from HISTORY_PLANT
175
+ * Fetches raw ticks and aggregates them into 1-minute OHLCV bars
175
176
  * @param {string} symbol - Symbol (e.g., 'MNQH6')
176
177
  * @param {string} exchange - Exchange (default: 'CME')
177
- * @param {number} barCount - Number of bars to fetch (default: 30)
178
- * @returns {Promise<Array>} Array of tick bar objects {timestamp, open, high, low, close, volume}
178
+ * @param {number} barCount - Number of 1-min bars to return (default: 30)
179
+ * @returns {Promise<Array>} Array of 1-min bar objects {timestamp, open, high, low, close, volume}
179
180
  */
180
181
  async getHistoricalBars(symbol, exchange = 'CME', barCount = 30) {
181
182
  if (!this.credentials) {
@@ -193,51 +194,55 @@ class MarketDataFeed extends EventEmitter {
193
194
  await historyConn.connect(historyConfig);
194
195
 
195
196
  return new Promise((resolve, reject) => {
196
- const bars = [];
197
+ const ticks = [];
197
198
  let isComplete = false;
198
199
 
200
+ // Request more ticks to cover barCount minutes (estimate ~1500 ticks/min for MNQ)
201
+ const ticksToRequest = Math.min(50000, barCount * 2000);
202
+
199
203
  const timeout = setTimeout(() => {
200
204
  if (!isComplete) {
201
205
  historyConn.disconnect();
202
- // Return what we have even on timeout
203
- resolve(bars);
206
+ // Aggregate what we have and return
207
+ const aggregatedBars = this._aggregateTicksTo1MinBars(ticks, barCount);
208
+ resolve(aggregatedBars);
204
209
  }
205
- }, 15000);
210
+ }, 20000);
206
211
 
207
- // Listen for bar data (template 207)
212
+ // Listen for tick data (template 207)
208
213
  historyConn.on('message', (msg) => {
209
214
  if (msg.templateId === RES.TICK_BAR_REPLAY) {
210
215
  try {
211
216
  const data = proto.decode('ResponseTickBarReplay', msg.data);
212
- const barObj = data.toJSON ? data.toJSON() : data;
217
+ const tickObj = data.toJSON ? data.toJSON() : data;
213
218
 
214
219
  // Check for error response
215
- if (barObj.rpCode && barObj.rpCode[0] !== '0') {
220
+ if (tickObj.rpCode && tickObj.rpCode[0] !== '0') {
216
221
  clearTimeout(timeout);
217
222
  isComplete = true;
218
223
  historyConn.disconnect();
219
- reject(new Error(`History request failed: ${barObj.rpCode.join(' - ')}`));
224
+ reject(new Error(`History request failed: ${tickObj.rpCode.join(' - ')}`));
220
225
  return;
221
226
  }
222
227
 
223
- // Add bar if it has price data (OHLCV format)
224
- if (barObj.closePrice !== undefined) {
225
- bars.push({
226
- timestamp: barObj.ssboe ? barObj.ssboe * 1000 : Date.now(),
227
- open: Number(barObj.openPrice) || Number(barObj.closePrice),
228
- high: Number(barObj.highPrice) || Number(barObj.closePrice),
229
- low: Number(barObj.lowPrice) || Number(barObj.closePrice),
230
- close: Number(barObj.closePrice),
231
- volume: Number(barObj.volume) || 1,
228
+ // Add tick if it has price data
229
+ if (tickObj.closePrice !== undefined) {
230
+ ticks.push({
231
+ timestamp: tickObj.ssboe ? tickObj.ssboe * 1000 : Date.now(),
232
+ price: Number(tickObj.closePrice),
233
+ volume: Number(tickObj.volume) || 1,
232
234
  });
233
235
  }
234
236
 
235
237
  // Check if this is the final response (rpCode = ['0'])
236
- if (barObj.rpCode && barObj.rpCode[0] === '0' && barObj.closePrice === undefined) {
238
+ if (tickObj.rpCode && tickObj.rpCode[0] === '0' && tickObj.closePrice === undefined) {
237
239
  clearTimeout(timeout);
238
240
  isComplete = true;
239
241
  historyConn.disconnect();
240
- resolve(bars);
242
+
243
+ // Aggregate ticks into 1-minute bars
244
+ const aggregatedBars = this._aggregateTicksTo1MinBars(ticks, barCount);
245
+ resolve(aggregatedBars);
241
246
  }
242
247
  } catch (e) {
243
248
  this.emit('debug', `History decode error: ${e.message}`);
@@ -247,25 +252,22 @@ class MarketDataFeed extends EventEmitter {
247
252
 
248
253
  // Login to HISTORY_PLANT
249
254
  historyConn.once('loggedIn', () => {
250
- // Request tick bar replay - limited count, most recent bars
251
- // startIndex/finishIndex are required, but userMaxCount + direction=LAST
252
- // will return only the most recent barCount bars
253
255
  const now = Math.floor(Date.now() / 1000);
254
- const startTime = now - (24 * 60 * 60); // 24 hours ago (wide range)
256
+ const startTime = now - (barCount * 2 * 60); // Request 2x the time needed
255
257
 
256
258
  historyConn.send('RequestTickBarReplay', {
257
259
  templateId: REQ.TICK_BAR_REPLAY, // 206
258
260
  userMsg: ['hqx-history'],
259
261
  symbol: symbol,
260
262
  exchange: exchange,
261
- barType: 1, // TICK_BAR
263
+ barType: 1, // TICK_BAR (only option for raw ticks)
262
264
  barSubType: 1, // REGULAR
263
265
  barTypeSpecifier: '1', // 1 tick per bar
264
- startIndex: startTime, // Required by Rithmic
265
- finishIndex: now, // Required by Rithmic
266
- userMaxCount: barCount, // Limit to barCount bars
267
- direction: 2, // LAST (most recent bars first)
268
- timeOrder: 1, // FORWARDS (return in chronological order)
266
+ startIndex: startTime, // Start time
267
+ finishIndex: now, // End time
268
+ userMaxCount: ticksToRequest, // Request enough ticks
269
+ direction: 2, // LAST (most recent first)
270
+ timeOrder: 1, // FORWARDS (chronological order)
269
271
  });
270
272
  });
271
273
 
@@ -283,6 +285,50 @@ class MarketDataFeed extends EventEmitter {
283
285
  }
284
286
  }
285
287
 
288
+ /**
289
+ * Aggregate raw ticks into 1-minute OHLCV bars
290
+ * @param {Array} ticks - Array of {timestamp, price, volume}
291
+ * @param {number} maxBars - Maximum number of bars to return
292
+ * @returns {Array} Array of 1-min bars {timestamp, open, high, low, close, volume}
293
+ */
294
+ _aggregateTicksTo1MinBars(ticks, maxBars) {
295
+ if (!ticks || ticks.length === 0) return [];
296
+
297
+ // Sort ticks by timestamp (oldest first)
298
+ const sortedTicks = [...ticks].sort((a, b) => a.timestamp - b.timestamp);
299
+
300
+ // Group ticks by minute
301
+ const barMap = new Map();
302
+ const barIntervalMs = 60000; // 1 minute
303
+
304
+ for (const tick of sortedTicks) {
305
+ const barStartTime = Math.floor(tick.timestamp / barIntervalMs) * barIntervalMs;
306
+
307
+ if (!barMap.has(barStartTime)) {
308
+ barMap.set(barStartTime, {
309
+ timestamp: barStartTime,
310
+ open: tick.price,
311
+ high: tick.price,
312
+ low: tick.price,
313
+ close: tick.price,
314
+ volume: tick.volume,
315
+ });
316
+ } else {
317
+ const bar = barMap.get(barStartTime);
318
+ bar.high = Math.max(bar.high, tick.price);
319
+ bar.low = Math.min(bar.low, tick.price);
320
+ bar.close = tick.price;
321
+ bar.volume += tick.volume;
322
+ }
323
+ }
324
+
325
+ // Convert to array and sort by timestamp
326
+ const bars = Array.from(barMap.values()).sort((a, b) => a.timestamp - b.timestamp);
327
+
328
+ // Return only the last maxBars bars
329
+ return bars.slice(-maxBars);
330
+ }
331
+
286
332
  /**
287
333
  * Handle incoming messages from TICKER_PLANT
288
334
  */
@@ -409,22 +409,11 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
409
409
  await marketFeed.connect(rithmicCredentials);
410
410
  await marketFeed.subscribe(symbolCode, contract.exchange || 'CME');
411
411
 
412
- // Preload historical bars for HQX-2B strategy only (bar-based strategy)
413
- // Note: HISTORY_PLANT may not be available on all accounts (e.g., paper trading)
414
- if (strategyId === 'hqx-2b' && strategy.preloadBars) {
415
- try {
416
- ui.addLog('system', 'Loading historical bars...');
417
- const historicalBars = await marketFeed.getHistoricalBars(symbolCode, contract.exchange || 'CME', 30);
418
- if (historicalBars && historicalBars.length > 0) {
419
- strategy.preloadBars(contractId, historicalBars);
420
- ui.addLog('system', `Loaded ${historicalBars.length} bars - ready!`);
421
- } else {
422
- ui.addLog('system', 'No historical data - warming up with live bars...');
423
- }
424
- } catch (histErr) {
425
- // HISTORY_PLANT not available (common on paper accounts)
426
- ui.addLog('system', 'Historical data not available - warming up with live bars...');
427
- }
412
+ // HQX-2B strategy warmup message
413
+ // Note: HISTORY_PLANT returns limited ticks (10k max = ~1 second of data)
414
+ // which is not enough for 1-min bar aggregation. Strategy warms up with live data.
415
+ if (strategyId === 'hqx-2b') {
416
+ ui.addLog('system', 'HQX-2B warming up - needs ~3 min for first signals...');
428
417
  }
429
418
  } catch (e) {
430
419
  ui.addLog('error', `Failed to connect: ${e.message}`);