hedgequantx 1.3.2 → 1.3.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": "1.3.2",
3
+ "version": "1.3.3",
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,6 +1,8 @@
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');
@@ -145,18 +147,131 @@ class ProjectXService {
145
147
 
146
148
  // ==================== ACCOUNTS ====================
147
149
 
150
+ /**
151
+ * Get trading accounts - ONLY returns values from API
152
+ * P&L comes from: today's trades + open positions (both from API)
153
+ */
148
154
  async getTradingAccounts() {
149
155
  try {
150
156
  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 : [] };
157
+ if (response.statusCode !== 200) {
158
+ return { success: false, accounts: [], error: 'Failed to get accounts' };
159
+ }
160
+
161
+ const accounts = Array.isArray(response.data) ? response.data : [];
162
+ const enrichedAccounts = [];
163
+
164
+ for (const account of accounts) {
165
+ // Start with RAW API data only
166
+ const enriched = {
167
+ accountId: account.accountId,
168
+ accountName: account.accountName,
169
+ balance: account.balance, // From API
170
+ status: account.status, // From API
171
+ type: account.type, // From API
172
+ platform: 'ProjectX',
173
+ propfirm: this.propfirm.name,
174
+ // P&L fields - will be populated from API calls
175
+ todayPnL: null,
176
+ openPnL: null,
177
+ profitAndLoss: null,
178
+ startingBalance: null,
179
+ };
180
+
181
+ // Only fetch P&L for active accounts
182
+ if (account.status === 0) {
183
+ // Get today's realized P&L from trades API
184
+ const todayPnL = await this._getTodayRealizedPnL(account.accountId);
185
+ enriched.todayPnL = todayPnL;
186
+
187
+ // Get unrealized P&L from open positions API
188
+ const openPnL = await this._getOpenPositionsPnL(account.accountId);
189
+ enriched.openPnL = openPnL;
190
+
191
+ // Total P&L = realized + unrealized (both from API)
192
+ if (todayPnL !== null || openPnL !== null) {
193
+ enriched.profitAndLoss = (todayPnL || 0) + (openPnL || 0);
194
+ }
195
+ }
196
+
197
+ enrichedAccounts.push(enriched);
153
198
  }
154
- return { success: false, accounts: [], error: 'Failed to get accounts' };
199
+
200
+ return { success: true, accounts: enrichedAccounts };
155
201
  } catch (error) {
156
202
  return { success: false, accounts: [], error: error.message };
157
203
  }
158
204
  }
159
205
 
206
+ /**
207
+ * Get today's realized P&L from Trade API
208
+ * Returns null if API fails (not 0)
209
+ * @private
210
+ */
211
+ async _getTodayRealizedPnL(accountId) {
212
+ try {
213
+ const now = new Date();
214
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
215
+
216
+ const response = await this._request(
217
+ this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
218
+ {
219
+ accountId: accountId,
220
+ startTimestamp: startOfDay.toISOString(),
221
+ endTimestamp: now.toISOString()
222
+ }
223
+ );
224
+
225
+ if (response.statusCode === 200 && response.data) {
226
+ const trades = Array.isArray(response.data)
227
+ ? response.data
228
+ : (response.data.trades || []);
229
+
230
+ // Sum P&L from API response only
231
+ let totalPnL = 0;
232
+ for (const trade of trades) {
233
+ if (trade.profitAndLoss !== undefined && trade.profitAndLoss !== null) {
234
+ totalPnL += trade.profitAndLoss;
235
+ }
236
+ }
237
+ return totalPnL;
238
+ }
239
+ return null; // API failed - return null, not 0
240
+ } catch (e) {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get unrealized P&L from open positions API
247
+ * Returns null if API fails (not 0)
248
+ * @private
249
+ */
250
+ async _getOpenPositionsPnL(accountId) {
251
+ try {
252
+ const response = await this._request(
253
+ this.propfirm.gatewayApi, '/api/Position/searchOpen', 'POST',
254
+ { accountId: accountId }
255
+ );
256
+
257
+ if (response.statusCode === 200 && response.data) {
258
+ const positions = response.data.positions || response.data || [];
259
+ if (Array.isArray(positions)) {
260
+ let totalPnL = 0;
261
+ for (const pos of positions) {
262
+ if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
263
+ totalPnL += pos.profitAndLoss;
264
+ }
265
+ }
266
+ return totalPnL;
267
+ }
268
+ }
269
+ return null;
270
+ } catch (e) {
271
+ return null;
272
+ }
273
+ }
274
+
160
275
  // ==================== TRADING ====================
161
276
 
162
277
  async getPositions(accountId) {
@@ -1,6 +1,8 @@
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');
@@ -60,10 +62,13 @@ const fetchAccounts = async (service) => {
60
62
  };
61
63
 
62
64
  /**
63
- * Get trading accounts formatted like ProjectX
65
+ * Get trading accounts - ONLY returns values from API
66
+ * No estimation, no simulation
67
+ *
64
68
  * @param {RithmicService} service - The Rithmic service instance
65
69
  */
66
70
  const getTradingAccounts = async (service) => {
71
+ // Fetch accounts if not already loaded
67
72
  if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
68
73
  try {
69
74
  await fetchAccounts(service);
@@ -72,40 +77,72 @@ const getTradingAccounts = async (service) => {
72
77
  }
73
78
  }
74
79
 
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;
80
+ // Request fresh P&L data from API
81
+ if (service.pnlConn && service.accounts.length > 0) {
82
+ await requestPnLSnapshot(service);
83
+ }
84
+
85
+ const tradingAccounts = service.accounts.map((acc) => {
86
+ // Get P&L data from API (stored in accountPnL map from handlers.js)
87
+ const pnlData = service.accountPnL.get(acc.accountId);
88
+
89
+ // ONLY use values that came from API - null if not available
90
+ let balance = null;
91
+ let todayPnL = null;
92
+ let openPnL = null;
93
+ let closedPnL = null;
94
+
95
+ if (pnlData) {
96
+ // These values come directly from Rithmic API via handleAccountPnLUpdate
97
+ balance = pnlData.accountBalance !== undefined ? pnlData.accountBalance : null;
98
+ openPnL = pnlData.openPositionPnl !== undefined ? pnlData.openPositionPnl : null;
99
+ closedPnL = pnlData.closedPositionPnl !== undefined ? pnlData.closedPositionPnl : null;
100
+ todayPnL = pnlData.dayPnl !== undefined ? pnlData.dayPnl : null;
101
+ }
102
+
103
+ // Total P&L from API only
104
+ let profitAndLoss = null;
105
+ if (todayPnL !== null) {
106
+ profitAndLoss = todayPnL;
107
+ } else if (openPnL !== null || closedPnL !== null) {
108
+ profitAndLoss = (openPnL || 0) + (closedPnL || 0);
109
+ }
80
110
 
81
111
  return {
82
112
  accountId: hashAccountId(acc.accountId),
83
113
  rithmicAccountId: acc.accountId,
84
114
  accountName: acc.accountName || acc.accountId,
85
115
  name: acc.accountName || acc.accountId,
116
+ // From API only - null if not available
86
117
  balance: balance,
87
- startingBalance: startingBalance,
118
+ todayPnL: closedPnL, // Realized P&L from API
119
+ openPnL: openPnL, // Unrealized P&L from API
88
120
  profitAndLoss: profitAndLoss,
121
+ // No estimation - these are null
122
+ startingBalance: null,
89
123
  status: 0,
90
124
  platform: 'Rithmic',
91
125
  propfirm: service.propfirm.name,
92
126
  };
93
127
  });
94
128
 
129
+ // Fallback if no accounts found
95
130
  if (tradingAccounts.length === 0 && service.user) {
96
131
  const userName = service.user.userName || 'Unknown';
97
- tradingAccounts = [{
132
+ tradingAccounts.push({
98
133
  accountId: hashAccountId(userName),
99
134
  rithmicAccountId: userName,
100
135
  accountName: userName,
101
136
  name: userName,
102
- balance: service.propfirm.defaultBalance,
103
- startingBalance: service.propfirm.defaultBalance,
104
- profitAndLoss: 0,
137
+ balance: null,
138
+ startingBalance: null,
139
+ todayPnL: null,
140
+ openPnL: null,
141
+ profitAndLoss: null,
105
142
  status: 0,
106
143
  platform: 'Rithmic',
107
144
  propfirm: service.propfirm.name,
108
- }];
145
+ });
109
146
  }
110
147
 
111
148
  return { success: true, accounts: tradingAccounts };
@@ -128,7 +165,8 @@ const requestPnLSnapshot = async (service) => {
128
165
  });
129
166
  }
130
167
 
131
- await new Promise(resolve => setTimeout(resolve, 2000));
168
+ // Wait for P&L data to arrive
169
+ await new Promise(resolve => setTimeout(resolve, 1500));
132
170
  };
133
171
 
134
172
  /**
@@ -151,10 +189,11 @@ const subscribePnLUpdates = (service) => {
151
189
  };
152
190
 
153
191
  /**
154
- * Get positions
192
+ * Get positions - ONLY returns values from API
155
193
  * @param {RithmicService} service - The Rithmic service instance
156
194
  */
157
195
  const getPositions = async (service) => {
196
+ // Ensure PnL connection is active
158
197
  if (!service.pnlConn && service.credentials) {
159
198
  await service.connectPnL(service.credentials.username, service.credentials.password);
160
199
  await requestPnLSnapshot(service);
@@ -165,8 +204,10 @@ const getPositions = async (service) => {
165
204
  exchange: pos.exchange,
166
205
  quantity: pos.quantity,
167
206
  averagePrice: pos.averagePrice,
168
- unrealizedPnl: pos.openPnl,
169
- realizedPnl: pos.closedPnl,
207
+ // From API only
208
+ unrealizedPnl: pos.openPnl !== undefined ? pos.openPnl : null,
209
+ realizedPnl: pos.closedPnl !== undefined ? pos.closedPnl : null,
210
+ dayPnl: pos.dayPnl !== undefined ? pos.dayPnl : null,
170
211
  side: pos.quantity > 0 ? 'LONG' : 'SHORT',
171
212
  }));
172
213
 
@@ -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
@@ -37,8 +37,10 @@ const prepareStdin = () => {
37
37
  // Remove any lingering keypress listeners
38
38
  process.stdin.removeAllListeners('keypress');
39
39
  process.stdin.removeAllListeners('data');
40
- // Pause stdin so inquirer can take control
41
- process.stdin.pause();
40
+ // Resume stdin so inquirer can take control
41
+ if (process.stdin.isPaused()) {
42
+ process.stdin.resume();
43
+ }
42
44
  } catch (e) {
43
45
  // Ignore errors
44
46
  }