hedgequantx 1.3.4 → 1.3.6

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.4",
3
+ "version": "1.3.6",
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": {
@@ -80,12 +80,14 @@ const showAccounts = async (service) => {
80
80
  const pf2 = acc2 ? chalk.magenta(acc2.propfirm || 'Unknown') : '';
81
81
  console.log(chalk.cyan('║') + fmtRow('PropFirm:', pf1, col1) + chalk.cyan('│') + (acc2 ? fmtRow('PropFirm:', pf2, col2) : ' '.repeat(col2)) + chalk.cyan('║'));
82
82
 
83
- // Balance
84
- const bal1 = acc1.balance || 0;
85
- const bal2 = acc2 ? (acc2.balance || 0) : 0;
86
- const balColor1 = bal1 >= 0 ? chalk.green : chalk.red;
87
- const balColor2 = bal2 >= 0 ? chalk.green : chalk.red;
88
- console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1('$' + bal1.toLocaleString()), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', balColor2('$' + bal2.toLocaleString()), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
83
+ // Balance - show '--' if null (not available from API)
84
+ const bal1 = acc1.balance;
85
+ const bal2 = acc2 ? acc2.balance : null;
86
+ const balStr1 = bal1 !== null && bal1 !== undefined ? '$' + bal1.toLocaleString() : '--';
87
+ const balStr2 = bal2 !== null && bal2 !== undefined ? '$' + bal2.toLocaleString() : '--';
88
+ const balColor1 = bal1 === null ? chalk.gray : (bal1 >= 0 ? chalk.green : chalk.red);
89
+ const balColor2 = bal2 === null ? chalk.gray : (bal2 >= 0 ? chalk.green : chalk.red);
90
+ console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1(balStr1), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', balColor2(balStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
89
91
 
90
92
  // Status
91
93
  const status1 = ACCOUNT_STATUS[acc1.status] || { text: 'Unknown', color: 'gray' };
@@ -152,12 +152,18 @@ class ProjectXService {
152
152
  // ==================== ACCOUNTS ====================
153
153
 
154
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
155
+ * Get trading accounts with REAL P&L from API
156
+ *
157
+ * Data sources (all from userApi):
158
+ * - /TradingAccount: accountId, accountName, balance, status, type
159
+ * - /AccountTemplate/userTemplates: startingBalance
160
+ * - /Position?accountId=X: profitAndLoss (unrealized P&L from open positions)
161
+ *
162
+ * All values come from API. No estimation.
158
163
  */
159
164
  async getTradingAccounts() {
160
165
  try {
166
+ // 1. Get accounts
161
167
  const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
162
168
  debug('getTradingAccounts response:', JSON.stringify(response.data, null, 2));
163
169
 
@@ -167,126 +173,81 @@ class ProjectXService {
167
173
 
168
174
  const accounts = Array.isArray(response.data) ? response.data : [];
169
175
 
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 };
187
- } catch (error) {
188
- return { success: false, accounts: [], error: error.message };
189
- }
190
- }
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
- }
176
+ // 2. Get account templates (for startingBalance)
177
+ let templates = [];
178
+ try {
179
+ const templateRes = await this._request(this.propfirm.userApi, '/AccountTemplate/userTemplates', 'GET');
180
+ if (templateRes.statusCode === 200 && Array.isArray(templateRes.data)) {
181
+ templates = templateRes.data;
182
+ debug('Templates:', JSON.stringify(templates, null, 2));
247
183
  }
248
- debug(` Total realized P&L: ${totalPnL}`);
249
- return totalPnL;
184
+ } catch (e) {
185
+ debug('Failed to get templates:', e.message);
250
186
  }
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
187
 
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
- );
188
+ const enrichedAccounts = [];
269
189
 
270
- if (response.statusCode === 200 && response.data) {
271
- const positions = response.data.positions || response.data || [];
272
- debug(`_getOpenPositionsPnL: ${positions.length} positions found`);
190
+ for (const account of accounts) {
191
+ // Find matching template for startingBalance
192
+ const template = templates.find(t =>
193
+ account.accountName && (
194
+ account.accountName.includes(t.title) ||
195
+ t.title.includes(account.accountName)
196
+ )
197
+ );
273
198
 
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}`);
199
+ const enriched = {
200
+ accountId: account.accountId,
201
+ accountName: account.accountName,
202
+ balance: account.balance, // From /TradingAccount
203
+ status: account.status, // From /TradingAccount
204
+ type: account.type, // From /TradingAccount
205
+ startingBalance: template?.startingBalance || null, // From /AccountTemplate
206
+ platform: 'ProjectX',
207
+ propfirm: this.propfirm.name,
208
+ openPnL: null,
209
+ profitAndLoss: null,
210
+ };
211
+
212
+ // Get P&L for active accounts only
213
+ if (account.status === 0) {
214
+ // Get unrealized P&L from /Position endpoint (userApi)
215
+ try {
216
+ const posRes = await this._request(
217
+ this.propfirm.userApi,
218
+ `/Position?accountId=${account.accountId}`,
219
+ 'GET'
220
+ );
221
+ debug(`Positions for ${account.accountId}:`, JSON.stringify(posRes.data, null, 2));
222
+
223
+ if (posRes.statusCode === 200 && Array.isArray(posRes.data)) {
224
+ let openPnL = 0;
225
+ for (const pos of posRes.data) {
226
+ if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
227
+ openPnL += pos.profitAndLoss;
228
+ }
229
+ }
230
+ enriched.openPnL = openPnL;
231
+ enriched.profitAndLoss = openPnL; // Open P&L from positions
280
232
  }
233
+ } catch (e) {
234
+ debug('Failed to get positions:', e.message);
281
235
  }
282
- debug(` Total open P&L: ${totalPnL}`);
283
- return totalPnL;
284
236
  }
237
+
238
+ debug(`Account ${account.accountId}:`, {
239
+ balance: enriched.balance,
240
+ startingBalance: enriched.startingBalance,
241
+ openPnL: enriched.openPnL,
242
+ profitAndLoss: enriched.profitAndLoss
243
+ });
244
+
245
+ enrichedAccounts.push(enriched);
285
246
  }
286
- debug('_getOpenPositionsPnL: API failed or no data');
287
- return null;
288
- } catch (e) {
289
- return null;
247
+
248
+ return { success: true, accounts: enrichedAccounts };
249
+ } catch (error) {
250
+ return { success: false, accounts: [], error: error.message };
290
251
  }
291
252
  }
292
253
 
@@ -2,12 +2,16 @@
2
2
  * Rithmic Accounts Module
3
3
  * Account fetching, PnL, and positions
4
4
  *
5
- * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
5
+ * P&L Data Sources:
6
+ * - accountBalance: from PNL_PLANT API
7
+ * - openPositionPnl: unrealized P&L from API
8
+ * - closedPositionPnl: realized P&L from API
9
+ * - dayPnl: total day P&L from API
6
10
  */
7
11
 
8
12
  const { REQ } = require('./constants');
9
13
 
10
- // Debug mode - set HQX_DEBUG=1 to enable
14
+ // Debug mode
11
15
  const DEBUG = process.env.HQX_DEBUG === '1';
12
16
  const debug = (...args) => DEBUG && console.log('[Rithmic]', ...args);
13
17
 
@@ -66,87 +70,88 @@ const fetchAccounts = async (service) => {
66
70
  };
67
71
 
68
72
  /**
69
- * Get trading accounts - ONLY returns values from API
70
- * No estimation, no simulation
71
- *
73
+ * Get trading accounts with P&L data from API
72
74
  * @param {RithmicService} service - The Rithmic service instance
73
75
  */
74
76
  const getTradingAccounts = async (service) => {
77
+ debug('getTradingAccounts called');
78
+
75
79
  // Fetch accounts if not already loaded
76
80
  if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
77
81
  try {
78
82
  await fetchAccounts(service);
83
+ debug('Accounts fetched:', service.accounts.length);
79
84
  } catch (e) {
80
- // Ignore fetch errors
85
+ debug('Failed to fetch accounts:', e.message);
81
86
  }
82
87
  }
83
88
 
84
- // Request fresh P&L data from API
89
+ // Request fresh P&L data if PnL connection exists
85
90
  if (service.pnlConn && service.accounts.length > 0) {
91
+ debug('Requesting P&L snapshot...');
86
92
  await requestPnLSnapshot(service);
87
93
  }
88
94
 
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);
95
+ let tradingAccounts = service.accounts.map((acc) => {
96
+ // Get P&L data from accountPnL map (populated by PNL_PLANT messages)
97
+ const pnlData = service.accountPnL.get(acc.accountId) || {};
98
+ debug(`Account ${acc.accountId} pnlData:`, pnlData);
92
99
 
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;
100
+ // Use API values if available, otherwise use defaults
101
+ const accountBalance = parseFloat(pnlData.accountBalance || 0);
102
+ const openPnL = parseFloat(pnlData.openPositionPnl || 0);
103
+ const closedPnL = parseFloat(pnlData.closedPositionPnl || 0);
104
+ const dayPnL = parseFloat(pnlData.dayPnl || 0);
98
105
 
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;
106
+ // Balance: use API value if available, otherwise default
107
+ const balance = accountBalance > 0 ? accountBalance : service.propfirm.defaultBalance;
108
+ const startingBalance = service.propfirm.defaultBalance;
109
+
110
+ // P&L: prefer dayPnl from API, otherwise calculate
111
+ let profitAndLoss = 0;
112
+ if (dayPnL !== 0) {
113
+ profitAndLoss = dayPnL;
114
+ } else if (openPnL !== 0 || closedPnL !== 0) {
115
+ profitAndLoss = openPnL + closedPnL;
116
+ } else if (accountBalance > 0) {
117
+ profitAndLoss = accountBalance - startingBalance;
105
118
  }
106
119
 
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
- }
120
+ debug(` balance: ${balance}, P&L: ${profitAndLoss}`);
114
121
 
115
122
  return {
116
123
  accountId: hashAccountId(acc.accountId),
117
124
  rithmicAccountId: acc.accountId,
118
125
  accountName: acc.accountName || acc.accountId,
119
126
  name: acc.accountName || acc.accountId,
120
- // From API only - null if not available
121
127
  balance: balance,
122
- todayPnL: closedPnL, // Realized P&L from API
123
- openPnL: openPnL, // Unrealized P&L from API
128
+ startingBalance: startingBalance,
124
129
  profitAndLoss: profitAndLoss,
125
- // No estimation - these are null
126
- startingBalance: null,
130
+ openPnL: openPnL,
131
+ todayPnL: closedPnL,
127
132
  status: 0,
128
133
  platform: 'Rithmic',
129
134
  propfirm: service.propfirm.name,
130
135
  };
131
136
  });
132
137
 
133
- // Fallback if no accounts found
138
+ // Fallback if no accounts
134
139
  if (tradingAccounts.length === 0 && service.user) {
135
140
  const userName = service.user.userName || 'Unknown';
136
- tradingAccounts.push({
141
+ tradingAccounts = [{
137
142
  accountId: hashAccountId(userName),
138
143
  rithmicAccountId: userName,
139
144
  accountName: userName,
140
145
  name: userName,
141
- balance: null,
142
- startingBalance: null,
143
- todayPnL: null,
144
- openPnL: null,
145
- profitAndLoss: null,
146
+ balance: service.propfirm.defaultBalance,
147
+ startingBalance: service.propfirm.defaultBalance,
148
+ profitAndLoss: 0,
149
+ openPnL: 0,
150
+ todayPnL: 0,
146
151
  status: 0,
147
152
  platform: 'Rithmic',
148
153
  propfirm: service.propfirm.name,
149
- });
154
+ }];
150
155
  }
151
156
 
152
157
  return { success: true, accounts: tradingAccounts };
@@ -157,8 +162,13 @@ const getTradingAccounts = async (service) => {
157
162
  * @param {RithmicService} service - The Rithmic service instance
158
163
  */
159
164
  const requestPnLSnapshot = async (service) => {
160
- if (!service.pnlConn || !service.loginInfo) return;
165
+ if (!service.pnlConn || !service.loginInfo) {
166
+ debug('Cannot request P&L - no pnlConn or loginInfo');
167
+ return;
168
+ }
161
169
 
170
+ debug('Requesting P&L for', service.accounts.length, 'accounts');
171
+
162
172
  for (const acc of service.accounts) {
163
173
  service.pnlConn.send('RequestPnLPositionSnapshot', {
164
174
  templateId: REQ.PNL_POSITION_SNAPSHOT,
@@ -169,8 +179,9 @@ const requestPnLSnapshot = async (service) => {
169
179
  });
170
180
  }
171
181
 
172
- // Wait for P&L data to arrive
173
- await new Promise(resolve => setTimeout(resolve, 1500));
182
+ // Wait for responses
183
+ await new Promise(resolve => setTimeout(resolve, 2000));
184
+ debug('P&L snapshot complete, accountPnL size:', service.accountPnL.size);
174
185
  };
175
186
 
176
187
  /**
@@ -193,11 +204,11 @@ const subscribePnLUpdates = (service) => {
193
204
  };
194
205
 
195
206
  /**
196
- * Get positions - ONLY returns values from API
207
+ * Get positions
197
208
  * @param {RithmicService} service - The Rithmic service instance
198
209
  */
199
210
  const getPositions = async (service) => {
200
- // Ensure PnL connection is active
211
+ // Connect to PnL if needed
201
212
  if (!service.pnlConn && service.credentials) {
202
213
  await service.connectPnL(service.credentials.username, service.credentials.password);
203
214
  await requestPnLSnapshot(service);
@@ -208,10 +219,8 @@ const getPositions = async (service) => {
208
219
  exchange: pos.exchange,
209
220
  quantity: pos.quantity,
210
221
  averagePrice: pos.averagePrice,
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,
222
+ unrealizedPnl: pos.openPnl,
223
+ realizedPnl: pos.closedPnl,
215
224
  side: pos.quantity > 0 ? 'LONG' : 'SHORT',
216
225
  }));
217
226
 
@@ -94,16 +94,21 @@ class RithmicService extends EventEmitter {
94
94
 
95
95
  this.credentials = { username, password };
96
96
 
97
- const formattedAccounts = this.accounts.map(acc => ({
98
- accountId: acc.accountId,
99
- accountName: acc.accountName || acc.accountId,
100
- balance: this.propfirm.defaultBalance,
101
- startingBalance: this.propfirm.defaultBalance,
102
- profitAndLoss: 0,
103
- status: 0
104
- }));
97
+ // Connect to PNL_PLANT for balance/P&L data
98
+ try {
99
+ await this.connectPnL(username, password);
100
+ if (this.pnlConn) {
101
+ await requestPnLSnapshot(this);
102
+ subscribePnLUpdates(this);
103
+ }
104
+ } catch (e) {
105
+ // PnL connection failed, continue without it
106
+ }
107
+
108
+ // Get accounts with P&L data (if available)
109
+ const result = await getTradingAccounts(this);
105
110
 
106
- resolve({ success: true, user: this.user, accounts: formattedAccounts });
111
+ resolve({ success: true, user: this.user, accounts: result.accounts });
107
112
  });
108
113
 
109
114
  this.orderConn.once('loginFailed', (data) => {