hedgequantx 1.2.34 → 1.2.36
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 +2 -1
- package/src/app.js +204 -6
- package/src/config/propfirms.js +8 -0
- package/src/services/rithmic/connection.js +203 -0
- package/src/services/rithmic/constants.js +156 -0
- package/src/services/rithmic/index.js +487 -0
- package/src/services/rithmic/proto/account_pnl_position_update.proto +59 -0
- package/src/services/rithmic/proto/base.proto +7 -0
- package/src/services/rithmic/proto/best_bid_offer.proto +39 -0
- package/src/services/rithmic/proto/exchange_order_notification.proto +140 -0
- package/src/services/rithmic/proto/instrument_pnl_position_update.proto +50 -0
- package/src/services/rithmic/proto/last_trade.proto +53 -0
- package/src/services/rithmic/proto/request_account_list.proto +20 -0
- package/src/services/rithmic/proto/request_cancel_all_orders.proto +15 -0
- package/src/services/rithmic/proto/request_heartbeat.proto +13 -0
- package/src/services/rithmic/proto/request_login.proto +28 -0
- package/src/services/rithmic/proto/request_login_info.proto +10 -0
- package/src/services/rithmic/proto/request_logout.proto +10 -0
- package/src/services/rithmic/proto/request_market_data_update.proto +42 -0
- package/src/services/rithmic/proto/request_new_order.proto +84 -0
- package/src/services/rithmic/proto/request_pnl_position_snapshot.proto +14 -0
- package/src/services/rithmic/proto/request_pnl_position_updates.proto +20 -0
- package/src/services/rithmic/proto/request_rithmic_system_info.proto +8 -0
- package/src/services/rithmic/proto/request_show_order_history.proto +16 -0
- package/src/services/rithmic/proto/request_show_order_history_dates.proto +10 -0
- package/src/services/rithmic/proto/request_show_order_history_summary.proto +14 -0
- package/src/services/rithmic/proto/request_show_orders.proto +14 -0
- package/src/services/rithmic/proto/request_subscribe_for_order_updates.proto +14 -0
- package/src/services/rithmic/proto/request_tick_bar_replay.proto +48 -0
- package/src/services/rithmic/proto/request_trade_routes.proto +11 -0
- package/src/services/rithmic/proto/response_account_list.proto +18 -0
- package/src/services/rithmic/proto/response_heartbeat.proto +14 -0
- package/src/services/rithmic/proto/response_login.proto +18 -0
- package/src/services/rithmic/proto/response_login_info.proto +24 -0
- package/src/services/rithmic/proto/response_logout.proto +11 -0
- package/src/services/rithmic/proto/response_market_data_update.proto +9 -0
- package/src/services/rithmic/proto/response_new_order.proto +18 -0
- package/src/services/rithmic/proto/response_pnl_position_snapshot.proto +11 -0
- package/src/services/rithmic/proto/response_pnl_position_updates.proto +11 -0
- package/src/services/rithmic/proto/response_rithmic_system_info.proto +12 -0
- package/src/services/rithmic/proto/response_show_order_history.proto +11 -0
- package/src/services/rithmic/proto/response_show_order_history_dates.proto +13 -0
- package/src/services/rithmic/proto/response_show_order_history_summary.proto +11 -0
- package/src/services/rithmic/proto/response_show_orders.proto +11 -0
- package/src/services/rithmic/proto/response_subscribe_for_order_updates.proto +11 -0
- package/src/services/rithmic/proto/response_tick_bar_replay.proto +40 -0
- package/src/services/rithmic/proto/response_trade_routes.proto +19 -0
- package/src/services/rithmic/proto/rithmic_order_notification.proto +124 -0
- package/src/services/rithmic/protobuf.js +259 -0
- package/src/services/tradovate/constants.js +109 -0
- package/src/services/tradovate/index.js +508 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hedgequantx",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.36",
|
|
4
4
|
"description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
|
|
5
5
|
"main": "src/app.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"figlet": "^1.7.0",
|
|
48
48
|
"inquirer": "^7.3.3",
|
|
49
49
|
"ora": "^5.4.1",
|
|
50
|
+
"protobufjs": "^8.0.0",
|
|
50
51
|
"ws": "^8.18.3"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/app.js
CHANGED
|
@@ -11,7 +11,9 @@ const { execSync } = require('child_process');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
13
|
const { ProjectXService, connections } = require('./services');
|
|
14
|
-
const {
|
|
14
|
+
const { RithmicService } = require('./services/rithmic');
|
|
15
|
+
const { TradovateService } = require('./services/tradovate');
|
|
16
|
+
const { PROPFIRM_CHOICES, getPropFirmsByPlatform, getPropFirm } = require('./config');
|
|
15
17
|
const { getDevice, getSeparator, printLogo, getLogoWidth, drawBoxHeader, drawBoxFooter, centerText, createBoxMenu } = require('./ui');
|
|
16
18
|
const { validateUsername, validatePassword, maskSensitive } = require('./security');
|
|
17
19
|
|
|
@@ -22,6 +24,7 @@ const { algoTradingMenu } = require('./pages/algo');
|
|
|
22
24
|
|
|
23
25
|
// Current service reference
|
|
24
26
|
let currentService = null;
|
|
27
|
+
let currentPlatform = null; // 'projectx' or 'rithmic'
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Displays the application banner with stats if connected
|
|
@@ -257,6 +260,189 @@ const projectXMenu = async () => {
|
|
|
257
260
|
await service.getUser();
|
|
258
261
|
connections.add('projectx', service, service.propfirm.name);
|
|
259
262
|
currentService = service;
|
|
263
|
+
currentPlatform = 'projectx';
|
|
264
|
+
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
265
|
+
return service;
|
|
266
|
+
} else {
|
|
267
|
+
spinner.fail(result.error || 'Authentication failed');
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
spinner.fail(error.message);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Rithmic platform connection menu
|
|
278
|
+
*/
|
|
279
|
+
const rithmicMenu = async () => {
|
|
280
|
+
const propfirms = getPropFirmsByPlatform('Rithmic');
|
|
281
|
+
const boxWidth = getLogoWidth();
|
|
282
|
+
const innerWidth = boxWidth - 2;
|
|
283
|
+
const numCols = 3;
|
|
284
|
+
const colWidth = Math.floor(innerWidth / numCols);
|
|
285
|
+
|
|
286
|
+
// Build numbered list
|
|
287
|
+
const numbered = propfirms.map((pf, i) => ({
|
|
288
|
+
num: i + 1,
|
|
289
|
+
key: pf.key,
|
|
290
|
+
name: pf.displayName,
|
|
291
|
+
systemName: pf.rithmicSystem
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
// PropFirm selection box
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
297
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (RITHMIC)', innerWidth)) + chalk.cyan('║'));
|
|
298
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
299
|
+
|
|
300
|
+
// Display in 3 columns with fixed width alignment
|
|
301
|
+
const rows = Math.ceil(numbered.length / numCols);
|
|
302
|
+
|
|
303
|
+
for (let row = 0; row < rows; row++) {
|
|
304
|
+
let line = '';
|
|
305
|
+
for (let col = 0; col < numCols; col++) {
|
|
306
|
+
const idx = row + col * rows;
|
|
307
|
+
if (idx < numbered.length) {
|
|
308
|
+
const item = numbered[idx];
|
|
309
|
+
const numStr = item.num.toString().padStart(2, ' ');
|
|
310
|
+
const coloredText = chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
311
|
+
const textLen = 4 + 1 + item.name.length;
|
|
312
|
+
const padding = colWidth - textLen - 2;
|
|
313
|
+
line += ' ' + coloredText + ' '.repeat(Math.max(0, padding));
|
|
314
|
+
} else {
|
|
315
|
+
line += ' '.repeat(colWidth);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const lineLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
319
|
+
const adjust = innerWidth - lineLen;
|
|
320
|
+
console.log(chalk.cyan('║') + line + ' '.repeat(Math.max(0, adjust)) + chalk.cyan('║'));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
324
|
+
const backText = ' ' + chalk.red('[X] Back');
|
|
325
|
+
const backLen = '[X] Back'.length + 2;
|
|
326
|
+
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
327
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
328
|
+
console.log();
|
|
329
|
+
|
|
330
|
+
const validInputs = numbered.map(n => n.num.toString());
|
|
331
|
+
validInputs.push('x', 'X');
|
|
332
|
+
|
|
333
|
+
const { action } = await inquirer.prompt([
|
|
334
|
+
{
|
|
335
|
+
type: 'input',
|
|
336
|
+
name: 'action',
|
|
337
|
+
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
338
|
+
validate: (input) => {
|
|
339
|
+
if (validInputs.includes(input)) return true;
|
|
340
|
+
return `Please enter 1-${numbered.length} or X`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
if (action.toLowerCase() === 'x') return null;
|
|
346
|
+
|
|
347
|
+
const selectedIdx = parseInt(action) - 1;
|
|
348
|
+
const selectedPropfirm = numbered[selectedIdx];
|
|
349
|
+
|
|
350
|
+
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
351
|
+
const spinner = ora('Connecting to Rithmic...').start();
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const service = new RithmicService(selectedPropfirm.key);
|
|
355
|
+
const result = await service.login(credentials.username, credentials.password);
|
|
356
|
+
|
|
357
|
+
if (result.success) {
|
|
358
|
+
spinner.text = 'Fetching accounts...';
|
|
359
|
+
await service.getTradingAccounts();
|
|
360
|
+
|
|
361
|
+
connections.add('rithmic', service, service.propfirm.name);
|
|
362
|
+
currentService = service;
|
|
363
|
+
currentPlatform = 'rithmic';
|
|
364
|
+
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
365
|
+
return service;
|
|
366
|
+
} else {
|
|
367
|
+
spinner.fail(result.error || 'Authentication failed');
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
spinner.fail(error.message);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Tradovate platform connection menu
|
|
378
|
+
*/
|
|
379
|
+
const tradovateMenu = async () => {
|
|
380
|
+
const propfirms = getPropFirmsByPlatform('Tradovate');
|
|
381
|
+
const boxWidth = getLogoWidth();
|
|
382
|
+
const innerWidth = boxWidth - 2;
|
|
383
|
+
|
|
384
|
+
// Build numbered list
|
|
385
|
+
const numbered = propfirms.map((pf, i) => ({
|
|
386
|
+
num: i + 1,
|
|
387
|
+
key: pf.key,
|
|
388
|
+
name: pf.displayName
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
// PropFirm selection box
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
394
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PROPFIRM (TRADOVATE)', innerWidth)) + chalk.cyan('║'));
|
|
395
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
396
|
+
|
|
397
|
+
// Display propfirms
|
|
398
|
+
for (const item of numbered) {
|
|
399
|
+
const numStr = item.num.toString().padStart(2, ' ');
|
|
400
|
+
const text = ' ' + chalk.cyan(`[${numStr}]`) + ' ' + chalk.white(item.name);
|
|
401
|
+
const textLen = 4 + 1 + item.name.length + 2;
|
|
402
|
+
console.log(chalk.cyan('║') + text + ' '.repeat(innerWidth - textLen) + chalk.cyan('║'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(chalk.cyan('║') + ' '.repeat(innerWidth) + chalk.cyan('║'));
|
|
406
|
+
const backText = ' ' + chalk.red('[X] Back');
|
|
407
|
+
const backLen = '[X] Back'.length + 2;
|
|
408
|
+
console.log(chalk.cyan('║') + backText + ' '.repeat(innerWidth - backLen) + chalk.cyan('║'));
|
|
409
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
410
|
+
console.log();
|
|
411
|
+
|
|
412
|
+
const validInputs = numbered.map(n => n.num.toString());
|
|
413
|
+
validInputs.push('x', 'X');
|
|
414
|
+
|
|
415
|
+
const { action } = await inquirer.prompt([
|
|
416
|
+
{
|
|
417
|
+
type: 'input',
|
|
418
|
+
name: 'action',
|
|
419
|
+
message: chalk.cyan(`Enter choice (1-${numbered.length}/X):`),
|
|
420
|
+
validate: (input) => {
|
|
421
|
+
if (validInputs.includes(input)) return true;
|
|
422
|
+
return `Please enter 1-${numbered.length} or X`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
if (action.toLowerCase() === 'x') return null;
|
|
428
|
+
|
|
429
|
+
const selectedIdx = parseInt(action) - 1;
|
|
430
|
+
const selectedPropfirm = numbered[selectedIdx];
|
|
431
|
+
|
|
432
|
+
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
433
|
+
const spinner = ora('Connecting to Tradovate...').start();
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const service = new TradovateService(selectedPropfirm.key);
|
|
437
|
+
const result = await service.login(credentials.username, credentials.password);
|
|
438
|
+
|
|
439
|
+
if (result.success) {
|
|
440
|
+
spinner.text = 'Fetching accounts...';
|
|
441
|
+
await service.getTradingAccounts();
|
|
442
|
+
|
|
443
|
+
connections.add('tradovate', service, service.propfirm.name);
|
|
444
|
+
currentService = service;
|
|
445
|
+
currentPlatform = 'tradovate';
|
|
260
446
|
spinner.succeed(`Connected to ${service.propfirm.name}`);
|
|
261
447
|
return service;
|
|
262
448
|
} else {
|
|
@@ -294,8 +480,8 @@ const mainMenu = async () => {
|
|
|
294
480
|
console.log(chalk.cyan('║') + leftText + ' '.repeat(Math.max(0, leftPad)) + rightText + ' '.repeat(Math.max(0, rightPad)) + chalk.cyan('║'));
|
|
295
481
|
};
|
|
296
482
|
|
|
297
|
-
menuRow(chalk.cyan('[1] ProjectX'), chalk.
|
|
298
|
-
menuRow(chalk.
|
|
483
|
+
menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
|
|
484
|
+
menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Exit'));
|
|
299
485
|
|
|
300
486
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
301
487
|
console.log();
|
|
@@ -304,11 +490,11 @@ const mainMenu = async () => {
|
|
|
304
490
|
{
|
|
305
491
|
type: 'input',
|
|
306
492
|
name: 'action',
|
|
307
|
-
message: chalk.cyan('Enter choice (1/X):'),
|
|
493
|
+
message: chalk.cyan('Enter choice (1/2/3/X):'),
|
|
308
494
|
validate: (input) => {
|
|
309
|
-
const valid = ['1', 'x', 'X'];
|
|
495
|
+
const valid = ['1', '2', '3', 'x', 'X'];
|
|
310
496
|
if (valid.includes(input)) return true;
|
|
311
|
-
return 'Please enter 1 or X';
|
|
497
|
+
return 'Please enter 1, 2, 3 or X';
|
|
312
498
|
}
|
|
313
499
|
}
|
|
314
500
|
]);
|
|
@@ -316,6 +502,8 @@ const mainMenu = async () => {
|
|
|
316
502
|
// Map input to action
|
|
317
503
|
const actionMap = {
|
|
318
504
|
'1': 'projectx',
|
|
505
|
+
'2': 'rithmic',
|
|
506
|
+
'3': 'tradovate',
|
|
319
507
|
'x': 'exit',
|
|
320
508
|
'X': 'exit'
|
|
321
509
|
};
|
|
@@ -512,6 +700,16 @@ const run = async () => {
|
|
|
512
700
|
const service = await projectXMenu();
|
|
513
701
|
if (service) currentService = service;
|
|
514
702
|
}
|
|
703
|
+
|
|
704
|
+
if (choice === 'rithmic') {
|
|
705
|
+
const service = await rithmicMenu();
|
|
706
|
+
if (service) currentService = service;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (choice === 'tradovate') {
|
|
710
|
+
const service = await tradovateMenu();
|
|
711
|
+
if (service) currentService = service;
|
|
712
|
+
}
|
|
515
713
|
} else {
|
|
516
714
|
const action = await dashboardMenu(currentService);
|
|
517
715
|
|
package/src/config/propfirms.js
CHANGED
|
@@ -179,6 +179,14 @@ const PROPFIRMS = {
|
|
|
179
179
|
userApi: 'userapi.takeprofittrader.tradovate.com',
|
|
180
180
|
gatewayApi: 'api.takeprofittrader.tradovate.com'
|
|
181
181
|
},
|
|
182
|
+
myfundedfutures: {
|
|
183
|
+
id: 'myfundedfutures',
|
|
184
|
+
name: 'MyFundedFutures',
|
|
185
|
+
displayName: 'MyFundedFutures',
|
|
186
|
+
platform: 'Tradovate',
|
|
187
|
+
userApi: 'live.tradovateapi.com',
|
|
188
|
+
gatewayApi: 'live.tradovateapi.com'
|
|
189
|
+
},
|
|
182
190
|
|
|
183
191
|
// ==================== Rithmic Platform ====================
|
|
184
192
|
apex_rithmic: {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Connection Manager
|
|
3
|
+
* Handles WebSocket connection and heartbeat
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
const EventEmitter = require('events');
|
|
8
|
+
const { proto } = require('./protobuf');
|
|
9
|
+
const { REQ, RES, INFRA_TYPE } = require('./constants');
|
|
10
|
+
|
|
11
|
+
class RithmicConnection extends EventEmitter {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.ws = null;
|
|
15
|
+
this.config = null;
|
|
16
|
+
this.state = 'DISCONNECTED';
|
|
17
|
+
this.heartbeatTimer = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get isConnected() {
|
|
21
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get connectionState() {
|
|
25
|
+
return this.state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Connect to Rithmic server
|
|
30
|
+
*/
|
|
31
|
+
async connect(config) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.state = 'CONNECTING';
|
|
34
|
+
|
|
35
|
+
await proto.load();
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
this.ws = new WebSocket(config.uri, { rejectUnauthorized: false });
|
|
39
|
+
|
|
40
|
+
this.ws.on('open', () => {
|
|
41
|
+
this.state = 'CONNECTED';
|
|
42
|
+
this.emit('connected');
|
|
43
|
+
resolve(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.ws.on('message', (data) => {
|
|
47
|
+
this.handleMessage(data);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.ws.on('error', (err) => {
|
|
51
|
+
this.state = 'ERROR';
|
|
52
|
+
this.emit('error', err);
|
|
53
|
+
reject(err);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.ws.on('close', (code, reason) => {
|
|
57
|
+
this.state = 'DISCONNECTED';
|
|
58
|
+
this.stopHeartbeat();
|
|
59
|
+
this.emit('disconnected', { code, reason: reason?.toString() });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Timeout
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
if (this.state === 'CONNECTING') {
|
|
65
|
+
reject(new Error('Connection timeout'));
|
|
66
|
+
}
|
|
67
|
+
}, 15000);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Disconnect
|
|
73
|
+
*/
|
|
74
|
+
async disconnect() {
|
|
75
|
+
this.stopHeartbeat();
|
|
76
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
77
|
+
this.send('RequestLogout', { templateId: REQ.LOGOUT, userMsg: ['HQX'] });
|
|
78
|
+
this.ws.close(1000, 'bye');
|
|
79
|
+
}
|
|
80
|
+
this.ws = null;
|
|
81
|
+
this.state = 'DISCONNECTED';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Send a protobuf message
|
|
86
|
+
*/
|
|
87
|
+
send(typeName, data) {
|
|
88
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
89
|
+
throw new Error('Not connected');
|
|
90
|
+
}
|
|
91
|
+
const buffer = proto.encode(typeName, data);
|
|
92
|
+
this.ws.send(buffer);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Login to system
|
|
97
|
+
*/
|
|
98
|
+
login(infraType = 'ORDER_PLANT') {
|
|
99
|
+
if (!this.config) throw new Error('No config');
|
|
100
|
+
|
|
101
|
+
this.send('RequestLogin', {
|
|
102
|
+
templateId: REQ.LOGIN,
|
|
103
|
+
templateVersion: '3.9',
|
|
104
|
+
userMsg: ['HQX'],
|
|
105
|
+
user: this.config.userId,
|
|
106
|
+
password: this.config.password,
|
|
107
|
+
appName: this.config.appName || 'HQX-CLI',
|
|
108
|
+
appVersion: this.config.appVersion || '1.0.0',
|
|
109
|
+
systemName: this.config.systemName,
|
|
110
|
+
infraType: INFRA_TYPE[infraType],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List available systems
|
|
116
|
+
*/
|
|
117
|
+
listSystems() {
|
|
118
|
+
this.send('RequestRithmicSystemInfo', {
|
|
119
|
+
templateId: REQ.SYSTEM_INFO,
|
|
120
|
+
userMsg: ['HQX'],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle incoming message
|
|
126
|
+
*/
|
|
127
|
+
handleMessage(data) {
|
|
128
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
129
|
+
const templateId = proto.getTemplateId(buffer);
|
|
130
|
+
|
|
131
|
+
switch (templateId) {
|
|
132
|
+
case RES.LOGIN:
|
|
133
|
+
this.onLoginResponse(buffer);
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case RES.HEARTBEAT:
|
|
137
|
+
// OK
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case RES.SYSTEM_INFO:
|
|
141
|
+
this.onSystemInfo(buffer);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
// Forward to listeners
|
|
146
|
+
this.emit('message', { templateId, data: buffer });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onLoginResponse(data) {
|
|
151
|
+
try {
|
|
152
|
+
const res = proto.decode('ResponseLogin', data);
|
|
153
|
+
|
|
154
|
+
if (res.rpCode?.[0] === '0') {
|
|
155
|
+
this.state = 'LOGGED_IN';
|
|
156
|
+
this.startHeartbeat(res.heartbeatInterval || 60);
|
|
157
|
+
this.emit('loggedIn', {
|
|
158
|
+
fcmId: res.fcmId,
|
|
159
|
+
ibId: res.ibId,
|
|
160
|
+
heartbeatInterval: res.heartbeatInterval,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
const errorCode = res.rpCode?.[0] || 'UNKNOWN';
|
|
164
|
+
const errorMsg = res.rpCode?.[1] || 'Login failed';
|
|
165
|
+
this.emit('loginFailed', { code: errorCode, message: errorMsg });
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
this.emit('loginFailed', { code: 'DECODE_ERROR', message: e.message });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onSystemInfo(data) {
|
|
173
|
+
try {
|
|
174
|
+
const res = proto.decode('ResponseRithmicSystemInfo', data);
|
|
175
|
+
|
|
176
|
+
if (res.rpCode?.[0] === '0') {
|
|
177
|
+
this.emit('systems', res.systemName || []);
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
startHeartbeat(intervalSec) {
|
|
185
|
+
this.stopHeartbeat();
|
|
186
|
+
this.heartbeatTimer = setInterval(() => {
|
|
187
|
+
try {
|
|
188
|
+
this.send('RequestHeartbeat', { templateId: REQ.HEARTBEAT });
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Ignore
|
|
191
|
+
}
|
|
192
|
+
}, (intervalSec - 5) * 1000);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
stopHeartbeat() {
|
|
196
|
+
if (this.heartbeatTimer) {
|
|
197
|
+
clearInterval(this.heartbeatTimer);
|
|
198
|
+
this.heartbeatTimer = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { RithmicConnection };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Endpoints
|
|
6
|
+
const RITHMIC_ENDPOINTS = {
|
|
7
|
+
TEST: 'wss://rituz00100.rithmic.com:443',
|
|
8
|
+
PAPER: 'wss://ritpa11120.11.rithmic.com:443',
|
|
9
|
+
LIVE: 'wss://ritpz01000.01.rithmic.com:443',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// System names for PropFirms
|
|
13
|
+
const RITHMIC_SYSTEMS = {
|
|
14
|
+
TEST: 'Rithmic Test',
|
|
15
|
+
PAPER: 'Rithmic Paper Trading',
|
|
16
|
+
APEX: 'Apex',
|
|
17
|
+
TOPSTEP: 'TopstepTrader',
|
|
18
|
+
MES_CAPITAL: 'MES Capital',
|
|
19
|
+
BULENOX: 'Bulenox',
|
|
20
|
+
TRADEFUNDRR: 'TradeFundrr',
|
|
21
|
+
THE_TRADING_PIT: 'TheTradingPit',
|
|
22
|
+
FUNDED_FUTURES_NETWORK: 'FundedFuturesNetwork',
|
|
23
|
+
PROPSHOP_TRADER: 'PropShopTrader',
|
|
24
|
+
FOUR_PROP_TRADER: '4PropTrader',
|
|
25
|
+
DAY_TRADERS: 'DayTraders.com',
|
|
26
|
+
TEN_X_FUTURES: '10XFutures',
|
|
27
|
+
LUCID_TRADING: 'LucidTrading',
|
|
28
|
+
THRIVE_TRADING: 'ThriveTrading',
|
|
29
|
+
LEGENDS_TRADING: 'LegendsTrading',
|
|
30
|
+
EARN_2_TRADE: 'Earn2Trade',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Infrastructure types
|
|
34
|
+
const INFRA_TYPE = {
|
|
35
|
+
TICKER_PLANT: 1,
|
|
36
|
+
ORDER_PLANT: 2,
|
|
37
|
+
HISTORY_PLANT: 3,
|
|
38
|
+
PNL_PLANT: 4,
|
|
39
|
+
REPOSITORY_PLANT: 5,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Request template IDs
|
|
43
|
+
const REQ = {
|
|
44
|
+
LOGIN: 10,
|
|
45
|
+
LOGOUT: 12,
|
|
46
|
+
SYSTEM_INFO: 16,
|
|
47
|
+
HEARTBEAT: 18,
|
|
48
|
+
MARKET_DATA: 100,
|
|
49
|
+
LOGIN_INFO: 300,
|
|
50
|
+
ACCOUNT_LIST: 302,
|
|
51
|
+
ACCOUNT_RMS: 304,
|
|
52
|
+
PRODUCT_RMS: 306,
|
|
53
|
+
ORDER_UPDATES: 308,
|
|
54
|
+
TRADE_ROUTES: 310,
|
|
55
|
+
NEW_ORDER: 312,
|
|
56
|
+
MODIFY_ORDER: 314,
|
|
57
|
+
CANCEL_ORDER: 316,
|
|
58
|
+
SHOW_ORDER_HISTORY_DATES: 318,
|
|
59
|
+
SHOW_ORDERS: 320,
|
|
60
|
+
SHOW_ORDER_HISTORY: 324,
|
|
61
|
+
BRACKET_ORDER: 330,
|
|
62
|
+
CANCEL_ALL_ORDERS: 346,
|
|
63
|
+
EXIT_POSITION: 3504,
|
|
64
|
+
PNL_POSITION_SNAPSHOT: 400,
|
|
65
|
+
PNL_POSITION_UPDATES: 402,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Response template IDs
|
|
69
|
+
const RES = {
|
|
70
|
+
LOGIN: 11,
|
|
71
|
+
LOGOUT: 13,
|
|
72
|
+
SYSTEM_INFO: 17,
|
|
73
|
+
HEARTBEAT: 19,
|
|
74
|
+
MARKET_DATA: 101,
|
|
75
|
+
LOGIN_INFO: 301,
|
|
76
|
+
ACCOUNT_LIST: 303,
|
|
77
|
+
ACCOUNT_RMS: 305,
|
|
78
|
+
PRODUCT_RMS: 307,
|
|
79
|
+
ORDER_UPDATES: 309,
|
|
80
|
+
TRADE_ROUTES: 311,
|
|
81
|
+
NEW_ORDER: 313,
|
|
82
|
+
MODIFY_ORDER: 315,
|
|
83
|
+
CANCEL_ORDER: 317,
|
|
84
|
+
SHOW_ORDER_HISTORY_DATES: 319,
|
|
85
|
+
SHOW_ORDERS: 321,
|
|
86
|
+
SHOW_ORDER_HISTORY: 325,
|
|
87
|
+
BRACKET_ORDER: 331,
|
|
88
|
+
CANCEL_ALL_ORDERS: 347,
|
|
89
|
+
EXIT_POSITION: 3505,
|
|
90
|
+
PNL_POSITION_SNAPSHOT: 401,
|
|
91
|
+
PNL_POSITION_UPDATES: 403,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Streaming template IDs
|
|
95
|
+
const STREAM = {
|
|
96
|
+
LAST_TRADE: 150,
|
|
97
|
+
BBO: 151,
|
|
98
|
+
TRADE_ROUTE_UPDATE: 350,
|
|
99
|
+
ORDER_NOTIFICATION: 351,
|
|
100
|
+
EXCHANGE_NOTIFICATION: 352,
|
|
101
|
+
BRACKET_UPDATE: 353,
|
|
102
|
+
INSTRUMENT_PNL_UPDATE: 450,
|
|
103
|
+
ACCOUNT_PNL_UPDATE: 451,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Proto files to load
|
|
107
|
+
const PROTO_FILES = [
|
|
108
|
+
'base.proto',
|
|
109
|
+
'request_heartbeat.proto',
|
|
110
|
+
'response_heartbeat.proto',
|
|
111
|
+
'request_rithmic_system_info.proto',
|
|
112
|
+
'response_rithmic_system_info.proto',
|
|
113
|
+
'request_login.proto',
|
|
114
|
+
'response_login.proto',
|
|
115
|
+
'request_logout.proto',
|
|
116
|
+
'response_logout.proto',
|
|
117
|
+
'request_login_info.proto',
|
|
118
|
+
'response_login_info.proto',
|
|
119
|
+
'request_account_list.proto',
|
|
120
|
+
'response_account_list.proto',
|
|
121
|
+
'request_trade_routes.proto',
|
|
122
|
+
'response_trade_routes.proto',
|
|
123
|
+
'request_subscribe_for_order_updates.proto',
|
|
124
|
+
'response_subscribe_for_order_updates.proto',
|
|
125
|
+
'request_new_order.proto',
|
|
126
|
+
'response_new_order.proto',
|
|
127
|
+
'request_cancel_all_orders.proto',
|
|
128
|
+
'rithmic_order_notification.proto',
|
|
129
|
+
'exchange_order_notification.proto',
|
|
130
|
+
'request_show_orders.proto',
|
|
131
|
+
'response_show_orders.proto',
|
|
132
|
+
'request_show_order_history.proto',
|
|
133
|
+
'response_show_order_history.proto',
|
|
134
|
+
'request_show_order_history_dates.proto',
|
|
135
|
+
'response_show_order_history_dates.proto',
|
|
136
|
+
'request_market_data_update.proto',
|
|
137
|
+
'response_market_data_update.proto',
|
|
138
|
+
'last_trade.proto',
|
|
139
|
+
'best_bid_offer.proto',
|
|
140
|
+
'request_pnl_position_snapshot.proto',
|
|
141
|
+
'response_pnl_position_snapshot.proto',
|
|
142
|
+
'request_pnl_position_updates.proto',
|
|
143
|
+
'response_pnl_position_updates.proto',
|
|
144
|
+
'account_pnl_position_update.proto',
|
|
145
|
+
'instrument_pnl_position_update.proto',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
RITHMIC_ENDPOINTS,
|
|
150
|
+
RITHMIC_SYSTEMS,
|
|
151
|
+
INFRA_TYPE,
|
|
152
|
+
REQ,
|
|
153
|
+
RES,
|
|
154
|
+
STREAM,
|
|
155
|
+
PROTO_FILES,
|
|
156
|
+
};
|