hedgequantx 1.3.2 → 1.3.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": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -70,30 +70,40 @@ const banner = async () => {
70
70
  const version = require('../package.json').version;
71
71
 
72
72
  // Get stats if connected (only active accounts: status === 0)
73
+ // STRICT: Only display values from API, no estimation
73
74
  let statsInfo = null;
74
75
  if (connections.count() > 0) {
75
76
  try {
76
77
  const allAccounts = await connections.getAllAccounts();
77
78
  const activeAccounts = allAccounts.filter(acc => acc.status === 0);
78
- let totalBalance = 0;
79
- let totalStartingBalance = 0;
80
- let totalPnl = 0;
79
+
80
+ // Sum only non-null values from API
81
+ let totalBalance = null;
82
+ let totalPnl = null;
83
+ let hasBalanceData = false;
84
+ let hasPnlData = false;
81
85
 
82
86
  activeAccounts.forEach(account => {
83
- totalBalance += account.balance || 0;
84
- totalStartingBalance += account.startingBalance || 0;
85
- totalPnl += account.profitAndLoss || 0;
87
+ // Balance: only sum if API returned a value
88
+ if (account.balance !== null && account.balance !== undefined) {
89
+ totalBalance = (totalBalance || 0) + account.balance;
90
+ hasBalanceData = true;
91
+ }
92
+
93
+ // P&L: only sum if API returned a value
94
+ if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
95
+ totalPnl = (totalPnl || 0) + account.profitAndLoss;
96
+ hasPnlData = true;
97
+ }
86
98
  });
87
99
 
88
- const pnl = totalPnl !== 0 ? totalPnl : (totalBalance - totalStartingBalance);
89
- const pnlPercent = totalStartingBalance > 0 ? ((pnl / totalStartingBalance) * 100).toFixed(1) : '0.0';
90
-
91
100
  statsInfo = {
92
101
  connections: connections.count(),
93
102
  accounts: activeAccounts.length,
94
- balance: totalBalance,
95
- pnl: pnl,
96
- pnlPercent: pnlPercent
103
+ balance: hasBalanceData ? totalBalance : null,
104
+ pnl: hasPnlData ? totalPnl : null,
105
+ // No percentage calculation if no verified data
106
+ pnlPercent: null
97
107
  };
98
108
  } catch (e) {
99
109
  // Ignore errors
@@ -168,19 +178,33 @@ const banner = async () => {
168
178
  console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
169
179
 
170
180
  // Stats bar if connected
181
+ // STRICT: Only display verified values from API, show '--' for unavailable data
171
182
  if (statsInfo) {
172
183
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
173
184
 
174
- const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
175
- const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
176
-
177
185
  const connStr = `Connections: ${statsInfo.connections}`;
178
186
  const accStr = `Accounts: ${statsInfo.accounts}`;
179
- const balStr = `Balance: $${statsInfo.balance.toLocaleString()}`;
180
- const pnlStr = `P&L: $${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnlPercent}%)`;
187
+
188
+ // Balance: show '--' if not available from API
189
+ const balStr = statsInfo.balance !== null
190
+ ? `Balance: $${statsInfo.balance.toLocaleString()}`
191
+ : `Balance: --`;
192
+ const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
193
+
194
+ // P&L: show '--' if not available from API
195
+ let pnlDisplay;
196
+ let pnlColor;
197
+ if (statsInfo.pnl !== null) {
198
+ const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
199
+ pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
200
+ pnlDisplay = `$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnl.toFixed(1)})`;
201
+ } else {
202
+ pnlColor = chalk.gray;
203
+ pnlDisplay = '--';
204
+ }
181
205
 
182
206
  // Build full stats text and calculate padding
183
- const statsText = `${connStr} ${accStr} ${balStr} ${pnlStr}`;
207
+ const statsText = `${connStr} ${accStr} ${balStr} P&L: ${pnlDisplay}`;
184
208
  const statsLen = statsText.length;
185
209
  const statsLeftPad = Math.floor((innerWidth - statsLen) / 2);
186
210
  const statsRightPad = innerWidth - statsLen - statsLeftPad;
@@ -188,9 +212,9 @@ const banner = async () => {
188
212
  console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
189
213
  chalk.white(connStr) + ' ' +
190
214
  chalk.white(accStr) + ' ' +
191
- chalk.white('Balance: ') + chalk.green(`$${statsInfo.balance.toLocaleString()}`) + ' ' +
192
- chalk.white('P&L: ') + pnlColor(`$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnlPercent}%)`) +
193
- ' '.repeat(statsRightPad) + chalk.cyan('║')
215
+ chalk.white('Balance: ') + balColor(statsInfo.balance !== null ? `$${statsInfo.balance.toLocaleString()}` : '--') + ' ' +
216
+ chalk.white('P&L: ') + pnlColor(pnlDisplay) +
217
+ ' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║')
194
218
  );
195
219
  }
196
220
 
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * ProjectX API Service
3
3
  * Main service for ProjectX prop firm connections
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const https = require('https');
7
9
  const { PROPFIRMS } = require('../../config');
10
+
11
+ // Debug mode - set HQX_DEBUG=1 to enable
12
+ const DEBUG = process.env.HQX_DEBUG === '1';
13
+ const debug = (...args) => DEBUG && console.log('[ProjectX]', ...args);
8
14
  const {
9
15
  validateUsername,
10
16
  validatePassword,
@@ -145,18 +151,145 @@ class ProjectXService {
145
151
 
146
152
  // ==================== ACCOUNTS ====================
147
153
 
154
+ /**
155
+ * Get trading accounts - ONLY returns values from API
156
+ * For banner display: returns basic account info quickly
157
+ * Use getAccountPnL() for detailed P&L data
158
+ */
148
159
  async getTradingAccounts() {
149
160
  try {
150
161
  const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
151
- if (response.statusCode === 200) {
152
- return { success: true, accounts: Array.isArray(response.data) ? response.data : [] };
162
+ debug('getTradingAccounts response:', JSON.stringify(response.data, null, 2));
163
+
164
+ if (response.statusCode !== 200) {
165
+ return { success: false, accounts: [], error: 'Failed to get accounts' };
153
166
  }
154
- return { success: false, accounts: [], error: 'Failed to get accounts' };
167
+
168
+ const accounts = Array.isArray(response.data) ? response.data : [];
169
+
170
+ // Return RAW API data only - no additional calls for speed
171
+ const enrichedAccounts = accounts.map(account => ({
172
+ accountId: account.accountId,
173
+ accountName: account.accountName,
174
+ balance: account.balance, // From API
175
+ status: account.status, // From API
176
+ type: account.type, // From API
177
+ platform: 'ProjectX',
178
+ propfirm: this.propfirm.name,
179
+ // P&L not available from /TradingAccount endpoint
180
+ todayPnL: null,
181
+ openPnL: null,
182
+ profitAndLoss: null,
183
+ startingBalance: null,
184
+ }));
185
+
186
+ return { success: true, accounts: enrichedAccounts };
155
187
  } catch (error) {
156
188
  return { success: false, accounts: [], error: error.message };
157
189
  }
158
190
  }
159
191
 
192
+ /**
193
+ * Get detailed P&L for a specific account
194
+ * Call this separately when P&L details are needed (e.g., stats page)
195
+ */
196
+ async getAccountPnL(accountId) {
197
+ const todayPnL = await this._getTodayRealizedPnL(accountId);
198
+ const openPnL = await this._getOpenPositionsPnL(accountId);
199
+
200
+ let totalPnL = null;
201
+ if (todayPnL !== null || openPnL !== null) {
202
+ totalPnL = (todayPnL || 0) + (openPnL || 0);
203
+ }
204
+
205
+ debug(`Account ${accountId} P&L:`, { todayPnL, openPnL, totalPnL });
206
+
207
+ return {
208
+ todayPnL,
209
+ openPnL,
210
+ profitAndLoss: totalPnL
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Get today's realized P&L from Trade API
216
+ * Returns null if API fails (not 0)
217
+ * @private
218
+ */
219
+ async _getTodayRealizedPnL(accountId) {
220
+ try {
221
+ const now = new Date();
222
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
223
+
224
+ const response = await this._request(
225
+ this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
226
+ {
227
+ accountId: accountId,
228
+ startTimestamp: startOfDay.toISOString(),
229
+ endTimestamp: now.toISOString()
230
+ }
231
+ );
232
+
233
+ if (response.statusCode === 200 && response.data) {
234
+ const trades = Array.isArray(response.data)
235
+ ? response.data
236
+ : (response.data.trades || []);
237
+
238
+ debug(`_getTodayRealizedPnL: ${trades.length} trades found`);
239
+
240
+ // Sum P&L from API response only
241
+ let totalPnL = 0;
242
+ for (const trade of trades) {
243
+ if (trade.profitAndLoss !== undefined && trade.profitAndLoss !== null) {
244
+ totalPnL += trade.profitAndLoss;
245
+ debug(` Trade P&L: ${trade.profitAndLoss}`);
246
+ }
247
+ }
248
+ debug(` Total realized P&L: ${totalPnL}`);
249
+ return totalPnL;
250
+ }
251
+ debug('_getTodayRealizedPnL: API failed or no data');
252
+ return null; // API failed - return null, not 0
253
+ } catch (e) {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get unrealized P&L from open positions API
260
+ * Returns null if API fails (not 0)
261
+ * @private
262
+ */
263
+ async _getOpenPositionsPnL(accountId) {
264
+ try {
265
+ const response = await this._request(
266
+ this.propfirm.gatewayApi, '/api/Position/searchOpen', 'POST',
267
+ { accountId: accountId }
268
+ );
269
+
270
+ if (response.statusCode === 200 && response.data) {
271
+ const positions = response.data.positions || response.data || [];
272
+ debug(`_getOpenPositionsPnL: ${positions.length} positions found`);
273
+
274
+ if (Array.isArray(positions)) {
275
+ let totalPnL = 0;
276
+ for (const pos of positions) {
277
+ if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
278
+ totalPnL += pos.profitAndLoss;
279
+ debug(` Position ${pos.symbolId || 'unknown'} P&L: ${pos.profitAndLoss}`);
280
+ }
281
+ }
282
+ debug(` Total open P&L: ${totalPnL}`);
283
+ return totalPnL;
284
+ }
285
+ }
286
+ debug('_getOpenPositionsPnL: API failed or no data');
287
+ return null;
288
+ } catch (e) {
289
+ return null;
290
+ }
291
+ }
292
+
160
293
  // ==================== TRADING ====================
161
294
 
162
295
  async getPositions(accountId) {
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Rithmic Accounts Module
3
3
  * Account fetching, PnL, and positions
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const { REQ } = require('./constants');
7
9
 
10
+ // Debug mode - set HQX_DEBUG=1 to enable
11
+ const DEBUG = process.env.HQX_DEBUG === '1';
12
+ const debug = (...args) => DEBUG && console.log('[Rithmic]', ...args);
13
+
8
14
  /**
9
15
  * Hash account ID to numeric (for compatibility)
10
16
  */
@@ -60,10 +66,13 @@ const fetchAccounts = async (service) => {
60
66
  };
61
67
 
62
68
  /**
63
- * Get trading accounts formatted like ProjectX
69
+ * Get trading accounts - ONLY returns values from API
70
+ * No estimation, no simulation
71
+ *
64
72
  * @param {RithmicService} service - The Rithmic service instance
65
73
  */
66
74
  const getTradingAccounts = async (service) => {
75
+ // Fetch accounts if not already loaded
67
76
  if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
68
77
  try {
69
78
  await fetchAccounts(service);
@@ -72,40 +81,72 @@ const getTradingAccounts = async (service) => {
72
81
  }
73
82
  }
74
83
 
75
- let tradingAccounts = service.accounts.map((acc) => {
76
- const pnl = service.accountPnL.get(acc.accountId) || {};
77
- const balance = parseFloat(pnl.accountBalance || pnl.marginBalance || pnl.cashOnHand || 0) || service.propfirm.defaultBalance;
78
- const startingBalance = service.propfirm.defaultBalance;
79
- const profitAndLoss = balance - startingBalance;
84
+ // Request fresh P&L data from API
85
+ if (service.pnlConn && service.accounts.length > 0) {
86
+ await requestPnLSnapshot(service);
87
+ }
88
+
89
+ const tradingAccounts = service.accounts.map((acc) => {
90
+ // Get P&L data from API (stored in accountPnL map from handlers.js)
91
+ const pnlData = service.accountPnL.get(acc.accountId);
92
+
93
+ // ONLY use values that came from API - null if not available
94
+ let balance = null;
95
+ let todayPnL = null;
96
+ let openPnL = null;
97
+ let closedPnL = null;
98
+
99
+ if (pnlData) {
100
+ // These values come directly from Rithmic API via handleAccountPnLUpdate
101
+ balance = pnlData.accountBalance !== undefined ? pnlData.accountBalance : null;
102
+ openPnL = pnlData.openPositionPnl !== undefined ? pnlData.openPositionPnl : null;
103
+ closedPnL = pnlData.closedPositionPnl !== undefined ? pnlData.closedPositionPnl : null;
104
+ todayPnL = pnlData.dayPnl !== undefined ? pnlData.dayPnl : null;
105
+ }
106
+
107
+ // Total P&L from API only
108
+ let profitAndLoss = null;
109
+ if (todayPnL !== null) {
110
+ profitAndLoss = todayPnL;
111
+ } else if (openPnL !== null || closedPnL !== null) {
112
+ profitAndLoss = (openPnL || 0) + (closedPnL || 0);
113
+ }
80
114
 
81
115
  return {
82
116
  accountId: hashAccountId(acc.accountId),
83
117
  rithmicAccountId: acc.accountId,
84
118
  accountName: acc.accountName || acc.accountId,
85
119
  name: acc.accountName || acc.accountId,
120
+ // From API only - null if not available
86
121
  balance: balance,
87
- startingBalance: startingBalance,
122
+ todayPnL: closedPnL, // Realized P&L from API
123
+ openPnL: openPnL, // Unrealized P&L from API
88
124
  profitAndLoss: profitAndLoss,
125
+ // No estimation - these are null
126
+ startingBalance: null,
89
127
  status: 0,
90
128
  platform: 'Rithmic',
91
129
  propfirm: service.propfirm.name,
92
130
  };
93
131
  });
94
132
 
133
+ // Fallback if no accounts found
95
134
  if (tradingAccounts.length === 0 && service.user) {
96
135
  const userName = service.user.userName || 'Unknown';
97
- tradingAccounts = [{
136
+ tradingAccounts.push({
98
137
  accountId: hashAccountId(userName),
99
138
  rithmicAccountId: userName,
100
139
  accountName: userName,
101
140
  name: userName,
102
- balance: service.propfirm.defaultBalance,
103
- startingBalance: service.propfirm.defaultBalance,
104
- profitAndLoss: 0,
141
+ balance: null,
142
+ startingBalance: null,
143
+ todayPnL: null,
144
+ openPnL: null,
145
+ profitAndLoss: null,
105
146
  status: 0,
106
147
  platform: 'Rithmic',
107
148
  propfirm: service.propfirm.name,
108
- }];
149
+ });
109
150
  }
110
151
 
111
152
  return { success: true, accounts: tradingAccounts };
@@ -128,7 +169,8 @@ const requestPnLSnapshot = async (service) => {
128
169
  });
129
170
  }
130
171
 
131
- await new Promise(resolve => setTimeout(resolve, 2000));
172
+ // Wait for P&L data to arrive
173
+ await new Promise(resolve => setTimeout(resolve, 1500));
132
174
  };
133
175
 
134
176
  /**
@@ -151,10 +193,11 @@ const subscribePnLUpdates = (service) => {
151
193
  };
152
194
 
153
195
  /**
154
- * Get positions
196
+ * Get positions - ONLY returns values from API
155
197
  * @param {RithmicService} service - The Rithmic service instance
156
198
  */
157
199
  const getPositions = async (service) => {
200
+ // Ensure PnL connection is active
158
201
  if (!service.pnlConn && service.credentials) {
159
202
  await service.connectPnL(service.credentials.username, service.credentials.password);
160
203
  await requestPnLSnapshot(service);
@@ -165,8 +208,10 @@ const getPositions = async (service) => {
165
208
  exchange: pos.exchange,
166
209
  quantity: pos.quantity,
167
210
  averagePrice: pos.averagePrice,
168
- unrealizedPnl: pos.openPnl,
169
- realizedPnl: pos.closedPnl,
211
+ // From API only
212
+ unrealizedPnl: pos.openPnl !== undefined ? pos.openPnl : null,
213
+ realizedPnl: pos.closedPnl !== undefined ? pos.closedPnl : null,
214
+ dayPnl: pos.dayPnl !== undefined ? pos.dayPnl : null,
170
215
  side: pos.quantity > 0 ? 'LONG' : 'SHORT',
171
216
  }));
172
217
 
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Tradovate Service
3
3
  * Main service for Tradovate prop firm connections (Apex, TakeProfitTrader)
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const https = require('https');
@@ -112,7 +114,8 @@ class TradovateService extends EventEmitter {
112
114
  }
113
115
 
114
116
  /**
115
- * Get trading accounts (formatted for HQX)
117
+ * Get trading accounts - ONLY returns values from API
118
+ * No estimation, no simulation
116
119
  */
117
120
  async getTradingAccounts() {
118
121
  if (this.accounts.length === 0) {
@@ -121,20 +124,34 @@ class TradovateService extends EventEmitter {
121
124
 
122
125
  const tradingAccounts = this.accounts.map((acc) => {
123
126
  const cb = acc.cashBalance || {};
124
- const balance = cb.totalCashValue || cb.netLiquidatingValue || this.propfirm.defaultBalance;
125
- const startingBalance = this.propfirm.defaultBalance;
126
- const profitAndLoss = cb.totalPnL || (balance - startingBalance);
127
- const openPnL = cb.openPnL || 0;
127
+
128
+ // ONLY use values from API - null if not available
129
+ const balance = cb.totalCashValue !== undefined ? cb.totalCashValue :
130
+ (cb.netLiquidatingValue !== undefined ? cb.netLiquidatingValue : null);
131
+
132
+ // P&L from API only - null if not available
133
+ const realizedPnL = cb.realizedPnL !== undefined ? cb.realizedPnL : null;
134
+ const openPnL = cb.openPnL !== undefined ? cb.openPnL : null;
135
+
136
+ // Total P&L from API
137
+ let profitAndLoss = null;
138
+ if (cb.totalPnL !== undefined) {
139
+ profitAndLoss = cb.totalPnL;
140
+ } else if (realizedPnL !== null || openPnL !== null) {
141
+ profitAndLoss = (realizedPnL || 0) + (openPnL || 0);
142
+ }
128
143
 
129
144
  return {
130
145
  accountId: acc.id,
131
146
  tradovateAccountId: acc.id,
132
147
  accountName: acc.name,
133
148
  name: acc.name,
149
+ // From API only - null if not available
134
150
  balance: balance,
135
- startingBalance: startingBalance,
136
- profitAndLoss: profitAndLoss,
151
+ todayPnL: realizedPnL,
137
152
  openPnL: openPnL,
153
+ profitAndLoss: profitAndLoss,
154
+ startingBalance: null, // API doesn't provide this
138
155
  status: acc.active ? 0 : 3,
139
156
  platform: 'Tradovate',
140
157
  propfirm: this.propfirm.name,
package/src/ui/index.js CHANGED
@@ -29,16 +29,11 @@ const { createBoxMenu } = require('./menu');
29
29
  * This fixes input leaking to bash after session restore or algo trading
30
30
  */
31
31
  const prepareStdin = () => {
32
+ // Minimal intervention - just ensure stdin is flowing
32
33
  try {
33
- // Remove any raw mode that might be left from previous operations
34
- if (process.stdin.isTTY && process.stdin.isRaw) {
35
- process.stdin.setRawMode(false);
34
+ if (process.stdin.isPaused()) {
35
+ process.stdin.resume();
36
36
  }
37
- // Remove any lingering keypress listeners
38
- process.stdin.removeAllListeners('keypress');
39
- process.stdin.removeAllListeners('data');
40
- // Pause stdin so inquirer can take control
41
- process.stdin.pause();
42
37
  } catch (e) {
43
38
  // Ignore errors
44
39
  }