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 +1 -1
- package/src/app.js +77 -11
- package/src/config/propfirms.js +16 -4
- package/src/menus/connect.js +2 -2
- package/src/pages/accounts.js +33 -6
- package/src/pages/stats/display.js +41 -28
- package/src/pages/stats/metrics.js +11 -3
- package/src/services/rithmic/accounts.js +120 -1
- package/src/services/rithmic/constants.js +4 -0
- package/src/services/rithmic/handlers.js +43 -0
- package/src/services/rithmic/index.js +27 -71
- package/src/services/rithmic/orders.js +97 -67
- package/src/services/rithmic/proto/request_account_rms_info.proto +20 -0
- package/src/services/rithmic/proto/response_account_rms_info.proto +61 -0
- package/src/services/rithmic/proto/response_show_order_history_summary.proto +5 -5
- package/src/services/rithmic/trades.js +334 -0
package/package.json
CHANGED
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
|
|
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('
|
|
186
|
-
console.log(chalk.cyan('
|
|
187
|
-
|
|
188
|
-
|
|
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 (
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 {
|
package/src/config/propfirms.js
CHANGED
|
@@ -10,7 +10,7 @@ const PROPFIRMS = {
|
|
|
10
10
|
apex_rithmic: {
|
|
11
11
|
id: 'rithmic-apex',
|
|
12
12
|
name: 'Apex',
|
|
13
|
-
displayName: 'Apex
|
|
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
|
|
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) =>
|
|
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
|
package/src/menus/connect.js
CHANGED
|
@@ -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('
|
|
74
|
-
console.log(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):'));
|
package/src/pages/accounts.js
CHANGED
|
@@ -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
|
|
121
|
-
|
|
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
|
|
126
|
-
|
|
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 <=
|
|
116
|
-
return contractId.substring(0,
|
|
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
|
|
121
|
-
const colSymbol = 12;
|
|
122
|
-
const colPrice = 12;
|
|
123
|
-
const colPnl = 12;
|
|
120
|
+
const colSymbol = 9;
|
|
124
121
|
const colSide = 6;
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
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 = ` ${'
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
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
|
-
|
|
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
|
|
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 ===
|
|
166
|
+
const sideColored = trade.side === 1 ? chalk.green(sideStr) : chalk.red(sideStr);
|
|
154
167
|
|
|
155
|
-
const row = ` ${
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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:
|
|
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
|