hedgequantx 1.3.4 → 1.3.5
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 +1 -1
- package/src/services/projectx/index.js +75 -114
package/package.json
CHANGED
|
@@ -152,12 +152,18 @@ class ProjectXService {
|
|
|
152
152
|
// ==================== ACCOUNTS ====================
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* Get trading accounts
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
287
|
-
return
|
|
288
|
-
} catch (
|
|
289
|
-
return
|
|
247
|
+
|
|
248
|
+
return { success: true, accounts: enrichedAccounts };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return { success: false, accounts: [], error: error.message };
|
|
290
251
|
}
|
|
291
252
|
}
|
|
292
253
|
|