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/package.json +1 -1
- package/src/app.js +10 -10
- package/src/lib/data.js +245 -471
- package/src/lib/m/s1-models.js +173 -0
- package/src/lib/m/s1.js +354 -735
- package/src/menus/dashboard.js +8 -5
- package/src/lib/api.js +0 -198
- package/src/lib/api2.js +0 -353
- package/src/lib/core.js +0 -539
- package/src/lib/core2.js +0 -341
- package/src/lib/data2.js +0 -492
- package/src/lib/decoder.js +0 -599
- package/src/lib/m/s2.js +0 -34
- package/src/lib/n/r1.js +0 -454
- package/src/lib/n/r2.js +0 -514
- package/src/lib/n/r3.js +0 -631
- package/src/lib/n/r4.js +0 -401
- package/src/lib/n/r5.js +0 -335
- package/src/lib/n/r6.js +0 -425
- package/src/lib/n/r7.js +0 -530
- package/src/lib/o/l1.js +0 -44
- package/src/lib/o/l2.js +0 -427
- package/src/lib/python-bridge.js +0 -206
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 };
|