hedgequantx 2.7.2 → 2.7.4

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.7.2",
3
+ "version": "2.7.4",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -214,8 +214,7 @@ const renderHQXScore = (data) => {
214
214
  * Render data source notice
215
215
  */
216
216
  const renderNotice = () => {
217
- console.log();
218
- console.log(chalk.gray(' Note: Rithmic API provides balance/P&L only. Trade history not available.'));
217
+ // No notice needed - all data comes from Rithmic API
219
218
  };
220
219
 
221
220
  module.exports = {
@@ -11,6 +11,7 @@ const ora = require('ora');
11
11
 
12
12
  const { connections } = require('../../services');
13
13
  const { prompts } = require('../../utils');
14
+ const { displayBanner } = require('../../ui');
14
15
  const { aggregateStats, calculateDerivedMetrics, calculateQuantMetrics, calculateHQXScore } = require('./metrics');
15
16
  const { renderOverview, renderPnLMetrics, renderQuantMetrics, renderTradesHistory, renderHQXScore, renderNotice } = require('./display');
16
17
  const { renderEquityCurve } = require('./chart');
@@ -196,6 +197,8 @@ const showStats = async (service) => {
196
197
  const accountData = await aggregateAccountData(activeAccounts);
197
198
 
198
199
  spinner.succeed('Stats loaded');
200
+ console.clear();
201
+ displayBanner();
199
202
  console.log();
200
203
 
201
204
  // Calculate stats from API data
@@ -36,10 +36,10 @@ const createOrderHandler = (service) => {
36
36
  handleShowOrdersResponse(service, data);
37
37
  break;
38
38
  case STREAM.EXCHANGE_NOTIFICATION:
39
- service.emit('exchangeNotification', data);
39
+ handleExchangeNotification(service, data);
40
40
  break;
41
41
  case STREAM.ORDER_NOTIFICATION:
42
- service.emit('orderNotification', data);
42
+ handleOrderNotification(service, data);
43
43
  break;
44
44
  }
45
45
  };
@@ -211,6 +211,93 @@ const handleInstrumentPnLUpdate = (service, data) => {
211
211
  }
212
212
  };
213
213
 
214
+ /**
215
+ * Handle exchange order notification (fills/trades)
216
+ * NotifyType: 5 = FILL
217
+ */
218
+ const handleExchangeNotification = (service, data) => {
219
+ try {
220
+ const res = proto.decode('ExchangeOrderNotification', data);
221
+ debug('Exchange notification:', res.notifyType, res.symbol);
222
+
223
+ // notifyType 5 = FILL (trade executed)
224
+ if (res.notifyType === 5 && res.fillPrice && res.fillSize) {
225
+ const trade = {
226
+ id: res.fillId || `${Date.now()}-${res.basketId}`,
227
+ accountId: res.accountId,
228
+ symbol: res.symbol,
229
+ exchange: res.exchange || 'CME',
230
+ side: res.transactionType, // 1=BUY, 2=SELL
231
+ price: parseFloat(res.fillPrice),
232
+ size: parseInt(res.fillSize),
233
+ fillTime: res.fillTime,
234
+ fillDate: res.fillDate,
235
+ basketId: res.basketId,
236
+ avgFillPrice: parseFloat(res.avgFillPrice || res.fillPrice),
237
+ totalFillSize: parseInt(res.totalFillSize || res.fillSize),
238
+ timestamp: Date.now(),
239
+ ssboe: res.ssboe,
240
+ usecs: res.usecs,
241
+ };
242
+
243
+ debug('Trade (fill) captured:', trade.symbol, trade.side === 1 ? 'BUY' : 'SELL', trade.size, '@', trade.price);
244
+
245
+ // Store in trades history
246
+ if (!service.trades) service.trades = [];
247
+ service.trades.push(trade);
248
+
249
+ // Keep max 1000 trades in memory
250
+ if (service.trades.length > 1000) {
251
+ service.trades = service.trades.slice(-1000);
252
+ }
253
+
254
+ service.emit('trade', trade);
255
+ }
256
+
257
+ service.emit('exchangeNotification', res);
258
+ } catch (e) {
259
+ debug('Error decoding ExchangeOrderNotification:', e.message);
260
+ }
261
+ };
262
+
263
+ /**
264
+ * Handle Rithmic order notification
265
+ */
266
+ const handleOrderNotification = (service, data) => {
267
+ try {
268
+ const res = proto.decode('RithmicOrderNotification', data);
269
+ debug('Order notification:', res.notifyType, res.symbol, res.status);
270
+
271
+ // Track order status changes
272
+ if (res.basketId) {
273
+ const order = {
274
+ basketId: res.basketId,
275
+ accountId: res.accountId,
276
+ symbol: res.symbol,
277
+ exchange: res.exchange || 'CME',
278
+ side: res.transactionType,
279
+ quantity: res.quantity,
280
+ price: res.price,
281
+ status: res.status,
282
+ notifyType: res.notifyType,
283
+ avgFillPrice: res.avgFillPrice,
284
+ totalFillSize: res.totalFillSize,
285
+ totalUnfilledSize: res.totalUnfilledSize,
286
+ timestamp: Date.now(),
287
+ };
288
+
289
+ service.emit('orderNotification', order);
290
+
291
+ // If order is complete (notifyType 15), calculate P&L
292
+ if (res.notifyType === 15 && res.avgFillPrice) {
293
+ debug('Order complete:', res.basketId, 'avg fill:', res.avgFillPrice);
294
+ }
295
+ }
296
+ } catch (e) {
297
+ debug('Error decoding RithmicOrderNotification:', e.message);
298
+ }
299
+ };
300
+
214
301
  module.exports = {
215
302
  createOrderHandler,
216
303
  createPnLHandler
@@ -16,7 +16,7 @@ const {
16
16
  subscribePnLUpdates,
17
17
  getPositions,
18
18
  } = require('./accounts');
19
- const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
19
+ const { placeOrder, cancelOrder, getOrders, getOrderHistory, getOrderHistoryDates, getTradeHistoryFull, closePosition } = require('./orders');
20
20
  const { getContracts, searchContracts } = require('./contracts');
21
21
  const { TIMEOUTS } = require('../../config/settings');
22
22
  const { logger } = require('../../utils/logger');
@@ -73,6 +73,9 @@ class RithmicService extends EventEmitter {
73
73
  // Cache
74
74
  this._contractsCache = null;
75
75
  this._contractsCacheTime = 0;
76
+
77
+ // Trades history (captured from ExchangeOrderNotification fills)
78
+ this.trades = [];
76
79
  }
77
80
 
78
81
  // ==================== AUTH ====================
@@ -226,18 +229,160 @@ class RithmicService extends EventEmitter {
226
229
  async getPositions() { return getPositions(this); }
227
230
  async getOrders() { return getOrders(this); }
228
231
  async getOrderHistory(date) { return getOrderHistory(this, date); }
232
+ async getOrderHistoryDates() { return getOrderHistoryDates(this); }
233
+ async getTradeHistoryFull(days) { return getTradeHistoryFull(this, days); }
229
234
  async placeOrder(orderData) { return placeOrder(this, orderData); }
230
235
  async cancelOrder(orderId) { return cancelOrder(this, orderId); }
231
236
  async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
232
237
  async getContracts() { return getContracts(this); }
233
238
  async searchContracts(searchText) { return searchContracts(this, searchText); }
234
239
 
235
- // ==================== STUBS ====================
240
+ // ==================== STATS & HISTORY ====================
236
241
 
237
242
  async getUser() { return this.user; }
238
- async getLifetimeStats() { return { success: true, stats: null }; }
239
- async getDailyStats() { return { success: true, stats: [] }; }
240
- async getTradeHistory() { return { success: true, trades: [] }; }
243
+
244
+ /**
245
+ * Get trade history from Rithmic API
246
+ * @param {string} accountId - Optional account filter
247
+ * @param {number} days - Number of days to look back (default 30)
248
+ */
249
+ async getTradeHistory(accountId, days = 30) {
250
+ // Fetch from API
251
+ const result = await getTradeHistoryFull(this, days);
252
+
253
+ if (!result.success) {
254
+ return { success: false, trades: [] };
255
+ }
256
+
257
+ let trades = result.trades || [];
258
+
259
+ // Filter by account if specified
260
+ if (accountId) {
261
+ trades = trades.filter(t => t.accountId === accountId);
262
+ }
263
+
264
+ // Add timestamp from fillDate/fillTime if not present
265
+ trades = trades.map(t => ({
266
+ ...t,
267
+ timestamp: t.timestamp || this._parseDateTime(t.fillDate, t.fillTime),
268
+ }));
269
+
270
+ // Sort by timestamp descending (newest first)
271
+ trades.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
272
+
273
+ return { success: true, trades };
274
+ }
275
+
276
+ /**
277
+ * Parse Rithmic date/time to timestamp
278
+ * @private
279
+ */
280
+ _parseDateTime(dateStr, timeStr) {
281
+ if (!dateStr) return Date.now();
282
+ try {
283
+ // dateStr format: YYYYMMDD, timeStr format: HH:MM:SS
284
+ const year = dateStr.slice(0, 4);
285
+ const month = dateStr.slice(4, 6);
286
+ const day = dateStr.slice(6, 8);
287
+ const time = timeStr || '00:00:00';
288
+ return new Date(`${year}-${month}-${day}T${time}Z`).getTime();
289
+ } catch (e) {
290
+ return Date.now();
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Get lifetime stats calculated from trade history
296
+ */
297
+ async getLifetimeStats(accountId) {
298
+ const { trades } = await this.getTradeHistory(accountId, 365);
299
+
300
+ if (trades.length === 0) {
301
+ return { success: true, stats: null };
302
+ }
303
+
304
+ // Calculate stats from trades
305
+ let totalTrades = trades.length;
306
+ let winningTrades = 0;
307
+ let losingTrades = 0;
308
+ let totalProfit = 0;
309
+ let totalLoss = 0;
310
+ let longTrades = 0;
311
+ let shortTrades = 0;
312
+ let totalVolume = 0;
313
+
314
+ // Group fills by basketId to calculate P&L per trade
315
+ const tradeGroups = new Map();
316
+ for (const trade of trades) {
317
+ const key = trade.basketId || trade.id;
318
+ if (!tradeGroups.has(key)) {
319
+ tradeGroups.set(key, []);
320
+ }
321
+ tradeGroups.get(key).push(trade);
322
+ }
323
+
324
+ for (const [, fills] of tradeGroups) {
325
+ const firstFill = fills[0];
326
+ totalVolume += fills.reduce((sum, f) => sum + f.size, 0);
327
+
328
+ if (firstFill.side === 1) longTrades++;
329
+ else if (firstFill.side === 2) shortTrades++;
330
+
331
+ // P&L calculation requires entry/exit matching which needs position tracking
332
+ // For now, count trades
333
+ totalTrades = tradeGroups.size;
334
+ }
335
+
336
+ const stats = {
337
+ totalTrades,
338
+ winningTrades,
339
+ losingTrades,
340
+ winRate: totalTrades > 0 ? ((winningTrades / totalTrades) * 100).toFixed(2) : 0,
341
+ totalProfit,
342
+ totalLoss,
343
+ netPnL: totalProfit - totalLoss,
344
+ longTrades,
345
+ shortTrades,
346
+ totalVolume,
347
+ };
348
+
349
+ return { success: true, stats };
350
+ }
351
+
352
+ /**
353
+ * Get daily stats from trade history
354
+ */
355
+ async getDailyStats(accountId, days = 30) {
356
+ const { trades } = await this.getTradeHistory(accountId, days);
357
+
358
+ // Group by date
359
+ const dailyStats = new Map();
360
+
361
+ for (const trade of trades) {
362
+ const date = new Date(trade.timestamp).toISOString().slice(0, 10);
363
+
364
+ if (!dailyStats.has(date)) {
365
+ dailyStats.set(date, {
366
+ date,
367
+ trades: 0,
368
+ volume: 0,
369
+ buys: 0,
370
+ sells: 0,
371
+ });
372
+ }
373
+
374
+ const day = dailyStats.get(date);
375
+ day.trades++;
376
+ day.volume += trade.size;
377
+ if (trade.side === 1) day.buys++;
378
+ else if (trade.side === 2) day.sells++;
379
+ }
380
+
381
+ // Convert to array and sort by date
382
+ const stats = Array.from(dailyStats.values()).sort((a, b) => b.date.localeCompare(a.date));
383
+
384
+ return { success: true, stats };
385
+ }
241
386
 
242
387
  async getMarketStatus() {
243
388
  const status = this.checkMarketHours();
@@ -119,9 +119,57 @@ const getOrders = async (service) => {
119
119
  };
120
120
 
121
121
  /**
122
- * Get order history
122
+ * Get available order history dates
123
+ * @param {RithmicService} service - The Rithmic service instance
124
+ * @returns {Promise<{success: boolean, dates: string[]}>}
125
+ */
126
+ const getOrderHistoryDates = async (service) => {
127
+ if (!service.orderConn || !service.loginInfo) {
128
+ return { success: false, dates: [] };
129
+ }
130
+
131
+ return new Promise((resolve) => {
132
+ const dates = [];
133
+ const timeout = setTimeout(() => {
134
+ resolve({ success: true, dates });
135
+ }, 5000);
136
+
137
+ const handler = (msg) => {
138
+ if (msg.templateId === 319 && msg.date) {
139
+ // ResponseShowOrderHistoryDates returns dates array
140
+ if (Array.isArray(msg.date)) {
141
+ dates.push(...msg.date);
142
+ } else {
143
+ dates.push(msg.date);
144
+ }
145
+ }
146
+ if (msg.templateId === 319 && msg.rpCode && msg.rpCode[0] === '0') {
147
+ clearTimeout(timeout);
148
+ service.orderConn.removeListener('message', handler);
149
+ resolve({ success: true, dates });
150
+ }
151
+ };
152
+
153
+ service.orderConn.on('message', handler);
154
+
155
+ try {
156
+ service.orderConn.send('RequestShowOrderHistoryDates', {
157
+ templateId: REQ.SHOW_ORDER_HISTORY_DATES,
158
+ userMsg: ['HQX'],
159
+ });
160
+ } catch (e) {
161
+ clearTimeout(timeout);
162
+ service.orderConn.removeListener('message', handler);
163
+ resolve({ success: false, error: e.message, dates: [] });
164
+ }
165
+ });
166
+ };
167
+
168
+ /**
169
+ * Get order history for a specific date
123
170
  * @param {RithmicService} service - The Rithmic service instance
124
171
  * @param {string} date - Date in YYYYMMDD format
172
+ * @returns {Promise<{success: boolean, orders: Array}>}
125
173
  */
126
174
  const getOrderHistory = async (service, date) => {
127
175
  if (!service.orderConn || !service.loginInfo) {
@@ -133,12 +181,46 @@ const getOrderHistory = async (service, date) => {
133
181
  return new Promise((resolve) => {
134
182
  const orders = [];
135
183
  const timeout = setTimeout(() => {
184
+ service.orderConn.removeListener('message', handler);
136
185
  resolve({ success: true, orders });
137
- }, 3000);
186
+ }, 10000);
187
+
188
+ const handler = (msg) => {
189
+ // ExchangeOrderNotification (352) contains order/fill details
190
+ if (msg.templateId === 352 && msg.data) {
191
+ try {
192
+ const notification = service.orderConn.proto.decode('ExchangeOrderNotification', msg.data);
193
+ if (notification && notification.symbol) {
194
+ orders.push({
195
+ id: notification.fillId || notification.basketId || `${Date.now()}`,
196
+ accountId: notification.accountId,
197
+ symbol: notification.symbol,
198
+ exchange: notification.exchange || 'CME',
199
+ side: notification.transactionType, // 1=BUY, 2=SELL
200
+ quantity: notification.quantity,
201
+ price: notification.price,
202
+ fillPrice: notification.fillPrice,
203
+ fillSize: notification.fillSize,
204
+ fillTime: notification.fillTime,
205
+ fillDate: notification.fillDate,
206
+ avgFillPrice: notification.avgFillPrice,
207
+ totalFillSize: notification.totalFillSize,
208
+ status: notification.status,
209
+ notifyType: notification.notifyType,
210
+ isSnapshot: notification.isSnapshot,
211
+ });
212
+ }
213
+ } catch (e) {
214
+ // Ignore decode errors
215
+ }
216
+ }
217
+ };
218
+
219
+ service.orderConn.on('message', handler);
138
220
 
139
221
  try {
140
222
  for (const acc of service.accounts) {
141
- service.orderConn.send('RequestShowOrderHistorySummary', {
223
+ service.orderConn.send('RequestShowOrderHistory', {
142
224
  templateId: REQ.SHOW_ORDER_HISTORY,
143
225
  userMsg: ['HQX'],
144
226
  fcmId: acc.fcmId || service.loginInfo.fcmId,
@@ -148,17 +230,53 @@ const getOrderHistory = async (service, date) => {
148
230
  });
149
231
  }
150
232
 
233
+ // Wait for responses
151
234
  setTimeout(() => {
152
235
  clearTimeout(timeout);
236
+ service.orderConn.removeListener('message', handler);
153
237
  resolve({ success: true, orders });
154
- }, 2000);
238
+ }, 5000);
155
239
  } catch (e) {
156
240
  clearTimeout(timeout);
241
+ service.orderConn.removeListener('message', handler);
157
242
  resolve({ success: false, error: e.message, orders: [] });
158
243
  }
159
244
  });
160
245
  };
161
246
 
247
+ /**
248
+ * Get full trade history for multiple dates
249
+ * @param {RithmicService} service - The Rithmic service instance
250
+ * @param {number} days - Number of days to fetch (default 30)
251
+ * @returns {Promise<{success: boolean, trades: Array}>}
252
+ */
253
+ const getTradeHistoryFull = async (service, days = 30) => {
254
+ if (!service.orderConn || !service.loginInfo) {
255
+ return { success: false, trades: [] };
256
+ }
257
+
258
+ // Get available dates
259
+ const { dates } = await getOrderHistoryDates(service);
260
+ if (!dates || dates.length === 0) {
261
+ return { success: true, trades: [] };
262
+ }
263
+
264
+ // Sort dates descending and limit to requested days
265
+ const sortedDates = dates.sort((a, b) => b.localeCompare(a)).slice(0, days);
266
+
267
+ const allTrades = [];
268
+
269
+ // Fetch history for each date
270
+ for (const date of sortedDates) {
271
+ const { orders } = await getOrderHistory(service, date);
272
+ // Filter only fills (notifyType 5)
273
+ const fills = orders.filter(o => o.notifyType === 5 || o.fillPrice);
274
+ allTrades.push(...fills);
275
+ }
276
+
277
+ return { success: true, trades: allTrades };
278
+ };
279
+
162
280
  /**
163
281
  * Close position (market order to flatten)
164
282
  * @param {RithmicService} service - The Rithmic service instance
@@ -188,5 +306,7 @@ module.exports = {
188
306
  cancelOrder,
189
307
  getOrders,
190
308
  getOrderHistory,
309
+ getOrderHistoryDates,
310
+ getTradeHistoryFull,
191
311
  closePosition
192
312
  };