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
package/src/menus/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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} ✔
|
|
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('
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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 = {
|
package/src/pages/stats/index.js
CHANGED
|
@@ -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
|
|
39
|
+
handleExchangeNotification(service, data);
|
|
40
40
|
break;
|
|
41
41
|
case STREAM.ORDER_NOTIFICATION:
|
|
42
|
-
service
|
|
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
|
-
// ====================
|
|
238
|
+
// ==================== STATS & HISTORY ====================
|
|
236
239
|
|
|
237
240
|
async getUser() { return this.user; }
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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();
|