hedgequantx 2.7.15 → 2.7.17

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/src/lib/n/r3.js DELETED
@@ -1,631 +0,0 @@
1
- /**
2
- * Rithmic Trading API
3
- * Handles order execution via ORDER_PLANT
4
- * Supports: Market, Limit, Stop Market, Stop Limit orders
5
- */
6
-
7
- const EventEmitter = require('events');
8
- const { RithmicConnection } = require('./r2');
9
- const { TEMPLATE_IDS, INFRA_TYPES, ORDER_TYPES, NOTIFY_TYPES, RITHMIC_NOTIFY_TYPES } = require('./r7');
10
-
11
- class RithmicTrading extends EventEmitter {
12
- constructor(options = {}) {
13
- super();
14
-
15
- this.connection = null;
16
- this.config = null;
17
-
18
- // Login info from ORDER_PLANT
19
- this.fcmId = null;
20
- this.ibId = null;
21
-
22
- // Trade routes cache
23
- this.tradeRoutes = new Map(); // exchange -> tradeRoute
24
-
25
- // Accounts cache
26
- this.accounts = new Map(); // accountId -> account info
27
-
28
- // Orders tracking
29
- this.pendingOrders = new Map(); // basketId -> { resolve, reject, order }
30
- this.openOrders = new Map(); // basketId -> order info
31
- this.orderHistory = [];
32
-
33
- // Positions tracking
34
- this.positions = new Map(); // symbol -> position
35
-
36
- // Order ID counter
37
- this._orderIdCounter = 0;
38
-
39
- // Options
40
- this.debug = options.debug || false;
41
- }
42
-
43
- /**
44
- * Connect to Rithmic ORDER_PLANT
45
- * @param {Object} credentials - { userId, password, systemName, gateway }
46
- */
47
- async connect(credentials) {
48
- this.config = credentials;
49
-
50
- this.connection = new RithmicConnection({
51
- debug: this.debug,
52
- maxReconnectAttempts: 5
53
- });
54
-
55
- // Forward connection events
56
- this.connection.on('error', (error) => {
57
- this.emit('error', error);
58
- });
59
-
60
- this.connection.on('disconnected', (data) => {
61
- this.emit('disconnected', data);
62
- });
63
-
64
- // Handle order notifications
65
- // Handle both notification types - RithmicOrderNotification (351) and ExchangeOrderNotification (352)
66
- this.connection.on('RithmicOrderNotification', (notif) => this._handleOrderNotification(notif));
67
- this.connection.on('ExchangeOrderNotification', (notif) => this._handleOrderNotification(notif));
68
- this.connection.on('ResponseNewOrder', (resp) => this._handleOrderResponse(resp));
69
- this.connection.on('ResponseTradeRoutes', (resp) => this._handleTradeRoutes(resp));
70
- this.connection.on('ResponseAccountList', (resp) => this._handleAccountList(resp));
71
-
72
- // Connect and login
73
- const loginData = await this.connection.connect({
74
- userId: credentials.userId,
75
- password: credentials.password,
76
- systemName: credentials.systemName,
77
- gateway: credentials.gateway,
78
- infraType: INFRA_TYPES.ORDER_PLANT
79
- });
80
-
81
- this.fcmId = loginData.fcmId;
82
- this.ibId = loginData.ibId;
83
-
84
- // Load trade routes and accounts BEFORE signaling ready
85
- await this._loadTradeRoutes();
86
- await this._loadAccounts();
87
- await this._subscribeOrderUpdates();
88
-
89
- this.emit('connected', loginData);
90
-
91
- return true;
92
- }
93
-
94
- /**
95
- * Load trade routes
96
- */
97
- async _loadTradeRoutes() {
98
- try {
99
- this.connection.send('RequestTradeRoutes', {
100
- templateId: TEMPLATE_IDS.REQUEST_TRADE_ROUTES,
101
- subscribeForUpdates: false
102
- });
103
-
104
- // Wait for response
105
- await new Promise(resolve => setTimeout(resolve, 1000));
106
- } catch (error) {
107
- // Silently fail - trade routes not critical
108
- }
109
- }
110
-
111
- /**
112
- * Load accounts
113
- */
114
- async _loadAccounts() {
115
- try {
116
- this.connection.send('RequestAccountList', {
117
- templateId: TEMPLATE_IDS.REQUEST_ACCOUNT_LIST,
118
- fcmId: this.fcmId,
119
- ibId: this.ibId,
120
- userType: 3 // USER_TYPE_TRADER
121
- });
122
-
123
- // Wait for response
124
- await new Promise(resolve => setTimeout(resolve, 1000));
125
- } catch (error) {
126
- // Silently fail - accounts loaded via events
127
- }
128
- }
129
-
130
- /**
131
- * Subscribe to order updates
132
- */
133
- async _subscribeOrderUpdates() {
134
- try {
135
- // Subscribe for all accounts
136
- for (const accountId of this.accounts.keys()) {
137
- this.connection.send('RequestSubscribeForOrderUpdates', {
138
- templateId: TEMPLATE_IDS.REQUEST_SUBSCRIBE_FOR_ORDER_UPDATES,
139
- fcmId: this.fcmId,
140
- ibId: this.ibId,
141
- accountId: accountId
142
- });
143
- }
144
-
145
-
146
-
147
- } catch (error) {
148
- console.error('[RITHMIC:Trading] Failed to subscribe to order updates:', error.message);
149
- }
150
- }
151
-
152
- /**
153
- * Handle trade routes response
154
- */
155
- _handleTradeRoutes(response) {
156
- if (response.exchange && response.tradeRoute) {
157
- this.tradeRoutes.set(response.exchange, {
158
- tradeRoute: response.tradeRoute,
159
- status: response.status,
160
- isDefault: response.isDefault
161
- });
162
- }
163
- }
164
-
165
- /**
166
- * Handle account list response
167
- */
168
- _handleAccountList(response) {
169
- if (response.accountId) {
170
- this.accounts.set(response.accountId, {
171
- accountId: response.accountId,
172
- accountName: response.accountName,
173
- currency: response.accountCurrency,
174
- fcmId: response.fcmId,
175
- ibId: response.ibId
176
- });
177
- }
178
- }
179
-
180
- /**
181
- * Handle order response (acknowledgment)
182
- */
183
- _handleOrderResponse(response) {
184
- // Get rpCode from either rpCode or rqHandlerRpCode (Rithmic sends both)
185
- const rpCodeArr = response.rpCode || response.rqHandlerRpCode;
186
- const rpCode = Array.isArray(rpCodeArr) ? rpCodeArr[0] : rpCodeArr;
187
- const userMsgs = response.userMsg || [];
188
-
189
- // Only process responses with success code or error code
190
- // Skip if we don't have any response code yet
191
- if (!rpCode && rpCode !== '0') {
192
- return;
193
- }
194
-
195
- // Find pending order by userMsg
196
- for (const [basketId, pending] of this.pendingOrders) {
197
- // Check if any userMsg matches the basketId
198
- const matches = userMsgs.some(msg => msg === basketId || msg.includes(basketId) || basketId.includes(msg));
199
-
200
- if (matches) {
201
- if (rpCode === '0') {
202
- // Success! Store the Rithmic basket ID for tracking
203
- const rithmicBasketId = response.basketId || basketId;
204
- pending.resolve({ success: true, basketId: rithmicBasketId, orderId: rithmicBasketId, response });
205
- } else {
206
- // Error - get error text from rpCode array (second element often contains message)
207
- const errorText = Array.isArray(rpCodeArr) && rpCodeArr.length > 1
208
- ? rpCodeArr.slice(1).join(' ')
209
- : (response.textMsg || response.text || `Error code: ${rpCode}`);
210
- console.log(`[RITHMIC] ORDER REJECTED: ${errorText}`);
211
- pending.reject(new Error(errorText));
212
- }
213
-
214
- this.pendingOrders.delete(basketId);
215
- return;
216
- }
217
- }
218
- }
219
-
220
- /**
221
- * Handle order notification (fill, cancel, etc.)
222
- * Handles both RithmicOrderNotification (351) and ExchangeOrderNotification (352)
223
- * Uses deduplication to avoid double-emitting fills
224
- */
225
- _handleOrderNotification(notif) {
226
- const notifyType = notif.notifyType;
227
- const status = notif.status || '';
228
- const symbol = notif.symbol;
229
- const basketId = notif.basketId;
230
-
231
- // Deduplicate fills - use basketId + totalFillSize as unique key
232
- const fillKey = `${basketId}-${notif.totalFillSize || 0}-${notif.fillPrice || notif.avgFillPrice || 0}`;
233
- if (!this._processedFills) this._processedFills = new Set();
234
-
235
- // Skip if already processed (within 2 seconds window)
236
- if (this._processedFills.has(fillKey)) {
237
- return;
238
- }
239
- this._processedFills.add(fillKey);
240
-
241
- // Cleanup old fills after 5 seconds
242
- setTimeout(() => this._processedFills.delete(fillKey), 5000);
243
-
244
- // Determine event type from notify_type and status
245
- // RithmicOrderNotification uses: COMPLETE=15, OPEN=13, etc.
246
- // ExchangeOrderNotification uses: FILL=5, STATUS=1, etc.
247
- let eventType = 'status';
248
-
249
- // Check by numeric notifyType first (most reliable)
250
- if (notifyType === RITHMIC_NOTIFY_TYPES.COMPLETE || notifyType === NOTIFY_TYPES.FILL) {
251
- eventType = 'fill';
252
- } else if (notifyType === RITHMIC_NOTIFY_TYPES.CANCELLATION_FAILED || notifyType === NOTIFY_TYPES.NOT_CANCELLED) {
253
- eventType = 'cancel_failed';
254
- } else if (notifyType === RITHMIC_NOTIFY_TYPES.MODIFICATION_FAILED) {
255
- eventType = 'modify_failed';
256
- } else if (status.includes('cancel') || status === 'cancelled') {
257
- eventType = 'cancel';
258
- } else if (status.includes('reject') || notifyType === NOTIFY_TYPES.REJECT) {
259
- eventType = 'reject';
260
- } else if (notifyType === RITHMIC_NOTIFY_TYPES.OPEN) {
261
- eventType = 'open';
262
- } else if (status === 'complete') {
263
- // Fallback for status string
264
- eventType = 'fill';
265
- }
266
-
267
- // Debug logging for significant events
268
- if (this.debug && (eventType === 'fill' || eventType === 'reject' || eventType === 'cancel' || eventType === 'open')) {
269
- console.log(`[RITHMIC] ${eventType.toUpperCase()}: ${symbol} type=${notifyType} status="${status}" fill=${notif.totalFillSize || 0}`);
270
- }
271
-
272
- const orderInfo = {
273
- type: eventType,
274
- basketId: basketId,
275
- symbol: symbol,
276
- exchange: notif.exchange,
277
- accountId: notif.accountId,
278
- side: notif.transactionType === 1 ? 'buy' : 'sell',
279
- quantity: notif.quantity,
280
- price: notif.price,
281
- triggerPrice: notif.triggerPrice,
282
- priceType: notif.priceType,
283
- status: notif.status,
284
-
285
- // Fill info
286
- fillPrice: notif.fillPrice,
287
- fillSize: notif.fillSize,
288
- avgFillPrice: notif.avgFillPrice,
289
- totalFillSize: notif.totalFillSize,
290
- totalUnfilledSize: notif.totalUnfilledSize,
291
-
292
- // Timestamps
293
- confirmedTime: notif.confirmedTime,
294
- fillTime: notif.fillTime,
295
-
296
- // Messages
297
- text: notif.text,
298
- reportText: notif.reportText
299
- };
300
-
301
- // Log based on event type
302
- if (eventType === 'fill') {
303
- // Use fillSize/fillPrice if available, otherwise use totalFillSize/avgFillPrice
304
- const fillSize = notif.fillSize || notif.totalFillSize || 0;
305
- const fillPrice = notif.fillPrice || notif.avgFillPrice || 0;
306
- const sideStr = notif.transactionType === 'BUY' || notif.transactionType === 1 ? 'BUY' : 'SELL';
307
-
308
- // Update orderInfo with resolved values
309
- orderInfo.fillSize = fillSize;
310
- orderInfo.fillPrice = fillPrice;
311
-
312
- this.emit('fill', orderInfo);
313
-
314
- // Update position
315
- this._updatePosition(symbol, notif);
316
-
317
- } else if (eventType === 'reject') {
318
- console.log(`[RITHMIC] REJECT: ${symbol} - ${notif.text || 'Unknown reason'}`);
319
- this.emit('reject', orderInfo);
320
-
321
- // Reject pending order
322
- const pending = this.pendingOrders.get(basketId);
323
- if (pending) {
324
- pending.reject(new Error(notif.text || 'Order rejected'));
325
- this.pendingOrders.delete(basketId);
326
- }
327
-
328
- } else if (eventType === 'cancel') {
329
- this.emit('cancel', orderInfo);
330
- this.openOrders.delete(basketId);
331
- }
332
-
333
- // Emit generic order event
334
- this.emit('order', orderInfo);
335
-
336
- // Store order info
337
- if (eventType !== 'cancel' && eventType !== 'reject') {
338
- this.openOrders.set(basketId, orderInfo);
339
- }
340
- }
341
-
342
- /**
343
- * Update position based on fill
344
- */
345
- _updatePosition(symbol, notif) {
346
- const current = this.positions.get(symbol) || { symbol, netQty: 0, avgPrice: 0 };
347
-
348
- // Use fillSize/fillPrice if available, otherwise use totalFillSize/avgFillPrice
349
- const fillQty = notif.fillSize || notif.totalFillSize || 0;
350
- const fillPrice = notif.fillPrice || notif.avgFillPrice || 0;
351
- // Handle both numeric and string transactionType
352
- const isBuy = notif.transactionType === 1 || notif.transactionType === 'BUY';
353
- const side = isBuy ? 1 : -1; // 1 = buy, -1 = sell
354
-
355
- const newQty = current.netQty + (fillQty * side);
356
-
357
- if (Math.abs(newQty) > Math.abs(current.netQty)) {
358
- // Adding to position
359
- const totalCost = (current.netQty * current.avgPrice) + (fillQty * fillPrice * side);
360
- current.avgPrice = newQty !== 0 ? totalCost / newQty : 0;
361
- }
362
-
363
- current.netQty = newQty;
364
- current.lastFillPrice = fillPrice;
365
- current.lastFillTime = Date.now();
366
-
367
- this.positions.set(symbol, current);
368
- this.emit('position', current);
369
- }
370
-
371
- /**
372
- * Generate unique basket ID
373
- */
374
- _generateBasketId() {
375
- return `X_${Date.now()}_${++this._orderIdCounter}`;
376
- }
377
-
378
- /**
379
- * Get trade route for exchange
380
- */
381
- _getTradeRoute(exchange) {
382
- const route = this.tradeRoutes.get(exchange);
383
- return route ? route.tradeRoute : 'DEFAULT';
384
- }
385
-
386
- /**
387
- * Place a MARKET order
388
- */
389
- async placeMarketOrder(accountId, symbol, exchange, side, quantity) {
390
- return this._placeOrder({
391
- accountId,
392
- symbol,
393
- exchange,
394
- side,
395
- quantity,
396
- priceType: ORDER_TYPES.PRICE_TYPE.MARKET
397
- });
398
- }
399
-
400
- /**
401
- * Place a LIMIT order
402
- */
403
- async placeLimitOrder(accountId, symbol, exchange, side, quantity, price) {
404
- return this._placeOrder({
405
- accountId,
406
- symbol,
407
- exchange,
408
- side,
409
- quantity,
410
- price,
411
- priceType: ORDER_TYPES.PRICE_TYPE.LIMIT
412
- });
413
- }
414
-
415
- /**
416
- * Place a STOP MARKET order
417
- */
418
- async placeStopMarketOrder(accountId, symbol, exchange, side, quantity, stopPrice) {
419
- return this._placeOrder({
420
- accountId,
421
- symbol,
422
- exchange,
423
- side,
424
- quantity,
425
- triggerPrice: stopPrice,
426
- priceType: ORDER_TYPES.PRICE_TYPE.STOP_MARKET
427
- });
428
- }
429
-
430
- /**
431
- * Place a STOP LIMIT order
432
- */
433
- async placeStopLimitOrder(accountId, symbol, exchange, side, quantity, stopPrice, limitPrice) {
434
- return this._placeOrder({
435
- accountId,
436
- symbol,
437
- exchange,
438
- side,
439
- quantity,
440
- price: limitPrice,
441
- triggerPrice: stopPrice,
442
- priceType: ORDER_TYPES.PRICE_TYPE.STOP_LIMIT
443
- });
444
- }
445
-
446
- /**
447
- * Internal order placement
448
- */
449
- async _placeOrder(params) {
450
- if (!this.connection || !this.connection.isReady) {
451
- throw new Error('Not connected');
452
- }
453
-
454
- const {
455
- accountId,
456
- symbol,
457
- exchange = 'CME',
458
- side,
459
- quantity,
460
- price,
461
- triggerPrice,
462
- priceType = ORDER_TYPES.PRICE_TYPE.MARKET,
463
- duration = ORDER_TYPES.DURATION.DAY
464
- } = params;
465
-
466
- const basketId = this._generateBasketId();
467
- const transactionType = side.toLowerCase() === 'buy'
468
- ? ORDER_TYPES.TRANSACTION_TYPE.BUY
469
- : ORDER_TYPES.TRANSACTION_TYPE.SELL;
470
-
471
- const tradeRoute = this._getTradeRoute(exchange);
472
-
473
- const orderRequest = {
474
- templateId: TEMPLATE_IDS.REQUEST_NEW_ORDER,
475
- userMsg: [basketId],
476
- fcmId: this.fcmId,
477
- ibId: this.ibId,
478
- accountId: accountId.toString(),
479
- symbol: symbol,
480
- exchange: exchange,
481
- quantity: quantity,
482
- transactionType: transactionType,
483
- duration: duration,
484
- priceType: priceType,
485
- tradeRoute: tradeRoute,
486
- manualOrAuto: ORDER_TYPES.ORDER_PLACEMENT.AUTO
487
- };
488
-
489
- // Add price for limit orders
490
- if (price && (priceType === ORDER_TYPES.PRICE_TYPE.LIMIT || priceType === ORDER_TYPES.PRICE_TYPE.STOP_LIMIT)) {
491
- orderRequest.price = price;
492
- }
493
-
494
- // Add trigger price for stop orders
495
- if (triggerPrice && (priceType === ORDER_TYPES.PRICE_TYPE.STOP_MARKET || priceType === ORDER_TYPES.PRICE_TYPE.STOP_LIMIT)) {
496
- orderRequest.triggerPrice = triggerPrice;
497
- }
498
-
499
- return new Promise((resolve, reject) => {
500
- this.pendingOrders.set(basketId, { resolve, reject, order: orderRequest });
501
-
502
- // Timeout
503
- setTimeout(() => {
504
- if (this.pendingOrders.has(basketId)) {
505
- this.pendingOrders.delete(basketId);
506
- reject(new Error('Order timeout'));
507
- }
508
- }, 15000);
509
-
510
- try {
511
- this.connection.send('RequestNewOrder', orderRequest);
512
- } catch (error) {
513
- this.pendingOrders.delete(basketId);
514
- reject(error);
515
- }
516
- });
517
- }
518
-
519
- /**
520
- * Cancel an order
521
- */
522
- async cancelOrder(basketId) {
523
- if (!this.connection || !this.connection.isReady) {
524
- throw new Error('Not connected');
525
- }
526
-
527
- const order = this.openOrders.get(basketId);
528
- if (!order) {
529
- throw new Error('Order not found');
530
- }
531
-
532
- this.connection.send('RequestCancelOrder', {
533
- templateId: TEMPLATE_IDS.REQUEST_CANCEL_ORDER,
534
- userMsg: [basketId],
535
- fcmId: this.fcmId,
536
- ibId: this.ibId,
537
- accountId: order.accountId,
538
- basketId: basketId
539
- });
540
-
541
- return { success: true, basketId };
542
- }
543
-
544
- /**
545
- * Cancel all orders for an account
546
- */
547
- async cancelAllOrders(accountId) {
548
- if (!this.connection || !this.connection.isReady) {
549
- throw new Error('Not connected');
550
- }
551
-
552
- this.connection.send('RequestCancelAllOrders', {
553
- templateId: TEMPLATE_IDS.REQUEST_CANCEL_ALL_ORDERS,
554
- fcmId: this.fcmId,
555
- ibId: this.ibId,
556
- accountId: accountId.toString()
557
- });
558
-
559
- return { success: true };
560
- }
561
-
562
- /**
563
- * Close a position (flatten)
564
- */
565
- async closePosition(accountId, symbol, exchange = 'CME') {
566
- const position = this.positions.get(symbol);
567
-
568
- if (!position || position.netQty === 0) {
569
- return { success: true, message: 'No position' };
570
- }
571
-
572
- // Place opposite order to flatten
573
- const side = position.netQty > 0 ? 'sell' : 'buy';
574
- const quantity = Math.abs(position.netQty);
575
-
576
- return this.placeMarketOrder(accountId, symbol, exchange, side, quantity);
577
- }
578
-
579
- /**
580
- * Get position for symbol
581
- */
582
- getPosition(symbol) {
583
- return this.positions.get(symbol) || null;
584
- }
585
-
586
- /**
587
- * Get all positions
588
- */
589
- getPositions() {
590
- return Array.from(this.positions.values());
591
- }
592
-
593
- /**
594
- * Get open orders
595
- */
596
- getOpenOrders() {
597
- return Array.from(this.openOrders.values());
598
- }
599
-
600
- /**
601
- * Get accounts
602
- */
603
- getAccounts() {
604
- return Array.from(this.accounts.values());
605
- }
606
-
607
- /**
608
- * Check if connected
609
- */
610
- get isConnected() {
611
- return this.connection && this.connection.isReady;
612
- }
613
-
614
- /**
615
- * Disconnect
616
- */
617
- async disconnect() {
618
- if (this.connection) {
619
- await this.connection.disconnect();
620
- this.connection = null;
621
- }
622
-
623
- this.pendingOrders.clear();
624
- this.openOrders.clear();
625
- this.positions.clear();
626
- this.tradeRoutes.clear();
627
- this.accounts.clear();
628
- }
629
- }
630
-
631
- module.exports = { RithmicTrading };