hedgequantx 2.9.247 → 2.9.249
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
|
@@ -118,6 +118,32 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
118
118
|
}
|
|
119
119
|
ui.addLog('system', 'Connecting to market data...');
|
|
120
120
|
|
|
121
|
+
// Listen for position updates from Rithmic (external closes, manual trades)
|
|
122
|
+
const accId = account.rithmicAccountId || account.accountId;
|
|
123
|
+
service.on('positionUpdate', (pos) => {
|
|
124
|
+
// Match by account and symbol
|
|
125
|
+
const posSymbol = pos.contractId || pos.symbol || '';
|
|
126
|
+
const matchesSymbol = posSymbol.includes(contract.name) || posSymbol.includes(contractId) ||
|
|
127
|
+
posSymbol === symbolCode || contractId.includes(posSymbol);
|
|
128
|
+
const matchesAccount = pos.accountId === accId || pos.accountId === account.accountId;
|
|
129
|
+
|
|
130
|
+
if (matchesSymbol && matchesAccount) {
|
|
131
|
+
const qty = parseInt(pos.quantity) || 0;
|
|
132
|
+
if (!isNaN(qty) && Math.abs(qty) < 1000 && qty !== currentPosition) {
|
|
133
|
+
const oldPos = currentPosition;
|
|
134
|
+
currentPosition = qty;
|
|
135
|
+
if (qty === 0 && oldPos !== 0) {
|
|
136
|
+
ui.addLog('trade', `Position closed externally (was ${oldPos})`);
|
|
137
|
+
sessionLogger.log('POSITION', `External close: ${oldPos} -> 0`);
|
|
138
|
+
pendingOrder = false; // Reset pending order flag
|
|
139
|
+
} else if (qty !== 0 && oldPos === 0) {
|
|
140
|
+
ui.addLog('trade', `Position opened externally: ${qty}`);
|
|
141
|
+
sessionLogger.log('POSITION', `External open: 0 -> ${qty}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
121
147
|
strategy.on('signal', async (signal) => {
|
|
122
148
|
const dir = signal.direction?.toUpperCase() || 'UNKNOWN';
|
|
123
149
|
const signalLog = smartLogs.getSignalLog(dir, symbolCode, (signal.confidence || 0) * 100, strategyName);
|
|
@@ -45,6 +45,9 @@ class DaemonProxyService extends EventEmitter {
|
|
|
45
45
|
|
|
46
46
|
/** @type {Object|null} Credentials for direct mode */
|
|
47
47
|
this.credentials = null;
|
|
48
|
+
|
|
49
|
+
/** @type {Map<string, Object>} Cached P&L data for daemon mode */
|
|
50
|
+
this._pnlCache = new Map();
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -192,7 +195,20 @@ class DaemonProxyService extends EventEmitter {
|
|
|
192
195
|
|
|
193
196
|
client.on('orderUpdate', (data) => this.emit('orderUpdate', data));
|
|
194
197
|
client.on('positionUpdate', (data) => this.emit('positionUpdate', data));
|
|
195
|
-
|
|
198
|
+
|
|
199
|
+
// Cache P&L data for sync getAccountPnL calls
|
|
200
|
+
client.on('pnlUpdate', (data) => {
|
|
201
|
+
if (data && data.accountId) {
|
|
202
|
+
this._pnlCache.set(data.accountId, {
|
|
203
|
+
pnl: data.dayPnl ?? data.pnl ?? null,
|
|
204
|
+
openPnl: data.openPositionPnl ?? data.openPnl ?? null,
|
|
205
|
+
closedPnl: data.closedPositionPnl ?? data.closedPnl ?? null,
|
|
206
|
+
balance: data.accountBalance ?? data.balance ?? null,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
this.emit('pnlUpdate', data);
|
|
210
|
+
});
|
|
211
|
+
|
|
196
212
|
client.on('fill', (data) => this.emit('fill', data));
|
|
197
213
|
client.on('marketData', (data) => this.emit('marketData', data));
|
|
198
214
|
client.on('rithmicDisconnected', (data) => this.emit('disconnected', data));
|
|
@@ -248,8 +264,15 @@ class DaemonProxyService extends EventEmitter {
|
|
|
248
264
|
if (!this._backend) return { pnl: null, openPnl: null, closedPnl: null, balance: null };
|
|
249
265
|
|
|
250
266
|
if (this._mode === 'daemon') {
|
|
251
|
-
//
|
|
252
|
-
|
|
267
|
+
// Return cached P&L from pnlUpdate events
|
|
268
|
+
const cached = this._pnlCache.get(accountId);
|
|
269
|
+
if (cached) return cached;
|
|
270
|
+
|
|
271
|
+
// Try without account ID (some events don't include it)
|
|
272
|
+
if (this._pnlCache.size === 1) {
|
|
273
|
+
return this._pnlCache.values().next().value;
|
|
274
|
+
}
|
|
275
|
+
|
|
253
276
|
return { pnl: null, openPnl: null, closedPnl: null, balance: null };
|
|
254
277
|
}
|
|
255
278
|
return this._backend.getAccountPnL(accountId);
|
|
@@ -215,32 +215,56 @@ const handleShowOrdersResponse = (service, data) => {
|
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
217
|
* Handle new order response (template 313)
|
|
218
|
+
* rpCode[0] = '0' means accepted, anything else = rejected by gateway
|
|
218
219
|
*/
|
|
219
220
|
const handleNewOrderResponse = (service, data) => {
|
|
220
221
|
try {
|
|
221
222
|
const res = proto.decode('ResponseNewOrder', data);
|
|
222
|
-
debug('New order response:', JSON.stringify(res));
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
const isAccepted = res.rpCode?.[0] === '0';
|
|
225
|
+
|
|
226
|
+
// Build rejection reason from rpCode array
|
|
227
|
+
// rpCode is typically ['code', 'message'] or just ['code']
|
|
228
|
+
let rejectReason = null;
|
|
229
|
+
if (!isAccepted) {
|
|
230
|
+
// Try to get the error message from rpCode[1] or rqHandlerRpCode[1]
|
|
231
|
+
const rpMsg = res.rpCode?.slice(1).join(' ') || '';
|
|
232
|
+
const rqMsg = res.rqHandlerRpCode?.slice(1).join(' ') || '';
|
|
233
|
+
rejectReason = rpMsg || rqMsg || null;
|
|
234
|
+
|
|
235
|
+
// If no message, interpret the code
|
|
236
|
+
if (!rejectReason && res.rpCode?.[0]) {
|
|
237
|
+
const codeMap = {
|
|
238
|
+
'1': 'Invalid request',
|
|
239
|
+
'2': 'Invalid account',
|
|
240
|
+
'3': 'Invalid symbol',
|
|
241
|
+
'4': 'Invalid quantity',
|
|
242
|
+
'5': 'Insufficient buying power',
|
|
243
|
+
'6': 'Market closed',
|
|
244
|
+
'7': 'Order rejected by risk system',
|
|
245
|
+
'8': 'Position limit exceeded',
|
|
246
|
+
'9': 'Rate limit exceeded',
|
|
247
|
+
};
|
|
248
|
+
rejectReason = codeMap[res.rpCode[0]] || `Gateway rejected (code: ${res.rpCode[0]})`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('[Rithmic] Gateway rejection:', rejectReason, '| rpCode:', res.rpCode, '| rqHandlerRpCode:', res.rqHandlerRpCode);
|
|
252
|
+
}
|
|
253
|
+
|
|
227
254
|
const order = {
|
|
228
|
-
basketId: res.basketId ||
|
|
229
|
-
accountId: res.accountId,
|
|
255
|
+
basketId: res.basketId || null,
|
|
230
256
|
symbol: res.symbol,
|
|
231
257
|
exchange: res.exchange || 'CME',
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
notifyType: isAccepted ? 1 : 6, // 1=STATUS (accepted), 6=REJECT
|
|
259
|
+
status: isAccepted ? 2 : 6,
|
|
260
|
+
text: rejectReason,
|
|
234
261
|
rpCode: res.rpCode,
|
|
235
|
-
rqHandlerRpCode: res.rqHandlerRpCode, // Also include request handler response code
|
|
236
262
|
userMsg: res.userMsg,
|
|
237
263
|
};
|
|
238
264
|
service.emit('orderNotification', order);
|
|
239
|
-
|
|
240
|
-
// Also emit specific event
|
|
241
265
|
service.emit('newOrderResponse', res);
|
|
242
266
|
} catch (e) {
|
|
243
|
-
|
|
267
|
+
console.error('Error decoding ResponseNewOrder:', e.message);
|
|
244
268
|
}
|
|
245
269
|
};
|
|
246
270
|
|
|
@@ -369,12 +393,50 @@ const handleAccountRmsInfo = (service, data) => {
|
|
|
369
393
|
|
|
370
394
|
/**
|
|
371
395
|
* Handle exchange order notification (fills/trades)
|
|
372
|
-
* NotifyType:
|
|
396
|
+
* NotifyType: 1=STATUS, 2=MODIFY, 3=CANCEL, 4=TRIGGER, 5=FILL, 6=REJECT
|
|
373
397
|
*/
|
|
374
398
|
const handleExchangeNotification = (service, data) => {
|
|
375
399
|
try {
|
|
376
400
|
const res = proto.decode('ExchangeOrderNotification', data);
|
|
377
|
-
debug('Exchange notification:', res.notifyType, res.symbol);
|
|
401
|
+
debug('Exchange notification:', res.notifyType, res.symbol, res.status);
|
|
402
|
+
|
|
403
|
+
// Build rejection text from available fields
|
|
404
|
+
// Priority: text > reportText > remarks > status code interpretation
|
|
405
|
+
let rejectText = res.text || res.reportText || res.remarks || null;
|
|
406
|
+
if (!rejectText && res.notifyType === 6) {
|
|
407
|
+
// Map common Rithmic status codes to human-readable messages
|
|
408
|
+
const statusMap = {
|
|
409
|
+
'1': 'Order pending',
|
|
410
|
+
'2': 'Order working',
|
|
411
|
+
'3': 'Order filled',
|
|
412
|
+
'4': 'Order partially filled',
|
|
413
|
+
'5': 'Order rejected by exchange',
|
|
414
|
+
'6': 'Order cancelled',
|
|
415
|
+
'7': 'Order expired',
|
|
416
|
+
'8': 'Order suspended',
|
|
417
|
+
};
|
|
418
|
+
rejectText = statusMap[res.status] || `Exchange rejected (code: ${res.status || 'unknown'})`;
|
|
419
|
+
console.log('[Rithmic] Exchange rejection:', rejectText, '| status:', res.status, '| symbol:', res.symbol);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Emit orderNotification for placeOrder listener
|
|
423
|
+
// This is critical for order tracking
|
|
424
|
+
const orderNotif = {
|
|
425
|
+
basketId: res.basketId,
|
|
426
|
+
accountId: res.accountId,
|
|
427
|
+
symbol: res.symbol,
|
|
428
|
+
exchange: res.exchange || 'CME',
|
|
429
|
+
notifyType: res.notifyType, // 1=STATUS, 5=FILL, 6=REJECT
|
|
430
|
+
status: res.notifyType === 5 ? 3 : res.notifyType === 6 ? 6 : 2,
|
|
431
|
+
fillPrice: res.fillPrice ? parseFloat(res.fillPrice) : null,
|
|
432
|
+
fillSize: res.fillSize ? parseInt(res.fillSize) : null,
|
|
433
|
+
avgFillPrice: res.avgFillPrice ? parseFloat(res.avgFillPrice) : null,
|
|
434
|
+
totalFillSize: res.totalFillSize ? parseInt(res.totalFillSize) : null,
|
|
435
|
+
confirmedSize: res.confirmedSize ? parseInt(res.confirmedSize) : null,
|
|
436
|
+
text: rejectText,
|
|
437
|
+
userMsg: res.userTag ? [res.userTag] : null,
|
|
438
|
+
};
|
|
439
|
+
service.emit('orderNotification', orderNotif);
|
|
378
440
|
|
|
379
441
|
// notifyType 5 = FILL (trade executed)
|
|
380
442
|
if (res.notifyType === 5 && res.fillPrice && res.fillSize) {
|
|
@@ -125,72 +125,83 @@ const placeOrder = async (service, orderData) => {
|
|
|
125
125
|
}, ORDER_TIMEOUTS.PLACE);
|
|
126
126
|
|
|
127
127
|
const onNotification = (order) => {
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
order.symbol === orderData.symbol;
|
|
128
|
+
// Match by symbol
|
|
129
|
+
if (order.symbol !== orderData.symbol) return;
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
const notifyType = order.notifyType;
|
|
132
|
+
|
|
133
|
+
// FILL (notifyType 5) - Order executed
|
|
134
|
+
if (notifyType === 5) {
|
|
133
135
|
clearTimeout(timeout);
|
|
134
136
|
service.removeListener('orderNotification', onNotification);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
orderTag,
|
|
167
|
-
rpCode,
|
|
168
|
-
rqHandlerRpCode: rqCode,
|
|
169
|
-
});
|
|
137
|
+
resolve({
|
|
138
|
+
success: true,
|
|
139
|
+
orderId: order.basketId,
|
|
140
|
+
status: 3,
|
|
141
|
+
fillPrice: order.avgFillPrice || order.fillPrice || orderData.price,
|
|
142
|
+
filledQty: order.totalFillSize || order.fillSize || orderData.size,
|
|
143
|
+
orderTag,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// REJECT (notifyType 6) - Order rejected
|
|
149
|
+
if (notifyType === 6) {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
service.removeListener('orderNotification', onNotification);
|
|
152
|
+
resolve({
|
|
153
|
+
success: false,
|
|
154
|
+
error: order.text || 'Order rejected',
|
|
155
|
+
orderId: order.basketId,
|
|
156
|
+
orderTag,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// STATUS (notifyType 1) with basketId - Order accepted by gateway
|
|
162
|
+
if (notifyType === 1 && order.basketId) {
|
|
163
|
+
// For market orders, wait a bit more for fill notification
|
|
164
|
+
// For limit orders, this is success (working)
|
|
165
|
+
if (orderData.type === 2) {
|
|
166
|
+
// Market order - don't resolve yet, wait for fill
|
|
167
|
+
return;
|
|
170
168
|
}
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
service.removeListener('orderNotification', onNotification);
|
|
171
|
+
resolve({
|
|
172
|
+
success: true,
|
|
173
|
+
orderId: order.basketId,
|
|
174
|
+
status: 2,
|
|
175
|
+
fillPrice: orderData.price,
|
|
176
|
+
filledQty: 0,
|
|
177
|
+
orderTag,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
171
180
|
}
|
|
172
181
|
};
|
|
173
182
|
|
|
174
183
|
service.on('orderNotification', onNotification);
|
|
175
184
|
|
|
176
|
-
// HFT: Inline order request construction (no try-catch in hot path)
|
|
177
185
|
const exchange = orderData.exchange || 'CME';
|
|
178
186
|
|
|
179
|
-
// Get trade route
|
|
187
|
+
// CRITICAL: Get trade route - orders WILL BE REJECTED without it
|
|
180
188
|
let tradeRoute = null;
|
|
181
189
|
const routes = service.tradeRoutes;
|
|
182
190
|
if (routes && routes.size > 0) {
|
|
183
191
|
const routeInfo = routes.get(exchange);
|
|
184
|
-
tradeRoute = routeInfo
|
|
185
|
-
routes.values().next().value?.tradeRoute || null;
|
|
192
|
+
tradeRoute = routeInfo?.tradeRoute || routes.values().next().value?.tradeRoute;
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
//
|
|
195
|
+
// FAIL FAST if no trade route
|
|
189
196
|
if (!tradeRoute) {
|
|
190
|
-
|
|
197
|
+
clearTimeout(timeout);
|
|
198
|
+
service.removeListener('orderNotification', onNotification);
|
|
199
|
+
console.log('[Orders] ERROR: No trade route for', exchange);
|
|
200
|
+
resolve({ success: false, error: `No trade route for ${exchange}. Login may be incomplete.`, orderTag });
|
|
201
|
+
return;
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
//
|
|
204
|
+
// Build order request
|
|
194
205
|
ORDER_REQUEST_TEMPLATE.userMsg[0] = orderTag;
|
|
195
206
|
ORDER_REQUEST_TEMPLATE.fcmId = service.loginInfo.fcmId;
|
|
196
207
|
ORDER_REQUEST_TEMPLATE.ibId = service.loginInfo.ibId;
|