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
package/src/pages/accounts.js
CHANGED
|
@@ -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
|
|
85
|
-
const bal2 = acc2 ?
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
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
|
|
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
|
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
* Rithmic Accounts Module
|
|
3
3
|
* Account fetching, PnL, and positions
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
85
|
+
debug('Failed to fetch accounts:', e.message);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
// Request fresh P&L data
|
|
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
|
-
|
|
90
|
-
// Get P&L data from
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
openPnL: openPnL, // Unrealized P&L from API
|
|
128
|
+
startingBalance: startingBalance,
|
|
124
129
|
profitAndLoss: profitAndLoss,
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
138
|
+
// Fallback if no accounts
|
|
134
139
|
if (tradingAccounts.length === 0 && service.user) {
|
|
135
140
|
const userName = service.user.userName || 'Unknown';
|
|
136
|
-
tradingAccounts
|
|
141
|
+
tradingAccounts = [{
|
|
137
142
|
accountId: hashAccountId(userName),
|
|
138
143
|
rithmicAccountId: userName,
|
|
139
144
|
accountName: userName,
|
|
140
145
|
name: userName,
|
|
141
|
-
balance:
|
|
142
|
-
startingBalance:
|
|
143
|
-
|
|
144
|
-
openPnL:
|
|
145
|
-
|
|
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)
|
|
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
|
|
173
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
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
|
|
207
|
+
* Get positions
|
|
197
208
|
* @param {RithmicService} service - The Rithmic service instance
|
|
198
209
|
*/
|
|
199
210
|
const getPositions = async (service) => {
|
|
200
|
-
//
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
111
|
+
resolve({ success: true, user: this.user, accounts: result.accounts });
|
|
107
112
|
});
|
|
108
113
|
|
|
109
114
|
this.orderConn.once('loginFailed', (data) => {
|