hedgequantx 1.2.41 → 1.2.42

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.42",
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": {
@@ -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,