hedgequantx 2.7.1 → 2.7.3

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.1",
3
+ "version": "2.7.3",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -51,18 +51,14 @@ const dashboardMenu = async (service) => {
51
51
  const balStr = statsInfo.balance !== null ? `$${statsInfo.balance.toLocaleString()}` : '--';
52
52
  const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
53
53
 
54
- let pnlDisplay, pnlColor;
55
- if (statsInfo.pnl !== null) {
56
- pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
57
- pnlDisplay = `${statsInfo.pnl >= 0 ? '+' : ''}$${Math.abs(statsInfo.pnl).toLocaleString()}`;
58
- } else {
59
- pnlColor = chalk.gray;
60
- pnlDisplay = '--';
61
- }
54
+ // AI Agents status
55
+ const agentCount = statsInfo.agents || 0;
56
+ const agentDisplay = agentCount > 0 ? `${agentCount} connected` : 'disconnected';
57
+ const agentColor = agentCount > 0 ? chalk.green : chalk.red;
62
58
 
63
59
  // Yellow icons: ✔ for each stat
64
60
  const icon = chalk.yellow('✔ ');
65
- const statsPlain = `✔ Connections: ${statsInfo.connections} ✔ Accounts: ${statsInfo.accounts} ✔ Balance: ${balStr} ✔ P&L: ${pnlDisplay}`;
61
+ const statsPlain = `✔ Connections: ${statsInfo.connections} ✔ Accounts: ${statsInfo.accounts} ✔ Balance: ${balStr} ✔ AI Agents: ${agentDisplay}`;
66
62
  const statsLeftPad = Math.floor((W - statsPlain.length) / 2);
67
63
  const statsRightPad = W - statsPlain.length - statsLeftPad;
68
64
 
@@ -70,19 +66,30 @@ const dashboardMenu = async (service) => {
70
66
  icon + chalk.white(`Connections: ${statsInfo.connections}`) + ' ' +
71
67
  icon + chalk.white(`Accounts: ${statsInfo.accounts}`) + ' ' +
72
68
  icon + chalk.white('Balance: ') + balColor(balStr) + ' ' +
73
- icon + chalk.white('P&L: ') + pnlColor(pnlDisplay) +
69
+ icon + chalk.white('AI Agents: ') + agentColor(agentDisplay) +
74
70
  ' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║'));
75
71
  }
76
72
 
77
73
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
78
74
 
79
- // Menu in 2 columns
75
+ // Menu in 2 columns - centered
80
76
  const col1Width = Math.floor(W / 2);
77
+ const col2Width = W - col1Width;
78
+
81
79
  const menuRow = (left, right) => {
82
80
  const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
83
81
  const rightPlain = right.replace(/\x1b\[[0-9;]*m/g, '');
84
- const leftPadded = ' ' + left + ' '.repeat(Math.max(0, col1Width - leftPlain.length - 2));
85
- const rightPadded = right + ' '.repeat(Math.max(0, W - col1Width - rightPlain.length));
82
+
83
+ // Center left item in col1
84
+ const leftPadL = Math.floor((col1Width - leftPlain.length) / 2);
85
+ const leftPadR = col1Width - leftPlain.length - leftPadL;
86
+ const leftPadded = ' '.repeat(leftPadL) + left + ' '.repeat(leftPadR);
87
+
88
+ // Center right item in col2
89
+ const rightPadL = Math.floor((col2Width - rightPlain.length) / 2);
90
+ const rightPadR = col2Width - rightPlain.length - rightPadL;
91
+ const rightPadded = ' '.repeat(rightPadL) + right + ' '.repeat(rightPadR);
92
+
86
93
  console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
87
94
  };
88
95
 
@@ -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
@@ -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 ====================
@@ -232,12 +235,121 @@ class RithmicService extends EventEmitter {
232
235
  async getContracts() { return getContracts(this); }
233
236
  async searchContracts(searchText) { return searchContracts(this, searchText); }
234
237
 
235
- // ==================== STUBS ====================
238
+ // ==================== STATS & HISTORY ====================
236
239
 
237
240
  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: [] }; }
241
+
242
+ /**
243
+ * Get trade history from captured fills
244
+ * @param {string} accountId - Optional account filter
245
+ * @param {number} days - Number of days to look back (default 30)
246
+ */
247
+ async getTradeHistory(accountId, days = 30) {
248
+ const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
249
+ let trades = this.trades.filter(t => t.timestamp >= cutoff);
250
+
251
+ if (accountId) {
252
+ trades = trades.filter(t => t.accountId === accountId);
253
+ }
254
+
255
+ // Sort by timestamp descending (newest first)
256
+ trades.sort((a, b) => b.timestamp - a.timestamp);
257
+
258
+ return { success: true, trades };
259
+ }
260
+
261
+ /**
262
+ * Get lifetime stats calculated from trade history
263
+ */
264
+ async getLifetimeStats(accountId) {
265
+ const { trades } = await this.getTradeHistory(accountId, 365);
266
+
267
+ if (trades.length === 0) {
268
+ return { success: true, stats: null };
269
+ }
270
+
271
+ // Calculate stats from trades
272
+ let totalTrades = trades.length;
273
+ let winningTrades = 0;
274
+ let losingTrades = 0;
275
+ let totalProfit = 0;
276
+ let totalLoss = 0;
277
+ let longTrades = 0;
278
+ let shortTrades = 0;
279
+ let totalVolume = 0;
280
+
281
+ // Group fills by basketId to calculate P&L per trade
282
+ const tradeGroups = new Map();
283
+ for (const trade of trades) {
284
+ const key = trade.basketId || trade.id;
285
+ if (!tradeGroups.has(key)) {
286
+ tradeGroups.set(key, []);
287
+ }
288
+ tradeGroups.get(key).push(trade);
289
+ }
290
+
291
+ for (const [, fills] of tradeGroups) {
292
+ const firstFill = fills[0];
293
+ totalVolume += fills.reduce((sum, f) => sum + f.size, 0);
294
+
295
+ if (firstFill.side === 1) longTrades++;
296
+ else if (firstFill.side === 2) shortTrades++;
297
+
298
+ // P&L calculation requires entry/exit matching which needs position tracking
299
+ // For now, count trades
300
+ totalTrades = tradeGroups.size;
301
+ }
302
+
303
+ const stats = {
304
+ totalTrades,
305
+ winningTrades,
306
+ losingTrades,
307
+ winRate: totalTrades > 0 ? ((winningTrades / totalTrades) * 100).toFixed(2) : 0,
308
+ totalProfit,
309
+ totalLoss,
310
+ netPnL: totalProfit - totalLoss,
311
+ longTrades,
312
+ shortTrades,
313
+ totalVolume,
314
+ };
315
+
316
+ return { success: true, stats };
317
+ }
318
+
319
+ /**
320
+ * Get daily stats from trade history
321
+ */
322
+ async getDailyStats(accountId, days = 30) {
323
+ const { trades } = await this.getTradeHistory(accountId, days);
324
+
325
+ // Group by date
326
+ const dailyStats = new Map();
327
+
328
+ for (const trade of trades) {
329
+ const date = new Date(trade.timestamp).toISOString().slice(0, 10);
330
+
331
+ if (!dailyStats.has(date)) {
332
+ dailyStats.set(date, {
333
+ date,
334
+ trades: 0,
335
+ volume: 0,
336
+ buys: 0,
337
+ sells: 0,
338
+ });
339
+ }
340
+
341
+ const day = dailyStats.get(date);
342
+ day.trades++;
343
+ day.volume += trade.size;
344
+ if (trade.side === 1) day.buys++;
345
+ else if (trade.side === 2) day.sells++;
346
+ }
347
+
348
+ // Convert to array and sort by date
349
+ const stats = Array.from(dailyStats.values()).sort((a, b) => b.date.localeCompare(a.date));
350
+
351
+ return { success: true, stats };
352
+ }
241
353
 
242
354
  async getMarketStatus() {
243
355
  const status = this.checkMarketHours();