hedgequantx 1.1.1

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Configuration Exports
3
+ */
4
+
5
+ const { PROPFIRMS, PROPFIRM_CHOICES } = require('./propfirms');
6
+ const {
7
+ ACCOUNT_STATUS,
8
+ ACCOUNT_TYPE,
9
+ ORDER_STATUS,
10
+ ORDER_TYPE,
11
+ ORDER_SIDE,
12
+ FUTURES_SYMBOLS
13
+ } = require('./constants');
14
+
15
+ module.exports = {
16
+ PROPFIRMS,
17
+ PROPFIRM_CHOICES,
18
+ ACCOUNT_STATUS,
19
+ ACCOUNT_TYPE,
20
+ ORDER_STATUS,
21
+ ORDER_TYPE,
22
+ ORDER_SIDE,
23
+ FUTURES_SYMBOLS
24
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * PropFirm API Configurations
3
+ * UserAPI: Authentication, accounts, user info
4
+ * GatewayAPI: Trading (orders, positions, trades)
5
+ */
6
+
7
+ const PROPFIRMS = {
8
+ topstep: {
9
+ name: 'Topstep',
10
+ userApi: 'userapi.topstepx.com',
11
+ gatewayApi: 'api.topstepx.com'
12
+ },
13
+ alpha_futures: {
14
+ name: 'Alpha Futures',
15
+ userApi: 'userapi.alphafutures.projectx.com',
16
+ gatewayApi: 'api.alphafutures.projectx.com'
17
+ },
18
+ tickticktrader: {
19
+ name: 'TickTickTrader',
20
+ userApi: 'userapi.tickticktrader.projectx.com',
21
+ gatewayApi: 'api.tickticktrader.projectx.com'
22
+ },
23
+ bulenox: {
24
+ name: 'Bulenox',
25
+ userApi: 'userapi.bulenox.projectx.com',
26
+ gatewayApi: 'api.bulenox.projectx.com'
27
+ },
28
+ tradeday: {
29
+ name: 'TradeDay',
30
+ userApi: 'userapi.tradeday.projectx.com',
31
+ gatewayApi: 'api.tradeday.projectx.com'
32
+ },
33
+ blusky: {
34
+ name: 'Blusky',
35
+ userApi: 'userapi.blusky.projectx.com',
36
+ gatewayApi: 'api.blusky.projectx.com'
37
+ },
38
+ goat_futures: {
39
+ name: 'Goat Futures',
40
+ userApi: 'userapi.goatfunded.projectx.com',
41
+ gatewayApi: 'api.goatfunded.projectx.com'
42
+ },
43
+ futures_desk: {
44
+ name: 'The Futures Desk',
45
+ userApi: 'userapi.thefuturesdesk.projectx.com',
46
+ gatewayApi: 'api.thefuturesdesk.projectx.com'
47
+ }
48
+ };
49
+
50
+ // PropFirm display list for menus
51
+ const PROPFIRM_CHOICES = Object.entries(PROPFIRMS).map(([key, val]) => ({
52
+ name: val.name,
53
+ value: key
54
+ }));
55
+
56
+ module.exports = { PROPFIRMS, PROPFIRM_CHOICES };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Pages Module Exports
3
+ */
4
+
5
+ const { showStats } = require('./stats');
6
+
7
+ module.exports = {
8
+ showStats
9
+ };
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Stats Page
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+ const inquirer = require('inquirer');
8
+ const asciichart = require('asciichart');
9
+
10
+ const { connections } = require('../services');
11
+ const { ACCOUNT_STATUS, ACCOUNT_TYPE } = require('../config');
12
+ const {
13
+ getLogoWidth,
14
+ visibleLength,
15
+ drawBoxHeader,
16
+ drawBoxFooter,
17
+ getColWidths,
18
+ draw2ColHeader,
19
+ draw2ColSeparator,
20
+ fmtRow
21
+ } = require('../ui');
22
+
23
+ /**
24
+ * Show Stats Page
25
+ */
26
+ const showStats = async (service) => {
27
+ const spinner = ora('Fetching stats for all accounts...').start();
28
+
29
+ let allAccountsData = [];
30
+
31
+ // Get accounts from all connections
32
+ if (connections.count() > 0) {
33
+ for (const conn of connections.getAll()) {
34
+ try {
35
+ const result = await conn.service.getTradingAccounts();
36
+ if (result.success && result.accounts) {
37
+ result.accounts.forEach(account => {
38
+ allAccountsData.push({
39
+ ...account,
40
+ propfirm: conn.propfirm || conn.type,
41
+ service: conn.service
42
+ });
43
+ });
44
+ }
45
+ } catch (e) { /* ignore */ }
46
+ }
47
+ } else if (service) {
48
+ const result = await service.getTradingAccounts();
49
+ if (result.success && result.accounts) {
50
+ allAccountsData = result.accounts.map(a => ({ ...a, service }));
51
+ }
52
+ }
53
+
54
+ if (allAccountsData.length === 0) {
55
+ spinner.fail('No accounts found');
56
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
57
+ return;
58
+ }
59
+
60
+ // Collect stats
61
+ let totalBalance = 0;
62
+ let totalStartingBalance = 0;
63
+ let totalPnL = 0;
64
+ let allTrades = [];
65
+ let totalOpenPositions = 0;
66
+ let totalOpenOrders = 0;
67
+ let allDailyStats = [];
68
+
69
+ spinner.text = 'Fetching detailed stats...';
70
+
71
+ for (const account of allAccountsData) {
72
+ const svc = account.service;
73
+ const currentBalance = account.balance || 0;
74
+ totalBalance += currentBalance;
75
+
76
+ // Estimate starting balance
77
+ let startingBalance = account.startingBalance || account.initialBalance || 0;
78
+ if (!startingBalance) {
79
+ const name = (account.accountName || '').toUpperCase();
80
+ if (name.includes('150K') || name.includes('150')) startingBalance = 150000;
81
+ else if (name.includes('100K') || name.includes('100')) startingBalance = 100000;
82
+ else if (name.includes('50K') || name.includes('50')) startingBalance = 50000;
83
+ else if (currentBalance >= 140000) startingBalance = 150000;
84
+ else if (currentBalance >= 90000) startingBalance = 100000;
85
+ else if (currentBalance >= 45000) startingBalance = 50000;
86
+ else startingBalance = currentBalance;
87
+ }
88
+
89
+ totalStartingBalance += startingBalance;
90
+ account.startingBalance = startingBalance;
91
+
92
+ if (account.profitAndLoss !== undefined) {
93
+ totalPnL += account.profitAndLoss;
94
+ }
95
+
96
+ // Positions & Orders
97
+ const posResult = await svc.getPositions(account.accountId);
98
+ if (posResult.success) totalOpenPositions += posResult.positions.length;
99
+
100
+ const ordResult = await svc.getOrders(account.accountId);
101
+ if (ordResult.success) totalOpenOrders += ordResult.orders.filter(o => o.status === 1).length;
102
+
103
+ // Lifetime stats
104
+ const lifetimeResult = await svc.getLifetimeStats(account.accountId);
105
+ if (lifetimeResult.success && lifetimeResult.stats) {
106
+ account.lifetimeStats = lifetimeResult.stats;
107
+ }
108
+
109
+ // Daily stats
110
+ const dailyResult = await svc.getDailyStats(account.accountId);
111
+ if (dailyResult.success && dailyResult.stats) {
112
+ account.dailyStats = dailyResult.stats;
113
+ allDailyStats = allDailyStats.concat(dailyResult.stats);
114
+ }
115
+
116
+ // Trade history
117
+ const tradesResult = await svc.getTradeHistory(account.accountId, 30);
118
+ if (tradesResult.success && tradesResult.trades.length > 0) {
119
+ allTrades = allTrades.concat(tradesResult.trades.map(t => ({
120
+ ...t,
121
+ accountName: account.accountName,
122
+ propfirm: account.propfirm
123
+ })));
124
+ }
125
+ }
126
+
127
+ if (totalPnL === 0 && totalStartingBalance > 0) {
128
+ totalPnL = totalBalance - totalStartingBalance;
129
+ }
130
+
131
+ // Aggregate stats
132
+ let stats = {
133
+ totalTrades: 0, winningTrades: 0, losingTrades: 0,
134
+ totalWinAmount: 0, totalLossAmount: 0,
135
+ bestTrade: 0, worstTrade: 0, totalVolume: 0,
136
+ maxConsecutiveWins: 0, maxConsecutiveLosses: 0,
137
+ longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
138
+ };
139
+
140
+ for (const account of allAccountsData) {
141
+ if (account.lifetimeStats) {
142
+ const s = account.lifetimeStats;
143
+ stats.totalTrades += s.totalTrades || 0;
144
+ stats.winningTrades += s.winningTrades || 0;
145
+ stats.losingTrades += s.losingTrades || 0;
146
+ stats.totalWinAmount += s.totalWinAmount || 0;
147
+ stats.totalLossAmount += s.totalLossAmount || 0;
148
+ stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
149
+ stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
150
+ stats.totalVolume += s.totalVolume || 0;
151
+ stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
152
+ stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
153
+ stats.longTrades += s.longTrades || 0;
154
+ stats.shortTrades += s.shortTrades || 0;
155
+ }
156
+ }
157
+
158
+ // If no stats from API, calculate from trades
159
+ if (stats.totalTrades === 0 && allTrades.length > 0) {
160
+ stats.totalTrades = allTrades.length;
161
+ let consecutiveWins = 0, consecutiveLosses = 0;
162
+
163
+ for (const trade of allTrades) {
164
+ const pnl = trade.profitAndLoss || trade.pnl || 0;
165
+ const size = trade.size || trade.quantity || 1;
166
+ const side = trade.side;
167
+
168
+ stats.totalVolume += Math.abs(size);
169
+
170
+ if (side === 0) {
171
+ stats.longTrades++;
172
+ if (pnl > 0) stats.longWins++;
173
+ } else if (side === 1) {
174
+ stats.shortTrades++;
175
+ if (pnl > 0) stats.shortWins++;
176
+ }
177
+
178
+ if (pnl > 0) {
179
+ stats.winningTrades++;
180
+ stats.totalWinAmount += pnl;
181
+ consecutiveWins++;
182
+ consecutiveLosses = 0;
183
+ if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
184
+ if (pnl > stats.bestTrade) stats.bestTrade = pnl;
185
+ } else if (pnl < 0) {
186
+ stats.losingTrades++;
187
+ stats.totalLossAmount += Math.abs(pnl);
188
+ consecutiveLosses++;
189
+ consecutiveWins = 0;
190
+ if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
191
+ if (pnl < stats.worstTrade) stats.worstTrade = pnl;
192
+ }
193
+ }
194
+ }
195
+
196
+ spinner.succeed('Stats loaded');
197
+ console.log();
198
+
199
+ // Display
200
+ const boxWidth = getLogoWidth();
201
+ const { col1, col2 } = getColWidths(boxWidth);
202
+
203
+ // Calculated metrics
204
+ const winRate = stats.totalTrades > 0 ? ((stats.winningTrades / stats.totalTrades) * 100).toFixed(1) : '0.0';
205
+ const avgWin = stats.winningTrades > 0 ? (stats.totalWinAmount / stats.winningTrades).toFixed(2) : '0.00';
206
+ const avgLoss = stats.losingTrades > 0 ? (stats.totalLossAmount / stats.losingTrades).toFixed(2) : '0.00';
207
+ const profitFactor = stats.totalLossAmount > 0 ? (stats.totalWinAmount / stats.totalLossAmount).toFixed(2) : '0.00';
208
+ const netPnL = stats.totalWinAmount - stats.totalLossAmount;
209
+ const returnPercent = totalStartingBalance > 0 ? ((totalPnL / totalStartingBalance) * 100).toFixed(2) : '0.00';
210
+ const longWinRate = stats.longTrades > 0 ? ((stats.longWins / stats.longTrades) * 100).toFixed(1) : '0.0';
211
+ const shortWinRate = stats.shortTrades > 0 ? ((stats.shortWins / stats.shortTrades) * 100).toFixed(1) : '0.0';
212
+
213
+ const totalBalanceColor = totalBalance >= 0 ? chalk.green : chalk.red;
214
+ const pnlColor = totalPnL >= 0 ? chalk.green : chalk.red;
215
+
216
+ // Main Summary
217
+ drawBoxHeader('HQX STATS', boxWidth);
218
+ draw2ColHeader('ACCOUNT OVERVIEW', 'TRADING PERFORMANCE', boxWidth);
219
+
220
+ console.log(chalk.cyan('\u2551') + fmtRow('Connections:', chalk.cyan(connections.count().toString() || '1'), col1) + chalk.cyan('\u2502') + fmtRow('Total Trades:', chalk.white(stats.totalTrades.toString()), col2) + chalk.cyan('\u2551'));
221
+ console.log(chalk.cyan('\u2551') + fmtRow('Total Accounts:', chalk.cyan(allAccountsData.length.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Winning Trades:', chalk.green(stats.winningTrades.toString()), col2) + chalk.cyan('\u2551'));
222
+ console.log(chalk.cyan('\u2551') + fmtRow('Total Balance:', totalBalanceColor('$' + totalBalance.toLocaleString()), col1) + chalk.cyan('\u2502') + fmtRow('Losing Trades:', chalk.red(stats.losingTrades.toString()), col2) + chalk.cyan('\u2551'));
223
+ console.log(chalk.cyan('\u2551') + fmtRow('Starting Balance:', chalk.white('$' + totalStartingBalance.toLocaleString()), col1) + chalk.cyan('\u2502') + fmtRow('Win Rate:', parseFloat(winRate) >= 50 ? chalk.green(winRate + '%') : chalk.yellow(winRate + '%'), col2) + chalk.cyan('\u2551'));
224
+ console.log(chalk.cyan('\u2551') + fmtRow('Total P&L:', pnlColor('$' + totalPnL.toLocaleString() + ' (' + returnPercent + '%)'), col1) + chalk.cyan('\u2502') + fmtRow('Long Trades:', chalk.white(stats.longTrades + ' (' + longWinRate + '%)'), col2) + chalk.cyan('\u2551'));
225
+ console.log(chalk.cyan('\u2551') + fmtRow('Open Positions:', chalk.white(totalOpenPositions.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Short Trades:', chalk.white(stats.shortTrades + ' (' + shortWinRate + '%)'), col2) + chalk.cyan('\u2551'));
226
+ console.log(chalk.cyan('\u2551') + fmtRow('Open Orders:', chalk.white(totalOpenOrders.toString()), col1) + chalk.cyan('\u2502') + fmtRow('Volume:', chalk.white(stats.totalVolume + ' contracts'), col2) + chalk.cyan('\u2551'));
227
+
228
+ // P&L Metrics
229
+ draw2ColSeparator(boxWidth);
230
+ draw2ColHeader('P&L METRICS', 'RISK METRICS', boxWidth);
231
+
232
+ const pfColor = parseFloat(profitFactor) >= 1.5 ? chalk.green(profitFactor) : parseFloat(profitFactor) >= 1 ? chalk.yellow(profitFactor) : chalk.red(profitFactor);
233
+
234
+ console.log(chalk.cyan('\u2551') + fmtRow('Net P&L:', netPnL >= 0 ? chalk.green('$' + netPnL.toFixed(2)) : chalk.red('$' + netPnL.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Profit Factor:', pfColor, col2) + chalk.cyan('\u2551'));
235
+ console.log(chalk.cyan('\u2551') + fmtRow('Gross Profit:', chalk.green('$' + stats.totalWinAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Wins:', chalk.green(stats.maxConsecutiveWins.toString()), col2) + chalk.cyan('\u2551'));
236
+ console.log(chalk.cyan('\u2551') + fmtRow('Gross Loss:', chalk.red('-$' + stats.totalLossAmount.toFixed(2)), col1) + chalk.cyan('\u2502') + fmtRow('Max Consec. Loss:', chalk.red(stats.maxConsecutiveLosses.toString()), col2) + chalk.cyan('\u2551'));
237
+ console.log(chalk.cyan('\u2551') + fmtRow('Avg Win:', chalk.green('$' + avgWin), col1) + chalk.cyan('\u2502') + fmtRow('Best Trade:', chalk.green('$' + stats.bestTrade.toFixed(2)), col2) + chalk.cyan('\u2551'));
238
+ console.log(chalk.cyan('\u2551') + fmtRow('Avg Loss:', chalk.red('-$' + avgLoss), col1) + chalk.cyan('\u2502') + fmtRow('Worst Trade:', chalk.red('$' + stats.worstTrade.toFixed(2)), col2) + chalk.cyan('\u2551'));
239
+
240
+ drawBoxFooter(boxWidth);
241
+
242
+ // Equity Curve
243
+ console.log();
244
+ drawBoxHeader('EQUITY CURVE', boxWidth);
245
+
246
+ const chartInnerWidth = boxWidth - 2;
247
+
248
+ if (allTrades.length > 0) {
249
+ const yAxisWidth = 10;
250
+ const chartAreaWidth = chartInnerWidth - yAxisWidth - 4;
251
+
252
+ let equityData = [totalStartingBalance];
253
+ let equity = totalStartingBalance;
254
+ allTrades.forEach(trade => {
255
+ equity += (trade.profitAndLoss || trade.pnl || 0);
256
+ equityData.push(equity);
257
+ });
258
+
259
+ const maxDataPoints = chartAreaWidth - 5;
260
+ if (equityData.length > maxDataPoints) {
261
+ const step = Math.ceil(equityData.length / maxDataPoints);
262
+ equityData = equityData.filter((_, i) => i % step === 0);
263
+ }
264
+
265
+ const chartConfig = {
266
+ height: 10,
267
+ colors: [equityData[equityData.length - 1] < equityData[0] ? asciichart.red : asciichart.green],
268
+ format: (x) => ('$' + (x / 1000).toFixed(0) + 'K').padStart(yAxisWidth)
269
+ };
270
+
271
+ const chart = asciichart.plot(equityData, chartConfig);
272
+ chart.split('\n').forEach(line => {
273
+ let chartLine = ' ' + line;
274
+ const len = chartLine.replace(/\x1b\[[0-9;]*m/g, '').length;
275
+ if (len < chartInnerWidth) chartLine += ' '.repeat(chartInnerWidth - len);
276
+ console.log(chalk.cyan('\u2551') + chartLine + chalk.cyan('\u2551'));
277
+ });
278
+ } else {
279
+ const msg = ' No trade data available';
280
+ console.log(chalk.cyan('\u2551') + chalk.gray(msg) + ' '.repeat(chartInnerWidth - msg.length) + chalk.cyan('\u2551'));
281
+ }
282
+
283
+ drawBoxFooter(boxWidth);
284
+ console.log();
285
+
286
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
287
+ };
288
+
289
+ module.exports = { showStats };