hedgequantx 1.2.41 → 1.2.43

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": "1.2.41",
3
+ "version": "1.2.43",
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": {
package/src/app.js CHANGED
@@ -359,19 +359,24 @@ const rithmicMenu = async () => {
359
359
 
360
360
  if (result.success) {
361
361
  spinner.text = 'Fetching accounts...';
362
- await service.getTradingAccounts();
362
+ const accResult = await service.getTradingAccounts();
363
363
 
364
364
  connections.add('rithmic', service, service.propfirm.name);
365
365
  currentService = service;
366
366
  currentPlatform = 'rithmic';
367
- spinner.succeed(`Connected to ${service.propfirm.name}`);
367
+ spinner.succeed(`Connected to ${service.propfirm.name} (${accResult.accounts?.length || 0} accounts)`);
368
+
369
+ // Small pause to see the success message
370
+ await new Promise(r => setTimeout(r, 1500));
368
371
  return service;
369
372
  } else {
370
373
  spinner.fail(result.error || 'Authentication failed');
374
+ await new Promise(r => setTimeout(r, 2000));
371
375
  return null;
372
376
  }
373
377
  } catch (error) {
374
- spinner.fail(error.message);
378
+ spinner.fail(`Connection error: ${error.message}`);
379
+ await new Promise(r => setTimeout(r, 2000));
375
380
  return null;
376
381
  }
377
382
  };
@@ -583,10 +588,14 @@ const dashboardMenu = async (service) => {
583
588
  console.log(chalk.cyan('║') + chalk.white.bold(centerText('DASHBOARD', innerWidth)) + chalk.cyan('║'));
584
589
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
585
590
 
586
- // Connection info
587
- const connInfo = chalk.green('Connected to ' + service.propfirm.name);
588
- const connLen = ('Connected to ' + service.propfirm.name).length;
589
- console.log(chalk.cyan('║') + ' ' + connInfo + ' '.repeat(innerWidth - connLen - 2) + chalk.cyan(''));
591
+ // Connection info - show all active connections
592
+ const allConns = connections.getAll();
593
+ if (allConns.length > 0) {
594
+ const connNames = allConns.map(c => c.propfirm || c.type).join(', ');
595
+ const connText = `Connected to ${connNames}`;
596
+ const connInfo = chalk.green(connText);
597
+ console.log(chalk.cyan('║') + ' ' + connInfo + ' '.repeat(Math.max(0, innerWidth - connText.length - 2)) + chalk.cyan('║'));
598
+ }
590
599
 
591
600
  if (user) {
592
601
  const userInfo = 'Welcome, ' + user.userName.toUpperCase() + '!';
@@ -61,8 +61,8 @@ const REQ = {
61
61
  BRACKET_ORDER: 330,
62
62
  CANCEL_ALL_ORDERS: 346,
63
63
  EXIT_POSITION: 3504,
64
- PNL_POSITION_SNAPSHOT: 400,
65
- PNL_POSITION_UPDATES: 402,
64
+ PNL_POSITION_UPDATES: 400,
65
+ PNL_POSITION_SNAPSHOT: 402,
66
66
  };
67
67
 
68
68
  // Response template IDs
@@ -87,8 +87,8 @@ const RES = {
87
87
  BRACKET_ORDER: 331,
88
88
  CANCEL_ALL_ORDERS: 347,
89
89
  EXIT_POSITION: 3505,
90
- PNL_POSITION_SNAPSHOT: 401,
91
- PNL_POSITION_UPDATES: 403,
90
+ PNL_POSITION_UPDATES: 401,
91
+ PNL_POSITION_SNAPSHOT: 403,
92
92
  };
93
93
 
94
94
  // Streaming template IDs
@@ -5,7 +5,7 @@
5
5
 
6
6
  const EventEmitter = require('events');
7
7
  const { RithmicConnection } = require('./connection');
8
- const { proto, decodeAccountPnL } = require('./protobuf');
8
+ const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
9
9
  const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ, RES, STREAM } = require('./constants');
10
10
 
11
11
  class RithmicService extends EventEmitter {
@@ -18,7 +18,10 @@ class RithmicService extends EventEmitter {
18
18
  this.loginInfo = null;
19
19
  this.accounts = [];
20
20
  this.accountPnL = new Map(); // accountId -> pnl data
21
+ this.positions = new Map(); // symbol -> position data (from InstrumentPnLPositionUpdate)
22
+ this.orders = []; // Active orders
21
23
  this.user = null;
24
+ this.credentials = null; // Store for PNL connection
22
25
  }
23
26
 
24
27
  /**
@@ -82,10 +85,37 @@ class RithmicService extends EventEmitter {
82
85
  try {
83
86
  await this.fetchAccounts();
84
87
  } catch (e) {
85
- // Accounts fetch failed, but login succeeded
86
- console.log('Note: Could not fetch accounts');
88
+ // Accounts fetch failed, ignore
87
89
  }
88
- resolve({ success: true });
90
+
91
+ // Create default account if none found
92
+ if (this.accounts.length === 0) {
93
+ this.accounts = [{
94
+ accountId: username,
95
+ accountName: username,
96
+ fcmId: data.fcmId,
97
+ ibId: data.ibId,
98
+ }];
99
+ }
100
+
101
+ // Store credentials for PNL connection
102
+ this.credentials = { username, password };
103
+
104
+ // Format accounts for response
105
+ const formattedAccounts = this.accounts.map(acc => ({
106
+ accountId: acc.accountId,
107
+ accountName: acc.accountName || acc.accountId,
108
+ balance: this.propfirm.defaultBalance,
109
+ startingBalance: this.propfirm.defaultBalance,
110
+ profitAndLoss: 0,
111
+ status: 0
112
+ }));
113
+
114
+ resolve({
115
+ success: true,
116
+ user: this.user,
117
+ accounts: formattedAccounts
118
+ });
89
119
  });
90
120
 
91
121
  this.orderConn.once('loginFailed', (data) => {
@@ -290,6 +320,9 @@ class RithmicService extends EventEmitter {
290
320
  case RES.TRADE_ROUTES:
291
321
  this.onTradeRoutes(data);
292
322
  break;
323
+ case RES.SHOW_ORDERS:
324
+ this.onShowOrdersResponse(data);
325
+ break;
293
326
  case STREAM.EXCHANGE_NOTIFICATION:
294
327
  this.onExchangeNotification(data);
295
328
  break;
@@ -299,6 +332,18 @@ class RithmicService extends EventEmitter {
299
332
  }
300
333
  }
301
334
 
335
+ onShowOrdersResponse(data) {
336
+ try {
337
+ const res = proto.decode('ResponseShowOrders', data);
338
+ if (res.rpCode?.[0] === '0') {
339
+ // End of orders list
340
+ this.emit('ordersReceived');
341
+ }
342
+ } catch (e) {
343
+ // Ignore
344
+ }
345
+ }
346
+
302
347
  /**
303
348
  * Handle PNL_PLANT messages
304
349
  */
@@ -386,7 +431,37 @@ class RithmicService extends EventEmitter {
386
431
  }
387
432
 
388
433
  onInstrumentPnLUpdate(data) {
389
- // Handle instrument-level PnL if needed
434
+ // Handle instrument-level PnL - this contains position data
435
+ try {
436
+ const pos = decodeInstrumentPnL(data);
437
+ if (pos.symbol && pos.accountId) {
438
+ const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
439
+ // Net quantity can come from netQuantity field or calculated from buy/sell
440
+ const netQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
441
+
442
+ if (netQty !== 0) {
443
+ // We have an open position
444
+ this.positions.set(key, {
445
+ accountId: pos.accountId,
446
+ symbol: pos.symbol,
447
+ exchange: pos.exchange || 'CME',
448
+ quantity: netQty,
449
+ averagePrice: pos.avgOpenFillPrice || 0,
450
+ openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
451
+ closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
452
+ dayPnl: parseFloat(pos.dayPnl || 0),
453
+ isSnapshot: pos.isSnapshot || false,
454
+ });
455
+ } else {
456
+ // Position closed
457
+ this.positions.delete(key);
458
+ }
459
+
460
+ this.emit('positionUpdate', this.positions.get(key));
461
+ }
462
+ } catch (e) {
463
+ // Ignore decode errors
464
+ }
390
465
  }
391
466
 
392
467
  onExchangeNotification(data) {
@@ -417,6 +492,133 @@ class RithmicService extends EventEmitter {
417
492
  return this.user;
418
493
  }
419
494
 
495
+ /**
496
+ * Get positions via PNL_PLANT
497
+ * Positions are streamed via InstrumentPnLPositionUpdate (template 450)
498
+ */
499
+ async getPositions() {
500
+ // If PNL connection not established, try to connect
501
+ if (!this.pnlConn && this.credentials) {
502
+ await this.connectPnL(this.credentials.username, this.credentials.password);
503
+ // Request snapshot to populate positions
504
+ await this.requestPnLSnapshot();
505
+ }
506
+
507
+ // Return cached positions
508
+ const positions = Array.from(this.positions.values()).map(pos => ({
509
+ symbol: pos.symbol,
510
+ exchange: pos.exchange,
511
+ quantity: pos.quantity,
512
+ averagePrice: pos.averagePrice,
513
+ unrealizedPnl: pos.openPnl,
514
+ realizedPnl: pos.closedPnl,
515
+ side: pos.quantity > 0 ? 'LONG' : 'SHORT',
516
+ }));
517
+
518
+ return { success: true, positions };
519
+ }
520
+
521
+ /**
522
+ * Get orders via ORDER_PLANT
523
+ * Uses RequestShowOrders (template 320) -> ResponseShowOrders (template 321)
524
+ */
525
+ async getOrders() {
526
+ if (!this.orderConn || !this.loginInfo) {
527
+ return { success: true, orders: [] };
528
+ }
529
+
530
+ return new Promise((resolve) => {
531
+ const orders = [];
532
+ const timeout = setTimeout(() => {
533
+ resolve({ success: true, orders });
534
+ }, 3000);
535
+
536
+ // Listen for order notifications
537
+ const orderHandler = (notification) => {
538
+ // RithmicOrderNotification contains order details
539
+ if (notification.orderId) {
540
+ orders.push({
541
+ orderId: notification.orderId,
542
+ symbol: notification.symbol,
543
+ exchange: notification.exchange,
544
+ side: notification.transactionType === 1 ? 'BUY' : 'SELL',
545
+ quantity: notification.quantity,
546
+ filledQuantity: notification.filledQuantity || 0,
547
+ price: notification.price,
548
+ orderType: notification.orderType,
549
+ status: notification.status,
550
+ });
551
+ }
552
+ };
553
+
554
+ this.once('ordersReceived', () => {
555
+ clearTimeout(timeout);
556
+ this.removeListener('orderNotification', orderHandler);
557
+ resolve({ success: true, orders });
558
+ });
559
+
560
+ this.on('orderNotification', orderHandler);
561
+
562
+ // Send request
563
+ try {
564
+ for (const acc of this.accounts) {
565
+ this.orderConn.send('RequestShowOrders', {
566
+ templateId: REQ.SHOW_ORDERS,
567
+ userMsg: ['HQX'],
568
+ fcmId: acc.fcmId || this.loginInfo.fcmId,
569
+ ibId: acc.ibId || this.loginInfo.ibId,
570
+ accountId: acc.accountId,
571
+ });
572
+ }
573
+ } catch (e) {
574
+ clearTimeout(timeout);
575
+ resolve({ success: false, error: e.message, orders: [] });
576
+ }
577
+ });
578
+ }
579
+
580
+ /**
581
+ * Get order history
582
+ * Uses RequestShowOrderHistorySummary (template 324)
583
+ */
584
+ async getOrderHistory(date) {
585
+ if (!this.orderConn || !this.loginInfo) {
586
+ return { success: true, orders: [] };
587
+ }
588
+
589
+ // Default to today
590
+ const dateStr = date || new Date().toISOString().slice(0, 10).replace(/-/g, '');
591
+
592
+ return new Promise((resolve) => {
593
+ const orders = [];
594
+ const timeout = setTimeout(() => {
595
+ resolve({ success: true, orders });
596
+ }, 3000);
597
+
598
+ try {
599
+ for (const acc of this.accounts) {
600
+ this.orderConn.send('RequestShowOrderHistorySummary', {
601
+ templateId: REQ.SHOW_ORDER_HISTORY,
602
+ userMsg: ['HQX'],
603
+ fcmId: acc.fcmId || this.loginInfo.fcmId,
604
+ ibId: acc.ibId || this.loginInfo.ibId,
605
+ accountId: acc.accountId,
606
+ date: dateStr,
607
+ });
608
+ }
609
+
610
+ // Wait for response
611
+ setTimeout(() => {
612
+ clearTimeout(timeout);
613
+ resolve({ success: true, orders });
614
+ }, 2000);
615
+ } catch (e) {
616
+ clearTimeout(timeout);
617
+ resolve({ success: false, error: e.message, orders: [] });
618
+ }
619
+ });
620
+ }
621
+
420
622
  /**
421
623
  * Check market hours (same as ProjectX)
422
624
  */
@@ -477,8 +679,11 @@ class RithmicService extends EventEmitter {
477
679
  }
478
680
  this.accounts = [];
479
681
  this.accountPnL.clear();
682
+ this.positions.clear();
683
+ this.orders = [];
480
684
  this.loginInfo = null;
481
685
  this.user = null;
686
+ this.credentials = null;
482
687
  }
483
688
  }
484
689
 
@@ -28,6 +28,36 @@ const PNL_FIELDS = {
28
28
  USECS: 150101,
29
29
  };
30
30
 
31
+ // Instrument PnL Position Update field IDs
32
+ const INSTRUMENT_PNL_FIELDS = {
33
+ TEMPLATE_ID: 154467,
34
+ IS_SNAPSHOT: 110121,
35
+ FCM_ID: 154013,
36
+ IB_ID: 154014,
37
+ ACCOUNT_ID: 154008,
38
+ SYMBOL: 110100,
39
+ EXCHANGE: 110101,
40
+ PRODUCT_CODE: 100749,
41
+ INSTRUMENT_TYPE: 110116,
42
+ FILL_BUY_QTY: 154041,
43
+ FILL_SELL_QTY: 154042,
44
+ ORDER_BUY_QTY: 154037,
45
+ ORDER_SELL_QTY: 154038,
46
+ BUY_QTY: 154260,
47
+ SELL_QTY: 154261,
48
+ AVG_OPEN_FILL_PRICE: 154434,
49
+ DAY_OPEN_PNL: 157954,
50
+ DAY_CLOSED_PNL: 157955,
51
+ DAY_PNL: 157956,
52
+ OPEN_POSITION_PNL: 156961,
53
+ OPEN_POSITION_QUANTITY: 156962,
54
+ CLOSED_POSITION_PNL: 156963,
55
+ CLOSED_POSITION_QUANTITY: 156964,
56
+ NET_QUANTITY: 156967,
57
+ SSBOE: 150100,
58
+ USECS: 150101,
59
+ };
60
+
31
61
  /**
32
62
  * Read a varint from buffer
33
63
  */
@@ -158,6 +188,106 @@ function decodeAccountPnL(buffer) {
158
188
  return result;
159
189
  }
160
190
 
191
+ /**
192
+ * Manually decode InstrumentPnLPositionUpdate from raw bytes
193
+ */
194
+ function decodeInstrumentPnL(buffer) {
195
+ const result = {};
196
+ let offset = 0;
197
+
198
+ while (offset < buffer.length) {
199
+ try {
200
+ const [tag, tagOffset] = readVarint(buffer, offset);
201
+ const wireType = tag & 0x7;
202
+ const fieldNumber = tag >>> 3;
203
+ offset = tagOffset;
204
+
205
+ switch (fieldNumber) {
206
+ case INSTRUMENT_PNL_FIELDS.TEMPLATE_ID:
207
+ [result.templateId, offset] = readVarint(buffer, offset);
208
+ break;
209
+ case INSTRUMENT_PNL_FIELDS.IS_SNAPSHOT:
210
+ const [isSnap, snapOffset] = readVarint(buffer, offset);
211
+ result.isSnapshot = isSnap !== 0;
212
+ offset = snapOffset;
213
+ break;
214
+ case INSTRUMENT_PNL_FIELDS.FCM_ID:
215
+ [result.fcmId, offset] = readLengthDelimited(buffer, offset);
216
+ break;
217
+ case INSTRUMENT_PNL_FIELDS.IB_ID:
218
+ [result.ibId, offset] = readLengthDelimited(buffer, offset);
219
+ break;
220
+ case INSTRUMENT_PNL_FIELDS.ACCOUNT_ID:
221
+ [result.accountId, offset] = readLengthDelimited(buffer, offset);
222
+ break;
223
+ case INSTRUMENT_PNL_FIELDS.SYMBOL:
224
+ [result.symbol, offset] = readLengthDelimited(buffer, offset);
225
+ break;
226
+ case INSTRUMENT_PNL_FIELDS.EXCHANGE:
227
+ [result.exchange, offset] = readLengthDelimited(buffer, offset);
228
+ break;
229
+ case INSTRUMENT_PNL_FIELDS.PRODUCT_CODE:
230
+ [result.productCode, offset] = readLengthDelimited(buffer, offset);
231
+ break;
232
+ case INSTRUMENT_PNL_FIELDS.BUY_QTY:
233
+ [result.buyQty, offset] = readVarint(buffer, offset);
234
+ break;
235
+ case INSTRUMENT_PNL_FIELDS.SELL_QTY:
236
+ [result.sellQty, offset] = readVarint(buffer, offset);
237
+ break;
238
+ case INSTRUMENT_PNL_FIELDS.FILL_BUY_QTY:
239
+ [result.fillBuyQty, offset] = readVarint(buffer, offset);
240
+ break;
241
+ case INSTRUMENT_PNL_FIELDS.FILL_SELL_QTY:
242
+ [result.fillSellQty, offset] = readVarint(buffer, offset);
243
+ break;
244
+ case INSTRUMENT_PNL_FIELDS.NET_QUANTITY:
245
+ [result.netQuantity, offset] = readVarint(buffer, offset);
246
+ break;
247
+ case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_QUANTITY:
248
+ [result.openPositionQuantity, offset] = readVarint(buffer, offset);
249
+ break;
250
+ case INSTRUMENT_PNL_FIELDS.AVG_OPEN_FILL_PRICE:
251
+ // Double is 64-bit fixed
252
+ if (wireType === 1) {
253
+ result.avgOpenFillPrice = buffer.readDoubleLE(offset);
254
+ offset += 8;
255
+ } else {
256
+ offset = skipField(buffer, offset, wireType);
257
+ }
258
+ break;
259
+ case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_PNL:
260
+ [result.openPositionPnl, offset] = readLengthDelimited(buffer, offset);
261
+ break;
262
+ case INSTRUMENT_PNL_FIELDS.CLOSED_POSITION_PNL:
263
+ [result.closedPositionPnl, offset] = readLengthDelimited(buffer, offset);
264
+ break;
265
+ case INSTRUMENT_PNL_FIELDS.DAY_PNL:
266
+ [result.dayPnl, offset] = readLengthDelimited(buffer, offset);
267
+ break;
268
+ case INSTRUMENT_PNL_FIELDS.DAY_OPEN_PNL:
269
+ [result.dayOpenPnl, offset] = readLengthDelimited(buffer, offset);
270
+ break;
271
+ case INSTRUMENT_PNL_FIELDS.DAY_CLOSED_PNL:
272
+ [result.dayClosedPnl, offset] = readLengthDelimited(buffer, offset);
273
+ break;
274
+ case INSTRUMENT_PNL_FIELDS.SSBOE:
275
+ [result.ssboe, offset] = readVarint(buffer, offset);
276
+ break;
277
+ case INSTRUMENT_PNL_FIELDS.USECS:
278
+ [result.usecs, offset] = readVarint(buffer, offset);
279
+ break;
280
+ default:
281
+ offset = skipField(buffer, offset, wireType);
282
+ }
283
+ } catch (error) {
284
+ break;
285
+ }
286
+ }
287
+
288
+ return result;
289
+ }
290
+
161
291
  /**
162
292
  * Protobuf Handler class
163
293
  */
@@ -253,6 +383,7 @@ const proto = new ProtobufHandler();
253
383
  module.exports = {
254
384
  proto,
255
385
  decodeAccountPnL,
386
+ decodeInstrumentPnL,
256
387
  readVarint,
257
388
  readLengthDelimited,
258
389
  skipField,