hedgequantx 1.3.5 → 1.3.7
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' };
|
|
@@ -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,89 @@ 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:`, JSON.stringify(pnlData));
|
|
99
|
+
debug(` accountPnL map size:`, service.accountPnL.size);
|
|
92
100
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
// Use API values if available
|
|
102
|
+
const accountBalance = parseFloat(pnlData.accountBalance || 0);
|
|
103
|
+
const openPnL = parseFloat(pnlData.openPositionPnl || 0);
|
|
104
|
+
const closedPnL = parseFloat(pnlData.closedPositionPnl || 0);
|
|
105
|
+
const dayPnL = parseFloat(pnlData.dayPnl || 0);
|
|
98
106
|
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
// Balance: use API value if > 0, otherwise default
|
|
108
|
+
// Most prop firms don't report balance via PnL stream, so we use default
|
|
109
|
+
const startingBalance = service.propfirm.defaultBalance;
|
|
110
|
+
const balance = accountBalance > 0 ? accountBalance : startingBalance;
|
|
111
|
+
|
|
112
|
+
// P&L: prefer dayPnl from API, otherwise calculate from open+closed
|
|
113
|
+
let profitAndLoss = 0;
|
|
114
|
+
if (dayPnL !== 0) {
|
|
115
|
+
profitAndLoss = dayPnL;
|
|
116
|
+
} else if (openPnL !== 0 || closedPnL !== 0) {
|
|
117
|
+
profitAndLoss = openPnL + closedPnL;
|
|
105
118
|
}
|
|
119
|
+
// Don't calculate P&L from balance difference - that's estimation
|
|
106
120
|
|
|
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
|
-
}
|
|
121
|
+
debug(` balance: ${balance}, startingBalance: ${startingBalance}, P&L: ${profitAndLoss}`);
|
|
114
122
|
|
|
115
123
|
return {
|
|
116
124
|
accountId: hashAccountId(acc.accountId),
|
|
117
125
|
rithmicAccountId: acc.accountId,
|
|
118
126
|
accountName: acc.accountName || acc.accountId,
|
|
119
127
|
name: acc.accountName || acc.accountId,
|
|
120
|
-
// From API only - null if not available
|
|
121
128
|
balance: balance,
|
|
122
|
-
|
|
123
|
-
openPnL: openPnL, // Unrealized P&L from API
|
|
129
|
+
startingBalance: startingBalance,
|
|
124
130
|
profitAndLoss: profitAndLoss,
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
openPnL: openPnL,
|
|
132
|
+
todayPnL: closedPnL,
|
|
127
133
|
status: 0,
|
|
128
134
|
platform: 'Rithmic',
|
|
129
135
|
propfirm: service.propfirm.name,
|
|
130
136
|
};
|
|
131
137
|
});
|
|
132
138
|
|
|
133
|
-
// Fallback if no accounts
|
|
139
|
+
// Fallback if no accounts
|
|
134
140
|
if (tradingAccounts.length === 0 && service.user) {
|
|
135
141
|
const userName = service.user.userName || 'Unknown';
|
|
136
|
-
tradingAccounts
|
|
142
|
+
tradingAccounts = [{
|
|
137
143
|
accountId: hashAccountId(userName),
|
|
138
144
|
rithmicAccountId: userName,
|
|
139
145
|
accountName: userName,
|
|
140
146
|
name: userName,
|
|
141
|
-
balance:
|
|
142
|
-
startingBalance:
|
|
143
|
-
|
|
144
|
-
openPnL:
|
|
145
|
-
|
|
147
|
+
balance: service.propfirm.defaultBalance,
|
|
148
|
+
startingBalance: service.propfirm.defaultBalance,
|
|
149
|
+
profitAndLoss: 0,
|
|
150
|
+
openPnL: 0,
|
|
151
|
+
todayPnL: 0,
|
|
146
152
|
status: 0,
|
|
147
153
|
platform: 'Rithmic',
|
|
148
154
|
propfirm: service.propfirm.name,
|
|
149
|
-
}
|
|
155
|
+
}];
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
return { success: true, accounts: tradingAccounts };
|
|
@@ -157,8 +163,13 @@ const getTradingAccounts = async (service) => {
|
|
|
157
163
|
* @param {RithmicService} service - The Rithmic service instance
|
|
158
164
|
*/
|
|
159
165
|
const requestPnLSnapshot = async (service) => {
|
|
160
|
-
if (!service.pnlConn || !service.loginInfo)
|
|
166
|
+
if (!service.pnlConn || !service.loginInfo) {
|
|
167
|
+
debug('Cannot request P&L - no pnlConn or loginInfo');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
161
170
|
|
|
171
|
+
debug('Requesting P&L for', service.accounts.length, 'accounts');
|
|
172
|
+
|
|
162
173
|
for (const acc of service.accounts) {
|
|
163
174
|
service.pnlConn.send('RequestPnLPositionSnapshot', {
|
|
164
175
|
templateId: REQ.PNL_POSITION_SNAPSHOT,
|
|
@@ -169,8 +180,9 @@ const requestPnLSnapshot = async (service) => {
|
|
|
169
180
|
});
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
// Wait for
|
|
173
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
183
|
+
// Wait for responses
|
|
184
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
185
|
+
debug('P&L snapshot complete, accountPnL size:', service.accountPnL.size);
|
|
174
186
|
};
|
|
175
187
|
|
|
176
188
|
/**
|
|
@@ -193,11 +205,11 @@ const subscribePnLUpdates = (service) => {
|
|
|
193
205
|
};
|
|
194
206
|
|
|
195
207
|
/**
|
|
196
|
-
* Get positions
|
|
208
|
+
* Get positions
|
|
197
209
|
* @param {RithmicService} service - The Rithmic service instance
|
|
198
210
|
*/
|
|
199
211
|
const getPositions = async (service) => {
|
|
200
|
-
//
|
|
212
|
+
// Connect to PnL if needed
|
|
201
213
|
if (!service.pnlConn && service.credentials) {
|
|
202
214
|
await service.connectPnL(service.credentials.username, service.credentials.password);
|
|
203
215
|
await requestPnLSnapshot(service);
|
|
@@ -208,10 +220,8 @@ const getPositions = async (service) => {
|
|
|
208
220
|
exchange: pos.exchange,
|
|
209
221
|
quantity: pos.quantity,
|
|
210
222
|
averagePrice: pos.averagePrice,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
realizedPnl: pos.closedPnl !== undefined ? pos.closedPnl : null,
|
|
214
|
-
dayPnl: pos.dayPnl !== undefined ? pos.dayPnl : null,
|
|
223
|
+
unrealizedPnl: pos.openPnl,
|
|
224
|
+
realizedPnl: pos.closedPnl,
|
|
215
225
|
side: pos.quantity > 0 ? 'LONG' : 'SHORT',
|
|
216
226
|
}));
|
|
217
227
|
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
|
|
7
7
|
const { RES, STREAM } = require('./constants');
|
|
8
8
|
|
|
9
|
+
// Debug mode
|
|
10
|
+
const DEBUG = process.env.HQX_DEBUG === '1';
|
|
11
|
+
const debug = (...args) => DEBUG && console.log('[Rithmic:Handler]', ...args);
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Create ORDER_PLANT message handler
|
|
11
15
|
* @param {RithmicService} service - The Rithmic service instance
|
|
@@ -44,18 +48,24 @@ const createOrderHandler = (service) => {
|
|
|
44
48
|
const createPnLHandler = (service) => {
|
|
45
49
|
return (msg) => {
|
|
46
50
|
const { templateId, data } = msg;
|
|
51
|
+
|
|
52
|
+
debug('PNL message received, templateId:', templateId);
|
|
47
53
|
|
|
48
54
|
switch (templateId) {
|
|
49
55
|
case RES.PNL_POSITION_SNAPSHOT:
|
|
50
56
|
case RES.PNL_POSITION_UPDATES:
|
|
51
|
-
|
|
57
|
+
debug('PNL snapshot/updates response OK');
|
|
52
58
|
break;
|
|
53
59
|
case STREAM.ACCOUNT_PNL_UPDATE:
|
|
60
|
+
debug('Account PNL update received');
|
|
54
61
|
handleAccountPnLUpdate(service, data);
|
|
55
62
|
break;
|
|
56
63
|
case STREAM.INSTRUMENT_PNL_UPDATE:
|
|
64
|
+
debug('Instrument PNL update received');
|
|
57
65
|
handleInstrumentPnLUpdate(service, data);
|
|
58
66
|
break;
|
|
67
|
+
default:
|
|
68
|
+
debug('Unknown PNL templateId:', templateId);
|
|
59
69
|
}
|
|
60
70
|
};
|
|
61
71
|
};
|
|
@@ -136,19 +146,25 @@ const handleShowOrdersResponse = (service, data) => {
|
|
|
136
146
|
const handleAccountPnLUpdate = (service, data) => {
|
|
137
147
|
try {
|
|
138
148
|
const pnl = decodeAccountPnL(data);
|
|
149
|
+
debug('Decoded Account PNL:', JSON.stringify(pnl));
|
|
150
|
+
|
|
139
151
|
if (pnl.accountId) {
|
|
140
|
-
|
|
152
|
+
const pnlData = {
|
|
141
153
|
accountBalance: parseFloat(pnl.accountBalance || 0),
|
|
142
154
|
cashOnHand: parseFloat(pnl.cashOnHand || 0),
|
|
143
155
|
marginBalance: parseFloat(pnl.marginBalance || 0),
|
|
144
156
|
openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
|
|
145
157
|
closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
|
|
146
158
|
dayPnl: parseFloat(pnl.dayPnl || 0),
|
|
147
|
-
}
|
|
159
|
+
};
|
|
160
|
+
debug('Storing PNL for account:', pnl.accountId, pnlData);
|
|
161
|
+
service.accountPnL.set(pnl.accountId, pnlData);
|
|
148
162
|
service.emit('pnlUpdate', pnl);
|
|
163
|
+
} else {
|
|
164
|
+
debug('No accountId in PNL response');
|
|
149
165
|
}
|
|
150
166
|
} catch (e) {
|
|
151
|
-
|
|
167
|
+
debug('Error decoding Account PNL:', e.message);
|
|
152
168
|
}
|
|
153
169
|
};
|
|
154
170
|
|
|
@@ -10,6 +10,10 @@ const { createOrderHandler, createPnLHandler } = require('./handlers');
|
|
|
10
10
|
const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
|
|
11
11
|
const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
|
|
12
12
|
|
|
13
|
+
// Debug mode
|
|
14
|
+
const DEBUG = process.env.HQX_DEBUG === '1';
|
|
15
|
+
const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
|
|
16
|
+
|
|
13
17
|
// PropFirm configurations
|
|
14
18
|
const PROPFIRM_CONFIGS = {
|
|
15
19
|
'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
@@ -94,16 +98,30 @@ class RithmicService extends EventEmitter {
|
|
|
94
98
|
|
|
95
99
|
this.credentials = { username, password };
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
debug('Accounts found:', this.accounts.length);
|
|
102
|
+
debug('Account IDs:', this.accounts.map(a => a.accountId));
|
|
103
|
+
|
|
104
|
+
// Connect to PNL_PLANT for balance/P&L data
|
|
105
|
+
try {
|
|
106
|
+
debug('Connecting to PNL_PLANT...');
|
|
107
|
+
const pnlConnected = await this.connectPnL(username, password);
|
|
108
|
+
debug('PNL_PLANT connected:', pnlConnected, 'pnlConn:', !!this.pnlConn);
|
|
109
|
+
|
|
110
|
+
if (this.pnlConn) {
|
|
111
|
+
debug('Requesting P&L snapshot...');
|
|
112
|
+
await requestPnLSnapshot(this);
|
|
113
|
+
debug('accountPnL map size after snapshot:', this.accountPnL.size);
|
|
114
|
+
subscribePnLUpdates(this);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
debug('PnL connection failed:', e.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get accounts with P&L data (if available)
|
|
121
|
+
const result = await getTradingAccounts(this);
|
|
122
|
+
debug('Final accounts:', result.accounts.length);
|
|
105
123
|
|
|
106
|
-
resolve({ success: true, user: this.user, accounts:
|
|
124
|
+
resolve({ success: true, user: this.user, accounts: result.accounts });
|
|
107
125
|
});
|
|
108
126
|
|
|
109
127
|
this.orderConn.once('loginFailed', (data) => {
|