hedgequantx 2.7.11 → 2.7.13

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": "2.7.11",
3
+ "version": "2.7.13",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -20,6 +20,7 @@ const { algoTradingMenu } = require('./pages/algo');
20
20
 
21
21
  // Menus
22
22
  const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
23
+ const { PROPFIRM_CHOICES } = require('./config');
23
24
 
24
25
  /** @type {Object|null} */
25
26
  let currentService = null;
@@ -177,29 +178,94 @@ const run = async () => {
177
178
  await banner();
178
179
 
179
180
  if (!connections.isConnected()) {
180
- // Not connected - show Rithmic menu directly
181
+ // Not connected - show propfirm selection directly
181
182
  const boxWidth = getLogoWidth();
182
183
  const innerWidth = boxWidth - 2;
184
+ const numCols = 3;
185
+
186
+ const propfirms = PROPFIRM_CHOICES;
187
+ const numbered = propfirms.map((pf, i) => ({ num: i + 1, key: pf.value, name: pf.name }));
188
+
189
+ // Find max name length for alignment
190
+ const maxNameLen = Math.max(...numbered.map(n => n.name.length));
191
+ const colWidth = 4 + 1 + maxNameLen + 2; // [##] + space + name + gap
192
+ const totalContentWidth = numCols * colWidth;
193
+ const leftMargin = Math.max(2, Math.floor((innerWidth - totalContentWidth) / 2));
183
194
 
184
195
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
185
- console.log(chalk.cyan('║') + chalk.white.bold(centerText('CONNECT TO PROPFIRM', innerWidth)) + chalk.cyan('║'));
186
- console.log(chalk.cyan('' + ''.repeat(innerWidth) + ''));
187
- console.log(chalk.cyan('║') + ' ' + chalk.cyan('[1] Connect') + ' '.repeat(innerWidth - 14) + chalk.cyan('║'));
188
- console.log(chalk.cyan('║') + ' ' + chalk.red('[X] Exit') + ' '.repeat(innerWidth - 11) + chalk.cyan('║'));
196
+ console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM', innerWidth)) + chalk.cyan('║'));
197
+ console.log(chalk.cyan('') + ' '.repeat(innerWidth) + chalk.cyan(''));
198
+
199
+ const rows = Math.ceil(numbered.length / numCols);
200
+ for (let row = 0; row < rows; row++) {
201
+ let lineParts = [];
202
+ for (let col = 0; col < numCols; col++) {
203
+ const idx = row + col * rows;
204
+ if (idx < numbered.length) {
205
+ const item = numbered[idx];
206
+ const numStr = item.num.toString().padStart(2, ' ');
207
+ const namePadded = item.name.padEnd(maxNameLen);
208
+ lineParts.push({ num: `[${numStr}]`, name: namePadded });
209
+ } else {
210
+ lineParts.push(null);
211
+ }
212
+ }
213
+
214
+ let line = ' '.repeat(leftMargin);
215
+ for (let i = 0; i < lineParts.length; i++) {
216
+ if (lineParts[i]) {
217
+ line += chalk.cyan(lineParts[i].num) + ' ' + chalk.white(lineParts[i].name);
218
+ } else {
219
+ line += ' '.repeat(4 + 1 + maxNameLen);
220
+ }
221
+ if (i < lineParts.length - 1) line += ' ';
222
+ }
223
+
224
+ const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
225
+ const rightPad = Math.max(0, innerWidth - lineLen);
226
+ console.log(chalk.cyan('║') + line + ' '.repeat(rightPad) + chalk.cyan('║'));
227
+ }
228
+
229
+ console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
230
+ console.log(chalk.cyan('║') + chalk.red(centerText('[X] Exit', innerWidth)) + chalk.cyan('║'));
189
231
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
190
232
 
191
- const input = await prompts.textInput(chalk.cyan('Select (1/X):'));
233
+ const input = await prompts.textInput(chalk.cyan('Select number (or X):'));
192
234
 
193
235
  if (!input || input.toLowerCase() === 'x') {
194
236
  console.log(chalk.gray('Goodbye!'));
195
237
  process.exit(0);
196
238
  }
197
239
 
198
- if (input === '1') {
199
- const service = await rithmicMenu();
200
- if (service) {
201
- currentService = service;
202
- await refreshStats();
240
+ const action = parseInt(input);
241
+ if (!isNaN(action) && action >= 1 && action <= numbered.length) {
242
+ const selectedPropfirm = numbered[action - 1];
243
+ const { loginPrompt } = require('./menus/connect');
244
+ const credentials = await loginPrompt(selectedPropfirm.name);
245
+
246
+ if (credentials) {
247
+ const spinner = ora({ text: 'Connecting to Rithmic...', color: 'yellow' }).start();
248
+ try {
249
+ const { RithmicService } = require('./services/rithmic');
250
+ const service = new RithmicService(selectedPropfirm.key);
251
+ const result = await service.login(credentials.username, credentials.password);
252
+
253
+ if (result.success) {
254
+ spinner.text = 'Fetching accounts...';
255
+ const accResult = await service.getTradingAccounts();
256
+ connections.add('rithmic', service, service.propfirm.name);
257
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
258
+ currentService = service;
259
+ await refreshStats();
260
+ await new Promise(r => setTimeout(r, 1500));
261
+ } else {
262
+ spinner.fail(result.error || 'Authentication failed');
263
+ await new Promise(r => setTimeout(r, 2000));
264
+ }
265
+ } catch (error) {
266
+ spinner.fail(`Connection error: ${error.message}`);
267
+ await new Promise(r => setTimeout(r, 2000));
268
+ }
203
269
  }
204
270
  }
205
271
  } else {
@@ -10,7 +10,7 @@ const PROPFIRMS = {
10
10
  apex_rithmic: {
11
11
  id: 'rithmic-apex',
12
12
  name: 'Apex',
13
- displayName: 'Apex (Rithmic)',
13
+ displayName: 'Apex',
14
14
  platform: 'Rithmic',
15
15
  rithmicSystem: 'Apex',
16
16
  wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443',
@@ -34,7 +34,7 @@ const PROPFIRMS = {
34
34
  bulenox_rithmic: {
35
35
  id: 'rithmic-bulenox',
36
36
  name: 'Bulenox',
37
- displayName: 'Bulenox (Rithmic)',
37
+ displayName: 'Bulenox',
38
38
  platform: 'Rithmic',
39
39
  rithmicSystem: 'Bulenox',
40
40
  wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443'
@@ -138,11 +138,23 @@ const PROPFIRMS = {
138
138
  };
139
139
 
140
140
  /**
141
- * PropFirm choices for menus
141
+ * PropFirm choices for menus (Apex first, 4PropTrader/10XFutures last, then alphabetical)
142
142
  */
143
143
  const PROPFIRM_CHOICES = Object.entries(PROPFIRMS)
144
144
  .map(([key, val]) => ({ name: val.displayName, value: key }))
145
- .sort((a, b) => a.name.localeCompare(b.name));
145
+ .sort((a, b) => {
146
+ // Apex always first
147
+ if (a.name === 'Apex') return -1;
148
+ if (b.name === 'Apex') return 1;
149
+ // 4PropTrader and 10XFutures always last
150
+ const lastItems = ['4PropTrader', '10XFutures'];
151
+ const aIsLast = lastItems.includes(a.name);
152
+ const bIsLast = lastItems.includes(b.name);
153
+ if (aIsLast && !bIsLast) return 1;
154
+ if (!aIsLast && bIsLast) return -1;
155
+ // Then alphabetical
156
+ return a.name.localeCompare(b.name);
157
+ });
146
158
 
147
159
  /**
148
160
  * Gets a PropFirm by key
@@ -70,8 +70,8 @@ const rithmicMenu = async () => {
70
70
  console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, innerWidth - lineLen)) + chalk.cyan('║'));
71
71
  }
72
72
 
73
- console.log(chalk.cyan('') + ' '.repeat(innerWidth) + chalk.cyan(''));
74
- console.log(chalk.cyan('║') + ' ' + chalk.red('[X] Back') + ' '.repeat(innerWidth - 10) + chalk.cyan('║'));
73
+ console.log(chalk.cyan('' + ''.repeat(innerWidth) + ''));
74
+ console.log(chalk.cyan('║') + chalk.red(centerText('[X] Exit', innerWidth)) + chalk.cyan('║'));
75
75
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
76
76
 
77
77
  const input = await prompts.textInput(chalk.cyan('Select number (or X):'));
@@ -116,14 +116,41 @@ const showAccounts = async (service) => {
116
116
  const pnlColor2 = pnl2 === null || pnl2 === undefined ? chalk.gray : (pnl2 >= 0 ? chalk.green : chalk.red);
117
117
  console.log(chalk.cyan('║') + fmtRow('P&L:', pnlColor1(pnlStr1), col1) + chalk.cyan('│') + (acc2 ? fmtRow('P&L:', pnlColor2(pnlStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
118
118
 
119
- // Status
120
- const status1 = ACCOUNT_STATUS[acc1.status] || { text: 'Unknown', color: 'gray' };
121
- const status2 = acc2 ? (ACCOUNT_STATUS[acc2.status] || { text: 'Unknown', color: 'gray' }) : null;
119
+ // Status - handle both string from API and numeric lookup
120
+ const getStatusDisplay = (status) => {
121
+ if (!status && status !== 0) return { text: '--', color: 'gray' };
122
+ if (typeof status === 'string') {
123
+ // Direct string from Rithmic API (e.g., "Active", "Disabled")
124
+ const lowerStatus = status.toLowerCase();
125
+ if (lowerStatus.includes('active') || lowerStatus.includes('open')) return { text: status, color: 'green' };
126
+ if (lowerStatus.includes('disabled') || lowerStatus.includes('closed')) return { text: status, color: 'red' };
127
+ if (lowerStatus.includes('halt')) return { text: status, color: 'red' };
128
+ return { text: status, color: 'yellow' };
129
+ }
130
+ return ACCOUNT_STATUS[status] || { text: 'Unknown', color: 'gray' };
131
+ };
132
+ const status1 = getStatusDisplay(acc1.status);
133
+ const status2 = acc2 ? getStatusDisplay(acc2.status) : null;
122
134
  console.log(chalk.cyan('║') + fmtRow('Status:', chalk[status1.color](status1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Status:', chalk[status2.color](status2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
123
135
 
124
- // Type
125
- const type1 = ACCOUNT_TYPE[acc1.type] || { text: 'Unknown', color: 'white' };
126
- const type2 = acc2 ? (ACCOUNT_TYPE[acc2.type] || { text: 'Unknown', color: 'white' }) : null;
136
+ // Type/Algorithm - handle both string from API and numeric lookup
137
+ const getTypeDisplay = (type, algorithm) => {
138
+ // Prefer algorithm from RMS info if available
139
+ const value = algorithm || type;
140
+ if (!value && value !== 0) return { text: '--', color: 'gray' };
141
+ if (typeof value === 'string') {
142
+ // Direct string from Rithmic API
143
+ const lowerValue = value.toLowerCase();
144
+ if (lowerValue.includes('eval')) return { text: value, color: 'yellow' };
145
+ if (lowerValue.includes('live') || lowerValue.includes('funded')) return { text: value, color: 'green' };
146
+ if (lowerValue.includes('sim') || lowerValue.includes('demo')) return { text: value, color: 'gray' };
147
+ if (lowerValue.includes('express')) return { text: value, color: 'magenta' };
148
+ return { text: value, color: 'cyan' };
149
+ }
150
+ return ACCOUNT_TYPE[value] || { text: 'Unknown', color: 'white' };
151
+ };
152
+ const type1 = getTypeDisplay(acc1.type, acc1.algorithm);
153
+ const type2 = acc2 ? getTypeDisplay(acc2.type, acc2.algorithm) : null;
127
154
  console.log(chalk.cyan('║') + fmtRow('Type:', chalk[type1.color](type1.text), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Type:', chalk[type2.color](type2.text), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
128
155
 
129
156
  if (i + 2 < allAccounts.length) {
@@ -100,7 +100,7 @@ const renderQuantMetrics = (data) => {
100
100
  };
101
101
 
102
102
  /**
103
- * Render trades history section
103
+ * Render trades history section (round-trips)
104
104
  */
105
105
  const renderTradesHistory = (data) => {
106
106
  const { allTrades } = data;
@@ -108,60 +108,73 @@ const renderTradesHistory = (data) => {
108
108
  const innerWidth = boxWidth - 2;
109
109
 
110
110
  console.log();
111
- drawBoxHeader('TRADES HISTORY', boxWidth);
111
+ drawBoxHeader('TRADES HISTORY (Round-Trips)', boxWidth);
112
112
 
113
113
  const extractSymbol = (contractId) => {
114
114
  if (!contractId) return 'N/A';
115
- if (contractId.length <= 10) return contractId;
116
- return contractId.substring(0, 10);
115
+ if (contractId.length <= 8) return contractId;
116
+ return contractId.substring(0, 8);
117
117
  };
118
118
 
119
119
  if (allTrades.length > 0) {
120
- const colTime = 10;
121
- const colSymbol = 12;
122
- const colPrice = 12;
123
- const colPnl = 12;
120
+ const colSymbol = 9;
124
121
  const colSide = 6;
125
- const separators = 15;
126
- const fixedWidth = colTime + colSymbol + colPrice + colPnl + colSide + separators;
127
- const colAccount = Math.max(10, innerWidth - fixedWidth);
122
+ const colQty = 4;
123
+ const colEntry = 11;
124
+ const colExit = 11;
125
+ const colPnl = 10;
126
+ const separators = 18;
127
+ const fixedWidth = colSymbol + colSide + colQty + colEntry + colExit + colPnl + separators;
128
+ const colDate = Math.max(10, innerWidth - fixedWidth);
128
129
 
129
- const header = ` ${'Time'.padEnd(colTime)}| ${'Symbol'.padEnd(colSymbol)}| ${'Price'.padEnd(colPrice)}| ${'P&L'.padEnd(colPnl)}| ${'Side'.padEnd(colSide)}| ${'Account'.padEnd(colAccount - 2)}`;
130
+ const header = ` ${'Symbol'.padEnd(colSymbol)}| ${'Side'.padEnd(colSide)}| ${'Qty'.padEnd(colQty)}| ${'Entry'.padEnd(colEntry)}| ${'Exit'.padEnd(colExit)}| ${'P&L'.padEnd(colPnl)}| ${'Date'.padEnd(colDate - 2)}`;
130
131
  console.log(chalk.cyan('\u2551') + chalk.white(header) + chalk.cyan('\u2551'));
131
132
  console.log(chalk.cyan('\u2551') + chalk.gray('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2551'));
132
133
 
133
- const recentTrades = allTrades.slice(-10).reverse();
134
+ // Show most recent trades first (already sorted by exitTime desc)
135
+ const recentTrades = allTrades.slice(0, 10);
134
136
 
135
137
  for (const trade of recentTrades) {
136
- const timestamp = trade.creationTimestamp || trade.timestamp;
137
- const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
138
- const symbol = extractSymbol(trade.contractId || trade.symbol);
139
- const price = (trade.price || 0).toFixed(2);
140
- const pnl = trade.profitAndLoss || trade.pnl || 0;
138
+ const symbol = extractSymbol(trade.symbol);
139
+ // Round-trip: side=1 means Long (BUY then SELL), side=2 means Short (SELL then BUY)
140
+ const side = trade.side === 1 ? 'LONG' : trade.side === 2 ? 'SHORT' : 'N/A';
141
+ const qty = String(trade.quantity || 1);
142
+ const entry = (trade.entryPrice || 0).toFixed(2);
143
+ const exit = (trade.exitPrice || 0).toFixed(2);
144
+ const pnl = trade.pnl || trade.profitAndLoss || 0;
141
145
  const pnlText = pnl >= 0 ? `+$${pnl.toFixed(0)}` : `-$${Math.abs(pnl).toFixed(0)}`;
142
- const side = trade.side === 0 ? 'BUY' : trade.side === 1 ? 'SELL' : 'N/A';
143
- const accountName = (trade.accountName || 'N/A').substring(0, colAccount - 3);
144
146
 
145
- const timeStr = time.padEnd(colTime);
147
+ // Format date from exitDate (YYYYMMDD) or exitTime
148
+ let dateStr = '--/--';
149
+ if (trade.exitDate) {
150
+ const d = trade.exitDate;
151
+ dateStr = `${d.slice(4,6)}/${d.slice(6,8)}`;
152
+ } else if (trade.exitTime) {
153
+ const dt = new Date(trade.exitTime);
154
+ dateStr = `${(dt.getMonth()+1).toString().padStart(2,'0')}/${dt.getDate().toString().padStart(2,'0')}`;
155
+ }
156
+
146
157
  const symbolStr = symbol.padEnd(colSymbol);
147
- const priceStr = price.padEnd(colPrice);
148
- const pnlStr = pnlText.padEnd(colPnl);
149
158
  const sideStr = side.padEnd(colSide);
150
- const accountStr = accountName.padEnd(colAccount - 2);
159
+ const qtyStr = qty.padEnd(colQty);
160
+ const entryStr = entry.padEnd(colEntry);
161
+ const exitStr = exit.padEnd(colExit);
162
+ const pnlStr = pnlText.padEnd(colPnl);
163
+ const dateStrPad = dateStr.padEnd(colDate - 2);
151
164
 
152
165
  const pnlColored = pnl >= 0 ? chalk.green(pnlStr) : chalk.red(pnlStr);
153
- const sideColored = trade.side === 0 ? chalk.green(sideStr) : chalk.red(sideStr);
166
+ const sideColored = trade.side === 1 ? chalk.green(sideStr) : chalk.red(sideStr);
154
167
 
155
- const row = ` ${timeStr}| ${symbolStr}| ${priceStr}| ${pnlColored}| ${sideColored}| ${accountStr}`;
168
+ const row = ` ${symbolStr}| ${sideColored}| ${qtyStr}| ${entryStr}| ${exitStr}| ${pnlColored}| ${dateStrPad}`;
156
169
  console.log(chalk.cyan('\u2551') + row + chalk.cyan('\u2551'));
157
170
  }
158
171
 
159
172
  if (allTrades.length > 10) {
160
- const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
173
+ const moreMsg = ` ... and ${allTrades.length - 10} more round-trips`;
161
174
  console.log(chalk.cyan('\u2551') + moreMsg.padEnd(innerWidth) + chalk.cyan('\u2551'));
162
175
  }
163
176
  } else {
164
- const msg = ' No trades found';
177
+ const msg = ' No round-trip trades found';
165
178
  console.log(chalk.cyan('\u2551') + chalk.gray(msg.padEnd(innerWidth)) + chalk.cyan('\u2551'));
166
179
  }
167
180
 
@@ -100,17 +100,25 @@ const aggregateStats = (activeAccounts, allTrades) => {
100
100
 
101
101
  for (const trade of allTrades) {
102
102
  const pnl = trade.profitAndLoss || trade.pnl || 0;
103
- const size = trade.size || trade.quantity || 1;
103
+ const size = trade.size || trade.fillSize || trade.quantity || 1;
104
+ // Rithmic: 1=BUY, 2=SELL. Other APIs: 0=BUY, 1=SELL
104
105
  const side = trade.side;
106
+ const isBuy = side === 0 || side === 1; // 0 or 1 = BUY depending on API
107
+ const isSell = side === 2 || (side === 1 && trade.connectionType !== 'rithmic'); // 2 = SELL for Rithmic
105
108
 
106
109
  stats.totalVolume += Math.abs(size);
107
110
 
108
- if (side === 0) {
111
+ // For Rithmic: 1=BUY (long), 2=SELL (short)
112
+ if (side === 1) {
109
113
  stats.longTrades++;
110
114
  if (pnl > 0) stats.longWins++;
111
- } else if (side === 1) {
115
+ } else if (side === 2) {
112
116
  stats.shortTrades++;
113
117
  if (pnl > 0) stats.shortWins++;
118
+ } else if (side === 0) {
119
+ // Other APIs: 0=BUY
120
+ stats.longTrades++;
121
+ if (pnl > 0) stats.longWins++;
114
122
  }
115
123
 
116
124
  if (pnl > 0) {
@@ -28,6 +28,108 @@ const hashAccountId = (str) => {
28
28
  return Math.abs(hash);
29
29
  };
30
30
 
31
+ /**
32
+ * Fetch account RMS info (status, limits) from ORDER_PLANT
33
+ * @param {RithmicService} service - The Rithmic service instance
34
+ * @param {string} accountId - The account ID to fetch RMS info for
35
+ */
36
+ const fetchAccountRmsInfo = async (service, accountId) => {
37
+ if (!service.orderConn || !service.loginInfo) {
38
+ debug('fetchAccountRmsInfo: no connection or loginInfo');
39
+ return null;
40
+ }
41
+
42
+ // Initialize map if needed
43
+ if (!service.accountRmsInfo) service.accountRmsInfo = new Map();
44
+
45
+ return new Promise((resolve) => {
46
+ const timeout = setTimeout(() => {
47
+ debug('fetchAccountRmsInfo: timeout for', accountId);
48
+ resolve(service.accountRmsInfo.get(accountId) || null);
49
+ }, 3000);
50
+
51
+ const onRmsInfo = (rmsInfo) => {
52
+ if (rmsInfo.accountId === accountId) {
53
+ debug('fetchAccountRmsInfo: received for', accountId);
54
+ clearTimeout(timeout);
55
+ service.removeListener('accountRmsInfoReceived', onRmsInfo);
56
+ resolve(rmsInfo);
57
+ }
58
+ };
59
+ service.on('accountRmsInfoReceived', onRmsInfo);
60
+
61
+ try {
62
+ debug('fetchAccountRmsInfo: sending RequestAccountRmsInfo for', accountId);
63
+ service.orderConn.send('RequestAccountRmsInfo', {
64
+ templateId: REQ.ACCOUNT_RMS,
65
+ userMsg: ['HQX'],
66
+ fcmId: service.loginInfo.fcmId,
67
+ ibId: service.loginInfo.ibId,
68
+ userType: 3, // USER_TYPE_TRADER
69
+ });
70
+ } catch (e) {
71
+ debug('fetchAccountRmsInfo: error', e.message);
72
+ clearTimeout(timeout);
73
+ service.removeListener('accountRmsInfoReceived', onRmsInfo);
74
+ resolve(null);
75
+ }
76
+ });
77
+ };
78
+
79
+ /**
80
+ * Fetch RMS info for all accounts
81
+ * @param {RithmicService} service - The Rithmic service instance
82
+ */
83
+ const fetchAllAccountsRmsInfo = async (service) => {
84
+ if (!service.orderConn || !service.loginInfo || service.accounts.length === 0) {
85
+ return;
86
+ }
87
+
88
+ debug('fetchAllAccountsRmsInfo: fetching for', service.accounts.length, 'accounts');
89
+
90
+ // Initialize map if needed
91
+ if (!service.accountRmsInfo) service.accountRmsInfo = new Map();
92
+
93
+ return new Promise((resolve) => {
94
+ let receivedCount = 0;
95
+ const expectedCount = service.accounts.length;
96
+
97
+ const timeout = setTimeout(() => {
98
+ debug('fetchAllAccountsRmsInfo: timeout, received', receivedCount, 'of', expectedCount);
99
+ service.removeListener('accountRmsInfoReceived', onRmsInfo);
100
+ resolve();
101
+ }, 5000);
102
+
103
+ const onRmsInfo = (rmsInfo) => {
104
+ receivedCount++;
105
+ debug('fetchAllAccountsRmsInfo: received', receivedCount, 'of', expectedCount);
106
+ if (receivedCount >= expectedCount) {
107
+ clearTimeout(timeout);
108
+ service.removeListener('accountRmsInfoReceived', onRmsInfo);
109
+ resolve();
110
+ }
111
+ };
112
+ service.on('accountRmsInfoReceived', onRmsInfo);
113
+
114
+ try {
115
+ // Request RMS info - one request returns all accounts
116
+ debug('fetchAllAccountsRmsInfo: sending RequestAccountRmsInfo');
117
+ service.orderConn.send('RequestAccountRmsInfo', {
118
+ templateId: REQ.ACCOUNT_RMS,
119
+ userMsg: ['HQX'],
120
+ fcmId: service.loginInfo.fcmId,
121
+ ibId: service.loginInfo.ibId,
122
+ userType: 3, // USER_TYPE_TRADER
123
+ });
124
+ } catch (e) {
125
+ debug('fetchAllAccountsRmsInfo: error', e.message);
126
+ clearTimeout(timeout);
127
+ service.removeListener('accountRmsInfoReceived', onRmsInfo);
128
+ resolve();
129
+ }
130
+ });
131
+ };
132
+
31
133
  /**
32
134
  * Fetch accounts from ORDER_PLANT
33
135
  * @param {RithmicService} service - The Rithmic service instance
@@ -103,12 +205,22 @@ const getTradingAccounts = async (service) => {
103
205
  await requestPnLSnapshot(service);
104
206
  }
105
207
 
208
+ // Fetch RMS info (status, limits) for all accounts
209
+ if (service.orderConn && service.accounts.length > 0) {
210
+ debug('Fetching account RMS info...');
211
+ await fetchAllAccountsRmsInfo(service);
212
+ }
213
+
106
214
  let tradingAccounts = service.accounts.map((acc) => {
107
215
  // Get P&L data from accountPnL map (populated by PNL_PLANT messages)
108
216
  const pnlData = service.accountPnL.get(acc.accountId) || {};
109
217
  debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
110
218
  debug(` accountPnL map size:`, service.accountPnL.size);
111
219
 
220
+ // Get RMS info (status) from accountRmsInfo map
221
+ const rmsInfo = service.accountRmsInfo?.get(acc.accountId) || {};
222
+ debug(`Account ${acc.accountId} rmsInfo:`, JSON.stringify(rmsInfo));
223
+
112
224
  // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
113
225
  const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
114
226
  const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
@@ -124,7 +236,12 @@ const getTradingAccounts = async (service) => {
124
236
  profitAndLoss: dayPnL !== null ? dayPnL : (openPnL !== null || closedPnL !== null ? (openPnL || 0) + (closedPnL || 0) : null),
125
237
  openPnL: openPnL,
126
238
  todayPnL: closedPnL,
127
- status: 0,
239
+ status: rmsInfo.status || null, // Real status from API
240
+ algorithm: rmsInfo.algorithm || null, // Trading algorithm/type
241
+ lossLimit: rmsInfo.lossLimit || null,
242
+ minAccountBalance: rmsInfo.minAccountBalance || null,
243
+ buyLimit: rmsInfo.buyLimit || null,
244
+ sellLimit: rmsInfo.sellLimit || null,
128
245
  platform: 'Rithmic',
129
246
  propfirm: service.propfirm.name,
130
247
  };
@@ -208,6 +325,8 @@ const getPositions = async (service) => {
208
325
  module.exports = {
209
326
  hashAccountId,
210
327
  fetchAccounts,
328
+ fetchAccountRmsInfo,
329
+ fetchAllAccountsRmsInfo,
211
330
  getTradingAccounts,
212
331
  requestPnLSnapshot,
213
332
  subscribePnLUpdates,
@@ -149,6 +149,8 @@ const PROTO_FILES = [
149
149
  'response_show_order_history.proto',
150
150
  'request_show_order_history_dates.proto',
151
151
  'response_show_order_history_dates.proto',
152
+ 'request_show_order_history_summary.proto',
153
+ 'response_show_order_history_summary.proto',
152
154
  'request_market_data_update.proto',
153
155
  'response_market_data_update.proto',
154
156
  'last_trade.proto',
@@ -163,6 +165,8 @@ const PROTO_FILES = [
163
165
  'response_product_codes.proto',
164
166
  'request_front_month_contract.proto',
165
167
  'response_front_month_contract.proto',
168
+ 'request_account_rms_info.proto',
169
+ 'response_account_rms_info.proto',
166
170
  ];
167
171
 
168
172
  // NO STATIC DATA - All contract/symbol info comes from Rithmic API
@@ -29,6 +29,10 @@ const createOrderHandler = (service) => {
29
29
  debug('Handling ACCOUNT_LIST (303)');
30
30
  handleAccountList(service, data);
31
31
  break;
32
+ case RES.ACCOUNT_RMS:
33
+ debug('Handling ACCOUNT_RMS (305)');
34
+ handleAccountRmsInfo(service, data);
35
+ break;
32
36
  case RES.TRADE_ROUTES:
33
37
  handleTradeRoutes(service, data);
34
38
  break;
@@ -211,6 +215,45 @@ const handleInstrumentPnLUpdate = (service, data) => {
211
215
  }
212
216
  };
213
217
 
218
+ /**
219
+ * Handle account RMS info response (status, limits, etc.)
220
+ */
221
+ const handleAccountRmsInfo = (service, data) => {
222
+ try {
223
+ const res = proto.decode('ResponseAccountRmsInfo', data);
224
+ debug('Decoded Account RMS Info:', JSON.stringify(res));
225
+
226
+ if (res.accountId) {
227
+ const rmsInfo = {
228
+ accountId: res.accountId,
229
+ status: res.status || null,
230
+ currency: res.currency || null,
231
+ algorithm: res.algorithm || null,
232
+ lossLimit: res.lossLimit || null,
233
+ minAccountBalance: res.minAccountBalance || null,
234
+ minMarginBalance: res.minMarginBalance || null,
235
+ buyLimit: res.buyLimit || null,
236
+ sellLimit: res.sellLimit || null,
237
+ maxOrderQuantity: res.maxOrderQuantity || null,
238
+ autoLiquidate: res.autoLiquidate || null,
239
+ autoLiquidateThreshold: res.autoLiquidateThreshold || null,
240
+ };
241
+ debug('Account RMS Info for', res.accountId, ':', rmsInfo);
242
+
243
+ // Store RMS info in service
244
+ if (!service.accountRmsInfo) service.accountRmsInfo = new Map();
245
+ service.accountRmsInfo.set(res.accountId, rmsInfo);
246
+
247
+ service.emit('accountRmsInfoReceived', rmsInfo);
248
+ } else if (res.rpCode?.[0] === '0') {
249
+ debug('Account RMS Info complete signal');
250
+ service.emit('accountRmsInfoComplete');
251
+ }
252
+ } catch (e) {
253
+ debug('Error decoding Account RMS Info:', e.message);
254
+ }
255
+ };
256
+
214
257
  /**
215
258
  * Handle exchange order notification (fills/trades)
216
259
  * NotifyType: 5 = FILL