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
|
@@ -61,8 +61,8 @@ const REQ = {
|
|
|
61
61
|
BRACKET_ORDER: 330,
|
|
62
62
|
CANCEL_ALL_ORDERS: 346,
|
|
63
63
|
EXIT_POSITION: 3504,
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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,
|
|
86
|
-
console.log('Note: Could not fetch accounts');
|
|
88
|
+
// Accounts fetch failed, ignore
|
|
87
89
|
}
|
|
88
|
-
|
|
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
|
|
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,
|