hedgequantx 2.9.190 → 2.9.192
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
|
@@ -486,6 +486,35 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
486
486
|
clearInterval(refreshInterval);
|
|
487
487
|
clearInterval(pnlInterval);
|
|
488
488
|
clearInterval(liveLogInterval);
|
|
489
|
+
|
|
490
|
+
// Flatten any open position before stopping
|
|
491
|
+
if (currentPosition !== 0) {
|
|
492
|
+
ui.addLog('system', `Flattening position: ${currentPosition > 0 ? 'LONG' : 'SHORT'} ${Math.abs(currentPosition)}`);
|
|
493
|
+
sessionLogger.log('EXIT', `Flattening position: ${currentPosition}`);
|
|
494
|
+
try {
|
|
495
|
+
const flattenResult = await service.placeOrder({
|
|
496
|
+
accountId: account.rithmicAccountId || account.accountId,
|
|
497
|
+
symbol: symbolCode,
|
|
498
|
+
exchange: contract.exchange || 'CME',
|
|
499
|
+
type: 2, // Market
|
|
500
|
+
side: currentPosition > 0 ? 1 : 0, // Sell if long, Buy if short
|
|
501
|
+
size: Math.abs(currentPosition)
|
|
502
|
+
});
|
|
503
|
+
if (flattenResult.success) {
|
|
504
|
+
ui.addLog('fill_' + (currentPosition > 0 ? 'sell' : 'buy'), `Position flattened @ market`);
|
|
505
|
+
sessionLogger.log('EXIT', `Position flattened successfully`);
|
|
506
|
+
} else {
|
|
507
|
+
ui.addLog('error', `Flatten failed: ${flattenResult.error}`);
|
|
508
|
+
sessionLogger.log('EXIT', `Flatten failed: ${flattenResult.error}`);
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
ui.addLog('error', `Flatten error: ${e.message}`);
|
|
512
|
+
sessionLogger.log('EXIT', `Flatten error: ${e.message}`);
|
|
513
|
+
}
|
|
514
|
+
// Wait for fill
|
|
515
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
516
|
+
}
|
|
517
|
+
|
|
489
518
|
await marketFeed.disconnect();
|
|
490
519
|
if (cleanupKeys) cleanupKeys();
|
|
491
520
|
ui.cleanup();
|
|
@@ -335,6 +335,47 @@ const getPositions = async (service) => {
|
|
|
335
335
|
return { success: true, positions };
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Fetch trade routes from ORDER_PLANT
|
|
340
|
+
* Routes are needed to place orders - Rithmic rejects orders without trade_route
|
|
341
|
+
* @param {RithmicService} service - The Rithmic service instance
|
|
342
|
+
*/
|
|
343
|
+
const fetchTradeRoutes = async (service) => {
|
|
344
|
+
if (!service.orderConn || !service.loginInfo) {
|
|
345
|
+
debug('fetchTradeRoutes: no connection or loginInfo');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Initialize map if needed
|
|
350
|
+
if (!service.tradeRoutes) service.tradeRoutes = new Map();
|
|
351
|
+
|
|
352
|
+
return new Promise((resolve) => {
|
|
353
|
+
const timeout = setTimeout(() => {
|
|
354
|
+
debug('fetchTradeRoutes: timeout, found', service.tradeRoutes.size, 'routes');
|
|
355
|
+
resolve();
|
|
356
|
+
}, 5000);
|
|
357
|
+
|
|
358
|
+
service.once('tradeRoutesComplete', () => {
|
|
359
|
+
debug('fetchTradeRoutes: complete, found', service.tradeRoutes.size, 'routes');
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
resolve();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
debug('fetchTradeRoutes: sending RequestTradeRoutes');
|
|
366
|
+
service.orderConn.send('RequestTradeRoutes', {
|
|
367
|
+
templateId: REQ.TRADE_ROUTES,
|
|
368
|
+
userMsg: ['HQX'],
|
|
369
|
+
subscribeForUpdates: false,
|
|
370
|
+
});
|
|
371
|
+
} catch (e) {
|
|
372
|
+
debug('fetchTradeRoutes: error', e.message);
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
resolve();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
338
379
|
module.exports = {
|
|
339
380
|
hashAccountId,
|
|
340
381
|
fetchAccounts,
|
|
@@ -343,5 +384,6 @@ module.exports = {
|
|
|
343
384
|
getTradingAccounts,
|
|
344
385
|
requestPnLSnapshot,
|
|
345
386
|
subscribePnLUpdates,
|
|
346
|
-
getPositions
|
|
387
|
+
getPositions,
|
|
388
|
+
fetchTradeRoutes
|
|
347
389
|
};
|
|
@@ -133,13 +133,41 @@ const handleAccountList = (service, data) => {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Handle trade routes response
|
|
136
|
+
* Stores trade routes in service.tradeRoutes Map keyed by exchange
|
|
136
137
|
*/
|
|
137
138
|
const handleTradeRoutes = (service, data) => {
|
|
138
139
|
try {
|
|
139
140
|
const res = proto.decode('ResponseTradeRoutes', data);
|
|
141
|
+
debug('Trade routes response:', JSON.stringify(res));
|
|
142
|
+
|
|
143
|
+
// Store trade route if we have exchange and trade_route
|
|
144
|
+
if (res.exchange && res.tradeRoute) {
|
|
145
|
+
const routeInfo = {
|
|
146
|
+
fcmId: res.fcmId,
|
|
147
|
+
ibId: res.ibId,
|
|
148
|
+
exchange: res.exchange,
|
|
149
|
+
tradeRoute: res.tradeRoute,
|
|
150
|
+
status: res.status,
|
|
151
|
+
isDefault: res.isDefault || false,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Use exchange as key, prefer default route
|
|
155
|
+
const existing = service.tradeRoutes.get(res.exchange);
|
|
156
|
+
if (!existing || res.isDefault) {
|
|
157
|
+
service.tradeRoutes.set(res.exchange, routeInfo);
|
|
158
|
+
debug('Stored trade route for', res.exchange, ':', res.tradeRoute, res.isDefault ? '(default)' : '');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Signal completion when rpCode is '0'
|
|
163
|
+
if (res.rpCode?.[0] === '0') {
|
|
164
|
+
debug('Trade routes complete, total:', service.tradeRoutes.size);
|
|
165
|
+
service.emit('tradeRoutesComplete');
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
service.emit('tradeRoutes', res);
|
|
141
169
|
} catch (e) {
|
|
142
|
-
|
|
170
|
+
debug('Error decoding trade routes:', e.message);
|
|
143
171
|
}
|
|
144
172
|
};
|
|
145
173
|
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
requestPnLSnapshot,
|
|
16
16
|
subscribePnLUpdates,
|
|
17
17
|
getPositions,
|
|
18
|
+
fetchTradeRoutes,
|
|
18
19
|
} = require('./accounts');
|
|
19
20
|
const { placeOrder, cancelOrder, getOrders, getOrderHistory, getOrderHistoryDates, getTradeHistoryFull, closePosition } = require('./orders');
|
|
20
21
|
const { fillsToRoundTrips, calculateTradeStats } = require('./trades');
|
|
@@ -78,6 +79,9 @@ class RithmicService extends EventEmitter {
|
|
|
78
79
|
this._contractsCache = null;
|
|
79
80
|
this._contractsCacheTime = 0;
|
|
80
81
|
|
|
82
|
+
// Trade routes cache (keyed by exchange)
|
|
83
|
+
this.tradeRoutes = new Map();
|
|
84
|
+
|
|
81
85
|
// Trades history (captured from ExchangeOrderNotification fills)
|
|
82
86
|
this.trades = [];
|
|
83
87
|
}
|
|
@@ -155,6 +159,14 @@ class RithmicService extends EventEmitter {
|
|
|
155
159
|
log.warn('PnL connection failed', { error: err.message });
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// Fetch trade routes (required for order placement)
|
|
163
|
+
try {
|
|
164
|
+
await fetchTradeRoutes(this);
|
|
165
|
+
log.debug('Trade routes fetched', { count: this.tradeRoutes.size });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
log.warn('Trade routes fetch failed', { error: err.message });
|
|
168
|
+
}
|
|
169
|
+
|
|
158
170
|
// Get trading accounts (uses existing this.accounts, no new API call)
|
|
159
171
|
const result = await getTradingAccounts(this);
|
|
160
172
|
log.info('Login successful', { accounts: result.accounts.length });
|
|
@@ -472,6 +484,7 @@ class RithmicService extends EventEmitter {
|
|
|
472
484
|
this.accounts = [];
|
|
473
485
|
this.accountPnL.clear();
|
|
474
486
|
this.positions.clear();
|
|
487
|
+
this.tradeRoutes.clear();
|
|
475
488
|
this.orders = [];
|
|
476
489
|
this.loginInfo = null;
|
|
477
490
|
this.user = null;
|
|
@@ -75,6 +75,35 @@ const placeOrder = async (service, orderData) => {
|
|
|
75
75
|
service.on('orderNotification', onNotification);
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
+
const exchange = orderData.exchange || 'CME';
|
|
79
|
+
|
|
80
|
+
// Get trade route from cache (fetched during login)
|
|
81
|
+
// Trade route is REQUIRED by Rithmic - orders rejected without it
|
|
82
|
+
let tradeRoute = null;
|
|
83
|
+
if (service.tradeRoutes && service.tradeRoutes.size > 0) {
|
|
84
|
+
const routeInfo = service.tradeRoutes.get(exchange);
|
|
85
|
+
if (routeInfo) {
|
|
86
|
+
tradeRoute = routeInfo.tradeRoute;
|
|
87
|
+
} else {
|
|
88
|
+
// Fallback: use first available route
|
|
89
|
+
const firstRoute = service.tradeRoutes.values().next().value;
|
|
90
|
+
if (firstRoute) {
|
|
91
|
+
tradeRoute = firstRoute.tradeRoute;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (DEBUG) {
|
|
97
|
+
console.log('[ORDER] Trade route for', exchange, ':', tradeRoute);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!tradeRoute) {
|
|
101
|
+
// No trade route available - order will likely fail
|
|
102
|
+
if (DEBUG) {
|
|
103
|
+
console.log('[ORDER] WARNING: No trade route available, order may be rejected');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
const orderRequest = {
|
|
79
108
|
templateId: REQ.NEW_ORDER,
|
|
80
109
|
userMsg: [orderTag],
|
|
@@ -82,12 +111,13 @@ const placeOrder = async (service, orderData) => {
|
|
|
82
111
|
ibId: service.loginInfo.ibId,
|
|
83
112
|
accountId: orderData.accountId,
|
|
84
113
|
symbol: orderData.symbol,
|
|
85
|
-
exchange:
|
|
114
|
+
exchange: exchange,
|
|
86
115
|
quantity: orderData.size,
|
|
87
116
|
transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
|
|
88
117
|
duration: 1, // DAY
|
|
89
118
|
priceType: orderData.type === 2 ? 2 : 1, // 2=Market, 1=Limit
|
|
90
119
|
price: orderData.price || 0,
|
|
120
|
+
tradeRoute: tradeRoute, // REQUIRED by Rithmic
|
|
91
121
|
manualOrAuto: 2, // AUTO
|
|
92
122
|
};
|
|
93
123
|
|